Fixes #23166: Apply zero-valued numeric bounds to profile attribute form fields
JSONSchemaProperty.to_form_field() guarded the numeric bounds on truthiness, so a minimum or maximum of 0 never reached the form field. The UI accepted values the model then rejected in ModuleType.clean(). Compare against None instead, matching the string bounds directly above. Skip the numeric branch entirely when the property carries an enum. Those resolve to a ChoiceField, which accepts neither min_value nor max_value, so passing the bounds through raised a TypeError. The zero-bound case reached this only once the truthiness guard was removed. A non-zero bound alongside an enum failed before.
This commit is contained in:
parent
1793874dd9
commit
2bed744ec2
|
|
@ -229,6 +229,33 @@ class ModuleTypeFormTestCase(TestCase):
|
|||
module_type = form.save()
|
||||
self.assertEqual(module_type.attribute_data, {'media': ['copper', 'qsfp28']})
|
||||
|
||||
def test_zero_bound_attribute_is_enforced_by_the_form(self):
|
||||
profile = ModuleTypeProfile.objects.create(
|
||||
name='Module Type Profile 2',
|
||||
schema={
|
||||
'properties': {
|
||||
'offset': {
|
||||
'title': 'Offset',
|
||||
'type': 'number',
|
||||
'minimum': 0,
|
||||
'maximum': 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
form = ModuleTypeForm(data={
|
||||
'manufacturer': self.manufacturer.pk,
|
||||
'model': 'Module Type 2',
|
||||
'profile': profile.pk,
|
||||
'attr_offset': -5,
|
||||
})
|
||||
|
||||
self.assertEqual(form.fields['attr_offset'].min_value, 0)
|
||||
self.assertEqual(form.fields['attr_offset'].max_value, 0)
|
||||
with patch('utilities.forms.fields.dynamic.get_action_url', return_value='/'):
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('attr_offset', form.errors)
|
||||
|
||||
|
||||
class ModuleTypeProfileDescriptionRenderingTestCase(TestCase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -121,11 +121,11 @@ class JSONSchemaProperty:
|
|||
]
|
||||
|
||||
# Integer/number validation
|
||||
elif self.type in (PropertyTypeEnum.INTEGER.value, PropertyTypeEnum.NUMBER.value):
|
||||
elif self.type in (PropertyTypeEnum.INTEGER.value, PropertyTypeEnum.NUMBER.value) and not self.enum:
|
||||
field_kwargs['widget'] = forms.NumberInput(attrs={'step': 'any'})
|
||||
if self.minimum:
|
||||
if self.minimum is not None:
|
||||
field_kwargs['min_value'] = self.minimum
|
||||
if self.maximum:
|
||||
if self.maximum is not None:
|
||||
field_kwargs['max_value'] = self.maximum
|
||||
if self.multipleOf:
|
||||
field_kwargs['validators'] = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from django import forms
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from utilities.jsonschema import JSONSchemaProperty
|
||||
|
|
@ -45,6 +46,89 @@ class JSONSchemaPropertyTestCase(TestCase):
|
|||
self.assertIsInstance(field.base_field, forms.CharField)
|
||||
self.assertEqual(field.clean('ge-0/0/0,ge-0/0/1'), ['ge-0/0/0', 'ge-0/0/1'])
|
||||
|
||||
def test_zero_minimum_is_applied_to_form_field(self):
|
||||
prop = JSONSchemaProperty(type='number', title='Offset', minimum=0)
|
||||
|
||||
field = prop.to_form_field('offset')
|
||||
|
||||
self.assertEqual(field.min_value, 0)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(-5)
|
||||
self.assertEqual(field.clean(0), 0)
|
||||
|
||||
def test_zero_maximum_is_applied_to_form_field(self):
|
||||
prop = JSONSchemaProperty(type='number', title='Offset', maximum=0)
|
||||
|
||||
field = prop.to_form_field('offset')
|
||||
|
||||
self.assertEqual(field.max_value, 0)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(5)
|
||||
self.assertEqual(field.clean(0), 0)
|
||||
|
||||
def test_zero_bounds_are_applied_to_integer_form_field(self):
|
||||
prop = JSONSchemaProperty(type='integer', title='Slots', minimum=0, maximum=0)
|
||||
|
||||
field = prop.to_form_field('slots')
|
||||
|
||||
self.assertEqual(field.min_value, 0)
|
||||
self.assertEqual(field.max_value, 0)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(-1)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(1)
|
||||
self.assertEqual(field.clean(0), 0)
|
||||
|
||||
def test_nonzero_bounds_are_applied_to_form_field(self):
|
||||
prop = JSONSchemaProperty(type='number', title='Offset', minimum=1, maximum=10)
|
||||
|
||||
field = prop.to_form_field('offset')
|
||||
|
||||
self.assertEqual(field.min_value, 1)
|
||||
self.assertEqual(field.max_value, 10)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(0)
|
||||
with self.assertRaises(ValidationError):
|
||||
field.clean(11)
|
||||
|
||||
def test_omitted_bounds_are_not_applied_to_form_field(self):
|
||||
prop = JSONSchemaProperty(type='number', title='Offset')
|
||||
|
||||
field = prop.to_form_field('offset')
|
||||
|
||||
self.assertIsNone(field.min_value)
|
||||
self.assertIsNone(field.max_value)
|
||||
self.assertEqual(field.clean(-100), -100)
|
||||
|
||||
def test_numeric_enum_with_zero_bound_builds_choice_field(self):
|
||||
"""A numeric property carrying both an enum and a zero bound resolves to a ChoiceField.
|
||||
|
||||
ChoiceField accepts neither min_value nor max_value, so the numeric bounds must not be
|
||||
passed through when an enum is present.
|
||||
"""
|
||||
prop = JSONSchemaProperty(type='integer', title='Slots', enum=[0, 1, 2], minimum=0)
|
||||
|
||||
field = prop.to_form_field('slots')
|
||||
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
self.assertEqual(list(field.choices), [(None, ''), (0, 0), (1, 1), (2, 2)])
|
||||
|
||||
def test_numeric_enum_with_nonzero_bound_builds_choice_field(self):
|
||||
prop = JSONSchemaProperty(type='integer', title='Slots', enum=[1, 2], minimum=1, maximum=2)
|
||||
|
||||
field = prop.to_form_field('slots')
|
||||
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
self.assertEqual(list(field.choices), [(None, ''), (1, 1), (2, 2)])
|
||||
|
||||
def test_numeric_enum_with_multiple_of_builds_choice_field(self):
|
||||
prop = JSONSchemaProperty(type='integer', title='Slots', enum=[2, 4], multipleOf=2)
|
||||
|
||||
field = prop.to_form_field('slots')
|
||||
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
self.assertEqual(list(field.choices), [(None, ''), (2, 2), (4, 4)])
|
||||
|
||||
|
||||
class JSONSchemaPropertyDescriptionSanitizationTestCase(TestCase):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue