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

279 lines
9.8 KiB
Python

from copy import deepcopy
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
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_tables2.data import TableQuerysetData
from core.models import ObjectType
from extras.choices import *
from extras.models import CustomField, CustomLink
from netbox.registry import registry
from netbox.tables import columns
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.utils import get_viewname, highlight_string, title
from .template_code import *
__all__ = (
'BaseTable',
'NetBoxTable',
'SearchTable',
)
class BaseTable(tables.Table):
"""
Base table class for NetBox objects. Adds support for:
* User configuration (column preferences)
* Automatic prefetching of related objects
* BS5 styling
:param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
"""
exempt_columns = ()
class Meta:
attrs = {
'class': 'table table-hover object-list',
}
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **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"
# Determine the table columns to display by checking the following:
# 1. User's configuration for the table
# 2. Meta.default_columns
# 3. Meta.fields
selected_columns = None
if user is not None and not isinstance(user, AnonymousUser):
selected_columns = user.config.get(f"tables.{self.name}.columns")
if not selected_columns:
selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields)
# Hide non-selected columns which are not exempt
for column in self.columns:
if column.name not in [*selected_columns, *self.exempt_columns]:
self.columns.hide(column.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(*prefetch_fields)
def _get_columns(self, visible=True):
columns = []
for name, column in self.columns.items():
if column.visible == visible and name not in self.exempt_columns:
columns.append((name, column.verbose_name))
return columns
@property
def name(self):
return self.__class__.__name__
@property
def available_columns(self):
return sorted(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
def configure(self, request):
"""
Configure the table for a specific request context. This performs pagination and records
the user's preferred ordering logic.
"""
# Save ordering preference
if request.user.is_authenticated:
if self.prefixed_order_by_field in request.GET:
if request.GET[self.prefixed_order_by_field]:
# 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(self.prefixed_order_by_field)
request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True)
else:
# If the ordering has been set to none (empty), clear any existing preference.
request.user.config.clear(f'tables.{self.name}.ordering', commit=True)
elif ordering := request.user.config.get(f'tables.{self.name}.ordering'):
# If no ordering has been specified, set the preferred ordering (if any).
self.order_by = ordering
# Paginate the table results
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request)
}
tables.RequestConfig(request, paginate).configure(self)
class NetBoxTable(BaseTable):
"""
Table class for most NetBox objects. Adds support for custom field & custom link columns. Includes
default columns for:
* PK (row selection)
* ID
* Actions
"""
pk = columns.ToggleColumn(
visible=False
)
id = tables.Column(
linkify=True,
verbose_name=_('ID')
)
actions = columns.ActionsColumn()
exempt_columns = ('pk', 'actions')
class Meta(BaseTable.Meta):
pass
def __init__(self, *args, extra_columns=None, **kwargs):
if extra_columns is None:
extra_columns = []
if registered_columns := registry['tables'].get(self.__class__):
extra_columns.extend([
# Create a copy to avoid modifying the original Column
(name, deepcopy(column)) for name, column in registered_columns.items()
])
# Add custom field & custom link columns
object_type = ObjectType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(
object_types=object_type
).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
extra_columns.extend([
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
])
custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True)
extra_columns.extend([
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
])
super().__init__(*args, extra_columns=extra_columns, **kwargs)
@property
def htmx_url(self):
"""
Return the base HTML request URL for embedded tables.
"""
if getattr(self, 'embedded', False):
viewname = get_viewname(self._meta.model, action='list')
try:
return reverse(viewname)
except NoReverseMatch:
pass
return ''
class SearchTable(tables.Table):
object_type = columns.ContentTypeColumn(
verbose_name=_('Type'),
order_by="object___meta__verbose_name",
)
object = tables.Column(
verbose_name=_('Object'),
linkify=True,
order_by=('name', )
)
field = tables.Column(
verbose_name=_('Field'),
)
value = tables.Column(
verbose_name=_('Value'),
)
attrs = columns.TemplateColumn(
template_code=SEARCH_RESULT_ATTRS,
verbose_name=_('Attributes')
)
trim_length = 30
class Meta:
attrs = {
'class': 'table table-hover object-list',
}
empty_text = _('No results found')
def __init__(self, data, highlight=None, **kwargs):
self.highlight = highlight
super().__init__(data, **kwargs)
def render_field(self, value, record):
try:
model_field = record.object._meta.get_field(value)
return title(model_field.verbose_name)
except FieldDoesNotExist:
return value
def render_value(self, value):
if not self.highlight:
return value
value = highlight_string(value, self.highlight, trim_pre=self.trim_length, trim_post=self.trim_length)
return mark_safe(value)