diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f21bc3204..1241143b7 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -7,7 +7,7 @@ from dcim.models import ( ) from tenancy.tables import TenantColumn from utilities.tables import ( - ActionsColumn, BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, + ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn, ) from .template_code import * @@ -322,10 +322,8 @@ class DeviceConsolePortTable(ConsolePortTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=ConsolePort, - buttons=('edit', 'delete'), - prepend_template=CONSOLEPORT_BUTTONS + actions = ActionsColumn( + extra_buttons=CONSOLEPORT_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -367,10 +365,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=ConsoleServerPort, - buttons=('edit', 'delete'), - prepend_template=CONSOLESERVERPORT_BUTTONS + actions = ActionsColumn( + extra_buttons=CONSOLESERVERPORT_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -412,10 +408,8 @@ class DevicePowerPortTable(PowerPortTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=PowerPort, - buttons=('edit', 'delete'), - prepend_template=POWERPORT_BUTTONS + actions = ActionsColumn( + extra_buttons=POWERPORT_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -461,10 +455,8 @@ class DevicePowerOutletTable(PowerOutletTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=PowerOutlet, - buttons=('edit', 'delete'), - prepend_template=POWEROUTLET_BUTTONS + actions = ActionsColumn( + extra_buttons=POWEROUTLET_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -551,10 +543,8 @@ class DeviceInterfaceTable(InterfaceTable): linkify=True, verbose_name='LAG' ) - actions = ButtonsColumn( - model=Interface, - buttons=('edit', 'delete'), - prepend_template=INTERFACE_BUTTONS + actions = ActionsColumn( + extra_buttons=INTERFACE_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -614,10 +604,8 @@ class DeviceFrontPortTable(FrontPortTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=FrontPort, - buttons=('edit', 'delete'), - prepend_template=FRONTPORT_BUTTONS + actions = ActionsColumn( + extra_buttons=FRONTPORT_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -662,10 +650,8 @@ class DeviceRearPortTable(RearPortTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=RearPort, - buttons=('edit', 'delete'), - prepend_template=REARPORT_BUTTONS + actions = ActionsColumn( + extra_buttons=REARPORT_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -713,10 +699,8 @@ class DeviceDeviceBayTable(DeviceBayTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=DeviceBay, - buttons=('edit', 'delete'), - prepend_template=DEVICEBAY_BUTTONS + actions = ActionsColumn( + extra_buttons=DEVICEBAY_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -749,10 +733,8 @@ class ModuleBayTable(DeviceComponentTable): class DeviceModuleBayTable(ModuleBayTable): - actions = ButtonsColumn( - model=DeviceBay, - buttons=('edit', 'delete'), - prepend_template=MODULEBAY_BUTTONS + actions = ActionsColumn( + extra_buttons=MODULEBAY_BUTTONS ) class Meta(DeviceComponentTable.Meta): @@ -803,10 +785,7 @@ class DeviceInventoryItemTable(InventoryItemTable): order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) - actions = ButtonsColumn( - model=InventoryItem, - buttons=('edit', 'delete') - ) + actions = ActionsColumn() class Meta(BaseTable.Meta): model = InventoryItem diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 29fa4d4de..ecec67f7d 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -6,8 +6,7 @@ from dcim.models import ( InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, ) from utilities.tables import ( - ActionsColumn, BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, - ToggleColumn, + ActionsColumn, BaseTable, BooleanColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn, ) from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS @@ -113,10 +112,9 @@ class ComponentTemplateTable(BaseTable): class ConsolePortTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=ConsolePortTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -126,10 +124,9 @@ class ConsolePortTemplateTable(ComponentTemplateTable): class ConsoleServerPortTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=ConsoleServerPortTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -139,10 +136,9 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable): class PowerPortTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=PowerPortTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -152,10 +148,9 @@ class PowerPortTemplateTable(ComponentTemplateTable): class PowerOutletTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=PowerOutletTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -168,10 +163,9 @@ class InterfaceTemplateTable(ComponentTemplateTable): mgmt_only = BooleanColumn( verbose_name='Management Only' ) - actions = ButtonsColumn( - model=InterfaceTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -185,10 +179,9 @@ class FrontPortTemplateTable(ComponentTemplateTable): verbose_name='Position' ) color = ColorColumn() - actions = ButtonsColumn( - model=FrontPortTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -199,10 +192,9 @@ class FrontPortTemplateTable(ComponentTemplateTable): class RearPortTemplateTable(ComponentTemplateTable): color = ColorColumn() - actions = ButtonsColumn( - model=RearPortTemplate, - buttons=('edit', 'delete'), - prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS ) class Meta(ComponentTemplateTable.Meta): @@ -212,9 +204,8 @@ class RearPortTemplateTable(ComponentTemplateTable): class ModuleBayTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=ModuleBayTemplate, - buttons=('edit', 'delete') + actions = ActionsColumn( + sequence=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -224,9 +215,8 @@ class ModuleBayTemplateTable(ComponentTemplateTable): class DeviceBayTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=DeviceBayTemplate, - buttons=('edit', 'delete') + actions = ActionsColumn( + sequence=('edit', 'delete') ) class Meta(ComponentTemplateTable.Meta): @@ -236,9 +226,8 @@ class DeviceBayTemplateTable(ComponentTemplateTable): class InventoryItemTemplateTable(ComponentTemplateTable): - actions = ButtonsColumn( - model=InventoryItemTemplate, - buttons=('edit', 'delete') + actions = ActionsColumn( + sequence=('edit', 'delete') ) role = tables.Column( linkify=True diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 23ffabae2..98c5e3fd3 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -3,9 +3,9 @@ import django_tables2 as tables from dcim.models import Location, Region, Site, SiteGroup from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, + ActionsColumn, BaseTable, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, ) -from .template_code import LOCATION_ELEVATIONS +from .template_code import LOCATION_BUTTONS __all__ = ( 'LocationTable', @@ -127,9 +127,8 @@ class LocationTable(BaseTable): tags = TagColumn( url_name='dcim:location_list' ) - actions = ButtonsColumn( - model=Location, - prepend_template=LOCATION_ELEVATIONS + actions = ActionsColumn( + extra_buttons=LOCATION_BUTTONS ) class Meta(BaseTable.Meta): diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 2b6c02b82..a1baeb336 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -87,7 +87,7 @@ POWERFEED_CABLETERMINATION = """ {{ value }} """ -LOCATION_ELEVATIONS = """ +LOCATION_BUTTONS = """ @@ -99,8 +99,8 @@ LOCATION_ELEVATIONS = """ MODULAR_COMPONENT_TEMPLATE_BUTTONS = """ {% load helpers %} -{% if perms.dcim.add_invnetoryitemtemplate %} - +{% if perms.dcim.add_inventoryitemtemplate %} + {% endif %} diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index 1379ad105..3454ddff4 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -5,8 +5,8 @@ from django_tables2.utils import Accessor from dcim.models import Interface from tenancy.tables import TenantColumn from utilities.tables import ( - ActionsColumn, BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, - TagColumn, TemplateColumn, ToggleColumn, + ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn, + TemplateColumn, ToggleColumn, ) from virtualization.models import VMInterface from ipam.models import * @@ -38,7 +38,7 @@ VLAN_PREFIXES = """ {% endfor %} """ -VLANGROUP_ADD_VLAN = """ +VLANGROUP_BUTTONS = """ {% with next_vid=record.get_next_available_vid %} {% if next_vid and perms.ipam.add_vlan %} @@ -77,9 +77,8 @@ class VLANGroupTable(BaseTable): tags = TagColumn( url_name='ipam:vlangroup_list' ) - actions = ButtonsColumn( - model=VLANGroup, - prepend_template=VLANGROUP_ADD_VLAN + actions = ActionsColumn( + extra_buttons=VLANGROUP_BUTTONS ) class Meta(BaseTable.Meta): diff --git a/netbox/utilities/tables/columns.py b/netbox/utilities/tables/columns.py index e601bd0cc..a319fc7ad 100644 --- a/netbox/utilities/tables/columns.py +++ b/netbox/utilities/tables/columns.py @@ -1,9 +1,10 @@ -from collections import namedtuple from dataclasses import dataclass from typing import Optional import django_tables2 as tables from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.template import Context, Template from django.urls import reverse from django.utils.safestring import mark_safe from django_tables2.utils import Accessor @@ -14,7 +15,6 @@ from utilities.utils import content_type_identifier, content_type_name __all__ = ( 'ActionsColumn', 'BooleanColumn', - 'ButtonsColumn', 'ChoiceFieldColumn', 'ColorColumn', 'ColoredLabelColumn', @@ -100,7 +100,14 @@ class ActionsItem: class ActionsColumn(tables.Column): - attrs = {'td': {'class': 'text-end noprint'}} + """ + A dropdown menu which provides edit, delete, and changelog links for an object. Can optionally include + additional buttons rendered from a template string. + + :param sequence: The ordered list of dropdown menu items to include + :param extra_buttons: A Django template string which renders additional buttons preceding the actions dropdown + """ + attrs = {'td': {'class': 'text-end text-nowrap noprint'}} empty_values = () actions = { 'edit': ActionsItem('Edit', 'pencil', 'change'), @@ -108,12 +115,10 @@ class ActionsColumn(tables.Column): 'changelog': ActionsItem('Changelog', 'history'), } - def __init__(self, *args, extra_actions=None, sequence=('edit', 'delete', 'changelog'), **kwargs): + def __init__(self, *args, sequence=('edit', 'delete', 'changelog'), extra_buttons='', **kwargs): super().__init__(*args, **kwargs) - # Add/update any extra actions passed - if extra_actions: - self.actions.update(extra_actions) + self.extra_buttons = extra_buttons # Determine which actions to enable self.actions = { @@ -134,9 +139,10 @@ class ActionsColumn(tables.Column): url_appendix = f'?return_url={request.path}' if request else '' links = [] + user = getattr(request, 'user', AnonymousUser()) for action, attrs in self.actions.items(): permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}' - if attrs.permission is None or request.user.has_perm(permission): + if attrs.permission is None or user.has_perm(permission): url = reverse(f'{viewname_base}_{action}', kwargs={'pk': record.pk}) links.append(f'
  • ' f' {attrs.title}
  • ') @@ -144,68 +150,21 @@ class ActionsColumn(tables.Column): if not links: return '' - menu = f'' + f'' + + # Render any extra buttons from template code + if self.extra_buttons: + template = Template(self.extra_buttons) + context = getattr(table, "context", Context()) + context.update({'record': record}) + menu = template.render(context) + menu return mark_safe(menu) -class ButtonsColumn(tables.TemplateColumn): - """ - Render edit, delete, and changelog buttons for an object. - - :param model: Model class to use for calculating URL view names - :param prepend_content: Additional template content to render in the column (optional) - """ - buttons = ('changelog', 'edit', 'delete') - attrs = {'td': {'class': 'text-end text-nowrap noprint'}} - # Note that braces are escaped to allow for string formatting prior to template rendering - template_code = """ - {{% if "changelog" in buttons %}} - - - - {{% endif %}} - {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}} - - - - {{% endif %}} - {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}} - - - - {{% endif %}} - """ - - def __init__(self, model, *args, buttons=None, prepend_template=None, **kwargs): - if prepend_template: - prepend_template = prepend_template.replace('{', '{{') - prepend_template = prepend_template.replace('}', '}}') - self.template_code = prepend_template + self.template_code - - template_code = self.template_code.format( - app_label=model._meta.app_label, - model_name=model._meta.model_name, - buttons=buttons - ) - - super().__init__(template_code=template_code, *args, **kwargs) - - # Exclude from export by default - if 'exclude_from_export' not in kwargs: - self.exclude_from_export = True - - self.extra_context.update({ - 'buttons': buttons or self.buttons, - }) - - def header(self): - return '' - - class ChoiceFieldColumn(tables.Column): """ Render a ChoiceField value inside a indicating a particular CSS class. This is useful for displaying colored diff --git a/netbox/utilities/tests/test_tables.py b/netbox/utilities/tests/test_tables.py index 119587ff8..55a5e4cc7 100644 --- a/netbox/utilities/tests/test_tables.py +++ b/netbox/utilities/tests/test_tables.py @@ -30,7 +30,8 @@ class TagColumnTest(TestCase): def test_tagcolumn(self): template = Template('{% load render_table from django_tables2 %}{% render_table table %}') + table = TagColumnTable(Site.objects.all(), orderable=False) context = Context({ - 'table': TagColumnTable(Site.objects.all(), orderable=False) + 'table': table }) template.render(context) diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 65f9f1257..0588f51a5 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -3,7 +3,7 @@ import django_tables2 as tables from dcim.tables.devices import BaseInterfaceTable from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, + ActionsColumn, BaseTable, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn, ) from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -183,10 +183,9 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable): bridge = tables.Column( linkify=True ) - actions = ButtonsColumn( - model=VMInterface, - buttons=('edit', 'delete'), - prepend_template=VMINTERFACE_BUTTONS + actions = ActionsColumn( + sequence=('edit', 'delete'), + extra_buttons=VMINTERFACE_BUTTONS ) class Meta(BaseTable.Meta):