From 3e26ce950cef2faf714e616955d9363626a3b071 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jun 2025 17:10:05 -0400 Subject: [PATCH] Work in progress --- netbox/core/object_actions.py | 18 +++ netbox/core/views.py | 14 +-- netbox/dcim/object_actions.py | 18 +++ netbox/dcim/views.py | 119 +++++------------- netbox/extras/views.py | 37 ++---- netbox/netbox/constants.py | 3 +- netbox/netbox/object_actions.py | 24 +++- netbox/netbox/views/generic/mixins.py | 26 +++- netbox/netbox/views/generic/object_views.py | 3 +- netbox/templates/dcim/component_list.html | 22 ---- .../dcim/device/components_base.html | 23 ---- .../templates/dcim/device/consoleports.html | 28 ----- .../dcim/device/consoleserverports.html | 28 ----- netbox/templates/dcim/device/devicebays.html | 14 --- netbox/templates/dcim/device/frontports.html | 28 ----- netbox/templates/dcim/device/interfaces.html | 29 +---- netbox/templates/dcim/device/inventory.html | 14 --- netbox/templates/dcim/device/modulebays.html | 14 --- .../templates/dcim/device/poweroutlets.html | 28 ----- netbox/templates/dcim/device/powerports.html | 28 ----- netbox/templates/dcim/device/rearports.html | 28 ----- .../dcim/devicetype/component_templates.html | 40 +++--- .../templates/extras/configtemplate_list.html | 11 -- .../templates/extras/exporttemplate_list.html | 11 -- netbox/templates/generic/object_children.html | 56 +++++---- netbox/templates/generic/object_list.html | 8 +- .../virtualmachine/interfaces.html | 14 --- netbox/tenancy/views.py | 8 +- .../templates/buttons/bulk_disconnect.html | 6 + .../buttons/bulk_remove_devices.html | 6 + .../templates/buttons/bulk_rename.html | 6 + .../templates/buttons/bulk_sync.html | 6 + netbox/utilities/templatetags/buttons.py | 4 + netbox/virtualization/views.py | 22 +--- 34 files changed, 211 insertions(+), 533 deletions(-) create mode 100644 netbox/core/object_actions.py create mode 100644 netbox/dcim/object_actions.py delete mode 100644 netbox/templates/dcim/component_list.html delete mode 100644 netbox/templates/dcim/device/components_base.html delete mode 100644 netbox/templates/dcim/device/consoleports.html delete mode 100644 netbox/templates/dcim/device/consoleserverports.html delete mode 100644 netbox/templates/dcim/device/devicebays.html delete mode 100644 netbox/templates/dcim/device/frontports.html delete mode 100644 netbox/templates/dcim/device/inventory.html delete mode 100644 netbox/templates/dcim/device/modulebays.html delete mode 100644 netbox/templates/dcim/device/poweroutlets.html delete mode 100644 netbox/templates/dcim/device/powerports.html delete mode 100644 netbox/templates/dcim/device/rearports.html delete mode 100644 netbox/templates/extras/configtemplate_list.html delete mode 100644 netbox/templates/extras/exporttemplate_list.html delete mode 100644 netbox/templates/virtualization/virtualmachine/interfaces.html create mode 100644 netbox/utilities/templates/buttons/bulk_disconnect.html create mode 100644 netbox/utilities/templates/buttons/bulk_remove_devices.html create mode 100644 netbox/utilities/templates/buttons/bulk_rename.html create mode 100644 netbox/utilities/templates/buttons/bulk_sync.html diff --git a/netbox/core/object_actions.py b/netbox/core/object_actions.py new file mode 100644 index 000000000..b65003764 --- /dev/null +++ b/netbox/core/object_actions.py @@ -0,0 +1,18 @@ +from django.utils.translation import gettext as _ + +from netbox.object_actions import ObjectAction + +__all__ = ( + 'BulkSync', +) + + +class BulkSync(ObjectAction): + """ + Synchronize multiple objects at once. + """ + name = 'bulk_sync' + label = _('Sync Data') + bulk = True + permissions_required = {'sync'} + template_name = 'buttons/bulk_sync.html' diff --git a/netbox/core/views.py b/netbox/core/views.py index ef52147f1..c766fdf80 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -22,6 +22,7 @@ from rq.worker_registration import clean_worker_registry from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job from netbox.config import get_config, PARAMS +from netbox.object_actions import BulkDelete, BulkExport from netbox.registry import registry from netbox.views import generic from netbox.views.generic.base import BaseObjectView @@ -138,9 +139,7 @@ class DataFileListView(generic.ObjectListView): filterset = filtersets.DataFileFilterSet filterset_form = forms.DataFileFilterForm table = tables.DataFileTable - actions = { - 'bulk_delete': {'delete'}, - } + actions = (BulkDelete,) @register_model_view(DataFile) @@ -170,10 +169,7 @@ class JobListView(generic.ObjectListView): filterset = filtersets.JobFilterSet filterset_form = forms.JobFilterForm table = tables.JobTable - actions = { - 'export': {'view'}, - 'bulk_delete': {'delete'}, - } + actions = (BulkExport, BulkDelete) @register_model_view(Job) @@ -204,9 +200,7 @@ class ObjectChangeListView(generic.ObjectListView): filterset_form = forms.ObjectChangeFilterForm table = tables.ObjectChangeTable template_name = 'core/objectchange_list.html' - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) @register_model_view(ObjectChange) diff --git a/netbox/dcim/object_actions.py b/netbox/dcim/object_actions.py new file mode 100644 index 000000000..d9a124733 --- /dev/null +++ b/netbox/dcim/object_actions.py @@ -0,0 +1,18 @@ +from django.utils.translation import gettext as _ + +from netbox.object_actions import ObjectAction + +__all__ = ( + 'BulkDisconnect', +) + + +class BulkDisconnect(ObjectAction): + """ + Disconnect each of a set of objects to which a cable is connected. + """ + name = 'bulk_disconnect' + label = _('Disconnect Selected') + bulk = True + permissions_required = {'change'} + template_name = 'buttons/bulk_disconnect.html' diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 304438698..5614310f5 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -15,7 +15,7 @@ from circuits.models import Circuit, CircuitTermination from extras.views import ObjectConfigContextView, ObjectRenderConfigView from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable -from netbox.constants import DEFAULT_ACTION_PERMISSIONS +from netbox.object_actions import * from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count @@ -34,6 +34,7 @@ from wireless.models import WirelessLAN from . import filtersets, forms, tables from .choices import DeviceFaceChoices, InterfaceModeChoices from .models import * +from .object_actions import BulkDisconnect CABLE_TERMINATION_TYPES = { 'dcim.consoleport': ConsolePort, @@ -49,11 +50,6 @@ CABLE_TERMINATION_TYPES = { class DeviceComponentsView(generic.ObjectChildrenView): - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - 'bulk_disconnect': {'change'}, - } queryset = Device.objects.all() def get_children(self, request, parent): @@ -61,10 +57,7 @@ class DeviceComponentsView(generic.ObjectChildrenView): class DeviceTypeComponentsView(generic.ObjectChildrenView): - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) queryset = DeviceType.objects.all() template_name = 'dcim/devicetype/component_templates.html' viewname = None # Used for return_url resolution @@ -78,8 +71,9 @@ class DeviceTypeComponentsView(generic.ObjectChildrenView): } -class ModuleTypeComponentsView(DeviceComponentsView): +class ModuleTypeComponentsView(generic.ObjectChildrenView): queryset = ModuleType.objects.all() + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) template_name = 'dcim/moduletype/component_templates.html' viewname = None # Used for return_url resolution @@ -2157,7 +2151,7 @@ class DeviceConsolePortsView(DeviceComponentsView): table = tables.DeviceConsolePortTable filterset = filtersets.ConsolePortFilterSet filterset_form = forms.ConsolePortFilterForm - template_name = 'dcim/device/consoleports.html', + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Console Ports'), badge=lambda obj: obj.console_port_count, @@ -2173,7 +2167,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView): table = tables.DeviceConsoleServerPortTable filterset = filtersets.ConsoleServerPortFilterSet filterset_form = forms.ConsoleServerPortFilterForm - template_name = 'dcim/device/consoleserverports.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Console Server Ports'), badge=lambda obj: obj.console_server_port_count, @@ -2189,7 +2183,7 @@ class DevicePowerPortsView(DeviceComponentsView): table = tables.DevicePowerPortTable filterset = filtersets.PowerPortFilterSet filterset_form = forms.PowerPortFilterForm - template_name = 'dcim/device/powerports.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Power Ports'), badge=lambda obj: obj.power_port_count, @@ -2205,7 +2199,7 @@ class DevicePowerOutletsView(DeviceComponentsView): table = tables.DevicePowerOutletTable filterset = filtersets.PowerOutletFilterSet filterset_form = forms.PowerOutletFilterForm - template_name = 'dcim/device/poweroutlets.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Power Outlets'), badge=lambda obj: obj.power_outlet_count, @@ -2221,6 +2215,7 @@ class DeviceInterfacesView(DeviceComponentsView): table = tables.DeviceInterfaceTable filterset = filtersets.InterfaceFilterSet filterset_form = forms.InterfaceFilterForm + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) template_name = 'dcim/device/interfaces.html' tab = ViewTab( label=_('Interfaces'), @@ -2243,7 +2238,7 @@ class DeviceFrontPortsView(DeviceComponentsView): table = tables.DeviceFrontPortTable filterset = filtersets.FrontPortFilterSet filterset_form = forms.FrontPortFilterForm - template_name = 'dcim/device/frontports.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Front Ports'), badge=lambda obj: obj.front_port_count, @@ -2259,7 +2254,7 @@ class DeviceRearPortsView(DeviceComponentsView): table = tables.DeviceRearPortTable filterset = filtersets.RearPortFilterSet filterset_form = forms.RearPortFilterForm - template_name = 'dcim/device/rearports.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete, BulkDisconnect) tab = ViewTab( label=_('Rear Ports'), badge=lambda obj: obj.rear_port_count, @@ -2275,11 +2270,7 @@ class DeviceModuleBaysView(DeviceComponentsView): table = tables.DeviceModuleBayTable filterset = filtersets.ModuleBayFilterSet filterset_form = forms.ModuleBayFilterForm - template_name = 'dcim/device/modulebays.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) tab = ViewTab( label=_('Module Bays'), badge=lambda obj: obj.module_bay_count, @@ -2295,11 +2286,7 @@ class DeviceDeviceBaysView(DeviceComponentsView): table = tables.DeviceDeviceBayTable filterset = filtersets.DeviceBayFilterSet filterset_form = forms.DeviceBayFilterForm - template_name = 'dcim/device/devicebays.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) tab = ViewTab( label=_('Device Bays'), badge=lambda obj: obj.device_bay_count, @@ -2315,11 +2302,7 @@ class DeviceInventoryView(DeviceComponentsView): table = tables.DeviceInventoryItemTable filterset = filtersets.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm - template_name = 'dcim/device/inventory.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) tab = ViewTab( label=_('Inventory Items'), badge=lambda obj: obj.inventory_item_count, @@ -2472,11 +2455,7 @@ class ConsolePortListView(generic.ObjectListView): filterset = filtersets.ConsolePortFilterSet filterset_form = forms.ConsolePortFilterForm table = tables.ConsolePortTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(ConsolePort) @@ -2547,11 +2526,7 @@ class ConsoleServerPortListView(generic.ObjectListView): filterset = filtersets.ConsoleServerPortFilterSet filterset_form = forms.ConsoleServerPortFilterForm table = tables.ConsoleServerPortTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(ConsoleServerPort) @@ -2622,11 +2597,7 @@ class PowerPortListView(generic.ObjectListView): filterset = filtersets.PowerPortFilterSet filterset_form = forms.PowerPortFilterForm table = tables.PowerPortTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(PowerPort) @@ -2697,11 +2668,7 @@ class PowerOutletListView(generic.ObjectListView): filterset = filtersets.PowerOutletFilterSet filterset_form = forms.PowerOutletFilterForm table = tables.PowerOutletTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(PowerOutlet) @@ -2772,11 +2739,7 @@ class InterfaceListView(generic.ObjectListView): filterset = filtersets.InterfaceFilterSet filterset_form = forms.InterfaceFilterForm table = tables.InterfaceTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(Interface) @@ -2920,11 +2883,7 @@ class FrontPortListView(generic.ObjectListView): filterset = filtersets.FrontPortFilterSet filterset_form = forms.FrontPortFilterForm table = tables.FrontPortTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(FrontPort) @@ -2995,11 +2954,7 @@ class RearPortListView(generic.ObjectListView): filterset = filtersets.RearPortFilterSet filterset_form = forms.RearPortFilterForm table = tables.RearPortTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(RearPort) @@ -3070,11 +3025,7 @@ class ModuleBayListView(generic.ObjectListView): filterset = filtersets.ModuleBayFilterSet filterset_form = forms.ModuleBayFilterForm table = tables.ModuleBayTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(ModuleBay) @@ -3136,11 +3087,7 @@ class DeviceBayListView(generic.ObjectListView): filterset = filtersets.DeviceBayFilterSet filterset_form = forms.DeviceBayFilterForm table = tables.DeviceBayTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(DeviceBay) @@ -3283,11 +3230,7 @@ class InventoryItemListView(generic.ObjectListView): filterset = filtersets.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable - template_name = 'dcim/component_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Add, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete) @register_model_view(InventoryItem) @@ -3627,9 +3570,7 @@ class ConsoleConnectionsListView(generic.ObjectListView): filterset_form = forms.ConsoleConnectionFilterForm table = tables.ConsoleConnectionTable template_name = 'dcim/connections_list.html' - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) def get_extra_context(self, request): return { @@ -3643,9 +3584,7 @@ class PowerConnectionsListView(generic.ObjectListView): filterset_form = forms.PowerConnectionFilterForm table = tables.PowerConnectionTable template_name = 'dcim/connections_list.html' - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) def get_extra_context(self, request): return { @@ -3659,9 +3598,7 @@ class InterfaceConnectionsListView(generic.ObjectListView): filterset_form = forms.InterfaceConnectionFilterForm table = tables.InterfaceConnectionTable template_name = 'dcim/connections_list.html' - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) def get_extra_context(self, request): return { diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ea465a4a4..7f274491b 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -14,12 +14,13 @@ from jinja2.exceptions import TemplateError from core.choices import ManagedFileRootPathChoices from core.models import Job +from core.object_actions import BulkSync from dcim.models import Device, DeviceRole, Platform from extras.choices import LogLevelChoices from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class from extras.utils import SharedObjectViewMixin -from netbox.constants import DEFAULT_ACTION_PERMISSIONS +from netbox.object_actions import * from netbox.views import generic from netbox.views.generic.mixins import TableMixin from utilities.forms import ConfirmationForm, get_field_value @@ -232,11 +233,7 @@ class ExportTemplateListView(generic.ObjectListView): filterset = filtersets.ExportTemplateFilterSet filterset_form = forms.ExportTemplateFilterForm table = tables.ExportTemplateTable - template_name = 'extras/exporttemplate_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_sync': {'sync'}, - } + actions = (Add, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete) @register_model_view(ExportTemplate) @@ -347,9 +344,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView): filterset = filtersets.TableConfigFilterSet filterset_form = forms.TableConfigFilterForm table = tables.TableConfigTable - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) @register_model_view(TableConfig) @@ -759,12 +754,7 @@ class ConfigContextListView(generic.ObjectListView): filterset_form = forms.ConfigContextFilterForm table = tables.ConfigContextTable template_name = 'extras/configcontext_list.html' - actions = { - 'add': {'add'}, - 'bulk_edit': {'change'}, - 'bulk_delete': {'delete'}, - 'bulk_sync': {'sync'}, - } + actions = (Add, BulkSync, BulkEdit, BulkDelete) @register_model_view(ConfigContext) @@ -877,11 +867,7 @@ class ConfigTemplateListView(generic.ObjectListView): filterset = filtersets.ConfigTemplateFilterSet filterset_form = forms.ConfigTemplateFilterForm table = tables.ConfigTemplateTable - template_name = 'extras/configtemplate_list.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_sync': {'sync'}, - } + actions = (Add, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete) @register_model_view(ConfigTemplate) @@ -992,9 +978,7 @@ class ImageAttachmentListView(generic.ObjectListView): filterset = filtersets.ImageAttachmentFilterSet filterset_form = forms.ImageAttachmentFilterForm table = tables.ImageAttachmentTable - actions = { - 'export': {'view'}, - } + actions = (BulkExport,) @register_model_view(ImageAttachment, 'add', detail=False) @@ -1038,12 +1022,7 @@ class JournalEntryListView(generic.ObjectListView): filterset = filtersets.JournalEntryFilterSet filterset_form = forms.JournalEntryFilterForm table = tables.JournalEntryTable - actions = { - 'export': {'view'}, - 'bulk_import': {'add'}, - 'bulk_edit': {'change'}, - 'bulk_delete': {'delete'}, - } + actions = (BulkImport, BulkSync, BulkEdit, BulkDelete) @register_model_view(JournalEntry) diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 8d20fed45..f088c8e4a 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -28,7 +28,8 @@ ADVISORY_LOCK_KEYS = { 'job-schedules': 110100, } -# Default view action permission mapping +# TODO: Remove in NetBox v4.6 +# Legacy default view action permission mapping DEFAULT_ACTION_PERMISSIONS = { 'add': {'add'}, 'export': {'view'}, diff --git a/netbox/netbox/object_actions.py b/netbox/netbox/object_actions.py index 0e8f91155..123e64f84 100644 --- a/netbox/netbox/object_actions.py +++ b/netbox/netbox/object_actions.py @@ -10,6 +10,7 @@ __all__ = ( 'BulkEdit', 'BulkExport', 'BulkImport', + 'BulkRename', 'Delete', 'Edit', 'ObjectAction', @@ -23,11 +24,13 @@ class ObjectAction: permissions_required = set() url_kwargs = [] - def get_context(self, context, obj): - viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{self.name}' - url = reverse(viewname, kwargs={kwarg: getattr(obj, kwarg) for kwarg in self.url_kwargs}) + @classmethod + def get_context(cls, context, obj): + viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{cls.name}' + url = reverse(viewname, kwargs={kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs}) return { 'url': url, + 'label': cls.label, } @@ -106,18 +109,29 @@ class BulkEdit(ObjectAction): Change the value of one or more fields on a set of objects. """ name = 'bulk_edit' - label = _('Edit') + label = _('Edit Selected') bulk = True permissions_required = {'change'} template_name = 'buttons/bulk_edit.html' +class BulkRename(ObjectAction): + """ + Rename multiple objects at once. + """ + name = 'bulk_rename' + label = _('Rename Selected') + bulk = True + permissions_required = {'change'} + template_name = 'buttons/bulk_rename.html' + + class BulkDelete(ObjectAction): """ Delete each of a set of objects. """ name = 'bulk_delete' - label = _('Delete') + label = _('Delete Selected') bulk = True permissions_required = {'delete'} template_name = 'buttons/bulk_delete.html' diff --git a/netbox/netbox/views/generic/mixins.py b/netbox/netbox/views/generic/mixins.py index 1576e127e..f43512b09 100644 --- a/netbox/netbox/views/generic/mixins.py +++ b/netbox/netbox/views/generic/mixins.py @@ -1,6 +1,7 @@ from django.shortcuts import get_object_or_404 from extras.models import TableConfig +from netbox import object_actions from utilities.permissions import get_permission_for_model __all__ = ( @@ -18,7 +19,27 @@ class ActionsMixin: Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map with custom actions, such as bulk_sync. """ - # actions = DEFAULT_ACTION_PERMISSIONS + + # TODO: Remove in NetBox v4.6 + @staticmethod + def _get_legacy_action(name): + """ + Given a legacy action name, return the corresponding action class. + """ + action = { + 'add': object_actions.Add, + 'edit': object_actions.Edit, + 'delete': object_actions.Delete, + 'export': object_actions.BulkExport, + 'bulk_import': object_actions.BulkImport, + 'bulk_edit': object_actions.BulkEdit, + 'bulk_rename': object_actions.BulkRename, + 'bulk_delete': object_actions.BulkDelete, + }.get(name) + if name is None: + raise ValueError(f"Unknown action: {action}") + + return action def get_permitted_actions(self, user, model=None): """ @@ -29,7 +50,8 @@ class ActionsMixin: # Resolve required permissions for each action permitted_actions = [] for action in self.actions: - perms = action if type(action) is str else action.permissions_required # Backward compatibility + # Backward compatibility + perms = self._get_legacy_action(action) if type(action) is str else action.permissions_required required_permissions = [ get_permission_for_model(model, perm) for perm in perms ] diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index f33b0ba6c..7ac2e7b11 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -143,7 +143,8 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): # Determine the available actions actions = self.get_permitted_actions(request.user, model=self.child_model) - has_bulk_actions = any([a.startswith('bulk_') for a in actions]) + # has_bulk_actions = any([a.startswith('bulk_') for a in actions]) + has_bulk_actions = True table_data = self.prep_table_data(request, child_objects, instance) table = self.get_table(table_data, request, has_bulk_actions) diff --git a/netbox/templates/dcim/component_list.html b/netbox/templates/dcim/component_list.html deleted file mode 100644 index 6f91aff3e..000000000 --- a/netbox/templates/dcim/component_list.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'generic/object_list.html' %} -{% load buttons %} -{% load helpers %} -{% load i18n %} - -{% block bulk_buttons %} -
- {% if 'bulk_edit' in actions %} - {% bulk_edit_button model query_params=request.GET %} - {% endif %} - {% if 'bulk_rename' in actions %} - {% with bulk_rename_view=model|validated_viewname:"bulk_rename" %} - - {% endwith %} - {% endif %} -
- {% if 'bulk_delete' in actions %} - {% bulk_delete_button model query_params=request.GET %} - {% endif %} -{% endblock %} diff --git a/netbox/templates/dcim/device/components_base.html b/netbox/templates/dcim/device/components_base.html deleted file mode 100644 index 47b0d7aab..000000000 --- a/netbox/templates/dcim/device/components_base.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'generic/object_children.html' %} -{% load helpers %} - -{% block bulk_edit_controls %} - {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} - {% if 'bulk_edit' in actions and bulk_edit_view %} - - {% endif %} - {% endwith %} - {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} - {% if 'bulk_rename' in actions and bulk_rename_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_edit_controls %} diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html deleted file mode 100644 index d8de85868..000000000 --- a/netbox/templates/dcim/device/consoleports.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_consoleport %} -
- - {% trans "Add Console Ports" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html deleted file mode 100644 index 29efdb65b..000000000 --- a/netbox/templates/dcim/device/consoleserverports.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_consoleserverport %} -
- - {% trans "Add Console Server Ports" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html deleted file mode 100644 index b558cdfb4..000000000 --- a/netbox/templates/dcim/device/devicebays.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load i18n %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_devicebay %} -
- - {% trans "Add Device Bays" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html deleted file mode 100644 index 4d7f14769..000000000 --- a/netbox/templates/dcim/device/frontports.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_frontport %} -
- - {% trans "Add Front Ports" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 8d46d77ea..be81f97cb 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -1,30 +1,5 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} +{% extends 'generic/object_children.html' %} {% block table_controls %} - {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} + {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} {% endblock table_controls %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_interface %} - - {% trans "Add Interfaces" %} - - {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html deleted file mode 100644 index cbc113d86..000000000 --- a/netbox/templates/dcim/device/inventory.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load i18n %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_inventoryitem %} -
- - {% trans "Add Inventory Item" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html deleted file mode 100644 index c23532efe..000000000 --- a/netbox/templates/dcim/device/modulebays.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load i18n %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_modulebay %} -
- - {% trans "Add Module Bays" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html deleted file mode 100644 index 66c765181..000000000 --- a/netbox/templates/dcim/device/poweroutlets.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_poweroutlet %} -
- - {% trans "Add Power Outlets" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html deleted file mode 100644 index a299130a4..000000000 --- a/netbox/templates/dcim/device/powerports.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_powerport %} -
- - {% trans "Add Power Port" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html deleted file mode 100644 index 184e80d99..000000000 --- a/netbox/templates/dcim/device/rearports.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'dcim/device/components_base.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_delete_controls %} - {{ block.super }} - {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} - {% if 'bulk_disconnect' in actions and bulk_disconnect_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_delete_controls %} - -{% block bulk_extra_controls %} - {{ block.super }} - {% if perms.dcim.add_rearport %} -
- - {% trans "Add Rear Ports" %} - -
- {% endif %} -{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/devicetype/component_templates.html b/netbox/templates/dcim/devicetype/component_templates.html index 291c7c988..4d1361942 100644 --- a/netbox/templates/dcim/devicetype/component_templates.html +++ b/netbox/templates/dcim/devicetype/component_templates.html @@ -3,23 +3,23 @@ {% load i18n %} {% load perms %} -{% block bulk_edit_controls %} - {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} - {% if 'bulk_edit' in actions and bulk_edit_view %} - - {% endif %} - {% endwith %} - {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} - {% if 'bulk_rename' in actions and bulk_rename_view %} - - {% endif %} - {% endwith %} -{% endblock bulk_edit_controls %} +{#{% block bulk_edit_controls %}#} +{# {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}#} +{# {% if 'bulk_edit' in actions and bulk_edit_view %}#} +{# #} +{# {% endif %}#} +{# {% endwith %}#} +{# {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}#} +{# {% if 'bulk_rename' in actions and bulk_rename_view %}#} +{# #} +{# {% endif %}#} +{# {% endwith %}#} +{#{% endblock bulk_edit_controls %}#} diff --git a/netbox/templates/extras/configtemplate_list.html b/netbox/templates/extras/configtemplate_list.html deleted file mode 100644 index c9db67e36..000000000 --- a/netbox/templates/extras/configtemplate_list.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'generic/object_list.html' %} -{% load i18n %} - -{% block bulk_buttons %} - {% if perms.extras.sync_configtemplate %} - - {% endif %} - {{ block.super }} -{% endblock %} diff --git a/netbox/templates/extras/exporttemplate_list.html b/netbox/templates/extras/exporttemplate_list.html deleted file mode 100644 index 4284336a5..000000000 --- a/netbox/templates/extras/exporttemplate_list.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'generic/object_list.html' %} -{% load i18n %} - -{% block bulk_buttons %} - {% if perms.extras.sync_configcontext %} - - {% endif %} - {{ block.super }} -{% endblock %} diff --git a/netbox/templates/generic/object_children.html b/netbox/templates/generic/object_children.html index c4d6dc52a..93d7323e4 100644 --- a/netbox/templates/generic/object_children.html +++ b/netbox/templates/generic/object_children.html @@ -1,4 +1,5 @@ {% extends base_template %} +{% load buttons %} {% load helpers %} {% load i18n %} @@ -36,34 +37,37 @@ Context:
{% block bulk_controls %} -
+ {% for name, action in actions.items %} + {% bulk_action_button action model %} + {% endfor %} +{#
#} {# Bulk edit buttons #} - {% block bulk_edit_controls %} - {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} - {% if 'bulk_edit' in actions and bulk_edit_view %} - - {% endif %} - {% endwith %} - {% endblock bulk_edit_controls %} -
-
+{# {% block bulk_edit_controls %}#} +{# {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}#} +{# {% if 'bulk_edit' in actions and bulk_edit_view %}#} +{# #} +{# {% endif %}#} +{# {% endwith %}#} +{# {% endblock bulk_edit_controls %}#} +{#
#} +{#
#} {# Bulk delete buttons #} - {% block bulk_delete_controls %} - {% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %} - {% if 'bulk_delete' in actions and bulk_delete_view %} - - {% endif %} - {% endwith %} - {% endblock bulk_delete_controls %} -
+{# {% block bulk_delete_controls %}#} +{# {% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}#} +{# {% if 'bulk_delete' in actions and bulk_delete_view %}#} +{# #} +{# {% endif %}#} +{# {% endwith %}#} +{# {% endblock bulk_delete_controls %}#} +{#
#} {# Other bulk action buttons #} {% block bulk_extra_controls %}{% endblock %} {% endblock bulk_controls %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 491569be0..ec15d17de 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -88,9 +88,7 @@ Context:
{% for name, action in actions.items %} - {% if action.bulk %} - {% bulk_action_button action model %} - {% endif %} + {% bulk_action_button action model %} {% endfor %}
@@ -120,9 +118,7 @@ Context: {% block bulk_buttons %}
{% for name, action in actions.items %} - {% if action.bulk %} - {% bulk_action_button action model %} - {% endif %} + {% bulk_action_button action model %} {% endfor %}
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html deleted file mode 100644 index f74bd9770..000000000 --- a/netbox/templates/virtualization/virtualmachine/interfaces.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'generic/object_children.html' %} -{% load helpers %} -{% load i18n %} - -{% block bulk_edit_controls %} - {{ block.super }} - {% if 'bulk_rename' in actions %} - - {% endif %} -{% endblock bulk_edit_controls %} diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index aca0a14eb..83794f21f 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 +from netbox.object_actions import BulkDelete, BulkEdit, BulkExport, BulkImport from netbox.views import generic from utilities.query import count_related from utilities.views import GetRelatedModelsMixin, register_model_view @@ -349,12 +350,7 @@ class ContactAssignmentListView(generic.ObjectListView): filterset = filtersets.ContactAssignmentFilterSet filterset_form = forms.ContactAssignmentFilterForm table = tables.ContactAssignmentTable - actions = { - 'export': {'view'}, - 'bulk_import': {'add'}, - 'bulk_edit': {'change'}, - 'bulk_delete': {'delete'}, - } + actions = (BulkExport, BulkImport, BulkEdit, BulkDelete) @register_model_view(ContactAssignment, 'add', detail=False) diff --git a/netbox/utilities/templates/buttons/bulk_disconnect.html b/netbox/utilities/templates/buttons/bulk_disconnect.html new file mode 100644 index 000000000..d5e6e6cd7 --- /dev/null +++ b/netbox/utilities/templates/buttons/bulk_disconnect.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% if url %} + +{% endif %} diff --git a/netbox/utilities/templates/buttons/bulk_remove_devices.html b/netbox/utilities/templates/buttons/bulk_remove_devices.html new file mode 100644 index 000000000..3a5d45420 --- /dev/null +++ b/netbox/utilities/templates/buttons/bulk_remove_devices.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% if url %} + +{% endif %} diff --git a/netbox/utilities/templates/buttons/bulk_rename.html b/netbox/utilities/templates/buttons/bulk_rename.html new file mode 100644 index 000000000..aa1588e8d --- /dev/null +++ b/netbox/utilities/templates/buttons/bulk_rename.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% if url %} + +{% endif %} diff --git a/netbox/utilities/templates/buttons/bulk_sync.html b/netbox/utilities/templates/buttons/bulk_sync.html new file mode 100644 index 000000000..2563f0a95 --- /dev/null +++ b/netbox/utilities/templates/buttons/bulk_sync.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% if url %} + +{% endif %} diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index 086586c74..37e1e514f 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -224,9 +224,13 @@ def bulk_delete_button(context, model, action='bulk_delete', query_params=None): @register.simple_tag(takes_context=True) def action_button(context, action, obj): + if action.bulk: + return '' return loader.render_to_string(action.template_name, action.get_context(context, obj)) @register.simple_tag(takes_context=True) def bulk_action_button(context, action, model): + if not action.bulk: + return '' return loader.render_to_string(action.template_name, action.get_context(context, model)) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 6013a56f4..7a638498d 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -13,7 +13,7 @@ from dcim.tables import DeviceTable from extras.views import ObjectConfigContextView, ObjectRenderConfigView from ipam.models import IPAddress, VLANGroup from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable -from netbox.constants import DEFAULT_ACTION_PERMISSIONS +from netbox.object_actions import * from netbox.views import generic from utilities.query import count_related from utilities.query_functions import CollateAsChar @@ -223,13 +223,7 @@ class ClusterDevicesView(generic.ObjectChildrenView): filterset = DeviceFilterSet filterset_form = DeviceFilterForm template_name = 'virtualization/cluster/devices.html' - actions = { - 'add': {'add'}, - 'export': {'view'}, - 'bulk_import': {'add'}, - 'bulk_edit': {'change'}, - 'bulk_remove_devices': {'change'}, - } + actions = (Add, BulkExport, BulkImport, BulkEdit) tab = ViewTab( label=_('Devices'), badge=lambda obj: obj.devices.count(), @@ -386,11 +380,7 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView): table = tables.VirtualMachineVMInterfaceTable filterset = filtersets.VMInterfaceFilterSet filterset_form = forms.VMInterfaceFilterForm - template_name = 'virtualization/virtualmachine/interfaces.html' - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) tab = ViewTab( label=_('Interfaces'), badge=lambda obj: obj.interface_count, @@ -412,17 +402,13 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView): table = tables.VirtualMachineVirtualDiskTable filterset = filtersets.VirtualDiskFilterSet filterset_form = forms.VirtualDiskFilterForm - template_name = 'virtualization/virtualmachine/virtual_disks.html' + actions = (Edit, Delete, BulkEdit, BulkRename, BulkDelete) tab = ViewTab( label=_('Virtual Disks'), badge=lambda obj: obj.virtual_disk_count, permission='virtualization.view_virtualdisk', weight=500 ) - actions = { - **DEFAULT_ACTION_PERMISSIONS, - 'bulk_rename': {'change'}, - } def get_children(self, request, parent): return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags')