1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #8600: Document built-in template tags & filters

This commit is contained in:
jeremystretch
2022-02-09 16:01:58 -05:00
parent ee566723d7
commit 7c105019d8
25 changed files with 289 additions and 189 deletions

View File

@ -193,3 +193,43 @@ This template is used by the `BulkDeleteView` generic view to delete multiple ob
| `form` | Yes | The bulk delete form class | | `form` | Yes | The bulk delete form class |
| `table` | Yes | The table class used for rendering the list of objects | | `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 | | `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

View File

@ -225,7 +225,7 @@ class ObjectJournalTable(NetBoxTable):
) )
kind = columns.ChoiceFieldColumn() kind = columns.ChoiceFieldColumn()
comments = tables.TemplateColumn( comments = tables.TemplateColumn(
template_code='{% load helpers %}{{ value|render_markdown|truncatewords_html:50 }}' template_code='{{ value|markdown|truncatewords_html:50 }}'
) )
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):

View File

@ -352,6 +352,10 @@ TEMPLATES = [
'DIRS': [TEMPLATES_DIR], 'DIRS': [TEMPLATES_DIR],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'builtins': [
'utilities.templatetags.builtins.filters',
'utilities.templatetags.builtins.tags',
],
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',

View File

@ -290,7 +290,7 @@ class TagColumn(tables.TemplateColumn):
template_code = """ template_code = """
{% load helpers %} {% load helpers %}
{% for tag in value.all %} {% for tag in value.all %}
{% tag tag url_name=url_name %} {% tag tag url_name %}
{% empty %} {% empty %}
<span class="text-muted">&mdash;</span> <span class="text-muted">&mdash;</span>
{% endfor %} {% endfor %}
@ -414,9 +414,8 @@ class MarkdownColumn(tables.TemplateColumn):
Render a Markdown string. Render a Markdown string.
""" """
template_code = """ template_code = """
{% load helpers %}
{% if value %} {% if value %}
{{ value|render_markdown }} {{ value|markdown }}
{% else %} {% else %}
&mdash; &mdash;
{% endif %} {% endif %}

View File

@ -41,11 +41,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">NOC Contact</th> <th scope="row">NOC Contact</th>
<td>{{ object.noc_contact|render_markdown|placeholder }}</td> <td>{{ object.noc_contact|markdown|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Admin Contact</th> <th scope="row">Admin Contact</th>
<td>{{ object.admin_contact|render_markdown|placeholder }}</td> <td>{{ object.admin_contact|markdown|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Circuits</th> <th scope="row">Circuits</th>

View File

@ -73,7 +73,7 @@
NAPALM Arguments NAPALM Arguments
</h5> </h5>
<div class="card-body"> <div class="card-body">
<pre>{{ object.napalm_args|render_json }}</pre> <pre>{{ object.napalm_args|json }}</pre>
</div> </div>
</div> </div>
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}

View File

@ -60,7 +60,7 @@
<span class="muted">&mdash;</span> <span class="muted">&mdash;</span>
{% endif %} {% endif %}
</td> </td>
<td class="rendered-markdown">{{ message|render_markdown }}</td> <td class="rendered-markdown">{{ message|markdown }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View File

@ -22,7 +22,7 @@
<tr> <tr>
<td>{{ forloop.counter }}</td> <td>{{ forloop.counter }}</td>
<td>{% log_level log.status %}</td> <td>{% log_level log.status %}</td>
<td class="rendered-markdown">{{ log.message|render_markdown }}</td> <td class="rendered-markdown">{{ log.message|markdown }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>

View File

@ -1,5 +1,5 @@
{% load helpers %} {% load helpers %}
<div class="rendered-context-data"> <div class="rendered-context-data">
<pre class="block">{% if format == 'json' %}{{ data|render_json }}{% elif format == 'yaml' %}{{ data|render_yaml }}{% else %}{{ data }}{% endif %}</pre> <pre class="block">{% if format == 'json' %}{{ data|json }}{% elif format == 'yaml' %}{{ data|yaml }}{% else %}{{ data }}{% endif %}</pre>
</div> </div>

View File

@ -98,8 +98,8 @@
{% endif %} {% endif %}
</span> </span>
{% else %} {% else %}
<pre class="change-diff change-removed">{{ diff_removed|render_json }}</pre> <pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|render_json }}</pre> <pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -114,7 +114,7 @@
<div class="card-body"> <div class="card-body">
{% if object.prechange_data %} {% if object.prechange_data %}
<pre class="change-data">{% for k, v in object.prechange_data.items %}{% spaceless %} <pre class="change-data">{% for k, v in object.prechange_data.items %}{% spaceless %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|render_json }}</span> <span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endspaceless %}{% endfor %} {% endspaceless %}{% endfor %}
</pre> </pre>
{% elif non_atomic_change %} {% elif non_atomic_change %}
@ -133,7 +133,7 @@
<div class="card-body"> <div class="card-body">
{% if object.postchange_data %} {% if object.postchange_data %}
<pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %} <pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span> <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endspaceless %}{% endfor %} {% endspaceless %}{% endfor %}
</pre> </pre>
{% else %} {% else %}

