diff --git a/netbox/core/object_actions.py b/netbox/core/object_actions.py
new file mode 100644
index 000000000..81b5fb2c8
--- /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')
+ multi = True
+ permissions_required = {'sync'}
+ template_name = 'core/buttons/bulk_sync.html'
diff --git a/netbox/core/views.py b/netbox/core/views.py
index ef52147f1..1bd952b62 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 AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.registry import registry
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
@@ -138,14 +139,13 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable
- actions = {
- 'bulk_delete': {'delete'},
- }
+ actions = (BulkDelete,)
@register_model_view(DataFile)
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
+ actions = (DeleteObject,)
@register_model_view(DataFile, 'delete')
@@ -170,15 +170,13 @@ 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)
class JobView(generic.ObjectView):
queryset = Job.objects.all()
+ actions = (DeleteObject,)
@register_model_view(Job, 'delete')
@@ -204,9 +202,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)
@@ -274,6 +270,7 @@ class ConfigRevisionListView(generic.ObjectListView):
filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable
+ actions = (AddObject, BulkExport)
@register_model_view(ConfigRevision)
diff --git a/netbox/dcim/object_actions.py b/netbox/dcim/object_actions.py
new file mode 100644
index 000000000..00a409274
--- /dev/null
+++ b/netbox/dcim/object_actions.py
@@ -0,0 +1,38 @@
+from django.utils.translation import gettext as _
+
+from netbox.object_actions import ObjectAction
+
+__all__ = (
+ 'BulkAddComponents',
+ 'BulkDisconnect',
+)
+
+
+class BulkAddComponents(ObjectAction):
+ """
+ Add components to the selected devices.
+ """
+ label = _('Add Components')
+ multi = True
+ permissions_required = {'change'}
+ template_name = 'dcim/buttons/bulk_add_components.html'
+
+ @classmethod
+ def get_context(cls, context, obj):
+ return {
+ 'perms': context.get('perms'),
+ 'request': context.get('request'),
+ 'formaction': context.get('formaction'),
+ 'label': cls.label,
+ }
+
+
+class BulkDisconnect(ObjectAction):
+ """
+ Disconnect each of a set of objects to which a cable is connected.
+ """
+ name = 'bulk_disconnect'
+ label = _('Disconnect Selected')
+ multi = True
+ permissions_required = {'change'}
+ template_name = 'dcim/buttons/bulk_disconnect.html'
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 304438698..6d72f5c43 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 BulkAddComponents, 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,12 +57,8 @@ class DeviceComponentsView(generic.ObjectChildrenView):
class DeviceTypeComponentsView(generic.ObjectChildrenView):
- actions = {
- **DEFAULT_ACTION_PERMISSIONS,
- 'bulk_rename': {'change'},
- }
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
queryset = DeviceType.objects.all()
- template_name = 'dcim/devicetype/component_templates.html'
viewname = None # Used for return_url resolution
def get_children(self, request, parent):
@@ -78,9 +70,9 @@ class DeviceTypeComponentsView(generic.ObjectChildrenView):
}
-class ModuleTypeComponentsView(DeviceComponentsView):
+class ModuleTypeComponentsView(generic.ObjectChildrenView):
queryset = ModuleType.objects.all()
- template_name = 'dcim/moduletype/component_templates.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
viewname = None # Used for return_url resolution
def get_children(self, request, parent):
@@ -2116,7 +2108,7 @@ class DeviceListView(generic.ObjectListView):
filterset = filtersets.DeviceFilterSet
filterset_form = forms.DeviceFilterForm
table = tables.DeviceTable
- template_name = 'dcim/device_list.html'
+ actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
@register_model_view(Device)
@@ -2157,7 +2149,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm
- template_name = 'dcim/device/consoleports.html',
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.console_port_count,
@@ -2173,7 +2165,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm
- template_name = 'dcim/device/consoleserverports.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.console_server_port_count,
@@ -2189,7 +2181,7 @@ class DevicePowerPortsView(DeviceComponentsView):
table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm
- template_name = 'dcim/device/powerports.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.power_port_count,
@@ -2205,7 +2197,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm
- template_name = 'dcim/device/poweroutlets.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.power_outlet_count,
@@ -2221,6 +2213,7 @@ class DeviceInterfacesView(DeviceComponentsView):
table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
template_name = 'dcim/device/interfaces.html'
tab = ViewTab(
label=_('Interfaces'),
@@ -2243,7 +2236,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm
- template_name = 'dcim/device/frontports.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.front_port_count,
@@ -2259,7 +2252,7 @@ class DeviceRearPortsView(DeviceComponentsView):
table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm
- template_name = 'dcim/device/rearports.html'
+ actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rear_port_count,
@@ -2275,11 +2268,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 = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.module_bay_count,
@@ -2295,11 +2284,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 = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.device_bay_count,
@@ -2315,11 +2300,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 = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count,
@@ -2472,11 +2453,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(ConsolePort)
@@ -2547,11 +2524,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(ConsoleServerPort)
@@ -2622,11 +2595,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(PowerPort)
@@ -2697,11 +2666,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(PowerOutlet)
@@ -2772,11 +2737,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(Interface)
@@ -2920,11 +2881,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(FrontPort)
@@ -2995,11 +2952,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(RearPort)
@@ -3070,11 +3023,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(ModuleBay)
@@ -3136,11 +3085,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(DeviceBay)
@@ -3283,11 +3228,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 = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
@register_model_view(InventoryItem)
@@ -3627,9 +3568,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 +3582,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 +3596,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..7216c4eec 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 = (AddObject, 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)
@@ -758,13 +753,7 @@ class ConfigContextListView(generic.ObjectListView):
filterset = filtersets.ConfigContextFilterSet
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 = (AddObject, BulkSync, BulkEdit, BulkDelete)
@register_model_view(ConfigContext)
@@ -877,11 +866,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 = (AddObject, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete)
@register_model_view(ConfigTemplate)
@@ -992,9 +977,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 +1021,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, 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
new file mode 100644
index 000000000..b427d58cd
--- /dev/null
+++ b/netbox/netbox/object_actions.py
@@ -0,0 +1,176 @@
+from django.urls import reverse
+from django.utils.translation import gettext as _
+
+from core.models import ObjectType
+from extras.models import ExportTemplate
+from utilities.querydict import prepare_cloned_fields
+
+__all__ = (
+ 'AddObject',
+ 'BulkDelete',
+ 'BulkEdit',
+ 'BulkExport',
+ 'BulkImport',
+ 'BulkRename',
+ 'CloneObject',
+ 'DeleteObject',
+ 'EditObject',
+ 'ObjectAction',
+)
+
+
+class ObjectAction:
+ """
+ Base class for single- and multi-object operations.
+
+ Params:
+ name: The action name appended to the module for view resolution
+ label: Human-friendly label for the rendered button
+ multi: Set to True if this action is performed by selecting multiple objects (i.e. using a table)
+ permissions_required: The set of permissions a user must have to perform the action
+ url_kwargs: The set of URL keyword arguments to pass when resolving the view's URL
+ """
+ name = ''
+ label = None
+ multi = False
+ permissions_required = set()
+ url_kwargs = []
+
+ @classmethod
+ def get_url(cls, obj):
+ viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{cls.name}'
+ kwargs = {
+ kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs
+ }
+ return reverse(viewname, kwargs=kwargs)
+
+ @classmethod
+ def get_context(cls, context, obj):
+ return {
+ 'url': cls.get_url(obj),
+ 'label': cls.label,
+ }
+
+
+class AddObject(ObjectAction):
+ """
+ Create a new object.
+ """
+ name = 'add'
+ label = _('Add')
+ permissions_required = {'add'}
+ template_name = 'buttons/add.html'
+
+
+class CloneObject(ObjectAction):
+ """
+ Populate the new object form with select details from an existing object.
+ """
+ name = 'add'
+ label = _('Clone')
+ permissions_required = {'add'}
+ template_name = 'buttons/clone.html'
+
+ @classmethod
+ def get_context(cls, context, obj):
+ param_string = prepare_cloned_fields(obj).urlencode()
+ url = f'{cls.get_url(obj)}?{param_string}' if param_string else None
+ return {
+ 'url': url,
+ 'label': cls.label,
+ }
+
+
+class EditObject(ObjectAction):
+ """
+ Edit a single object.
+ """
+ name = 'edit'
+ label = _('Edit')
+ permissions_required = {'change'}
+ url_kwargs = ['pk']
+ template_name = 'buttons/edit.html'
+
+
+class DeleteObject(ObjectAction):
+ """
+ Delete a single object.
+ """
+ name = 'delete'
+ label = _('Delete')
+ permissions_required = {'delete'}
+ url_kwargs = ['pk']
+ template_name = 'buttons/delete.html'
+
+
+class BulkImport(ObjectAction):
+ """
+ Import multiple objects at once.
+ """
+ name = 'bulk_import'
+ label = _('Import')
+ permissions_required = {'add'}
+ template_name = 'buttons/import.html'
+
+
+class BulkExport(ObjectAction):
+ """
+ Export multiple objects at once.
+ """
+ name = 'export'
+ label = _('Export')
+ permissions_required = {'view'}
+ template_name = 'buttons/export.html'
+
+ @classmethod
+ def get_context(cls, context, model):
+ object_type = ObjectType.objects.get_for_model(model)
+ user = context['request'].user
+
+ # Determine if the "all data" export returns CSV or YAML
+ data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV'
+
+ # Retrieve all export templates for this model
+ export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
+
+ return {
+ 'label': cls.label,
+ 'perms': context['perms'],
+ 'object_type': object_type,
+ 'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
+ 'export_templates': export_templates,
+ 'data_format': data_format,
+ }
+
+
+class BulkEdit(ObjectAction):
+ """
+ Change the value of one or more fields on a set of objects.
+ """
+ name = 'bulk_edit'
+ label = _('Edit Selected')
+ multi = True
+ permissions_required = {'change'}
+ template_name = 'buttons/bulk_edit.html'
+
+
+class BulkRename(ObjectAction):
+ """
+ Rename multiple objects at once.
+ """
+ name = 'bulk_rename'
+ label = _('Rename Selected')
+ multi = 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 Selected')
+ multi = True
+ permissions_required = {'delete'}
+ template_name = 'buttons/bulk_delete.html'
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index b52d12d98..08b060634 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -22,6 +22,7 @@ from core.models import ObjectType
from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate
+from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@@ -60,6 +61,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
template_name = 'generic/object_list.html'
filterset = None
filterset_form = None
+ actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
@@ -150,13 +152,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Determine the available actions
actions = self.get_permitted_actions(request.user)
- has_bulk_actions = any([a.startswith('bulk_') for a in actions])
+ has_table_actions = any(action.multi for action in actions)
if 'export' in request.GET:
# Export the current table view
if request.GET['export'] == 'table':
- table = self.get_table(self.queryset, request, has_bulk_actions)
+ table = self.get_table(self.queryset, request, has_table_actions)
columns = [name for name, _ in table.selected_columns]
return self.export_table(table, columns)
@@ -174,11 +176,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Fall back to default table/YAML export
else:
- table = self.get_table(self.queryset, request, has_bulk_actions)
+ table = self.get_table(self.queryset, request, has_table_actions)
return self.export_table(table)
# Render the objects table
- table = self.get_table(self.queryset, request, has_bulk_actions)
+ table = self.get_table(self.queryset, request, has_table_actions)
# If this is an HTMX request, return only the rendered table HTML
if htmx_partial(request):
diff --git a/netbox/netbox/views/generic/mixins.py b/netbox/netbox/views/generic/mixins.py
index 5f9f62120..079164ed9 100644
--- a/netbox/netbox/views/generic/mixins.py
+++ b/netbox/netbox/views/generic/mixins.py
@@ -1,7 +1,7 @@
from django.shortcuts import get_object_or_404
from extras.models import TableConfig
-from netbox.constants import DEFAULT_ACTION_PERMISSIONS
+from netbox import object_actions
from utilities.permissions import get_permission_for_model
__all__ = (
@@ -9,6 +9,18 @@ __all__ = (
'TableMixin',
)
+# TODO: Remove in NetBox v4.5
+LEGACY_ACTIONS = {
+ 'add': object_actions.AddObject,
+ 'edit': object_actions.EditObject,
+ 'delete': object_actions.DeleteObject,
+ 'export': object_actions.BulkExport,
+ 'bulk_import': object_actions.BulkImport,
+ 'bulk_edit': object_actions.BulkEdit,
+ 'bulk_rename': object_actions.BulkRename,
+ 'bulk_delete': object_actions.BulkDelete,
+}
+
class ActionsMixin:
"""
@@ -19,7 +31,24 @@ 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
+ actions = tuple()
+
+ # TODO: Remove in NetBox v4.5
+ def _convert_legacy_actions(self):
+ """
+ Convert a legacy dictionary mapping action name to required permissions to a list of ObjectAction subclasses.
+ """
+ if type(self.actions) is not dict:
+ return
+
+ actions = []
+ for name in self.actions.keys():
+ try:
+ actions.append(LEGACY_ACTIONS[name])
+ except KeyError:
+ raise ValueError(f"Unsupported legacy action: {name}")
+
+ self.actions = actions
def get_permitted_actions(self, user, model=None):
"""
@@ -27,11 +56,15 @@ class ActionsMixin:
"""
model = model or self.queryset.model
+ # TODO: Remove in NetBox v4.5
+ # Handle legacy action sets
+ self._convert_legacy_actions()
+
# Resolve required permissions for each action
permitted_actions = []
for action in self.actions:
required_permissions = [
- get_permission_for_model(model, name) for name in self.actions.get(action, set())
+ get_permission_for_model(model, perm) for perm in action.permissions_required
]
if not required_permissions or user.has_perms(required_permissions):
permitted_actions.append(action)
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
index a7acbffc0..2e9b73d20 100644
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -14,6 +14,9 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from core.signals import clear_events
+from netbox.object_actions import (
+ AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, CloneObject, DeleteObject, EditObject,
+)
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields
@@ -36,7 +39,7 @@ __all__ = (
)
-class ObjectView(BaseObjectView):
+class ObjectView(ActionsMixin, BaseObjectView):
"""
Retrieve a single object for display.
@@ -46,6 +49,7 @@ class ObjectView(BaseObjectView):
tab: A ViewTab instance for the view
"""
tab = None
+ actions = (CloneObject, EditObject, DeleteObject)
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view')
@@ -72,9 +76,11 @@ class ObjectView(BaseObjectView):
request: The current request
"""
instance = self.get_object(**kwargs)
+ actions = self.get_permitted_actions(request.user, model=instance)
return render(request, self.get_template_name(), {
'object': instance,
+ 'actions': actions,
'tab': self.tab,
**self.get_extra_context(request, instance),
})
@@ -97,6 +103,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
table = None
filterset = None
filterset_form = None
+ actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
template_name = 'generic/object_children.html'
def get_children(self, request, parent):
@@ -138,10 +145,10 @@ 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_table_actions = any(action.multi for action in actions)
table_data = self.prep_table_data(request, child_objects, instance)
- table = self.get_table(table_data, request, has_bulk_actions)
+ table = self.get_table(table_data, request, has_table_actions)
# If this is an HTMX request, return only the rendered table HTML
if htmx_partial(request):
diff --git a/netbox/templates/core/buttons/bulk_sync.html b/netbox/templates/core/buttons/bulk_sync.html
new file mode 100644
index 000000000..e92ad15df
--- /dev/null
+++ b/netbox/templates/core/buttons/bulk_sync.html
@@ -0,0 +1,3 @@
+
+ {{ label }}
+
diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html
index 175a0e2bc..0747547b1 100644
--- a/netbox/templates/core/datafile.html
+++ b/netbox/templates/core/datafile.html
@@ -11,12 +11,6 @@
{{ object.source }}
{% endblock %}
-{% block control-buttons %}
- {% if request.user|can_delete:object %}
- {% delete_button object %}
- {% endif %}
-{% endblock control-buttons %}
-
{% block content %}
diff --git a/netbox/templates/core/job.html b/netbox/templates/core/job.html
index a38c3650a..49fa0231a 100644
--- a/netbox/templates/core/job.html
+++ b/netbox/templates/core/job.html
@@ -22,12 +22,6 @@
{% endif %}
{% endblock breadcrumbs %}
-{% block control-buttons %}
- {% if request.user|can_delete:object %}
- {% delete_button object %}
- {% endif %}
-{% endblock control-buttons %}
-
{% block content %}
diff --git a/netbox/templates/dcim/buttons/bulk_add_components.html b/netbox/templates/dcim/buttons/bulk_add_components.html
new file mode 100644
index 000000000..b5eadeeac
--- /dev/null
+++ b/netbox/templates/dcim/buttons/bulk_add_components.html
@@ -0,0 +1,71 @@
+{% load i18n %}
+
+
+ {{ label }}
+
+
+
diff --git a/netbox/templates/dcim/buttons/bulk_disconnect.html b/netbox/templates/dcim/buttons/bulk_disconnect.html
new file mode 100644
index 000000000..9ab53472b
--- /dev/null
+++ b/netbox/templates/dcim/buttons/bulk_disconnect.html
@@ -0,0 +1,3 @@
+
+ {{ label }}
+
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" %}
-
- {% trans "Rename Selected" %}
-
- {% 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 %}
-
- Edit Selected
-
- {% endif %}
- {% endwith %}
- {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
- {% if 'bulk_rename' in actions and bulk_rename_view %}
-
- Rename
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_consoleport %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_consoleserverport %}
-
- {% 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 %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_frontport %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% 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 %}
-
- {% 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 %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_poweroutlet %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_powerport %}
-
- {% 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 %}
-
- {% trans "Disconnect" %}
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_delete_controls %}
-
-{% block bulk_extra_controls %}
- {{ block.super }}
- {% if perms.dcim.add_rearport %}
-
- {% endif %}
-{% endblock bulk_extra_controls %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html
deleted file mode 100644
index 493b652f5..000000000
--- a/netbox/templates/dcim/device_list.html
+++ /dev/null
@@ -1,89 +0,0 @@
-{% extends 'generic/object_list.html' %}
-{% load buttons %}
-{% load i18n %}
-
-{% block bulk_buttons %}
- {% if perms.dcim.change_device %}
-
-
- {% trans "Add Components" %}
-
-
-
- {% endif %}
- {% if 'bulk_edit' in actions %}
-
- {% bulk_edit_button model query_params=request.GET %}
-
- {% trans "Rename" %}
-
-
- {% endif %}
- {% if 'bulk_delete' in actions %}
- {% bulk_delete_button model query_params=request.GET %}
- {% endif %}
-{% endblock %}
diff --git a/netbox/templates/dcim/devicetype/component_templates.html b/netbox/templates/dcim/devicetype/component_templates.html
deleted file mode 100644
index 291c7c988..000000000
--- a/netbox/templates/dcim/devicetype/component_templates.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends 'generic/object_children.html' %}
-{% load helpers %}
-{% 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 %}
-
- Edit Selected
-
- {% endif %}
- {% endwith %}
- {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
- {% if 'bulk_rename' in actions and bulk_rename_view %}
-
- Rename Selected
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_edit_controls %}
diff --git a/netbox/templates/dcim/moduletype/component_templates.html b/netbox/templates/dcim/moduletype/component_templates.html
deleted file mode 100644
index 3cee0bbd9..000000000
--- a/netbox/templates/dcim/moduletype/component_templates.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends 'generic/object_children.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load i18n %}
-
-{% block extra_controls %}
- {% include 'dcim/inc/moduletype_buttons.html' %}
-{% endblock %}
-
-{% block bulk_edit_controls %}
- {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
- {% if 'bulk_edit' in actions and bulk_edit_view %}
-
- Edit Selected
-
- {% endif %}
- {% endwith %}
- {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
- {% if 'bulk_rename' in actions and bulk_rename_view %}
-
- Rename Selected
-
- {% endif %}
- {% endwith %}
-{% endblock bulk_edit_controls %}
-
diff --git a/netbox/templates/dcim/virtualchassis.html b/netbox/templates/dcim/virtualchassis.html
index cce005ed1..da5a812a2 100644
--- a/netbox/templates/dcim/virtualchassis.html
+++ b/netbox/templates/dcim/virtualchassis.html
@@ -1,18 +1,8 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
-{% block buttons %}
- {% if perms.dcim.change_virtualchassis %}
- {% edit_button object %}
- {% endif %}
- {% if perms.dcim.delete_virtualchassis %}
- {% delete_button object %}
- {% endif %}
-{% endblock %}
-
{% block content %}
diff --git a/netbox/templates/extras/configcontext_list.html b/netbox/templates/extras/configcontext_list.html
deleted file mode 100644
index 648323532..000000000
--- a/netbox/templates/extras/configcontext_list.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends 'generic/object_list.html' %}
-{% load i18n %}
-
-{% block bulk_buttons %}
- {% if perms.extras.sync_configcontext %}
-
- {% trans "Sync Data" %}
-
- {% endif %}
- {{ block.super }}
-{% endblock %}
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 %}
-
- {% trans "Sync Data" %}
-
- {% 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 %}
-
- {% trans "Sync Data" %}
-
- {% endif %}
- {{ block.super }}
-{% endblock %}
diff --git a/netbox/templates/generic/bulk_remove.html b/netbox/templates/generic/bulk_remove.html
deleted file mode 100644
index d0ba23097..000000000
--- a/netbox/templates/generic/bulk_remove.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{% extends 'generic/_base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% comment %}
-Blocks:
- - title: Page title
- - tabs: Page tabs
- - content: Primary page content
-
-Context:
- - form: The bulk edit form class
- - parent_obj: The parent object
- - table: A table of objects being removed
- - obj_type_plural: The plural form of the object type
- - return_url: The URL to which the user is redirected after submitting the form
-{% endcomment %}
-
-{% block title %}
- {% trans "Remove" %} {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?
-{% endblock %}
-
-{% block tabs %}
-
-
-
- {% trans "Bulk Remove" %}
-
-
-
-{% endblock tabs %}
-
-{% block content %}
-
-
-
-
-
-
-
-
{% trans "Confirm Bulk Removal" %}
- {% blocktrans trimmed with count=table.rows|length %}
- The following operation will remove {{ count }} {{ obj_type_plural }} from {{ parent_obj }}. Please
- carefully review the {{ obj_type_plural }} to be removed and confirm below.
- {% endblocktrans %}
-
-
-
-
-
-
- {% render_table table 'inc/table.html' %}
-
-
-
-
-
-{% endblock content %}
diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html
index 42083280a..4bccf108c 100644
--- a/netbox/templates/generic/object.html
+++ b/netbox/templates/generic/object.html
@@ -80,15 +80,7 @@ Context:
{% if perms.extras.add_subscription and object.subscriptions %}
{% subscribe_button object %}
{% endif %}
- {% if request.user|can_add:object %}
- {% clone_button object %}
- {% endif %}
- {% if request.user|can_change:object %}
- {% edit_button object %}
- {% endif %}
- {% if request.user|can_delete:object %}
- {% delete_button object %}
- {% endif %}
+ {% action_buttons actions object %}
{% endblock control-buttons %}
diff --git a/netbox/templates/generic/object_children.html b/netbox/templates/generic/object_children.html
index c4d6dc52a..b9eabdc9d 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 %}
@@ -7,8 +8,6 @@ Blocks:
- content: Primary page content
- table_controls: Control elements for the child objects table
- bulk_controls: Bulk action buttons which appear beneath the child objects table
- - bulk_edit_controls: Bulk edit buttons
- - bulk_delete_controls: Bulk delete buttons
- bulk_extra_controls: Other bulk action buttons
- modals: Any pre-loaded modals
@@ -36,36 +35,8 @@ Context:
{% block bulk_controls %}
-
- {# 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 %}
-
- {% trans "Edit Selected" %}
-
- {% 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 %}
-
- {% trans "Delete Selected" %}
-
- {% endif %}
- {% endwith %}
- {% endblock bulk_delete_controls %}
-
- {# Other bulk action buttons #}
- {% block bulk_extra_controls %}{% endblock %}
+ {% action_buttons actions model multi=True %}
+ {% 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 e6d5505a4..6a2c3d4c9 100644
--- a/netbox/templates/generic/object_list.html
+++ b/netbox/templates/generic/object_list.html
@@ -31,15 +31,7 @@ Context:
{% plugin_list_buttons model %}
{% block extra_controls %}{% endblock %}
- {% if 'add' in actions %}
- {% add_button model %}
- {% endif %}
- {% if 'bulk_import' in actions %}
- {% import_button model %}
- {% endif %}
- {% if 'export' in actions %}
- {% export_button model %}
- {% endif %}
+ {% action_buttons actions model %}
{% endblock controls %}
@@ -91,12 +83,7 @@ Context:
- {% 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 %}
@@ -124,12 +111,7 @@ Context:
{% block bulk_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 %}
{% endblock %}
diff --git a/netbox/templates/virtualization/buttons/bulk_add_components.html b/netbox/templates/virtualization/buttons/bulk_add_components.html
new file mode 100644
index 000000000..8b0500862
--- /dev/null
+++ b/netbox/templates/virtualization/buttons/bulk_add_components.html
@@ -0,0 +1,22 @@
+{% load i18n %}
+
+
+ {% trans "Add Components" %}
+
+
+
diff --git a/netbox/templates/virtualization/cluster/devices.html b/netbox/templates/virtualization/cluster/devices.html
deleted file mode 100644
index a71fab8b5..000000000
--- a/netbox/templates/virtualization/cluster/devices.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends 'generic/object_children.html' %}
-{% load i18n %}
-
-{% block bulk_delete_controls %}
- {{ block.super }}
- {% if 'bulk_remove_devices' in actions %}
-
- {% trans "Remove Selected" %}
-
- {% endif %}
-{% endblock bulk_delete_controls %}
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 %}
-
- {% trans "Rename" %}
-
- {% endif %}
-{% endblock bulk_edit_controls %}
diff --git a/netbox/templates/virtualization/virtualmachine/virtual_disks.html b/netbox/templates/virtualization/virtualmachine/virtual_disks.html
deleted file mode 100644
index 2637d851a..000000000
--- a/netbox/templates/virtualization/virtualmachine/virtual_disks.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 %}
-
- {% trans "Rename" %}
-
- {% endif %}
-{% endblock bulk_edit_controls %}
diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html
deleted file mode 100644
index 63fbdc2b7..000000000
--- a/netbox/templates/virtualization/virtualmachine_list.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends 'generic/object_list.html' %}
-{% load i18n %}
-
-{% block bulk_buttons %}
- {% if perms.virtualization.change_virtualmachine %}
-
-
- {% trans "Add Components" %}
-
-
-
- {% endif %}
- {{ block.super }}
-{% endblock %}
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/add.html b/netbox/utilities/templates/buttons/add.html
index df257b5d2..cf14769d3 100644
--- a/netbox/utilities/templates/buttons/add.html
+++ b/netbox/utilities/templates/buttons/add.html
@@ -1,6 +1,3 @@
-{% if url %}
-{% load i18n %}
-
- {% trans "Add" %}
-
-{% endif %}
+
+ {{ label }}
+
diff --git a/netbox/utilities/templates/buttons/bulk_delete.html b/netbox/utilities/templates/buttons/bulk_delete.html
index a4860ce2d..42dd7ce30 100644
--- a/netbox/utilities/templates/buttons/bulk_delete.html
+++ b/netbox/utilities/templates/buttons/bulk_delete.html
@@ -1,6 +1,3 @@
-{% load i18n %}
-{% if url %}
-
- {% trans "Delete Selected" %}
-
-{% endif %}
+
+ {{ label }}
+
diff --git a/netbox/utilities/templates/buttons/bulk_edit.html b/netbox/utilities/templates/buttons/bulk_edit.html
index a5aa03112..bc50d9b6e 100644
--- a/netbox/utilities/templates/buttons/bulk_edit.html
+++ b/netbox/utilities/templates/buttons/bulk_edit.html
@@ -1,6 +1,3 @@
-{% load i18n %}
-{% if url %}
-
- {% trans "Edit Selected" %}
-
-{% endif %}
+
+ {{ label }}
+
diff --git a/netbox/utilities/templates/buttons/bulk_rename.html b/netbox/utilities/templates/buttons/bulk_rename.html
new file mode 100644
index 000000000..376faa88b
--- /dev/null
+++ b/netbox/utilities/templates/buttons/bulk_rename.html
@@ -0,0 +1,3 @@
+
+ {{ label }}
+
diff --git a/netbox/utilities/templates/buttons/delete.html b/netbox/utilities/templates/buttons/delete.html
index e367cfed1..768f605d0 100644
--- a/netbox/utilities/templates/buttons/delete.html
+++ b/netbox/utilities/templates/buttons/delete.html
@@ -1,12 +1,12 @@
-{% load i18n %}
- {% trans "Delete" %}
+ {{ label }}
diff --git a/netbox/utilities/templates/buttons/edit.html b/netbox/utilities/templates/buttons/edit.html
index 9dc9a0b46..9890e5952 100644
--- a/netbox/utilities/templates/buttons/edit.html
+++ b/netbox/utilities/templates/buttons/edit.html
@@ -1,4 +1,3 @@
-{% load i18n %}
- {% trans "Edit" %}
+ {{ label }}
diff --git a/netbox/utilities/templates/buttons/export.html b/netbox/utilities/templates/buttons/export.html
index 279757236..af388ca53 100644
--- a/netbox/utilities/templates/buttons/export.html
+++ b/netbox/utilities/templates/buttons/export.html
@@ -1,7 +1,7 @@
{% load i18n %}
- {% trans "Export" %}
+ {{ label }}
diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py
index d38c8863f..404386910 100644
--- a/netbox/utilities/templatetags/buttons.py
+++ b/netbox/utilities/templatetags/buttons.py
@@ -1,6 +1,9 @@
from django import template
from django.contrib.contenttypes.models import ContentType
+from django.template import loader
from django.urls import NoReverseMatch, reverse
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
from core.models import ObjectType
from extras.models import Bookmark, ExportTemplate, Subscription
@@ -9,6 +12,7 @@ from utilities.querydict import prepare_cloned_fields
from utilities.views import get_viewname
__all__ = (
+ 'action_buttons',
'add_button',
'bookmark_button',
'bulk_delete_button',
@@ -25,9 +29,14 @@ __all__ = (
register = template.Library()
-#
-# Instance buttons
-#
+@register.simple_tag(takes_context=True)
+def action_buttons(context, actions, obj, multi=False):
+ buttons = [
+ loader.render_to_string(action.template_name, action.get_context(context, obj))
+ for action in actions if action.multi == multi
+ ]
+ return mark_safe(''.join(buttons))
+
@register.inclusion_tag('buttons/bookmark.html', takes_context=True)
def bookmark_button(context, instance):
@@ -60,42 +69,6 @@ def bookmark_button(context, instance):
}
-@register.inclusion_tag('buttons/clone.html')
-def clone_button(instance):
- url = reverse(get_viewname(instance, 'add'))
-
- # Populate cloned field values
- param_string = prepare_cloned_fields(instance).urlencode()
- if param_string:
- url = f'{url}?{param_string}'
- else:
- url = None
-
- return {
- 'url': url,
- }
-
-
-@register.inclusion_tag('buttons/edit.html')
-def edit_button(instance):
- viewname = get_viewname(instance, 'edit')
- url = reverse(viewname, kwargs={'pk': instance.pk})
-
- return {
- 'url': url,
- }
-
-
-@register.inclusion_tag('buttons/delete.html')
-def delete_button(instance):
- viewname = get_viewname(instance, 'delete')
- url = reverse(viewname, kwargs={'pk': instance.pk})
-
- return {
- 'url': url,
- }
-
-
@register.inclusion_tag('buttons/subscribe.html', takes_context=True)
def subscribe_button(context, instance):
# Skip for objects which don't support notifications
@@ -131,20 +104,70 @@ def subscribe_button(context, instance):
}
+#
+# Legacy object buttons
+#
+
+# TODO: Remove in NetBox v4.6
+@register.inclusion_tag('buttons/clone.html')
+def clone_button(instance):
+ # Resolve URL path
+ viewname = get_viewname(instance, 'add')
+ try:
+ url = reverse(viewname)
+ except NoReverseMatch:
+ return {
+ 'url': None,
+ }
+
+ # Populate cloned field values and return full URL
+ param_string = prepare_cloned_fields(instance).urlencode()
+ return {
+ 'url': f'{url}?{param_string}' if param_string else None,
+ }
+
+
+# TODO: Remove in NetBox v4.6
+@register.inclusion_tag('buttons/edit.html')
+def edit_button(instance):
+ viewname = get_viewname(instance, 'edit')
+ url = reverse(viewname, kwargs={'pk': instance.pk})
+
+ return {
+ 'url': url,
+ 'label': _('Edit'),
+ }
+
+
+# TODO: Remove in NetBox v4.6
+@register.inclusion_tag('buttons/delete.html')
+def delete_button(instance):
+ viewname = get_viewname(instance, 'delete')
+ url = reverse(viewname, kwargs={'pk': instance.pk})
+
+ return {
+ 'url': url,
+ 'label': _('Delete'),
+ }
+
+
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/sync.html')
def sync_button(instance):
viewname = get_viewname(instance, 'sync')
url = reverse(viewname, kwargs={'pk': instance.pk})
return {
+ 'label': _('Sync'),
'url': url,
}
#
-# List buttons
+# Legacy list buttons
#
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/add.html')
def add_button(model, action='add'):
try:
@@ -154,9 +177,11 @@ def add_button(model, action='add'):
return {
'url': url,
+ 'label': _('Add'),
}
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/import.html')
def import_button(model, action='bulk_import'):
try:
@@ -166,9 +191,11 @@ def import_button(model, action='bulk_import'):
return {
'url': url,
+ 'label': _('Import'),
}
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/export.html', takes_context=True)
def export_button(context, model):
object_type = ObjectType.objects.get_for_model(model)
@@ -181,6 +208,7 @@ def export_button(context, model):
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
return {
+ 'label': _('Export'),
'perms': context['perms'],
'object_type': object_type,
'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
@@ -189,6 +217,7 @@ def export_button(context, model):
}
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/bulk_edit.html', takes_context=True)
def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
try:
@@ -199,11 +228,13 @@ def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
url = None
return {
- 'htmx_navigation': context.get('htmx_navigation'),
+ 'label': _('Edit Selected'),
'url': url,
+ 'htmx_navigation': context.get('htmx_navigation'),
}
+# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/bulk_delete.html', takes_context=True)
def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
try:
@@ -214,6 +245,7 @@ def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
url = None
return {
- 'htmx_navigation': context.get('htmx_navigation'),
+ 'label': _('Delete Selected'),
'url': url,
+ 'htmx_navigation': context.get('htmx_navigation'),
}
diff --git a/netbox/virtualization/object_actions.py b/netbox/virtualization/object_actions.py
new file mode 100644
index 000000000..0f248b4e4
--- /dev/null
+++ b/netbox/virtualization/object_actions.py
@@ -0,0 +1,26 @@
+from django.utils.translation import gettext as _
+
+from netbox.object_actions import ObjectAction
+
+__all__ = (
+ 'BulkAddComponents',
+)
+
+
+class BulkAddComponents(ObjectAction):
+ """
+ Add components to the selected virtual machines.
+ """
+ label = _('Add Components')
+ multi = True
+ permissions_required = {'change'}
+ template_name = 'virtualization/buttons/bulk_add_components.html'
+
+ @classmethod
+ def get_context(cls, context, obj):
+ return {
+ 'perms': context.get('perms'),
+ 'request': context.get('request'),
+ 'formaction': context.get('formaction'),
+ 'label': cls.label,
+ }
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 6013a56f4..e6ab07571 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -13,13 +13,14 @@ 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
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables
from .models import *
+from .object_actions import BulkAddComponents
#
@@ -204,6 +205,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
table = tables.VirtualMachineTable
filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm
+ actions = (EditObject, DeleteObject, BulkEdit)
tab = ViewTab(
label=_('Virtual Machines'),
badge=lambda obj: obj.virtual_machines.count(),
@@ -222,14 +224,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
table = DeviceTable
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 = (EditObject, DeleteObject, BulkEdit)
tab = ViewTab(
label=_('Devices'),
badge=lambda obj: obj.devices.count(),
@@ -317,50 +312,6 @@ class ClusterAddDevicesView(generic.ObjectEditView):
})
-@register_model_view(Cluster, 'remove_devices', path='devices/remove')
-class ClusterRemoveDevicesView(generic.ObjectEditView):
- queryset = Cluster.objects.all()
- form = forms.ClusterRemoveDevicesForm
- template_name = 'generic/bulk_remove.html'
-
- def post(self, request, pk):
-
- cluster = get_object_or_404(self.queryset, pk=pk)
-
- if '_confirm' in request.POST:
- form = self.form(request.POST)
- if form.is_valid():
-
- device_pks = form.cleaned_data['pk']
- with transaction.atomic(using=router.db_for_write(Device)):
-
- # Remove the selected Devices from the Cluster
- for device in Device.objects.filter(pk__in=device_pks):
- device.cluster = None
- device.save()
-
- messages.success(request, _("Removed {count} devices from cluster {cluster}").format(
- count=len(device_pks),
- cluster=cluster
- ))
- return redirect(cluster.get_absolute_url())
-
- else:
- form = self.form(initial={'pk': request.POST.getlist('pk')})
-
- selected_objects = Device.objects.filter(pk__in=form.initial['pk'])
- device_table = DeviceTable(list(selected_objects), orderable=False)
- device_table.configure(request)
-
- return render(request, self.template_name, {
- 'form': form,
- 'parent_obj': cluster,
- 'table': device_table,
- 'obj_type_plural': 'devices',
- 'return_url': cluster.get_absolute_url(),
- })
-
-
#
# Virtual machines
#
@@ -371,7 +322,7 @@ class VirtualMachineListView(generic.ObjectListView):
filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable
- template_name = 'virtualization/virtualmachine_list.html'
+ actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkDelete)
@register_model_view(VirtualMachine)
@@ -386,11 +337,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 = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interface_count,
@@ -412,17 +359,13 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
table = tables.VirtualMachineVirtualDiskTable
filterset = filtersets.VirtualDiskFilterSet
filterset_form = forms.VirtualDiskFilterForm
- template_name = 'virtualization/virtualmachine/virtual_disks.html'
+ actions = (EditObject, DeleteObject, 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')