Przeglądaj źródła

Refactor table utilities

jeremystretch 4 lat temu
rodzic
commit
00a8fd654e

+ 30 - 0
netbox/utilities/tables/__init__.py

@@ -0,0 +1,30 @@
+from django_tables2 import RequestConfig
+
+from utilities.paginator import EnhancedPaginator, get_paginate_count
+from .columns import *
+from .tables import *
+
+
+#
+# Pagination
+#
+
+def paginate_table(table, request):
+    """
+    Paginate a table given a request context.
+    """
+    paginate = {
+        'paginator_class': EnhancedPaginator,
+        'per_page': get_paginate_count(request)
+    }
+    RequestConfig(request, paginate).configure(table)
+
+
+#
+# Callables
+#
+
+def linkify_phone(value):
+    if value is None:
+        return None
+    return f"tel:{value}"

+ 21 - 169
netbox/utilities/tables.py → netbox/utilities/tables/columns.py

@@ -2,151 +2,34 @@ from collections import namedtuple
 
 import django_tables2 as tables
 from django.conf import settings
-from django.contrib.auth.models import AnonymousUser
-from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import FieldDoesNotExist
-from django.db.models.fields.related import RelatedField
 from django.urls import reverse
 from django.utils.safestring import mark_safe
-from django_tables2 import RequestConfig
-from django_tables2.data import TableQuerysetData
 from django_tables2.utils import Accessor
 
 from extras.choices import CustomFieldTypeChoices
-from extras.models import CustomField, CustomLink
-from .utils import content_type_identifier, content_type_name
-from .paginator import EnhancedPaginator, get_paginate_count
+from utilities.utils import content_type_identifier, content_type_name
+
+__all__ = (
+    'ActionsColumn',
+    'BooleanColumn',
+    'ButtonsColumn',
+    'ChoiceFieldColumn',
+    'ColorColumn',
+    'ColoredLabelColumn',
+    'ContentTypeColumn',
+    'ContentTypesColumn',
+    'CustomFieldColumn',
+    'CustomLinkColumn',
+    'LinkedCountColumn',
+    'MarkdownColumn',
+    'MPTTColumn',
+    'TagColumn',
+    'TemplateColumn',
+    'ToggleColumn',
+    'UtilizationColumn',
+)
 
 
-class BaseTable(tables.Table):
-    """
-    Default table for object lists
-
-    :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
-    """
-    id = tables.Column(
-        linkify=True,
-        verbose_name='ID'
-    )
-
-    class Meta:
-        attrs = {
-            'class': 'table table-hover object-list',
-        }
-
-    def __init__(self, *args, user=None, extra_columns=None, **kwargs):
-        if extra_columns is None:
-            extra_columns = []
-
-        # Add custom field columns
-        obj_type = ContentType.objects.get_for_model(self._meta.model)
-        cf_columns = [
-            (f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
-        ]
-        cl_columns = [
-            (f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
-        ]
-        extra_columns.extend([*cf_columns, *cl_columns])
-
-        super().__init__(*args, extra_columns=extra_columns, **kwargs)
-
-        # Set default empty_text if none was provided
-        if self.empty_text is None:
-            self.empty_text = f"No {self._meta.model._meta.verbose_name_plural} found"
-
-        # Hide non-default columns
-        default_columns = getattr(self.Meta, 'default_columns', list())
-        if default_columns:
-            for column in self.columns:
-                if column.name not in default_columns:
-                    self.columns.hide(column.name)
-
-        # Apply custom column ordering for user
-        if user is not None and not isinstance(user, AnonymousUser):
-            selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
-            if selected_columns:
-
-                # Show only persistent or selected columns
-                for name, column in self.columns.items():
-                    if name in ['pk', 'actions', *selected_columns]:
-                        self.columns.show(name)
-                    else:
-                        self.columns.hide(name)
-
-                # Rearrange the sequence to list selected columns first, followed by all remaining columns
-                # TODO: There's probably a more clever way to accomplish this
-                self.sequence = [
-                    *[c for c in selected_columns if c in self.columns.names()],
-                    *[c for c in self.columns.names() if c not in selected_columns]
-                ]
-
-                # PK column should always come first
-                if 'pk' in self.sequence:
-                    self.sequence.remove('pk')
-                    self.sequence.insert(0, 'pk')
-
-                # Actions column should always come last
-                if 'actions' in self.sequence:
-                    self.sequence.remove('actions')
-                    self.sequence.append('actions')
-
-        # Dynamically update the table's QuerySet to ensure related fields are pre-fetched
-        if isinstance(self.data, TableQuerysetData):
-
-            prefetch_fields = []
-            for column in self.columns:
-                if column.visible:
-                    model = getattr(self.Meta, 'model')
-                    accessor = column.accessor
-                    prefetch_path = []
-                    for field_name in accessor.split(accessor.SEPARATOR):
-                        try:
-                            field = model._meta.get_field(field_name)
-                        except FieldDoesNotExist:
-                            break
-                        if isinstance(field, RelatedField):
-                            # Follow ForeignKeys to the related model
-                            prefetch_path.append(field_name)
-                            model = field.remote_field.model
-                        elif isinstance(field, GenericForeignKey):
-                            # Can't prefetch beyond a GenericForeignKey
-                            prefetch_path.append(field_name)
-                            break
-                    if prefetch_path:
-                        prefetch_fields.append('__'.join(prefetch_path))
-            self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields)
-
-    def _get_columns(self, visible=True):
-        columns = []
-        for name, column in self.columns.items():
-            if column.visible == visible and name not in ['pk', 'actions']:
-                columns.append((name, column.verbose_name))
-        return columns
-
-    @property
-    def available_columns(self):
-        return self._get_columns(visible=False)
-
-    @property
-    def selected_columns(self):
-        return self._get_columns(visible=True)
-
-    @property
-    def objects_count(self):
-        """
-        Return the total number of real objects represented by the Table. This is useful when dealing with
-        prefixes/IP addresses/etc., where some table rows may represent available address space.
-        """
-        if not hasattr(self, '_objects_count'):
-            self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
-        return self._objects_count
-
-
-#
-# Table columns
-#
-
 class ToggleColumn(tables.CheckBoxColumn):
     """
     Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
@@ -557,34 +440,3 @@ class MarkdownColumn(tables.TemplateColumn):
 
     def value(self, value):
         return value
-
-
-#
-# Pagination
-#
-
-def paginate_table(table, request):
-    """
-    Paginate a table given a request context.
-    """
-    paginate = {
-        'paginator_class': EnhancedPaginator,
-        'per_page': get_paginate_count(request)
-    }
-    RequestConfig(request, paginate).configure(table)
-
-
-#
-# Callables
-#
-
-def linkify_email(value):
-    if value is None:
-        return None
-    return f"mailto:{value}"
-
-
-def linkify_phone(value):
-    if value is None:
-        return None
-    return f"tel:{value}"

+ 138 - 0
netbox/utilities/tables/tables.py

@@ -0,0 +1,138 @@
+import django_tables2 as tables
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import FieldDoesNotExist
+from django.db.models.fields.related import RelatedField
+from django_tables2.data import TableQuerysetData
+
+from extras.models import CustomField, CustomLink
+from . import columns
+
+__all__ = (
+    'BaseTable',
+)
+
+
+class BaseTable(tables.Table):
+    """
+    Default table for object lists
+
+    :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
+    """
+    id = tables.Column(
+        linkify=True,
+        verbose_name='ID'
+    )
+
+    class Meta:
+        attrs = {
+            'class': 'table table-hover object-list',
+        }
+
+    def __init__(self, *args, user=None, extra_columns=None, **kwargs):
+        if extra_columns is None:
+            extra_columns = []
+
+        # Add custom field columns
+        obj_type = ContentType.objects.get_for_model(self._meta.model)
+        cf_columns = [
+            (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
+        ]
+        cl_columns = [
+            (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
+        ]
+        extra_columns.extend([*cf_columns, *cl_columns])
+
+        super().__init__(*args, extra_columns=extra_columns, **kwargs)
+
+        # Set default empty_text if none was provided
+        if self.empty_text is None:
+            self.empty_text = f"No {self._meta.model._meta.verbose_name_plural} found"
+
+        # Hide non-default columns
+        default_columns = getattr(self.Meta, 'default_columns', list())
+        if default_columns:
+            for column in self.columns:
+                if column.name not in default_columns:
+                    self.columns.hide(column.name)
+
+        # Apply custom column ordering for user
+        if user is not None and not isinstance(user, AnonymousUser):
+            selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
+            if selected_columns:
+
+                # Show only persistent or selected columns
+                for name, column in self.columns.items():
+                    if name in ['pk', 'actions', *selected_columns]:
+                        self.columns.show(name)
+                    else:
+                        self.columns.hide(name)
+
+                # Rearrange the sequence to list selected columns first, followed by all remaining columns
+                # TODO: There's probably a more clever way to accomplish this
+                self.sequence = [
+                    *[c for c in selected_columns if c in self.columns.names()],
+                    *[c for c in self.columns.names() if c not in selected_columns]
+                ]
+
+                # PK column should always come first
+                if 'pk' in self.sequence:
+                    self.sequence.remove('pk')
+                    self.sequence.insert(0, 'pk')
+
+                # Actions column should always come last
+                if 'actions' in self.sequence:
+                    self.sequence.remove('actions')
+                    self.sequence.append('actions')
+
+        # Dynamically update the table's QuerySet to ensure related fields are pre-fetched
+        if isinstance(self.data, TableQuerysetData):
+
+            prefetch_fields = []
+            for column in self.columns:
+                if column.visible:
+                    model = getattr(self.Meta, 'model')
+                    accessor = column.accessor
+                    prefetch_path = []
+                    for field_name in accessor.split(accessor.SEPARATOR):
+                        try:
+                            field = model._meta.get_field(field_name)
+                        except FieldDoesNotExist:
+                            break
+                        if isinstance(field, RelatedField):
+                            # Follow ForeignKeys to the related model
+                            prefetch_path.append(field_name)
+                            model = field.remote_field.model
+                        elif isinstance(field, GenericForeignKey):
+                            # Can't prefetch beyond a GenericForeignKey
+                            prefetch_path.append(field_name)
+                            break
+                    if prefetch_path:
+                        prefetch_fields.append('__'.join(prefetch_path))
+            self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields)
+
+    def _get_columns(self, visible=True):
+        columns = []
+        for name, column in self.columns.items():
+            if column.visible == visible and name not in ['pk', 'actions']:
+                columns.append((name, column.verbose_name))
+        return columns
+
+    @property
+    def available_columns(self):
+        return self._get_columns(visible=False)
+
+    @property
+    def selected_columns(self):
+        return self._get_columns(visible=True)
+
+    @property
+    def objects_count(self):
+        """
+        Return the total number of real objects represented by the Table. This is useful when dealing with
+        prefixes/IP addresses/etc., where some table rows may represent available address space.
+        """
+        if not hasattr(self, '_objects_count'):
+            self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
+        return self._objects_count