mirror of
https://github.com/django/django.git
synced 2025-04-25 09:44:36 +00:00
Fixed #34473 -- Fixed step validation for form fields with non-zero minimum value.
This commit is contained in:
parent
549d6ffeb6
commit
1fe0b167af
@ -397,8 +397,37 @@ class StepValueValidator(BaseValidator):
|
|||||||
message = _("Ensure this value is a multiple of step size %(limit_value)s.")
|
message = _("Ensure this value is a multiple of step size %(limit_value)s.")
|
||||||
code = "step_size"
|
code = "step_size"
|
||||||
|
|
||||||
|
def __init__(self, limit_value, message=None, offset=None):
|
||||||
|
super().__init__(limit_value, message)
|
||||||
|
if offset is not None:
|
||||||
|
self.message = _(
|
||||||
|
"Ensure this value is a multiple of step size %(limit_value)s, "
|
||||||
|
"starting from %(offset)s, e.g. %(offset)s, %(valid_value1)s, "
|
||||||
|
"%(valid_value2)s, and so on."
|
||||||
|
)
|
||||||
|
self.offset = offset
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if self.offset is None:
|
||||||
|
super().__call__(value)
|
||||||
|
else:
|
||||||
|
cleaned = self.clean(value)
|
||||||
|
limit_value = (
|
||||||
|
self.limit_value() if callable(self.limit_value) else self.limit_value
|
||||||
|
)
|
||||||
|
if self.compare(cleaned, limit_value):
|
||||||
|
offset = cleaned.__class__(self.offset)
|
||||||
|
params = {
|
||||||
|
"limit_value": limit_value,
|
||||||
|
"offset": offset,
|
||||||
|
"valid_value1": offset + limit_value,
|
||||||
|
"valid_value2": offset + 2 * limit_value,
|
||||||
|
}
|
||||||
|
raise ValidationError(self.message, code=self.code, params=params)
|
||||||
|
|
||||||
def compare(self, a, b):
|
def compare(self, a, b):
|
||||||
return not math.isclose(math.remainder(a, b), 0, abs_tol=1e-9)
|
offset = 0 if self.offset is None else self.offset
|
||||||
|
return not math.isclose(math.remainder(a - offset, b), 0, abs_tol=1e-9)
|
||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
|
@ -316,7 +316,9 @@ class IntegerField(Field):
|
|||||||
if min_value is not None:
|
if min_value is not None:
|
||||||
self.validators.append(validators.MinValueValidator(min_value))
|
self.validators.append(validators.MinValueValidator(min_value))
|
||||||
if step_size is not None:
|
if step_size is not None:
|
||||||
self.validators.append(validators.StepValueValidator(step_size))
|
self.validators.append(
|
||||||
|
validators.StepValueValidator(step_size, offset=min_value)
|
||||||
|
)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""
|
"""
|
||||||
|
@ -562,7 +562,9 @@ For each field, we describe the default widget used if you don't specify
|
|||||||
|
|
||||||
.. attribute:: step_size
|
.. attribute:: step_size
|
||||||
|
|
||||||
Limit valid inputs to an integral multiple of ``step_size``.
|
Limit valid inputs to an integral multiple of ``step_size``. If
|
||||||
|
``min_value`` is also provided, it's added as an offset to determine if
|
||||||
|
the step size matches.
|
||||||
|
|
||||||
``DurationField``
|
``DurationField``
|
||||||
-----------------
|
-----------------
|
||||||
@ -695,7 +697,9 @@ For each field, we describe the default widget used if you don't specify
|
|||||||
|
|
||||||
.. attribute:: step_size
|
.. attribute:: step_size
|
||||||
|
|
||||||
Limit valid inputs to an integral multiple of ``step_size``.
|
Limit valid inputs to an integral multiple of ``step_size``. If
|
||||||
|
``min_value`` is also provided, it's added as an offset to determine if
|
||||||
|
the step size matches.
|
||||||
|
|
||||||
``GenericIPAddressField``
|
``GenericIPAddressField``
|
||||||
-------------------------
|
-------------------------
|
||||||
@ -831,7 +835,9 @@ For each field, we describe the default widget used if you don't specify
|
|||||||
|
|
||||||
.. attribute:: step_size
|
.. attribute:: step_size
|
||||||
|
|
||||||
Limit valid inputs to an integral multiple of ``step_size``.
|
Limit valid inputs to an integral multiple of ``step_size``. If
|
||||||
|
``min_value`` is also provided, it's added as an offset to determine if
|
||||||
|
the step size matches.
|
||||||
|
|
||||||
``JSONField``
|
``JSONField``
|
||||||
-------------
|
-------------
|
||||||
|
@ -340,9 +340,16 @@ to, or in lieu of custom ``field.clean()`` methods.
|
|||||||
``StepValueValidator``
|
``StepValueValidator``
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
.. class:: StepValueValidator(limit_value, message=None)
|
.. class:: StepValueValidator(limit_value, message=None, offset=None)
|
||||||
|
|
||||||
Raises a :exc:`~django.core.exceptions.ValidationError` with a code of
|
Raises a :exc:`~django.core.exceptions.ValidationError` with a code of
|
||||||
``'step_size'`` if ``value`` is not an integral multiple of
|
``'step_size'`` if ``value`` is not an integral multiple of
|
||||||
``limit_value``, which can be a float, integer or decimal value or a
|
``limit_value``, which can be a float, integer or decimal value or a
|
||||||
callable.
|
callable. When ``offset`` is set, the validation occurs against
|
||||||
|
``limit_value`` plus ``offset``. For example, for
|
||||||
|
``StepValueValidator(3, offset=1.4)`` valid values include ``1.4``,
|
||||||
|
``4.4``, ``7.4``, ``10.4``, and so on.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
The ``offset`` argument was added.
|
||||||
|
@ -367,7 +367,9 @@ Utilities
|
|||||||
Validators
|
Validators
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new ``offset`` argument of
|
||||||
|
:class:`~django.core.validators.StepValueValidator` allows specifying an
|
||||||
|
offset for valid values.
|
||||||
|
|
||||||
.. _backwards-incompatible-5.0:
|
.. _backwards-incompatible-5.0:
|
||||||
|
|
||||||
|
@ -152,6 +152,25 @@ class DecimalFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
|||||||
with self.assertRaisesMessage(ValidationError, msg):
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
f.clean("1.1")
|
f.clean("1.1")
|
||||||
|
|
||||||
|
def test_decimalfield_step_size_min_value(self):
|
||||||
|
f = DecimalField(
|
||||||
|
step_size=decimal.Decimal("0.3"),
|
||||||
|
min_value=decimal.Decimal("-0.4"),
|
||||||
|
)
|
||||||
|
self.assertWidgetRendersTo(
|
||||||
|
f,
|
||||||
|
'<input name="f" min="-0.4" step="0.3" type="number" id="id_f" required>',
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
"Ensure this value is a multiple of step size 0.3, starting from -0.4, "
|
||||||
|
"e.g. -0.4, -0.1, 0.2, and so on."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
f.clean("1")
|
||||||
|
self.assertEqual(f.clean("0.2"), decimal.Decimal("0.2"))
|
||||||
|
self.assertEqual(f.clean(2), decimal.Decimal(2))
|
||||||
|
self.assertEqual(f.step_size, decimal.Decimal("0.3"))
|
||||||
|
|
||||||
def test_decimalfield_scientific(self):
|
def test_decimalfield_scientific(self):
|
||||||
f = DecimalField(max_digits=4, decimal_places=2)
|
f = DecimalField(max_digits=4, decimal_places=2)
|
||||||
with self.assertRaisesMessage(ValidationError, "Ensure that there are no more"):
|
with self.assertRaisesMessage(ValidationError, "Ensure that there are no more"):
|
||||||
|
@ -84,6 +84,18 @@ class FloatFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
|||||||
self.assertEqual(-1.26, f.clean("-1.26"))
|
self.assertEqual(-1.26, f.clean("-1.26"))
|
||||||
self.assertEqual(f.step_size, 0.02)
|
self.assertEqual(f.step_size, 0.02)
|
||||||
|
|
||||||
|
def test_floatfield_step_size_min_value(self):
|
||||||
|
f = FloatField(step_size=0.02, min_value=0.01)
|
||||||
|
msg = (
|
||||||
|
"Ensure this value is a multiple of step size 0.02, starting from 0.01, "
|
||||||
|
"e.g. 0.01, 0.03, 0.05, and so on."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
f.clean("0.02")
|
||||||
|
self.assertEqual(f.clean("2.33"), 2.33)
|
||||||
|
self.assertEqual(f.clean("0.11"), 0.11)
|
||||||
|
self.assertEqual(f.step_size, 0.02)
|
||||||
|
|
||||||
def test_floatfield_widget_attrs(self):
|
def test_floatfield_widget_attrs(self):
|
||||||
f = FloatField(widget=NumberInput(attrs={"step": 0.01, "max": 1.0, "min": 0.0}))
|
f = FloatField(widget=NumberInput(attrs={"step": 0.01, "max": 1.0, "min": 0.0}))
|
||||||
self.assertWidgetRendersTo(
|
self.assertWidgetRendersTo(
|
||||||
|
@ -126,6 +126,22 @@ class IntegerFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
|||||||
self.assertEqual(12, f.clean("12"))
|
self.assertEqual(12, f.clean("12"))
|
||||||
self.assertEqual(f.step_size, 3)
|
self.assertEqual(f.step_size, 3)
|
||||||
|
|
||||||
|
def test_integerfield_step_size_min_value(self):
|
||||||
|
f = IntegerField(step_size=3, min_value=-1)
|
||||||
|
self.assertWidgetRendersTo(
|
||||||
|
f,
|
||||||
|
'<input name="f" min="-1" step="3" type="number" id="id_f" required>',
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
"Ensure this value is a multiple of step size 3, starting from -1, e.g. "
|
||||||
|
"-1, 2, 5, and so on."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
f.clean("9")
|
||||||
|
self.assertEqual(f.clean("2"), 2)
|
||||||
|
self.assertEqual(f.clean("-1"), -1)
|
||||||
|
self.assertEqual(f.step_size, 3)
|
||||||
|
|
||||||
def test_integerfield_localized(self):
|
def test_integerfield_localized(self):
|
||||||
"""
|
"""
|
||||||
A localized IntegerField's widget renders to a text input without any
|
A localized IntegerField's widget renders to a text input without any
|
||||||
|
@ -451,11 +451,39 @@ TEST_DATA = [
|
|||||||
(StepValueValidator(3), 1, ValidationError),
|
(StepValueValidator(3), 1, ValidationError),
|
||||||
(StepValueValidator(3), 8, ValidationError),
|
(StepValueValidator(3), 8, ValidationError),
|
||||||
(StepValueValidator(3), 9, None),
|
(StepValueValidator(3), 9, None),
|
||||||
|
(StepValueValidator(2), 4, None),
|
||||||
|
(StepValueValidator(2, offset=1), 3, None),
|
||||||
|
(StepValueValidator(2, offset=1), 4, ValidationError),
|
||||||
(StepValueValidator(0.001), 0.55, None),
|
(StepValueValidator(0.001), 0.55, None),
|
||||||
(StepValueValidator(0.001), 0.5555, ValidationError),
|
(StepValueValidator(0.001), 0.5555, ValidationError),
|
||||||
|
(StepValueValidator(0.001, offset=0.0005), 0.5555, None),
|
||||||
|
(StepValueValidator(0.001, offset=0.0005), 0.555, ValidationError),
|
||||||
(StepValueValidator(Decimal(0.02)), 0.88, None),
|
(StepValueValidator(Decimal(0.02)), 0.88, None),
|
||||||
(StepValueValidator(Decimal(0.02)), Decimal(0.88), None),
|
(StepValueValidator(Decimal(0.02)), Decimal(0.88), None),
|
||||||
(StepValueValidator(Decimal(0.02)), Decimal(0.77), ValidationError),
|
(StepValueValidator(Decimal(0.02)), Decimal(0.77), ValidationError),
|
||||||
|
(StepValueValidator(Decimal(0.02), offset=Decimal(0.01)), Decimal(0.77), None),
|
||||||
|
(StepValueValidator(Decimal(2.0), offset=Decimal(0.1)), Decimal(0.1), None),
|
||||||
|
(
|
||||||
|
StepValueValidator(Decimal(0.02), offset=Decimal(0.01)),
|
||||||
|
Decimal(0.88),
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
|
(StepValueValidator(Decimal("1.2"), offset=Decimal("2.2")), Decimal("3.4"), None),
|
||||||
|
(
|
||||||
|
StepValueValidator(Decimal("1.2"), offset=Decimal("2.2")),
|
||||||
|
Decimal("1.2"),
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
StepValueValidator(Decimal("-1.2"), offset=Decimal("2.2")),
|
||||||
|
Decimal("1.1"),
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
StepValueValidator(Decimal("-1.2"), offset=Decimal("2.2")),
|
||||||
|
Decimal("1.0"),
|
||||||
|
None,
|
||||||
|
),
|
||||||
(URLValidator(EXTENDED_SCHEMES), "file://localhost/path", None),
|
(URLValidator(EXTENDED_SCHEMES), "file://localhost/path", None),
|
||||||
(URLValidator(EXTENDED_SCHEMES), "git://example.com/", None),
|
(URLValidator(EXTENDED_SCHEMES), "git://example.com/", None),
|
||||||
(
|
(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user