Skip to content

Commit cab9733

Browse files
Merge pull request #8159 from netbox-community/6782-custom-link-columns
Closes #6782: Custom link columns
2 parents d650d10 + 99e0dce commit cab9733

File tree

5 files changed

+72
-19
lines changed

5 files changed

+72
-19
lines changed

docs/models/extras/customlink.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
5555
## Link Groups
5656

5757
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.
58+
59+
## Table Columns
60+
61+
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.

docs/release-notes/version-3.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Enhancements
66

7+
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
78
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
89

910
### Bug Fixes

netbox/extras/models/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,24 @@ def __str__(self):
229229
def get_absolute_url(self):
230230
return reverse('extras:customlink', args=[self.pk])
231231

232+
def render(self, context):
233+
"""
234+
Render the CustomLink given the provided context, and return the text, link, and link_target.
235+
236+
:param context: The context passed to Jinja2
237+
"""
238+
text = render_jinja2(self.link_text, context)
239+
if not text:
240+
return {}
241+
link = render_jinja2(self.link_url, context)
242+
link_target = ' target="_blank"' if self.new_window else ''
243+
244+
return {
245+
'text': text,
246+
'link': link,
247+
'link_target': link_target,
248+
}
249+
232250

233251
@extras_features('webhooks', 'export_templates')
234252
class ExportTemplate(ChangeLoggedModel):

netbox/extras/templatetags/custom_links.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,14 @@ def custom_links(context, obj):
6262
# Add non-grouped links
6363
else:
6464
try:
65-
text_rendered = render_jinja2(cl.link_text, link_context)
66-
if text_rendered:
67-
link_rendered = render_jinja2(cl.link_url, link_context)
68-
link_target = ' target="_blank"' if cl.new_window else ''
65+
rendered = cl.render(link_context)
66+
if rendered:
6967
template_code += LINK_BUTTON.format(
70-
link_rendered, link_target, cl.button_class, text_rendered
68+
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
7169
)
7270
except Exception as e:
73-
template_code += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
74-
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
71+
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
72+
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
7573

7674
# Add grouped links to template
7775
for group, links in group_names.items():
@@ -80,17 +78,15 @@ def custom_links(context, obj):
8078

8179
for cl in links:
8280
try:
83-
text_rendered = render_jinja2(cl.link_text, link_context)
84-
if text_rendered:
85-
link_target = ' target="_blank"' if cl.new_window else ''
86-
link_rendered = render_jinja2(cl.link_url, link_context)
81+
rendered = cl.render(link_context)
82+
if rendered:
8783
links_rendered.append(
88-
GROUP_LINK.format(link_rendered, link_target, text_rendered)
84+
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
8985
)
9086
except Exception as e:
9187
links_rendered.append(
92-
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
93-
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
88+
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
89+
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
9490
)
9591

9692
if links_rendered:

netbox/utilities/tables.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django_tables2.utils import Accessor
1313

1414
from extras.choices import CustomFieldTypeChoices
15-
from extras.models import CustomField
15+
from extras.models import CustomField, CustomLink
1616
from .utils import content_type_identifier, content_type_name
1717
from .paginator import EnhancedPaginator, get_paginate_count
1818

@@ -34,15 +34,18 @@ class Meta:
3434
}
3535

3636
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
37+
if extra_columns is None:
38+
extra_columns = []
39+
3740
# Add custom field columns
3841
obj_type = ContentType.objects.get_for_model(self._meta.model)
3942
cf_columns = [
4043
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
4144
]
42-
if extra_columns is not None:
43-
extra_columns.extend(cf_columns)
44-
else:
45-
extra_columns = cf_columns
45+
cl_columns = [
46+
(f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
47+
]
48+
extra_columns.extend([*cf_columns, *cl_columns])
4649

4750
super().__init__(*args, extra_columns=extra_columns, **kwargs)
4851

@@ -418,6 +421,37 @@ def render(self, value):
418421
return self.default
419422

420423

424+
class CustomLinkColumn(tables.Column):
425+
"""
426+
Render a custom links as a table column.
427+
"""
428+
def __init__(self, customlink, *args, **kwargs):
429+
self.customlink = customlink
430+
kwargs['accessor'] = Accessor('pk')
431+
if 'verbose_name' not in kwargs:
432+
kwargs['verbose_name'] = customlink.name
433+
434+
super().__init__(*args, **kwargs)
435+
436+
def render(self, record):
437+
try:
438+
rendered = self.customlink.render({'obj': record})
439+
if rendered:
440+
return mark_safe(f'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
441+
except Exception as e:
442+
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> Error</span>')
443+
return ''
444+
445+
def value(self, record):
446+
try:
447+
rendered = self.customlink.render({'obj': record})
448+
if rendered:
449+
return rendered['link']
450+
except Exception:
451+
pass
452+
return None
453+
454+
421455
class MPTTColumn(tables.TemplateColumn):
422456
"""
423457
Display a nested hierarchy for MPTT-enabled models.

0 commit comments

Comments
 (0)