View File

@ -15,7 +15,7 @@
{% block subtitle %} {% block subtitle %}
{% if report.description %} {% if report.description %}
<div class="object-subtitle"> <div class="object-subtitle">
<div class="text-muted">{{ report.description|render_markdown }}</div> <div class="text-muted">{{ report.description|markdown }}</div>
</div> </div>
{% endif %} {% endif %}
{% endblock subtitle %} {% endblock subtitle %}

View File

@ -40,7 +40,7 @@
<td> <td>
{% include 'extras/inc/job_label.html' with result=report.result %} {% include 'extras/inc/job_label.html' with result=report.result %}
</td> </td>
<td>{{ report.description|render_markdown|placeholder }}</td> <td>{{ report.description|markdown|placeholder }}</td>
<td class="text-end"> <td class="text-end">
{% if report.result %} {% if report.result %}
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created|annotated_date }}</a> <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created|annotated_date }}</a>

View File

@ -16,7 +16,7 @@
{% block subtitle %} {% block subtitle %}
<div class="object-subtitle"> <div class="object-subtitle">
<div class="text-muted">{{ script.Meta.description|render_markdown }}</div> <div class="text-muted">{{ script.Meta.description|markdown }}</div>
</div> </div>
{% endblock subtitle %} {% endblock subtitle %}

View File

@ -40,7 +40,7 @@
{% include 'extras/inc/job_label.html' with result=script.result %} {% include 'extras/inc/job_label.html' with result=script.result %}
</td> </td>
<td> <td>
{{ script.Meta.description|render_markdown|placeholder }} {{ script.Meta.description|markdown|placeholder }}
</td> </td>
{% if script.result %} {% if script.result %}
<td class="text-end"> <td class="text-end">

View File

@ -4,7 +4,7 @@
{% block title %}{{ script }}{% endblock %} {% block title %}{{ script }}{% endblock %}
{% block subtitle %} {% block subtitle %}
{{ script.Meta.description|render_markdown }} {{ script.Meta.description|markdown }}
{% endblock %} {% endblock %}
{% block header %} {% block header %}

View File

@ -108,7 +108,7 @@
</h5> </h5>
<div class="card-body"> <div class="card-body">
{% if object.conditions %} {% if object.conditions %}
<pre>{{ object.conditions|render_json }}</pre> <pre>{{ object.conditions|json }}</pre>
{% else %} {% else %}
<p class="text-muted">None</p> <p class="text-muted">None</p>
{% endif %} {% endif %}

View File

@ -6,7 +6,7 @@
</h5> </h5>
<div class="card-body"> <div class="card-body">
{% if object.comments %} {% if object.comments %}
{{ object.comments|render_markdown }} {{ object.comments|markdown }}
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}

