Преглед изворни кода

Merge pull request #4555 from netbox-community/492-table-column-ordering

Closes #492: Table column ordering
Jeremy Stretch пре 5 година
родитељ
комит
80f08e6830

+ 1 - 0
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) |
 | 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 |
 | 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 |

+ 12 - 10
netbox/circuits/tables.py

@@ -27,18 +27,15 @@ STATUS_LABEL = """
 class ProviderTable(BaseTable):
 class ProviderTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
+    circuit_count = tables.Column(
+        accessor=Accessor('count_circuits'),
+        verbose_name='Circuits'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Provider
         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):
     class Meta(BaseTable.Meta):
         model = CircuitType
         model = CircuitType
         fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions')
         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):
     class Meta(BaseTable.Meta):
         model = Circuit
         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')

+ 3 - 3
netbox/circuits/views.py

@@ -28,7 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
     queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     filterset_form = forms.ProviderFilterForm
-    table = tables.ProviderDetailTable
+    table = tables.ProviderTable
 
 
 
 
 class ProviderView(PermissionRequiredMixin, View):
 class ProviderView(PermissionRequiredMixin, View):
@@ -87,7 +87,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
 
 
 class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
 class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'circuits.change_provider'
     permission_required = 'circuits.change_provider'
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
@@ -96,7 +96,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
 
 
 class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'circuits.delete_provider'
     permission_required = 'circuits.delete_provider'
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     default_return_url = 'circuits:provider_list'
     default_return_url = 'circuits:provider_list'

+ 252 - 80
netbox/dcim/tables.py

@@ -205,9 +205,13 @@ def get_component_template_actions(model_name):
 
 
 class RegionTable(BaseTable):
 class RegionTable(BaseTable):
     pk = ToggleColumn()
     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(
     actions = tables.TemplateColumn(
         template_code=REGION_ACTIONS,
         template_code=REGION_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -216,7 +220,8 @@ class RegionTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Region
         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):
 class SiteTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = Site
         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(
     rack_count = tables.Column(
         verbose_name='Racks'
         verbose_name='Racks'
     )
     )
-    slug = tables.Column()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=RACKGROUP_ACTIONS,
         template_code=RACKGROUP_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -263,6 +280,7 @@ class RackGroupTable(BaseTable):
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RackGroup
         model = RackGroup
         fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
         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):
     class Meta(BaseTable.Meta):
         model = RackRole
         model = RackRole
         fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions')
         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):
 class RackTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = Rack
         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):
 class RackDetailTable(RackTable):
@@ -321,6 +357,10 @@ class RackDetailTable(RackTable):
 
 
     class Meta(RackTable.Meta):
     class Meta(RackTable.Meta):
         fields = (
         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',
             'pk', 'name', 'site', 'group', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
             'get_utilization', 'get_power_utilization',
             'get_utilization', 'get_power_utilization',
         )
         )
@@ -364,6 +404,9 @@ class RackReservationTable(BaseTable):
         fields = (
         fields = (
             'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions',
             '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):
     class Meta(BaseTable.Meta):
         model = DeviceType
         model = DeviceType
         fields = (
         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',
             '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):
 class ConsolePortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('consoleporttemplate'),
         template_code=get_component_template_actions('consoleporttemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -441,7 +489,10 @@ class ConsolePortTemplateTable(BaseTable):
 
 
 
 
 class ConsolePortImportTable(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):
     class Meta(BaseTable.Meta):
         model = ConsolePort
         model = ConsolePort
@@ -451,7 +502,9 @@ class ConsolePortImportTable(BaseTable):
 
 
 class ConsoleServerPortTemplateTable(BaseTable):
 class ConsoleServerPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('consoleserverporttemplate'),
         template_code=get_component_template_actions('consoleserverporttemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -465,7 +518,10 @@ class ConsoleServerPortTemplateTable(BaseTable):
 
 
 
 
 class ConsoleServerPortImportTable(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):
     class Meta(BaseTable.Meta):
         model = ConsoleServerPort
         model = ConsoleServerPort
@@ -475,7 +531,9 @@ class ConsoleServerPortImportTable(BaseTable):
 
 
 class PowerPortTemplateTable(BaseTable):
 class PowerPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('powerporttemplate'),
         template_code=get_component_template_actions('powerporttemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -489,7 +547,10 @@ class PowerPortTemplateTable(BaseTable):
 
 
 
 
 class PowerPortImportTable(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):
     class Meta(BaseTable.Meta):
         model = PowerPort
         model = PowerPort
@@ -499,7 +560,9 @@ class PowerPortImportTable(BaseTable):
 
 
 class PowerOutletTemplateTable(BaseTable):
 class PowerOutletTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('poweroutlettemplate'),
         template_code=get_component_template_actions('poweroutlettemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -513,7 +576,10 @@ class PowerOutletTemplateTable(BaseTable):
 
 
 
 
 class PowerOutletImportTable(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):
     class Meta(BaseTable.Meta):
         model = PowerOutlet
         model = PowerOutlet
@@ -523,7 +589,9 @@ class PowerOutletImportTable(BaseTable):
 
 
 class InterfaceTemplateTable(BaseTable):
 class InterfaceTemplateTable(BaseTable):
     pk = ToggleColumn()
     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(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('interfacetemplate'),
         template_code=get_component_template_actions('interfacetemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -537,18 +605,30 @@ class InterfaceTemplateTable(BaseTable):
 
 
 
 
 class InterfaceImportTable(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):
     class Meta(BaseTable.Meta):
         model = Interface
         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
         empty_text = False
 
 
 
 
 class FrontPortTemplateTable(BaseTable):
 class FrontPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     rear_port_position = tables.Column(
     rear_port_position = tables.Column(
         verbose_name='Position'
         verbose_name='Position'
     )
     )
@@ -565,7 +645,10 @@ class FrontPortTemplateTable(BaseTable):
 
 
 
 
 class FrontPortImportTable(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):
     class Meta(BaseTable.Meta):
         model = FrontPort
         model = FrontPort
@@ -575,7 +658,9 @@ class FrontPortImportTable(BaseTable):
 
 
 class RearPortTemplateTable(BaseTable):
 class RearPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('rearporttemplate'),
         template_code=get_component_template_actions('rearporttemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -589,7 +674,10 @@ class RearPortTemplateTable(BaseTable):
 
 
 
 
 class RearPortImportTable(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):
     class Meta(BaseTable.Meta):
         model = RearPort
         model = RearPort
@@ -599,7 +687,9 @@ class RearPortImportTable(BaseTable):
 
 
 class DeviceBayTemplateTable(BaseTable):
 class DeviceBayTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.Column(order_by=('_name',))
+    name = tables.Column(
+        order_by=('_name',)
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=get_component_template_actions('devicebaytemplate'),
         template_code=get_component_template_actions('devicebaytemplate'),
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -630,8 +720,10 @@ class DeviceRoleTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='VMs'
         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(
     actions = tables.TemplateColumn(
         template_code=DEVICEROLE_ACTIONS,
         template_code=DEVICEROLE_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -641,6 +733,7 @@ class DeviceRoleTable(BaseTable):
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = DeviceRole
         model = DeviceRole
         fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
         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):
     class Meta(BaseTable.Meta):
         model = Platform
         model = Platform
         fields = (
         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',),
         order_by=('_name',),
         template_code=DEVICE_LINK
         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(
     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
         text=lambda record: record.device_type.display_name
     )
     )
-
-    class Meta(BaseTable.Meta):
-        model = Device
-        fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
-
-
-class DeviceDetailTable(DeviceTable):
     primary_ip = tables.TemplateColumn(
     primary_ip = tables.TemplateColumn(
-        orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
+        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(DeviceTable.Meta):
+    class Meta(BaseTable.Meta):
         model = Device
         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):
 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):
     class Meta(BaseTable.Meta):
         model = Device
         model = Device
@@ -893,23 +1046,23 @@ class CableTable(BaseTable):
         template_code=CABLE_TERMINATION_PARENT,
         template_code=CABLE_TERMINATION_PARENT,
         accessor=Accessor('termination_a'),
         accessor=Accessor('termination_a'),
         orderable=False,
         orderable=False,
-        verbose_name='Termination A'
+        verbose_name='Side A'
     )
     )
     termination_a = tables.LinkColumn(
     termination_a = tables.LinkColumn(
         accessor=Accessor('termination_a'),
         accessor=Accessor('termination_a'),
         orderable=False,
         orderable=False,
-        verbose_name=''
+        verbose_name='Termination A'
     )
     )
     termination_b_parent = tables.TemplateColumn(
     termination_b_parent = tables.TemplateColumn(
         template_code=CABLE_TERMINATION_PARENT,
         template_code=CABLE_TERMINATION_PARENT,
         accessor=Accessor('termination_b'),
         accessor=Accessor('termination_b'),
         orderable=False,
         orderable=False,
-        verbose_name='Termination B'
+        verbose_name='Side B'
     )
     )
     termination_b = tables.LinkColumn(
     termination_b = tables.LinkColumn(
         accessor=Accessor('termination_b'),
         accessor=Accessor('termination_b'),
         orderable=False,
         orderable=False,
-        verbose_name=''
+        verbose_name='Termination B'
     )
     )
     status = tables.TemplateColumn(
     status = tables.TemplateColumn(
         template_code=STATUS_LABEL
         template_code=STATUS_LABEL
@@ -926,6 +1079,10 @@ class CableTable(BaseTable):
             'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
             'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
             'status', 'type', 'color', 'length',
             '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')],
         args=[Accessor('pk')],
         verbose_name='Interface A'
         verbose_name='Interface A'
     )
     )
-    description_a = tables.Column(
-        accessor=Accessor('description'),
-        verbose_name='Description'
-    )
     device_b = tables.LinkColumn(
     device_b = tables.LinkColumn(
         viewname='dcim:device',
         viewname='dcim:device',
         accessor=Accessor('_connected_interface.device'),
         accessor=Accessor('_connected_interface.device'),
@@ -1009,15 +1162,11 @@ class InterfaceConnectionTable(BaseTable):
         args=[Accessor('_connected_interface.pk')],
         args=[Accessor('_connected_interface.pk')],
         verbose_name='Interface B'
         verbose_name='Interface B'
     )
     )
-    description_b = tables.Column(
-        accessor=Accessor('_connected_interface.description'),
-        verbose_name='Description'
-    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Interface
         model = Interface
         fields = (
         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):
 class InventoryItemTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = InventoryItem
         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):
     class Meta(BaseTable.Meta):
         model = VirtualChassis
         model = VirtualChassis
         fields = ('pk', 'name', 'domain', 'member_count')
         fields = ('pk', 'name', 'domain', 'member_count')
+        default_columns = ('pk', 'name', 'domain', 'member_count')
 
 
 
 
 #
 #
@@ -1073,6 +1232,7 @@ class PowerPanelTable(BaseTable):
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerPanel
         model = PowerPanel
         fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
         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(
     type = tables.TemplateColumn(
         template_code=TYPE_LABEL
         template_code=TYPE_LABEL
     )
     )
+    max_utilization = tables.TemplateColumn(
+        template_code="{{ value }}%"
+    )
+    available_power = tables.Column(
+        verbose_name='Available power (VA)'
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerFeed
         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',
+        )

+ 2 - 6
netbox/dcim/views.py

@@ -1095,7 +1095,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
     )
     )
     filterset = filters.DeviceFilterSet
     filterset = filters.DeviceFilterSet
     filterset_form = forms.DeviceFilterForm
     filterset_form = forms.DeviceFilterForm
-    table = tables.DeviceDetailTable
+    table = tables.DeviceTable
     template_name = 'dcim/device_list.html'
     template_name = 'dcim/device_list.html'
 
 
 
 
@@ -2278,19 +2278,15 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
         csv_data = [
         csv_data = [
             # Headers
             # Headers
             ','.join([
             ','.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:
         for obj in self.queryset:
             csv = csv_format([
             csv = csv_format([
                 obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
                 obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
                 obj.connected_endpoint.name 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.device.identifier,
                 obj.name,
                 obj.name,
-                obj.description,
                 obj.get_connection_status_display(),
                 obj.get_connection_status_display(),
             ])
             ])
             csv_data.append(csv)
             csv_data.append(csv)

+ 5 - 1
netbox/extras/tables.py

@@ -104,7 +104,11 @@ class ConfigContextTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ConfigContext
         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):
 class ObjectChangeTable(BaseTable):

+ 220 - 63
netbox/ipam/tables.py

@@ -190,12 +190,20 @@ TENANT_LINK = """
 class VRFTable(BaseTable):
 class VRFTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     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):
     class Meta(BaseTable.Meta):
         model = VRF
         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):
 class RIRTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = RIR
         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):
 class RIRDetailTable(RIRTable):
