Procházet zdrojové kódy

Closes #20776: Add changelog message to bulk rename process (#22100)

Add changelog message support to BulkRenameView for models that support
change logging. Introduce NetBoxModelBulkRenameForm as a changelog-aware
wrapper around the existing utilities.forms.BulkRenameForm, preserving the
existing import path while avoiding circular imports.

Set _changelog_message before saving renamed objects in both MPTT and
non-MPTT rename paths, and add a regression test to verify that the
submitted message is recorded on the resulting ObjectChange records.

Remove unused VMInterfaceBulkRenameForm and VirtualDiskBulkRenameForm,
along with their unused view references, since BulkRenameView constructs
its form dynamically.
Jason Novinger před 2 týdny
rodič
revize
c20e6dd2ee

+ 1 - 0
netbox/netbox/forms/__init__.py

@@ -1,5 +1,6 @@
 from .bulk_edit import *
 from .bulk_edit import *
 from .bulk_import import *
 from .bulk_import import *
+from .bulk_rename import *
 from .filtersets import *
 from .filtersets import *
 from .model_forms import *
 from .model_forms import *
 from .search import *
 from .search import *

+ 14 - 0
netbox/netbox/forms/bulk_rename.py

@@ -0,0 +1,14 @@
+from utilities.forms import BulkRenameForm
+
+from .mixins import ChangelogMessageMixin
+
+__all__ = (
+    'NetBoxModelBulkRenameForm',
+)
+
+
+class NetBoxModelBulkRenameForm(ChangelogMessageMixin, BulkRenameForm):
+    """
+    Extends BulkRenameForm with a changelog message field for NetBox models that support change logging.
+    """
+    pass

+ 11 - 2
netbox/netbox/views/generic/bulk_views.py

@@ -22,6 +22,7 @@ from core.models import ObjectType
 from core.signals import clear_events
 from core.signals import clear_events
 from extras.choices import CustomFieldUIEditableChoices
 from extras.choices import CustomFieldUIEditableChoices
 from extras.models import CustomField, ExportTemplate
 from extras.models import CustomField, ExportTemplate
+from netbox.forms.bulk_rename import NetBoxModelBulkRenameForm
 from netbox.models.features import ChangeLoggingMixin
 from netbox.models.features import ChangeLoggingMixin
 from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
 from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
 from utilities.error_handlers import handle_protectederror
 from utilities.error_handlers import handle_protectederror