View File

@ -13,7 +13,7 @@
</td> </td>
<td> <td>
{% if field.type == 'longtext' and value %} {% if field.type == 'longtext' and value %}
{{ value|render_markdown }} {{ value|markdown }}
{% elif field.type == 'boolean' and value == True %} {% elif field.type == 'boolean' and value == True %}
{% checkmark value true="True" %} {% checkmark value true="True" %}
{% elif field.type == 'boolean' and value == False %} {% elif field.type == 'boolean' and value == False %}
@ -21,7 +21,7 @@
{% elif field.type == 'url' and value %} {% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a> <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'json' and value %} {% elif field.type == 'json' and value %}
<pre>{{ value|render_json }}</pre> <pre>{{ value|json }}</pre>
{% elif field.type == 'multiselect' and value %} {% elif field.type == 'multiselect' and value %}
{{ value|join:", " }} {{ value|join:", " }}
{% elif field.type == 'object' and value %} {% elif field.type == 'object' and value %}

View File

@ -1,3 +1,3 @@
{% load helpers %} {% load helpers %}
{% if url_name %}<a href="{% url url_name %}?tag={{ tag.slug }}">{% endif %}<span class="badge" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span>{% if url_name %}</a>{% endif %} {% if viewname %}<a href="{% url viewname %}?tag={{ tag.slug }}">{% endif %}<span class="badge" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span>{% if viewname %}</a>{% endif %}

View File

@ -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 = '<span class="text-muted">&mdash;</span>'
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'<div class="rendered-markdown">{html}</div>'
return mark_safe(html)
@register.filter('json')
def render_json(value):
"""
Render a dictionary as formatted JSON. This filter is invoked as "json":
{{ data_dict|json }}
"""
return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
@register.filter('yaml')
def render_yaml(value):
"""
Render a dictionary as formatted YAML. This filter is invoked as "yaml":
{{ data_dict|yaml }}
"""
return yaml.dump(json.loads(json.dumps(value)))

View File

@ -0,0 +1,54 @@
from django import template
register = template.Library()
@register.inclusion_tag('builtins/tag.html')
def tag(value, viewname=None):
"""
Display a tag, optionally linked to a filtered list of objects.
Args:
value: A Tag instance
viewname: If provided, the tag will be a hyperlink to the specified view's URL
"""
return {
'tag': value,
'viewname': viewname,
}
@register.inclusion_tag('builtins/badge.html')
def badge(value, bg_class='secondary', show_empty=False):
"""
Display the specified number as a badge.
Args:
value: The value to be displayed within the badge
bg_class: Bootstrap 5 background CSS name
show_empty: If true, display the badge even if value is None or zero
"""
return {
'value': value,
'bg_class': bg_class,
'show_empty': show_empty,
}
@register.inclusion_tag('builtins/checkmark.html')
def checkmark(value, show_false=True, true='Yes', false='No'):
"""
Display either a green checkmark or red X to indicate a boolean value.
Args:
value: True or False
show_false: Show false values
true: Text label for true values
false: Text label for false values
"""
return {
'value': bool(value),
'show_false': show_false,
'true_label': true,
'false_label': false,
}

View File

