mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #8303 from netbox-community/7679-table-actions
Closes #7679: Object table actions menus
This commit is contained in:
@ -57,6 +57,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
|
||||
### Enhancements
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
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 *
|
||||
|
||||
|
||||
@ -88,12 +88,11 @@ class CircuitTypeTable(BaseTable):
|
||||
circuit_count = tables.Column(
|
||||
verbose_name='Circuits'
|
||||
)
|
||||
actions = ButtonsColumn(CircuitType)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
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')
|
||||
|
||||
|
||||
#
|
||||
|
@ -7,7 +7,7 @@ from dcim.models import (
|
||||
)
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||
ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn,
|
||||
MarkdownColumn, TagColumn, TemplateColumn, ToggleColumn,
|
||||
)
|
||||
from .template_code import *
|
||||
@ -94,7 +94,6 @@ class DeviceRoleTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:devicerole_list'
|
||||
)
|
||||
actions = ButtonsColumn(DeviceRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
@ -102,7 +101,7 @@ class DeviceRoleTable(BaseTable):
|
||||
'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags',
|
||||
'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(
|
||||
url_name='dcim:platform_list'
|
||||
)
|
||||
actions = ButtonsColumn(Platform)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
@ -136,7 +134,7 @@ class PlatformTable(BaseTable):
|
||||
'description', 'tags', 'actions',
|
||||
)
|
||||
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'),
|
||||
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):
|
||||
@ -336,7 +332,7 @@ class DeviceConsolePortTable(ConsolePortTable):
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'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 = {
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
@ -369,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):
|
||||
@ -381,7 +375,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
|
||||
'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 = {
|
||||
'class': get_cabletermination_row_class
|
||||
}
|
||||
@ -414,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):
|
||||
@ -428,7 +420,6 @@ class DevicePowerPortTable(PowerPortTable):
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
|
||||
'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class
|
||||
@ -464,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):
|
||||
@ -477,7 +466,7 @@ class DevicePowerOutletTable(PowerOutletTable):
|
||||
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
|
||||
)
|
||||
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 = {
|
||||
'class': get_cabletermination_row_class
|
||||
@ -557,10 +546,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):
|
||||
@ -575,7 +562,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
'cable', 'connection', 'actions',
|
||||
'cable', 'connection',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_interface_row_class,
|
||||
@ -620,10 +607,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):
|
||||
@ -634,7 +619,6 @@ class DeviceFrontPortTable(FrontPortTable):
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
|
||||
'actions',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class
|
||||
@ -669,10 +653,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):
|
||||
@ -682,7 +664,7 @@ class DeviceRearPortTable(RearPortTable):
|
||||
'cable', 'cable_color', 'link_peer', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer', 'actions',
|
||||
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class
|
||||
@ -720,10 +702,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):
|
||||
@ -731,9 +711,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'label', 'status', 'installed_device', 'description', 'tags', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'status', 'installed_device', 'description', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
|
||||
|
||||
|
||||
class ModuleBayTable(DeviceComponentTable):
|
||||
@ -758,16 +736,14 @@ 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):
|
||||
model = ModuleBay
|
||||
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):
|
||||
@ -812,10 +788,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
|
||||
@ -824,7 +797,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
||||
'description', 'discovered', 'tags', 'actions',
|
||||
)
|
||||
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(
|
||||
url_name='dcim:inventoryitemrole_list'
|
||||
)
|
||||
actions = ButtonsColumn(InventoryItemRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = InventoryItemRole
|
||||
fields = (
|
||||
'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')
|
||||
|
||||
|
||||
#
|
||||
|
@ -6,7 +6,7 @@ from dcim.models import (
|
||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
)
|
||||
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
|
||||
|
||||
@ -48,7 +48,6 @@ class ManufacturerTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:manufacturer_list'
|
||||
)
|
||||
actions = ButtonsColumn(Manufacturer)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
@ -57,7 +56,7 @@ class ManufacturerTable(BaseTable):
|
||||
'actions',
|
||||
)
|
||||
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):
|
||||
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
|
||||
|
@ -4,8 +4,8 @@ from django_tables2.utils import Accessor
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn,
|
||||
TagColumn, ToggleColumn, UtilizationColumn,
|
||||
BaseTable, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn,
|
||||
ToggleColumn, UtilizationColumn,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
@ -27,12 +27,11 @@ class RackRoleTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rackrole_list'
|
||||
)
|
||||
actions = ButtonsColumn(RackRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackRole
|
||||
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(
|
||||
url_name='dcim:rackreservation_list'
|
||||
)
|
||||
actions = ButtonsColumn(RackReservation)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
@ -129,6 +127,4 @@ class RackReservationTable(BaseTable):
|
||||
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
|
||||
|
@ -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',
|
||||
@ -32,12 +32,11 @@ class RegionTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:region_list'
|
||||
)
|
||||
actions = ButtonsColumn(Region)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
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(
|
||||
url_name='dcim:sitegroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(SiteGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SiteGroup
|
||||
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 = (
|
||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
|
||||
|
||||
@ -128,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):
|
||||
@ -139,4 +137,4 @@ class LocationTable(BaseTable):
|
||||
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
|
||||
|
@ -87,7 +87,7 @@ POWERFEED_CABLETERMINATION = """
|
||||
<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">
|
||||
<i class="mdi mdi-server"></i>
|
||||
</a>
|
||||
@ -99,8 +99,8 @@ LOCATION_ELEVATIONS = """
|
||||
|
||||
MODULAR_COMPONENT_TEMPLATE_BUTTONS = """
|
||||
{% load helpers %}
|
||||
{% if perms.dcim.add_invnetoryitemtemplate %}
|
||||
<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">
|
||||
{% if perms.dcim.add_inventoryitemtemplate %}
|
||||
<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>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,7 @@ import django_tables2 as tables
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn,
|
||||
ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn,
|
||||
MarkdownColumn, ToggleColumn,
|
||||
)
|
||||
from .models import *
|
||||
@ -152,12 +152,11 @@ class TagTable(BaseTable):
|
||||
linkify=True
|
||||
)
|
||||
color = ColorColumn()
|
||||
actions = ButtonsColumn(Tag)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tag
|
||||
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):
|
||||
@ -215,6 +214,7 @@ class ObjectChangeTable(BaseTable):
|
||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||
verbose_name='Request ID'
|
||||
)
|
||||
actions = ActionsColumn(sequence=())
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ObjectChange
|
||||
@ -233,9 +233,6 @@ class ObjectJournalTable(BaseTable):
|
||||
comments = tables.TemplateColumn(
|
||||
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=JournalEntry
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = JournalEntry
|
||||
@ -261,6 +258,5 @@ class JournalEntryTable(ObjectJournalTable):
|
||||
'comments', 'actions'
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind',
|
||||
'comments', 'actions'
|
||||
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 *
|
||||
|
||||
__all__ = (
|
||||
@ -58,9 +58,8 @@ class FHRPGroupAssignmentTable(BaseTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=FHRPGroupAssignment,
|
||||
buttons=('edit', 'delete', 'foo')
|
||||
actions = ActionsColumn(
|
||||
sequence=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -2,12 +2,11 @@ import django_tables2 as tables
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from ipam.models import *
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn,
|
||||
ToggleColumn, UtilizationColumn,
|
||||
BaseTable, BooleanColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn, UtilizationColumn,
|
||||
)
|
||||
from ipam.models import *
|
||||
|
||||
__all__ = (
|
||||
'AggregateTable',
|
||||
@ -89,12 +88,11 @@ class RIRTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='ipam:rir_list'
|
||||
)
|
||||
actions = ButtonsColumn(RIR)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
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'},
|
||||
verbose_name='Sites'
|
||||
)
|
||||
actions = ButtonsColumn(ASN)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ASN
|
||||
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(
|
||||
url_name='ipam:role_list'
|
||||
)
|
||||
actions = ButtonsColumn(Role)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
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()
|
||||
tenant = TenantColumn()
|
||||
actions = ButtonsColumn(
|
||||
model=IPAddress
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
|
@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import TenantColumn
|
||||
from utilities.tables import (
|
||||
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
|
||||
ActionsColumn, BaseTable, BooleanColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn,
|
||||
TemplateColumn, ToggleColumn,
|
||||
)
|
||||
from virtualization.models import VMInterface
|
||||
@ -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 %}
|
||||
<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(
|
||||
url_name='ipam:vlangroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=VLANGroup,
|
||||
prepend_template=VLANGROUP_ADD_VLAN
|
||||
actions = ActionsColumn(
|
||||
extra_buttons=VLANGROUP_BUTTONS
|
||||
)
|
||||
|
||||
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',
|
||||
'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(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(Interface, buttons=['edit'])
|
||||
actions = ActionsColumn(
|
||||
sequence=('edit',)
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
@ -165,7 +166,9 @@ class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
virtual_machine = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(VMInterface, buttons=['edit'])
|
||||
actions = ActionsColumn(
|
||||
sequence=('edit',)
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
|
@ -203,7 +203,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
||||
:param table: The Table instance to export
|
||||
: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:
|
||||
all_columns = [col_name for col_name, _ in table.selected_columns + table.available_columns]
|
||||
exclude_columns.update({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ContentTypeColumn, LinkedCountColumn, linkify_phone, MarkdownColumn, MPTTColumn,
|
||||
ActionsColumn, BaseTable, ContentTypeColumn, LinkedCountColumn, linkify_phone, MarkdownColumn, MPTTColumn,
|
||||
TagColumn, ToggleColumn,
|
||||
)
|
||||
from .models import *
|
||||
@ -59,12 +59,11 @@ class TenantGroupTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='tenancy:tenantgroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(TenantGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TenantGroup
|
||||
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):
|
||||
@ -103,12 +102,11 @@ class ContactGroupTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='tenancy:contactgroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(ContactGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactGroup
|
||||
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):
|
||||
@ -116,12 +114,11 @@ class ContactRoleTable(BaseTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(ContactRole)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactRole
|
||||
fields = ('pk', 'name', 'description', 'slug', 'actions')
|
||||
default_columns = ('pk', 'name', 'description', 'actions')
|
||||
default_columns = ('pk', 'name', 'description')
|
||||
|
||||
|
||||
class ContactTable(BaseTable):
|
||||
@ -164,12 +161,11 @@ class ContactAssignmentTable(BaseTable):
|
||||
role = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
model=ContactAssignment,
|
||||
buttons=('edit', 'delete')
|
||||
actions = ActionsColumn(
|
||||
sequence=('edit', 'delete')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ContactAssignment
|
||||
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')
|
||||
|
30
netbox/utilities/tables/__init__.py
Normal file
30
netbox/utilities/tables/__init__.py
Normal 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}"
|
@ -1,149 +1,36 @@
|
||||
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.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.template import Context, Template
|
||||
from django.urls import reverse
|
||||
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 extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField, CustomLink
|
||||
from .utils import content_type_identifier, content_type_name
|
||||
from .paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.utils import content_type_identifier, content_type_name
|
||||
|
||||
__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):
|
||||
"""
|
||||
@ -205,59 +92,78 @@ class TemplateColumn(tables.TemplateColumn):
|
||||
return ret
|
||||
|
||||
|
||||
class ButtonsColumn(tables.TemplateColumn):
|
||||
"""
|
||||
Render edit, delete, and changelog buttons for an object.
|
||||
@dataclass
|
||||
class ActionsItem:
|
||||
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'}}
|
||||
# Note that braces are escaped to allow for string formatting prior to template rendering
|
||||
template_code = """
|
||||
{{% if "changelog" in buttons %}}
|
||||
<a href="{{% url '{app_label}:{model_name}_changelog' pk=record.pk %}}" class="btn btn-outline-dark btn-sm" title="Change log">
|
||||
<i class="mdi mdi-history"></i>
|
||||
</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 %}}
|
||||
"""
|
||||
empty_values = ()
|
||||
actions = {
|
||||
'edit': ActionsItem('Edit', 'pencil', 'change'),
|
||||
'delete': ActionsItem('Delete', 'trash-can-outline', 'delete'),
|
||||
'changelog': ActionsItem('Changelog', 'history'),
|
||||
}
|
||||
|
||||
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
|
||||
def __init__(self, *args, sequence=('edit', 'delete', 'changelog'), extra_buttons='', **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
template_code = self.template_code.format(
|
||||
app_label=model._meta.app_label,
|
||||
model_name=model._meta.model_name,
|
||||
buttons=buttons
|
||||
)
|
||||
self.extra_buttons = extra_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,
|
||||
})
|
||||
# Determine which actions to enable
|
||||
self.actions = {
|
||||
name: self.actions[name] for name in sequence
|
||||
}
|
||||
|
||||
def header(self):
|
||||
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):
|
||||
"""
|
||||
@ -509,34 +415,3 @@ class MarkdownColumn(tables.TemplateColumn):
|
||||
|
||||
def value(self, 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}"
|
138
netbox/utilities/tables/tables.py
Normal file
138
netbox/utilities/tables/tables.py
Normal 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
|
@ -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)
|
||||
|
@ -1,8 +1,9 @@
|
||||
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
|
||||
@ -40,12 +41,11 @@ class ClusterTypeTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='virtualization:clustertype_list'
|
||||
)
|
||||
actions = ButtonsColumn(ClusterType)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ClusterType
|
||||
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(
|
||||
url_name='virtualization:clustergroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(ClusterGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ClusterGroup
|
||||
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(
|
||||
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):
|
||||
@ -196,9 +194,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
||||
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
||||
row_attrs = {
|
||||
'data-name': lambda record: record.name,
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from dcim.models import Interface
|
||||
from utilities.tables import (
|
||||
BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
|
||||
)
|
||||
from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
@ -26,12 +24,11 @@ class WirelessLANGroupTable(BaseTable):
|
||||
tags = TagColumn(
|
||||
url_name='wireless:wirelesslangroup_list'
|
||||
)
|
||||
actions = ButtonsColumn(WirelessLANGroup)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = WirelessLANGroup
|
||||
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):
|
||||
|
Reference in New Issue
Block a user