Просмотр исходного кода

Merge pull request #5288 from netbox-community/5252-table-config

Closes #5252: Introduce an API endpoint for writing user preferences
Jeremy Stretch 5 лет назад
Родитель
Сommit
6edd65c4ed

+ 45 - 9
netbox/dcim/views.py

@@ -1019,7 +1019,11 @@ class DeviceView(ObjectView):
         consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'cable', '_path__destination',
         )
-        consoleport_table = tables.DeviceConsolePortTable(consoleports, orderable=False)
+        consoleport_table = tables.DeviceConsolePortTable(
+            data=consoleports,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
             consoleport_table.columns.show('pk')
 
@@ -1029,7 +1033,11 @@ class DeviceView(ObjectView):
         ).prefetch_related(
             'cable', '_path__destination',
         )
-        consoleserverport_table = tables.DeviceConsoleServerPortTable(consoleserverports, orderable=False)
+        consoleserverport_table = tables.DeviceConsoleServerPortTable(
+            data=consoleserverports,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_consoleserverport') or \
                 request.user.has_perm('dcim.delete_consoleserverport'):
             consoleserverport_table.columns.show('pk')
@@ -1038,7 +1046,11 @@ class DeviceView(ObjectView):
         powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'cable', '_path__destination',
         )
-        powerport_table = tables.DevicePowerPortTable(powerports, orderable=False)
+        powerport_table = tables.DevicePowerPortTable(
+            data=powerports,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
             powerport_table.columns.show('pk')
 
@@ -1046,7 +1058,11 @@ class DeviceView(ObjectView):
         poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'cable', 'power_port', '_path__destination',
         )
-        poweroutlet_table = tables.DevicePowerOutletTable(poweroutlets, orderable=False)
+        poweroutlet_table = tables.DevicePowerOutletTable(
+            data=poweroutlets,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
             poweroutlet_table.columns.show('pk')
 
@@ -1056,7 +1072,11 @@ class DeviceView(ObjectView):
             Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
             'lag', 'cable', '_path__destination', 'tags',
         )
-        interface_table = tables.DeviceInterfaceTable(interfaces, orderable=False)
+        interface_table = tables.DeviceInterfaceTable(
+            data=interfaces,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
             interface_table.columns.show('pk')
 
@@ -1064,13 +1084,21 @@ class DeviceView(ObjectView):
         frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'rear_port', 'cable',
         )
