diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index e1ba63ded67..7915203cda9 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -986,6 +986,131 @@ def test_import_objects(self): ii1 = InventoryItemTemplate.objects.first() self.assertEqual(ii1.name, 'Inventory Item 1') + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_import_error_numbering(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', + ) + + import_data = ''' +--- +manufacturer: Manufacturer 1 +model: TEST-2001 +slug: test-2001 +u_height: 1 +module-bays: + - name: Module Bay 1-1 + - name: Module Bay 1-2 +--- +- manufacturer: Manufacturer 1 + model: TEST-2002 + slug: test-2002 + u_height: 1 + module-bays: + - name: Module Bay 2-1 + - name: Module Bay 2-2 + - not_name: Module Bay 2-3 +- manufacturer: Manufacturer 1 + model: TEST-2003 + slug: test-2003 + u_height: 1 + module-bays: + - name: Module Bay 3-1 +''' + 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 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.") + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_import_nodict(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"', '["My other console port"]'): + with self.subTest(value=value): + import_data = f''' +manufacturer: Manufacturer 1 +model: TEST-4000 +slug: test-4000 +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[1]: Must be a dictionary.") + def test_export_objects(self): url = reverse('dcim:devicetype_list') self.add_permissions('dcim.view_devicetype') diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 36c8ce1c45d..7b78765322b 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -323,7 +323,7 @@ def post(self, request): class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): """ - Import objects in bulk (CSV format). + Import objects in bulk (CSV/JSON/YAML format). Attributes: model_form: The form used to create each imported object @@ -368,7 +368,7 @@ def _compile_form_errors(self, errors, index, prefix=None): error_messages.append(f"Record {index} {prefix}{field_name}: {err}") return error_messages - def _save_object(self, model_form, request): + def _save_object(self, model_form, request, parent_idx): _action = 'Updated' if model_form.instance.pk else 'Created' # Save the primary object @@ -381,8 +381,25 @@ def _save_object(self, model_form, request): # 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(): + 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 = [] - for i, rel_obj_data in enumerate(model_form.data.get(field_name, list())): + for i, rel_obj_data in enumerate(related_objects, start=1): + if not isinstance(rel_obj_data, dict): + raise ValidationError( + self._compile_form_errors( + {f'{field_name}[{i}]': [_("Must be a dictionary.")]}, + index=parent_idx, + ) + ) + rel_obj_data = self.prep_related_object_data(obj, rel_obj_data) f = related_object_form(rel_obj_data) @@ -396,7 +413,7 @@ def _save_object(self, model_form, request): else: # Replicate errors on the related object form to the import form for display and abort raise ValidationError( - self._compile_form_errors(f.errors, index=i, prefix=f'{field_name}[{i}]') + self._compile_form_errors(f.errors, index=parent_idx, prefix=f'{field_name}[{i}]') ) # Enforce object-level permissions on related objects @@ -439,8 +456,12 @@ def create_and_update_objects(self, form, request): try: instance = prefetched_objects[object_id] except KeyError: - form.add_error('data', _("Row {i}: Object with ID {id} does not exist").format(i=i, id=object_id)) - raise ValidationError('') + raise ValidationError( + self._compile_form_errors( + {'id': [_("Object with ID {id} does not exist").format(id=object_id)]}, + index=i + ) + ) # Take a snapshot for change logging if instance.pk and hasattr(instance, 'snapshot'): @@ -481,7 +502,7 @@ def create_and_update_objects(self, form, request): restrict_form_fields(model_form, request.user) if model_form.is_valid(): - obj = self._save_object(model_form, request) + obj = self._save_object(model_form, request, i) saved_objects.append(obj) else: # Raise model form errors diff --git a/netbox/translations/cs/LC_MESSAGES/django.po b/netbox/translations/cs/LC_MESSAGES/django.po index 18a542b780d..b755550e83b 100644 --- a/netbox/translations/cs/LC_MESSAGES/django.po +++ b/netbox/translations/cs/LC_MESSAGES/django.po @@ -12822,8 +12822,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Řádek {i}: Objekt s ID {id} neexistuje" +msgid "Object with ID {id} does not exist" +msgstr "Objekt s ID {id} neexistuje" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/da/LC_MESSAGES/django.po b/netbox/translations/da/LC_MESSAGES/django.po index 4bbe9c02cac..52b507cd007 100644 --- a/netbox/translations/da/LC_MESSAGES/django.po +++ b/netbox/translations/da/LC_MESSAGES/django.po @@ -12857,8 +12857,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Række {i}: Objekt med ID {id} findes ikke" +msgid "Object with ID {id} does not exist" +msgstr "Objekt med ID {id} findes ikke" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/de/LC_MESSAGES/django.po b/netbox/translations/de/LC_MESSAGES/django.po index 6f7a9c873c0..1f6f4b642dd 100644 --- a/netbox/translations/de/LC_MESSAGES/django.po +++ b/netbox/translations/de/LC_MESSAGES/django.po @@ -13055,8 +13055,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Reihe {i}: Objekt mit ID {id} existiert nicht" +msgid "Object with ID {id} does not exist" +msgstr "Objekt mit ID {id} existiert nicht" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 7104111c4de..d45c9b5337f 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -12513,7 +12513,7 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" +msgid "Object with ID {id} does not exist" msgstr "" #: netbox/netbox/views/generic/bulk_views.py:525 diff --git a/netbox/translations/es/LC_MESSAGES/django.po b/netbox/translations/es/LC_MESSAGES/django.po index fd19f2b2ade..9af4648ecca 100644 --- a/netbox/translations/es/LC_MESSAGES/django.po +++ b/netbox/translations/es/LC_MESSAGES/django.po @@ -12999,8 +12999,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Fila {i}: Objeto con ID {id} no existe" +msgid "Object with ID {id} does not exist" +msgstr "Objeto con ID {id} no existe" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/fr/LC_MESSAGES/django.po b/netbox/translations/fr/LC_MESSAGES/django.po index 1487fa2b5de..53166a755da 100644 --- a/netbox/translations/fr/LC_MESSAGES/django.po +++ b/netbox/translations/fr/LC_MESSAGES/django.po @@ -13041,8 +13041,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Rangée {i}: Objet avec identifiant {id} n'existe pas" +msgid "Object with ID {id} does not exist" +msgstr "Objet avec identifiant {id} n'existe pas" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/it/LC_MESSAGES/django.po b/netbox/translations/it/LC_MESSAGES/django.po index e1132e62070..9cd49edc04c 100644 --- a/netbox/translations/it/LC_MESSAGES/django.po +++ b/netbox/translations/it/LC_MESSAGES/django.po @@ -13033,8 +13033,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Fila {i}: Oggetto con ID {id} non esiste" +msgid "Object with ID {id} does not exist" +msgstr "Oggetto con ID {id} non esiste" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/ja/LC_MESSAGES/django.po b/netbox/translations/ja/LC_MESSAGES/django.po index 4ffdf7838c9..76db9e26138 100644 --- a/netbox/translations/ja/LC_MESSAGES/django.po +++ b/netbox/translations/ja/LC_MESSAGES/django.po @@ -12645,8 +12645,8 @@ msgstr "選択したエクスポートテンプレートをレンダリング中 #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "行 {i}: ID {id}のオブジェクトは存在しません" +msgid "Object with ID {id} does not exist" +msgstr "ID {id}のオブジェクトは存在しません" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/nl/LC_MESSAGES/django.po b/netbox/translations/nl/LC_MESSAGES/django.po index 0f7b297a465..2f820f94ca9 100644 --- a/netbox/translations/nl/LC_MESSAGES/django.po +++ b/netbox/translations/nl/LC_MESSAGES/django.po @@ -13000,8 +13000,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Rij {i}: Object met ID {id} bestaat niet" +msgid "Object with ID {id} does not exist" +msgstr "Object met ID {id} bestaat niet" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/pl/LC_MESSAGES/django.po b/netbox/translations/pl/LC_MESSAGES/django.po index 6f7ccee0254..be5b12c07fa 100644 --- a/netbox/translations/pl/LC_MESSAGES/django.po +++ b/netbox/translations/pl/LC_MESSAGES/django.po @@ -12920,8 +12920,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Wiersz {i}: Obiekt z identyfikatorem {id} nie istnieje" +msgid "Object with ID {id} does not exist" +msgstr "Obiekt z identyfikatorem {id} nie istnieje" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/pt/LC_MESSAGES/django.po b/netbox/translations/pt/LC_MESSAGES/django.po index 0e9f8e32c3a..a96ea2695f2 100644 --- a/netbox/translations/pt/LC_MESSAGES/django.po +++ b/netbox/translations/pt/LC_MESSAGES/django.po @@ -12944,8 +12944,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Linha {i}: Objeto com ID {id} não existe" +msgid "Object with ID {id} does not exist" +msgstr "Objeto com ID {id} não existe" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/ru/LC_MESSAGES/django.po b/netbox/translations/ru/LC_MESSAGES/django.po index 00946d05025..c1d6722f2aa 100644 --- a/netbox/translations/ru/LC_MESSAGES/django.po +++ b/netbox/translations/ru/LC_MESSAGES/django.po @@ -12939,8 +12939,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Ряд {i}: Объект с идентификатором {id} не существует" +msgid "Object with ID {id} does not exist" +msgstr "Объект с идентификатором {id} не существует" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/tr/LC_MESSAGES/django.po b/netbox/translations/tr/LC_MESSAGES/django.po index 24e98b629aa..254c994b36b 100644 --- a/netbox/translations/tr/LC_MESSAGES/django.po +++ b/netbox/translations/tr/LC_MESSAGES/django.po @@ -12835,8 +12835,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Satır {i}: Kimliği olan nesne {id} mevcut değil" +msgid "Object with ID {id} does not exist" +msgstr "Kimliği olan nesne {id} mevcut değil" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/uk/LC_MESSAGES/django.po b/netbox/translations/uk/LC_MESSAGES/django.po index edd3d9d831d..a3d8ecda8b8 100644 --- a/netbox/translations/uk/LC_MESSAGES/django.po +++ b/netbox/translations/uk/LC_MESSAGES/django.po @@ -12920,8 +12920,8 @@ msgstr "" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "Ряд {i}: Об'єкт з ідентифікатором {id} не існує" +msgid "Object with ID {id} does not exist" +msgstr "Об'єкт з ідентифікатором {id} не існує" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format diff --git a/netbox/translations/zh/LC_MESSAGES/django.po b/netbox/translations/zh/LC_MESSAGES/django.po index ce86225046e..524612e27e2 100644 --- a/netbox/translations/zh/LC_MESSAGES/django.po +++ b/netbox/translations/zh/LC_MESSAGES/django.po @@ -12622,8 +12622,8 @@ msgstr "渲染所选导出模板时出错 ({template}): {error}" #: netbox/netbox/views/generic/bulk_views.py:442 #, python-brace-format -msgid "Row {i}: Object with ID {id} does not exist" -msgstr "第{i}行: ID为{id}的对象不存在" +msgid "Object with ID {id} does not exist" +msgstr "ID为{id}的对象不存在" #: netbox/netbox/views/generic/bulk_views.py:525 #, python-brace-format