From 0ee1112d9de7dfb8b02c7fb8b9326298a7f2c73d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 27 Apr 2020 16:56:25 -0400 Subject: [PATCH 01/20] Initial support for table column reordering --- netbox/circuits/tables.py | 11 ++++++++++- netbox/utilities/tables.py | 18 +++++++++++++++++- netbox/utilities/views.py | 3 ++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index a425b3ace19..a7e1b0e84e8 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -76,7 +76,16 @@ class CircuitTable(BaseTable): z_side = tables.Column( verbose_name='Z Side' ) + install_date = tables.Column( + visible=False + ) + commit_rate = tables.Column( + visible=False + ) class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') + fields = ( + 'pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', + 'description', + ) diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 9e91aebd281..bdbaa0b9b07 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -6,13 +6,29 @@ class BaseTable(tables.Table): """ Default table for object lists """ - def __init__(self, *args, **kwargs): + 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) + # Apply custom column ordering + if columns is not None: + pk = self.base_columns.pop('pk', 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 column, if defined on the table + if pk: + self.base_columns['pk'] = pk + self.sequence.insert(0, 'pk') + class Meta: attrs = { 'class': 'table table-hover table-headings', diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 294acb1d112..1782f1457a2 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -164,7 +164,8 @@ def get(self, request): 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') From e8d493578b4890743abae7cbbdd9da1143876b23 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 12:14:51 -0400 Subject: [PATCH 02/20] Create form for setting table preferences --- netbox/templates/inc/table_config_form.html | 23 +++++++++ netbox/templates/utilities/obj_list.html | 3 ++ netbox/utilities/forms.py | 55 ++++++++++++++++----- netbox/utilities/tables.py | 22 +++++++-- netbox/utilities/views.py | 7 ++- 5 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 netbox/templates/inc/table_config_form.html diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html new file mode 100644 index 00000000000..7a831be3cca --- /dev/null +++ b/netbox/templates/inc/table_config_form.html @@ -0,0 +1,23 @@ +{% load form_helpers %} + + + diff --git a/netbox/templates/utilities/obj_list.html b/netbox/templates/utilities/obj_list.html index f5482baf004..ea28c768281 100644 --- a/netbox/templates/utilities/obj_list.html +++ b/netbox/templates/utilities/obj_list.html @@ -18,6 +18,9 @@

{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}

