| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 |
- 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
- from .utils import content_type_identifier, content_type_name
- from .paginator import EnhancedPaginator, get_paginate_count
- 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):
- # 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)
- ]
- if extra_columns is not None:
- extra_columns.extend(cf_columns)
- else:
- extra_columns = cf_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.
- """
- 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('<input type="checkbox" class="toggle form-check-input" title="Toggle All" />')
- 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 = '<span class="text-success"><i class="mdi mdi-check-bold"></i></span>'
- elif value is None:
- rendered = '<span class="text-muted">—</span>'
- else:
- rendered = '<span class="text-danger"><i class="mdi mdi-close-thick"></i></span>'
- 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
- class ButtonsColumn(tables.TemplateColumn):
- """
- Render edit, delete, and changelog buttons for an object.
- :param model: Model class to use for calculating URL view names
- :param prepend_content: Additional template content to render in the column (optional)
- """
- buttons = ('changelog', 'edit', 'delete')
- attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
- # Note that braces are escaped to allow for string formatting prior to template rendering
- template_code = """
- {{% if "changelog" in buttons %}}
- <a href="{{% url '{app_label}:{model_name}_changelog' pk=record.pk %}}" class="btn btn-outline-dark btn-sm" title="Change log">
- <i class="mdi mdi-history"></i>
- </a>
- {{% endif %}}
- {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
- <a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-warning" title="Edit">
- <i class="mdi mdi-pencil"></i>
- </a>
- {{% endif %}}
- {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
- <a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-danger" title="Delete">
- <i class="mdi mdi-trash-can-outline"></i>
- </a>
- {{% endif %}}
- """
- def __init__(self, model, *args, buttons=None, prepend_template=None, **kwargs):
- if prepend_template:
- prepend_template = prepend_template.replace('{', '{{')
- prepend_template = prepend_template.replace('}', '}}')
- self.template_code = prepend_template + self.template_code
- template_code = self.template_code.format(
- app_label=model._meta.app_label,
- model_name=model._meta.model_name,
- buttons=buttons
- )
- super().__init__(template_code=template_code, *args, **kwargs)
- # Exclude from export by default
- if 'exclude_from_export' not in kwargs:
- self.exclude_from_export = True
- self.extra_context.update({
- 'buttons': buttons or self.buttons,
- })
- def header(self):
- return ''
- class ChoiceFieldColumn(tables.Column):
- """
- Render a ChoiceField value inside a <span> 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'<span class="badge bg-{css_class}">{label}</span>'
- )
- 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('<br />')
- 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'<span class="color-label" style="background-color: #{value}"> </span>'
- )
- 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 %}
- <span class="badge" style="color: {{ value.color|fgcolor }}; background-color: #{{ value.color }}">
- {{ value }}
- </span>
- {% 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'<a href="{url}">{value}</a>')
- return value
- def value(self, value):
- return value
- class TagColumn(tables.TemplateColumn):
- """
- Display a list of tags assigned to the object.
- """
- template_code = """
- {% for tag in value.all %}
- {% include 'utilities/templatetags/tag.html' %}
- {% empty %}
- <span class="text-muted">—</span>
- {% 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'<a href="{value}">{value}</a>')
- if value is not None:
- return value
- return self.default
- class MPTTColumn(tables.TemplateColumn):
- """
- Display a nested hierarchy for MPTT-enabled models.
- """
- template_code = """
- {% load helpers %}
- {% for i in record.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
- <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
- """
- 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
- #
- # 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}"
|