Browse Source

Closes #19231: Add bulk renaming support for all models (#19795)

* Closes #19231: Add bulk renaming support for all models

* Introduce a template filter for getattr()

* Extend BulkRenameView to support arbitrary field names

* Address bulk renaming support for remaining models

* Bulk rename URL resolution should fail silently

* Update documentation

* Fix bulk button rendering for HTMX requests
Jeremy Stretch 7 months ago
parent
commit
ce12de8b6d

+ 5 - 0
docs/plugins/development/views.md

@@ -64,6 +64,7 @@ Generic view classes (documented below) facilitate common operations, such as cr
 | `ObjectListView`     | View a list of objects                                 |
 | `ObjectListView`     | View a list of objects                                 |
 | `BulkImportView`     | Import a set of new objects                            |
 | `BulkImportView`     | Import a set of new objects                            |
 | `BulkEditView`       | Edit multiple objects                                  |
 | `BulkEditView`       | Edit multiple objects                                  |
+| `BulkRenameView`     | Rename multiple objects                                |
 | `BulkDeleteView`     | Delete multiple objects                                |
 | `BulkDeleteView`     | Delete multiple objects                                |
 
 
 !!! warning
 !!! warning
@@ -171,6 +172,10 @@ Below are the class definitions for NetBox's multi-object views. These views han
     options:
     options:
       members: false
       members: false
 
 
+::: netbox.views.generic.BulkRenameView
+    options:
+      members: false
+
 ::: netbox.views.generic.BulkDeleteView
 ::: netbox.views.generic.BulkDeleteView
     options:
     options:
       members:
       members:

+ 46 - 0
netbox/circuits/views.py

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
 
 
 from dcim.views import PathTraceView
 from dcim.views import PathTraceView
 from ipam.models import ASN
 from ipam.models import ASN
+from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
 from netbox.views import generic
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.query import count_related
 from utilities.query import count_related