+ {% if table_config_form %} + {% include 'inc/table_config_form.html' %} + {% endif %} {% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %} {% if permissions.change or permissions.delete %}
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index d95c865275e..f8d098d6689 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -137,6 +137,27 @@ def form_from_model(model, fields): return type('FormFromModel', (forms.Form,), form_fields) +def apply_bootstrap_classes(form): + """ + Apply Bootstrap CSS classes to form elements. + """ + exempt_widgets = [ + forms.CheckboxInput, + forms.ClearableFileInput, + forms.FileInput, + forms.RadioSelect + ] + + for field_name, field in form.fields.items(): + if field.widget.__class__ not in exempt_widgets: + css = field.widget.attrs.get('class', '') + field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip() + if field.required and not isinstance(field.widget, forms.FileInput): + field.widget.attrs['required'] = 'required' + if 'placeholder' not in field.widget.attrs: + field.widget.attrs['placeholder'] = field.label + + # # Widgets # @@ -663,19 +684,7 @@ class BootstrapMixin(forms.BaseForm): """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - exempt_widgets = [ - forms.CheckboxInput, forms.ClearableFileInput, forms.FileInput, forms.RadioSelect - ] - - for field_name, field in self.fields.items(): - if field.widget.__class__ not in exempt_widgets: - css = field.widget.attrs.get('class', '') - field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip() - if field.required and not isinstance(field.widget, forms.FileInput): - field.widget.attrs['required'] = 'required' - if 'placeholder' not in field.widget.attrs: - field.widget.attrs['placeholder'] = field.label + apply_bootstrap_classes(self) class ReturnURLForm(forms.Form): @@ -752,3 +761,23 @@ def clean(self): raise forms.ValidationError({ 'data': "Invalid YAML data: {}".format(err) }) + + +class TableConfigForm(forms.Form): + """ + Form for configuring user's table preferences. + """ + def __init__(self, table, *args, **kwargs): + super().__init__(*args, **kwargs) + + field_name = f"tables.{table.__class__.__name__}.columns" + self.fields[field_name] = forms.MultipleChoiceField( + choices=table.configurable_columns, + initial=table.visible_columns, + label='Columns', + widget=forms.SelectMultiple( + attrs={'size': 10} + ) + ) + + apply_bootstrap_classes(self) diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index bdbaa0b9b07..7da664c43b6 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -6,6 +6,11 @@ class BaseTable(tables.Table): """ Default table for object lists """ + class Meta: + attrs = { + 'class': 'table table-hover table-headings', + } + def __init__(self, *args, columns=None, **kwargs): super().__init__(*args, **kwargs) @@ -29,10 +34,19 @@ def __init__(self, *args, columns=None, **kwargs): self.base_columns['pk'] = pk self.sequence.insert(0, 'pk') - class Meta: - attrs = { - 'class': 'table table-hover table-headings', - } + @property + def configurable_columns(self): + selected_columns = [ + (name, self.columns[name].verbose_name) for name in self.sequence if name != 'pk' + ] + available_columns = [ + (name, column.verbose_name) for name, column in self.columns.items() if name not in self.sequence and name != 'pk' + ] + 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 1782f1457a2..8da8a19610a 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -24,7 +24,7 @@ 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 @@ -176,11 +176,16 @@ def get(self, request): } RequestConfig(request, paginate).configure(table) + table_config_form = TableConfigForm( + table=table + ) + context = { 'content_type': content_type, 'table': table, 'permissions': permissions, 'action_buttons': self.action_buttons, + 'table_config_form': table_config_form, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, } context.update(self.extra_context()) From 3442ec77a7cc4dbc34d02f6d3f1085e8ad21c1fe Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 13:21:58 -0400 Subject: [PATCH 03/20] Enable setting/clearing of table column prefs --- netbox/templates/inc/table_config_form.html | 2 +- netbox/utilities/forms.py | 61 +++++++++------------ netbox/utilities/views.py | 22 ++++++-- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html index 7a831be3cca..311345b4f02 100644 --- a/netbox/templates/inc/table_config_form.html +++ b/netbox/templates/inc/table_config_form.html @@ -9,7 +9,7 @@
diff --git a/netbox/templates/utilities/obj_list.html b/netbox/templates/utilities/obj_list.html index ea28c768281..4cfa8b1ce1c 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 %} @@ -18,9 +21,6 @@

{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}

- {% if table_config_form %} - {% include 'inc/table_config_form.html' %} - {% endif %} {% with bulk_edit_url=content_type.model_class|url_name:"bulk_edit" bulk_delete_url=content_type.model_class|url_name:"bulk_delete" %} {% if permissions.change or permissions.delete %}
@@ -71,6 +71,9 @@

