Validate related object field is list

The related object fields are not covered by the form, so don't pass
any validation before trying to iterate over them and accessing their
elements. Instead of allowing a hard technical error to be raised,
explicitly check that it is indeed a list, and raise a normal validation
error if not.

The error message is chosen to be similar in format and wording to the
other existing validation errors. The used word "list" is quite
universal, and conveys the wanted meaning in the context of python,
json and yaml.
This commit is contained in:
Marko Hauptvogel 2025-10-30 13:33:34 +01:00
parent 8452222761
commit 78223cea03
2 changed files with 46 additions and 1 deletions

View File

@ -1038,6 +1038,42 @@ module-bays:
self.assertHttpStatus(response, 200) self.assertHttpStatus(response, 200)
self.assertContains(response, "Record 2 module-bays[3].name: This field is required.") self.assertContains(response, "Record 2 module-bays[3].name: This field is required.")
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_import_nolist(self):
# Add all required permissions to the test user
self.add_permissions(
'dcim.view_devicetype',
'dcim.add_devicetype',
'dcim.add_consoleporttemplate',
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
'dcim.add_modulebaytemplate',
'dcim.add_devicebaytemplate',
'dcim.add_inventoryitemtemplate',
)
for value in ('', 'null', '3', '"My console port"', '{name: "My other console port"}'):
with self.subTest(value=value):
import_data = f'''
manufacturer: Manufacturer 1
model: TEST-3000
slug: test-3000
u_height: 1
console-ports: {value}
'''
form_data = {
'data': import_data,
'format': 'yaml'
}
response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
self.assertHttpStatus(response, 200)
self.assertContains(response, "Record 1 console-ports: Must be a list.")
def test_export_objects(self): def test_export_objects(self):
url = reverse('dcim:devicetype_list') url = reverse('dcim:devicetype_list')
self.add_permissions('dcim.view_devicetype') self.add_permissions('dcim.view_devicetype')

View File

@ -381,8 +381,17 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
# Iterate through the related object forms (if any), validating and saving each instance. # Iterate through the related object forms (if any), validating and saving each instance.
for field_name, related_object_form in self.related_object_forms.items(): for field_name, related_object_form in self.related_object_forms.items():
related_objects = model_form.data.get(field_name, list())
if not isinstance(related_objects, list):
raise ValidationError(
self._compile_form_errors(
{field_name: [_("Must be a list.")]},
index=parent_idx
)
)
related_obj_pks = [] related_obj_pks = []
for i, rel_obj_data in enumerate(model_form.data.get(field_name, list()), start=1): for i, rel_obj_data in enumerate(related_objects, start=1):
rel_obj_data = self.prep_related_object_data(obj, rel_obj_data) rel_obj_data = self.prep_related_object_data(obj, rel_obj_data)
f = related_object_form(rel_obj_data) f = related_object_form(rel_obj_data)