Closes #19735: Implement reuable bulk operations classes (#19774)

* Initial work on #19735

* Work in progress

* Remove ClusterRemoveDevicesView (anti-pattern)

* Misc cleanup

* Fix has_bulk_actions

* Fix has_bulk_actions for ObjectChildrenView

* Restore clone button

* Misc cleanup

* Clean up custom bulk actions

* Rename individual object actions

* Collapse into a single template tag

* Fix support for legacy action dicts

* Rename bulk attr to multi

* clone_button tag should fail silently if view name is invalid

* Clean up action buttons

* Fix export button label

* Replace clone_button with an ObjectAction

* Create object actions for adding device/VM components

* Move core_sync.html to core app

* Remove extra_bulk_buttons from template doc
This commit is contained in:
Jeremy Stretch 2025-06-30 13:03:07 -04:00 committed by GitHub
parent 71e6ea5785
commit 601a77ac73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 567 additions and 973 deletions

View File

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

View File

@ -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 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.config import get_config, PARAMS
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.registry import registry from netbox.registry import registry
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.base import BaseObjectView from netbox.views.generic.base import BaseObjectView
@ -138,14 +139,13 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable table = tables.DataFileTable
actions = { actions = (BulkDelete,)
'bulk_delete': {'delete'},
}
@register_model_view(DataFile) @register_model_view(DataFile)
class DataFileView(generic.ObjectView): class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all() queryset = DataFile.objects.all()
actions = (DeleteObject,)
@register_model_view(DataFile, 'delete') @register_model_view(DataFile, 'delete')
@ -170,15 +170,13 @@ class JobListView(generic.ObjectListView):
filterset = filtersets.JobFilterSet filterset = filtersets.JobFilterSet
filterset_form = forms.JobFilterForm filterset_form = forms.JobFilterForm
table = tables.JobTable table = tables.JobTable
actions = { actions = (BulkExport, BulkDelete)
'export': {'view'},
'bulk_delete': {'delete'},
}
@register_model_view(Job) @register_model_view(Job)
class JobView(generic.ObjectView): class JobView(generic.ObjectView):
queryset = Job.objects.all() queryset = Job.objects.all()
actions = (DeleteObject,)
@register_model_view(Job, 'delete') @register_model_view(Job, 'delete')
@ -204,9 +202,7 @@ class ObjectChangeListView(generic.ObjectListView):
filterset_form = forms.ObjectChangeFilterForm filterset_form = forms.ObjectChangeFilterForm
table = tables.ObjectChangeTable table = tables.ObjectChangeTable
template_name = 'core/objectchange_list.html' template_name = 'core/objectchange_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
@register_model_view(ObjectChange) @register_model_view(ObjectChange)
@ -274,6 +270,7 @@ class ConfigRevisionListView(generic.ObjectListView):
filterset = filtersets.ConfigRevisionFilterSet filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable table = tables.ConfigRevisionTable
actions = (AddObject, BulkExport)
@register_model_view(ConfigRevision) @register_model_view(ConfigRevision)

View File

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

View File

