diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md index 3b502cab2..7fd510841 100644 --- a/docs/models/extras/customlink.md +++ b/docs/models/extras/customlink.md @@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis ## Link Groups Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group. + +## Table Columns + +Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL. diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index c748bff14..d50404261 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables * [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol ### Bug Fixes diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 47da21e19..36457efae 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel): def get_absolute_url(self): return reverse('extras:customlink', args=[self.pk]) + def render(self, context): + """ + Render the CustomLink given the provided context, and return the text, link, and link_target. + + :param context: The context passed to Jinja2 + """ + text = render_jinja2(self.link_text, context) + if not text: + return {} + link = render_jinja2(self.link_url, context) + link_target = ' target="_blank"' if self.new_window else '' + + return { + 'text': text, + 'link': link, + 'link_target': link_target, + } + @extras_features('webhooks', 'export_templates') class ExportTemplate(ChangeLoggedModel): diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index fec5cf65a..32ec966b3 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -62,16 +62,14 @@ def custom_links(context, obj): # Add non-grouped links else: try: - text_rendered = render_jinja2(cl.link_text, link_context) - if text_rendered: - link_rendered = render_jinja2(cl.link_url, link_context) - link_target = ' target="_blank"' if cl.new_window else '' + rendered = cl.render(link_context) + if rendered: template_code += LINK_BUTTON.format( - link_rendered, link_target, cl.button_class, text_rendered + rendered['link'], rendered['link_target'], cl.button_class, rendered['text'] ) except Exception as e: - template_code += '' \ - ' {}\n'.format(e, cl.name) + template_code += f'' \ + f' {cl.name}\n' # Add grouped links to template for group, links in group_names.items(): @@ -80,17 +78,15 @@ def custom_links(context, obj): for cl in links: try: - text_rendered = render_jinja2(cl.link_text, link_context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' - link_rendered = render_jinja2(cl.link_url, link_context) + rendered = cl.render(link_context) + if rendered: links_rendered.append( - GROUP_LINK.format(link_rendered, link_target, text_rendered) + GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text']) ) except Exception as e: links_rendered.append( - '
  • ' - ' {}
  • '.format(e, cl.name) + f'
  • ' + f' {cl.name}
  • ' ) if links_rendered: diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index 16abd273a..183d64023 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -12,7 +12,7 @@ from django_tables2.data import TableQuerysetData from django_tables2.utils import Accessor from extras.choices import CustomFieldTypeChoices -from extras.models import CustomField +from extras.models import CustomField, CustomLink from .utils import content_type_identifier, content_type_name from .paginator import EnhancedPaginator, get_paginate_count @@ -34,15 +34,18 @@ class BaseTable(tables.Table): } def __init__(self, *args, user=None, extra_columns=None, **kwargs): + if extra_columns is None: + extra_columns = [] + # 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 + cl_columns = [ + (f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type) + ] + extra_columns.extend([*cf_columns, *cl_columns]) super().__init__(*args, extra_columns=extra_columns, **kwargs) @@ -418,6 +421,37 @@ class CustomFieldColumn(tables.Column): 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.