1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #6954: Remember users' table ordering preferences

This commit is contained in:
jeremystretch
2022-01-10 14:03:07 -05:00
parent 72e17914e2
commit 21e0e6e495
12 changed files with 84 additions and 49 deletions

View File

@ -4,9 +4,10 @@ The `users.UserConfig` model holds individual preferences for each user in the f
## Available Preferences ## Available Preferences
| Name | Description | | Name | Description |
|-------------------------|-------------| |--------------------------|---------------------------------------------------------------|
| data_format | Preferred format when rendering raw data (JSON or YAML) | | data_format | Preferred format when rendering raw data (JSON or YAML) |
| pagination.per_page | The number of items to display per page of a paginated table | | pagination.per_page | The number of items to display per page of a paginated table |
| tables.${table}.columns | The ordered list of columns to display when viewing the table | | tables.${table}.columns | The ordered list of columns to display when viewing the table |
| ui.colormode | Light or dark mode in the user interface | | tables.${table}.ordering | A list of column names by which the table should be ordered |
| ui.colormode | Light or dark mode in the user interface |

View File

@ -56,6 +56,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
### Enhancements ### Enhancements
* [#6954](https://github.com/netbox-community/netbox/issues/6954) - Remember users' table ordering preferences
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation * [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
* [#7679](https://github.com/netbox-community/netbox/issues/7679) - Add actions menu to all object tables * [#7679](https://github.com/netbox-community/netbox/issues/7679) - Add actions menu to all object tables
* [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks * [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks

View File

@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import CircuitTerminationSideChoices from .choices import CircuitTerminationSideChoices
@ -35,7 +35,7 @@ class ProviderView(generic.ObjectView):
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
) )
circuits_table = tables.CircuitTable(circuits, exclude=('provider',)) circuits_table = tables.CircuitTable(circuits, exclude=('provider',))
paginate_table(circuits_table, request) configure_table(circuits_table, request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -96,7 +96,7 @@ class ProviderNetworkView(generic.ObjectView):
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
) )
circuits_table = tables.CircuitTable(circuits) circuits_table = tables.CircuitTable(circuits)
paginate_table(circuits_table, request) configure_table(circuits_table, request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -150,7 +150,7 @@ class CircuitTypeView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance) circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
circuits_table = tables.CircuitTable(circuits, exclude=('type',)) circuits_table = tables.CircuitTable(circuits, exclude=('type',))
paginate_table(circuits_table, request) configure_table(circuits_table, request)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,

View File

@ -20,7 +20,7 @@ from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -165,7 +165,7 @@ class RegionView(generic.ObjectView):
region=instance region=instance
) )
sites_table = tables.SiteTable(sites, exclude=('region',)) sites_table = tables.SiteTable(sites, exclude=('region',))
paginate_table(sites_table, request) configure_table(sites_table, request)
return { return {
'child_regions_table': child_regions_table, 'child_regions_table': child_regions_table,
@ -250,7 +250,7 @@ class SiteGroupView(generic.ObjectView):
group=instance group=instance
) )
sites_table = tables.SiteTable(sites, exclude=('group',)) sites_table = tables.SiteTable(sites, exclude=('group',))
paginate_table(sites_table, request) configure_table(sites_table, request)
return { return {
'child_groups_table': child_groups_table, 'child_groups_table': child_groups_table,
@ -422,7 +422,7 @@ class LocationView(generic.ObjectView):
cumulative=True cumulative=True
).filter(pk__in=location_ids).exclude(pk=instance.pk) ).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations) child_locations_table = tables.LocationTable(child_locations)
paginate_table(child_locations_table, request) configure_table(child_locations_table, request)
return { return {
'rack_count': rack_count, 'rack_count': rack_count,
@ -493,7 +493,7 @@ class RackRoleView(generic.ObjectView):
) )
racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization')) racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization'))
paginate_table(racks_table, request) configure_table(racks_table, request)
return { return {
'racks_table': racks_table, 'racks_table': racks_table,
@ -743,7 +743,7 @@ class ManufacturerView(generic.ObjectView):
) )
devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',)) devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',))
paginate_table(devicetypes_table, request) configure_table(devicetypes_table, request)
return { return {
'devicetypes_table': devicetypes_table, 'devicetypes_table': devicetypes_table,
@ -1439,7 +1439,7 @@ class DeviceRoleView(generic.ObjectView):
device_role=instance device_role=instance
) )
devices_table = tables.DeviceTable(devices, exclude=('device_role',)) devices_table = tables.DeviceTable(devices, exclude=('device_role',))
paginate_table(devices_table, request) configure_table(devices_table, request)
return { return {
'devices_table': devices_table, 'devices_table': devices_table,
@ -1503,7 +1503,7 @@ class PlatformView(generic.ObjectView):
platform=instance platform=instance
) )
devices_table = tables.DeviceTable(devices, exclude=('platform',)) devices_table = tables.DeviceTable(devices, exclude=('platform',))
paginate_table(devices_table, request) configure_table(devices_table, request)
return { return {
'devices_table': devices_table, 'devices_table': devices_table,

View File

@ -11,7 +11,7 @@ from rq import Worker
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import is_htmx from utilities.htmx import is_htmx
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin from utilities.views import ContentTypePermissionRequiredMixin
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -215,7 +215,7 @@ class TagView(generic.ObjectView):
data=tagged_items, data=tagged_items,
orderable=False orderable=False
) )
paginate_table(taggeditem_table, request) configure_table(taggeditem_table, request)
object_types = [ object_types = [
{ {
@ -451,7 +451,7 @@ class ObjectChangeLogView(View):
data=objectchanges, data=objectchanges,
orderable=False orderable=False
) )
paginate_table(objectchanges_table, request) configure_table(objectchanges_table, request)
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise, # Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
# fall back to using base.html. # fall back to using base.html.
@ -571,7 +571,7 @@ class ObjectJournalView(View):
assigned_object_id=obj.pk assigned_object_id=obj.pk
) )
journalentry_table = tables.ObjectJournalTable(journalentries) journalentry_table = tables.ObjectJournalTable(journalentries)
paginate_table(journalentry_table, request) configure_table(journalentry_table, request)
if request.user.has_perm('extras.add_journalentry'): if request.user.has_perm('extras.add_journalentry'):
form = forms.JournalEntryForm( form = forms.JournalEntryForm(

View File

@ -8,7 +8,7 @@ from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site from dcim.models import Interface, Site
from dcim.tables import SiteTable from dcim.tables import SiteTable
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface from virtualization.models import VMInterface
@ -161,7 +161,7 @@ class RIRView(generic.ObjectView):
rir=instance rir=instance
) )
aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization')) aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization'))
paginate_table(aggregates_table, request) configure_table(aggregates_table, request)
return { return {
'aggregates_table': aggregates_table, 'aggregates_table': aggregates_table,
@ -219,7 +219,7 @@ class ASNView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
sites = instance.sites.restrict(request.user, 'view') sites = instance.sites.restrict(request.user, 'view')
sites_table = SiteTable(sites) sites_table = SiteTable(sites)
paginate_table(sites_table, request) configure_table(sites_table, request)
return { return {
'sites_table': sites_table, 'sites_table': sites_table,
@ -356,7 +356,7 @@ class RoleView(generic.ObjectView):
) )
prefixes_table = tables.PrefixTable(prefixes, exclude=('role', 'utilization')) prefixes_table = tables.PrefixTable(prefixes, exclude=('role', 'utilization'))
paginate_table(prefixes_table, request) configure_table(prefixes_table, request)
return { return {
'prefixes_table': prefixes_table, 'prefixes_table': prefixes_table,
@ -664,7 +664,7 @@ class IPAddressView(generic.ObjectView):
vrf=instance.vrf, address__net_contained_or_equal=str(instance.address) vrf=instance.vrf, address__net_contained_or_equal=str(instance.address)
) )
related_ips_table = tables.IPAddressTable(related_ips, orderable=False) related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
paginate_table(related_ips_table, request) configure_table(related_ips_table, request)
return { return {
'parent_prefixes_table': parent_prefixes_table, 'parent_prefixes_table': parent_prefixes_table,
@ -800,7 +800,7 @@ class VLANGroupView(generic.ObjectView):
vlans_table = tables.VLANTable(vlans, exclude=('site', 'group', 'prefixes')) vlans_table = tables.VLANTable(vlans, exclude=('site', 'group', 'prefixes'))
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'): if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
vlans_table.columns.show('pk') vlans_table.columns.show('pk')
paginate_table(vlans_table, request) configure_table(vlans_table, request)
# Compile permissions list for rendering the object table # Compile permissions list for rendering the object table
permissions = { permissions = {

View File

@ -15,7 +15,6 @@ from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from django_tables2.export import TableExport from django_tables2.export import TableExport
from dcim.forms.object_create import ComponentCreateForm
from extras.models import ExportTemplate from extras.models import ExportTemplate
from extras.signals import clear_webhooks from extras.signals import clear_webhooks
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
@ -23,7 +22,7 @@ from utilities.exceptions import AbortTransaction, PermissionsViolation
from utilities.forms import ConfirmationForm, ImportForm, restrict_form_fields from utilities.forms import ConfirmationForm, ImportForm, restrict_form_fields
from utilities.htmx import is_htmx from utilities.htmx import is_htmx
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import normalize_querydict, prepare_cloned_fields from utilities.utils import normalize_querydict, prepare_cloned_fields
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
@ -135,7 +134,7 @@ class ObjectChildrenView(ObjectView):
# Determine whether to display bulk action checkboxes # Determine whether to display bulk action checkboxes
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']): if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
table.columns.show('pk') table.columns.show('pk')
paginate_table(table, request) configure_table(table, request)
# If this is an HTMX request, return only the rendered table HTML # If this is an HTMX request, return only the rendered table HTML
if is_htmx(request): if is_htmx(request):
@ -284,7 +283,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
# Render the objects table # Render the objects table
table = self.get_table(request, permissions) table = self.get_table(request, permissions)
paginate_table(table, request) configure_table(table, request)
# If this is an HTMX request, return only the rendered table HTML # If this is an HTMX request, return only the rendered table HTML
if is_htmx(request): if is_htmx(request):

View File

@ -6,7 +6,7 @@ from circuits.models import Circuit
from dcim.models import Site, Rack, Device, RackReservation from dcim.models import Site, Rack, Device, RackReservation
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -38,7 +38,7 @@ class TenantGroupView(generic.ObjectView):
group=instance group=instance
) )
tenants_table = tables.TenantTable(tenants, exclude=('group',)) tenants_table = tables.TenantTable(tenants, exclude=('group',))
paginate_table(tenants_table, request) configure_table(tenants_table, request)
return { return {
'tenants_table': tenants_table, 'tenants_table': tenants_table,
@ -184,7 +184,7 @@ class ContactGroupView(generic.ObjectView):
group=instance group=instance
) )
contacts_table = tables.ContactTable(contacts, exclude=('group',)) contacts_table = tables.ContactTable(contacts, exclude=('group',))
paginate_table(contacts_table, request) configure_table(contacts_table, request)
return { return {
'child_groups_table': child_groups_table, 'child_groups_table': child_groups_table,
@ -251,7 +251,7 @@ class ContactRoleView(generic.ObjectView):
) )
contacts_table = tables.ContactAssignmentTable(contact_assignments) contacts_table = tables.ContactAssignmentTable(contact_assignments)
contacts_table.columns.hide('role') contacts_table.columns.hide('role')
paginate_table(contacts_table, request) configure_table(contacts_table, request)
return { return {
'contacts_table': contacts_table, 'contacts_table': contacts_table,
@ -308,7 +308,7 @@ class ContactView(generic.ObjectView):
) )
assignments_table = tables.ContactAssignmentTable(contact_assignments) assignments_table = tables.ContactAssignmentTable(contact_assignments)
assignments_table.columns.hide('contact') assignments_table.columns.hide('contact')
paginate_table(assignments_table, request) configure_table(assignments_table, request)
return { return {
'assignments_table': assignments_table, 'assignments_table': assignments_table,

View File

@ -1,7 +1,13 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings, TestCase from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from dcim.models import Site
from dcim.tables import SiteTable
from users.preferences import UserPreference from users.preferences import UserPreference
from utilities.tables import configure_table
from utilities.testing import TestCase
DEFAULT_USER_PREFERENCES = { DEFAULT_USER_PREFERENCES = {
@ -12,6 +18,7 @@ DEFAULT_USER_PREFERENCES = {
class UserPreferencesTest(TestCase): class UserPreferencesTest(TestCase):
user_permissions = ['dcim.view_site']
def test_userpreference(self): def test_userpreference(self):
CHOICES = ( CHOICES = (
@ -37,3 +44,21 @@ class UserPreferencesTest(TestCase):
userconfig = user.config userconfig = user.config
self.assertEqual(userconfig.data, DEFAULT_USER_PREFERENCES) self.assertEqual(userconfig.data, DEFAULT_USER_PREFERENCES)
def test_table_ordering(self):
url = reverse('dcim:site_list')
response = self.client.get(f"{url}?sort=status")
self.assertEqual(response.status_code, 200)
# Check that table ordering preference has been recorded
self.user.refresh_from_db()
ordering = self.user.config.get(f'tables.SiteTable.ordering')
self.assertEqual(ordering, ['status'])
# Check that a recorded preference is honored by default
self.user.config.set(f'tables.SiteTable.ordering', ['-status'], commit=True)
table = SiteTable(Site.objects.all())
request = RequestFactory().get(url)
request.user = self.user
configure_table(table, request)
self.assertEqual(table.order_by, ('-status',))

View File

@ -5,14 +5,23 @@ from .columns import *
from .tables import * from .tables import *
# def configure_table(table, request):
# Pagination
#
def paginate_table(table, request):
""" """
Paginate a table given a request context. Paginate a table given a request context.
""" """
# Save ordering preference
if request.user.is_authenticated:
table_name = table.__class__.__name__
if table.prefixed_order_by_field in request.GET:
# If an ordering has been specified as a query parameter, save it as the
# user's preferred ordering for this table.
ordering = request.GET.getlist(table.prefixed_order_by_field)
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
# If no ordering has been specified, set the preferred ordering (if any).
table.order_by = ordering
# Paginate the table results
paginate = { paginate = {
'paginator_class': EnhancedPaginator, 'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request) 'per_page': get_paginate_count(request)

View File

@ -11,7 +11,7 @@ from extras.views import ObjectConfigContextView
from ipam.models import IPAddress, Service from ipam.models import IPAddress, Service
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -41,7 +41,7 @@ class ClusterTypeView(generic.ObjectView):
vm_count=count_related(VirtualMachine, 'cluster') vm_count=count_related(VirtualMachine, 'cluster')
) )
clusters_table = tables.ClusterTable(clusters, exclude=('type',)) clusters_table = tables.ClusterTable(clusters, exclude=('type',))
paginate_table(clusters_table, request) configure_table(clusters_table, request)
return { return {
'clusters_table': clusters_table, 'clusters_table': clusters_table,
@ -103,7 +103,7 @@ class ClusterGroupView(generic.ObjectView):
vm_count=count_related(VirtualMachine, 'cluster') vm_count=count_related(VirtualMachine, 'cluster')
) )
clusters_table = tables.ClusterTable(clusters, exclude=('group',)) clusters_table = tables.ClusterTable(clusters, exclude=('group',))
paginate_table(clusters_table, request) configure_table(clusters_table, request)
return { return {
'clusters_table': clusters_table, 'clusters_table': clusters_table,

View File

@ -1,6 +1,6 @@
from dcim.models import Interface from dcim.models import Interface
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import configure_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -31,7 +31,7 @@ class WirelessLANGroupView(generic.ObjectView):
group=instance group=instance
) )
wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',)) wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',))
paginate_table(wirelesslans_table, request) configure_table(wirelesslans_table, request)
return { return {
'wirelesslans_table': wirelesslans_table, 'wirelesslans_table': wirelesslans_table,
@ -99,7 +99,7 @@ class WirelessLANView(generic.ObjectView):
wireless_lans=instance wireless_lans=instance
) )
interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces) interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces)
paginate_table(interfaces_table, request) configure_table(interfaces_table, request)
return { return {
'interfaces_table': interfaces_table, 'interfaces_table': interfaces_table,