from dataclasses import dataclass from typing import Optional import django_tables2 as tables from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.template import Context, Template from django.urls import reverse from django.utils.safestring import mark_safe from django_tables2.utils import Accessor from extras.choices import CustomFieldTypeChoices from utilities.utils import content_type_identifier, content_type_name __all__ = ( 'ActionsColumn', 'BooleanColumn', 'ChoiceFieldColumn', 'ColorColumn', 'ColoredLabelColumn', 'ContentTypeColumn', 'ContentTypesColumn', 'CustomFieldColumn', 'CustomLinkColumn', 'LinkedCountColumn', 'MarkdownColumn', 'MPTTColumn', 'TagColumn', 'TemplateColumn', 'ToggleColumn', 'UtilizationColumn', ) class ToggleColumn(tables.CheckBoxColumn): """ Extend CheckBoxColumn to add a "toggle all" checkbox in the column header. """ def __init__(self, *args, **kwargs): default = kwargs.pop('default', '') visible = kwargs.pop('visible', False) if 'attrs' not in kwargs: kwargs['attrs'] = { 'td': { 'class': 'min-width', }, 'input': { 'class': 'form-check-input' } } super().__init__(*args, default=default, visible=visible, **kwargs) @property def header(self): return mark_safe('') class BooleanColumn(tables.Column): """ Custom implementation of BooleanColumn to render a nicely-formatted checkmark or X icon instead of a Unicode character. """ def render(self, value): if value: rendered = '' elif value is None: rendered = '' else: rendered = '' return mark_safe(rendered) def value(self, value): return str(value) class TemplateColumn(tables.TemplateColumn): """ Overrides the stock TemplateColumn to render a placeholder if the returned value is an empty string. """ PLACEHOLDER = mark_safe('—') def render(self, *args, **kwargs): ret = super().render(*args, **kwargs) if not ret.strip(): return self.PLACEHOLDER return ret def value(self, **kwargs): ret = super().value(**kwargs) if ret == self.PLACEHOLDER: return '' return ret @dataclass class ActionsItem: title: str icon: str permission: Optional[str] = None class ActionsColumn(tables.Column): """ A dropdown menu which provides edit, delete, and changelog links for an object. Can optionally include additional buttons rendered from a template string. :param sequence: The ordered list of dropdown menu items to include :param extra_buttons: A Django template string which renders additional buttons preceding the actions dropdown """ attrs = {'td': {'class': 'text-end text-nowrap noprint'}} empty_values = () actions = { 'edit': ActionsItem('Edit', 'pencil', 'change'), 'delete': ActionsItem('Delete', 'trash-can-outline', 'delete'), 'changelog': ActionsItem('Changelog', 'history'), } def __init__(self, *args, sequence=('edit', 'delete', 'changelog'), extra_buttons='', **kwargs): super().__init__(*args, **kwargs) self.extra_buttons = extra_buttons # Determine which actions to enable self.actions = { name: self.actions[name] for name in sequence } def header(self): return '' def render(self, record, table, **kwargs): # Skip dummy records (e.g. available VLANs) or those with no actions if not hasattr(record, 'pk') or not self.actions: return '' model = table.Meta.model viewname_base = f'{model._meta.app_label}:{model._meta.model_name}' request = getattr(table, 'context', {}).get('request') url_appendix = f'?return_url={request.path}' if request else '' links = [] user = getattr(request, 'user', AnonymousUser()) for action, attrs in self.actions.items(): permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}' if attrs.permission is None or user.has_perm(permission): url = reverse(f'{viewname_base}_{action}', kwargs={'pk': record.pk}) links.append(f'
  • ' f' {attrs.title}
  • ') if not links: return '' menu = f'' \ f'' \ f'' \ f'' # Render any extra buttons from template code if self.extra_buttons: template = Template(self.extra_buttons) context = getattr(table, "context", Context()) context.update({'record': record}) menu = template.render(context) + menu return mark_safe(menu) class ChoiceFieldColumn(tables.Column): """ Render a ChoiceField value inside a indicating a particular CSS class. This is useful for displaying colored choices. The CSS class is derived by calling .get_FOO_class() on the row record. """ def render(self, record, bound_column, value): if value: name = bound_column.name css_class = getattr(record, f'get_{name}_class')() label = getattr(record, f'get_{name}_display')() return mark_safe( f'{label}' ) return self.default def value(self, value): return value class ContentTypeColumn(tables.Column): """ Display a ContentType instance. """ def render(self, value): if value is None: return None return content_type_name(value) def value(self, value): if value is None: return None return content_type_identifier(value) class ContentTypesColumn(tables.ManyToManyColumn): """ Display a list of ContentType instances. """ def __init__(self, separator=None, *args, **kwargs): # Use a line break as the default separator if separator is None: separator = mark_safe('
    ') super().__init__(separator=separator, *args, **kwargs) def transform(self, obj): return content_type_name(obj) def value(self, value): return ','.join([ content_type_identifier(ct) for ct in self.filter(value) ]) class ColorColumn(tables.Column): """ Display a color (#RRGGBB). """ def render(self, value): return mark_safe( f' ' ) def value(self, value): return f'#{value}' class ColoredLabelColumn(tables.TemplateColumn): """ Render a colored label (e.g. for DeviceRoles). """ template_code = """ {% load helpers %} {% if value %} {{ value }} {% else %} — {% endif %} """ def __init__(self, *args, **kwargs): super().__init__(template_code=self.template_code, *args, **kwargs) def value(self, value): return str(value) class LinkedCountColumn(tables.Column): """ Render a count of related objects linked to a filtered URL. :param viewname: The view name to use for URL resolution :param view_kwargs: Additional kwargs to pass for URL resolution (optional) :param url_params: A dict of query parameters to append to the URL (e.g. ?foo=bar) (optional) """ def __init__(self, viewname, *args, view_kwargs=None, url_params=None, default=0, **kwargs): self.viewname = viewname self.view_kwargs = view_kwargs or {} self.url_params = url_params super().__init__(*args, default=default, **kwargs) def render(self, record, value): if value: url = reverse(self.viewname, kwargs=self.view_kwargs) if self.url_params: url += '?' + '&'.join([ f'{k}={getattr(record, v) or settings.FILTERS_NULL_CHOICE_VALUE}' for k, v in self.url_params.items() ]) return mark_safe(f'{value}') return value def value(self, value): return value class TagColumn(tables.TemplateColumn): """ Display a list of tags assigned to the object. """ template_code = """ {% load helpers %} {% for tag in value.all %} {% tag tag url_name=url_name %} {% empty %} {% endfor %} """ def __init__(self, url_name=None): super().__init__( template_code=self.template_code, extra_context={'url_name': url_name} ) def value(self, value): return ",".join([tag.name for tag in value.all()]) class CustomFieldColumn(tables.Column): """ Display custom fields in the appropriate format. """ def __init__(self, customfield, *args, **kwargs): self.customfield = customfield kwargs['accessor'] = Accessor(f'custom_field_data__{customfield.name}') if 'verbose_name' not in kwargs: kwargs['verbose_name'] = customfield.label or customfield.name super().__init__(*args, **kwargs) def render(self, value): if isinstance(value, list): return ', '.join(v for v in value) elif self.customfield.type == CustomFieldTypeChoices.TYPE_URL: # Linkify custom URLs return mark_safe(f'{value}') if value is not None: return value return self.default class CustomLinkColumn(tables.Column): """ Render a custom links as a table column. """ def __init__(self, customlink, *args, **kwargs): self.customlink = customlink kwargs['accessor'] = Accessor('pk') if 'verbose_name' not in kwargs: kwargs['verbose_name'] = customlink.name super().__init__(*args, **kwargs) def render(self, record): try: rendered = self.customlink.render({'obj': record}) if rendered: return mark_safe(f'{rendered["text"]}') except Exception as e: return mark_safe(f' Error') return '' def value(self, record): try: rendered = self.customlink.render({'obj': record}) if rendered: return rendered['link'] except Exception: pass return None class MPTTColumn(tables.TemplateColumn): """ Display a nested hierarchy for MPTT-enabled models. """ template_code = """ {% load helpers %} {% for i in record.level|as_range %}{% endfor %} {{ record.name }} """ def __init__(self, *args, **kwargs): super().__init__( template_code=self.template_code, orderable=False, attrs={'td': {'class': 'text-nowrap'}}, *args, **kwargs ) def value(self, value): return value class UtilizationColumn(tables.TemplateColumn): """ Display a colored utilization bar graph. """ template_code = """{% load helpers %}{% if record.pk %}{% utilization_graph value %}{% endif %}""" def __init__(self, *args, **kwargs): super().__init__(template_code=self.template_code, *args, **kwargs) def value(self, value): return f'{value}%' class MarkdownColumn(tables.TemplateColumn): """ Render a Markdown string. """ template_code = """ {% load helpers %} {% if value %} {{ value|render_markdown }} {% else %} — {% endif %} """ def __init__(self): super().__init__( template_code=self.template_code ) def value(self, value): return value