@ -1,24 +1,17 @@
import datetime import datetime
import decimal import decimal
import json
import re import re
from typing import Dict, Any from typing import Dict, Any
import yaml
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import date from django.template.defaultfilters import date
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils import timezone from django.utils import timezone
from django.utils.html import strip_tags
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from markdown import markdown
from netbox.config import get_config
from utilities.forms import get_selected_values, TableConfigForm from utilities.forms import get_selected_values, TableConfigForm
from utilities.markdown import StrikethroughExtension from utilities.utils import get_viewname
from utilities.utils import foreground_color, get_viewname
register = template.Library() register = template.Library()
@ -27,88 +20,6 @@ register = template.Library()
# Filters # Filters
# #
@register.filter()
def placeholder(value):
"""
Render a muted placeholder if value equates to False.
"""
if value not in ('', None):
return value
placeholder = '<span class="text-muted">&mdash;</span>'
return mark_safe(placeholder)
@register.filter(is_safe=True)
def render_markdown(value):
"""
Render text as 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'<div class="rendered-markdown">{html}</div>'
return mark_safe(html)
@register.filter()
def render_json(value):
"""
Render a dictionary as formatted JSON.
"""
return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
@register.filter()
def render_yaml(value):
"""
Render a dictionary as formatted YAML.
"""
return yaml.dump(json.loads(json.dumps(value)))
@register.filter()
def meta(obj, 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).
"""
return getattr(obj._meta, attr, '')
@register.filter()
def content_type(obj):
"""
Return the ContentType for the given object.
"""
return ContentType.objects.get_for_model(obj)
@register.filter()
def content_type_id(obj):
"""
Return the ContentType ID for the given object.
"""
content_type = ContentType.objects.get_for_model(obj)
if content_type:
return content_type.pk
return None
@register.filter() @register.filter()
def viewname(model, action): def viewname(model, action):
@ -133,14 +44,6 @@ def validated_viewname(model, action):
return None return None
@register.filter()
def bettertitle(value):
"""
Alternative to the builtin title(); uppercases words without replacing letters that are already uppercase.
"""
return ' '.join([w[0].upper() + w[1:] for w in value.split()])
@register.filter() @register.filter()
def humanize_speed(speed): def humanize_speed(speed):
""" """
@ -191,14 +94,6 @@ def simplify_decimal(value):
return str(value).rstrip('0').rstrip('.') return str(value).rstrip('0').rstrip('.')
@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')
@register.filter(expects_localtime=True) @register.filter(expects_localtime=True)
def annotated_date(date_value): def annotated_date(date_value):
""" """
@ -229,17 +124,6 @@ def annotated_now():
return annotated_date(datetime.datetime.now(tz=tzinfo)) return annotated_date(datetime.datetime.now(tz=tzinfo))
@register.filter()
def fgcolor(value):
"""
Return black (#000000) or white (#ffffff) given an arbitrary background color in RRGGBB format.
"""
value = value.lower().strip('#')
if not re.match('^[0-9a-f]{6}$', value):
return ''
return f'#{foreground_color(value)}'
@register.filter() @register.filter()
def divide(x, y): def divide(x, y):
""" """
@ -276,14 +160,6 @@ def has_perms(user, permissions_list):
return user.has_perms(permissions_list) return user.has_perms(permissions_list)
@register.filter()
def split(string, sep=','):
"""
Split a string by the given value (default: comma)
"""
return string.split(sep)
@register.filter() @register.filter()
def as_range(n): def as_range(n):
""" """
@ -403,46 +279,6 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
} }
@register.inclusion_tag('helpers/tag.html')
def tag(tag, url_name=None):
"""
Display a tag, optionally linked to a filtered list of objects.
"""
return {
'tag': tag,
'url_name': url_name,
}
@register.inclusion_tag('helpers/badge.html')
def badge(value, bg_class='secondary', show_empty=False):
"""
Display the specified number as a badge.
"""
return {
'value': value,
'bg_class': bg_class,
'show_empty': show_empty,
}
@register.inclusion_tag('helpers/checkmark.html')
def checkmark(value, show_false=True, true='Yes', false='No'):
"""
Display either a green checkmark or red X to indicate a boolean value.
:param show_false: Display a red X if the value is False
:param true: Text label for true value
:param false: Text label for false value
"""
return {
'value': bool(value),
'show_false': show_false,
'true_label': true,
'false_label': false,
}
@register.inclusion_tag('helpers/table_config_form.html') @register.inclusion_tag('helpers/table_config_form.html')
def table_config_form(table, table_name=None): def table_config_form(table, table_name=None):
return { return {