-        frontport_table = tables.DeviceFrontPortTable(frontports, orderable=False)
+        frontport_table = tables.DeviceFrontPortTable(
+            data=frontports,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
             frontport_table.columns.show('pk')
 
         # Rear ports
         rearports = RearPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related('cable')
-        rearport_table = tables.DeviceRearPortTable(rearports, orderable=False)
+        rearport_table = tables.DeviceRearPortTable(
+            data=rearports,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
             rearport_table.columns.show('pk')
 
@@ -1078,7 +1106,11 @@ class DeviceView(ObjectView):
         devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
             'installed_device__device_type__manufacturer',
         )
-        devicebay_table = tables.DeviceDeviceBayTable(devicebays, orderable=False)
+        devicebay_table = tables.DeviceDeviceBayTable(
+            data=devicebays,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
             devicebay_table.columns.show('pk')
 
@@ -1086,7 +1118,11 @@ class DeviceView(ObjectView):
         inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter(
             device=device
         ).prefetch_related('manufacturer')
-        inventoryitem_table = tables.DeviceInventoryItemTable(inventoryitems, orderable=False)
+        inventoryitem_table = tables.DeviceInventoryItemTable(
+            data=inventoryitems,
+            user=request.user,
+            orderable=False
+        )
         if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
             devicebay_table.columns.show('pk')
 

+ 0 - 4
netbox/project-static/css/base.css

@@ -193,10 +193,6 @@ table.component-list td.subtable td {
     padding-bottom: 6px;
     padding-top: 6px;
 }
-table.interface-ips th {
-    font-size: 80%;
-    font-weight: normal;
-}
 
 /* Reports */
 table.reports td.method {

+ 47 - 0
netbox/project-static/js/tableconfig.js

@@ -0,0 +1,47 @@
+$(document).ready(function() {
+    $('form.userconfigform input.reset').click(function(event) {
+        // Deselect all columns when the reset button is clicked
+        $('select[name="columns"]').val([]);
+    });
+
+    $('form.userconfigform').submit(function(event) {
+        event.preventDefault();
+
+        // Derive an array from the dotted path to the config root
+        let path = this.getAttribute('data-config-root').split('.');
+        let data = {};
+        let pointer = data;
+
+        // Construct a nested JSON object from the path
+        let node;
+        for (node of path) {
+            pointer[node] = {};
+            pointer = pointer[node];
+        }
+
+        // Assign the form data to the child node
+        let field;
+        $.each($(this).find('[id^="id_"]:input'), function(index, value) {
+            field = $(value);
+            pointer[field.attr("name")] = field.val();
+        });
+
+        // Make the REST API request
+        $.ajax({
+            url: netbox_api_path + 'users/config/',
+            async: true,
+            contentType: 'application/json',
+            dataType: 'json',
+            type: 'PATCH',
+            beforeSend: function(xhr, settings) {
+                xhr.setRequestHeader("X-CSRFToken", netbox_csrf_token);
+            },
+            data: JSON.stringify(data),
+        }).done(function () {
+            // Reload the page
+            window.location.reload(true);
+        }).fail(function (xhr, status, error) {
+            alert("Failed to update user config (" + status + "): " + error);
+        });
+    });
+});

+ 1 - 0
netbox/templates/base.html

@@ -96,6 +96,7 @@
         onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
 <script type="text/javascript">
     var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
+    var netbox_csrf_token = "{{ csrf_token }}";
     var loading = $(".loading");
     $(document).ajaxStart(function() {
         loading.show();

+ 56 - 1
netbox/templates/dcim/device.html

@@ -485,7 +485,12 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Interfaces</strong>
-                                <div class="col-md-2 pull-right noprint">
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceInterfaceTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
+                                <div class="pull-right col-md-2 noprint">
                                     <input class="form-control interface-filter" type="text" placeholder="Filter" title="Filter text (regular expressions supported)" style="height: 23px" />
                                 </div>
                             </div>
@@ -527,6 +532,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Front Ports</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceFrontPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=frontport_table %}
                             <div class="panel-footer noprint">
@@ -564,6 +574,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Rear Ports</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceRearPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=rearport_table %}
                             <div class="panel-footer noprint">
@@ -601,6 +616,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Console Ports</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceConsolePortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=consoleport_table %}
                             <div class="panel-footer noprint">
@@ -638,6 +658,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Console Server Ports</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceConsoleServerPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=consoleserverport_table %}
                             <div class="panel-footer noprint">
@@ -675,6 +700,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Power Ports</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DevicePowerPortTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=powerport_table %}
                             <div class="panel-footer noprint">
@@ -711,6 +741,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Power Outlets</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DevicePowerOutletTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=poweroutlet_table %}
                             <div class="panel-footer noprint">
@@ -748,6 +783,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Device Bays</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceDeviceBayTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=devicebay_table %}
                             <div class="panel-footer noprint">
@@ -779,6 +819,11 @@
                         <div class="panel panel-default">
                             <div class="panel-heading">
                                 <strong>Inventory Items</strong>
+                                <div class="pull-right noprint">
+                                    {% if request.user.is_authenticated %}
+                                        <button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target="#DeviceInventoryItemTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
+                                    {% endif %}
+                                </div>
                             </div>
                             {% include 'responsive_table.html' with table=inventoryitem_table %}
                             <div class="panel-footer noprint">
@@ -811,6 +856,15 @@
         </div>
     </div>
 {% include 'secrets/inc/private_key_modal.html' %}
+{% table_config_form interface_table %}
+{% table_config_form frontport_table %}
+{% table_config_form rearport_table %}
+{% table_config_form consoleport_table %}
+{% table_config_form consoleserverport_table %}
+{% table_config_form powerport_table %}
+{% table_config_form poweroutlet_table %}
+{% table_config_form devicebay_table %}
+{% table_config_form inventoryitem_table %}
 {% endblock %}
 
 {% block javascript %}
