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

Refactor table utilities

This commit is contained in:
jeremystretch
2022-01-07 09:12:48 -05:00
parent 58f7eb319f
commit 00a8fd654e
3 changed files with 188 additions and 168 deletions

View File

@ -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}"

View File

@ -2,150 +2,33 @@ 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):
"""
@ -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}"

View File

@ -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