@@ -881,8 +882,14 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
-        # Create a new Form class from BulkRenameForm
-        class _Form(BulkRenameForm):
+        # Use the changelog-aware form for models that support change logging
+        base_form = (
+            NetBoxModelBulkRenameForm
+            if issubclass(self.queryset.model, ChangeLoggingMixin)
+            else BulkRenameForm
+        )
+
+        class _Form(base_form):
             pk = ModelMultipleChoiceField(
             pk = ModelMultipleChoiceField(
                 queryset=self.queryset,
                 queryset=self.queryset,
                 widget=MultipleHiddenInput()
                 widget=MultipleHiddenInput()
@@ -940,10 +947,12 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
                                 with self.queryset.model.objects.delay_mptt_updates():
                                 with self.queryset.model.objects.delay_mptt_updates():
                                     for obj in selected_objects:
                                     for obj in selected_objects:
                                         setattr(obj, self.field_name, obj.new_name)
                                         setattr(obj, self.field_name, obj.new_name)
+                                        obj._changelog_message = form.cleaned_data.get('changelog_message', '')
                                         obj.save()
                                         obj.save()
                             else:
                             else:
                                 for obj in selected_objects:
                                 for obj in selected_objects:
                                     setattr(obj, self.field_name, obj.new_name)
                                     setattr(obj, self.field_name, obj.new_name)
+                                    obj._changelog_message = form.cleaned_data.get('changelog_message', '')
                                     obj.save()
                                     obj.save()
 
 
                             # Enforce constrained permissions
                             # Enforce constrained permissions

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

@@ -1049,6 +1049,41 @@ class ViewTestCases:
             for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
             for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
                 self.assertEqual(instance.name, f'{objects[i].name}X')
                 self.assertEqual(instance.name, f'{objects[i].name}X')
 
 
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+        def test_bulk_rename_objects_with_changelog_message(self):
+            if not issubclass(self.model, ChangeLoggingMixin):
+                self.skipTest("Model does not support change logging")
+            objects = self._get_queryset().all()[:3]
+            pk_list = [obj.pk for obj in objects]
+            data = {
+                'pk': pk_list,
+                '_apply': True,
+                'changelog_message': 'Bulk rename test message',
+            }
+            data.update(self.rename_data)
+
+            # Assign model-level permission
+            obj_perm = ObjectPermission(
+                name='Test permission',
+                actions=['change']
+            )
+            obj_perm.save()
+            obj_perm.users.add(self.user)
+            obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
+
+            self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
+
+            # Verify changelog message was recorded on each renamed object
+            object_type = ObjectType.objects.get_for_model(self.model)
+            for pk in pk_list:
+                oc = ObjectChange.objects.filter(
+                    changed_object_type=object_type,
+                    changed_object_id=pk,
+                    action=ObjectChangeActionChoices.ACTION_UPDATE,
+                ).order_by('-time').first()
+                self.assertIsNotNone(oc)
+                self.assertEqual(oc.message, 'Bulk rename test message')
+
         @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
         @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
         def test_bulk_rename_objects_with_constrained_permission(self):
         def test_bulk_rename_objects_with_constrained_permission(self):
             objects = self._get_queryset().all()[:3]
             objects = self._get_queryset().all()[:3]

+ 1 - 17
netbox/virtualization/forms/bulk_edit.py

@@ -11,7 +11,7 @@ from ipam.models import VLAN, VRF, VLANGroup, VLANTranslationPolicy
 from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
 from netbox.forms.mixins import OwnerMixin
 from netbox.forms.mixins import OwnerMixin
 from tenancy.models import Tenant
 from tenancy.models import Tenant
-from utilities.forms import BulkRenameForm, add_blank_choice
+from utilities.forms import add_blank_choice
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.utils import get_capacity_unit_label
 from utilities.forms.utils import get_capacity_unit_label
@@ -25,9 +25,7 @@ __all__ = (
     'ClusterGroupBulkEditForm',
     'ClusterGroupBulkEditForm',
     'ClusterTypeBulkEditForm',
     'ClusterTypeBulkEditForm',
     'VMInterfaceBulkEditForm',
     'VMInterfaceBulkEditForm',
-    'VMInterfaceBulkRenameForm',
     'VirtualDiskBulkEditForm',
     'VirtualDiskBulkEditForm',
-    'VirtualDiskBulkRenameForm',
     'VirtualMachineBulkEditForm',
     'VirtualMachineBulkEditForm',
     'VirtualMachineTypeBulkEditForm',
     'VirtualMachineTypeBulkEditForm',
 )
 )
@@ -343,13 +341,6 @@ class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
             self.fields['bridge'].widget.attrs['disabled'] = True
             self.fields['bridge'].widget.attrs['disabled'] = True
 
 
 
 
-class VMInterfaceBulkRenameForm(BulkRenameForm):
-    pk = forms.ModelMultipleChoiceField(
-        queryset=VMInterface.objects.all(),
-        widget=forms.MultipleHiddenInput()
-    )
-
-
 class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
 class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     virtual_machine = forms.ModelChoiceField(
     virtual_machine = forms.ModelChoiceField(
         label=_('Virtual machine'),
         label=_('Virtual machine'),
@@ -379,10 +370,3 @@ class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
 
 
         # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
         # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
         self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
         self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
-
-
-class VirtualDiskBulkRenameForm(BulkRenameForm):
-    pk = forms.ModelMultipleChoiceField(
-        queryset=VirtualDisk.objects.all(),
-        widget=forms.MultipleHiddenInput()
-    )

+ 0 - 2
netbox/virtualization/views.py

@@ -744,7 +744,6 @@ class VMInterfaceBulkEditView(generic.BulkEditView):
 class VMInterfaceBulkRenameView(generic.BulkRenameView):
 class VMInterfaceBulkRenameView(generic.BulkRenameView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
     filterset = filtersets.VMInterfaceFilterSet
     filterset = filtersets.VMInterfaceFilterSet
-    form = forms.VMInterfaceBulkRenameForm
 
 
 
 
 @register_model_view(VMInterface, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VMInterface, 'bulk_delete', path='delete', detail=False)
@@ -817,7 +816,6 @@ class VirtualDiskBulkEditView(generic.BulkEditView):
 class VirtualDiskBulkRenameView(generic.BulkRenameView):
 class VirtualDiskBulkRenameView(generic.BulkRenameView):
     queryset = VirtualDisk.objects.all()
     queryset = VirtualDisk.objects.all()
     filterset = filtersets.VirtualDiskFilterSet
     filterset = filtersets.VirtualDiskFilterSet
-    form = forms.VirtualDiskBulkRenameForm
 
 
 
 
 @register_model_view(VirtualDisk, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VirtualDisk, 'bulk_delete', path='delete', detail=False)