@@ -247,6 +264,10 @@ class RIRDetailTable(RIRTable):
 
 
     class Meta(RIRTable.Meta):
     class Meta(RIRTable.Meta):
         fields = (
         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',
             'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
             'stats_deprecated', 'stats_available', 'utilization', 'actions',
             'stats_deprecated', 'stats_available', 'utilization', 'actions',
         )
         )
@@ -258,8 +279,13 @@ class RIRDetailTable(RIRTable):
 
 
 class AggregateTable(BaseTable):
 class AggregateTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = Aggregate
         model = Aggregate
@@ -267,8 +293,13 @@ class AggregateTable(BaseTable):
 
 
 
 
 class AggregateDetailTable(AggregateTable):
 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):
     class Meta(AggregateTable.Meta):
         fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
         fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
@@ -300,7 +331,8 @@ class RoleTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Role
         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):
 class PrefixTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = Prefix
         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 = {
         row_attrs = {
             'class': lambda record: 'success' if not record.pk else '',
             'class': lambda record: 'success' if not record.pk else '',
         }
         }
 
 
 
 
 class PrefixDetailTable(PrefixTable):
 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):
     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):
 class IPAddressTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = IPAddress
         model = IPAddress
@@ -358,22 +438,40 @@ class IPAddressTable(BaseTable):
 
 
 class IPAddressDetailTable(IPAddressTable):
 class IPAddressDetailTable(IPAddressTable):
     nat_inside = tables.LinkColumn(
     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):
     class Meta(IPAddressTable.Meta):
         fields = (
         fields = (
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
             'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
             'description',
             'description',
         )
         )