{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bett {% 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/utilities/forms.py b/netbox/utilities/forms.py index e0ce09997aa..c1d9259993e 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -765,7 +765,8 @@ class TableConfigForm(BootstrapMixin, forms.Form): 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): diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 8b4942201cd..eca124a4ac1 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -202,7 +202,7 @@ def post(self, request): request.user.config.clear(preference_name, commit=True) messages.success(request, "Your preferences have been updated.") - return redirect(request.path) + return redirect(request.get_full_path()) def alter_queryset(self, request): # .all() is necessary to avoid caching queries From 96eafe6dc157c635c6069179094dfe3085ee9c36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 14:32:32 -0400 Subject: [PATCH 05/20] Document table columns preference --- docs/development/user-preferences.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development/user-preferences.md b/docs/development/user-preferences.md index f9ca05188f9..80088186c68 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 | From 725e3cdbf3a304deaf62f4cde7871c256336cb76 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 16:20:11 -0400 Subject: [PATCH 06/20] Extend circuits tables to include all relevant model fields --- netbox/circuits/tables.py | 25 +++++++++++++++---------- netbox/circuits/views.py | 6 +++--- netbox/netbox/views.py | 2 +- netbox/utilities/tables.py | 4 ++-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index a7e1b0e84e8..b04d0cbfc2a 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -27,18 +27,23 @@ class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + portal_url = tables.URLColumn( + visible=False + ) + noc_contact = tables.Column( + visible=False + ) + admin_contact = tables.Column( + visible=False + ) + 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') # @@ -86,6 +91,6 @@ class CircuitTable(BaseTable): class Meta(BaseTable.Meta): model = Circuit fields = ( - 'pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', + 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', 'description', ) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index b092e185546..709d2a726f6 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/netbox/views.py b/netbox/netbox/views.py index 25c32338ba7..98272a50ac0 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -44,7 +44,7 @@ # 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', diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 7da664c43b6..be08902747e 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -37,10 +37,10 @@ def __init__(self, *args, columns=None, **kwargs): @property def configurable_columns(self): selected_columns = [ - (name, self.columns[name].verbose_name) for name in self.sequence if name != 'pk' + (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 != 'pk' + (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 From 8ec2e3cc7b5bf75abd929456121f6b1ca6fa0ee8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 16:33:06 -0400 Subject: [PATCH 07/20] Introduce default_columns Meta parameter to reduce boilerplate --- netbox/circuits/tables.py | 18 +++--------------- netbox/utilities/tables.py | 7 +++++++ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index b04d0cbfc2a..0878279ec2a 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -27,15 +27,6 @@ class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - portal_url = tables.URLColumn( - visible=False - ) - noc_contact = tables.Column( - visible=False - ) - admin_contact = tables.Column( - visible=False - ) circuit_count = tables.Column( accessor=Accessor('count_circuits'), verbose_name='Circuits' @@ -44,6 +35,7 @@ class ProviderTable(BaseTable): class Meta(BaseTable.Meta): model = Provider fields = ('pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count') + default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') # @@ -63,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') # @@ -81,12 +74,6 @@ class CircuitTable(BaseTable): z_side = tables.Column( verbose_name='Z Side' ) - install_date = tables.Column( - visible=False - ) - commit_rate = tables.Column( - visible=False - ) class Meta(BaseTable.Meta): model = Circuit @@ -94,3 +81,4 @@ class Meta(BaseTable.Meta): '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/utilities/tables.py b/netbox/utilities/tables.py index be08902747e..57adb425637 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -18,6 +18,13 @@ def __init__(self, *args, columns=None, **kwargs): if self.empty_text is None: self.empty_text = 'No {} found'.format(self._meta.model._meta.verbose_name_plural) + # 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) From 55b40d92d474c0bc11ec09017e69df8e4997ec6f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Apr 2020 17:06:16 -0400 Subject: [PATCH 08/20] Extend DCIM tables (WIP) --- netbox/dcim/tables.py | 165 +++++++++++++++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 36 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 0e5e9dc7a90..75b319ff5af 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 Meta(BaseTable.Meta): 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,37 @@ class Meta(BaseTable.Meta): 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')] + ) + group = tables.Column( + accessor=Accessor('group.name') + ) + 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 +360,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 +407,9 @@ class Meta(BaseTable.Meta): 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 +462,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 +476,9 @@ class Meta(BaseTable.Meta): 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 +492,10 @@ class Meta(BaseTable.Meta): 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 +505,9 @@ class Meta(BaseTable.Meta): 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 +521,10 @@ class Meta(BaseTable.Meta): 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 +534,9 @@ class Meta(BaseTable.Meta): 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 +550,10 @@ class Meta(BaseTable.Meta): 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 +563,9 @@ class Meta(BaseTable.Meta): 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 +579,10 @@ class Meta(BaseTable.Meta): 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 +592,9 @@ class Meta(BaseTable.Meta): 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 +608,30 @@ class Meta(BaseTable.Meta): 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 +648,10 @@ class Meta(BaseTable.Meta): 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 +661,9 @@ class Meta(BaseTable.Meta): 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 +677,10 @@ class Meta(BaseTable.Meta): 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 +690,9 @@ class Meta(BaseTable.Meta): 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'}}, From 88687608e7b1a07e61e68a28134998d2dd759150 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 10:17:52 -0400 Subject: [PATCH 09/20] Always include the 'actions' column, if present --- netbox/utilities/tables.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 57adb425637..d59e4b647ba 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -28,6 +28,7 @@ def __init__(self, *args, columns=None, **kwargs): # 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: @@ -36,10 +37,13 @@ def __init__(self, *args, columns=None, **kwargs): self.columns.hide(name) self.sequence = columns - # Always include PK column, if defined on the table + # 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') @property def configurable_columns(self): From e3cfc9ad807bf7e634756e04ff2ab2ef80a7982c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 10:58:08 -0400 Subject: [PATCH 10/20] #492: Extend DCIM tables --- netbox/dcim/tables.py | 160 ++++++++++++++++++++++++++++++++--------- netbox/dcim/views.py | 2 +- netbox/netbox/views.py | 4 +- 3 files changed, 128 insertions(+), 38 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 75b319ff5af..2e310530ce6 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -723,8 +723,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'}}, @@ -734,6 +736,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') # @@ -763,7 +766,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', ) @@ -777,40 +784,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 ) - - 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 + 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 - 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 @@ -986,23 +1049,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 @@ -1019,6 +1082,10 @@ class Meta(BaseTable.Meta): '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', + ) # @@ -1120,12 +1187,21 @@ class Meta(BaseTable.Meta): 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') # @@ -1145,6 +1221,7 @@ class VirtualChassisTable(BaseTable): class Meta(BaseTable.Meta): model = VirtualChassis fields = ('pk', 'name', 'domain', 'member_count') + default_columns = ('pk', 'name', 'domain', 'member_count') # @@ -1166,6 +1243,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') # @@ -1189,7 +1267,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 66b59add4ac..ce3a3d068f0 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' diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 98272a50ac0..37a5164090e 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -20,7 +20,7 @@ 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 @@ -93,7 +93,7 @@ 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6', ), 'filterset': DeviceFilterSet, - 'table': DeviceDetailTable, + 'table': DeviceTable, 'url': 'dcim:device_list', }), ('virtualchassis', { From 7ad27a2b652db4c820a4b84924e3ee8a5822f62a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 11:03:49 -0400 Subject: [PATCH 11/20] #492: Extend extras tables --- netbox/extras/tables.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index b145824c6b7..7a78d4b199c 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): From 6e9e6af2f08cc7f35a8c0a4c8f1571ee7480c8c7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 11:29:30 -0400 Subject: [PATCH 12/20] #492: Extend IPAM tables --- netbox/ipam/tables.py | 283 ++++++++++++++++++++++++++++++++---------- 1 file changed, 220 insertions(+), 63 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 19735b81c06..56729f9dbea 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -190,12 +190,20 @@ 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 Meta(BaseTable.Meta): 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 Meta(RIRTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(PrefixTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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') From cd0ee4cd69378ca6d796ea4d8f10e62b68380a85 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 11:32:53 -0400 Subject: [PATCH 13/20] #492: Extend secrets tables --- netbox/secrets/tables.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 1e8a4e66929..11646f5deb0 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -20,14 +20,19 @@ class SecretRoleTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() - secret_count = tables.Column(verbose_name='Secrets') + secret_count = tables.Column( + verbose_name='Secrets' + ) 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): 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): 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') From 33c44c2dd952b5771d357d3c056e75bfb2455563 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 11:34:51 -0400 Subject: [PATCH 14/20] #492: Extend tenancy tables --- netbox/tenancy/tables.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0eca7de71ef..72fb98e80b8 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') From c096232cb1fb36f981c6ea986a2d96018596a174 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 11:42:44 -0400 Subject: [PATCH 15/20] #492: Extend virtualization tables --- netbox/virtualization/tables.py | 75 +++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 09c22ab8afa..ddc5b8ff76b 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -46,7 +46,9 @@ 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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 Meta(BaseTable.Meta): 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', + ) # From 3b6d9dc5529cca6cb45addf6d4547b6d19be04ce Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 14:56:22 -0400 Subject: [PATCH 16/20] Add button to select all columns --- netbox/project-static/js/forms.js | 4 ++++ netbox/templates/inc/table_config_form.html | 1 + 2 files changed, 5 insertions(+) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 59e1d8ff3e9..06d4a742a6d 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -472,5 +472,9 @@ $(document).ready(function() { } }); }); + $('#select-all-options').bind('click', function() { + var select_id = '#' + $(this).attr('data-target'); + $(select_id + ' option').prop('selected',true); + }); }); diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html index 9cce92a8fac..66844c7ca4c 100644 --- a/netbox/templates/inc/table_config_form.html +++ b/netbox/templates/inc/table_config_form.html @@ -14,6 +14,7 @@
From f8060ce112102924b0865c38057f935c715e045e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 29 Apr 2020 15:05:29 -0400 Subject: [PATCH 17/20] Ignore clearing of invalid user config keys --- netbox/users/models.py | 8 +++++--- netbox/users/tests/__init__.py | 0 netbox/users/tests/test_models.py | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 netbox/users/tests/__init__.py diff --git a/netbox/users/models.py b/netbox/users/models.py index 02356696fc1..ea5762232c0 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -108,7 +108,7 @@ def clear(self, path, commit=False): 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 @@ def clear(self, path, commit=False): 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 00000000000..e69de29bb2d diff --git a/netbox/users/tests/test_models.py b/netbox/users/tests/test_models.py index 0157d8fddb6..8047796c4b2 100644 --- a/netbox/users/tests/test_models.py +++ b/netbox/users/tests/test_models.py @@ -104,6 +104,5 @@ def test_clear(self): 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') From b0478a7e5bc5d53458588f91bef556e4aa36866e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 May 2020 11:47:30 -0400 Subject: [PATCH 18/20] Enable dynamic queryset field prefetching based on table columns --- netbox/utilities/tables.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index d59e4b647ba..e5433e3f891 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 @@ -45,6 +48,21 @@ def __init__(self, *args, columns=None, **kwargs): 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 = [ From 51ccbdf6c4a65d335d5518c9ba4531b500364736 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 May 2020 14:10:40 -0400 Subject: [PATCH 19/20] Remove descriptions from interface connections list --- netbox/dcim/tables.py | 10 +--------- netbox/dcim/views.py | 6 +----- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 2e310530ce6..2d2c5e6f864 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1153,10 +1153,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'), @@ -1169,15 +1165,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', ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ce3a3d068f0..cd1b4edf49e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2278,19 +2278,15 @@ def queryset_to_csv(self): 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) From 7c4d634ae6d65377cfc877b6c78dd6de41f87a49 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 May 2020 14:56:29 -0400 Subject: [PATCH 20/20] Fix group column on RackTable --- netbox/dcim/tables.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 2d2c5e6f864..5ce96841fce 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -316,9 +316,6 @@ class RackTable(BaseTable): viewname='dcim:site', args=[Accessor('site.slug')] ) - group = tables.Column( - accessor=Accessor('group.name') - ) tenant = tables.TemplateColumn( template_code=COL_TENANT )