tables.py 7.8 KB

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