Przeglądaj źródła

fix(dcim): Add bulk edit support for Module Bay Types

Adds `add_module_bay_types` and `remove_module_bay_types` fields to
ModuleBayTemplate bulk edit form with fieldsets. Implements per-object
delta operations in post_save_operations to preserve existing type
assignments while adding/removing specified types.

Fixes #22961
Martin Hauser 3 dni temu
rodzic
commit
cff012ac9c

+ 14 - 0
netbox/dcim/forms/bulk_edit.py

@@ -1601,7 +1601,21 @@ class ModuleBayTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         required=False,
         required=False,
         widget=BulkEditNullBooleanSelect,
         widget=BulkEditNullBooleanSelect,
     )
     )
+    add_module_bay_types = DynamicModelMultipleChoiceField(
+        label=_('Add bay types'),
+        queryset=ModuleBayType.objects.all(),
+        required=False,
+    )
+    remove_module_bay_types = DynamicModelMultipleChoiceField(
+        label=_('Remove bay types'),
+        queryset=ModuleBayType.objects.all(),
+        required=False,
+    )
 
 
+    fieldsets = (
+        FieldSet('label', 'enabled', 'description'),
+        FieldSet('add_module_bay_types', 'remove_module_bay_types', name=_('Bay Types')),
+    )
     nullable_fields = ('label', 'position', 'description')
     nullable_fields = ('label', 'position', 'description')
 
 
 
 

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

@@ -1853,6 +1853,47 @@ module-bays:
         self.assertGreater(len(rows), 1)
         self.assertGreater(len(rows), 1)
         self.assertEqual(len(rows) - 1, ModuleType.objects.count())
         self.assertEqual(len(rows) - 1, ModuleType.objects.count())
 
 
+    @tag('regression')  # Issue #22961
+    def test_bulk_edit_module_bay_types(self):
+        """
+        Bulk edit adds and removes bay types per object, leaving each object's other assignments intact.
+        """
+        self.add_permissions(
+            'dcim.view_moduletype',
+            'dcim.change_moduletype',
+            'dcim.view_modulebaytype',
+        )
+
+        kept_types = (
+            ModuleBayType.objects.create(name='Kept Type 1', slug='kept-type-1'),
+            ModuleBayType.objects.create(name='Kept Type 2', slug='kept-type-2'),
+        )
+        removed_type = ModuleBayType.objects.create(name='Removed Type', slug='removed-type')
+        added_type = ModuleBayType.objects.create(name='Added Type', slug='added-type')
+
+        # Differing starting sets prove a per-object delta rather than a wholesale replacement
+        module_types = list(
+            ModuleType.objects.filter(model__in=('Module Type 2', 'Module Type 3')).order_by('pk')
+        )
+        for module_type, kept_type in zip(module_types, kept_types):
+            module_type.module_bay_types.set((kept_type, removed_type))
+
+        pk_list = [module_type.pk for module_type in module_types]
+
+        response = self.client.post(self._get_url('bulk_edit'), post_data({
+            'pk': pk_list,
+            'add_module_bay_types': [added_type.pk],
+            'remove_module_bay_types': [removed_type.pk],
+            '_apply': True,
+        }))
+        self.assertHttpStatus(response, 302)
+
+        for module_type, kept_type in zip(module_types, kept_types):
+            self.assertEqual(
+                set(module_type.module_bay_types.values_list('pk', flat=True)),
+                {kept_type.pk, added_type.pk},
+            )
+
 
 
 class ModuleTypeProfileTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 class ModuleTypeProfileTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
     model = ModuleTypeProfile
     model = ModuleTypeProfile
@@ -2274,6 +2315,56 @@ class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCas
             'description': 'Foo bar',
             'description': 'Foo bar',
         }
         }
 
 
