Merge pull request #8303 from netbox-community/7679-table-actions

Closes #7679: Object table actions menus
This commit is contained in:
Jeremy Stretch 2022-01-10 11:38:07 -05:00 committed by GitHub
commit 17aa37ae21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 374 additions and 395 deletions

View File

@ -57,6 +57,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
### Enhancements ### Enhancements
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation * [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
* [#7679](https://github.com/netbox-community/netbox/issues/7679) - Add actions menu to all object tables
* [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks * [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks
* [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form * [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form
* [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts * [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn from utilities.tables import BaseTable, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn
from .models import * from .models import *
@ -88,12 +88,11 @@ class CircuitTypeTable(BaseTable):
circuit_count = tables.Column( circuit_count = tables.Column(
verbose_name='Circuits' verbose_name='Circuits'
) )
actions = ButtonsColumn(CircuitType)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
# #

View File

@ -7,7 +7,7 @@ from dcim.models import (
) )
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn, MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
) )
from .template_code import * from .template_code import *
@ -94,7 +94,6 @@ class DeviceRoleTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:devicerole_list' url_name='dcim:devicerole_list'
) )
actions = ButtonsColumn(DeviceRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
@ -102,7 +101,7 @@ class DeviceRoleTable(BaseTable):
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
'actions', 'actions',
) )
default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description')
# #
@ -127,7 +126,6 @@ class PlatformTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:platform_list' url_name='dcim:platform_list'
) )
actions = ButtonsColumn(Platform)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Platform model = Platform
@ -136,7 +134,7 @@ class PlatformTable(BaseTable):
'description', 'tags', 'actions', 'description', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
) )
@ -324,10 +322,8 @@ class DeviceConsolePortTable(ConsolePortTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=ConsolePort, extra_buttons=CONSOLEPORT_BUTTONS
buttons=('edit', 'delete'),
prepend_template=CONSOLEPORT_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -336,7 +332,7 @@ class DeviceConsolePortTable(ConsolePortTable):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions' 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
} }
@ -369,10 +365,8 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=ConsoleServerPort, extra_buttons=CONSOLESERVERPORT_BUTTONS
buttons=('edit', 'delete'),
prepend_template=CONSOLESERVERPORT_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -381,7 +375,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection', 'actions') default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
} }
@ -414,10 +408,8 @@ class DevicePowerPortTable(PowerPortTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=PowerPort, extra_buttons=POWERPORT_BUTTONS
buttons=('edit', 'delete'),
prepend_template=POWERPORT_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -428,7 +420,6 @@ class DevicePowerPortTable(PowerPortTable):
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection', 'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
'actions',
) )
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
@ -464,10 +455,8 @@ class DevicePowerOutletTable(PowerOutletTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=PowerOutlet, extra_buttons=POWEROUTLET_BUTTONS
buttons=('edit', 'delete'),
prepend_template=POWEROUTLET_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -477,7 +466,7 @@ class DevicePowerOutletTable(PowerOutletTable):
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'actions', 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
) )
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
@ -557,10 +546,8 @@ class DeviceInterfaceTable(InterfaceTable):
linkify=True, linkify=True,
verbose_name='LAG' verbose_name='LAG'
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=Interface, extra_buttons=INTERFACE_BUTTONS
buttons=('edit', 'delete'),
prepend_template=INTERFACE_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -575,7 +562,7 @@ class DeviceInterfaceTable(InterfaceTable):
order_by = ('name',) order_by = ('name',)
default_columns = ( default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses', 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
'cable', 'connection', 'actions', 'cable', 'connection',
) )
row_attrs = { row_attrs = {
'class': get_interface_row_class, 'class': get_interface_row_class,
@ -620,10 +607,8 @@ class DeviceFrontPortTable(FrontPortTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=FrontPort, extra_buttons=FRONTPORT_BUTTONS
buttons=('edit', 'delete'),
prepend_template=FRONTPORT_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -634,7 +619,6 @@ class DeviceFrontPortTable(FrontPortTable):
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer', 'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
'actions',
) )
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
@ -669,10 +653,8 @@ class DeviceRearPortTable(RearPortTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=RearPort, extra_buttons=REARPORT_BUTTONS
buttons=('edit', 'delete'),
prepend_template=REARPORT_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -682,7 +664,7 @@ class DeviceRearPortTable(RearPortTable):
'cable', 'cable_color', 'link_peer', 'tags', 'actions', 'cable', 'cable_color', 'link_peer', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions', 'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
) )
row_attrs = { row_attrs = {
'class': get_cabletermination_row_class 'class': get_cabletermination_row_class
@ -720,10 +702,8 @@ class DeviceDeviceBayTable(DeviceBayTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=DeviceBay, extra_buttons=DEVICEBAY_BUTTONS
buttons=('edit', 'delete'),
prepend_template=DEVICEBAY_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
@ -731,9 +711,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
fields = ( fields = (
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions', 'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
) )
default_columns = ( default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
)
class ModuleBayTable(DeviceComponentTable): class ModuleBayTable(DeviceComponentTable):
@ -758,16 +736,14 @@ class ModuleBayTable(DeviceComponentTable):
class DeviceModuleBayTable(ModuleBayTable): class DeviceModuleBayTable(ModuleBayTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=DeviceBay, extra_buttons=MODULEBAY_BUTTONS
buttons=('edit', 'delete'),
prepend_template=MODULEBAY_BUTTONS
) )
class Meta(DeviceComponentTable.Meta): class Meta(DeviceComponentTable.Meta):
model = ModuleBay model = ModuleBay
fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions') fields = ('pk', 'id', 'name', 'label', 'description', 'installed_module', 'tags', 'actions')
default_columns = ('pk', 'name', 'label', 'description', 'installed_module', 'actions') default_columns = ('pk', 'name', 'label', 'description', 'installed_module')
class InventoryItemTable(DeviceComponentTable): class InventoryItemTable(DeviceComponentTable):
@ -812,10 +788,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
order_by=Accessor('_name'), order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ActionsColumn()
model=InventoryItem,
buttons=('edit', 'delete')
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItem model = InventoryItem
@ -824,7 +797,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
'description', 'discovered', 'tags', 'actions', 'description', 'discovered', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'actions', 'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
) )
@ -842,14 +815,13 @@ class InventoryItemRoleTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:inventoryitemrole_list' url_name='dcim:inventoryitemrole_list'
) )
actions = ButtonsColumn(InventoryItemRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InventoryItemRole model = InventoryItemRole
fields = ( fields = (
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions', 'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description', 'actions') default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description')
# #

View File

@ -6,7 +6,7 @@ from dcim.models import (
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
) )
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn, ActionsColumn, BaseTable, BooleanColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn,
) )
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
@ -48,7 +48,6 @@ class ManufacturerTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:manufacturer_list' url_name='dcim:manufacturer_list'
) )
actions = ButtonsColumn(Manufacturer)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Manufacturer model = Manufacturer
@ -57,7 +56,7 @@ class ManufacturerTable(BaseTable):
'actions', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions', 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug',
) )
@ -113,10 +112,9 @@ class ComponentTemplateTable(BaseTable):
class ConsolePortTemplateTable(ComponentTemplateTable): class ConsolePortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=ConsolePortTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -126,10 +124,9 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
class ConsoleServerPortTemplateTable(ComponentTemplateTable): class ConsoleServerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=ConsoleServerPortTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -139,10 +136,9 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
class PowerPortTemplateTable(ComponentTemplateTable): class PowerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=PowerPortTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -152,10 +148,9 @@ class PowerPortTemplateTable(ComponentTemplateTable):
class PowerOutletTemplateTable(ComponentTemplateTable): class PowerOutletTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=PowerOutletTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -168,10 +163,9 @@ class InterfaceTemplateTable(ComponentTemplateTable):
mgmt_only = BooleanColumn( mgmt_only = BooleanColumn(
verbose_name='Management Only' verbose_name='Management Only'
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=InterfaceTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -185,10 +179,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
verbose_name='Position' verbose_name='Position'
) )
color = ColorColumn() color = ColorColumn()
actions = ButtonsColumn( actions = ActionsColumn(
model=FrontPortTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -199,10 +192,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
class RearPortTemplateTable(ComponentTemplateTable): class RearPortTemplateTable(ComponentTemplateTable):
color = ColorColumn() color = ColorColumn()
actions = ButtonsColumn( actions = ActionsColumn(
model=RearPortTemplate, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
prepend_template=MODULAR_COMPONENT_TEMPLATE_BUTTONS
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -212,9 +204,8 @@ class RearPortTemplateTable(ComponentTemplateTable):
class ModuleBayTemplateTable(ComponentTemplateTable): class ModuleBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=ModuleBayTemplate, sequence=('edit', 'delete')
buttons=('edit', 'delete')
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -224,9 +215,8 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
class DeviceBayTemplateTable(ComponentTemplateTable): class DeviceBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=DeviceBayTemplate, sequence=('edit', 'delete')
buttons=('edit', 'delete')
) )
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
@ -236,9 +226,8 @@ class DeviceBayTemplateTable(ComponentTemplateTable):
class InventoryItemTemplateTable(ComponentTemplateTable): class InventoryItemTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn( actions = ActionsColumn(
model=InventoryItemTemplate, sequence=('edit', 'delete')
buttons=('edit', 'delete')
) )
role = tables.Column( role = tables.Column(
linkify=True linkify=True

View File

@ -4,8 +4,8 @@ from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole from dcim.models import Rack, RackReservation, RackRole
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, BaseTable, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn,
TagColumn, ToggleColumn, UtilizationColumn, ToggleColumn, UtilizationColumn,
) )
__all__ = ( __all__ = (
@ -27,12 +27,11 @@ class RackRoleTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:rackrole_list' url_name='dcim:rackrole_list'
) )
actions = ButtonsColumn(RackRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
# #
@ -121,7 +120,6 @@ class RackReservationTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:rackreservation_list' url_name='dcim:rackreservation_list'
) )
actions = ButtonsColumn(RackReservation)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackReservation model = RackReservation
@ -129,6 +127,4 @@ class RackReservationTable(BaseTable):
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags', 'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
'actions', 'actions',
) )
default_columns = ( default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
)

