diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index fe5e5f7d389..e61824035a6 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,5 +1,6 @@ from .bulk_edit import * from .bulk_import import * +from .bulk_rename import * from .filtersets import * from .model_forms import * from .search import * diff --git a/netbox/netbox/forms/bulk_rename.py b/netbox/netbox/forms/bulk_rename.py new file mode 100644 index 00000000000..975466f6a83 --- /dev/null +++ b/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 diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8f0a98b5054..41eeaba88dc 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -22,6 +22,7 @@ from core.signals import clear_events from extras.choices import CustomFieldUIEditableChoices from extras.models import CustomField, ExportTemplate +from netbox.forms.bulk_rename import NetBoxModelBulkRenameForm from netbox.models.features import ChangeLoggingMixin from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from utilities.error_handlers import handle_protectederror @@ -881,8 +882,14 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): def __init__(self, *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( queryset=self.queryset, widget=MultipleHiddenInput() @@ -940,10 +947,12 @@ def post(self, request): with self.queryset.model.objects.delay_mptt_updates(): for obj in selected_objects: setattr(obj, self.field_name, obj.new_name) + obj._changelog_message = form.cleaned_data.get('changelog_message', '') obj.save() else: for obj in selected_objects: setattr(obj, self.field_name, obj.new_name) + obj._changelog_message = form.cleaned_data.get('changelog_message', '') obj.save() # Enforce constrained permissions diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 6abea2f984a..38edc8aa91b 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -1049,6 +1049,41 @@ def test_bulk_rename_objects_with_permission(self): for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)): 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=['*']) def test_bulk_rename_objects_with_constrained_permission(self): objects = self._get_queryset().all()[:3] diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index f51e51e6041..6f0feaf0ea0 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -11,7 +11,7 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm from netbox.forms.mixins import OwnerMixin 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.rendering import FieldSet from utilities.forms.utils import get_capacity_unit_label @@ -25,9 +25,7 @@ 'ClusterGroupBulkEditForm', 'ClusterTypeBulkEditForm', 'VMInterfaceBulkEditForm', - 'VMInterfaceBulkRenameForm', 'VirtualDiskBulkEditForm', - 'VirtualDiskBulkRenameForm', 'VirtualMachineBulkEditForm', 'VirtualMachineTypeBulkEditForm', ) @@ -343,13 +341,6 @@ def __init__(self, *args, **kwargs): self.fields['bridge'].widget.attrs['disabled'] = True -class VMInterfaceBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=VMInterface.objects.all(), - widget=forms.MultipleHiddenInput() - ) - - class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm): virtual_machine = forms.ModelChoiceField( label=_('Virtual machine'), @@ -379,10 +370,3 @@ def __init__(self, *args, **kwargs): # 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)) - - -class VirtualDiskBulkRenameForm(BulkRenameForm): - pk = forms.ModelMultipleChoiceField( - queryset=VirtualDisk.objects.all(), - widget=forms.MultipleHiddenInput() - ) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index d6a758dad69..a90a20a7313 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -744,7 +744,6 @@ class VMInterfaceBulkEditView(generic.BulkEditView): class VMInterfaceBulkRenameView(generic.BulkRenameView): queryset = VMInterface.objects.all() filterset = filtersets.VMInterfaceFilterSet - form = forms.VMInterfaceBulkRenameForm @register_model_view(VMInterface, 'bulk_delete', path='delete', detail=False) @@ -817,7 +816,6 @@ class VirtualDiskBulkEditView(generic.BulkEditView): class VirtualDiskBulkRenameView(generic.BulkRenameView): queryset = VirtualDisk.objects.all() filterset = filtersets.VirtualDiskFilterSet - form = forms.VirtualDiskBulkRenameForm @register_model_view(VirtualDisk, 'bulk_delete', path='delete', detail=False)