@@ -79,6 +80,11 @@ class ProviderBulkEditView(generic.BulkEditView):
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
 
 
 
 
+@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
+class ProviderBulkRenameView(generic.BulkRenameView):
+    queryset = Provider.objects.all()
+
+
 @register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
 class ProviderBulkDeleteView(generic.BulkDeleteView):
 class ProviderBulkDeleteView(generic.BulkDeleteView):
     queryset = Provider.objects.annotate(
     queryset = Provider.objects.annotate(
@@ -141,6 +147,11 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
     form = forms.ProviderAccountBulkEditForm
     form = forms.ProviderAccountBulkEditForm
 
 
 
 
+@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
+class ProviderAccountBulkRenameView(generic.BulkRenameView):
+    queryset = ProviderAccount.objects.all()
+
+
 @register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
 class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
 class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
     queryset = ProviderAccount.objects.annotate(
     queryset = ProviderAccount.objects.annotate(
@@ -212,6 +223,11 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
     form = forms.ProviderNetworkBulkEditForm
     form = forms.ProviderNetworkBulkEditForm
 
 
 
 
+@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
+class ProviderNetworkBulkRenameView(generic.BulkRenameView):
+    queryset = ProviderNetwork.objects.all()
+
+
 @register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
 class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
 class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
     queryset = ProviderNetwork.objects.all()
     queryset = ProviderNetwork.objects.all()
@@ -271,6 +287,11 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
     form = forms.CircuitTypeBulkEditForm
     form = forms.CircuitTypeBulkEditForm
 
 
 
 
+@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
+class CircuitTypeBulkRenameView(generic.BulkRenameView):
+    queryset = CircuitType.objects.all()
+
+
 @register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
 class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
 class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = CircuitType.objects.annotate(
     queryset = CircuitType.objects.annotate(
@@ -337,6 +358,12 @@ class CircuitBulkEditView(generic.BulkEditView):
     form = forms.CircuitBulkEditForm
     form = forms.CircuitBulkEditForm
 
 
 
 
+@register_model_view(Circuit, 'bulk_rename', path='rename', detail=False)
+class CircuitBulkRenameView(generic.BulkRenameView):
+    queryset = Circuit.objects.all()
+    field_name = 'cid'
+
+
 @register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
 class CircuitBulkDeleteView(generic.BulkDeleteView):
 class CircuitBulkDeleteView(generic.BulkDeleteView):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
@@ -432,6 +459,7 @@ class CircuitTerminationListView(generic.ObjectListView):
     filterset = filtersets.CircuitTerminationFilterSet
     filterset = filtersets.CircuitTerminationFilterSet
     filterset_form = forms.CircuitTerminationFilterForm
     filterset_form = forms.CircuitTerminationFilterForm
     table = tables.CircuitTerminationTable
     table = tables.CircuitTerminationTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(CircuitTermination)
 @register_model_view(CircuitTermination)
@@ -526,6 +554,11 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
     form = forms.CircuitGroupBulkEditForm
     form = forms.CircuitGroupBulkEditForm
 
 
 
 
+@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
+class CircuitGroupBulkRenameView(generic.BulkRenameView):
+    queryset = CircuitGroup.objects.all()
+
+
 @register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
 class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
 class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = CircuitGroup.objects.all()
     queryset = CircuitGroup.objects.all()
@@ -543,6 +576,7 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
     filterset = filtersets.CircuitGroupAssignmentFilterSet
     filterset = filtersets.CircuitGroupAssignmentFilterSet
     filterset_form = forms.CircuitGroupAssignmentFilterForm
     filterset_form = forms.CircuitGroupAssignmentFilterForm
     table = tables.CircuitGroupAssignmentTable
     table = tables.CircuitGroupAssignmentTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(CircuitGroupAssignment)
 @register_model_view(CircuitGroupAssignment)
@@ -635,6 +669,11 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
     form = forms.VirtualCircuitTypeBulkEditForm
     form = forms.VirtualCircuitTypeBulkEditForm
 
 
 
 
+@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
+class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
+    queryset = VirtualCircuitType.objects.all()
+
+
 @register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
 class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
 class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualCircuitType.objects.annotate(
     queryset = VirtualCircuitType.objects.annotate(
@@ -697,6 +736,12 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
     form = forms.VirtualCircuitBulkEditForm
     form = forms.VirtualCircuitBulkEditForm
 
 
 
 
+@register_model_view(VirtualCircuit, 'bulk_rename', path='rename', detail=False)
+class VirtualCircuitulkRenameView(generic.BulkRenameView):
+    queryset = VirtualCircuit.objects.all()
+    field_name = 'cid'
+
+
 class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
 class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualCircuit.objects.annotate(
     queryset = VirtualCircuit.objects.annotate(
         termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
         termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
@@ -714,6 +759,7 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
     filterset = filtersets.VirtualCircuitTerminationFilterSet
     filterset = filtersets.VirtualCircuitTerminationFilterSet
     filterset_form = forms.VirtualCircuitTerminationFilterForm
     filterset_form = forms.VirtualCircuitTerminationFilterForm
     table = tables.VirtualCircuitTerminationTable
     table = tables.VirtualCircuitTerminationTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(VirtualCircuitTermination)
 @register_model_view(VirtualCircuitTermination)

+ 5 - 0
netbox/core/views.py

@@ -120,6 +120,11 @@ class DataSourceBulkEditView(generic.BulkEditView):
     form = forms.DataSourceBulkEditForm
     form = forms.DataSourceBulkEditForm
 
 
 
 
+@register_model_view(DataSource, 'bulk_rename', path='rename', detail=False)
+class DataSourceBulkRenameView(generic.BulkRenameView):
+    queryset = DataSource.objects.all()
+
+
 @register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
 @register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
 class DataSourceBulkDeleteView(generic.BulkDeleteView):
 class DataSourceBulkDeleteView(generic.BulkDeleteView):
     queryset = DataSource.objects.annotate(
     queryset = DataSource.objects.annotate(

+ 108 - 16
netbox/dcim/views.py

@@ -292,6 +292,11 @@ class RegionBulkEditView(generic.BulkEditView):
     form = forms.RegionBulkEditForm
     form = forms.RegionBulkEditForm
 
 
 
 
+@register_model_view(Region, 'bulk_rename', path='rename', detail=False)
+class RegionBulkRenameView(generic.BulkRenameView):
+    queryset = Region.objects.all()
+
+
 @register_model_view(Region, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Region, 'bulk_delete', path='delete', detail=False)
 class RegionBulkDeleteView(generic.BulkDeleteView):
 class RegionBulkDeleteView(generic.BulkDeleteView):
     queryset = Region.objects.add_related_count(
     queryset = Region.objects.add_related_count(
@@ -418,6 +423,11 @@ class SiteGroupBulkEditView(generic.BulkEditView):
     form = forms.SiteGroupBulkEditForm
     form = forms.SiteGroupBulkEditForm
 
 
 
 
+@register_model_view(SiteGroup, 'bulk_rename', path='rename', detail=False)
+class SiteGroupBulkRenameView(generic.BulkRenameView):
+    queryset = SiteGroup.objects.all()
+
+
 @register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(SiteGroup, 'bulk_delete', path='delete', detail=False)
 class SiteGroupBulkDeleteView(generic.BulkDeleteView):
 class SiteGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = SiteGroup.objects.add_related_count(
     queryset = SiteGroup.objects.add_related_count(
@@ -503,6 +513,11 @@ class SiteBulkEditView(generic.BulkEditView):
     form = forms.SiteBulkEditForm
     form = forms.SiteBulkEditForm
 
 
 
 
+@register_model_view(Site, 'bulk_rename', path='rename', detail=False)
+class SiteBulkRenameView(generic.BulkRenameView):
+    queryset = Site.objects.all()
+
+
 @register_model_view(Site, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Site, 'bulk_delete', path='delete', detail=False)
 class SiteBulkDeleteView(generic.BulkDeleteView):
 class SiteBulkDeleteView(generic.BulkDeleteView):
     queryset = Site.objects.all()
     queryset = Site.objects.all()
@@ -607,6 +622,11 @@ class LocationBulkEditView(generic.BulkEditView):
     form = forms.LocationBulkEditForm
     form = forms.LocationBulkEditForm
 
 
 
 
+@register_model_view(Location, 'bulk_rename', path='rename', detail=False)
+class LocationBulkRenameView(generic.BulkRenameView):
+    queryset = Location.objects.all()
+
+
 @register_model_view(Location, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Location, 'bulk_delete', path='delete', detail=False)
 class LocationBulkDeleteView(generic.BulkDeleteView):
 class LocationBulkDeleteView(generic.BulkDeleteView):
     queryset = Location.objects.add_related_count(
     queryset = Location.objects.add_related_count(
@@ -672,6 +692,11 @@ class RackRoleBulkEditView(generic.BulkEditView):
     form = forms.RackRoleBulkEditForm
     form = forms.RackRoleBulkEditForm
 
 
 
 
+@register_model_view(RackRole, 'bulk_rename', path='rename', detail=False)
+class RackRoleBulkRenameView(generic.BulkRenameView):
+    queryset = RackRole.objects.all()
+
+
 @register_model_view(RackRole, 'bulk_delete', path='delete', detail=False)
 @register_model_view(RackRole, 'bulk_delete', path='delete', detail=False)
 class RackRoleBulkDeleteView(generic.BulkDeleteView):
 class RackRoleBulkDeleteView(generic.BulkDeleteView):
     queryset = RackRole.objects.annotate(
     queryset = RackRole.objects.annotate(
@@ -731,6 +756,12 @@ class RackTypeBulkEditView(generic.BulkEditView):
     form = forms.RackTypeBulkEditForm
     form = forms.RackTypeBulkEditForm
 
 
 
 
+@register_model_view(RackType, 'bulk_rename', path='rename', detail=False)
+class RackTypeBulkRenameView(generic.BulkRenameView):
+    queryset = RackType.objects.all()
+    field_name = 'model'
+
+
 @register_model_view(RackType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(RackType, 'bulk_delete', path='delete', detail=False)
 class RackTypeBulkDeleteView(generic.BulkDeleteView):
 class RackTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = RackType.objects.all()
     queryset = RackType.objects.all()
@@ -910,6 +941,11 @@ class RackBulkEditView(generic.BulkEditView):
     form = forms.RackBulkEditForm
     form = forms.RackBulkEditForm
 
 
 
 
+@register_model_view(Rack, 'bulk_rename', path='rename', detail=False)
+class RackBulkRenameView(generic.BulkRenameView):
+    queryset = Rack.objects.all()
+
+
 @register_model_view(Rack, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Rack, 'bulk_delete', path='delete', detail=False)
 class RackBulkDeleteView(generic.BulkDeleteView):
 class RackBulkDeleteView(generic.BulkDeleteView):
     queryset = Rack.objects.all()
     queryset = Rack.objects.all()
@@ -927,6 +963,7 @@ class RackReservationListView(generic.ObjectListView):
     filterset = filtersets.RackReservationFilterSet
     filterset = filtersets.RackReservationFilterSet
     filterset_form = forms.RackReservationFilterForm
     filterset_form = forms.RackReservationFilterForm
     table = tables.RackReservationTable
     table = tables.RackReservationTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(RackReservation)
 @register_model_view(RackReservation)
@@ -1043,6 +1080,11 @@ class ManufacturerBulkEditView(generic.BulkEditView):
     form = forms.ManufacturerBulkEditForm
     form = forms.ManufacturerBulkEditForm
 
 
 
 
+@register_model_view(Manufacturer, 'bulk_rename', path='rename', detail=False)
+class ManufacturerBulkRenameView(generic.BulkRenameView):
+    queryset = Manufacturer.objects.all()
+
+
 @register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Manufacturer, 'bulk_delete', path='delete', detail=False)
 class ManufacturerBulkDeleteView(generic.BulkDeleteView):
 class ManufacturerBulkDeleteView(generic.BulkDeleteView):
     queryset = Manufacturer.objects.annotate(
     queryset = Manufacturer.objects.annotate(
@@ -1290,6 +1332,12 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
     form = forms.DeviceTypeBulkEditForm
     form = forms.DeviceTypeBulkEditForm
 
 
 
 
+@register_model_view(DeviceType, 'bulk_rename', path='rename', detail=False)
+class DeviceTypeBulkRenameView(generic.BulkRenameView):
+    queryset = DeviceType.objects.all()
+    field_name = 'model'
+
+
 @register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(DeviceType, 'bulk_delete', path='delete', detail=False)
 class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
 class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = DeviceType.objects.annotate(
     queryset = DeviceType.objects.annotate(
@@ -1346,6 +1394,11 @@ class ModuleTypeProfileBulkEditView(generic.BulkEditView):
     form = forms.ModuleTypeProfileBulkEditForm
     form = forms.ModuleTypeProfileBulkEditForm
 
 
 
 
+@register_model_view(ModuleTypeProfile, 'bulk_rename', path='rename', detail=False)
+class ModuleTypeProfileBulkRenameView(generic.BulkRenameView):
+    queryset = ModuleTypeProfile.objects.all()
+
+
 @register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ModuleTypeProfile, 'bulk_delete', path='delete', detail=False)
 class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
 class ModuleTypeProfileBulkDeleteView(generic.BulkDeleteView):
     queryset = ModuleTypeProfile.objects.annotate(
     queryset = ModuleTypeProfile.objects.annotate(
@@ -1556,6 +1609,11 @@ class ModuleTypeBulkEditView(generic.BulkEditView):
     form = forms.ModuleTypeBulkEditForm
     form = forms.ModuleTypeBulkEditForm
 
 
 
 
+@register_model_view(ModuleType, 'bulk_rename', path='rename', detail=False)
+class ModuleTypeBulkRenameView(generic.BulkRenameView):
+    queryset = ModuleType.objects.all()
+
+
 @register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ModuleType, 'bulk_delete', path='delete', detail=False)
 class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
 class ModuleTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = ModuleType.objects.annotate(
     queryset = ModuleType.objects.annotate(
@@ -2030,6 +2088,11 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
     form = forms.DeviceRoleBulkEditForm
     form = forms.DeviceRoleBulkEditForm
 
 
 
 
+@register_model_view(DeviceRole, 'bulk_rename', path='rename', detail=False)
+class DeviceRoleBulkRenameView(generic.BulkRenameView):
+    queryset = DeviceRole.objects.all()
+
+
 @register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False)
 @register_model_view(DeviceRole, 'bulk_delete', path='delete', detail=False)
 class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
 class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
     queryset = DeviceRole.objects.annotate(
     queryset = DeviceRole.objects.annotate(
@@ -2091,6 +2154,11 @@ class PlatformBulkEditView(generic.BulkEditView):
     form = forms.PlatformBulkEditForm
     form = forms.PlatformBulkEditForm
 
 
 
 
+@register_model_view(Platform, 'bulk_rename', path='rename', detail=False)
+class PlatformBulkRenameView(generic.BulkRenameView):
+    queryset = Platform.objects.all()
+
+
 @register_model_view(Platform, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Platform, 'bulk_delete', path='delete', detail=False)
 class PlatformBulkDeleteView(generic.BulkDeleteView):
 class PlatformBulkDeleteView(generic.BulkDeleteView):
     queryset = Platform.objects.all()
     queryset = Platform.objects.all()
@@ -2374,16 +2442,16 @@ class DeviceBulkEditView(generic.BulkEditView):
     form = forms.DeviceBulkEditForm
     form = forms.DeviceBulkEditForm
 
 
 
 
-@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
-class DeviceBulkDeleteView(generic.BulkDeleteView):
-    queryset = Device.objects.prefetch_related('device_type__manufacturer')
+@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
+class DeviceBulkRenameView(generic.BulkRenameView):
+    queryset = Device.objects.all()
     filterset = filtersets.DeviceFilterSet
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
 
 
 
 
-@register_model_view(Device, 'bulk_rename', path='rename', detail=False)
-class DeviceBulkRenameView(generic.BulkRenameView):
-    queryset = Device.objects.all()
+@register_model_view(Device, 'bulk_delete', path='delete', detail=False)
+class DeviceBulkDeleteView(generic.BulkDeleteView):
+    queryset = Device.objects.prefetch_related('device_type__manufacturer')
     filterset = filtersets.DeviceFilterSet
     filterset = filtersets.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
 
 
@@ -2398,6 +2466,7 @@ class ModuleListView(generic.ObjectListView):
     filterset = filtersets.ModuleFilterSet
     filterset = filtersets.ModuleFilterSet
     filterset_form = forms.ModuleFilterForm
     filterset_form = forms.ModuleFilterForm
     table = tables.ModuleTable
     table = tables.ModuleTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(Module)
 @register_model_view(Module)
@@ -2453,7 +2522,6 @@ class ConsolePortListView(generic.ObjectListView):
     filterset = filtersets.ConsolePortFilterSet
     filterset = filtersets.ConsolePortFilterSet
     filterset_form = forms.ConsolePortFilterForm
     filterset_form = forms.ConsolePortFilterForm
     table = tables.ConsolePortTable
     table = tables.ConsolePortTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(ConsolePort)
 @register_model_view(ConsolePort)
@@ -2524,7 +2592,6 @@ class ConsoleServerPortListView(generic.ObjectListView):
     filterset = filtersets.ConsoleServerPortFilterSet
     filterset = filtersets.ConsoleServerPortFilterSet
     filterset_form = forms.ConsoleServerPortFilterForm
     filterset_form = forms.ConsoleServerPortFilterForm
     table = tables.ConsoleServerPortTable
     table = tables.ConsoleServerPortTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(ConsoleServerPort)
 @register_model_view(ConsoleServerPort)
@@ -2595,7 +2662,6 @@ class PowerPortListView(generic.ObjectListView):
     filterset = filtersets.PowerPortFilterSet
     filterset = filtersets.PowerPortFilterSet
     filterset_form = forms.PowerPortFilterForm
     filterset_form = forms.PowerPortFilterForm
     table = tables.PowerPortTable
     table = tables.PowerPortTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(PowerPort)
 @register_model_view(PowerPort)
@@ -2666,7 +2732,6 @@ class PowerOutletListView(generic.ObjectListView):
     filterset = filtersets.PowerOutletFilterSet
     filterset = filtersets.PowerOutletFilterSet
     filterset_form = forms.PowerOutletFilterForm
     filterset_form = forms.PowerOutletFilterForm
     table = tables.PowerOutletTable
     table = tables.PowerOutletTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(PowerOutlet)
 @register_model_view(PowerOutlet)
@@ -2737,7 +2802,6 @@ class InterfaceListView(generic.ObjectListView):
     filterset = filtersets.InterfaceFilterSet
     filterset = filtersets.InterfaceFilterSet
     filterset_form = forms.InterfaceFilterForm
     filterset_form = forms.InterfaceFilterForm
     table = tables.InterfaceTable
     table = tables.InterfaceTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(Interface)
 @register_model_view(Interface)
@@ -2881,7 +2945,6 @@ class FrontPortListView(generic.ObjectListView):
     filterset = filtersets.FrontPortFilterSet
     filterset = filtersets.FrontPortFilterSet
     filterset_form = forms.FrontPortFilterForm
     filterset_form = forms.FrontPortFilterForm
     table = tables.FrontPortTable
     table = tables.FrontPortTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(FrontPort)
 @register_model_view(FrontPort)
@@ -2952,7 +3015,6 @@ class RearPortListView(generic.ObjectListView):
     filterset = filtersets.RearPortFilterSet
     filterset = filtersets.RearPortFilterSet
     filterset_form = forms.RearPortFilterForm
     filterset_form = forms.RearPortFilterForm
     table = tables.RearPortTable
     table = tables.RearPortTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(RearPort)
 @register_model_view(RearPort)
@@ -3023,7 +3085,6 @@ class ModuleBayListView(generic.ObjectListView):
     filterset = filtersets.ModuleBayFilterSet
     filterset = filtersets.ModuleBayFilterSet
     filterset_form = forms.ModuleBayFilterForm
     filterset_form = forms.ModuleBayFilterForm
     table = tables.ModuleBayTable
     table = tables.ModuleBayTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(ModuleBay)
 @register_model_view(ModuleBay)
@@ -3085,7 +3146,6 @@ class DeviceBayListView(generic.ObjectListView):
     filterset = filtersets.DeviceBayFilterSet
     filterset = filtersets.DeviceBayFilterSet
     filterset_form = forms.DeviceBayFilterForm
     filterset_form = forms.DeviceBayFilterForm
     table = tables.DeviceBayTable
     table = tables.DeviceBayTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(DeviceBay)
 @register_model_view(DeviceBay)
@@ -3228,7 +3288,6 @@ class InventoryItemListView(generic.ObjectListView):
     filterset = filtersets.InventoryItemFilterSet
     filterset = filtersets.InventoryItemFilterSet
     filterset_form = forms.InventoryItemFilterForm
     filterset_form = forms.InventoryItemFilterForm
     table = tables.InventoryItemTable
     table = tables.InventoryItemTable
-    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(InventoryItem)
 @register_model_view(InventoryItem)
@@ -3351,6 +3410,11 @@ class InventoryItemRoleBulkEditView(generic.BulkEditView):
     form = forms.InventoryItemRoleBulkEditForm
     form = forms.InventoryItemRoleBulkEditForm
 
 
 
 
+@register_model_view(InventoryItemRole, 'bulk_rename', path='rename', detail=False)
+class InventoryItemRoleBulkRenameView(generic.BulkRenameView):
+    queryset = InventoryItemRole.objects.all()
+
+
 @register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False)
 @register_model_view(InventoryItemRole, 'bulk_delete', path='delete', detail=False)
 class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
 class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
     queryset = InventoryItemRole.objects.annotate(
     queryset = InventoryItemRole.objects.annotate(
@@ -3548,6 +3612,12 @@ class CableBulkEditView(generic.BulkEditView):
     form = forms.CableBulkEditForm
     form = forms.CableBulkEditForm
 
 
 
 
+@register_model_view(Cable, 'bulk_rename', path='rename', detail=False)
+class CableBulkRenameView(generic.BulkRenameView):
+    queryset = Cable.objects.all()
+    field_name = 'label'
+
+
 @register_model_view(Cable, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Cable, 'bulk_delete', path='delete', detail=False)
 class CableBulkDeleteView(generic.BulkDeleteView):
 class CableBulkDeleteView(generic.BulkDeleteView):
     queryset = Cable.objects.prefetch_related(
     queryset = Cable.objects.prefetch_related(
@@ -3840,6 +3910,11 @@ class VirtualChassisBulkEditView(generic.BulkEditView):
     form = forms.VirtualChassisBulkEditForm
     form = forms.VirtualChassisBulkEditForm
 
 
 
 
+@register_model_view(VirtualChassis, 'bulk_rename', path='rename', detail=False)
+class VirtualChassisBulkRenameView(generic.BulkRenameView):
+    queryset = VirtualChassis.objects.all()
+
+
 @register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VirtualChassis, 'bulk_delete', path='delete', detail=False)
 class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
 class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualChassis.objects.all()
     queryset = VirtualChassis.objects.all()
@@ -3897,6 +3972,11 @@ class PowerPanelBulkEditView(generic.BulkEditView):
     form = forms.PowerPanelBulkEditForm
     form = forms.PowerPanelBulkEditForm
 
 
 
 
+@register_model_view(PowerPanel, 'bulk_rename', path='rename', detail=False)
+class PowerPanelBulkRenameView(generic.BulkRenameView):
+    queryset = PowerPanel.objects.all()
+
+
 @register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False)
 @register_model_view(PowerPanel, 'bulk_delete', path='delete', detail=False)
 class PowerPanelBulkDeleteView(generic.BulkDeleteView):
 class PowerPanelBulkDeleteView(generic.BulkDeleteView):
     queryset = PowerPanel.objects.annotate(
     queryset = PowerPanel.objects.annotate(
@@ -3949,6 +4029,11 @@ class PowerFeedBulkEditView(generic.BulkEditView):
     form = forms.PowerFeedBulkEditForm
     form = forms.PowerFeedBulkEditForm
 
 
 
 
+@register_model_view(PowerFeed, 'bulk_rename', path='rename', detail=False)
+class PowerFeedBulkRenameView(generic.BulkRenameView):
+    queryset = PowerFeed.objects.all()
+
+
 @register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False)
 @register_model_view(PowerFeed, 'bulk_disconnect', path='disconnect', detail=False)
 class PowerFeedBulkDisconnectView(BulkDisconnectView):
 class PowerFeedBulkDisconnectView(BulkDisconnectView):
     queryset = PowerFeed.objects.all()
     queryset = PowerFeed.objects.all()
@@ -3977,6 +4062,7 @@ class VirtualDeviceContextListView(generic.ObjectListView):
     filterset = filtersets.VirtualDeviceContextFilterSet
     filterset = filtersets.VirtualDeviceContextFilterSet
     filterset_form = forms.VirtualDeviceContextFilterForm
     filterset_form = forms.VirtualDeviceContextFilterForm
     table = tables.VirtualDeviceContextTable
     table = tables.VirtualDeviceContextTable
+    actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
 
 
 
 
 @register_model_view(VirtualDeviceContext)
 @register_model_view(VirtualDeviceContext)
@@ -4021,6 +4107,11 @@ class VirtualDeviceContextBulkEditView(generic.BulkEditView):
     form = forms.VirtualDeviceContextBulkEditForm
     form = forms.VirtualDeviceContextBulkEditForm
 
 
 
 
+@register_model_view(VirtualDeviceContext, 'bulk_rename', path='rename', detail=False)
+class VirtualDeviceContextBulkRenameView(generic.BulkRenameView):
+    queryset = VirtualDeviceContext.objects.all()
+
+
 @register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VirtualDeviceContext, 'bulk_delete', path='delete', detail=False)
 class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
 class VirtualDeviceContextBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualDeviceContext.objects.all()
     queryset = VirtualDeviceContext.objects.all()
@@ -4038,6 +4129,7 @@ class MACAddressListView(generic.ObjectListView):
     filterset = filtersets.MACAddressFilterSet
     filterset = filtersets.MACAddressFilterSet
     filterset_form = forms.MACAddressFilterForm
     filterset_form = forms.MACAddressFilterForm
     table = tables.MACAddressTable
     table = tables.MACAddressTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(MACAddress)
 @register_model_view(MACAddress)

+ 64 - 4
netbox/extras/views.py

@@ -97,6 +97,11 @@ class CustomFieldBulkEditView(generic.BulkEditView):
     form = forms.CustomFieldBulkEditForm
     form = forms.CustomFieldBulkEditForm
 
 
 
 
+@register_model_view(CustomField, 'bulk_rename', path='rename', detail=False)
+class CustomFieldBulkRenameView(generic.BulkRenameView):
+    queryset = CustomField.objects.all()
+
+
 @register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
 @register_model_view(CustomField, 'bulk_delete', path='delete', detail=False)
 class CustomFieldBulkDeleteView(generic.BulkDeleteView):
 class CustomFieldBulkDeleteView(generic.BulkDeleteView):
     queryset = CustomField.objects.select_related('choice_set')
     queryset = CustomField.objects.select_related('choice_set')
@@ -166,6 +171,11 @@ class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
     form = forms.CustomFieldChoiceSetBulkEditForm
     form = forms.CustomFieldChoiceSetBulkEditForm
 
 
 
 
+@register_model_view(CustomFieldChoiceSet, 'bulk_rename', path='rename', detail=False)
+class CustomFieldChoiceSetBulkRenameView(generic.BulkRenameView):
+    queryset = CustomFieldChoiceSet.objects.all()
+
+
 @register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
 @register_model_view(CustomFieldChoiceSet, 'bulk_delete', path='delete', detail=False)
 class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
 class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
     queryset = CustomFieldChoiceSet.objects.all()
     queryset = CustomFieldChoiceSet.objects.all()
@@ -216,6 +226,11 @@ class CustomLinkBulkEditView(generic.BulkEditView):
     form = forms.CustomLinkBulkEditForm
     form = forms.CustomLinkBulkEditForm
 
 
 
 
+@register_model_view(CustomLink, 'bulk_rename', path='rename', detail=False)
+class CustomLinkBulkRenameView(generic.BulkRenameView):
+    queryset = CustomLink.objects.all()
+
+
 @register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
 @register_model_view(CustomLink, 'bulk_delete', path='delete', detail=False)
 class CustomLinkBulkDeleteView(generic.BulkDeleteView):
 class CustomLinkBulkDeleteView(generic.BulkDeleteView):
     queryset = CustomLink.objects.all()
     queryset = CustomLink.objects.all()
@@ -233,7 +248,7 @@ class ExportTemplateListView(generic.ObjectListView):
     filterset = filtersets.ExportTemplateFilterSet
     filterset = filtersets.ExportTemplateFilterSet
     filterset_form = forms.ExportTemplateFilterForm
     filterset_form = forms.ExportTemplateFilterForm
     table = tables.ExportTemplateTable
     table = tables.ExportTemplateTable
-    actions = (AddObject, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete)
+    actions = (AddObject, BulkImport, BulkSync, BulkExport, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(ExportTemplate)
 @register_model_view(ExportTemplate)
@@ -267,6 +282,11 @@ class ExportTemplateBulkEditView(generic.BulkEditView):
     form = forms.ExportTemplateBulkEditForm
     form = forms.ExportTemplateBulkEditForm
 
 
 
 
+@register_model_view(ExportTemplate, 'bulk_rename', path='rename', detail=False)
+class ExportTemplateBulkRenameView(generic.BulkRenameView):
+    queryset = ExportTemplate.objects.all()
+
+
 @register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ExportTemplate, 'bulk_delete', path='delete', detail=False)
 class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
 class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
     queryset = ExportTemplate.objects.all()
     queryset = ExportTemplate.objects.all()
@@ -327,6 +347,11 @@ class SavedFilterBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
     form = forms.SavedFilterBulkEditForm
     form = forms.SavedFilterBulkEditForm
 
 
 
 
+@register_model_view(SavedFilter, 'bulk_rename', path='rename', detail=False)
+class SavedFilterBulkRenameView(generic.BulkRenameView):
+    queryset = SavedFilter.objects.all()
+
+
 @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
 @register_model_view(SavedFilter, 'bulk_delete', path='delete', detail=False)
 class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
 class SavedFilterBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
     queryset = SavedFilter.objects.all()
     queryset = SavedFilter.objects.all()
@@ -344,7 +369,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
     filterset = filtersets.TableConfigFilterSet
     filterset = filtersets.TableConfigFilterSet
     filterset_form = forms.TableConfigFilterForm
     filterset_form = forms.TableConfigFilterForm
     table = tables.TableConfigTable
     table = tables.TableConfigTable
-    actions = (BulkExport,)
+    actions = (BulkExport, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(TableConfig)
 @register_model_view(TableConfig)
@@ -384,6 +409,11 @@ class TableConfigBulkEditView(SharedObjectViewMixin, generic.BulkEditView):
     form = forms.TableConfigBulkEditForm
     form = forms.TableConfigBulkEditForm
 
 
 
 
+@register_model_view(TableConfig, 'bulk_rename', path='rename', detail=False)
+class TableConfigBulkRenameView(generic.BulkRenameView):
+    queryset = TableConfig.objects.all()
+
+
 @register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
 @register_model_view(TableConfig, 'bulk_delete', path='delete', detail=False)
 class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
 class TableConfigBulkDeleteView(SharedObjectViewMixin, generic.BulkDeleteView):
     queryset = TableConfig.objects.all()
     queryset = TableConfig.objects.all()
@@ -465,6 +495,11 @@ class NotificationGroupBulkEditView(generic.BulkEditView):
     form = forms.NotificationGroupBulkEditForm
     form = forms.NotificationGroupBulkEditForm
 
 
 
 
+@register_model_view(NotificationGroup, 'bulk_rename', path='rename', detail=False)
+class NotificationGroupBulkRenameView(generic.BulkRenameView):
+    queryset = NotificationGroup.objects.all()
+
+
 @register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(NotificationGroup, 'bulk_delete', path='delete', detail=False)
 class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
 class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = NotificationGroup.objects.all()
     queryset = NotificationGroup.objects.all()
@@ -611,6 +646,11 @@ class WebhookBulkEditView(generic.BulkEditView):
     form = forms.WebhookBulkEditForm
     form = forms.WebhookBulkEditForm
 
 
 
 
+@register_model_view(Webhook, 'bulk_rename', path='rename', detail=False)
+class WebhookBulkRenameView(generic.BulkRenameView):
+    queryset = Webhook.objects.all()
+
+
 @register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Webhook, 'bulk_delete', path='delete', detail=False)
 class WebhookBulkDeleteView(generic.BulkDeleteView):
 class WebhookBulkDeleteView(generic.BulkDeleteView):
     queryset = Webhook.objects.all()
     queryset = Webhook.objects.all()
@@ -661,6 +701,11 @@ class EventRuleBulkEditView(generic.BulkEditView):
     form = forms.EventRuleBulkEditForm
     form = forms.EventRuleBulkEditForm
 
 
 
 
+@register_model_view(EventRule, 'bulk_rename', path='rename', detail=False)
+class EventRuleBulkRenameView(generic.BulkRenameView):
+    queryset = EventRule.objects.all()
+
+
 @register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
 @register_model_view(EventRule, 'bulk_delete', path='delete', detail=False)
 class EventRuleBulkDeleteView(generic.BulkDeleteView):
 class EventRuleBulkDeleteView(generic.BulkDeleteView):
     queryset = EventRule.objects.all()
     queryset = EventRule.objects.all()
@@ -735,6 +780,11 @@ class TagBulkEditView(generic.BulkEditView):
     form = forms.TagBulkEditForm
     form = forms.TagBulkEditForm
 
 
 
 
+@register_model_view(Tag, 'bulk_rename', path='rename', detail=False)
+class TagBulkRenameView(generic.BulkRenameView):
+    queryset = Tag.objects.all()
+
+
 @register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Tag, 'bulk_delete', path='delete', detail=False)
 class TagBulkDeleteView(generic.BulkDeleteView):
 class TagBulkDeleteView(generic.BulkDeleteView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
@@ -753,7 +803,7 @@ class ConfigContextListView(generic.ObjectListView):
     filterset = filtersets.ConfigContextFilterSet
     filterset = filtersets.ConfigContextFilterSet
     filterset_form = forms.ConfigContextFilterForm
     filterset_form = forms.ConfigContextFilterForm
     table = tables.ConfigContextTable
     table = tables.ConfigContextTable
-    actions = (AddObject, BulkSync, BulkEdit, BulkDelete)
+    actions = (AddObject, BulkSync, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(ConfigContext)
 @register_model_view(ConfigContext)
@@ -814,6 +864,11 @@ class ConfigContextBulkEditView(generic.BulkEditView):
     form = forms.ConfigContextBulkEditForm
     form = forms.ConfigContextBulkEditForm
 
 
 
 
+@register_model_view(ConfigContext, 'bulk_rename', path='rename', detail=False)
+class ConfigContextBulkRenameView(generic.BulkRenameView):
+    queryset = ConfigContext.objects.all()
+
+
 @register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ConfigContext, 'bulk_delete', path='delete', detail=False)
 class ConfigContextBulkDeleteView(generic.BulkDeleteView):
 class ConfigContextBulkDeleteView(generic.BulkDeleteView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
@@ -866,7 +921,7 @@ class ConfigTemplateListView(generic.ObjectListView):
     filterset = filtersets.ConfigTemplateFilterSet
     filterset = filtersets.ConfigTemplateFilterSet
     filterset_form = forms.ConfigTemplateFilterForm
     filterset_form = forms.ConfigTemplateFilterForm
     table = tables.ConfigTemplateTable
     table = tables.ConfigTemplateTable
-    actions = (AddObject, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete)
+    actions = (AddObject, BulkImport, BulkExport, BulkSync, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(ConfigTemplate)
 @register_model_view(ConfigTemplate)
@@ -900,6 +955,11 @@ class ConfigTemplateBulkEditView(generic.BulkEditView):
     form = forms.ConfigTemplateBulkEditForm
     form = forms.ConfigTemplateBulkEditForm
 
 
 
 
+@register_model_view(ConfigTemplate, 'bulk_rename', path='rename', detail=False)
+class ConfigTemplateBulkRenameView(generic.BulkRenameView):
+    queryset = ConfigTemplate.objects.all()
+
+
 @register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ConfigTemplate, 'bulk_delete', path='delete', detail=False)
 class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
 class ConfigTemplateBulkDeleteView(generic.BulkDeleteView):
     queryset = ConfigTemplate.objects.all()
     queryset = ConfigTemplate.objects.all()

+ 70 - 0
netbox/ipam/views.py

@@ -10,6 +10,7 @@ from dcim.filtersets import InterfaceFilterSet
 from dcim.forms import InterfaceFilterForm
 from dcim.forms import InterfaceFilterForm
 from dcim.models import Device, Interface, Site
 from dcim.models import Device, Interface, Site
 from ipam.tables import VLANTranslationRuleTable
 from ipam.tables import VLANTranslationRuleTable
+from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
 from netbox.views import generic
 from netbox.views import generic
 from utilities.query import count_related
 from utilities.query import count_related
 from utilities.tables import get_table_ordering
 from utilities.tables import get_table_ordering
@@ -86,6 +87,11 @@ class VRFBulkEditView(generic.BulkEditView):
     form = forms.VRFBulkEditForm
     form = forms.VRFBulkEditForm
 
 
 
 
+@register_model_view(VRF, 'bulk_rename', path='rename', detail=False)
+class VRFBulkRenameView(generic.BulkRenameView):
+    queryset = VRF.objects.all()
+
+
 @register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VRF, 'bulk_delete', path='delete', detail=False)
 class VRFBulkDeleteView(generic.BulkDeleteView):
 class VRFBulkDeleteView(generic.BulkDeleteView):
     queryset = VRF.objects.all()
     queryset = VRF.objects.all()
@@ -136,6 +142,11 @@ class RouteTargetBulkEditView(generic.BulkEditView):
     form = forms.RouteTargetBulkEditForm
     form = forms.RouteTargetBulkEditForm
 
 
 
 
+@register_model_view(RouteTarget, 'bulk_rename', path='rename', detail=False)
+class RouteTargetBulkRenameView(generic.BulkRenameView):
+    queryset = RouteTarget.objects.all()
+
+
 @register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
 @register_model_view(RouteTarget, 'bulk_delete', path='delete', detail=False)
 class RouteTargetBulkDeleteView(generic.BulkDeleteView):
 class RouteTargetBulkDeleteView(generic.BulkDeleteView):
     queryset = RouteTarget.objects.all()
     queryset = RouteTarget.objects.all()
@@ -195,6 +206,11 @@ class RIRBulkEditView(generic.BulkEditView):
     form = forms.RIRBulkEditForm
     form = forms.RIRBulkEditForm
 
 
 
 
+@register_model_view(RIR, 'bulk_rename', path='rename', detail=False)
+class RIRBulkRenameView(generic.BulkRenameView):
+    queryset = RIR.objects.all()
+
+
 @register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
 @register_model_view(RIR, 'bulk_delete', path='delete', detail=False)
 class RIRBulkDeleteView(generic.BulkDeleteView):
 class RIRBulkDeleteView(generic.BulkDeleteView):
     queryset = RIR.objects.annotate(
     queryset = RIR.objects.annotate(
@@ -268,6 +284,11 @@ class ASNRangeBulkEditView(generic.BulkEditView):
     form = forms.ASNRangeBulkEditForm
     form = forms.ASNRangeBulkEditForm
 
 
 
 
+@register_model_view(ASNRange, 'bulk_rename', path='rename', detail=False)
+class ASNRangeBulkRenameView(generic.BulkRenameView):
+    queryset = ASNRange.objects.all()
+
+
 @register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ASNRange, 'bulk_delete', path='delete', detail=False)
 class ASNRangeBulkDeleteView(generic.BulkDeleteView):
 class ASNRangeBulkDeleteView(generic.BulkDeleteView):
     queryset = ASNRange.objects.annotate_asn_counts()
     queryset = ASNRange.objects.annotate_asn_counts()
@@ -335,6 +356,11 @@ class ASNBulkEditView(generic.BulkEditView):
     form = forms.ASNBulkEditForm
     form = forms.ASNBulkEditForm
 
 
 
 
+@register_model_view(ASN, 'bulk_rename', path='rename', detail=False)
+class ASNBulkRenameView(generic.BulkRenameView):
+    queryset = ASN.objects.all()
+
+
 @register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ASN, 'bulk_delete', path='delete', detail=False)
 class ASNBulkDeleteView(generic.BulkDeleteView):
 class ASNBulkDeleteView(generic.BulkDeleteView):
     queryset = ASN.objects.annotate(
     queryset = ASN.objects.annotate(
@@ -356,6 +382,7 @@ class AggregateListView(generic.ObjectListView):
     filterset = filtersets.AggregateFilterSet
     filterset = filtersets.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateTable
     table = tables.AggregateTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(Aggregate)
 @register_model_view(Aggregate)
@@ -488,6 +515,11 @@ class RoleBulkEditView(generic.BulkEditView):
     form = forms.RoleBulkEditForm
     form = forms.RoleBulkEditForm
 
 
 
 
+@register_model_view(Role, 'bulk_rename', path='rename', detail=False)
+class RoleBulkRenameView(generic.BulkRenameView):
+    queryset = Role.objects.all()
+
+
 @register_model_view(Role, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Role, 'bulk_delete', path='delete', detail=False)
 class RoleBulkDeleteView(generic.BulkDeleteView):
 class RoleBulkDeleteView(generic.BulkDeleteView):
     queryset = Role.objects.all()
     queryset = Role.objects.all()
@@ -506,6 +538,7 @@ class PrefixListView(generic.ObjectListView):
     filterset_form = forms.PrefixFilterForm
     filterset_form = forms.PrefixFilterForm
     table = tables.PrefixTable
     table = tables.PrefixTable
     template_name = 'ipam/prefix_list.html'
     template_name = 'ipam/prefix_list.html'
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(Prefix)
 @register_model_view(Prefix)
@@ -766,6 +799,11 @@ class IPRangeBulkEditView(generic.BulkEditView):
     form = forms.IPRangeBulkEditForm
     form = forms.IPRangeBulkEditForm
 
 
 
 
+@register_model_view(IPRange, 'bulk_rename', path='rename', detail=False)
+class IPRangeBulkRenameView(generic.BulkRenameView):
+    queryset = IPRange.objects.all()
+
+
 @register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IPRange, 'bulk_delete', path='delete', detail=False)
 class IPRangeBulkDeleteView(generic.BulkDeleteView):
 class IPRangeBulkDeleteView(generic.BulkDeleteView):
     queryset = IPRange.objects.all()
     queryset = IPRange.objects.all()
@@ -783,6 +821,7 @@ class IPAddressListView(generic.ObjectListView):
     filterset = filtersets.IPAddressFilterSet
     filterset = filtersets.IPAddressFilterSet
     filterset_form = forms.IPAddressFilterForm
     filterset_form = forms.IPAddressFilterForm
     table = tables.IPAddressTable
     table = tables.IPAddressTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(IPAddress)
 @register_model_view(IPAddress)
@@ -1006,6 +1045,11 @@ class VLANGroupBulkEditView(generic.BulkEditView):
     form = forms.VLANGroupBulkEditForm
     form = forms.VLANGroupBulkEditForm
 
 
 
 
+@register_model_view(VLANGroup, 'bulk_rename', path='rename', detail=False)
+class VLANGroupBulkRenameView(generic.BulkRenameView):
+    queryset = VLANGroup.objects.all()
+
+
 @register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VLANGroup, 'bulk_delete', path='delete', detail=False)
 class VLANGroupBulkDeleteView(generic.BulkDeleteView):
 class VLANGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
     queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
@@ -1095,6 +1139,11 @@ class VLANTranslationPolicyBulkEditView(generic.BulkEditView):
     form = forms.VLANTranslationPolicyBulkEditForm
     form = forms.VLANTranslationPolicyBulkEditForm
 
 
 
 
+@register_model_view(VLANTranslationPolicy, 'bulk_rename', path='rename', detail=False)
+class VLANTranslationPolicyBulkRenameView(generic.BulkRenameView):
+    queryset = VLANTranslationPolicy.objects.all()
+
+
 @register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VLANTranslationPolicy, 'bulk_delete', path='delete', detail=False)
 class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
 class VLANTranslationPolicyBulkDeleteView(generic.BulkDeleteView):
     queryset = VLANTranslationPolicy.objects.all()
     queryset = VLANTranslationPolicy.objects.all()
@@ -1112,6 +1161,7 @@ class VLANTranslationRuleListView(generic.ObjectListView):
     filterset = filtersets.VLANTranslationRuleFilterSet
     filterset = filtersets.VLANTranslationRuleFilterSet
     filterset_form = forms.VLANTranslationRuleFilterForm
     filterset_form = forms.VLANTranslationRuleFilterForm
     table = tables.VLANTranslationRuleTable
     table = tables.VLANTranslationRuleTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(VLANTranslationRule)
 @register_model_view(VLANTranslationRule)
@@ -1244,6 +1294,11 @@ class FHRPGroupBulkEditView(generic.BulkEditView):
     form = forms.FHRPGroupBulkEditForm
     form = forms.FHRPGroupBulkEditForm
 
 
 
 
+@register_model_view(FHRPGroup, 'bulk_rename', path='rename', detail=False)
+class FHRPGroupBulkRenameView(generic.BulkRenameView):
+    queryset = FHRPGroup.objects.all()
+
+
 @register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(FHRPGroup, 'bulk_delete', path='delete', detail=False)
 class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
 class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = FHRPGroup.objects.all()
     queryset = FHRPGroup.objects.all()
@@ -1371,6 +1426,11 @@ class VLANBulkEditView(generic.BulkEditView):
     form = forms.VLANBulkEditForm
     form = forms.VLANBulkEditForm
 
 
 
 
+@register_model_view(VLAN, 'bulk_rename', path='rename', detail=False)
+class VLANBulkRenameView(generic.BulkRenameView):
+    queryset = VLAN.objects.all()
+
+
 @register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VLAN, 'bulk_delete', path='delete', detail=False)
 class VLANBulkDeleteView(generic.BulkDeleteView):
 class VLANBulkDeleteView(generic.BulkDeleteView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
@@ -1421,6 +1481,11 @@ class ServiceTemplateBulkEditView(generic.BulkEditView):
     form = forms.ServiceTemplateBulkEditForm
     form = forms.ServiceTemplateBulkEditForm
 
 
 
 
+@register_model_view(ServiceTemplate, 'bulk_rename', path='rename', detail=False)
+class ServiceTemplateBulkRenameView(generic.BulkRenameView):
+    queryset = ServiceTemplate.objects.all()
+
+
 @register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ServiceTemplate, 'bulk_delete', path='delete', detail=False)
 class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
 class ServiceTemplateBulkDeleteView(generic.BulkDeleteView):
     queryset = ServiceTemplate.objects.all()
     queryset = ServiceTemplate.objects.all()
@@ -1488,6 +1553,11 @@ class ServiceBulkEditView(generic.BulkEditView):
     form = forms.ServiceBulkEditForm
     form = forms.ServiceBulkEditForm
 
 
 
 
+@register_model_view(Service, 'bulk_rename', path='rename', detail=False)
+class ServiceBulkRenameView(generic.BulkRenameView):
+    queryset = Service.objects.all()
+
+
 @register_model_view(Service, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Service, 'bulk_delete', path='delete', detail=False)
 class ServiceBulkDeleteView(generic.BulkDeleteView):
 class ServiceBulkDeleteView(generic.BulkDeleteView):
     queryset = Service.objects.prefetch_related('parent')
     queryset = Service.objects.prefetch_related('parent')

+ 5 - 1
netbox/netbox/object_actions.py

@@ -1,4 +1,5 @@
 from django.urls import reverse
 from django.urls import reverse
+from django.urls.exceptions import NoReverseMatch
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from core.models import ObjectType
 from core.models import ObjectType
@@ -42,7 +43,10 @@ class ObjectAction:
         kwargs = {
         kwargs = {
             kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs
             kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs
         }
         }
-        return reverse(viewname, kwargs=kwargs)
+        try:
+            return reverse(viewname, kwargs=kwargs)
+        except NoReverseMatch:
+            return
 
 
     @classmethod
     @classmethod
     def get_context(cls, context, obj):
     def get_context(cls, context, obj):

+ 12 - 8
netbox/netbox/views/generic/bulk_views.py

@@ -22,7 +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.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
+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
 from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
 from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
 from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
 from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@@ -55,13 +55,12 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
     Attributes:
     Attributes:
         filterset: A django-filter FilterSet that is applied to the queryset
         filterset: A django-filter FilterSet that is applied to the queryset
         filterset_form: The form class used to render filter options
         filterset_form: The form class used to render filter options
-        actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
-            action names must be prefixed with `bulk_`. (See ActionsMixin.)
+        actions: An iterable of ObjectAction subclasses (see ActionsMixin)
     """
     """
     template_name = 'generic/object_list.html'
     template_name = 'generic/object_list.html'
     filterset = None
     filterset = None
     filterset_form = None
     filterset_form = None
-    actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkRename, BulkDelete)
 
 
     def get_required_permission(self):
     def get_required_permission(self):
         return get_permission_for_model(self.queryset.model, 'view')
         return get_permission_for_model(self.queryset.model, 'view')
@@ -731,7 +730,11 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
 class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
 class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
     """
     """
     An extendable view for renaming objects in bulk.
     An extendable view for renaming objects in bulk.
+
+    Attributes:
+        field_name: The name of the object attribute for which the value is being updated (defaults to "name")
     """
     """
+    field_name = 'name'
     template_name = 'generic/bulk_rename.html'
     template_name = 'generic/bulk_rename.html'
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -761,12 +764,12 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
             replace = form.cleaned_data['replace']
             replace = form.cleaned_data['replace']
             if form.cleaned_data['use_regex']:
             if form.cleaned_data['use_regex']:
                 try:
                 try:
-                    obj.new_name = re.sub(find, replace, obj.name or '')
+                    obj.new_name = re.sub(find, replace, getattr(obj, self.field_name, ''))
                 # Catch regex group reference errors
                 # Catch regex group reference errors
                 except re.error:
                 except re.error:
-                    obj.new_name = obj.name
+                    obj.new_name = getattr(obj, self.field_name)
             else:
             else:
-                obj.new_name = (obj.name or '').replace(find, replace)
+                obj.new_name = getattr(obj, self.field_name, '').replace(find, replace)
             renamed_pks.append(obj.pk)
             renamed_pks.append(obj.pk)
 
 
         return renamed_pks
         return renamed_pks
@@ -785,7 +788,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
 
 
                         if '_apply' in request.POST:
                         if '_apply' in request.POST:
                             for obj in selected_objects:
                             for obj in selected_objects:
-                                obj.name = obj.new_name
+                                setattr(obj, self.field_name, obj.new_name)
                                 obj.save()
                                 obj.save()
 
 
                             # Enforce constrained permissions
                             # Enforce constrained permissions
@@ -815,6 +818,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
             selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
             selected_objects = self.queryset.filter(pk__in=form.initial['pk'])
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
+            'field_name': self.field_name,
             'form': form,
             'form': form,
             'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
             'obj_type_plural': self.queryset.model._meta.verbose_name_plural,
             'selected_objects': selected_objects,
             'selected_objects': selected_objects,

+ 2 - 2
netbox/netbox/views/generic/object_views.py

@@ -47,6 +47,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
 
 
     Attributes:
     Attributes:
         tab: A ViewTab instance for the view
         tab: A ViewTab instance for the view
+        actions: An iterable of ObjectAction subclasses (see ActionsMixin)
     """
     """
     tab = None
     tab = None
     actions = (CloneObject, EditObject, DeleteObject)
     actions = (CloneObject, EditObject, DeleteObject)
@@ -96,8 +97,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
         table: The django-tables2 Table class used to render the child objects list
         table: The django-tables2 Table class used to render the child objects list
         filterset: A django-filter FilterSet that is applied to the queryset
         filterset: A django-filter FilterSet that is applied to the queryset
         filterset_form: The form class used to render filter options
         filterset_form: The form class used to render filter options
-        actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
-            action names must be prefixed with `bulk_`. (See ActionsMixin.)
+        actions: An iterable of ObjectAction subclasses (see ActionsMixin)
     """
     """
     child_model = None
     child_model = None
     table = None
     table = None

+ 6 - 4
netbox/templates/generic/bulk_rename.html

@@ -42,10 +42,12 @@ Context:
             </thead>
             </thead>
             <tbody>
             <tbody>
                 {% for obj in selected_objects %}
                 {% for obj in selected_objects %}
-                    <tr{% if obj.new_name and obj.name != obj.new_name %} class="success"{% endif %}>
-                        <td>{{ obj.name }}</td>
-                        <td>{{ obj.new_name }}</td>
-                    </tr>
+                    {% with obj_name=obj|getattr:field_name %}
+                        <tr{% if obj.new_name and obj_name != obj.new_name %} class="success"{% endif %}>
+                            <td>{{ obj_name }}</td>
+                            <td>{{ obj.new_name }}</td>
+                        </tr>
+                    {% endwith %}
                 {% endfor %}
                 {% endfor %}
             </tbody>
             </tbody>
         </table>
         </table>

+ 1 - 6
netbox/templates/htmx/table.html

@@ -27,12 +27,7 @@
   {# Update the bulk action buttons with new query parameters #}
   {# Update the bulk action buttons with new query parameters #}
   {% if actions %}
   {% if actions %}
     <div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
     <div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
-      {% if 'bulk_edit' in actions %}
-        {% bulk_edit_button model query_params=request.GET %}
-      {% endif %}
-      {% if 'bulk_delete' in actions %}
-        {% bulk_delete_button model query_params=request.GET %}
-      {% endif %}
+      {% action_buttons actions model multi=True %}
     </div>
     </div>
   {% endif %}
   {% endif %}
 {% endif %}
 {% endif %}

+ 25 - 0
netbox/tenancy/views.py

@@ -71,6 +71,11 @@ class TenantGroupBulkEditView(generic.BulkEditView):
     form = forms.TenantGroupBulkEditForm
     form = forms.TenantGroupBulkEditForm
 
 
 
 
+@register_model_view(TenantGroup, 'bulk_rename', path='rename', detail=False)
+class TenantGroupBulkRenameView(generic.BulkRenameView):
+    queryset = TenantGroup.objects.all()
+
+
 @register_model_view(TenantGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(TenantGroup, 'bulk_delete', path='delete', detail=False)
 class TenantGroupBulkDeleteView(generic.BulkDeleteView):
 class TenantGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = TenantGroup.objects.add_related_count(
     queryset = TenantGroup.objects.add_related_count(
@@ -132,6 +137,11 @@ class TenantBulkEditView(generic.BulkEditView):
     form = forms.TenantBulkEditForm
     form = forms.TenantBulkEditForm
 
 
 
 
+@register_model_view(Tenant, 'bulk_rename', path='rename', detail=False)
+class TenantBulkRenameView(generic.BulkRenameView):
+    queryset = Tenant.objects.all()
+
+
 @register_model_view(Tenant, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Tenant, 'bulk_delete', path='delete', detail=False)
 class TenantBulkDeleteView(generic.BulkDeleteView):
 class TenantBulkDeleteView(generic.BulkDeleteView):
     queryset = Tenant.objects.all()
     queryset = Tenant.objects.all()
@@ -207,6 +217,11 @@ class ContactGroupBulkEditView(generic.BulkEditView):
     form = forms.ContactGroupBulkEditForm
     form = forms.ContactGroupBulkEditForm
 
 
 
 
+@register_model_view(ContactGroup, 'bulk_rename', path='rename', detail=False)
+class ContactGroupBulkRenameView(generic.BulkRenameView):
+    queryset = ContactGroup.objects.all()
+
+
 @register_model_view(ContactGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ContactGroup, 'bulk_delete', path='delete', detail=False)
 class ContactGroupBulkDeleteView(generic.BulkDeleteView):
 class ContactGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = ContactGroup.objects.add_related_count(
     queryset = ContactGroup.objects.add_related_count(
@@ -268,6 +283,11 @@ class ContactRoleBulkEditView(generic.BulkEditView):
     form = forms.ContactRoleBulkEditForm
     form = forms.ContactRoleBulkEditForm
 
 
 
 
+@register_model_view(ContactRole, 'bulk_rename', path='rename', detail=False)
+class ContactRoleBulkRenameView(generic.BulkRenameView):
+    queryset = ContactRole.objects.all()
+
+
 @register_model_view(ContactRole, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ContactRole, 'bulk_delete', path='delete', detail=False)
 class ContactRoleBulkDeleteView(generic.BulkDeleteView):
 class ContactRoleBulkDeleteView(generic.BulkDeleteView):
     queryset = ContactRole.objects.all()
     queryset = ContactRole.objects.all()
@@ -331,6 +351,11 @@ class ContactBulkEditView(generic.BulkEditView):
             obj.groups.remove(*form.cleaned_data['remove_groups'])
             obj.groups.remove(*form.cleaned_data['remove_groups'])
 
 
 
 
+@register_model_view(Contact, 'bulk_rename', path='rename', detail=False)
+class ContactBulkRenameView(generic.BulkRenameView):
+    queryset = Contact.objects.all()
+
+
 @register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Contact, 'bulk_delete', path='delete', detail=False)
 class ContactBulkDeleteView(generic.BulkDeleteView):
 class ContactBulkDeleteView(generic.BulkDeleteView):
     queryset = Contact.objects.annotate(
     queryset = Contact.objects.annotate(

+ 19 - 0
netbox/users/views.py

@@ -2,6 +2,7 @@ from django.db.models import Count
 
 
 from core.models import ObjectChange
 from core.models import ObjectChange
 from core.tables import ObjectChangeTable
 from core.tables import ObjectChangeTable
+from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
 from netbox.views import generic
 from netbox.views import generic
 from utilities.views import register_model_view
 from utilities.views import register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
@@ -18,6 +19,7 @@ class TokenListView(generic.ObjectListView):
     filterset = filtersets.TokenFilterSet
     filterset = filtersets.TokenFilterSet
     filterset_form = forms.TokenFilterForm
     filterset_form = forms.TokenFilterForm
     table = tables.TokenTable
     table = tables.TokenTable
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(Token)
 @register_model_view(Token)
@@ -111,6 +113,12 @@ class UserBulkEditView(generic.BulkEditView):
     form = forms.UserBulkEditForm
     form = forms.UserBulkEditForm
 
 
 
 
+@register_model_view(User, 'bulk_rename', path='rename', detail=False)
+class UserBulkRenameView(generic.BulkRenameView):
+    queryset = User.objects.all()
+    field_name = 'username'
+
+
 @register_model_view(User, 'bulk_delete', path='delete', detail=False)
 @register_model_view(User, 'bulk_delete', path='delete', detail=False)
 class UserBulkDeleteView(generic.BulkDeleteView):
 class UserBulkDeleteView(generic.BulkDeleteView):
     queryset = User.objects.all()
     queryset = User.objects.all()
@@ -162,6 +170,11 @@ class GroupBulkEditView(generic.BulkEditView):
     form = forms.GroupBulkEditForm
     form = forms.GroupBulkEditForm
 
 
 
 
+@register_model_view(Group, 'bulk_rename', path='rename', detail=False)
+class GroupBulkRenameView(generic.BulkRenameView):
+    queryset = Group.objects.all()
+
+
 @register_model_view(Group, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Group, 'bulk_delete', path='delete', detail=False)
 class GroupBulkDeleteView(generic.BulkDeleteView):
 class GroupBulkDeleteView(generic.BulkDeleteView):
     queryset = Group.objects.annotate(users_count=Count('user')).order_by('name')
     queryset = Group.objects.annotate(users_count=Count('user')).order_by('name')
@@ -179,6 +192,7 @@ class ObjectPermissionListView(generic.ObjectListView):
     filterset = filtersets.ObjectPermissionFilterSet
     filterset = filtersets.ObjectPermissionFilterSet
     filterset_form = forms.ObjectPermissionFilterForm
     filterset_form = forms.ObjectPermissionFilterForm
     table = tables.ObjectPermissionTable
     table = tables.ObjectPermissionTable
+    actions = (AddObject, BulkExport, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(ObjectPermission)
 @register_model_view(ObjectPermission)
@@ -207,6 +221,11 @@ class ObjectPermissionBulkEditView(generic.BulkEditView):
     form = forms.ObjectPermissionBulkEditForm
     form = forms.ObjectPermissionBulkEditForm
 
 
 
 
+@register_model_view(ObjectPermission, 'bulk_rename', path='rename', detail=False)
+class ObjectPermissionBulkRenameView(generic.BulkRenameView):
+    queryset = ObjectPermission.objects.all()
+
+
 @register_model_view(ObjectPermission, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ObjectPermission, 'bulk_delete', path='delete', detail=False)
 class ObjectPermissionBulkDeleteView(generic.BulkDeleteView):
 class ObjectPermissionBulkDeleteView(generic.BulkDeleteView):
     queryset = ObjectPermission.objects.all()
     queryset = ObjectPermission.objects.all()

+ 5 - 3
netbox/utilities/templates/buttons/bulk_rename.html

@@ -1,3 +1,5 @@
-<button type="submit" name="_rename" {% formaction %}="{{ url }}" class="btn btn-yellow">
-  <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
-</button>
+{% if url %}
+  <button type="submit" name="_rename" {% formaction %}="{{ url }}" class="btn btn-yellow">
+    <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
+  </button>
+{% endif %}

+ 9 - 0
netbox/utilities/templatetags/builtins/filters.py

@@ -22,6 +22,7 @@ __all__ = (
     'content_type',
     'content_type',
     'content_type_id',
     'content_type_id',
     'fgcolor',
     'fgcolor',
+    'getattr_',
     'isodate',
     'isodate',
     'isodatetime',
     'isodatetime',
     'isotime',
     'isotime',
@@ -88,6 +89,14 @@ def fgcolor(value, dark='000000', light='ffffff'):
     return f'#{foreground_color(value, dark, light)}'
     return f'#{foreground_color(value, dark, light)}'
 
 
 
 
+@register.filter('getattr')
+def getattr_(instance, name):
+    """
+    Call getattr() on the object for the specified attribute.
+    """
+    return getattr(instance, name, None)
+
+
 @register.filter()
 @register.filter()
 def meta(model, attr):
 def meta(model, attr):
     """
     """

+ 24 - 2
netbox/virtualization/views.py

@@ -13,7 +13,9 @@ from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from ipam.models import IPAddress, VLANGroup
 from ipam.models import IPAddress, VLANGroup
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
-from netbox.object_actions import *
+from netbox.object_actions import (
+    AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
+)
 from netbox.views import generic
 from netbox.views import generic
 from utilities.query import count_related
 from utilities.query import count_related
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
@@ -75,6 +77,11 @@ class ClusterTypeBulkEditView(generic.BulkEditView):
     form = forms.ClusterTypeBulkEditForm
     form = forms.ClusterTypeBulkEditForm
 
 
 
 
+@register_model_view(ClusterType, 'bulk_rename', path='rename', detail=False)
+class ClusterTypeBulkRenameView(generic.BulkRenameView):
+    queryset = ClusterType.objects.all()
+
+
 @register_model_view(ClusterType, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ClusterType, 'bulk_delete', path='delete', detail=False)
 class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
 class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = ClusterType.objects.annotate(
     queryset = ClusterType.objects.annotate(
@@ -148,6 +155,11 @@ class ClusterGroupBulkEditView(generic.BulkEditView):
     form = forms.ClusterGroupBulkEditForm
     form = forms.ClusterGroupBulkEditForm
 
 
 
 
+@register_model_view(ClusterGroup, 'bulk_rename', path='rename', detail=False)
+class ClusterGroupBulkRenameView(generic.BulkRenameView):
+    queryset = ClusterGroup.objects.all()
+
+
 @register_model_view(ClusterGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(ClusterGroup, 'bulk_delete', path='delete', detail=False)
 class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
 class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = ClusterGroup.objects.annotate(
     queryset = ClusterGroup.objects.annotate(
@@ -262,6 +274,11 @@ class ClusterBulkEditView(generic.BulkEditView):
     form = forms.ClusterBulkEditForm
     form = forms.ClusterBulkEditForm
 
 
 
 
+@register_model_view(Cluster, 'bulk_rename', path='rename', detail=False)
+class ClusterBulkRenameView(generic.BulkRenameView):
+    queryset = Cluster.objects.all()
+
+
 @register_model_view(Cluster, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Cluster, 'bulk_delete', path='delete', detail=False)
 class ClusterBulkDeleteView(generic.BulkDeleteView):
 class ClusterBulkDeleteView(generic.BulkDeleteView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
@@ -322,7 +339,7 @@ class VirtualMachineListView(generic.ObjectListView):
     filterset = filtersets.VirtualMachineFilterSet
     filterset = filtersets.VirtualMachineFilterSet
     filterset_form = forms.VirtualMachineFilterForm
     filterset_form = forms.VirtualMachineFilterForm
     table = tables.VirtualMachineTable
     table = tables.VirtualMachineTable
-    actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkDelete)
+    actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
 
 
 
 
 @register_model_view(VirtualMachine)
 @register_model_view(VirtualMachine)
@@ -417,6 +434,11 @@ class VirtualMachineBulkEditView(generic.BulkEditView):
     form = forms.VirtualMachineBulkEditForm
     form = forms.VirtualMachineBulkEditForm
 
 
 
 
+@register_model_view(VirtualMachine, 'bulk_rename', path='rename', detail=False)
+class VirtualMachineBulkRenameView(generic.BulkRenameView):
+    queryset = VirtualMachine.objects.all()
+
+
 @register_model_view(VirtualMachine, 'bulk_delete', path='delete', detail=False)
 @register_model_view(VirtualMachine, 'bulk_delete', path='delete', detail=False)
 class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
 class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
     queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
     queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')

+ 42 - 0
netbox/vpn/views.py

@@ -1,4 +1,5 @@
 from ipam.tables import RouteTargetTable
 from ipam.tables import RouteTargetTable
+from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
 from netbox.views import generic
 from netbox.views import generic
 from utilities.query import count_related
 from utilities.query import count_related
 from utilities.views import GetRelatedModelsMixin, register_model_view
 from utilities.views import GetRelatedModelsMixin, register_model_view
@@ -58,6 +59,11 @@ class TunnelGroupBulkEditView(generic.BulkEditView):
     form = forms.TunnelGroupBulkEditForm
     form = forms.TunnelGroupBulkEditForm
 
 
 
 
+@register_model_view(TunnelGroup, 'bulk_rename', path='rename', detail=False)
+class TunnelGroupBulkRenameView(generic.BulkRenameView):
+    queryset = TunnelGroup.objects.all()
+
+
 @register_model_view(TunnelGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(TunnelGroup, 'bulk_delete', path='delete', detail=False)
 class TunnelGroupBulkDeleteView(generic.BulkDeleteView):
 class TunnelGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = TunnelGroup.objects.annotate(
     queryset = TunnelGroup.objects.annotate(
@@ -122,6 +128,11 @@ class TunnelBulkEditView(generic.BulkEditView):
     form = forms.TunnelBulkEditForm
     form = forms.TunnelBulkEditForm
 
 
 
 
+@register_model_view(Tunnel, 'bulk_rename', path='rename', detail=False)
+class TunnelBulkRenameView(generic.BulkRenameView):
+    queryset = Tunnel.objects.all()
+
+
 @register_model_view(Tunnel, 'bulk_delete', path='delete', detail=False)
 @register_model_view(Tunnel, 'bulk_delete', path='delete', detail=False)
 class TunnelBulkDeleteView(generic.BulkDeleteView):
 class TunnelBulkDeleteView(generic.BulkDeleteView):
     queryset = Tunnel.objects.annotate(
     queryset = Tunnel.objects.annotate(
@@ -224,6 +235,11 @@ class IKEProposalBulkEditView(generic.BulkEditView):
     form = forms.IKEProposalBulkEditForm
     form = forms.IKEProposalBulkEditForm
 
 
 
 
+@register_model_view(IKEProposal, 'bulk_rename', path='rename', detail=False)
+class IKEProposalBulkRenameView(generic.BulkRenameView):
+    queryset = IKEProposal.objects.all()
+
+
 @register_model_view(IKEProposal, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IKEProposal, 'bulk_delete', path='delete', detail=False)
 class IKEProposalBulkDeleteView(generic.BulkDeleteView):
 class IKEProposalBulkDeleteView(generic.BulkDeleteView):
     queryset = IKEProposal.objects.all()
     queryset = IKEProposal.objects.all()
@@ -274,6 +290,11 @@ class IKEPolicyBulkEditView(generic.BulkEditView):
     form = forms.IKEPolicyBulkEditForm
     form = forms.IKEPolicyBulkEditForm
 
 
 
 
+@register_model_view(IKEPolicy, 'bulk_rename', path='rename', detail=False)
+class IKEPolicyBulkRenameView(generic.BulkRenameView):
+    queryset = IKEPolicy.objects.all()
+
+
 @register_model_view(IKEPolicy, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IKEPolicy, 'bulk_delete', path='delete', detail=False)
 class IKEPolicyBulkDeleteView(generic.BulkDeleteView):
 class IKEPolicyBulkDeleteView(generic.BulkDeleteView):
     queryset = IKEPolicy.objects.all()
     queryset = IKEPolicy.objects.all()
@@ -324,6 +345,11 @@ class IPSecProposalBulkEditView(generic.BulkEditView):
     form = forms.IPSecProposalBulkEditForm
     form = forms.IPSecProposalBulkEditForm
 
 
 
 
+@register_model_view(IPSecProposal, 'bulk_rename', path='rename', detail=False)
+class IPSecProposalBulkRenameView(generic.BulkRenameView):
+    queryset = IPSecProposal.objects.all()
+
+
 @register_model_view(IPSecProposal, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IPSecProposal, 'bulk_delete', path='delete', detail=False)
 class IPSecProposalBulkDeleteView(generic.BulkDeleteView):
 class IPSecProposalBulkDeleteView(generic.BulkDeleteView):
     queryset = IPSecProposal.objects.all()
     queryset = IPSecProposal.objects.all()
@@ -374,6 +400,11 @@ class IPSecPolicyBulkEditView(generic.BulkEditView):
     form = forms.IPSecPolicyBulkEditForm
     form = forms.IPSecPolicyBulkEditForm
 
 
 
 
+@register_model_view(IPSecPolicy, 'bulk_rename', path='rename', detail=False)
+class IPSecPolicyBulkRenameView(generic.BulkRenameView):
+    queryset = IPSecPolicy.objects.all()
+
+
 @register_model_view(IPSecPolicy, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IPSecPolicy, 'bulk_delete', path='delete', detail=False)
 class IPSecPolicyBulkDeleteView(generic.BulkDeleteView):
 class IPSecPolicyBulkDeleteView(generic.BulkDeleteView):
     queryset = IPSecPolicy.objects.all()
     queryset = IPSecPolicy.objects.all()
@@ -424,6 +455,11 @@ class IPSecProfileBulkEditView(generic.BulkEditView):
     form = forms.IPSecProfileBulkEditForm
     form = forms.IPSecProfileBulkEditForm
 
 
 
 
+@register_model_view(IPSecProfile, 'bulk_rename', path='rename', detail=False)
+class IPSecProfileBulkRenameView(generic.BulkRenameView):
+    queryset = IPSecProfile.objects.all()
+
+
 @register_model_view(IPSecProfile, 'bulk_delete', path='delete', detail=False)
 @register_model_view(IPSecProfile, 'bulk_delete', path='delete', detail=False)
 class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
 class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
     queryset = IPSecProfile.objects.all()
     queryset = IPSecProfile.objects.all()
@@ -491,6 +527,11 @@ class L2VPNBulkEditView(generic.BulkEditView):
     form = forms.L2VPNBulkEditForm
     form = forms.L2VPNBulkEditForm
 
 
 
 
+@register_model_view(L2VPN, 'bulk_rename', path='rename', detail=False)
+class L2VPNBulkRenameView(generic.BulkRenameView):
+    queryset = L2VPN.objects.all()
+
+
 @register_model_view(L2VPN, 'bulk_delete', path='delete', detail=False)
 @register_model_view(L2VPN, 'bulk_delete', path='delete', detail=False)
 class L2VPNBulkDeleteView(generic.BulkDeleteView):
 class L2VPNBulkDeleteView(generic.BulkDeleteView):
     queryset = L2VPN.objects.all()
     queryset = L2VPN.objects.all()
@@ -508,6 +549,7 @@ class L2VPNTerminationListView(generic.ObjectListView):
     table = tables.L2VPNTerminationTable
     table = tables.L2VPNTerminationTable
     filterset = filtersets.L2VPNTerminationFilterSet
     filterset = filtersets.L2VPNTerminationFilterSet
     filterset_form = forms.L2VPNTerminationFilterForm
     filterset_form = forms.L2VPNTerminationFilterForm
+    actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
 
 
 
 
 @register_model_view(L2VPNTermination)
 @register_model_view(L2VPNTermination)

+ 17 - 0
netbox/wireless/views.py

@@ -68,6 +68,11 @@ class WirelessLANGroupBulkEditView(generic.BulkEditView):
     form = forms.WirelessLANGroupBulkEditForm
     form = forms.WirelessLANGroupBulkEditForm
 
 
 
 
+@register_model_view(WirelessLANGroup, 'bulk_rename', path='rename', detail=False)
+class WirelessLANGroupBulkRenameView(generic.BulkRenameView):
+    queryset = WirelessLANGroup.objects.all()
+
+
 @register_model_view(WirelessLANGroup, 'bulk_delete', path='delete', detail=False)
 @register_model_view(WirelessLANGroup, 'bulk_delete', path='delete', detail=False)
 class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
 class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = WirelessLANGroup.objects.add_related_count(
     queryset = WirelessLANGroup.objects.add_related_count(
@@ -137,6 +142,12 @@ class WirelessLANBulkEditView(generic.BulkEditView):
     form = forms.WirelessLANBulkEditForm
     form = forms.WirelessLANBulkEditForm
 
 
 
 
+@register_model_view(WirelessLAN, 'bulk_rename', path='rename', detail=False)
+class WirelessLANBulkRenameView(generic.BulkRenameView):
+    queryset = WirelessLAN.objects.all()
+    field_name = 'ssid'
+
+
 @register_model_view(WirelessLAN, 'bulk_delete', path='delete', detail=False)
 @register_model_view(WirelessLAN, 'bulk_delete', path='delete', detail=False)
 class WirelessLANBulkDeleteView(generic.BulkDeleteView):
 class WirelessLANBulkDeleteView(generic.BulkDeleteView):
     queryset = WirelessLAN.objects.all()
     queryset = WirelessLAN.objects.all()
@@ -187,6 +198,12 @@ class WirelessLinkBulkEditView(generic.BulkEditView):
     form = forms.WirelessLinkBulkEditForm
     form = forms.WirelessLinkBulkEditForm
 
 
 
 
+@register_model_view(WirelessLink, 'bulk_rename', path='rename', detail=False)
+class WirelessLinkBulkRenameView(generic.BulkRenameView):
+    queryset = WirelessLink.objects.all()
+    field_name = 'ssid'
+
+
 @register_model_view(WirelessLink, 'bulk_delete', path='delete', detail=False)
 @register_model_view(WirelessLink, 'bulk_delete', path='delete', detail=False)
 class WirelessLinkBulkDeleteView(generic.BulkDeleteView):
 class WirelessLinkBulkDeleteView(generic.BulkDeleteView):
     queryset = WirelessLink.objects.all()
     queryset = WirelessLink.objects.all()