Просмотр исходного кода

fix(dcim): Preserve parent assignment in "Add Another" redirect

Retain parent object IDs (device, module, device_type, module_type,
virtual_machine) when redirecting after "Create & Add Another" instead
of only cloning the created object's fields.

Fixes #23150
Martin Hauser 4 часов назад
Родитель
Сommit
1d5f020e64

+ 26 - 1
netbox/dcim/tests/test_views.py

@@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo
 import yaml
 from django.contrib.contenttypes.models import ContentType
 from django.db import connection
-from django.http import StreamingHttpResponse
+from django.http import QueryDict, StreamingHttpResponse
 from django.test import override_settings, tag
 from django.test.utils import CaptureQueriesContext
 from django.urls import reverse
@@ -2481,6 +2481,31 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
             'mgmt_only': True,
         }
 
+    def test_addanother_preserves_saved_parent(self):
+        """The Add Another redirect names the saved module type, not the device type the form was opened with."""
+        device_type = DeviceType.objects.get(pk=self.form_data['device_type'])
+        module_type = ModuleType.objects.create(manufacturer=device_type.manufacturer, model='Module Type 1')
+        return_url = reverse('dcim:moduletype_interfaces', kwargs={'pk': module_type.pk})
+
+        self.add_permissions('dcim.add_interfacetemplate')
+
+        response = self.client.post(
+            f"{self._get_url('add')}?device_type={device_type.pk}&return_url={return_url}",
+            post_data({
+                'module_type': module_type.pk,
+                'name': 'Interface Template [7-8]',
+                'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
+                '_addanother': True,
+            })
+        )
+        self.assertHttpStatus(response, 302)
+        self.assertEqual(InterfaceTemplate.objects.filter(module_type=module_type).count(), 2)
+
+        params = QueryDict(response['Location'].partition('?')[2])
+        self.assertEqual(params.get('module_type'), str(module_type.pk))
+        self.assertIsNone(params.get('device_type'))
+        self.assertEqual(params.get('return_url'), return_url)
+
 
 class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = FrontPortTemplate

+ 9 - 1
netbox/netbox/views/generic/object_views.py

@@ -539,6 +539,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
     form = None
     model_form = None
 
+    # Parent assignments carried into the "Add Another" redirect, which cloning cannot supply
+    parent_fields = ('device', 'module', 'device_type', 'module_type', 'virtual_machine')
+
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'add')
 
@@ -623,8 +626,13 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
 
                         # Redirect user on success
                         if '_addanother' in request.POST:
+                            # A name pattern may create several components, so follow the last one
+                            new_obj = new_objs[-1]
                             redirect_url = request.path
-                            params = prepare_cloned_fields(new_objs[-1])
+                            params = prepare_cloned_fields(new_obj)
+                            for field_name in self.parent_fields:
+                                if (parent_id := getattr(new_obj, f'{field_name}_id', None)) is not None:
+                                    params[field_name] = parent_id
                             if 'return_url' in request.GET:
                                 params['return_url'] = request.GET.get('return_url')
                             if params:

+ 32 - 0
netbox/utilities/testing/views.py

@@ -3,6 +3,7 @@ import csv
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
+from django.http import QueryDict
 from django.test import override_settings
 from django.urls import reverse
 from django.utils.translation import gettext as _
@@ -616,6 +617,37 @@ class ViewTestCases:
             for instance in self._get_queryset().order_by('-pk')[: self.bulk_create_count]:
                 self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
 
+        def test_create_multiple_objects_addanother(self):
+            """The "Create & Add Another" redirect retains the parent the objects were created under."""
+            parent_fields = ('device', 'module', 'device_type', 'module_type', 'virtual_machine')
+            parents = {
+                field: getattr(value, 'pk', value) for field, value in self.bulk_create_data.items()
+                if field in parent_fields and value
+            }
+            if not parents:
+                self.skipTest("bulk_create_data declares no parent assignment")
+
+            # Assign non-constrained permission
+            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))
+
+            response = self.client.post(
+                path=self._get_url('add'),
+                data=post_data({**self.bulk_create_data, '_addanother': True}),
+            )
+            self.assertHttpStatus(response, 302)
+
+            path, _sep, query = response['Location'].partition('?')
+            self.assertEqual(path, self._get_url('add'))
+            params = QueryDict(query)
+            for field, value in parents.items():
+                self.assertEqual(params.getlist(field), [str(value)])
+
     class BulkImportObjectsViewTestCase(ModelViewTestCase):
         """
         Create multiple instances from imported data.