summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKapil Bansal <kapilbansal.gbpecdelhi@gmail.com>2022-05-12 11:30:47 +0200
committerCarlton Gibson <carlton@noumenal.es>2022-05-12 14:16:52 +0200
commit3a82b5f655446f0ca89e3b6a92b100aa458f348f (patch)
tree6e600054369b9316865b3b5b3ab1838769a2b54e
parent68da6b389c403cb91650754be0e2287696807333 (diff)
Fixed #32559 -- Added 'step_size’ to numeric form fields.
Co-authored-by: Jacob Rief <jacob.rief@uibk.ac.at>
-rw-r--r--AUTHORS2
-rw-r--r--django/core/validators.py10
-rw-r--r--django/forms/fields.py14
-rw-r--r--docs/ref/forms/fields.txt61
-rw-r--r--docs/ref/validators.txt12
-rw-r--r--docs/releases/4.1.txt10
-rw-r--r--tests/forms_tests/field_tests/test_floatfield.py15
-rw-r--r--tests/forms_tests/field_tests/test_integerfield.py14
-rw-r--r--tests/validators/tests.py18
9 files changed, 137 insertions, 19 deletions
diff --git a/AUTHORS b/AUTHORS
index 9f25f5933e..3a9a521c19 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -413,6 +413,7 @@ answer newbie questions, and generally made Django that much better:
Jacob Burch <jacobburch@gmail.com>
Jacob Green
Jacob Kaplan-Moss <jacob@jacobian.org>
+ Jacob Rief <jacob.rief@gmail.com>
Jacob Walls <http://www.jacobtylerwalls.com/>
Jakub Paczkowski <jakub@paczkowski.eu>
Jakub Wilk <jwilk@jwilk.net>
@@ -526,6 +527,7 @@ answer newbie questions, and generally made Django that much better:
Justin Myles Holmes <justin@slashrootcafe.com>
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
Kadesarin Sanjek
+ Kapil Bansal <kapilbansal.gbpecdelhi@gmail.com>
Karderio <karderio@gmail.com>
Karen Tracey <kmtracey@gmail.com>
Karol Sikora <elektrrrus@gmail.com>
diff --git a/django/core/validators.py b/django/core/validators.py
index 605b491ba7..473203a67e 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -1,4 +1,5 @@
import ipaddress
+import math
import re
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
@@ -402,6 +403,15 @@ class MinValueValidator(BaseValidator):
@deconstructible
+class StepValueValidator(BaseValidator):
+ message = _("Ensure this value is a multiple of step size %(limit_value)s.")
+ code = "step_size"
+
+ def compare(self, a, b):
+ return not math.isclose(math.remainder(a, b), 0, abs_tol=1e-9)
+
+
+@deconstructible
class MinLengthValidator(BaseValidator):
message = ngettext_lazy(
"Ensure this value has at least %(limit_value)d character (it has "
diff --git a/django/forms/fields.py b/django/forms/fields.py
index a7031936dd..8fa7b72cfd 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -299,8 +299,8 @@ class IntegerField(Field):
}
re_decimal = _lazy_re_compile(r"\.0*\s*$")
- def __init__(self, *, max_value=None, min_value=None, **kwargs):
- self.max_value, self.min_value = max_value, min_value
+ def __init__(self, *, max_value=None, min_value=None, step_size=None, **kwargs):
+ self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
if kwargs.get("localize") and self.widget == NumberInput:
# Localized number input is not well supported on most browsers
kwargs.setdefault("widget", super().widget)
@@ -310,6 +310,8 @@ class IntegerField(Field):
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
+ if step_size is not None:
+ self.validators.append(validators.StepValueValidator(step_size))
def to_python(self, value):
"""
@@ -335,6 +337,8 @@ class IntegerField(Field):
attrs["min"] = self.min_value
if self.max_value is not None:
attrs["max"] = self.max_value
+ if self.step_size is not None:
+ attrs["step"] = self.step_size
return attrs
@@ -369,7 +373,11 @@ class FloatField(IntegerField):
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
if isinstance(widget, NumberInput) and "step" not in widget.attrs:
- attrs.setdefault("step", "any")
+ if self.step_size is not None:
+ step = str(self.step_size)
+ else:
+ step = "any"
+ attrs.setdefault("step", step)
return attrs
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index fc92f8998f..4d0205b4d3 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -492,18 +492,20 @@ For each field, we describe the default widget used if you don't specify
* Normalizes to: A Python ``decimal``.
* Validates that the given value is a decimal. Uses
:class:`~django.core.validators.MaxValueValidator` and
- :class:`~django.core.validators.MinValueValidator` if ``max_value`` and
- ``min_value`` are provided. Leading and trailing whitespace is ignored.
+ :class:`~django.core.validators.MinValueValidator` if ``max_value`` and
+ ``min_value`` are provided. Uses
+ :class:`~django.core.validators.StepValueValidator` if ``step_size`` is
+ provided. Leading and trailing whitespace is ignored.
* Error message keys: ``required``, ``invalid``, ``max_value``,
``min_value``, ``max_digits``, ``max_decimal_places``,
- ``max_whole_digits``
+ ``max_whole_digits``, ``step_size``.
The ``max_value`` and ``min_value`` error messages may contain
``%(limit_value)s``, which will be substituted by the appropriate limit.
Similarly, the ``max_digits``, ``max_decimal_places`` and
``max_whole_digits`` error messages may contain ``%(max)s``.
- Takes four optional arguments:
+ Takes five optional arguments:
.. attribute:: max_value
.. attribute:: min_value
@@ -521,6 +523,14 @@ For each field, we describe the default widget used if you don't specify
The maximum number of decimal places permitted.
+ .. attribute:: step_size
+
+ Limit valid inputs to an integral multiple of ``step_size``.
+
+ .. versionchanged:: 4.1
+
+ The ``step_size`` argument was added.
+
``DurationField``
-----------------
@@ -636,13 +646,25 @@ For each field, we describe the default widget used if you don't specify
* Validates that the given value is a float. Uses
:class:`~django.core.validators.MaxValueValidator` and
:class:`~django.core.validators.MinValueValidator` if ``max_value`` and
- ``min_value`` are provided. Leading and trailing whitespace is allowed,
- as in Python's ``float()`` function.
+ ``min_value`` are provided. Uses
+ :class:`~django.core.validators.StepValueValidator` if ``step_size`` is
+ provided. Leading and trailing whitespace is allowed, as in Python's
+ ``float()`` function.
* Error message keys: ``required``, ``invalid``, ``max_value``,
- ``min_value``
+ ``min_value``, ``step_size``.
+
+ Takes three optional arguments:
+
+ .. attribute:: max_value
+ .. attribute:: min_value
- Takes two optional arguments for validation, ``max_value`` and ``min_value``.
- These control the range of values permitted in the field.
+ These control the range of values permitted in the field.
+
+ .. attribute:: step_size
+
+ .. versionadded:: 4.1
+
+ Limit valid inputs to an integral multiple of ``step_size``.
``GenericIPAddressField``
-------------------------
@@ -755,21 +777,30 @@ For each field, we describe the default widget used if you don't specify
* Validates that the given value is an integer. Uses
:class:`~django.core.validators.MaxValueValidator` and
:class:`~django.core.validators.MinValueValidator` if ``max_value`` and
- ``min_value`` are provided. Leading and trailing whitespace is allowed,
- as in Python's ``int()`` function.
+ ``min_value`` are provided. Uses
+ :class:`~django.core.validators.StepValueValidator` if ``step_size`` is
+ provided. Leading and trailing whitespace is allowed, as in Python's
+ ``int()`` function.
* Error message keys: ``required``, ``invalid``, ``max_value``,
- ``min_value``
+ ``min_value``, ``step_size``
- The ``max_value`` and ``min_value`` error messages may contain
- ``%(limit_value)s``, which will be substituted by the appropriate limit.
+ The ``max_value``, ``min_value`` and ``step_size`` error messages may
+ contain ``%(limit_value)s``, which will be substituted by the appropriate
+ limit.
- Takes two optional arguments for validation:
+ Takes three optional arguments for validation:
.. attribute:: max_value
.. attribute:: min_value
These control the range of values permitted in the field.
+ .. attribute:: step_size
+
+ .. versionadded:: 4.1
+
+ Limit valid inputs to an integral multiple of ``step_size``.
+
``JSONField``
-------------
diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
index 5fe1384bd8..2a7b85e614 100644
--- a/docs/ref/validators.txt
+++ b/docs/ref/validators.txt
@@ -333,3 +333,15 @@ to, or in lieu of custom ``field.clean()`` methods.
The error code used by :exc:`~django.core.exceptions.ValidationError`
if validation fails. Defaults to ``"null_characters_not_allowed"``.
+
+``StepValueValidator``
+----------------------
+
+.. versionadded:: 4.1
+
+.. class:: StepValueValidator(limit_value, message=None)
+
+ Raises a :exc:`~django.core.exceptions.ValidationError` with a code of
+ ``'step_size'`` if ``value`` is not an integral multiple of
+ ``limit_value``, which can be a float, integer or decimal value or a
+ callable.
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 84eca03563..677e8c0345 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -297,6 +297,11 @@ Forms
error messages for invalid number of forms by passing ``'too_few_forms'``
and ``'too_many_forms'`` keys.
+* :class:`~django.forms.IntegerField`, :class:`~django.forms.FloatField`, and
+ :class:`~django.forms.DecimalField` now optionally accept a ``step_size``
+ argument. This is used to set the ``step`` HTML attribute, and is validated
+ on form submission.
+
Generic Views
~~~~~~~~~~~~~
@@ -444,7 +449,10 @@ Utilities
Validators
~~~~~~~~~~
-* ...
+* The new :class:`~django.core.validators.StepValueValidator` checks if a value
+ is an integral multiple of a given step size. This new validator is used for
+ the new ``step_size`` argument added to form fields representing numeric
+ values.
.. _backwards-incompatible-4.1:
diff --git a/tests/forms_tests/field_tests/test_floatfield.py b/tests/forms_tests/field_tests/test_floatfield.py
index f6022807be..94676fb9ee 100644
--- a/tests/forms_tests/field_tests/test_floatfield.py
+++ b/tests/forms_tests/field_tests/test_floatfield.py
@@ -70,6 +70,21 @@ class FloatFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
self.assertEqual(f.max_value, 1.5)
self.assertEqual(f.min_value, 0.5)
+ def test_floatfield_4(self):
+ f = FloatField(step_size=0.02)
+ self.assertWidgetRendersTo(
+ f,
+ '<input name="f" step="0.02" type="number" id="id_f" required>',
+ )
+ msg = "'Ensure this value is a multiple of step size 0.02.'"
+ with self.assertRaisesMessage(ValidationError, msg):
+ f.clean("0.01")
+ self.assertEqual(2.34, f.clean("2.34"))
+ self.assertEqual(2.1, f.clean("2.1"))
+ self.assertEqual(-0.50, f.clean("-.5"))
+ self.assertEqual(-1.26, f.clean("-1.26"))
+ self.assertEqual(f.step_size, 0.02)
+
def test_floatfield_widget_attrs(self):
f = FloatField(widget=NumberInput(attrs={"step": 0.01, "max": 1.0, "min": 0.0}))
self.assertWidgetRendersTo(
diff --git a/tests/forms_tests/field_tests/test_integerfield.py b/tests/forms_tests/field_tests/test_integerfield.py
index 15314d53a4..1361b5cc9e 100644
--- a/tests/forms_tests/field_tests/test_integerfield.py
+++ b/tests/forms_tests/field_tests/test_integerfield.py
@@ -112,6 +112,20 @@ class IntegerFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
self.assertEqual(f.max_value, 20)
self.assertEqual(f.min_value, 10)
+ def test_integerfield_6(self):
+ f = IntegerField(step_size=3)
+ self.assertWidgetRendersTo(
+ f,
+ '<input name="f" step="3" type="number" id="id_f" required>',
+ )
+ with self.assertRaisesMessage(
+ ValidationError, "'Ensure this value is a multiple of step size 3.'"
+ ):
+ f.clean("10")
+ self.assertEqual(12, f.clean(12))
+ self.assertEqual(12, f.clean("12"))
+ self.assertEqual(f.step_size, 3)
+
def test_integerfield_localized(self):
"""
A localized IntegerField's widget renders to a text input without any
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 79369b172e..f9ffdfd605 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -17,6 +17,7 @@ from django.core.validators import (
MinValueValidator,
ProhibitNullCharactersValidator,
RegexValidator,
+ StepValueValidator,
URLValidator,
int_list_validator,
validate_comma_separated_integer_list,
@@ -440,12 +441,21 @@ TEST_DATA = [
# limit_value may be a callable.
(MinValueValidator(lambda: 1), 0, ValidationError),
(MinValueValidator(lambda: 1), 1, None),
+ (StepValueValidator(3), 0, None),
(MaxLengthValidator(10), "", None),
(MaxLengthValidator(10), 10 * "x", None),
(MaxLengthValidator(10), 15 * "x", ValidationError),
(MinLengthValidator(10), 15 * "x", None),
(MinLengthValidator(10), 10 * "x", None),
(MinLengthValidator(10), "", ValidationError),
+ (StepValueValidator(3), 1, ValidationError),
+ (StepValueValidator(3), 8, ValidationError),
+ (StepValueValidator(3), 9, None),
+ (StepValueValidator(0.001), 0.55, None),
+ (StepValueValidator(0.001), 0.5555, ValidationError),
+ (StepValueValidator(Decimal(0.02)), 0.88, None),
+ (StepValueValidator(Decimal(0.02)), Decimal(0.88), None),
+ (StepValueValidator(Decimal(0.02)), Decimal(0.77), ValidationError),
(URLValidator(EXTENDED_SCHEMES), "file://localhost/path", None),
(URLValidator(EXTENDED_SCHEMES), "git://example.com/", None),
(
@@ -715,6 +725,10 @@ class TestValidatorEquality(TestCase):
MaxValueValidator(44),
)
self.assertEqual(MaxValueValidator(44), mock.ANY)
+ self.assertEqual(
+ StepValueValidator(0.003),
+ StepValueValidator(0.003),
+ )
self.assertNotEqual(
MaxValueValidator(44),
MinValueValidator(44),
@@ -723,6 +737,10 @@ class TestValidatorEquality(TestCase):
MinValueValidator(45),
MinValueValidator(11),
)
+ self.assertNotEqual(
+ StepValueValidator(3),
+ StepValueValidator(2),
+ )
def test_decimal_equality(self):
self.assertEqual(