@@ -864,4 +918,5 @@ $(".cable-toggle").click(function() {
 </script>
 <script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
+<script src="{% static 'js/tableconfig.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}

+ 7 - 4
netbox/templates/utilities/obj_list.html

@@ -1,12 +1,13 @@
 {% extends 'base.html' %}
 {% load buttons %}
 {% load helpers %}
+{% load static %}
 
 {% block content %}
 <div class="pull-right noprint">
     {% block buttons %}{% endblock %}
     {% if request.user.is_authenticated and 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>
+        <button type="button" class="btn btn-default" data-toggle="modal" data-target="#ObjectTable_config" title="Configure table"><i class="fa fa-cog"></i> Configure</button>
     {% endif %}
     {% if permissions.add and 'add' in action_buttons %}
         {% add_button content_type.model_class|validated_viewname:"add" %}
@@ -71,9 +72,6 @@
         {% endwith %}
         {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
         <div class="clearfix"></div>
-        {% if table_config_form %}
-            {% include 'inc/table_config_form.html' %}
-        {% endif %}
     </div>
     {% if filter_form %}
         <div class="col-md-3 noprint">
@@ -82,4 +80,9 @@
         </div>
     {% endif %}
 </div>
+{% table_config_form table table_name="ObjectTable" %}
+{% endblock %}
+
+{% block javascript %}
+<script src="{% static 'js/tableconfig.js' %}"></script>
 {% endblock %}

+ 4 - 5
netbox/templates/inc/table_config_form.html → netbox/templates/utilities/templatetags/table_config_form.html

@@ -1,5 +1,5 @@
 {% load form_helpers %}
-<div class="modal fade" tabindex="-1" id="tableconfig">
+<div class="modal fade" tabindex="-1" id="{{ table_name }}_config">
     <div class="modal-dialog">
         <div class="modal-content">
             <div class="modal-header">
@@ -7,8 +7,7 @@
                 <h4 class="modal-title">Table Configuration</h4>
             </div>
             <div class="modal-body">
-                <form action="" method="post" class="form-horizontal">
-                    {% csrf_token %}
+                <form class="form-horizontal userconfigform" data-config-root="tables.{{ table_config_form.table_name }}">
                     {% render_form table_config_form %}
                     <div class="row">
                         <div class="col-md-9 col-md-offset-3">
@@ -18,8 +17,8 @@
                         </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" />
+                        <input type="submit" class="btn btn-primary" value="Save" />
+                        <input type="submit" class="btn btn-danger reset" value="Reset" />
                     </div>
                 </form>
             </div>

+ 3 - 0
netbox/users/api/urls.py

@@ -12,5 +12,8 @@ router.register('groups', views.GroupViewSet)
 # Permissions
 router.register('permissions', views.ObjectPermissionViewSet)
 
+# User preferences
+router.register('config', views.UserConfigViewSet, basename='userconfig')
+
 app_name = 'users-api'
 urlpatterns = router.urls

+ 38 - 1
netbox/users/api/views.py

@@ -1,11 +1,15 @@
 from django.contrib.auth.models import Group, User
 from django.db.models import Count
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
 from rest_framework.routers import APIRootView
+from rest_framework.viewsets import ViewSet
 
 from netbox.api.views import ModelViewSet
 from users import filters
-from users.models import ObjectPermission
+from users.models import ObjectPermission, UserConfig
 from utilities.querysets import RestrictedQuerySet
+from utilities.utils import deepmerge
 from . import serializers
 
 
@@ -41,3 +45,36 @@ class ObjectPermissionViewSet(ModelViewSet):
     queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
     serializer_class = serializers.ObjectPermissionSerializer
     filterset_class = filters.ObjectPermissionFilterSet
+
+
+#
+# User preferences
+#
+
+class UserConfigViewSet(ViewSet):
+    """
+    An API endpoint via which a user can update his or her own UserConfig data (but no one else's).
+    """
+    permission_classes = [IsAuthenticated]
+
+    def get_queryset(self):
+        return UserConfig.objects.filter(user=self.request.user)
+
+    def list(self, request):
+        """
+        Return the UserConfig for the currently authenticated User.
+        """
+        userconfig = self.get_queryset().first()
+
+        return Response(userconfig.data)
+
+    def patch(self, request):
+        """
+        Update the UserConfig for the currently authenticated User.
+        """
+        # TODO: How can we validate this data?
+        userconfig = self.get_queryset().first()
+        userconfig.data = deepmerge(userconfig.data, request.data)
+        userconfig.save()
+
+        return Response(userconfig.data)

+ 55 - 3
netbox/users/tests/test_api.py

@@ -1,11 +1,10 @@
 from django.contrib.auth.models import Group, User
 from django.contrib.contenttypes.models import ContentType
-from django.test import override_settings
 from django.urls import reverse
-from rest_framework import status
 
 from users.models import ObjectPermission
-from utilities.testing import APIViewTestCases, APITestCase, disable_warnings
+from utilities.testing import APIViewTestCases, APITestCase
+from utilities.utils import deepmerge
 
 
 class AppTest(APITestCase):
@@ -132,3 +131,56 @@ class ObjectPermissionTest(APIViewTestCases.APIViewTestCase):
                 'constraints': {'name': 'TEST6'},
             },
         ]
+
+
+class UserConfigTest(APITestCase):
+
+    def test_get(self):
+        """
+        Retrieve user configuration via GET request.
+        """
+        userconfig = self.user.config
+        url = reverse('users-api:userconfig-list')
+
+        response = self.client.get(url, **self.header)
+        self.assertEqual(response.data, {})
+
+        data = {
+            "a": 123,
+            "b": 456,
+            "c": 789,
+        }
+        userconfig.data = data
+        userconfig.save()
+        response = self.client.get(url, **self.header)
+        self.assertEqual(response.data, data)
+
+    def test_patch(self):
+        """
+        Set user config via PATCH requests.
+        """
+        userconfig = self.user.config
+        url = reverse('users-api:userconfig-list')
+
+        data = {
+            "a": {
+                "a1": "X",
+                "a2": "Y",
+            },
+            "b": {
+                "b1": "Z",
+            }
+        }
+        response = self.client.patch(url, data=data, format='json', **self.header)
+        self.assertDictEqual(response.data, data)
+        userconfig.refresh_from_db()
+        self.assertDictEqual(userconfig.data, data)
+
+        update_data = {
+            "c": 123
+        }
+        response = self.client.patch(url, data=update_data, format='json', **self.header)
+        new_data = deepmerge(data, update_data)
+        self.assertDictEqual(response.data, new_data)
+        userconfig.refresh_from_db()
+        self.assertDictEqual(userconfig.data, new_data)

+ 7 - 0
netbox/utilities/forms/forms.py

@@ -161,6 +161,7 @@ class TableConfigForm(BootstrapMixin, forms.Form):
     """
     columns = forms.MultipleChoiceField(
         choices=[],
+        required=False,
         widget=forms.SelectMultiple(
             attrs={'size': 10}
         ),
@@ -168,8 +169,14 @@ class TableConfigForm(BootstrapMixin, forms.Form):
     )
 
     def __init__(self, table, *args, **kwargs):
+        self.table = table
+
         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
+
+    @property
+    def table_name(self):
+        return self.table.__class__.__name__

+ 27 - 23
netbox/utilities/tables.py

@@ -1,4 +1,5 @@
 import django_tables2 as tables
+from django.contrib.auth.models import AnonymousUser
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.core.exceptions import FieldDoesNotExist
 from django.db.models.fields.related import RelatedField
@@ -11,10 +12,11 @@ class BaseTable(tables.Table):
     """
     Default table for object lists
 
-    :param add_prefetch: By default, modify the queryset passed to the table upon initialization to automatically
-      prefetch related data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to
-      accommodate PrefixQuerySet.annotate_depth()).
+    :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
     """
+    # By default, modify the queryset passed to the table upon initialization to automatically prefetch related
+    # data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to accommodate
+    # PrefixQuerySet.annotate_depth()).
     add_prefetch = True
 
     class Meta:
@@ -22,7 +24,7 @@ class BaseTable(tables.Table):
             'class': 'table table-hover table-headings',
         }
 
-    def __init__(self, *args, columns=None, **kwargs):
+    def __init__(self, *args, user=None, **kwargs):
         super().__init__(*args, **kwargs)
 
         # Set default empty_text if none was provided
@@ -36,25 +38,27 @@ class BaseTable(tables.Table):
                 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 = [c for c in columns if c in self.base_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')
+        # Apply custom column ordering for user
+        if user is not None and not isinstance(user, AnonymousUser):
+            columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
+            if columns:
+                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 = [c for c in columns if c in self.base_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 self.add_prefetch and isinstance(self.data, TableQuerysetData):

+ 9 - 0
netbox/utilities/templatetags/helpers.py

@@ -10,6 +10,7 @@ from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
 from markdown import markdown
 
+from utilities.forms import TableConfigForm
 from utilities.utils import foreground_color
 
 register = template.Library()
@@ -261,3 +262,11 @@ def badge(value, show_empty=False):
         'value': value,
         'show_empty': show_empty,
     }
+
+
+@register.inclusion_tag('utilities/templatetags/table_config_form.html')
+def table_config_form(table, table_name=None):
+    return {
+        'table_name': table_name or table.__class__.__name__,
+        'table_config_form': TableConfigForm(table=table),
+    }

+ 2 - 23
netbox/utilities/views.py

@@ -289,12 +289,8 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
             perm_name = get_permission_for_model(model, action)
             permissions[action] = request.user.has_perm(perm_name)
 
-        # Construct the table based on the user's permissions
-        if request.user.is_authenticated:
-            columns = request.user.config.get(f"tables.{self.table.__name__}.columns")
-        else:
-            columns = None
-        table = self.table(self.queryset, columns=columns)
+        # Construct the objects table
+        table = self.table(self.queryset, user=request.user)
         if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
             table.columns.show('pk')
 
@@ -317,23 +313,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
 
         return render(request, self.template_name, context)
 
-    @method_decorator(login_required)
-    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 extra_context(self):
         return {}