Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/models/extras/customlink.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions docs/release-notes/version-3.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions netbox/extras/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,24 @@ def __str__(self):
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):
Expand Down
24 changes: 10 additions & 14 deletions netbox/extras/templatetags/custom_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'

# Add grouped links to template
for group, links in group_names.items():
Expand All @@ -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(
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
)

if links_rendered:
Expand Down
44 changes: 39 additions & 5 deletions netbox/utilities/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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

Expand All @@ -34,15 +34,18 @@ class Meta:
}

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)

Expand Down Expand Up @@ -418,6 +421,37 @@ def render(self, 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'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
except Exception as e:
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> Error</span>')
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.
Expand Down