mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-09 01:49:35 -06:00
Merge pull request #20717 from m-hau/bugfix/related-object-validation
Fixes: #20670: Related Object Validation
This commit is contained in:
commit
730d73042d
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user