+        default_columns = (
+            'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
+        )
 
 
 
 
 class IPAddressAssignTable(BaseTable):
 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):
     class Meta(BaseTable.Meta):
         model = IPAddress
         model = IPAddress
@@ -385,10 +483,19 @@ class InterfaceIPAddressTable(BaseTable):
     """
     """
     List IP addresses assigned to a specific Interface.
     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):
     class Meta(BaseTable.Meta):
         model = IPAddress
         model = IPAddress
@@ -401,16 +508,24 @@ class InterfaceIPAddressTable(BaseTable):
 
 
 class VLANGroupTable(BaseTable):
 class VLANGroupTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = VLANGroup
         model = VLANGroup
         fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions')
         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):
 class VLANTable(BaseTable):
     pk = ToggleColumn()
     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):
     class Meta(BaseTable.Meta):
         model = VLAN
         model = VLAN
@@ -435,16 +565,26 @@ class VLANTable(BaseTable):
 
 
 
 
 class VLANDetailTable(VLANTable):
 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):
     class Meta(VLANTable.Meta):
         fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
         fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
 
 
 
 
 class VLANMemberTable(BaseTable):
 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(
     untagged = tables.TemplateColumn(
         template_code=VLAN_MEMBER_UNTAGGED,
         template_code=VLAN_MEMBER_UNTAGGED,
         orderable=False
         orderable=False
@@ -464,13 +604,29 @@ class InterfaceVLANTable(BaseTable):
     """
     """
     List VLANs assigned to a specific Interface.
     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()
     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):
     class Meta(BaseTable.Meta):
         model = VLAN
         model = VLAN
@@ -494,4 +650,5 @@ class ServiceTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Service
         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')

+ 3 - 3
netbox/netbox/views.py

@@ -20,7 +20,7 @@ from dcim.models import (
     Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
     Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
 )
 )
 from dcim.tables import (
 from dcim.tables import (
-    CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
+    CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
     VirtualChassisTable,
     VirtualChassisTable,
 )
 )
 from extras.models import ObjectChange, ReportResult
 from extras.models import ObjectChange, ReportResult
