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'