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 "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 %}
-
-
-
-
-
-
- {% 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 %}
-
-
-
-
- {% for column in object.ordering %}
- - {{ column }}
- {% endfor %}
-
+
+
+
+
+
+ {% for column in object.columns %}
+ - {{ column }}
+ {% endfor %}
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+
+{% 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