diff --git a/netbox/templates/inc/table_config_form.html b/netbox/templates/inc/table_config_form.html new file mode 100644 index 000000000..7a831be3c --- /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 f5482baf0..ea28c7682 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 d95c86527..f8d098d66 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 @@ class ImportForm(BootstrapMixin, forms.Form): 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 bdbaa0b9b..7da664c43 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 @@ class BaseTable(tables.Table): 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 1782f1457..8da8a1961 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -24,7 +24,7 @@ from django_tables2 import RequestConfig from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset from utilities.exceptions import AbortTransaction -from utilities.forms import BootstrapMixin, CSVDataField +from utilities.forms import BootstrapMixin, CSVDataField, TableConfigForm from utilities.utils import csv_format, prepare_cloned_fields from .error_handlers import handle_protectederror from .forms import ConfirmationForm, ImportForm @@ -176,11 +176,16 @@ class ObjectListView(View): } 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())