+    @tag('regression')  # Issue #22961
+    def test_bulk_edit_module_bay_types(self):
+        """
+        Bulk edit adds and removes bay types per object, leaving each object's other assignments intact.
+        """
+        self.add_permissions(
+            'dcim.view_modulebaytemplate',
+            'dcim.change_modulebaytemplate',
+            'dcim.view_modulebaytype',
+        )
+
+        kept_types = (
+            ModuleBayType.objects.create(name='Kept Type 1', slug='kept-type-1'),
+            ModuleBayType.objects.create(name='Kept Type 2', slug='kept-type-2'),
+        )
+        removed_type = ModuleBayType.objects.create(name='Removed Type', slug='removed-type')
+        added_type = ModuleBayType.objects.create(name='Added Type', slug='added-type')
+
+        # Differing starting sets prove a per-object delta rather than a wholesale replacement
+        module_bay_templates = list(ModuleBayTemplate.objects.order_by('pk')[:2])
+        for module_bay_template, kept_type in zip(module_bay_templates, kept_types):
+            module_bay_template.module_bay_types.set((kept_type, removed_type))
+
+        pk_list = [module_bay_template.pk for module_bay_template in module_bay_templates]
+
+        # The reported symptom: both controls must appear on the bulk-edit form
+        response = self.client.post(self._get_url('bulk_edit'), {'pk': pk_list})
+        self.assertHttpStatus(response, 200)
+        self.assertContains(response, 'Add bay types')
+        self.assertContains(response, 'Remove bay types')
+
+        # Declaring fieldsets switches the template to its grouped branch, which must keep
+        # annotating the nullable fields
+        self.assertContains(response, 'id="nullify_id_label"')
+        self.assertContains(response, 'id="nullify_id_description"')
+
+        response = self.client.post(self._get_url('bulk_edit'), post_data({
+            'pk': pk_list,
+            'add_module_bay_types': [added_type.pk],
+            'remove_module_bay_types': [removed_type.pk],
+            '_apply': True,
+        }))
+        self.assertHttpStatus(response, 302)
+
+        for module_bay_template, kept_type in zip(module_bay_templates, kept_types):
+            self.assertEqual(
+                set(module_bay_template.module_bay_types.values_list('pk', flat=True)),
+                {kept_type.pk, added_type.pk},
+            )
+
 
 
 class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
 class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
     model = DeviceBayTemplate
     model = DeviceBayTemplate
@@ -4177,6 +4268,45 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 
 
         self.assertEqual(initial_count + 8, self._get_queryset().count())
         self.assertEqual(initial_count + 8, self._get_queryset().count())
 
 
+    @tag('regression')  # Issue #22961
+    def test_bulk_edit_module_bay_types(self):
+        """
+        Bulk edit adds and removes bay types per object, leaving each object's other assignments intact.
+        """
+        self.add_permissions(
+            'dcim.view_modulebay',
+            'dcim.change_modulebay',
+            'dcim.view_modulebaytype',
+        )
+
+        kept_types = (
+            ModuleBayType.objects.create(name='Kept Type 1', slug='kept-type-1'),
+            ModuleBayType.objects.create(name='Kept Type 2', slug='kept-type-2'),
+        )
+        removed_type = ModuleBayType.objects.create(name='Removed Type', slug='removed-type')
+        added_type = ModuleBayType.objects.create(name='Added Type', slug='added-type')
+
+        # Differing starting sets prove a per-object delta rather than a wholesale replacement
+        module_bays = list(ModuleBay.objects.order_by('pk')[:2])
+        for module_bay, kept_type in zip(module_bays, kept_types):
+            module_bay.module_bay_types.set((kept_type, removed_type))
+
+        pk_list = [module_bay.pk for module_bay in module_bays]
+
+        response = self.client.post(self._get_url('bulk_edit'), post_data({
+            'pk': pk_list,
+            'add_module_bay_types': [added_type.pk],
+            'remove_module_bay_types': [removed_type.pk],
+            '_apply': True,
+        }))
+        self.assertHttpStatus(response, 302)
+
+        for module_bay, kept_type in zip(module_bays, kept_types):
+            self.assertEqual(
+                set(module_bay.module_bay_types.values_list('pk', flat=True)),
+                {kept_type.pk, added_type.pk},
+            )
+
 
 
 class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
 class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = DeviceBay
     model = DeviceBay

+ 11 - 0
netbox/dcim/views.py

@@ -2626,6 +2626,17 @@ class ModuleBayTemplateBulkEditView(generic.BulkEditView):
     table = tables.ModuleBayTemplateTable
     table = tables.ModuleBayTemplateTable
     form = forms.ModuleBayTemplateBulkEditForm
     form = forms.ModuleBayTemplateBulkEditForm
 
 
+    def post_save_operations(self, form, obj):
+        # Unlike the ModuleType and ModuleBay editors, no compatibility warning: a template
+        # has no installed module to invalidate.
+        super().post_save_operations(form, obj)
+        add = form.cleaned_data.get('add_module_bay_types')
+        remove = form.cleaned_data.get('remove_module_bay_types')
+        if add:
+            obj.module_bay_types.add(*add)
+        if remove:
+            obj.module_bay_types.remove(*remove)
+
 
 
 @register_model_view(ModuleBayTemplate, 'bulk_rename', path='rename', detail=False)
 @register_model_view(ModuleBayTemplate, 'bulk_rename', path='rename', detail=False)
 class ModuleBayTemplateBulkRenameView(generic.BulkRenameView):
 class ModuleBayTemplateBulkRenameView(generic.BulkRenameView):