@ -15,7 +15,7 @@ from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView, ObjectRenderConfigView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.object_actions import *
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
@ -34,6 +34,7 @@ from wireless.models import WirelessLAN
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DeviceFaceChoices, InterfaceModeChoices from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import * from .models import *
from .object_actions import BulkAddComponents, BulkDisconnect
CABLE_TERMINATION_TYPES = { CABLE_TERMINATION_TYPES = {
'dcim.consoleport': ConsolePort, 'dcim.consoleport': ConsolePort,
@ -49,11 +50,6 @@ CABLE_TERMINATION_TYPES = {
class DeviceComponentsView(generic.ObjectChildrenView): class DeviceComponentsView(generic.ObjectChildrenView):
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
'bulk_disconnect': {'change'},
}
queryset = Device.objects.all() queryset = Device.objects.all()
def get_children(self, request, parent): def get_children(self, request, parent):
@ -61,12 +57,8 @@ class DeviceComponentsView(generic.ObjectChildrenView):
class DeviceTypeComponentsView(generic.ObjectChildrenView): class DeviceTypeComponentsView(generic.ObjectChildrenView):
actions = { actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
template_name = 'dcim/devicetype/component_templates.html'
viewname = None # Used for return_url resolution viewname = None # Used for return_url resolution
def get_children(self, request, parent): 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() queryset = ModuleType.objects.all()
template_name = 'dcim/moduletype/component_templates.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
viewname = None # Used for return_url resolution viewname = None # Used for return_url resolution
def get_children(self, request, parent): def get_children(self, request, parent):
@ -2116,7 +2108,7 @@ class DeviceListView(generic.ObjectListView):
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
filterset_form = forms.DeviceFilterForm filterset_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceTable
template_name = 'dcim/device_list.html' actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkRename, BulkDelete)
@register_model_view(Device) @register_model_view(Device)
@ -2157,7 +2149,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
table = tables.DeviceConsolePortTable table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
template_name = 'dcim/device/consoleports.html', actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Console Ports'), label=_('Console Ports'),
badge=lambda obj: obj.console_port_count, badge=lambda obj: obj.console_port_count,
@ -2173,7 +2165,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
table = tables.DeviceConsoleServerPortTable table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
template_name = 'dcim/device/consoleserverports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Console Server Ports'), label=_('Console Server Ports'),
badge=lambda obj: obj.console_server_port_count, badge=lambda obj: obj.console_server_port_count,
@ -2189,7 +2181,7 @@ class DevicePowerPortsView(DeviceComponentsView):
table = tables.DevicePowerPortTable table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
template_name = 'dcim/device/powerports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Power Ports'), label=_('Power Ports'),
badge=lambda obj: obj.power_port_count, badge=lambda obj: obj.power_port_count,
@ -2205,7 +2197,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
table = tables.DevicePowerOutletTable table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
template_name = 'dcim/device/poweroutlets.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Power Outlets'), label=_('Power Outlets'),
badge=lambda obj: obj.power_outlet_count, badge=lambda obj: obj.power_outlet_count,
@ -2221,6 +2213,7 @@ class DeviceInterfacesView(DeviceComponentsView):
table = tables.DeviceInterfaceTable table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
template_name = 'dcim/device/interfaces.html' template_name = 'dcim/device/interfaces.html'
tab = ViewTab( tab = ViewTab(
label=_('Interfaces'), label=_('Interfaces'),
@ -2243,7 +2236,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
table = tables.DeviceFrontPortTable table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
template_name = 'dcim/device/frontports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Front Ports'), label=_('Front Ports'),
badge=lambda obj: obj.front_port_count, badge=lambda obj: obj.front_port_count,
@ -2259,7 +2252,7 @@ class DeviceRearPortsView(DeviceComponentsView):
table = tables.DeviceRearPortTable table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
template_name = 'dcim/device/rearports.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDisconnect, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Rear Ports'), label=_('Rear Ports'),
badge=lambda obj: obj.rear_port_count, badge=lambda obj: obj.rear_port_count,
@ -2275,11 +2268,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
table = tables.DeviceModuleBayTable table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm filterset_form = forms.ModuleBayFilterForm
template_name = 'dcim/device/modulebays.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Module Bays'), label=_('Module Bays'),
badge=lambda obj: obj.module_bay_count, badge=lambda obj: obj.module_bay_count,
@ -2295,11 +2284,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
table = tables.DeviceDeviceBayTable table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
template_name = 'dcim/device/devicebays.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Device Bays'), label=_('Device Bays'),
badge=lambda obj: obj.device_bay_count, badge=lambda obj: obj.device_bay_count,
@ -2315,11 +2300,7 @@ class DeviceInventoryView(DeviceComponentsView):
table = tables.DeviceInventoryItemTable table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
template_name = 'dcim/device/inventory.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Inventory Items'), label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count, badge=lambda obj: obj.inventory_item_count,
@ -2472,11 +2453,7 @@ class ConsolePortListView(generic.ObjectListView):
filterset = filtersets.ConsolePortFilterSet filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortTable table = tables.ConsolePortTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ConsolePort) @register_model_view(ConsolePort)
@ -2547,11 +2524,7 @@ class ConsoleServerPortListView(generic.ObjectListView):
filterset = filtersets.ConsoleServerPortFilterSet filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ConsoleServerPort) @register_model_view(ConsoleServerPort)
@ -2622,11 +2595,7 @@ class PowerPortListView(generic.ObjectListView):
filterset = filtersets.PowerPortFilterSet filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortTable table = tables.PowerPortTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(PowerPort) @register_model_view(PowerPort)
@ -2697,11 +2666,7 @@ class PowerOutletListView(generic.ObjectListView):
filterset = filtersets.PowerOutletFilterSet filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletTable table = tables.PowerOutletTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(PowerOutlet) @register_model_view(PowerOutlet)
@ -2772,11 +2737,7 @@ class InterfaceListView(generic.ObjectListView):
filterset = filtersets.InterfaceFilterSet filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceTable table = tables.InterfaceTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(Interface) @register_model_view(Interface)
@ -2920,11 +2881,7 @@ class FrontPortListView(generic.ObjectListView):
filterset = filtersets.FrontPortFilterSet filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortTable table = tables.FrontPortTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(FrontPort) @register_model_view(FrontPort)
@ -2995,11 +2952,7 @@ class RearPortListView(generic.ObjectListView):
filterset = filtersets.RearPortFilterSet filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
table = tables.RearPortTable table = tables.RearPortTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(RearPort) @register_model_view(RearPort)
@ -3070,11 +3023,7 @@ class ModuleBayListView(generic.ObjectListView):
filterset = filtersets.ModuleBayFilterSet filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable table = tables.ModuleBayTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(ModuleBay) @register_model_view(ModuleBay)
@ -3136,11 +3085,7 @@ class DeviceBayListView(generic.ObjectListView):
filterset = filtersets.DeviceBayFilterSet filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayTable table = tables.DeviceBayTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(DeviceBay) @register_model_view(DeviceBay)
@ -3283,11 +3228,7 @@ class InventoryItemListView(generic.ObjectListView):
filterset = filtersets.InventoryItemFilterSet filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/component_list.html' actions = (AddObject, BulkImport, BulkEdit, BulkRename, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
@register_model_view(InventoryItem) @register_model_view(InventoryItem)
@ -3627,9 +3568,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
filterset_form = forms.ConsoleConnectionFilterForm filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable table = tables.ConsoleConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {
@ -3643,9 +3582,7 @@ class PowerConnectionsListView(generic.ObjectListView):
filterset_form = forms.PowerConnectionFilterForm filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable table = tables.PowerConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {
@ -3659,9 +3596,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
filterset_form = forms.InterfaceConnectionFilterForm filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable table = tables.InterfaceConnectionTable
template_name = 'dcim/connections_list.html' template_name = 'dcim/connections_list.html'
actions = { actions = (BulkExport,)
'export': {'view'},
}
def get_extra_context(self, request): def get_extra_context(self, request):
return { return {

View File

@ -14,12 +14,13 @@ from jinja2.exceptions import TemplateError
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import Job from core.models import Job
from core.object_actions import BulkSync
from dcim.models import Device, DeviceRole, Platform from dcim.models import Device, DeviceRole, Platform
from extras.choices import LogLevelChoices from extras.choices import LogLevelChoices
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from extras.utils import SharedObjectViewMixin 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 import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
@ -232,11 +233,7 @@ class ExportTemplateListView(generic.ObjectListView):
filterset = filtersets.ExportTemplateFilterSet filterset = filtersets.ExportTemplateFilterSet
filterset_form = forms.ExportTemplateFilterForm filterset_form = forms.ExportTemplateFilterForm
table = tables.ExportTemplateTable table = tables.ExportTemplateTable
template_name = 'extras/exporttemplate_list.html' actions = (AddObject, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ExportTemplate) @register_model_view(ExportTemplate)
@ -347,9 +344,7 @@ class TableConfigListView(SharedObjectViewMixin, generic.ObjectListView):
filterset = filtersets.TableConfigFilterSet filterset = filtersets.TableConfigFilterSet
filterset_form = forms.TableConfigFilterForm filterset_form = forms.TableConfigFilterForm
table = tables.TableConfigTable table = tables.TableConfigTable
actions = { actions = (BulkExport,)
'export': {'view'},
}
@register_model_view(TableConfig) @register_model_view(TableConfig)
@ -758,13 +753,7 @@ class ConfigContextListView(generic.ObjectListView):
filterset = filtersets.ConfigContextFilterSet filterset = filtersets.ConfigContextFilterSet
filterset_form = forms.ConfigContextFilterForm filterset_form = forms.ConfigContextFilterForm
table = tables.ConfigContextTable table = tables.ConfigContextTable
template_name = 'extras/configcontext_list.html' actions = (AddObject, BulkSync, BulkEdit, BulkDelete)
actions = {
'add': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
'bulk_sync': {'sync'},
}
@register_model_view(ConfigContext) @register_model_view(ConfigContext)
@ -877,11 +866,7 @@ class ConfigTemplateListView(generic.ObjectListView):
filterset = filtersets.ConfigTemplateFilterSet filterset = filtersets.ConfigTemplateFilterSet
filterset_form = forms.ConfigTemplateFilterForm filterset_form = forms.ConfigTemplateFilterForm
table = tables.ConfigTemplateTable table = tables.ConfigTemplateTable
template_name = 'extras/configtemplate_list.html' actions = (AddObject, BulkImport, BulkSync, BulkEdit, BulkExport, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_sync': {'sync'},
}
@register_model_view(ConfigTemplate) @register_model_view(ConfigTemplate)
@ -992,9 +977,7 @@ class ImageAttachmentListView(generic.ObjectListView):
filterset = filtersets.ImageAttachmentFilterSet filterset = filtersets.ImageAttachmentFilterSet
filterset_form = forms.ImageAttachmentFilterForm filterset_form = forms.ImageAttachmentFilterForm
table = tables.ImageAttachmentTable table = tables.ImageAttachmentTable
actions = { actions = (BulkExport,)
'export': {'view'},
}
@register_model_view(ImageAttachment, 'add', detail=False) @register_model_view(ImageAttachment, 'add', detail=False)
@ -1038,12 +1021,7 @@ class JournalEntryListView(generic.ObjectListView):
filterset = filtersets.JournalEntryFilterSet filterset = filtersets.JournalEntryFilterSet
filterset_form = forms.JournalEntryFilterForm filterset_form = forms.JournalEntryFilterForm
table = tables.JournalEntryTable table = tables.JournalEntryTable
actions = { actions = (BulkImport, BulkEdit, BulkDelete)
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(JournalEntry) @register_model_view(JournalEntry)

View File

@ -28,7 +28,8 @@ ADVISORY_LOCK_KEYS = {
'job-schedules': 110100, 'job-schedules': 110100,
} }
# Default view action permission mapping # TODO: Remove in NetBox v4.6
# Legacy default view action permission mapping
DEFAULT_ACTION_PERMISSIONS = { DEFAULT_ACTION_PERMISSIONS = {
'add': {'add'}, 'add': {'add'},
'export': {'view'}, 'export': {'view'},

View File

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

View File

@ -22,6 +22,7 @@ from core.models import ObjectType
from core.signals import clear_events from core.signals import clear_events
from extras.choices import CustomFieldUIEditableChoices from extras.choices import CustomFieldUIEditableChoices
from extras.models import CustomField, ExportTemplate from extras.models import CustomField, ExportTemplate
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@ -60,6 +61,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
template_name = 'generic/object_list.html' template_name = 'generic/object_list.html'
filterset = None filterset = None
filterset_form = None filterset_form = None
actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view') return get_permission_for_model(self.queryset.model, 'view')
@ -150,13 +152,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Determine the available actions # Determine the available actions
actions = self.get_permitted_actions(request.user) 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: if 'export' in request.GET:
# Export the current table view # Export the current table view
if request.GET['export'] == 'table': 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] columns = [name for name, _ in table.selected_columns]
return self.export_table(table, columns) return self.export_table(table, columns)
@ -174,11 +176,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
# Fall back to default table/YAML export # Fall back to default table/YAML export
else: 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) return self.export_table(table)
# Render the objects 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 this is an HTMX request, return only the rendered table HTML
if htmx_partial(request): if htmx_partial(request):

View File

@ -1,7 +1,7 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from extras.models import TableConfig 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 from utilities.permissions import get_permission_for_model
__all__ = ( __all__ = (
@ -9,6 +9,18 @@ __all__ = (
'TableMixin', '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: 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 Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
with custom actions, such as bulk_sync. 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): def get_permitted_actions(self, user, model=None):
""" """
@ -27,11 +56,15 @@ class ActionsMixin:
""" """
model = model or self.queryset.model 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 # Resolve required permissions for each action
permitted_actions = [] permitted_actions = []
for action in self.actions: for action in self.actions:
required_permissions = [ 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): if not required_permissions or user.has_perms(required_permissions):
permitted_actions.append(action) permitted_actions.append(action)

View File

@ -14,6 +14,9 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.signals import clear_events 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.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields 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. Retrieve a single object for display.
@ -46,6 +49,7 @@ class ObjectView(BaseObjectView):
tab: A ViewTab instance for the view tab: A ViewTab instance for the view
""" """
tab = None tab = None
actions = (CloneObject, EditObject, DeleteObject)
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'view') return get_permission_for_model(self.queryset.model, 'view')
@ -72,9 +76,11 @@ class ObjectView(BaseObjectView):
request: The current request request: The current request
""" """
instance = self.get_object(**kwargs) instance = self.get_object(**kwargs)
actions = self.get_permitted_actions(request.user, model=instance)
return render(request, self.get_template_name(), { return render(request, self.get_template_name(), {
'object': instance, 'object': instance,
'actions': actions,
'tab': self.tab, 'tab': self.tab,
**self.get_extra_context(request, instance), **self.get_extra_context(request, instance),
}) })
@ -97,6 +103,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
table = None table = None
filterset = None filterset = None
filterset_form = None filterset_form = None
actions = (AddObject, BulkImport, BulkEdit, BulkExport, BulkDelete)
template_name = 'generic/object_children.html' template_name = 'generic/object_children.html'
def get_children(self, request, parent): def get_children(self, request, parent):
@ -138,10 +145,10 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
# Determine the available actions # Determine the available actions
actions = self.get_permitted_actions(request.user, model=self.child_model) 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_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 this is an HTMX request, return only the rendered table HTML
if htmx_partial(request): if htmx_partial(request):

View File

@ -0,0 +1,3 @@
<button type="submit" name="_sync" {% formaction %}="{{ url }}" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
</button>

View File

@ -11,12 +11,6 @@
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li> <li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
{% endblock %} {% endblock %}
{% block control-buttons %}
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
{% endblock control-buttons %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">

View File

@ -22,12 +22,6 @@
{% endif %} {% endif %}
{% endblock breadcrumbs %} {% endblock breadcrumbs %}
{% block control-buttons %}
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
{% endblock control-buttons %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-12 col-md-6"> <div class="col col-12 col-md-6">

View File

@ -0,0 +1,71 @@
{% load i18n %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {{ label }}
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Console Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
{% trans "Console Server Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_powerport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Power Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Power Outlets" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_interface %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Interfaces" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_rearport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Rear Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Device Bays" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_modulebay %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Module Bays" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_inventoryitem %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Inventory Items" %}
</button>
</li>
{% endif %}
</ul>
</div>

View File

@ -0,0 +1,3 @@
<button type="submit" name="_disconnect" {% formaction %}="{{ url }}" class="btn btn-red">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {{ label }}
</button>

View File

@ -1,22 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load buttons %}
{% load helpers %}
{% load i18n %}
{% block bulk_buttons %}
<div class="btn-group" role="group">
{% 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" %}
<button type="submit" name="_rename" {% formaction %}="{% url bulk_rename_view %}" class="btn btn-outline-warning btn-float">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
</button>
{% endwith %}
{% endif %}
</div>
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
{% endblock %}

View File

@ -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 %}
<button type="submit" name="_edit"
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% endwith %}
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename"
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_consoleport %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Ports" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_consoleserverport %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Server Ports" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -1,14 +0,0 @@
{% extends 'dcim/device/components_base.html' %}
{% load i18n %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_devicebay %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Device Bays" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_frontport %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Front Ports" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -1,30 +1,5 @@
{% extends 'dcim/device/components_base.html' %} {% extends 'generic/object_children.html' %}
{% load helpers %}
{% load i18n %}
{% block table_controls %} {% 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 %} {% 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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
</a>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -1,14 +0,0 @@
{% extends 'dcim/device/components_base.html' %}
{% load i18n %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_inventoryitem %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Inventory Item" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -1,14 +0,0 @@
{% extends 'dcim/device/components_base.html' %}
{% load i18n %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_modulebay %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Module Bays" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_poweroutlet %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Outlets" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_powerport %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Port" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -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 %}
<button type="submit" name="_disconnect"
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_rearport %}
<div class="btn-group" role="group">
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Rear Ports" %}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -1,89 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load buttons %}
{% load i18n %}
{% block bulk_buttons %}
{% if perms.dcim.change_device %}
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Console Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
{% trans "Console Server Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_powerport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Power Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Power Outlets" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_interface %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Interfaces" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_rearport %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Rear Ports" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Device Bays" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_modulebay %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Module Bays" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_inventoryitem %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Inventory Items" %}
</button>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
{% bulk_edit_button model query_params=request.GET %}
<button type="submit" name="_rename" {% formaction %}="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-float">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button>
</div>
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
{% endblock %}

View File

@ -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 %}
<button type="submit" name="_edit"
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% endwith %}
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename"
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}

View File

@ -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 %}
<button type="submit" name="_edit"
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% endwith %}
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename"
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}

View File

@ -1,18 +1,8 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %} {% 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 %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-12 col-md-4"> <div class="col col-12 col-md-4">

View File

@ -1,11 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block bulk_buttons %}
{% if perms.extras.sync_configcontext %}
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button>
{% endif %}
{{ block.super }}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block bulk_buttons %}
{% if perms.extras.sync_configtemplate %}
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button>
{% endif %}
{{ block.super }}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block bulk_buttons %}
{% if perms.extras.sync_configcontext %}
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button>
{% endif %}
{{ block.super }}
{% endblock %}

View File

@ -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 %}
<ul class="nav nav-tabs">
<li class="nav-item" role="presentation">
<button class="nav-link active" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
{% trans "Bulk Remove" %}
</button>
</li>
</ul>
{% endblock tabs %}
{% block content %}
<div class="tab-pane show active" role="tabpanel">
<div class="alert alert-danger bg-danger-subtle" role="alert">
<div class="d-flex">
<div>
<i class="mdi mdi-alert-octagon p-2"></i>
</div>
<div>
<h4 class="alert-title">{% trans "Confirm Bulk Removal" %}</h4>
{% 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 %}
</div>
</div>
</div>
<div class="container-fluid px-0">
<div class="card">
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
</div>
<form action="." method="post" class="form">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="text-end">
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
<button type="submit" name="_confirm" class="btn btn-danger">
{% blocktrans trimmed with count=table.rows|length %}
Remove these {{ count }} {{ obj_type_plural }}
{% endblocktrans %}
</button>
</div>
</form>
</div>
</div>
{% endblock content %}

