diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 35d623ff0..26b7a552c 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -2,6 +2,7 @@ import json import re from django import forms +from django.contrib.postgres.forms import SimpleArrayField from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -21,6 +22,7 @@ from utilities.forms.fields import ( ) from utilities.forms.rendering import FieldSet, ObjectAttribute from utilities.forms.widgets import ChoicesWidget, HTMXSelect +from utilities.tables import get_table_for_model from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( @@ -308,6 +310,22 @@ class TableConfigForm(forms.ModelForm): label=_('Object type'), queryset=ObjectType.objects.all() ) + available_columns = SimpleArrayField( + base_field=forms.CharField(), + required=False, + widget=forms.SelectMultiple( + attrs={'size': 10, 'class': 'form-select'} + ), + label=_('Available Columns') + ) + columns = SimpleArrayField( + base_field=forms.CharField(), + required=False, + widget=forms.SelectMultiple( + attrs={'size': 10, 'class': 'form-select'} + ), + label=_('Selected Columns') + ) fieldsets = ( FieldSet( @@ -321,6 +339,31 @@ class TableConfigForm(forms.ModelForm): model = TableConfig exclude = ('user',) + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + object_type = ObjectType.objects.get(pk=get_field_value(self, 'object_type')) + model = object_type.model_class() + table_name = get_field_value(self, 'table') + table_class = get_table_for_model(model, table_name) + table = table_class(model.objects.all()) + + if columns := self._get_columns(): + table._set_columns(columns) + + # Initialize columns field based on table attributes + self.fields['available_columns'].widget.choices = table.available_columns + self.fields['columns'].widget.choices = table.selected_columns + + def _get_columns(self): + if self.is_bound and (columns := self.data.get('columns')): + return columns + if 'columns' in self.initial: + columns = self.get_initial_for_field(self.fields['columns'], 'columns') + return columns.split(',') if type(columns) is str else columns + if self.instance is not None: + return self.instance.columns + class BookmarkForm(forms.ModelForm): object_type = ContentTypeChoiceField( diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 9fe5ca153..b24a8045a 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -27,6 +27,7 @@ from utilities.html import clean_html from utilities.jinja2 import render_jinja2 from utilities.querydict import dict_to_querydict from utilities.querysets import RestrictedQuerySet +from utilities.tables import get_table_for_model __all__ = ( 'Bookmark', @@ -594,6 +595,10 @@ class TableConfig(ChangeLoggedModel): def docs_url(self): return f'{settings.STATIC_URL}docs/models/extras/tableconfig/' + @property + def table_class(self): + return get_table_for_model(self.object_type.model_class(), name=self.table) + class ImageAttachment(ChangeLoggedModel): """ diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 30f49698e..b3ac0aee1 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -359,6 +359,7 @@ class TableConfigView(SharedObjectViewMixin, generic.ObjectView): class TableConfigEditView(SharedObjectViewMixin, generic.ObjectEditView): queryset = TableConfig.objects.all() form = forms.TableConfigForm + template_name = 'extras/tableconfig_edit.html' def alter_object(self, obj, request, url_args, url_kwargs): if not obj.pk: diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index a52d771f1..90a6a9910 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -349,7 +349,7 @@ CUSTOMIZATION_MENU = Menu( get_model_item('extras', 'customlink', _('Custom Links')), get_model_item('extras', 'exporttemplate', _('Export Templates')), get_model_item('extras', 'savedfilter', _('Saved Filters')), - get_model_item('extras', 'tableconfig', _('Table Configs'), actions=('add',)), + get_model_item('extras', 'tableconfig', _('Table Configs'), actions=()), get_model_item('extras', 'tag', 'Tags'), get_model_item('extras', 'imageattachment', _('Image Attachments'), actions=()), ), diff --git a/netbox/templates/extras/tableconfig.html b/netbox/templates/extras/tableconfig.html index c2d6a4867..d8b4d0f30 100644 --- a/netbox/templates/extras/tableconfig.html +++ b/netbox/templates/extras/tableconfig.html @@ -4,74 +4,74 @@ {% load i18n %} {% block content %} -
-
-
-

{% trans "Table Config" %}

- - - - - - +
+
+
+

{% trans "Table Config" %}

+
{% trans "Name" %}{{ object.name }}
+ + + + + - - + + - - + + - - + + - - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %} {{ object.description|placeholder }}
{% trans "Object Type" %} {{ object.object_type }}
{% trans "Table" %} {{ object.table }}
{% trans "User" %} {{ object.user|placeholder }}
{% trans "Enabled" %}{% checkmark object.enabled %}
{% trans "Shared" %}{% checkmark object.shared %}
{% trans "Weight" %}{{ object.weight }}
-
- {% plugin_left_page object %} -
-
-
-

{% trans "Columns" %}

-
-
    - {% for column in object.columns %} -
  • {{ column }}
  • - {% endfor %} -
+ + + {% trans "Enabled" %} + {% checkmark object.enabled %} + + + {% trans "Shared" %} + {% checkmark object.shared %} + + + {% trans "Weight" %} + {{ object.weight }} + +
+ {% plugin_left_page object %}
-
-

{% trans "Ordering" %}

-
-
    - {% for column in object.ordering %} -
  • {{ column }}
  • - {% endfor %} -
+
+
+

{% trans "Columns" %}

+
+
    + {% for column in object.columns %} +
  • {{ column }}
  • + {% endfor %} +
+
+
+

{% trans "Ordering" %}

+
+
    + {% for column in object.ordering %} +
  • {{ column }}
  • + {% endfor %} +
+
+
+ {% plugin_right_page object %}
- {% plugin_right_page object %}
-
-
+
- {% plugin_full_width_page object %} + {% plugin_full_width_page object %}
-
+
{% endblock %} diff --git a/netbox/templates/extras/tableconfig_edit.html b/netbox/templates/extras/tableconfig_edit.html new file mode 100644 index 000000000..bfe508a8e --- /dev/null +++ b/netbox/templates/extras/tableconfig_edit.html @@ -0,0 +1,52 @@ +{% extends 'generic/object_edit.html' %} +{% load form_helpers %} +{% load i18n %} + +{% block form %} + {% render_errors form %} + +
+
+

{% trans "Device" %}

+
+ {% render_field form.name %} + {% render_field form.slug %} + {% render_field form.object_type %} + {% render_field form.table %} + {% render_field form.description %} + {% render_field form.weight %} + {% render_field form.enabled %} + {% render_field form.shared %} + {% render_field form.ordering %} +
+ +
+
+
+ {{ form.available_columns.label }} + {{ form.available_columns }} +
+ +
+ {{ form.columns.label }} + {{ form.columns }} + + {% trans "Move Up" %} + + + {% trans "Move Down" %} + +
+
+
+ +{% endblock %} diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 141ca66d6..8d2b260d5 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -1,13 +1,21 @@ +from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ + from netbox.registry import registry __all__ = ( + 'get_table_for_model', 'get_table_ordering', 'linkify_phone', 'register_table_column' ) +def get_table_for_model(model, name=None): + name = name or f'{model.__name__}Table' + return import_string(f'{model._meta.app_label}.tables.{name}') + + def get_table_ordering(request, table): """ Given a request, return the prescribed table ordering, if any. This may be necessary to determine prior to rendering