diff --git a/docs/development/user-preferences.md b/docs/development/user-preferences.md index f9ca05188..80088186c 100644 --- a/docs/development/user-preferences.md +++ b/docs/development/user-preferences.md @@ -8,3 +8,4 @@ The `users.UserConfig` model holds individual preferences for each user in the f | ---- | ----------- | | extras.configcontext.format | Preferred format when rendering config context data (JSON or YAML) | | pagination.per_page | The number of items to display per page of a paginated table | +| tables.${table_name}.columns | The ordered list of columns to display when viewing the table | diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index a425b3ace..0878279ec 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -27,18 +27,15 @@ STATUS_LABEL = """ class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + circuit_count = tables.Column( + accessor=Accessor('count_circuits'), + verbose_name='Circuits' + ) class Meta(BaseTable.Meta): model = Provider - fields = ('pk', 'name', 'asn', 'account',) - - -class ProviderDetailTable(ProviderTable): - circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits') - - class Meta(ProviderTable.Meta): - model = Provider - fields = ('pk', 'name', 'asn', 'account', 'circuit_count') + fields = ('pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count') + default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') # @@ -58,6 +55,7 @@ class CircuitTypeTable(BaseTable): class Meta(BaseTable.Meta): model = CircuitType fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') # @@ -79,4 +77,8 @@ class CircuitTable(BaseTable): class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') + fields = ( + 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', + 'description', + ) + default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description') diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index b092e1855..709d2a726 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -28,7 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView): queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet filterset_form = forms.ProviderFilterForm - table = tables.ProviderDetailTable + table = tables.ProviderTable class ProviderView(PermissionRequiredMixin, View): @@ -87,7 +87,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_provider' - queryset = Provider.objects.all() + queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm @@ -96,7 +96,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_provider' - queryset = Provider.objects.all() + queryset = Provider.objects.annotate(count_circuits=Count('circuits')) filterset = filters.ProviderFilterSet table = tables.ProviderTable default_return_url = 'circuits:provider_list' diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 0e5e9dc7a..5ce96841f 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -205,9 +205,13 @@ def get_component_template_actions(model_name): class RegionTable(BaseTable): pk = ToggleColumn() - name = tables.TemplateColumn(template_code=MPTT_LINK, orderable=False) - site_count = tables.Column(verbose_name='Sites') - slug = tables.Column(verbose_name='Slug') + name = tables.TemplateColumn( + template_code=MPTT_LINK, + orderable=False + ) + site_count = tables.Column( + verbose_name='Sites' + ) actions = tables.TemplateColumn( template_code=REGION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -216,7 +220,8 @@ class RegionTable(BaseTable): class Meta(BaseTable.Meta): model = Region - fields = ('pk', 'name', 'site_count', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') + default_columns = ('pk', 'name', 'site_count', 'description', 'actions') # @@ -225,14 +230,27 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_name',)) - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - region = tables.TemplateColumn(template_code=SITE_REGION_LINK) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + name = tables.LinkColumn( + order_by=('_name',) + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + region = tables.TemplateColumn( + template_code=SITE_REGION_LINK + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) class Meta(BaseTable.Meta): model = Site - fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description') + fields = ( + 'pk', 'name', 'slug', 'status', 'facility', 'region', 'tenant', 'asn', 'time_zone', 'description', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', + ) + default_columns = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'description') # @@ -253,7 +271,6 @@ class RackGroupTable(BaseTable): rack_count = tables.Column( verbose_name='Racks' ) - slug = tables.Column() actions = tables.TemplateColumn( template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -263,6 +280,7 @@ class RackGroupTable(BaseTable): class Meta(BaseTable.Meta): model = RackGroup fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions') # @@ -282,6 +300,7 @@ class RackRoleTable(BaseTable): class Meta(BaseTable.Meta): model = RackRole fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') # @@ -290,17 +309,34 @@ class RackRoleTable(BaseTable): class RackTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(order_by=('_name',)) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(RACK_ROLE) - u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') + name = tables.LinkColumn( + order_by=('_name',) + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=RACK_ROLE + ) + u_height = tables.TemplateColumn( + template_code="{{ record.u_height }}U", + verbose_name='Height' + ) class Meta(BaseTable.Meta): model = Rack - fields = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') + fields = ( + 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'width', 'u_height', + ) + default_columns = ('pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height') class RackDetailTable(RackTable): @@ -321,6 +357,10 @@ class RackDetailTable(RackTable): class Meta(RackTable.Meta): fields = ( + 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', + 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', + ) + default_columns = ( 'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', ) @@ -364,6 +404,9 @@ class RackReservationTable(BaseTable): fields = ( 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions', ) + default_columns = ( + 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions', + ) # @@ -416,9 +459,12 @@ class DeviceTypeTable(BaseTable): class Meta(BaseTable.Meta): model = DeviceType fields = ( - 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'instance_count', ) + default_columns = ( + 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', + ) # @@ -427,7 +473,9 @@ class DeviceTypeTable(BaseTable): class ConsolePortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -441,7 +489,10 @@ class ConsolePortTemplateTable(BaseTable): class ConsolePortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = ConsolePort @@ -451,7 +502,9 @@ class ConsolePortImportTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('consoleserverporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -465,7 +518,10 @@ class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = ConsoleServerPort @@ -475,7 +531,9 @@ class ConsoleServerPortImportTable(BaseTable): class PowerPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('powerporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -489,7 +547,10 @@ class PowerPortTemplateTable(BaseTable): class PowerPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = PowerPort @@ -499,7 +560,9 @@ class PowerPortImportTable(BaseTable): class PowerOutletTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('poweroutlettemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -513,7 +576,10 @@ class PowerOutletTemplateTable(BaseTable): class PowerOutletImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = PowerOutlet @@ -523,7 +589,9 @@ class PowerOutletImportTable(BaseTable): class InterfaceTemplateTable(BaseTable): pk = ToggleColumn() - mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") + mgmt_only = tables.TemplateColumn( + template_code="{% if value %}OOB Management{% endif %}" + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('interfacetemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -537,18 +605,30 @@ class InterfaceTemplateTable(BaseTable): class InterfaceImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') - virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) + virtual_machine = tables.LinkColumn( + viewname='virtualization:virtualmachine', + args=[Accessor('virtual_machine.pk')], + verbose_name='Virtual Machine' + ) class Meta(BaseTable.Meta): model = Interface - fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode') + fields = ( + 'device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', + 'mgmt_only', 'mode', + ) empty_text = False class FrontPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) rear_port_position = tables.Column( verbose_name='Position' ) @@ -565,7 +645,10 @@ class FrontPortTemplateTable(BaseTable): class FrontPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = FrontPort @@ -575,7 +658,9 @@ class FrontPortImportTable(BaseTable): class RearPortTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('rearporttemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -589,7 +674,10 @@ class RearPortTemplateTable(BaseTable): class RearPortImportTable(BaseTable): - device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device') + device = tables.LinkColumn( + viewname='dcim:device', + args=[Accessor('device.pk')] + ) class Meta(BaseTable.Meta): model = RearPort @@ -599,7 +687,9 @@ class RearPortImportTable(BaseTable): class DeviceBayTemplateTable(BaseTable): pk = ToggleColumn() - name = tables.Column(order_by=('_name',)) + name = tables.Column( + order_by=('_name',) + ) actions = tables.TemplateColumn( template_code=get_component_template_actions('devicebaytemplate'), attrs={'td': {'class': 'text-right noprint'}}, @@ -630,8 +720,10 @@ class DeviceRoleTable(BaseTable): orderable=False, verbose_name='VMs' ) - color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Label') - slug = tables.Column(verbose_name='Slug') + color = tables.TemplateColumn( + template_code=COLOR_LABEL, + verbose_name='Label' + ) actions = tables.TemplateColumn( template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -641,6 +733,7 @@ class DeviceRoleTable(BaseTable): class Meta(BaseTable.Meta): model = DeviceRole fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') # @@ -670,7 +763,11 @@ class PlatformTable(BaseTable): class Meta(BaseTable.Meta): model = Platform fields = ( - 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'description', 'actions', + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', + 'description', 'actions', + ) + default_columns = ( + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', ) @@ -684,40 +781,96 @@ class DeviceTable(BaseTable): order_by=('_name',), template_code=DEVICE_LINK ) - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) - device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role') + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + args=[Accessor('rack.pk')] + ) + device_role = tables.TemplateColumn( + template_code=DEVICE_ROLE, + verbose_name='Role' + ) device_type = tables.LinkColumn( - 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', + viewname='dcim:devicetype', + args=[Accessor('device_type.pk')], + verbose_name='Type', text=lambda record: record.device_type.display_name ) + primary_ip = tables.TemplateColumn( + template_code=DEVICE_PRIMARY_IP, + orderable=False, + verbose_name='IP Address' + ) + primary_ip4 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip4.pk')], + verbose_name='IPv4 Address' + ) + primary_ip6 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip6.pk')], + verbose_name='IPv6 Address' + ) + cluster = tables.LinkColumn( + viewname='virtualization:cluster', + args=[Accessor('cluster.pk')] + ) + virtual_chassis = tables.LinkColumn( + viewname='dcim:virtualchassis', + args=[Accessor('virtual_chassis.pk')] + ) + vc_position = tables.Column( + verbose_name='VC Position' + ) + vc_priority = tables.Column( + verbose_name='VC Priority' + ) class Meta(BaseTable.Meta): model = Device - fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type') - - -class DeviceDetailTable(DeviceTable): - primary_ip = tables.TemplateColumn( - orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP - ) - - class Meta(DeviceTable.Meta): - model = Device - fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') + fields = ( + 'pk', 'name', 'status', 'tenant', 'device_role', 'device_type', 'platform', 'serial', 'asset_tag', 'site', + 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', + ) + default_columns = ( + 'pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip', + ) class DeviceImportTable(BaseTable): - name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') - status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') - position = tables.Column(verbose_name='Position') - device_role = tables.Column(verbose_name='Role') - device_type = tables.Column(verbose_name='Type') + name = tables.TemplateColumn( + template_code=DEVICE_LINK + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + rack = tables.LinkColumn( + viewname='dcim:rack', + args=[Accessor('rack.pk')] + ) + device_role = tables.Column( + verbose_name='Role' + ) + device_type = tables.Column( + verbose_name='Type' + ) class Meta(BaseTable.Meta): model = Device @@ -893,23 +1046,23 @@ class CableTable(BaseTable): template_code=CABLE_TERMINATION_PARENT, accessor=Accessor('termination_a'), orderable=False, - verbose_name='Termination A' + verbose_name='Side A' ) termination_a = tables.LinkColumn( accessor=Accessor('termination_a'), orderable=False, - verbose_name='' + verbose_name='Termination A' ) termination_b_parent = tables.TemplateColumn( template_code=CABLE_TERMINATION_PARENT, accessor=Accessor('termination_b'), orderable=False, - verbose_name='Termination B' + verbose_name='Side B' ) termination_b = tables.LinkColumn( accessor=Accessor('termination_b'), orderable=False, - verbose_name='' + verbose_name='Termination B' ) status = tables.TemplateColumn( template_code=STATUS_LABEL @@ -926,6 +1079,10 @@ class CableTable(BaseTable): 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'status', 'type', 'color', 'length', ) + default_columns = ( + 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', + 'status', 'type', + ) # @@ -993,10 +1150,6 @@ class InterfaceConnectionTable(BaseTable): args=[Accessor('pk')], verbose_name='Interface A' ) - description_a = tables.Column( - accessor=Accessor('description'), - verbose_name='Description' - ) device_b = tables.LinkColumn( viewname='dcim:device', accessor=Accessor('_connected_interface.device'), @@ -1009,15 +1162,11 @@ class InterfaceConnectionTable(BaseTable): args=[Accessor('_connected_interface.pk')], verbose_name='Interface B' ) - description_b = tables.Column( - accessor=Accessor('_connected_interface.description'), - verbose_name='Description' - ) class Meta(BaseTable.Meta): model = Interface fields = ( - 'device_a', 'interface_a', 'description_a', 'device_b', 'interface_b', 'description_b', 'connection_status', + 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status', ) @@ -1027,12 +1176,21 @@ class InterfaceConnectionTable(BaseTable): class InventoryItemTable(BaseTable): pk = ToggleColumn() - device = tables.LinkColumn('dcim:device_inventory', args=[Accessor('device.pk')]) - manufacturer = tables.Column(accessor=Accessor('manufacturer.name'), verbose_name='Manufacturer') + device = tables.LinkColumn( + viewname='dcim:device_inventory', + args=[Accessor('device.pk')] + ) + manufacturer = tables.Column( + accessor=Accessor('manufacturer.name') + ) + discovered = BooleanColumn() class Meta(BaseTable.Meta): model = InventoryItem - fields = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description') + fields = ( + 'pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'discovered' + ) + default_columns = ('pk', 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag') # @@ -1052,6 +1210,7 @@ class VirtualChassisTable(BaseTable): class Meta(BaseTable.Meta): model = VirtualChassis fields = ('pk', 'name', 'domain', 'member_count') + default_columns = ('pk', 'name', 'domain', 'member_count') # @@ -1073,6 +1232,7 @@ class PowerPanelTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPanel fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') + default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') # @@ -1096,7 +1256,19 @@ class PowerFeedTable(BaseTable): type = tables.TemplateColumn( template_code=TYPE_LABEL ) + max_utilization = tables.TemplateColumn( + template_code="{{ value }}%" + ) + available_power = tables.Column( + verbose_name='Available power (VA)' + ) class Meta(BaseTable.Meta): model = PowerFeed - fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase') + fields = ( + 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', + 'max_utilization', 'available_power', + ) + default_columns = ( + 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', + ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 66b59add4..cd1b4edf4 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1095,7 +1095,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView): ) filterset = filters.DeviceFilterSet filterset_form = forms.DeviceFilterForm - table = tables.DeviceDetailTable + table = tables.DeviceTable template_name = 'dcim/device_list.html' @@ -2278,19 +2278,15 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): csv_data = [ # Headers ','.join([ - 'device_a', 'interface_a', 'interface_a_description', - 'device_b', 'interface_b', 'interface_b_description', - 'connection_status' + 'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status' ]) ] for obj in self.queryset: csv = csv_format([ obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj.connected_endpoint.name if obj.connected_endpoint else None, - obj.connected_endpoint.description if obj.connected_endpoint else None, obj.device.identifier, obj.name, - obj.description, obj.get_connection_status_display(), ]) csv_data.append(csv) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index b145824c6..7a78d4b19 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -104,7 +104,11 @@ class ConfigContextTable(BaseTable): class Meta(BaseTable.Meta): model = ConfigContext - fields = ('pk', 'name', 'weight', 'is_active', 'description') + fields = ( + 'pk', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms', + 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', + ) + default_columns = ('pk', 'name', 'weight', 'is_active', 'description') class ObjectChangeTable(BaseTable): diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 19735b81c..56729f9db 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -190,12 +190,20 @@ TENANT_LINK = """ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - rd = tables.Column(verbose_name='RD') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + rd = tables.Column( + verbose_name='RD' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + enforce_unique = BooleanColumn( + verbose_name='Unique' + ) class Meta(BaseTable.Meta): model = VRF - fields = ('pk', 'name', 'rd', 'tenant', 'description') + fields = ('pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description') + default_columns = ('pk', 'name', 'rd', 'tenant', 'description') # @@ -204,14 +212,23 @@ class VRFTable(BaseTable): class RIRTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') - is_private = BooleanColumn(verbose_name='Private') - aggregate_count = tables.Column(verbose_name='Aggregates') - actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') + name = tables.LinkColumn() + is_private = BooleanColumn( + verbose_name='Private' + ) + aggregate_count = tables.Column( + verbose_name='Aggregates' + ) + actions = tables.TemplateColumn( + template_code=RIR_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = RIR - fields = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions') + default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') class RIRDetailTable(RIRTable): @@ -247,6 +264,10 @@ class RIRDetailTable(RIRTable): class Meta(RIRTable.Meta): fields = ( + 'pk', 'name', 'slug', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', + 'stats_deprecated', 'stats_available', 'utilization', 'actions', + ) + default_columns = ( 'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'stats_deprecated', 'stats_available', 'utilization', 'actions', ) @@ -258,8 +279,13 @@ class RIRDetailTable(RIRTable): class AggregateTable(BaseTable): pk = ToggleColumn() - prefix = tables.LinkColumn(verbose_name='Aggregate') - date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') + prefix = tables.LinkColumn( + verbose_name='Aggregate' + ) + date_added = tables.DateColumn( + format="Y-m-d", + verbose_name='Added' + ) class Meta(BaseTable.Meta): model = Aggregate @@ -267,8 +293,13 @@ class AggregateTable(BaseTable): class AggregateDetailTable(AggregateTable): - child_count = tables.Column(verbose_name='Prefixes') - utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') + child_count = tables.Column( + verbose_name='Prefixes' + ) + utilization = tables.TemplateColumn( + template_code=UTILIZATION_GRAPH, + orderable=False + ) class Meta(AggregateTable.Meta): fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description') @@ -300,7 +331,8 @@ class RoleTable(BaseTable): class Meta(BaseTable.Meta): model = Role - fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'slug', 'weight', 'actions') + fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions') + default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') # @@ -309,28 +341,61 @@ class RoleTable(BaseTable): class PrefixTable(BaseTable): pk = ToggleColumn() - prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) - status = tables.TemplateColumn(STATUS_LABEL) - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - tenant = tables.TemplateColumn(template_code=TENANT_LINK) - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') - role = tables.TemplateColumn(PREFIX_ROLE_LINK) + prefix = tables.TemplateColumn( + template_code=PREFIX_LINK, + attrs={'th': {'style': 'padding-left: 17px'}} + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + vlan = tables.LinkColumn( + viewname='ipam:vlan', + args=[Accessor('vlan.pk')], + verbose_name='VLAN' + ) + role = tables.TemplateColumn( + template_code=PREFIX_ROLE_LINK + ) + is_pool = BooleanColumn( + verbose_name='Pool' + ) class Meta(BaseTable.Meta): model = Prefix - fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') + fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description') + default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') row_attrs = { 'class': lambda record: 'success' if not record.pk else '', } class PrefixDetailTable(PrefixTable): - utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + utilization = tables.TemplateColumn( + template_code=UTILIZATION_GRAPH, + orderable=False + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) class Meta(PrefixTable.Meta): - fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description') + fields = ( + 'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description', + ) + default_columns = ( + 'pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', + ) # @@ -339,12 +404,27 @@ class PrefixDetailTable(PrefixTable): class IPAddressTable(BaseTable): pk = ToggleColumn() - address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - status = tables.TemplateColumn(STATUS_LABEL) - tenant = tables.TemplateColumn(template_code=TENANT_LINK) - parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) - interface = tables.Column(orderable=False) + address = tables.TemplateColumn( + template_code=IPADDRESS_LINK, + verbose_name='IP Address' + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) + parent = tables.TemplateColumn( + template_code=IPADDRESS_PARENT, + orderable=False + ) + interface = tables.Column( + orderable=False + ) class Meta(BaseTable.Meta): model = IPAddress @@ -358,22 +438,40 @@ class IPAddressTable(BaseTable): class IPAddressDetailTable(IPAddressTable): nat_inside = tables.LinkColumn( - 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' + viewname='ipam:ipaddress', + args=[Accessor('nat_inside.pk')], + orderable=False, + verbose_name='NAT (Inside)' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT ) - tenant = tables.TemplateColumn(template_code=COL_TENANT) class Meta(IPAddressTable.Meta): fields = ( 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name', 'description', ) + default_columns = ( + 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description', + ) class IPAddressAssignTable(BaseTable): - address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address') - status = tables.TemplateColumn(STATUS_LABEL) - parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False) - interface = tables.Column(orderable=False) + address = tables.TemplateColumn( + template_code=IPADDRESS_ASSIGN_LINK, + verbose_name='IP Address' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + parent = tables.TemplateColumn( + template_code=IPADDRESS_PARENT, + orderable=False + ) + interface = tables.Column( + orderable=False + ) class Meta(BaseTable.Meta): model = IPAddress @@ -385,10 +483,19 @@ class InterfaceIPAddressTable(BaseTable): """ List IP addresses assigned to a specific Interface. """ - address = tables.LinkColumn(verbose_name='IP Address') - vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') - status = tables.TemplateColumn(STATUS_LABEL) - tenant = tables.TemplateColumn(template_code=TENANT_LINK) + address = tables.LinkColumn( + verbose_name='IP Address' + ) + vrf = tables.TemplateColumn( + template_code=VRF_LINK, + verbose_name='VRF' + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + tenant = tables.TemplateColumn( + template_code=TENANT_LINK + ) class Meta(BaseTable.Meta): model = IPAddress @@ -401,16 +508,24 @@ class InterfaceIPAddressTable(BaseTable): class VLANGroupTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - vlan_count = tables.Column(verbose_name='VLANs') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='') + name = tables.LinkColumn() + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + vlan_count = tables.Column( + verbose_name='VLANs' + ) + actions = tables.TemplateColumn( + template_code=VLANGROUP_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = VLANGroup fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions') + default_columns = ('pk', 'name', 'site', 'vlan_count', 'description', 'actions') # @@ -419,12 +534,27 @@ class VLANGroupTable(BaseTable): class VLANTable(BaseTable): pk = ToggleColumn() - vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(VLAN_ROLE_LINK) + vid = tables.TemplateColumn( + template_code=VLAN_LINK, + verbose_name='ID' + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + group = tables.LinkColumn( + viewname='ipam:vlangroup_vlans', + args=[Accessor('group.pk')] + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) class Meta(BaseTable.Meta): model = VLAN @@ -435,16 +565,26 @@ class VLANTable(BaseTable): class VLANDetailTable(VLANTable): - prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes') - tenant = tables.TemplateColumn(template_code=COL_TENANT) + prefixes = tables.TemplateColumn( + template_code=VLAN_PREFIXES, + orderable=False, + verbose_name='Prefixes' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) class Meta(VLANTable.Meta): fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') class VLANMemberTable(BaseTable): - parent = tables.LinkColumn(order_by=['device', 'virtual_machine']) - name = tables.LinkColumn(verbose_name='Interface') + parent = tables.LinkColumn( + order_by=['device', 'virtual_machine'] + ) + name = tables.LinkColumn( + verbose_name='Interface' + ) untagged = tables.TemplateColumn( template_code=VLAN_MEMBER_UNTAGGED, orderable=False @@ -464,13 +604,29 @@ class InterfaceVLANTable(BaseTable): """ List VLANs assigned to a specific Interface. """ - vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') + vid = tables.LinkColumn( + viewname='ipam:vlan', + args=[Accessor('pk')], + verbose_name='ID' + ) tagged = BooleanColumn() - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') - tenant = tables.TemplateColumn(template_code=COL_TENANT) - status = tables.TemplateColumn(STATUS_LABEL) - role = tables.TemplateColumn(VLAN_ROLE_LINK) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + group = tables.Column( + accessor=Accessor('group.name'), + verbose_name='Group' + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + status = tables.TemplateColumn( + template_code=STATUS_LABEL + ) + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) class Meta(BaseTable.Meta): model = VLAN @@ -494,4 +650,5 @@ class ServiceTable(BaseTable): class Meta(BaseTable.Meta): model = Service - fields = ('pk', 'name', 'parent', 'protocol', 'port', 'description') + fields = ('pk', 'name', 'parent', 'protocol', 'port', 'ipaddresses', 'description') + default_columns = ('pk', 'name', 'parent', 'protocol', 'port', 'description') diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 25c32338b..37a516409 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -20,7 +20,7 @@ from dcim.models import ( Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis ) from dcim.tables import ( - CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable, + CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable, ) from extras.models import ObjectChange, ReportResult @@ -44,7 +44,7 @@ SEARCH_TYPES = OrderedDict(( # Circuits ('provider', { 'permission': 'circuits.view_provider', - 'queryset': Provider.objects.all(), + 'queryset': Provider.objects.annotate(count_circuits=Count('circuits')), 'filterset': ProviderFilterSet, 'table': ProviderTable, 'url': 'circuits:provider_list', @@ -93,7 +93,7 @@ SEARCH_TYPES = OrderedDict(( 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', ), 'filterset': DeviceFilterSet, - 'table': DeviceDetailTable, + 'table': DeviceTable, 'url': 'dcim:device_list', }), ('virtualchassis', { diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index ff520a23f..06d4a742a 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -448,4 +448,33 @@ $(document).ready(function() { $('a.image-preview').on('mouseout', function() { $('#image-preview-window').fadeOut('fast'); }); + + // Rearrange options within a + + + + + + + diff --git a/netbox/templates/utilities/obj_list.html b/netbox/templates/utilities/obj_list.html index f5482baf0..4cfa8b1ce 100644 --- a/netbox/templates/utilities/obj_list.html +++ b/netbox/templates/utilities/obj_list.html @@ -5,6 +5,9 @@ {% block content %}
{% block buttons %}{% endblock %} + {% if table_config_form %} + + {% endif %} {% if permissions.add and 'add' in action_buttons %} {% add_button content_type.model_class|url_name:"add" %} {% endif %} @@ -68,6 +71,9 @@ {% endwith %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+ {% if table_config_form %} + {% include 'inc/table_config_form.html' %} + {% endif %}
{% if filter_form %}
diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0eca7de71..72fb98e80 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -44,7 +44,6 @@ class TenantGroupTable(BaseTable): tenant_count = tables.Column( verbose_name='Tenants' ) - slug = tables.Column() actions = tables.TemplateColumn( template_code=TENANTGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -54,6 +53,7 @@ class TenantGroupTable(BaseTable): class Meta(BaseTable.Meta): model = TenantGroup fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions') + default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') # @@ -66,4 +66,5 @@ class TenantTable(BaseTable): class Meta(BaseTable.Meta): model = Tenant - fields = ('pk', 'name', 'group', 'description') + fields = ('pk', 'name', 'slug', 'group', 'description') + default_columns = ('pk', 'name', 'group', 'description') diff --git a/netbox/users/models.py b/netbox/users/models.py index 02356696f..ea5762232 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -108,7 +108,7 @@ class UserConfig(models.Model): userconfig.clear('foo.bar.baz') - A KeyError is raised in the event any key along the path does not exist. + Invalid keys will be ignored silently. :param path: Dotted path to the configuration key. For example, 'foo.bar' deletes self.data['foo']['bar']. :param commit: If true, the UserConfig instance will be saved once the new value has been applied. @@ -117,11 +117,13 @@ class UserConfig(models.Model): keys = path.split('.') for key in keys[:-1]: - if key in d and type(d[key]) is dict: + if key not in d: + break + if type(d[key]) is dict: d = d[key] key = keys[-1] - del(d[key]) + d.pop(key, None) # Avoid a KeyError on invalid keys if commit: self.save() diff --git a/netbox/users/tests/__init__.py b/netbox/users/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/users/tests/test_models.py b/netbox/users/tests/test_models.py index 0157d8fdd..8047796c4 100644 --- a/netbox/users/tests/test_models.py +++ b/netbox/users/tests/test_models.py @@ -104,6 +104,5 @@ class UserConfigTest(TestCase): self.assertTrue('foo' not in userconfig.data['b']) self.assertEqual(userconfig.data['b']['bar'], 102) - # Clear an invalid value - with self.assertRaises(KeyError): - userconfig.clear('invalid') + # Clear a non-existing value; should fail silently + userconfig.clear('invalid') diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d95c86527..c1d925999 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -665,7 +665,10 @@ class BootstrapMixin(forms.BaseForm): super().__init__(*args, **kwargs) exempt_widgets = [ - forms.CheckboxInput, forms.ClearableFileInput, forms.FileInput, forms.RadioSelect + forms.CheckboxInput, + forms.ClearableFileInput, + forms.FileInput, + forms.RadioSelect ] for field_name, field in self.fields.items(): @@ -752,3 +755,23 @@ class ImportForm(BootstrapMixin, forms.Form): raise forms.ValidationError({ 'data': "Invalid YAML data: {}".format(err) }) + + +class TableConfigForm(BootstrapMixin, forms.Form): + """ + Form for configuring user's table preferences. + """ + columns = forms.MultipleChoiceField( + choices=[], + widget=forms.SelectMultiple( + attrs={'size': 10} + ), + help_text="Use the buttons below to arrange columns in the desired order, then select all columns to display." + ) + + def __init__(self, table, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Initialize columns field based on table attributes + self.fields['columns'].choices = table.configurable_columns + self.fields['columns'].initial = table.visible_columns diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 9e91aebd2..e5433e3f8 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -1,4 +1,7 @@ import django_tables2 as tables +from django.core.exceptions import FieldDoesNotExist +from django.db.models import ForeignKey +from django_tables2.data import TableQuerysetData from django.utils.safestring import mark_safe @@ -6,17 +9,73 @@ class BaseTable(tables.Table): """ Default table for object lists """ - def __init__(self, *args, **kwargs): + class Meta: + attrs = { + 'class': 'table table-hover table-headings', + } + + def __init__(self, *args, columns=None, **kwargs): super().__init__(*args, **kwargs) # Set default empty_text if none was provided if self.empty_text is None: self.empty_text = 'No {} found'.format(self._meta.model._meta.verbose_name_plural) - class Meta: - attrs = { - 'class': 'table table-hover table-headings', - } + # 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 + if columns is not None: + pk = self.base_columns.pop('pk', None) + actions = self.base_columns.pop('actions', None) + + for name, column in self.base_columns.items(): + if name in columns: + self.columns.show(name) + else: + self.columns.hide(name) + self.sequence = columns + + # Always include PK and actions column, if defined on the table + if pk: + self.base_columns['pk'] = pk + self.sequence.insert(0, 'pk') + if actions: + self.base_columns['actions'] = actions + self.sequence.append('actions') + + # Dynamically update the table's QuerySet to ensure related fields are pre-fetched + if isinstance(self.data, TableQuerysetData): + model = getattr(self.Meta, 'model') + prefetch_fields = [] + for column in self.columns: + if column.visible: + field_path = column.accessor.split('.') + try: + model_field = model._meta.get_field(field_path[0]) + if isinstance(model_field, ForeignKey): + prefetch_fields.append('__'.join(field_path)) + except FieldDoesNotExist: + pass + self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields) + + @property + def configurable_columns(self): + selected_columns = [ + (name, self.columns[name].verbose_name) for name in self.sequence if name not in ['pk', 'actions'] + ] + available_columns = [ + (name, column.verbose_name) for name, column in self.columns.items() if name not in self.sequence and name not in ['pk', 'actions'] + ] + return selected_columns + available_columns + + @property + def visible_columns(self): + return [name for name in self.sequence if self.columns[name].visible] class ToggleColumn(tables.CheckBoxColumn): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 294acb1d1..eca124a4a 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -24,7 +24,7 @@ from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset from utilities.exceptions import AbortTransaction -from utilities.forms import BootstrapMixin, CSVDataField +from utilities.forms import BootstrapMixin, CSVDataField, TableConfigForm from utilities.utils import csv_format, prepare_cloned_fields from .error_handlers import handle_protectederror from .forms import ConfirmationForm, ImportForm @@ -164,7 +164,8 @@ class ObjectListView(View): permissions[action] = request.user.has_perm(perm_name) # Construct the table based on the user's permissions - table = self.table(self.queryset) + columns = request.user.config.get(f"tables.{self.table.__name__}.columns") + table = self.table(self.queryset, columns=columns) if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): table.columns.show('pk') @@ -180,12 +181,29 @@ class ObjectListView(View): 'table': table, 'permissions': permissions, 'action_buttons': self.action_buttons, + 'table_config_form': TableConfigForm(table=table), 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, } context.update(self.extra_context()) return render(request, self.template_name, context) + def post(self, request): + + # Update the user's table configuration + table = self.table(self.queryset) + form = TableConfigForm(table=table, data=request.POST) + preference_name = f"tables.{self.table.__name__}.columns" + + if form.is_valid(): + if 'set' in request.POST: + request.user.config.set(preference_name, form.cleaned_data['columns'], commit=True) + elif 'clear' in request.POST: + request.user.config.clear(preference_name, commit=True) + messages.success(request, "Your preferences have been updated.") + + return redirect(request.get_full_path()) + def alter_queryset(self, request): # .all() is necessary to avoid caching queries return self.queryset.all() diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 09c22ab8a..ddc5b8ff7 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -46,7 +46,9 @@ VIRTUALMACHINE_PRIMARY_IP = """ class ClusterTypeTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - cluster_count = tables.Column(verbose_name='Clusters') + cluster_count = tables.Column( + verbose_name='Clusters' + ) actions = tables.TemplateColumn( template_code=CLUSTERTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -55,7 +57,8 @@ class ClusterTypeTable(BaseTable): class Meta(BaseTable.Meta): model = ClusterType - fields = ('pk', 'name', 'cluster_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') + default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') # @@ -65,7 +68,9 @@ class ClusterTypeTable(BaseTable): class ClusterGroupTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - cluster_count = tables.Column(verbose_name='Clusters') + cluster_count = tables.Column( + verbose_name='Clusters' + ) actions = tables.TemplateColumn( template_code=CLUSTERGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -74,7 +79,8 @@ class ClusterGroupTable(BaseTable): class Meta(BaseTable.Meta): model = ClusterGroup - fields = ('pk', 'name', 'cluster_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') + default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') # @@ -84,10 +90,24 @@ class ClusterGroupTable(BaseTable): class ClusterTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') - site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices') - vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs') + tenant = tables.LinkColumn( + viewname='tenancy:tenant', + args=[Accessor('tenant.slug')] + ) + site = tables.LinkColumn( + viewname='dcim:site', + args=[Accessor('site.slug')] + ) + device_count = tables.Column( + accessor=Accessor('devices.count'), + orderable=False, + verbose_name='Devices' + ) + vm_count = tables.Column( + accessor=Accessor('virtual_machines.count'), + orderable=False, + verbose_name='VMs' + ) class Meta(BaseTable.Meta): model = Cluster @@ -101,10 +121,19 @@ class ClusterTable(BaseTable): class VirtualMachineTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - status = tables.TemplateColumn(template_code=VIRTUALMACHINE_STATUS) - cluster = tables.LinkColumn('virtualization:cluster', args=[Accessor('cluster.pk')]) - role = tables.TemplateColumn(VIRTUALMACHINE_ROLE) - tenant = tables.TemplateColumn(template_code=COL_TENANT) + status = tables.TemplateColumn( + template_code=VIRTUALMACHINE_STATUS + ) + cluster = tables.LinkColumn( + viewname='virtualization:cluster', + args=[Accessor('cluster.pk')] + ) + role = tables.TemplateColumn( + template_code=VIRTUALMACHINE_ROLE + ) + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) class Meta(BaseTable.Meta): model = VirtualMachine @@ -112,13 +141,31 @@ class VirtualMachineTable(BaseTable): class VirtualMachineDetailTable(VirtualMachineTable): + primary_ip4 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip4.pk')], + verbose_name='IPv4 Address' + ) + primary_ip6 = tables.LinkColumn( + viewname='ipam:ipaddress', + args=[Accessor('primary_ip6.pk')], + verbose_name='IPv6 Address' + ) primary_ip = tables.TemplateColumn( - orderable=False, verbose_name='IP Address', template_code=VIRTUALMACHINE_PRIMARY_IP + orderable=False, + verbose_name='IP Address', + template_code=VIRTUALMACHINE_PRIMARY_IP ) class Meta(BaseTable.Meta): model = VirtualMachine - fields = ('pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip') + fields = ( + 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4', + 'primary_ip6', 'primary_ip', + ) + default_columns = ( + 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', + ) #