From c81429649c23035de0642131e600dccb77f2b984 Mon Sep 17 00:00:00 2001 From: breken-ai Date: Wed, 9 Sep 2026 00:35:18 -0700 Subject: [PATCH] Fixes #23150: Pre-select the parent object on 'create & add another' for non-cloning models ObjectEditView and ComponentCreateView build the add-another redirect from prepare_cloned_fields(), which returns an empty QueryDict for models that don't support cloning - the component templates (InterfaceTemplate, etc.). When adding components to a device type via 'Create & Add Another', the parent device type was dropped and the form came back blank. For non-cloning models, fall back to pre-selecting any parent object the instance links to (device type/module type/device/module). Models that support cloning (device components etc.) are unaffected and continue to use clone() as before. Fixes #23150 --- netbox/dcim/tests/test_views.py | 30 ++++++++++++++++++++++++++++++ netbox/utilities/querydict.py | 8 +++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 374ad4a40..2cd974aa5 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2425,6 +2425,22 @@ class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas model = InterfaceTemplate validation_excluded_fields = ('name', 'label') + def test_create_object_add_another_preserves_device_type(self): + # Regression test for #23150: "create & add another" must retain the + # parent device type, since component templates don't support cloning + self.add_permissions('dcim.add_interfacetemplate') + + device_type = DeviceType.objects.first() + data = dict(self.form_data) + data.update({ + '_addanother': True, + 'name': 'Interface Template Y', + }) + response = self.client.post(self._get_url('add'), data=post_data(data)) + self.assertHttpStatus(response, 302) + self.assertIn(f'device_type={device_type.pk}', response.headers['Location']) + + @classmethod def setUpTestData(cls): manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') @@ -3901,6 +3917,20 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): model = Interface validation_excluded_fields = ('name', 'label') + def test_bulk_create_add_another_preserves_device(self): + # Regression test for #23150: "add components" with "create & add another" + # must retain the parent device, since components don't support cloning + self.add_permissions('dcim.add_interface') + + device = Device.objects.first() + response = self.client.post( + self._get_url('add'), + data=post_data(dict(self.bulk_create_data, _addanother=True)), + ) + self.assertHttpStatus(response, 302) + self.assertIn(f'device={device.pk}', response.headers['Location']) + + @classmethod def setUpTestData(cls): device = create_test_device('Device 1') diff --git a/netbox/utilities/querydict.py b/netbox/utilities/querydict.py index 0d5702426..83ff6d78d 100644 --- a/netbox/utilities/querydict.py +++ b/netbox/utilities/querydict.py @@ -49,7 +49,13 @@ def prepare_cloned_fields(instance): """ # Generate the clone attributes from the instance if not issubclass(type(instance), CloningMixin): - return QueryDict(mutable=True) + # The model doesn't support cloning (e.g. device components & templates), but we + # can still pre-select its parent object when the user clicks "create & add another" + params = [] + for field_name in ('device_type', 'module_type', 'device', 'module'): + if value := getattr(instance, f'{field_name}_id', None): + params.append((field_name, value)) + return QueryDict(urlencode(params), mutable=True) attrs = instance.clone() # Prepare QueryDict parameters