2
0

tables.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import django_tables2 as tables
  2. from django.contrib.auth.models import AnonymousUser
  3. from django.contrib.contenttypes.fields import GenericForeignKey
  4. from django.core.exceptions import FieldDoesNotExist
  5. from django.db.models.fields.related import RelatedField
  6. from django.urls import reverse
  7. from django.utils.safestring import mark_safe
  8. from django_tables2.data import TableQuerysetData
  9. class BaseTable(tables.Table):
  10. """
  11. Default table for object lists
  12. :param user: Personalize table display for the given user (optional). Has no effect if AnonymousUser is passed.
  13. """
  14. # By default, modify the queryset passed to the table upon initialization to automatically prefetch related
  15. # data. Set this to False if it's necessary to avoid modifying the queryset (e.g. to accommodate
  16. # PrefixQuerySet.annotate_depth()).
  17. add_prefetch = True
  18. class Meta:
  19. attrs = {
  20. 'class': 'table table-hover table-headings',
  21. }
  22. def __init__(self, *args, user=None, **kwargs):
  23. super().__init__(*args, **kwargs)
  24. # Set default empty_text if none was provided
  25. if self.empty_text is None:
  26. self.empty_text = 'No {} found'.format(self._meta.model._meta.verbose_name_plural)
  27. # Hide non-default columns
  28. default_columns = getattr(self.Meta, 'default_columns', list())
  29. if default_columns:
  30. for column in self.columns:
  31. if column.name not in default_columns:
  32. self.columns.hide(column.name)
  33. # Apply custom column ordering for user
  34. if user is not None and not isinstance(user, AnonymousUser):
  35. columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
  36. if columns:
  37. pk = self.base_columns.pop('pk', None)
  38. actions = self.base_columns.pop('actions', None)
  39. for name, column in self.base_columns.items():
  40. if name in columns:
  41. self.columns.show(name)
  42. else:
  43. self.columns.hide(name)
  44. self.sequence = [c for c in columns if c in self.base_columns]
  45. # Always include PK and actions column, if defined on the table
  46. if pk:
  47. self.base_columns['pk'] = pk
  48. self.sequence.insert(0, 'pk')
  49. if actions:
  50. self.base_columns['actions'] = actions
  51. self.sequence.append('actions')
  52. # Dynamically update the table's QuerySet to ensure related fields are pre-fetched
  53. if self.add_prefetch and isinstance(self.data, TableQuerysetData):
  54. model = getattr(self.Meta, 'model')
  55. prefetch_fields = []
  56. for column in self.columns:
  57. if column.visible:
  58. field_path = column.accessor.split('.')
  59. try:
  60. model_field = model._meta.get_field(field_path[0])
  61. if isinstance(model_field, (RelatedField, GenericForeignKey)):
  62. prefetch_fields.append('__'.join(field_path))
  63. except FieldDoesNotExist:
  64. pass
  65. self.data.data = self.data.data.prefetch_related(None).prefetch_related(*prefetch_fields)
  66. @property
  67. def configurable_columns(self):
  68. selected_columns = [
  69. (name, self.columns[name].verbose_name) for name in self.sequence if name not in ['pk', 'actions']
  70. ]
  71. available_columns = [
  72. (name, column.verbose_name) for name, column in self.columns.items() if name not in self.sequence and name not in ['pk', 'actions']
  73. ]
  74. return selected_columns + available_columns
  75. @property
  76. def visible_columns(self):
  77. return [name for name in self.sequence if self.columns[name].visible]
  78. #
  79. # Table columns
  80. #
  81. class ToggleColumn(tables.CheckBoxColumn):
  82. """
  83. Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
  84. """
  85. def __init__(self, *args, **kwargs):
  86. default = kwargs.pop('default', '')
  87. visible = kwargs.pop('visible', False)
  88. if 'attrs' not in kwargs:
  89. kwargs['attrs'] = {
  90. 'td': {
  91. 'class': 'min-width'
  92. }
  93. }
  94. super().__init__(*args, default=default, visible=visible, **kwargs)
  95. @property
  96. def header(self):
  97. return mark_safe('<input type="checkbox" class="toggle" title="Toggle all" />')
  98. class BooleanColumn(tables.Column):
  99. """
  100. Custom implementation of BooleanColumn to render a nicely-formatted checkmark or X icon instead of a Unicode
  101. character.
  102. """
  103. def render(self, value):
  104. if value:
  105. rendered = '<span class="text-success"><i class="fa fa-check"></i></span>'
  106. elif value is None:
  107. rendered = '<span class="text-muted">&mdash;</span>'
  108. else:
  109. rendered = '<span class="text-danger"><i class="fa fa-close"></i></span>'
  110. return mark_safe(rendered)
  111. class ButtonsColumn(tables.TemplateColumn):
  112. """
  113. Render edit, delete, and changelog buttons for an object.
  114. :param model: Model class to use for calculating URL view names
  115. :param prepend_content: Additional template content to render in the column (optional)
  116. :param return_url_extra: String to append to the return URL (e.g. for specifying a tab) (optional)
  117. """
  118. buttons = ('changelog', 'edit', 'delete')
  119. attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
  120. # Note that braces are escaped to allow for string formatting prior to template rendering
  121. template_code = """
  122. {{% if "changelog" in buttons %}}
  123. <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
  124. <i class="fa fa-history"></i>
  125. </a>
  126. {{% endif %}}
  127. {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
  128. <a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-warning" title="Edit">
  129. <i class="fa fa-pencil"></i>
  130. </a>
  131. {{% endif %}}
  132. {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
  133. <a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-xs btn-danger" title="Delete">
  134. <i class="fa fa-trash"></i>
  135. </a>
  136. {{% endif %}}
  137. """
  138. def __init__(self, model, *args, pk_field='pk', buttons=None, prepend_template=None, return_url_extra='',
  139. **kwargs):
  140. if prepend_template:
  141. prepend_template = prepend_template.replace('{', '{{')
  142. prepend_template = prepend_template.replace('}', '}}')
  143. self.template_code = prepend_template + self.template_code
  144. template_code = self.template_code.format(
  145. app_label=model._meta.app_label,
  146. model_name=model._meta.model_name,
  147. pk_field=pk_field,
  148. buttons=buttons
  149. )
  150. super().__init__(template_code=template_code, *args, **kwargs)
  151. self.extra_context.update({
  152. 'buttons': buttons or self.buttons,
  153. 'return_url_extra': return_url_extra,
  154. })
  155. def header(self):
  156. return ''
  157. class ChoiceFieldColumn(tables.Column):
  158. """
  159. Render a ChoiceField value inside a <span> indicating a particular CSS class. This is useful for displaying colored
  160. choices. The CSS class is derived by calling .get_FOO_class() on the row record.
  161. """
  162. def render(self, record, bound_column, value):
  163. if value:
  164. name = bound_column.name
  165. css_class = getattr(record, f'get_{name}_class')()
  166. label = getattr(record, f'get_{name}_display')()
  167. return mark_safe(
  168. f'<span class="label label-{css_class}">{label}</span>'
  169. )
  170. return self.default
  171. class ColorColumn(tables.Column):
  172. """
  173. Display a color (#RRGGBB).
  174. """
  175. def render(self, value):
  176. return mark_safe(
  177. f'<span class="label color-block" style="background-color: #{value}">&nbsp;</span>'
  178. )
  179. class ColoredLabelColumn(tables.TemplateColumn):
  180. """
  181. Render a colored label (e.g. for DeviceRoles).
  182. """
  183. template_code = """
  184. {% load helpers %}
  185. {% if value %}<label class="label" style="color: {{ value.color|fgcolor }}; background-color: #{{ value.color }}">{{ value }}</label>{% else %}&mdash;{% endif %}
  186. """
  187. def __init__(self, *args, **kwargs):
  188. super().__init__(template_code=self.template_code, *args, **kwargs)
  189. class LinkedCountColumn(tables.Column):
  190. """
  191. Render a count of related objects linked to a filtered URL.
  192. :param viewname: The view name to use for URL resolution
  193. :param view_kwargs: Additional kwargs to pass for URL resolution (optional)
  194. :param url_params: A dict of query parameters to append to the URL (e.g. ?foo=bar) (optional)
  195. """
  196. def __init__(self, viewname, *args, view_kwargs=None, url_params=None, default=0, **kwargs):
  197. self.viewname = viewname
  198. self.view_kwargs = view_kwargs or {}
  199. self.url_params = url_params
  200. super().__init__(*args, default=default, **kwargs)
  201. def render(self, record, value):
  202. if value:
  203. url = reverse(self.viewname, kwargs=self.view_kwargs)
  204. if self.url_params:
  205. url += '?' + '&'.join([f'{k}={getattr(record, v)}' for k, v in self.url_params.items()])
  206. return mark_safe(f'<a href="{url}">{value}</a>')
  207. return value
  208. class TagColumn(tables.TemplateColumn):
  209. """
  210. Display a list of tags assigned to the object.
  211. """
  212. template_code = """
  213. {% for tag in value.all %}
  214. {% include 'utilities/templatetags/tag.html' %}
  215. {% empty %}
  216. <span class="text-muted">&mdash;</span>
  217. {% endfor %}
  218. """
  219. def __init__(self, url_name=None):
  220. super().__init__(
  221. template_code=self.template_code,
  222. extra_context={'url_name': url_name}
  223. )