View File

@ -80,15 +80,7 @@ Context:
{% if perms.extras.add_subscription and object.subscriptions %} {% if perms.extras.add_subscription and object.subscriptions %}
{% subscribe_button object %} {% subscribe_button object %}
{% endif %} {% endif %}
{% if request.user|can_add:object %} {% action_buttons actions 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 %}
{% endblock control-buttons %} {% endblock control-buttons %}
</div> </div>

View File

@ -1,4 +1,5 @@
{% extends base_template %} {% extends base_template %}
{% load buttons %}
{% load helpers %} {% load helpers %}
{% load i18n %} {% load i18n %}
@ -7,8 +8,6 @@ Blocks:
- content: Primary page content - content: Primary page content
- table_controls: Control elements for the child objects table - table_controls: Control elements for the child objects table
- bulk_controls: Bulk action buttons which appear beneath 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 - bulk_extra_controls: Other bulk action buttons
- modals: Any pre-loaded modals - modals: Any pre-loaded modals
@ -36,36 +35,8 @@ Context:
</div> </div>
<div class="d-print-none mt-2"> <div class="d-print-none mt-2">
{% block bulk_controls %} {% block bulk_controls %}
<div class="btn-group" role="group"> {% action_buttons actions model multi=True %}
{# Bulk edit buttons #} {% block bulk_extra_controls %}{% endblock %}
{% block bulk_edit_controls %}
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit"
{% formaction %}="{% url bulk_edit_view %}?return_url={{ return_url }}"
class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}
</div>
<div class="btn-group" role="group">
{# 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 %}
<button type="submit"
{% formaction %}="{% url bulk_delete_view %}?return_url={{ return_url }}"
class="btn btn-danger">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
</div>
{# Other bulk action buttons #}
{% block bulk_extra_controls %}{% endblock %}
{% endblock bulk_controls %} {% endblock bulk_controls %}
</div> </div>
</form> </form>

View File

@ -31,15 +31,7 @@ Context:
<div class="btn-list"> <div class="btn-list">
{% plugin_list_buttons model %} {% plugin_list_buttons model %}
{% block extra_controls %}{% endblock %} {% block extra_controls %}{% endblock %}
{% if 'add' in actions %} {% action_buttons actions model %}
{% add_button model %}
{% endif %}
{% if 'bulk_import' in actions %}
{% import_button model %}
{% endif %}
{% if 'export' in actions %}
{% export_button model %}
{% endif %}
</div> </div>
{% endblock controls %} {% endblock controls %}
@ -91,12 +83,7 @@ Context:
</label> </label>
</div> </div>
<div class="bulk-action-buttons"> <div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %} {% action_buttons actions model multi=True %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -124,12 +111,7 @@ Context:
<div class="btn-list d-print-none"> <div class="btn-list d-print-none">
{% block bulk_buttons %} {% block bulk_buttons %}
<div class="bulk-action-buttons"> <div class="bulk-action-buttons">
{% if 'bulk_edit' in actions %} {% action_buttons actions model multi=True %}
{% bulk_edit_button model query_params=request.GET %}
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
</div> </div>

View File

@ -0,0 +1,22 @@
{% load i18n %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button>
<ul class="dropdown-menu">
{% if perms.virtualization.add_vminterface %}
<li>
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Interfaces" %}
</button>
</li>
{% endif %}
{% if perms.virtualization.add_virtualdisk %}
<li>
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Virtual Disks" %}
</button>
</li>
{% endif %}
</ul>
</div>

View File

@ -1,13 +0,0 @@
{% extends 'generic/object_children.html' %}
{% load i18n %}
{% block bulk_delete_controls %}
{{ block.super }}
{% if 'bulk_remove_devices' in actions %}
<button type="submit" name="_remove"
{% formaction %}="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
class="btn btn-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
</button>
{% endif %}
{% endblock bulk_delete_controls %}

View File

@ -1,14 +0,0 @@
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% load i18n %}
{% block bulk_edit_controls %}
{{ block.super }}
{% if 'bulk_rename' in actions %}
<button type="submit" name="_rename"
{% formaction %}="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button>
{% endif %}
{% endblock bulk_edit_controls %}

View File

@ -1,14 +0,0 @@
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% load i18n %}
{% block bulk_edit_controls %}
{{ block.super }}
{% if 'bulk_rename' in actions %}
<button type="submit" name="_rename"
{% formaction %}="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button>
{% endif %}
{% endblock bulk_edit_controls %}

View File

@ -1,29 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block bulk_buttons %}
{% if perms.virtualization.change_virtualmachine %}
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button>
<ul class="dropdown-menu">
{% if perms.virtualization.add_vminterface %}
<li>
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Interfaces" %}
</button>
</li>
{% endif %}
{% if perms.virtualization.add_virtualdisk %}
<li>
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Virtual Disks" %}
</button>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{{ block.super }}
{% endblock %}

