mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
Fixed #32559 -- Added 'step_size’ to numeric form fields.
Co-authored-by: Jacob Rief <jacob.rief@uibk.ac.at>
This commit is contained in:
parent
68da6b389c
commit
3a82b5f655
2
AUTHORS
2
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>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ipaddress
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
@ -401,6 +402,15 @@ class MinValueValidator(BaseValidator):
|
||||
return a < b
|
||||
|
||||
|
||||
@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(
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 two optional arguments for validation, ``max_value`` and ``min_value``.
|
||||
These control the range of values permitted in the field.
|
||||
Takes three optional arguments:
|
||||
|
||||
.. 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``.
|
||||
|
||||
``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``
|
||||
-------------
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user