Преглед изворни кода

Merge pull request #20717 from m-hau/bugfix/related-object-validation

Fixes: #20670: Related Object Validation
bctiemann пре 3 месеци
родитељ
комит
730d73042d

+ 125 - 0
netbox/dcim/tests/test_views.py

@@ -986,6 +986,131 @@ inventory-items:
         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')

+ 28 - 7
netbox/netbox/views/generic/bulk_views.py

@@ -323,7 +323,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
 
 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
         # 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
                 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 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
             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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 1 - 1
netbox/translations/en/LC_MESSAGES/django.po

@@ -12541,7 +12541,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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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

+ 2 - 2
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