Selaa lähdekoodia

Fixes #23172: Save numeric enum profile attributes as numbers

JSONSchemaProperty resolved every scalar enum to a ChoiceField, which
cleans to the submitted string. ModuleType.clean() then validated '1'
against an integer enum and rejected every option. Use a
TypedChoiceField that maps the submitted value back to the enum member,
so it keeps the type the schema declares.
Fabiology 4 päivää sitten
vanhempi
sitoutus
3ad2e5db94

+ 28 - 0
netbox/dcim/tests/test_forms.py

@@ -256,6 +256,34 @@ class ModuleTypeFormTestCase(TestCase):
             self.assertFalse(form.is_valid())
         self.assertIn('attr_offset', form.errors)
 
+    def test_numeric_enum_attribute_is_saved_as_a_number(self):
+        profile = ModuleTypeProfile.objects.create(
+            name='Module Type Profile 3',
+            schema={
+                'properties': {
+                    'slots': {
+                        'title': 'Slots',
+                        'type': 'integer',
+                        'enum': [0, 1, 2],
+                    },
+                },
+            },
+        )
+        for value in (0, 1):
+            with self.subTest(value=value):
+                form = ModuleTypeForm(data={
+                    'manufacturer': self.manufacturer.pk,
+                    'model': f'Module Type {value}',
+                    'profile': profile.pk,
+                    'attr_slots': str(value),
+                })
+
+                with patch('utilities.forms.fields.dynamic.get_action_url', return_value='/'):
+                    self.assertTrue(form.is_valid(), form.errors)
+
+                    module_type = form.save()
+                    self.assertEqual(module_type.attribute_data, {'slots': value})
+
 
 class ModuleTypeProfileDescriptionRenderingTestCase(TestCase):
     """

+ 5 - 1
netbox/utilities/jsonschema.py

@@ -103,6 +103,10 @@ class JSONSchemaProperty:
             if not required:
                 choices = [(None, ''), *choices]
             field_kwargs['choices'] = choices
+            if issubclass(field_class, forms.TypedChoiceField):
+                # The widget submits each choice as a string; map it back to the enum member so a
+                # non-string enum (e.g. integer) keeps the type the schema validates it against.
+                field_kwargs['coerce'] = {str(v): v for v in self.enum}.get
 
         # Arrays
         if self.type == PropertyTypeEnum.ARRAY.value:
@@ -150,7 +154,7 @@ class JSONSchemaProperty:
         if self.enum:
             if self.type == PropertyTypeEnum.ARRAY.value:
                 return forms.MultipleChoiceField
-            return forms.ChoiceField
+            return forms.TypedChoiceField
         if self.type == PropertyTypeEnum.STRING.value and self.format is not None:
             try:
                 return STRING_FORM_FIELDS[self.format]

+ 23 - 0
netbox/utilities/tests/test_jsonschema.py

@@ -140,6 +140,29 @@ class JSONSchemaPropertyTestCase(TestCase):
         self.assertEqual(len(field.validators), 1)
         self.assertIsInstance(field.validators[0], MultipleOfValidator)
 
+    def test_numeric_enum_cleans_to_the_enum_member(self):
+        """The widget submits a choice as a string, so a numeric enum must be coerced back.
+
+        ModuleType.clean() validates the attribute data against the schema, which rejects the
+        string '1' for an integer enum.
+        """
+        prop = JSONSchemaProperty(type='integer', title='Slots', enum=[0, 1, 2])
+
+        field = prop.to_form_field('slots')
+
+        self.assertEqual(field.clean('0'), 0)
+        self.assertEqual(field.clean('2'), 2)
+        self.assertEqual(field.clean(''), '')
+        with self.assertRaises(ValidationError):
+            field.clean('3')
+
+    def test_float_enum_cleans_to_the_enum_member(self):
+        prop = JSONSchemaProperty(type='number', title='Ratio', enum=[0.5, 1.5])
+
+        field = prop.to_form_field('ratio')
+
+        self.assertEqual(field.clean('1.5'), 1.5)
+
     def test_string_enum_with_min_length_builds_choice_field(self):
         """A string property carrying both an enum and a length bound resolves to a ChoiceField.