View File

@ -3,9 +3,9 @@ import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup from dcim.models import Location, Region, Site, SiteGroup
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( 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__ = ( __all__ = (
'LocationTable', 'LocationTable',
@ -32,12 +32,11 @@ class RegionTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:region_list' url_name='dcim:region_list'
) )
actions = ButtonsColumn(Region)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Region model = Region
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description')
# #
@ -57,12 +56,11 @@ class SiteGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:sitegroup_list' url_name='dcim:sitegroup_list'
) )
actions = ButtonsColumn(SiteGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SiteGroup model = SiteGroup
fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'site_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'site_count', 'description', 'actions') default_columns = ('pk', 'name', 'site_count', 'description')
# #
@ -98,6 +96,7 @@ class SiteTable(BaseTable):
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
'actions',
) )
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
@ -128,9 +127,8 @@ class LocationTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='dcim:location_list' url_name='dcim:location_list'
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=Location, extra_buttons=LOCATION_BUTTONS
prepend_template=LOCATION_ELEVATIONS
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -139,4 +137,4 @@ class LocationTable(BaseTable):
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
'actions', 'actions',
) )
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

View File

@ -87,7 +87,7 @@ POWERFEED_CABLETERMINATION = """
<a href="{{ value.get_absolute_url }}">{{ value }}</a> <a href="{{ value.get_absolute_url }}">{{ value }}</a>
""" """
LOCATION_ELEVATIONS = """ LOCATION_BUTTONS = """
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations"> <a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-sm btn-primary" title="View elevations">
<i class="mdi mdi-server"></i> <i class="mdi mdi-server"></i>
</a> </a>
@ -99,8 +99,8 @@ LOCATION_ELEVATIONS = """
MODULAR_COMPONENT_TEMPLATE_BUTTONS = """ MODULAR_COMPONENT_TEMPLATE_BUTTONS = """
{% load helpers %} {% load helpers %}
{% if perms.dcim.add_invnetoryitemtemplate %} {% if perms.dcim.add_inventoryitemtemplate %}
<a href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ record.device_type.pk }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={{ request.path }}" title="Add inventory item" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ record.device_type_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={{ request.path }}" title="Add inventory item" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -2,7 +2,7 @@ import django_tables2 as tables
from django.conf import settings from django.conf import settings
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn, ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn,
MarkdownColumn, ToggleColumn, MarkdownColumn, ToggleColumn,
) )
from .models import * from .models import *
@ -152,12 +152,11 @@ class TagTable(BaseTable):
linkify=True linkify=True
) )
color = ColorColumn() color = ColorColumn()
actions = ButtonsColumn(Tag)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Tag model = Tag
fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions') fields = ('pk', 'id', 'name', 'items', 'slug', 'color', 'description', 'actions')
default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description', 'actions') default_columns = ('pk', 'name', 'items', 'slug', 'color', 'description')
class TaggedItemTable(BaseTable): class TaggedItemTable(BaseTable):
@ -215,6 +214,7 @@ class ObjectChangeTable(BaseTable):
template_code=OBJECTCHANGE_REQUEST_ID, template_code=OBJECTCHANGE_REQUEST_ID,
verbose_name='Request ID' verbose_name='Request ID'
) )
actions = ActionsColumn(sequence=())
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ObjectChange model = ObjectChange
@ -233,9 +233,6 @@ class ObjectJournalTable(BaseTable):
comments = tables.TemplateColumn( comments = tables.TemplateColumn(
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}' template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}'
) )
actions = ButtonsColumn(
model=JournalEntry
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = JournalEntry model = JournalEntry
@ -261,6 +258,5 @@ class JournalEntryTable(ObjectJournalTable):
'comments', 'actions' 'comments', 'actions'
) )
default_columns = ( default_columns = (
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
'comments', 'actions'
) )

