diff --git a/docs/plugins/development/templates.md b/docs/plugins/development/templates.md index 70228c623..61ffd3ce2 100644 --- a/docs/plugins/development/templates.md +++ b/docs/plugins/development/templates.md @@ -193,3 +193,43 @@ This template is used by the `BulkDeleteView` generic view to delete multiple ob | `form` | Yes | The bulk delete form class | | `table` | Yes | The table class used for rendering the list of objects | | `return_url` | Yes | The URL to which the user is redirect after submitting the form | + +## Tags + +The following custom template tags are available in NetBox. + +!!! info + These are loaded automatically by the template backend: You do _not_ need to include a `{% load %}` tag in your template to activate them. + +::: utilities.templatetags.builtins.tags.badge + +::: utilities.templatetags.builtins.tags.checkmark + +::: utilities.templatetags.builtins.tags.tag + +## Filters + +The following custom template filters are available in NetBox. + +!!! info + These are loaded automatically by the template backend: You do _not_ need to include a `{% load %}` tag in your template to activate them. + +::: utilities.templatetags.builtins.filters.bettertitle + +::: utilities.templatetags.builtins.filters.content_type + +::: utilities.templatetags.builtins.filters.content_type_id + +::: utilities.templatetags.builtins.filters.meta + +::: utilities.templatetags.builtins.filters.placeholder + +::: utilities.templatetags.builtins.filters.render_json + +::: utilities.templatetags.builtins.filters.render_markdown + +::: utilities.templatetags.builtins.filters.render_yaml + +::: utilities.templatetags.builtins.filters.split + +::: utilities.templatetags.builtins.filters.tzoffset diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 88a2dbe54..d37c44763 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -225,7 +225,7 @@ class ObjectJournalTable(NetBoxTable): ) kind = columns.ChoiceFieldColumn() comments = tables.TemplateColumn( - template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}' + template_code='{{ value|markdown|truncatewords_html:50 }}' ) class Meta(NetBoxTable.Meta): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4ca24fbf1..eaf1d3033 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -352,6 +352,10 @@ TEMPLATES = [ 'DIRS': [TEMPLATES_DIR], 'APP_DIRS': True, 'OPTIONS': { + 'builtins': [ + 'utilities.templatetags.builtins.filters', + 'utilities.templatetags.builtins.tags', + ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index cfc220536..43350acb0 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -290,7 +290,7 @@ class TagColumn(tables.TemplateColumn): template_code = """ {% load helpers %} {% for tag in value.all %} - {% tag tag url_name=url_name %} + {% tag tag url_name %} {% empty %} — {% endfor %} @@ -414,9 +414,8 @@ class MarkdownColumn(tables.TemplateColumn): Render a Markdown string. """ template_code = """ - {% load helpers %} {% if value %} - {{ value|render_markdown }} + {{ value|markdown }} {% else %} — {% endif %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 14fd00863..3fd275c7c 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -41,11 +41,11 @@
{{ object.napalm_args|render_json }}+
{{ object.napalm_args|json }}
{% if format == 'json' %}{{ data|render_json }}{% elif format == 'yaml' %}{{ data|render_yaml }}{% else %}{{ data }}{% endif %}+
{% if format == 'json' %}{{ data|json }}{% elif format == 'yaml' %}{{ data|yaml }}{% else %}{{ data }}{% endif %}
{{ diff_removed|render_json }}-
{{ diff_added|render_json }}+
{{ diff_removed|json }}+
{{ diff_added|json }}{% endif %} @@ -114,7 +114,7 @@
{% for k, v in object.prechange_data.items %}{% spaceless %} - {{ k }}: {{ v|render_json }} + {{ k }}: {{ v|json }} {% endspaceless %}{% endfor %}{% elif non_atomic_change %} @@ -133,7 +133,7 @@
{% for k, v in object.postchange_data.items %}{% spaceless %} - {{ k }}: {{ v|render_json }} + {{ k }}: {{ v|json }} {% endspaceless %}{% endfor %}{% else %} diff --git a/netbox/templates/extras/report.html b/netbox/templates/extras/report.html index 68e888097..391de6614 100644 --- a/netbox/templates/extras/report.html +++ b/netbox/templates/extras/report.html @@ -15,7 +15,7 @@ {% block subtitle %} {% if report.description %}
{{ object.conditions|render_json }}+
{{ object.conditions|json }}{% else %}
None
{% endif %} diff --git a/netbox/templates/inc/panels/comments.html b/netbox/templates/inc/panels/comments.html index 3219a25a5..8ccbf8949 100644 --- a/netbox/templates/inc/panels/comments.html +++ b/netbox/templates/inc/panels/comments.html @@ -6,7 +6,7 @@{{ value|render_json }}+
{{ value|json }}{% elif field.type == 'multiselect' and value %} {{ value|join:", " }} {% elif field.type == 'object' and value %} diff --git a/netbox/utilities/templates/helpers/badge.html b/netbox/utilities/templates/builtins/badge.html similarity index 100% rename from netbox/utilities/templates/helpers/badge.html rename to netbox/utilities/templates/builtins/badge.html diff --git a/netbox/utilities/templates/helpers/checkmark.html b/netbox/utilities/templates/builtins/checkmark.html similarity index 100% rename from netbox/utilities/templates/helpers/checkmark.html rename to netbox/utilities/templates/builtins/checkmark.html diff --git a/netbox/utilities/templates/helpers/tag.html b/netbox/utilities/templates/builtins/tag.html similarity index 61% rename from netbox/utilities/templates/helpers/tag.html rename to netbox/utilities/templates/builtins/tag.html index addb2380b..d63b6afa6 100644 --- a/netbox/utilities/templates/helpers/tag.html +++ b/netbox/utilities/templates/builtins/tag.html @@ -1,3 +1,3 @@ {% load helpers %} -{% if url_name %}{% endif %}{{ tag }}{% if url_name %}{% endif %} +{% if viewname %}{% endif %}{{ tag }}{% if viewname %}{% endif %} diff --git a/netbox/utilities/templatetags/builtins/__init__.py b/netbox/utilities/templatetags/builtins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py new file mode 100644 index 000000000..afb40a308 --- /dev/null +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -0,0 +1,167 @@ +import datetime +import json +import re + +import yaml +from django import template +from django.contrib.contenttypes.models import ContentType +from django.utils.html import strip_tags +from django.utils.safestring import mark_safe +from markdown import markdown + +from netbox.config import get_config +from utilities.markdown import StrikethroughExtension +from utilities.utils import foreground_color + +register = template.Library() + + +# +# General +# + +@register.filter() +def bettertitle(value): + """ + Alternative to the builtin title(). Ensures that the first letter of each word is uppercase but retains the + original case of all others. + """ + return ' '.join([w[0].upper() + w[1:] for w in value.split()]) + + +@register.filter() +def fgcolor(value, dark='000000', light='ffffff'): + """ + Return black (#000000) or white (#ffffff) given an arbitrary background color in RRGGBB format. The foreground + color with the better contrast is returned. + + Args: + value: The background color + dark: The foreground color to use for light backgrounds + light: The foreground color to use for dark backgrounds + """ + value = value.lower().strip('#') + if not re.match('^[0-9a-f]{6}$', value): + return '' + return f'#{foreground_color(value, dark, light)}' + + +@register.filter() +def meta(model, attr): + """ + Return the specified Meta attribute of a model. This is needed because Django does not permit templates + to access attributes which begin with an underscore (e.g. _meta). + + Args: + model: A Django model class or instance + attr: The attribute name + """ + return getattr(model._meta, attr, '') + + +@register.filter() +def placeholder(value): + """ + Render a muted placeholder if the value equates to False. + """ + if value not in ('', None): + return value + placeholder = '—' + return mark_safe(placeholder) + + +@register.filter() +def split(value, separator=','): + """ + Wrapper for Python's `split()` string method. + + Args: + value: A string + separator: String on which the value will be split + """ + return value.split(separator) + + +@register.filter() +def tzoffset(value): + """ + Returns the hour offset of a given time zone using the current time. + """ + return datetime.datetime.now(value).strftime('%z') + + +# +# Content types +# + +@register.filter() +def content_type(model): + """ + Return the ContentType for the given object. + """ + return ContentType.objects.get_for_model(model) + + +@register.filter() +def content_type_id(model): + """ + Return the ContentType ID for the given object. + """ + content_type = ContentType.objects.get_for_model(model) + if content_type: + return content_type.pk + return None + + +# +# Rendering +# + +@register.filter('markdown', is_safe=True) +def render_markdown(value): + """ + Render a string as Markdown. This filter is invoked as "markdown": + + {{ md_source_text|markdown }} + """ + schemes = '|'.join(get_config().ALLOWED_URL_SCHEMES) + + # Strip HTML tags + value = strip_tags(value) + + # Sanitize Markdown links + pattern = fr'\[([^\]]+)\]\((?!({schemes})).*:(.+)\)' + value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE) + + # Sanitize Markdown reference links + pattern = fr'\[(.+)\]:\s*(?!({schemes}))\w*:(.+)' + value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE) + + # Render Markdown + html = markdown(value, extensions=['fenced_code', 'tables', StrikethroughExtension()]) + + # If the string is not empty wrap it in rendered-markdown to style tables + if html: + html = f'