@@ -44,7 +44,7 @@ SEARCH_TYPES = OrderedDict((
     # Circuits
     # Circuits
     ('provider', {
     ('provider', {
         'permission': 'circuits.view_provider',
         'permission': 'circuits.view_provider',
-        'queryset': Provider.objects.all(),
+        'queryset': Provider.objects.annotate(count_circuits=Count('circuits')),
         'filterset': ProviderFilterSet,
         'filterset': ProviderFilterSet,
         'table': ProviderTable,
         'table': ProviderTable,
         'url': 'circuits:provider_list',
         'url': 'circuits:provider_list',
@@ -93,7 +93,7 @@ SEARCH_TYPES = OrderedDict((
             'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
             'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
         ),
         ),
         'filterset': DeviceFilterSet,
         'filterset': DeviceFilterSet,
-        'table': DeviceDetailTable,
+        'table': DeviceTable,
         'url': 'dcim:device_list',
         'url': 'dcim:device_list',
     }),
     }),
     ('virtualchassis', {
     ('virtualchassis', {

+ 29 - 0
netbox/project-static/js/forms.js

@@ -448,4 +448,33 @@ $(document).ready(function() {
     $('a.image-preview').on('mouseout', function() {
     $('a.image-preview').on('mouseout', function() {
         $('#image-preview-window').fadeOut('fast');
         $('#image-preview-window').fadeOut('fast');
     });
     });
+
+    // Rearrange options within a <select> list
+    $('#move-option-up').bind('click', function() {
+        var select_id = '#' + $(this).attr('data-target');
+        $(select_id + ' option:selected').each(function () {
+            var newPos = $(select_id + ' option').index(this) - 1;
+            if (newPos > -1) {
+                $(select_id + ' option').eq(newPos).before("<option value='" + $(this).val() + "' selected='selected'>" + $(this).text() + "</option>");
+                $(this).remove();
+            }
+        });
+    });
+    $('#move-option-down').bind('click', function() {
+        var select_id = '#' + $(this).attr('data-target');
+        var countOptions = $(select_id + ' option').length;
+        var countSelectedOptions = $(select_id + ' option:selected').length;
+        $(select_id + ' option:selected').each(function () {
+            var newPos = $(select_id + ' option').index(this) + countSelectedOptions;
+            if (newPos < countOptions) {
+                $(select_id + ' option').eq(newPos).after("<option value='" + $(this).val() + "' selected='selected'>" + $(this).text() + "</option>");
+                $(this).remove();
+            }
+        });
+    });
+    $('#select-all-options').bind('click', function() {
+        var select_id = '#' + $(this).attr('data-target');
+        $(select_id + ' option').prop('selected',true);
+    });
+
 });
 });

+ 10 - 4
netbox/secrets/tables.py

@@ -20,14 +20,19 @@ SECRETROLE_ACTIONS = """
 class SecretRoleTable(BaseTable):
 class SecretRoleTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
-    secret_count = tables.Column(verbose_name='Secrets')
+    secret_count = tables.Column(
+        verbose_name='Secrets'
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
-        template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
+        template_code=SECRETROLE_ACTIONS,
+        attrs={'td': {'class': 'text-right noprint'}},
+        verbose_name=''
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = SecretRole
         model = SecretRole
-        fields = ('pk', 'name', 'secret_count', 'description', 'slug', 'actions')
+        fields = ('pk', 'name', 'secret_count', 'description', 'slug', 'users', 'groups', 'actions')
+        default_columns = ('pk', 'name', 'secret_count', 'description', 'actions')
 
 
 
 
 #
 #
@@ -40,4 +45,5 @@ class SecretTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Secret
         model = Secret
-        fields = ('pk', 'device', 'role', 'name', 'last_updated')
+        fields = ('pk', 'device', 'role', 'name', 'last_updated', 'hash')
+        default_columns = ('pk', 'device', 'role', 'name', 'last_updated')

+ 28 - 0
netbox/templates/inc/table_config_form.html

@@ -0,0 +1,28 @@
+{% load form_helpers %}
+<div class="modal fade" tabindex="-1" id="tableconfig">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <h4 class="modal-title">Table Configuration</h4>
+            </div>
+            <div class="modal-body">
+                <form action="" method="post" class="form-horizontal">
+                    {% csrf_token %}
+                    {% render_form table_config_form %}
+                    <div class="row">
+                        <div class="col-md-9 col-md-offset-3">
+                            <a class="btn btn-primary btn-xs" id="move-option-up" data-target="id_columns"><i class="fa fa-arrow-up"></i> Move up</a>
+                            <a class="btn btn-primary btn-xs" id="move-option-down" data-target="id_columns"><i class="fa fa-arrow-down"></i> Move down</a>
+                            <a class="btn btn-success btn-xs" id="select-all-options" data-target="id_columns"><i class="fa fa-ellipsis-v"></i> Select all</a>
+                        </div>
+                    </div>
+                    <div class="text-right">
+                        <input type="submit" class="btn btn-primary" name="set" value="Save" />
+                        <input type="submit" class="btn btn-danger" name="clear" value="Reset" />
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>

+ 6 - 0
netbox/templates/utilities/obj_list.html

@@ -5,6 +5,9 @@
 {% block content %}
 {% block content %}
 <div class="pull-right noprint">
 <div class="pull-right noprint">
     {% block buttons %}{% endblock %}
     {% block buttons %}{% endblock %}
+    {% if table_config_form %}
+        <button type="button" class="btn btn-default" data-toggle="modal" data-target="#tableconfig" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+    {% endif %}
     {% if permissions.add and 'add' in action_buttons %}
     {% if permissions.add and 'add' in action_buttons %}
         {% add_button content_type.model_class|url_name:"add" %}
         {% add_button content_type.model_class|url_name:"add" %}
     {% endif %}
     {% endif %}
@@ -68,6 +71,9 @@
         {% endwith %}
         {% endwith %}
         {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
         {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
         <div class="clearfix"></div>
         <div class="clearfix"></div>
+        {% if table_config_form %}
+            {% include 'inc/table_config_form.html' %}
+        {% endif %}
     </div>
     </div>
     {% if filter_form %}
     {% if filter_form %}
         <div class="col-md-3 noprint">
         <div class="col-md-3 noprint">

+ 3 - 2
netbox/tenancy/tables.py

@@ -44,7 +44,6 @@ class TenantGroupTable(BaseTable):
     tenant_count = tables.Column(
     tenant_count = tables.Column(
         verbose_name='Tenants'
         verbose_name='Tenants'
     )
     )
-    slug = tables.Column()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=TENANTGROUP_ACTIONS,
         template_code=TENANTGROUP_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -54,6 +53,7 @@ class TenantGroupTable(BaseTable):
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = TenantGroup
         model = TenantGroup
         fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions')
         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):
     class Meta(BaseTable.Meta):
         model = Tenant
         model = Tenant
-        fields = ('pk', 'name', 'group', 'description')
+        fields = ('pk', 'name', 'slug', 'group', 'description')
+        default_columns = ('pk', 'name', 'group', 'description')

+ 5 - 3
netbox/users/models.py

@@ -108,7 +108,7 @@ class UserConfig(models.Model):
 
 
             userconfig.clear('foo.bar.baz')
             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 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.
         :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('.')
         keys = path.split('.')
 
 
         for key in keys[:-1]:
         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]
                 d = d[key]
 
 
         key = keys[-1]
         key = keys[-1]
-        del(d[key])
+        d.pop(key, None)  # Avoid a KeyError on invalid keys
 
 
         if commit:
         if commit:
             self.save()
             self.save()

+ 0 - 0
netbox/users/tests/__init__.py


+ 2 - 3
netbox/users/tests/test_models.py

@@ -104,6 +104,5 @@ class UserConfigTest(TestCase):
         self.assertTrue('foo' not in userconfig.data['b'])
         self.assertTrue('foo' not in userconfig.data['b'])
         self.assertEqual(userconfig.data['b']['bar'], 102)
         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')

+ 24 - 1
netbox/utilities/forms.py

@@ -665,7 +665,10 @@ class BootstrapMixin(forms.BaseForm):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         exempt_widgets = [
         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():
         for field_name, field in self.fields.items():
@@ -752,3 +755,23 @@ class ImportForm(BootstrapMixin, forms.Form):
                 raise forms.ValidationError({
                 raise forms.ValidationError({
                     'data': "Invalid YAML data: {}".format(err)
                     '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

+ 64 - 5
netbox/utilities/tables.py

@@ -1,4 +1,7 @@
 import django_tables2 as tables
 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
 from django.utils.safestring import mark_safe
 
 
 
 
@@ -6,17 +9,73 @@ class BaseTable(tables.Table):
     """
     """
     Default table for object lists
     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)
         super().__init__(*args, **kwargs)
 
 
         # Set default empty_text if none was provided
         # Set default empty_text if none was provided
         if self.empty_text is None:
         if self.empty_text is None:
             self.empty_text = 'No {} found'.format(self._meta.model._meta.verbose_name_plural)
             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):
 class ToggleColumn(tables.CheckBoxColumn):

+ 20 - 2
netbox/utilities/views.py

@@ -24,7 +24,7 @@ from django_tables2 import RequestConfig
 from extras.models import CustomField, CustomFieldValue, ExportTemplate
 from extras.models import CustomField, CustomFieldValue, ExportTemplate
 from extras.querysets import CustomFieldQueryset
 from extras.querysets import CustomFieldQueryset
 from utilities.exceptions import AbortTransaction
 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 utilities.utils import csv_format, prepare_cloned_fields
 from .error_handlers import handle_protectederror
 from .error_handlers import handle_protectederror
 from .forms import ConfirmationForm, ImportForm
 from .forms import ConfirmationForm, ImportForm
@@ -164,7 +164,8 @@ class ObjectListView(View):
             permissions[action] = request.user.has_perm(perm_name)
             permissions[action] = request.user.has_perm(perm_name)
 
 
         # Construct the table based on the user's permissions
         # 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']):
         if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
             table.columns.show('pk')
             table.columns.show('pk')
 
 
@@ -180,12 +181,29 @@ class ObjectListView(View):
             'table': table,
             'table': table,
             'permissions': permissions,
             'permissions': permissions,
             'action_buttons': self.action_buttons,
             '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,
             'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
         }
         }
         context.update(self.extra_context())
         context.update(self.extra_context())
 
 
         return render(request, self.template_name, 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):
     def alter_queryset(self, request):
         # .all() is necessary to avoid caching queries
         # .all() is necessary to avoid caching queries
         return self.queryset.all()
         return self.queryset.all()

+ 61 - 14
netbox/virtualization/tables.py

@@ -46,7 +46,9 @@ VIRTUALMACHINE_PRIMARY_IP = """
 class ClusterTypeTable(BaseTable):
 class ClusterTypeTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
-    cluster_count = tables.Column(verbose_name='Clusters')
+    cluster_count = tables.Column(
+        verbose_name='Clusters'
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=CLUSTERTYPE_ACTIONS,
         template_code=CLUSTERTYPE_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -55,7 +57,8 @@ class ClusterTypeTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ClusterType
         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):
 class ClusterGroupTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     name = tables.LinkColumn()
-    cluster_count = tables.Column(verbose_name='Clusters')
+    cluster_count = tables.Column(
+        verbose_name='Clusters'
+    )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
         template_code=CLUSTERGROUP_ACTIONS,
         template_code=CLUSTERGROUP_ACTIONS,
         attrs={'td': {'class': 'text-right noprint'}},
         attrs={'td': {'class': 'text-right noprint'}},
@@ -74,7 +79,8 @@ class ClusterGroupTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ClusterGroup
         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):
 class ClusterTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     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):
     class Meta(BaseTable.Meta):
         model = Cluster
         model = Cluster
@@ -101,10 +121,19 @@ class ClusterTable(BaseTable):
 class VirtualMachineTable(BaseTable):
 class VirtualMachineTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn()
     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):
     class Meta(BaseTable.Meta):
         model = VirtualMachine
         model = VirtualMachine
@@ -112,13 +141,31 @@ class VirtualMachineTable(BaseTable):
 
 
 
 
 class VirtualMachineDetailTable(VirtualMachineTable):
 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(
     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):
     class Meta(BaseTable.Meta):
         model = VirtualMachine
         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',
+        )
 
 
 
 
 #
 #