mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +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:
		
				
					committed by
					
						 Carlton Gibson
						Carlton Gibson
					
				
			
			
				
	
			
			
			
						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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user