diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index e021e78f4..83b147607 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -88,7 +88,7 @@ class RackBaseSerializer(PrimaryModelSerializer): class RackTypeSerializer(RackBaseSerializer): # Unlike Rack.form_factor (optional & nullable), RackType.form_factor is required # (blank=False, no default), so override RackBaseSerializer's optional declaration. - form_factor = ChoiceField(choices=RackFormFactorChoices) + form_factor = ChoiceField(choices=RackFormFactorChoices, required=True) manufacturer = ManufacturerSerializer(nested=True) rack_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index edb416782..0097b2ffa 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -225,7 +225,6 @@ class RackTypeImportForm(PrimaryModelImportForm): form_factor = CSVChoiceField( label=_('Type'), choices=RackFormFactorChoices, - required=False, help_text=_('Form factor') ) starting_unit = forms.IntegerField( diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index cd07b6e6d..3e7287597 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1226,7 +1226,7 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase): } response = self.client.post(self._get_list_url(), data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) - self.assertIn('form_factor', response.data) + self.assertEqual(response.data['form_factor'][0].code, 'required') class RackTestCase(APIViewTestCases.APIViewTestCase): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 374ad4a40..686e3e0a0 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -503,10 +503,10 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit", - "Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg", - "Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg", - "Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg", + "manufacturer,model,slug,form_factor,width,u_height,weight,max_weight,weight_unit", + f"Manufacturer 1,RackType 4,rack-type-4,{RackFormFactorChoices.TYPE_CABINET},19,42,100,2000,kg", + f"Manufacturer 1,RackType 5,rack-type-5,{RackFormFactorChoices.TYPE_CABINET},19,42,100,2000,kg", + f"Manufacturer 1,RackType 6,rack-type-6,{RackFormFactorChoices.TYPE_CABINET},19,42,100,2000,kg", ) cls.csv_update_data = ( @@ -531,6 +531,30 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'comments': 'New comments', } + def test_bulk_import_objects_without_form_factor(self): + """ + A CSV import row omitting form_factor must be rejected, not silently saved + with form_factor=''. + """ + obj_perm = ObjectPermission(name='Test permission', actions=['add']) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) + + initial_count = self._get_queryset().count() + csv_data = ( + "manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit", + "Manufacturer 1,RackType Missing Form Factor,rack-type-missing-form-factor,19,42,100,2000,kg", + ) + data = { + 'data': '\n'.join(csv_data), + 'format': ImportFormatChoices.CSV, + 'csv_delimiter': CSVDelimiterChoices.AUTO, + } + response = self.client.post(self._get_url('bulk_import'), data) + self.assertHttpStatus(response, 200) + self.assertEqual(self._get_queryset().count(), initial_count) + class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Rack diff --git a/netbox/vpn/tests/test_api.py b/netbox/vpn/tests/test_api.py index 9c8542886..849eae3c5 100644 --- a/netbox/vpn/tests/test_api.py +++ b/netbox/vpn/tests/test_api.py @@ -619,7 +619,7 @@ class L2VPNTestCase(APIViewTestCases.APIViewTestCase): } response = self.client.post(self._get_list_url(), data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) - self.assertIn('type', response.data) + self.assertEqual(response.data['type'][0].code, 'required') class L2VPNTerminationTestCase(APIViewTestCases.APIViewTestCase):