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.
This commit is contained in:
Jason Novinger 2026-05-07 11:23:00 -05:00 committed by GitHub
parent f2187ceb8f
commit c20e6dd2ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 62 additions and 21 deletions

View File

@ -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 *

View File

@ -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

View File

@ -22,6 +22,7 @@ from core.models import ObjectType
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 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
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

View File

@ -1049,6 +1049,41 @@ class ViewTestCases:
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]

View File

@ -11,7 +11,7 @@ from ipam.models import VLAN, VRF, VLANGroup, VLANTranslationPolicy
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 @@ __all__ = (
'ClusterGroupBulkEditForm',
'ClusterTypeBulkEditForm',
'VMInterfaceBulkEditForm',
'VMInterfaceBulkRenameForm',
'VirtualDiskBulkEditForm',
'VirtualDiskBulkRenameForm',
'VirtualMachineBulkEditForm',
'VirtualMachineTypeBulkEditForm',
)
@ -343,13 +341,6 @@ class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
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 @@ class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
# 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()
)

View File

@ -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)