View File

@ -1,6 +1,6 @@
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import BaseTable, ButtonsColumn, MarkdownColumn, TagColumn, ToggleColumn from utilities.tables import ActionsColumn, BaseTable, MarkdownColumn, TagColumn, ToggleColumn
from ipam.models import * from ipam.models import *
__all__ = ( __all__ = (
@ -58,9 +58,8 @@ class FHRPGroupAssignmentTable(BaseTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=FHRPGroupAssignment, sequence=('edit', 'delete')
buttons=('edit', 'delete', 'foo')
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -2,12 +2,11 @@ import django_tables2 as tables
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from ipam.models import *
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn, UtilizationColumn,
ToggleColumn, UtilizationColumn,
) )
from ipam.models import *
__all__ = ( __all__ = (
'AggregateTable', 'AggregateTable',
@ -89,12 +88,11 @@ class RIRTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='ipam:rir_list' url_name='ipam:rir_list'
) )
actions = ButtonsColumn(RIR)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RIR model = RIR
fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description')
# #
@ -111,12 +109,11 @@ class ASNTable(BaseTable):
url_params={'asn_id': 'pk'}, url_params={'asn_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
actions = ButtonsColumn(ASN)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ASN model = ASN
fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions') fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions')
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'tenant', 'actions') default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'tenant')
# #
@ -173,12 +170,11 @@ class RoleTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='ipam:role_list' url_name='ipam:role_list'
) )
actions = ButtonsColumn(Role)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions')
default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description')
# #
@ -405,9 +401,6 @@ class AssignedIPAddressesTable(BaseTable):
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = TenantColumn() tenant = TenantColumn()
actions = ButtonsColumn(
model=IPAddress
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress

View File

@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface from dcim.models import Interface
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn, ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
TemplateColumn, ToggleColumn, TemplateColumn, ToggleColumn,
) )
from virtualization.models import VMInterface from virtualization.models import VMInterface
@ -38,7 +38,7 @@ VLAN_PREFIXES = """
{% endfor %} {% endfor %}
""" """
VLANGROUP_ADD_VLAN = """ VLANGROUP_BUTTONS = """
{% with next_vid=record.get_next_available_vid %} {% with next_vid=record.get_next_available_vid %}
{% if next_vid and perms.ipam.add_vlan %} {% if next_vid and perms.ipam.add_vlan %}
<a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-sm btn-success"> <a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-sm btn-success">
@ -77,9 +77,8 @@ class VLANGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='ipam:vlangroup_list' url_name='ipam:vlangroup_list'
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=VLANGroup, extra_buttons=VLANGROUP_BUTTONS
prepend_template=VLANGROUP_ADD_VLAN
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -88,7 +87,7 @@ class VLANGroupTable(BaseTable):
'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description', 'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description',
'tags', 'actions', 'tags', 'actions',
) )
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description')
# #
@ -153,7 +152,9 @@ class VLANDevicesTable(VLANMembersTable):
device = tables.Column( device = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn(Interface, buttons=['edit']) actions = ActionsColumn(
sequence=('edit',)
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
@ -165,7 +166,9 @@ class VLANVirtualMachinesTable(VLANMembersTable):
virtual_machine = tables.Column( virtual_machine = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn(VMInterface, buttons=['edit']) actions = ActionsColumn(
sequence=('edit',)
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VMInterface model = VMInterface

View File

@ -203,7 +203,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
:param table: The Table instance to export :param table: The Table instance to export
:param columns: A list of specific columns to include. If not specified, all columns will be exported. :param columns: A list of specific columns to include. If not specified, all columns will be exported.
""" """
exclude_columns = {'pk'} exclude_columns = {'pk', 'actions'}
if columns: if columns:
all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns] all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns]
exclude_columns.update({ exclude_columns.update({

View File

@ -1,7 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import ( from utilities.tables import (
BaseTable, ButtonsColumn, ContentTypeColumn, LinkedCountColumn, linkify_phone, MarkdownColumn, MPTTColumn, ActionsColumn, BaseTable, ContentTypeColumn, LinkedCountColumn, linkify_phone, MarkdownColumn, MPTTColumn,
TagColumn, ToggleColumn, TagColumn, ToggleColumn,
) )
from .models import * from .models import *
@ -59,12 +59,11 @@ class TenantGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='tenancy:tenantgroup_list' url_name='tenancy:tenantgroup_list'
) )
actions = ButtonsColumn(TenantGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = TenantGroup model = TenantGroup
fields = ('pk', 'id', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'id', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') default_columns = ('pk', 'name', 'tenant_count', 'description')
class TenantTable(BaseTable): class TenantTable(BaseTable):
@ -103,12 +102,11 @@ class ContactGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='tenancy:contactgroup_list' url_name='tenancy:contactgroup_list'
) )
actions = ButtonsColumn(ContactGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ContactGroup model = ContactGroup
fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'contact_count', 'description', 'actions') default_columns = ('pk', 'name', 'contact_count', 'description')
class ContactRoleTable(BaseTable): class ContactRoleTable(BaseTable):
@ -116,12 +114,11 @@ class ContactRoleTable(BaseTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn(ContactRole)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ContactRole model = ContactRole
fields = ('pk', 'name', 'description', 'slug', 'actions') fields = ('pk', 'name', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'description', 'actions') default_columns = ('pk', 'name', 'description')
class ContactTable(BaseTable): class ContactTable(BaseTable):
@ -164,12 +161,11 @@ class ContactAssignmentTable(BaseTable):
role = tables.Column( role = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=ContactAssignment, sequence=('edit', 'delete')
buttons=('edit', 'delete')
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ContactAssignment model = ContactAssignment
fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority', 'actions') fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority', 'actions')
default_columns = ('pk', 'content_type', 'object', 'contact', 'role', 'priority', 'actions') default_columns = ('pk', 'content_type', 'object', 'contact', 'role', 'priority')

View File

@ -0,0 +1,30 @@
from django_tables2 import RequestConfig
from utilities.paginator import EnhancedPaginator, get_paginate_count
from .columns import *
from .tables import *
#
# Pagination
#
def paginate_table(table, request):
"""
Paginate a table given a request context.
"""
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request)
}
RequestConfig(request, paginate).configure(table)
#
# Callables
#
def linkify_phone(value):
if value is None:
return None
return f"tel:{value}"

View File

@ -1,149 +1,36 @@
from dataclasses import dataclass
from typing import Optional
import django_tables2 as tables import django_tables2 as tables
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey from django.template import Context, Template
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2 import RequestConfig
from django_tables2.data import TableQuerysetData
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField, CustomLink from utilities.utils import content_type_identifier, content_type_name
from .utils import content_type_identifier, content_type_name
from .paginator import EnhancedPaginator, get_paginate_count
__all__ = (
'ActionsColumn',
'BooleanColumn',
'ChoiceFieldColumn',
'ColorColumn',
'ColoredLabelColumn',
'ContentTypeColumn',
'ContentTypesColumn',
'CustomFieldColumn',
'CustomLinkColumn',
'LinkedCountColumn',
'MarkdownColumn',
'MPTTColumn',
'TagColumn',
'TemplateColumn',
'ToggleColumn',
'UtilizationColumn',
)
class BaseTable(tables.Table):
"""
Default table for object lists
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
"""
id = tables.Column(
linkify=True,
verbose_name='ID'
)
class Meta:
attrs = {
'class': 'table table-hover object-list',
}
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
if extra_columns is None:
extra_columns = []
# Add custom field columns
obj_type = ContentType.objects.get_for_model(self._meta.model)
cf_columns = [
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
]
cl_columns = [
(f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
]
extra_columns.extend([*cf_columns, *cl_columns])
super().__init__(*args, extra_columns=extra_columns, **kwargs)
# Set default empty_text if none was provided
if self.empty_text is None:
self.empty_text = f"No {self._meta.model._meta.verbose_name_plural} found"
# Hide non-default columns
default_columns = getattr(self.Meta, 'default_columns', list())
if default_columns:
for column in self.columns:
if column.name not in default_columns:
self.columns.hide(column.name)
# Apply custom column ordering for user
if user is not None and not isinstance(user, AnonymousUser):
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
if selected_columns:
# Show only persistent or selected columns
for name, column in self.columns.items():
if name in ['pk', 'actions', *selected_columns]:
self.columns.show(name)
else:
self.columns.hide(name)
# Rearrange the sequence to list selected columns first, followed by all remaining columns
# TODO: There's probably a more clever way to accomplish this
self.sequence = [
*[c for c in selected_columns if c in self.columns.names()],
*[c for c in self.columns.names() if c not in selected_columns]
]
# PK column should always come first
if 'pk' in self.sequence:
self.sequence.remove('pk')
self.sequence.insert(0, 'pk')
# Actions column should always come last
if 'actions' in self.sequence:
self.sequence.remove('actions')
self.sequence.append('actions')
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
if isinstance(self.data, TableQuerysetData):
prefetch_fields = []
for column in self.columns:
if column.visible:
model = getattr(self.Meta, 'model')
accessor = column.accessor
prefetch_path = []
for field_name in accessor.split(accessor.SEPARATOR):
try:
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
break
if isinstance(field, RelatedField):
# Follow ForeignKeys to the related model
prefetch_path.append(field_name)
model = field.remote_field.model
elif isinstance(field, GenericForeignKey):
# Can't prefetch beyond a GenericForeignKey
prefetch_path.append(field_name)
break
if prefetch_path:
prefetch_fields.append('__'.join(prefetch_path))
self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields)
def _get_columns(self, visible=True):
columns = []
for name, column in self.columns.items():
if column.visible == visible and name not in ['pk', 'actions']:
columns.append((name, column.verbose_name))
return columns
@property
def available_columns(self):
return self._get_columns(visible=False)
@property
def selected_columns(self):
return self._get_columns(visible=True)
@property
def objects_count(self):
"""
Return the total number of real objects represented by the Table. This is useful when dealing with
prefixes/IP addresses/etc., where some table rows may represent available address space.
"""
if not hasattr(self, '_objects_count'):
self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
return self._objects_count
#
# Table columns
#
class ToggleColumn(tables.CheckBoxColumn): class ToggleColumn(tables.CheckBoxColumn):
""" """
@ -205,59 +92,78 @@ class TemplateColumn(tables.TemplateColumn):
return ret return ret
class ButtonsColumn(tables.TemplateColumn): @dataclass
""" class ActionsItem:
Render edit, delete, and changelog buttons for an object. title: str
icon: str
permission: Optional[str] = None
:param model: Model class to use for calculating URL view names
:param prepend_content: Additional template content to render in the column (optional) class ActionsColumn(tables.Column):
"""
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
""" """
buttons = ('changelog', 'edit', 'delete')
attrs = {'td': {'class': 'text-end text-nowrap noprint'}} attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
# Note that braces are escaped to allow for string formatting prior to template rendering empty_values = ()
template_code = """ actions = {
{{% if "changelog" in buttons %}} 'edit': ActionsItem('Edit', 'pencil', 'change'),
<a href="{{% url '{app_label}:{model_name}_changelog' pk=record.pk %}}" class="btn btn-outline-dark btn-sm" title="Change log"> 'delete': ActionsItem('Delete', 'trash-can-outline', 'delete'),
<i class="mdi mdi-history"></i> 'changelog': ActionsItem('Changelog', 'history'),
</a> }
{{% endif %}}
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
<a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-warning" title="Edit">
<i class="mdi mdi-pencil"></i>
</a>
{{% endif %}}
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
<a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-danger" title="Delete">
<i class="mdi mdi-trash-can-outline"></i>
</a>
{{% endif %}}
"""
def __init__(self, model, *args, buttons=None, prepend_template=None, **kwargs): def __init__(self, *args, sequence=('edit', 'delete', 'changelog'), extra_buttons='', **kwargs):
if prepend_template: super().__init__(*args, **kwargs)
prepend_template = prepend_template.replace('{', '{{')
prepend_template = prepend_template.replace('}', '}}')
self.template_code = prepend_template + self.template_code
template_code = self.template_code.format( self.extra_buttons = extra_buttons
app_label=model._meta.app_label,
model_name=model._meta.model_name,
buttons=buttons
)
super().__init__(template_code=template_code, *args, **kwargs) # Determine which actions to enable
self.actions = {
# Exclude from export by default name: self.actions[name] for name in sequence
if 'exclude_from_export' not in kwargs: }
self.exclude_from_export = True
self.extra_context.update({
'buttons': buttons or self.buttons,
})
def header(self): def header(self):
return '' return ''
def render(self, record, table, **kwargs):
# Skip dummy records (e.g. available VLANs) or those with no actions
if not hasattr(record, 'pk') or not self.actions:
return ''
model = table.Meta.model
viewname_base = f'{model._meta.app_label}:{model._meta.model_name}'
request = getattr(table, 'context', {}).get('request')
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 user.has_perm(permission):
url = reverse(f'{viewname_base}_{action}', kwargs={'pk': record.pk})
links.append(f'<li><a class="dropdown-item" href="{url}{url_appendix}">'
f'<i class="mdi mdi-{attrs.icon}"></i> {attrs.title}</a></li>')
if not links:
return ''
menu = f'<span class="dropdown">' \
f'<a class="btn btn-sm btn-secondary dropdown-toggle" href="#" type="button" data-bs-toggle="dropdown">' \
f'<i class="mdi mdi-wrench"></i></a>' \
f'<ul class="dropdown-menu">{"".join(links)}</ul></span>'
# 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 ChoiceFieldColumn(tables.Column): class ChoiceFieldColumn(tables.Column):
""" """
@ -509,34 +415,3 @@ class MarkdownColumn(tables.TemplateColumn):
def value(self, value): def value(self, value):
return value return value
#
# Pagination
#
def paginate_table(table, request):
"""
Paginate a table given a request context.
"""
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request)
}
RequestConfig(request, paginate).configure(table)
#
# Callables
#
def linkify_email(value):
if value is None:
return None
return f"mailto:{value}"
def linkify_phone(value):
if value is None:
return None
return f"tel:{value}"

View File

@ -0,0 +1,138 @@
import django_tables2 as tables
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
from django_tables2.data import TableQuerysetData
from extras.models import CustomField, CustomLink
from . import columns
__all__ = (
'BaseTable',
)
class BaseTable(tables.Table):
"""
Default table for object lists
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
"""
id = tables.Column(
linkify=True,
verbose_name='ID'
)
actions = columns.ActionsColumn()
class Meta:
attrs = {
'class': 'table table-hover object-list',
}
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
if extra_columns is None:
extra_columns = []
# Add custom field columns
obj_type = ContentType.objects.get_for_model(self._meta.model)
cf_columns = [
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
]
cl_columns = [
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
]
extra_columns.extend([*cf_columns, *cl_columns])
super().__init__(*args, extra_columns=extra_columns, **kwargs)
# Set default empty_text if none was provided
if self.empty_text is None:
self.empty_text = f"No {self._meta.model._meta.verbose_name_plural} found"
# Hide non-default columns (except for actions)
default_columns = [*getattr(self.Meta, 'default_columns', self.Meta.fields), 'actions']
for column in self.columns:
if column.name not in default_columns:
self.columns.hide(column.name)
# Apply custom column ordering for user
if user is not None and not isinstance(user, AnonymousUser):
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
if selected_columns:
# Show only persistent or selected columns
for name, column in self.columns.items():
if name in ['pk', 'actions', *selected_columns]:
self.columns.show(name)
else:
self.columns.hide(name)
# Rearrange the sequence to list selected columns first, followed by all remaining columns
# TODO: There's probably a more clever way to accomplish this
self.sequence = [
*[c for c in selected_columns if c in self.columns.names()],
*[c for c in self.columns.names() if c not in selected_columns]
]
# PK column should always come first
if 'pk' in self.sequence:
self.sequence.remove('pk')
self.sequence.insert(0, 'pk')
# Actions column should always come last
if 'actions' in self.sequence:
self.sequence.remove('actions')
self.sequence.append('actions')
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
if isinstance(self.data, TableQuerysetData):
prefetch_fields = []
for column in self.columns:
if column.visible:
model = getattr(self.Meta, 'model')
accessor = column.accessor
prefetch_path = []
for field_name in accessor.split(accessor.SEPARATOR):
try:
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
break
if isinstance(field, RelatedField):
# Follow ForeignKeys to the related model
prefetch_path.append(field_name)
model = field.remote_field.model
elif isinstance(field, GenericForeignKey):
# Can't prefetch beyond a GenericForeignKey
prefetch_path.append(field_name)
break
if prefetch_path:
prefetch_fields.append('__'.join(prefetch_path))
self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields)
def _get_columns(self, visible=True):
columns = []
for name, column in self.columns.items():
if column.visible == visible and name not in ['pk', 'actions']:
columns.append((name, column.verbose_name))
return columns
@property
def available_columns(self):
return self._get_columns(visible=False)
@property
def selected_columns(self):
return self._get_columns(visible=True)
@property
def objects_count(self):
"""
Return the total number of real objects represented by the Table. This is useful when dealing with
prefixes/IP addresses/etc., where some table rows may represent available address space.
"""
if not hasattr(self, '_objects_count'):
self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
return self._objects_count

View File

@ -30,7 +30,8 @@ class TagColumnTest(TestCase):
def test_tagcolumn(self): def test_tagcolumn(self):
template = Template('{% load render_table from django_tables2 %}{% render_table table %}') template = Template('{% load render_table from django_tables2 %}{% render_table table %}')
table = TagColumnTable(Site.objects.all(), orderable=False)
context = Context({ context = Context({
'table': TagColumnTable(Site.objects.all(), orderable=False) 'table': table
}) })
template.render(context) template.render(context)

View File

@ -1,8 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from tenancy.tables import TenantColumn from tenancy.tables import TenantColumn
from utilities.tables import ( from utilities.tables import (
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ActionsColumn, BaseTable, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn,
ToggleColumn, ToggleColumn,
) )
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -40,12 +41,11 @@ class ClusterTypeTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='virtualization:clustertype_list' url_name='virtualization:clustertype_list'
) )
actions = ButtonsColumn(ClusterType)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterType model = ClusterType
fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description')
# #
@ -63,12 +63,11 @@ class ClusterGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='virtualization:clustergroup_list' url_name='virtualization:clustergroup_list'
) )
actions = ButtonsColumn(ClusterGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ClusterGroup model = ClusterGroup
fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') fields = ('pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions')
default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description')
# #
@ -184,10 +183,9 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
bridge = tables.Column( bridge = tables.Column(
linkify=True linkify=True
) )
actions = ButtonsColumn( actions = ActionsColumn(
model=VMInterface, sequence=('edit', 'delete'),
buttons=('edit', 'delete'), extra_buttons=VMINTERFACE_BUTTONS
prepend_template=VMINTERFACE_BUTTONS
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -196,9 +194,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags', 'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
) )
default_columns = ( default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions',
)
row_attrs = { row_attrs = {
'data-name': lambda record: record.name, 'data-name': lambda record: record.name,
} }

View File

@ -1,9 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import Interface from dcim.models import Interface
from utilities.tables import ( from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
)
from .models import * from .models import *
__all__ = ( __all__ = (
@ -26,12 +24,11 @@ class WirelessLANGroupTable(BaseTable):
tags = TagColumn( tags = TagColumn(
url_name='wireless:wirelesslangroup_list' url_name='wireless:wirelesslangroup_list'
) )
actions = ButtonsColumn(WirelessLANGroup)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = WirelessLANGroup model = WirelessLANGroup
fields = ('pk', 'name', 'wirelesslan_count', 'description', 'slug', 'tags', 'actions') fields = ('pk', 'name', 'wirelesslan_count', 'description', 'slug', 'tags', 'actions')
default_columns = ('pk', 'name', 'wirelesslan_count', 'description', 'actions') default_columns = ('pk', 'name', 'wirelesslan_count', 'description')
class WirelessLANTable(BaseTable): class WirelessLANTable(BaseTable):