Fixed #24636 -- Added model field validation for decimal places and max digits.
This commit is contained in:
parent
6f1b09bb5c
commit
75ed590032
|
@ -346,3 +346,72 @@ class MaxLengthValidator(BaseValidator):
|
||||||
'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).',
|
'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).',
|
||||||
'limit_value')
|
'limit_value')
|
||||||
code = 'max_length'
|
code = 'max_length'
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class DecimalValidator(object):
|
||||||
|
"""
|
||||||
|
Validate that the input does not exceed the maximum number of digits
|
||||||
|
expected, otherwise raise ValidationError.
|
||||||
|
"""
|
||||||
|
messages = {
|
||||||
|
'max_digits': ungettext_lazy(
|
||||||
|
'Ensure that there are no more than %(max)s digit in total.',
|
||||||
|
'Ensure that there are no more than %(max)s digits in total.',
|
||||||
|
'max'
|
||||||
|
),
|
||||||
|
'max_decimal_places': ungettext_lazy(
|
||||||
|
'Ensure that there are no more than %(max)s decimal place.',
|
||||||
|
'Ensure that there are no more than %(max)s decimal places.',
|
||||||
|
'max'
|
||||||
|
),
|
||||||
|
'max_whole_digits': ungettext_lazy(
|
||||||
|
'Ensure that there are no more than %(max)s digit before the decimal point.',
|
||||||
|
'Ensure that there are no more than %(max)s digits before the decimal point.',
|
||||||
|
'max'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, max_digits, decimal_places):
|
||||||
|
self.max_digits = max_digits
|
||||||
|
self.decimal_places = decimal_places
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
digit_tuple, exponent = value.as_tuple()[1:]
|
||||||
|
decimals = abs(exponent)
|
||||||
|
# digit_tuple doesn't include any leading zeros.
|
||||||
|
digits = len(digit_tuple)
|
||||||
|
if decimals > digits:
|
||||||
|
# We have leading zeros up to or past the decimal point. Count
|
||||||
|
# everything past the decimal point as a digit. We do not count
|
||||||
|
# 0 before the decimal point as a digit since that would mean
|
||||||
|
# we would not allow max_digits = decimal_places.
|
||||||
|
digits = decimals
|
||||||
|
whole_digits = digits - decimals
|
||||||
|
|
||||||
|
if self.max_digits is not None and digits > self.max_digits:
|
||||||
|
raise ValidationError(
|
||||||
|
self.messages['max_digits'],
|
||||||
|
code='max_digits',
|
||||||
|
params={'max': self.max_digits},
|
||||||
|
)
|
||||||
|
if self.decimal_places is not None and decimals > self.decimal_places:
|
||||||
|
raise ValidationError(
|
||||||
|
self.messages['max_decimal_places'],
|
||||||
|
code='max_decimal_places',
|
||||||
|
params={'max': self.decimal_places},
|
||||||
|
)
|
||||||
|
if (self.max_digits is not None and self.decimal_places is not None
|
||||||
|
and whole_digits > (self.max_digits - self.decimal_places)):
|
||||||
|
raise ValidationError(
|
||||||
|
self.messages['max_whole_digits'],
|
||||||
|
code='max_whole_digits',
|
||||||
|
params={'max': (self.max_digits - self.decimal_places)},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, self.__class__) and
|
||||||
|
self.max_digits == other.max_digits and
|
||||||
|
self.decimal_places == other.decimal_places
|
||||||
|
)
|
||||||
|
|
|
@ -1578,6 +1578,12 @@ class DecimalField(Field):
|
||||||
]
|
]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def validators(self):
|
||||||
|
return super(DecimalField, self).validators + [
|
||||||
|
validators.DecimalValidator(self.max_digits, self.decimal_places)
|
||||||
|
]
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
name, path, args, kwargs = super(DecimalField, self).deconstruct()
|
name, path, args, kwargs = super(DecimalField, self).deconstruct()
|
||||||
if self.max_digits is not None:
|
if self.max_digits is not None:
|
||||||
|
|
|
@ -334,23 +334,12 @@ class FloatField(IntegerField):
|
||||||
class DecimalField(IntegerField):
|
class DecimalField(IntegerField):
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _('Enter a number.'),
|
'invalid': _('Enter a number.'),
|
||||||
'max_digits': ungettext_lazy(
|
|
||||||
'Ensure that there are no more than %(max)s digit in total.',
|
|
||||||
'Ensure that there are no more than %(max)s digits in total.',
|
|
||||||
'max'),
|
|
||||||
'max_decimal_places': ungettext_lazy(
|
|
||||||
'Ensure that there are no more than %(max)s decimal place.',
|
|
||||||
'Ensure that there are no more than %(max)s decimal places.',
|
|
||||||
'max'),
|
|
||||||
'max_whole_digits': ungettext_lazy(
|
|
||||||
'Ensure that there are no more than %(max)s digit before the decimal point.',
|
|
||||||
'Ensure that there are no more than %(max)s digits before the decimal point.',
|
|
||||||
'max'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
|
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
|
||||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||||
super(DecimalField, self).__init__(max_value, min_value, *args, **kwargs)
|
super(DecimalField, self).__init__(max_value, min_value, *args, **kwargs)
|
||||||
|
self.validators.append(validators.DecimalValidator(max_digits, decimal_places))
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""
|
"""
|
||||||
|
@ -379,38 +368,6 @@ class DecimalField(IntegerField):
|
||||||
# isn't equal to itself, so we can use this to identify NaN
|
# isn't equal to itself, so we can use this to identify NaN
|
||||||
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
|
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
|
||||||
raise ValidationError(self.error_messages['invalid'], code='invalid')
|
raise ValidationError(self.error_messages['invalid'], code='invalid')
|
||||||
sign, digittuple, exponent = value.as_tuple()
|
|
||||||
decimals = abs(exponent)
|
|
||||||
# digittuple doesn't include any leading zeros.
|
|
||||||
digits = len(digittuple)
|
|
||||||
if decimals > digits:
|
|
||||||
# We have leading zeros up to or past the decimal point. Count
|
|
||||||
# everything past the decimal point as a digit. We do not count
|
|
||||||
# 0 before the decimal point as a digit since that would mean
|
|
||||||
# we would not allow max_digits = decimal_places.
|
|
||||||
digits = decimals
|
|
||||||
whole_digits = digits - decimals
|
|
||||||
|
|
||||||
if self.max_digits is not None and digits > self.max_digits:
|
|
||||||
raise ValidationError(
|
|
||||||
self.error_messages['max_digits'],
|
|
||||||
code='max_digits',
|
|
||||||
params={'max': self.max_digits},
|
|
||||||
)
|
|
||||||
if self.decimal_places is not None and decimals > self.decimal_places:
|
|
||||||
raise ValidationError(
|
|
||||||
self.error_messages['max_decimal_places'],
|
|
||||||
code='max_decimal_places',
|
|
||||||
params={'max': self.decimal_places},
|
|
||||||
)
|
|
||||||
if (self.max_digits is not None and self.decimal_places is not None
|
|
||||||
and whole_digits > (self.max_digits - self.decimal_places)):
|
|
||||||
raise ValidationError(
|
|
||||||
self.error_messages['max_whole_digits'],
|
|
||||||
code='max_whole_digits',
|
|
||||||
params={'max': (self.max_digits - self.decimal_places)},
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def widget_attrs(self, widget):
|
def widget_attrs(self, widget):
|
||||||
attrs = super(DecimalField, self).widget_attrs(widget)
|
attrs = super(DecimalField, self).widget_attrs(widget)
|
||||||
|
|
|
@ -281,3 +281,19 @@ to, or in lieu of custom ``field.clean()`` methods.
|
||||||
.. versionchanged:: 1.8
|
.. versionchanged:: 1.8
|
||||||
|
|
||||||
The ``message`` parameter was added.
|
The ``message`` parameter was added.
|
||||||
|
|
||||||
|
``DecimalValidator``
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. class:: DecimalValidator(max_digits, decimal_places)
|
||||||
|
|
||||||
|
.. versionadded:: 1.9
|
||||||
|
|
||||||
|
Raises :exc:`~django.core.exceptions.ValidationError` with the following
|
||||||
|
codes:
|
||||||
|
|
||||||
|
- ``'max_digits'`` if the number of digits is larger than ``max_digits``.
|
||||||
|
- ``'max_decimal_places'`` if the number of decimals is larger than
|
||||||
|
``decimal_places``.
|
||||||
|
- ``'max_whole_digits'`` if the number of whole digits is larger than
|
||||||
|
the difference between ``max_digits`` and ``decimal_places``.
|
||||||
|
|
|
@ -165,6 +165,24 @@ class DecimalFieldTests(test.TestCase):
|
||||||
# This should not crash. That counts as a win for our purposes.
|
# This should not crash. That counts as a win for our purposes.
|
||||||
Foo.objects.filter(d__gte=100000000000)
|
Foo.objects.filter(d__gte=100000000000)
|
||||||
|
|
||||||
|
def test_max_digits_validation(self):
|
||||||
|
field = models.DecimalField(max_digits=2)
|
||||||
|
expected_message = validators.DecimalValidator.messages['max_digits'] % {'max': 2}
|
||||||
|
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||||
|
field.clean(100, None)
|
||||||
|
|
||||||
|
def test_max_decimal_places_validation(self):
|
||||||
|
field = models.DecimalField(decimal_places=1)
|
||||||
|
expected_message = validators.DecimalValidator.messages['max_decimal_places'] % {'max': 1}
|
||||||
|
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||||
|
field.clean(Decimal('0.99'), None)
|
||||||
|
|
||||||
|
def test_max_whole_digits_validation(self):
|
||||||
|
field = models.DecimalField(max_digits=3, decimal_places=1)
|
||||||
|
expected_message = validators.DecimalValidator.messages['max_whole_digits'] % {'max': 2}
|
||||||
|
with self.assertRaisesMessage(ValidationError, expected_message):
|
||||||
|
field.clean(Decimal('999'), None)
|
||||||
|
|
||||||
|
|
||||||
class ForeignKeyTests(test.TestCase):
|
class ForeignKeyTests(test.TestCase):
|
||||||
def test_callable_default(self):
|
def test_callable_default(self):
|
||||||
|
|
|
@ -10,11 +10,12 @@ from unittest import TestCase
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
BaseValidator, EmailValidator, MaxLengthValidator, MaxValueValidator,
|
BaseValidator, DecimalValidator, EmailValidator, MaxLengthValidator,
|
||||||
MinLengthValidator, MinValueValidator, RegexValidator, URLValidator,
|
MaxValueValidator, MinLengthValidator, MinValueValidator, RegexValidator,
|
||||||
int_list_validator, validate_comma_separated_integer_list, validate_email,
|
URLValidator, int_list_validator, validate_comma_separated_integer_list,
|
||||||
validate_integer, validate_ipv4_address, validate_ipv6_address,
|
validate_email, validate_integer, validate_ipv4_address,
|
||||||
validate_ipv46_address, validate_slug, validate_unicode_slug,
|
validate_ipv6_address, validate_ipv46_address, validate_slug,
|
||||||
|
validate_unicode_slug,
|
||||||
)
|
)
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.test.utils import str_prefix
|
from django.test.utils import str_prefix
|
||||||
|
@ -401,3 +402,21 @@ class TestValidatorEquality(TestCase):
|
||||||
MinValueValidator(45),
|
MinValueValidator(45),
|
||||||
MinValueValidator(11),
|
MinValueValidator(11),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_decimal_equality(self):
|
||||||
|
self.assertEqual(
|
||||||
|
DecimalValidator(1, 2),
|
||||||
|
DecimalValidator(1, 2),
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
DecimalValidator(1, 2),
|
||||||
|
DecimalValidator(1, 1),
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
DecimalValidator(1, 2),
|
||||||
|
DecimalValidator(2, 2),
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
DecimalValidator(1, 2),
|
||||||
|
MinValueValidator(11),
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue