2

I have a model field full_time_equivalent:

full_time_equivalent = models.DecimalField(
    max_digits=5,
    decimal_places=2,
    default=100,
    validators=[
        MinValueValidator(Decimal(0)),
        MaxValueValidator(Decimal(100))
    ]
)

To ensure that the validators fire I have override save with:

def save(self, *args, **kwargs):
    # Run validations
    self.full_clean()
    return super().save(*args, **kwargs)

With the following test:

    project2_membership = ProjectMembership(
        user=self.new_user,
        project=project2,
        is_project_manager=False,
        full_time_equivalent=10.01
    )

When I step into the validation the following value is shown and respective error:

Decimal('10.0099999999999997868371792719699442386627197265625')


django.core.exceptions.ValidationError: 
{'full_time_equivalent': ['Ensure that there are no more than 5 digits in total.']

What am I doing wrong?

tread
  • 10,133
  • 17
  • 95
  • 170
  • had a look at the [django-code](https://github.com/django/django/blob/399a8db33b14a1f707912ac48a185fb0a1204913/django/db/models/base.py)...what happens if you only call `self.clean()`? Just want to figure out where's this issue coming from. – Thomas Schwärzl Oct 19 '17 at 14:44
  • Looks like a floating point issue. If it is, then it is a bug either in the django validator (but it uses ``Decimal`` type which should not have such issues) or in your class getting the ``10.01`` argument. – allo Oct 19 '17 at 14:54
  • I guess you may need to add some rounding. In the python shell, when I type `print(Decimal(10.01))`, I get `10.009999...` like you do. – jcfollower Oct 19 '17 at 15:01
  • I wonder if this will help https://stackoverflow.com/questions/1995615/how-can-i-format-a-decimal-to-always-show-2-decimal-places `Decimal(10.01).quantize(Decimal(10) ** -2))` – jcfollower Oct 19 '17 at 15:09
  • Note that by calling `full_clean()` in `save()`, you'll end up running the validation twice in places that use model forms like the Django admin. – Alasdair Oct 19 '17 at 15:33
  • Ah, perhaps these validations can just be handled on the form? What is your opinion? – tread Oct 19 '17 at 18:45
  • It's a trade off. It's good to avoid the duplicated validation in `save()`. But there's a chance that you might write a view and forget to call `full_clean()` before saving. – Alasdair Oct 19 '17 at 18:55

1 Answers1

6

The decimal value 10.01 can't be expressed exactly as a float. When the value is converted to a decimal, you end up with Decimal('10.0099999999999997868371792719699442386627197265625'), which is very nearly equal to Decimal('10.01'), but fails your max_digits validation.

You can prevent the error by using the string '10.01' or the decimal Decimal('10.01') in your test.

from decimal import Decimal

project2_membership = ProjectMembership(
    user=self.new_user,
    project=project2,
    is_project_manager=False,
    full_time_equivalent=Decimal('10.01')
)
Alasdair
  • 298,606
  • 55
  • 578
  • 516