View File

@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from netbox.object_actions import BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
@ -349,12 +350,7 @@ class ContactAssignmentListView(generic.ObjectListView):
filterset = filtersets.ContactAssignmentFilterSet filterset = filtersets.ContactAssignmentFilterSet
filterset_form = forms.ContactAssignmentFilterForm filterset_form = forms.ContactAssignmentFilterForm
table = tables.ContactAssignmentTable table = tables.ContactAssignmentTable
actions = { actions = (BulkExport, BulkImport, BulkEdit, BulkDelete)
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(ContactAssignment, 'add', detail=False) @register_model_view(ContactAssignment, 'add', detail=False)

View File

@ -1,6 +1,3 @@
{% if url %} <a href="{{ url }}" class="btn btn-primary" role="button">
{% load i18n %} <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {{ label }}
<a href="{{ url }}" type="button" class="btn btn-primary"> </a>
<i class="mdi mdi-plus-thick"></i> {% trans "Add" %}
</a>
{% endif %}

View File

@ -1,6 +1,3 @@
{% load i18n %} <button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red">
{% if url %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {{ label }}
<button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red"> </button>
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
</button>
{% endif %}

View File

@ -1,6 +1,3 @@
{% load i18n %} <button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow">
{% if url %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
<button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow"> </button>
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
</button>
{% endif %}

View File

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

View File

@ -1,12 +1,12 @@
{% load i18n %}
<a href="#" <a href="#"
hx-get="{{ url }}" hx-get="{{ url }}"
hx-target="#htmx-modal-content" hx-target="#htmx-modal-content"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-select="form" hx-select="form"
class="btn btn-red" class="btn btn-red"
role="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#htmx-modal" data-bs-target="#htmx-modal"
> >
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {{ label }}
</a> </a>

View File

@ -1,4 +1,3 @@
{% load i18n %}
<a href="{{ url }}" class="btn btn-yellow" role="button"> <a href="{{ url }}" class="btn btn-yellow" role="button">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {{ label }}
</a> </a>

View File

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-purple dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-purple dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-download"></i> {% trans "Export" %} <i class="mdi mdi-download" aria-hidden="true"></i> {{ label }}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li> <li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li>

View File

@ -1,6 +1,3 @@
{% load i18n %} <a href="{{ url }}" class="btn btn-cyan" role="button">
{% if url %} <i class="mdi mdi-upload" aria-hidden="true"></i> {{ label }}
<a href="{{ url }}" type="button" class="btn btn-cyan"> </a>
<i class="mdi mdi-upload"></i> {% trans "Import" %}
</a>
{% endif %}

View File

@ -1,7 +1,6 @@
{% load i18n %}
<form action="{{ url }}" method="post"> <form action="{{ url }}" method="post">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync" %} <i class="mdi mdi-sync" aria-hidden="true"></i> {{ label }}
</button> </button>
</form> </form>

View File

@ -1,6 +1,9 @@
from django import template from django import template
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template import loader
from django.urls import NoReverseMatch, reverse 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 core.models import ObjectType
from extras.models import Bookmark, ExportTemplate, Subscription from extras.models import Bookmark, ExportTemplate, Subscription
@ -9,6 +12,7 @@ from utilities.querydict import prepare_cloned_fields
from utilities.views import get_viewname from utilities.views import get_viewname
__all__ = ( __all__ = (
'action_buttons',
'add_button', 'add_button',
'bookmark_button', 'bookmark_button',
'bulk_delete_button', 'bulk_delete_button',
@ -25,9 +29,14 @@ __all__ = (
register = template.Library() register = template.Library()
# @register.simple_tag(takes_context=True)
# Instance buttons 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) @register.inclusion_tag('buttons/bookmark.html', takes_context=True)
def bookmark_button(context, instance): 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) @register.inclusion_tag('buttons/subscribe.html', takes_context=True)
def subscribe_button(context, instance): def subscribe_button(context, instance):
# Skip for objects which don't support notifications # 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') @register.inclusion_tag('buttons/sync.html')
def sync_button(instance): def sync_button(instance):
viewname = get_viewname(instance, 'sync') viewname = get_viewname(instance, 'sync')
url = reverse(viewname, kwargs={'pk': instance.pk}) url = reverse(viewname, kwargs={'pk': instance.pk})
return { return {
'label': _('Sync'),
'url': url, 'url': url,
} }
# #
# List buttons # Legacy list buttons
# #
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/add.html') @register.inclusion_tag('buttons/add.html')
def add_button(model, action='add'): def add_button(model, action='add'):
try: try:
@ -154,9 +177,11 @@ def add_button(model, action='add'):
return { return {
'url': url, 'url': url,
'label': _('Add'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/import.html') @register.inclusion_tag('buttons/import.html')
def import_button(model, action='bulk_import'): def import_button(model, action='bulk_import'):
try: try:
@ -166,9 +191,11 @@ def import_button(model, action='bulk_import'):
return { return {
'url': url, 'url': url,
'label': _('Import'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/export.html', takes_context=True) @register.inclusion_tag('buttons/export.html', takes_context=True)
def export_button(context, model): def export_button(context, model):
object_type = ObjectType.objects.get_for_model(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) export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
return { return {
'label': _('Export'),
'perms': context['perms'], 'perms': context['perms'],
'object_type': object_type, 'object_type': object_type,
'url_params': context['request'].GET.urlencode() if context['request'].GET else '', '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) @register.inclusion_tag('buttons/bulk_edit.html', takes_context=True)
def bulk_edit_button(context, model, action='bulk_edit', query_params=None): def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
try: try:
@ -199,11 +228,13 @@ def bulk_edit_button(context, model, action='bulk_edit', query_params=None):
url = None url = None
return { return {
'htmx_navigation': context.get('htmx_navigation'), 'label': _('Edit Selected'),
'url': url, 'url': url,
'htmx_navigation': context.get('htmx_navigation'),
} }
# TODO: Remove in NetBox v4.6
@register.inclusion_tag('buttons/bulk_delete.html', takes_context=True) @register.inclusion_tag('buttons/bulk_delete.html', takes_context=True)
def bulk_delete_button(context, model, action='bulk_delete', query_params=None): def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
try: try:
@ -214,6 +245,7 @@ def bulk_delete_button(context, model, action='bulk_delete', query_params=None):
url = None url = None
return { return {
'htmx_navigation': context.get('htmx_navigation'), 'label': _('Delete Selected'),
'url': url, 'url': url,
'htmx_navigation': context.get('htmx_navigation'),
} }

View File

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

View File

@ -13,13 +13,14 @@ from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView, ObjectRenderConfigView from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import IPAddress, VLANGroup from ipam.models import IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.object_actions import *
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
from .object_actions import BulkAddComponents
# #
@ -204,6 +205,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
filterset = filtersets.VirtualMachineFilterSet filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm filterset_form = forms.VirtualMachineFilterForm
actions = (EditObject, DeleteObject, BulkEdit)
tab = ViewTab( tab = ViewTab(
label=_('Virtual Machines'), label=_('Virtual Machines'),
badge=lambda obj: obj.virtual_machines.count(), badge=lambda obj: obj.virtual_machines.count(),
@ -222,14 +224,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
table = DeviceTable table = DeviceTable
filterset = DeviceFilterSet filterset = DeviceFilterSet
filterset_form = DeviceFilterForm filterset_form = DeviceFilterForm
template_name = 'virtualization/cluster/devices.html' actions = (EditObject, DeleteObject, BulkEdit)
actions = {
'add': {'add'},
'export': {'view'},
'bulk_import': {'add'},
'bulk_edit': {'change'},
'bulk_remove_devices': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Devices'), label=_('Devices'),
badge=lambda obj: obj.devices.count(), 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 # Virtual machines
# #
@ -371,7 +322,7 @@ class VirtualMachineListView(generic.ObjectListView):
filterset = filtersets.VirtualMachineFilterSet filterset = filtersets.VirtualMachineFilterSet
filterset_form = forms.VirtualMachineFilterForm filterset_form = forms.VirtualMachineFilterForm
table = tables.VirtualMachineTable table = tables.VirtualMachineTable
template_name = 'virtualization/virtualmachine_list.html' actions = (AddObject, BulkImport, BulkExport, BulkAddComponents, BulkEdit, BulkDelete)
@register_model_view(VirtualMachine) @register_model_view(VirtualMachine)
@ -386,11 +337,7 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
table = tables.VirtualMachineVMInterfaceTable table = tables.VirtualMachineVMInterfaceTable
filterset = filtersets.VMInterfaceFilterSet filterset = filtersets.VMInterfaceFilterSet
filterset_form = forms.VMInterfaceFilterForm filterset_form = forms.VMInterfaceFilterForm
template_name = 'virtualization/virtualmachine/interfaces.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
tab = ViewTab( tab = ViewTab(
label=_('Interfaces'), label=_('Interfaces'),
badge=lambda obj: obj.interface_count, badge=lambda obj: obj.interface_count,
@ -412,17 +359,13 @@ class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
table = tables.VirtualMachineVirtualDiskTable table = tables.VirtualMachineVirtualDiskTable
filterset = filtersets.VirtualDiskFilterSet filterset = filtersets.VirtualDiskFilterSet
filterset_form = forms.VirtualDiskFilterForm filterset_form = forms.VirtualDiskFilterForm
template_name = 'virtualization/virtualmachine/virtual_disks.html' actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab( tab = ViewTab(
label=_('Virtual Disks'), label=_('Virtual Disks'),
badge=lambda obj: obj.virtual_disk_count, badge=lambda obj: obj.virtual_disk_count,
permission='virtualization.view_virtualdisk', permission='virtualization.view_virtualdisk',
weight=500 weight=500
) )
actions = {
**DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
}
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags') return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags')