Coverage for utilities/templatetags/builtins/filters.py: 39%
111 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import datetime
2import json
3import re
5import yaml
6from django import template
7from django.contrib.contenttypes.models import ContentType
8from django.contrib.humanize.templatetags.humanize import naturalday, naturaltime
9from django.utils.html import escape
10from django.utils.safestring import mark_safe
11from django.utils.timezone import localtime
12from markdown import markdown
13from markdown.extensions.tables import TableExtension
15from netbox.config import get_config
16from utilities.html import clean_html, foreground_color
17from utilities.markdown import StrikethroughExtension
18from utilities.string import title
20__all__ = (
21 'bettertitle',
22 'content_type',
23 'content_type_id',
24 'fgcolor',
25 'getattr_',
26 'isodate',
27 'isodatetime',
28 'isotime',
29 'linkify',
30 'meta',
31 'placeholder',
32 'render_json',
33 'render_markdown',
34 'render_yaml',
35 'split',
36 'tzoffset',
37)
39register = template.Library()
42#
43# General
44#
46@register.filter()
47def linkify(instance, attr=None):
48 """
49 Render a hyperlink for an object with a `get_absolute_url()` method, optionally specifying the name of an
50 attribute to use for the link text. If no attribute is given, the object's string representation will be
51 used.
53 If the object has no `get_absolute_url()` method, return the text without a hyperlink element.
54 """
55 if instance is None:
56 return ''
58 text = getattr(instance, attr) if attr is not None else str(instance)
59 try:
60 url = instance.get_absolute_url()
61 return mark_safe(f'<a href="{url}">{escape(text)}</a>')
62 except (AttributeError, TypeError):
63 return escape(text)
66@register.filter()
67def bettertitle(value):
68 """
69 Alternative to the builtin title(). Ensures that the first letter of each word is uppercase but retains the
70 original case of all others.
71 """
72 return title(value)
75@register.filter()
76def fgcolor(value, dark='000000', light='ffffff'):
77 """
78 Return black (#000000) or white (#ffffff) given an arbitrary background color in RRGGBB format. The foreground
79 color with the better contrast is returned.
81 Args:
82 value: The background color
83 dark: The foreground color to use for light backgrounds
84 light: The foreground color to use for dark backgrounds
85 """
86 value = value.lower().strip('#')
87 if not re.match('^[0-9a-f]{6}$', value):
88 return ''
89 return f'#{foreground_color(value, dark, light)}'
92@register.filter('getattr')
93def getattr_(instance, name):
94 """
95 Call getattr() on the object for the specified attribute.
96 """
97 return getattr(instance, name, None)
100@register.filter()
101def meta(model, attr):
102 """
103 Return the specified Meta attribute of a model. This is needed because Django does not permit templates
104 to access attributes which begin with an underscore (e.g. _meta).
106 Args:
107 model: A Django model class or instance
108 attr: The attribute name
109 """
110 return getattr(model._meta, attr, '')
113@register.filter()
114def placeholder(value):
115 """
116 Render a muted placeholder if the value equates to False.
117 """
118 if value not in ('', None):
119 return value
121 return mark_safe('<span class="text-muted">—</span>')
124@register.filter()
125def split(value, separator=','):
126 """
127 Wrapper for Python's `split()` string method.
129 Args:
130 value: A string
131 separator: String on which the value will be split
132 """
133 return value.split(separator)
136@register.filter()
137def tzoffset(value):
138 """
139 Returns the hour offset of a given time zone using the current time.
140 """
141 return datetime.datetime.now(value).strftime('%z')
144#
145# Content types
146#
148@register.filter()
149def content_type(model):
150 """
151 Return the ContentType for the given object.
152 """
153 return ContentType.objects.get_for_model(model)
156@register.filter()
157def content_type_id(model):
158 """
159 Return the ContentType ID for the given object.
160 """
161 content_type = ContentType.objects.get_for_model(model)
162 if content_type:
163 return content_type.pk
164 return None
167#
168# Rendering
169#
171@register.filter('markdown', is_safe=True)
172def render_markdown(value):
173 """
174 Render a string as Markdown. This filter is invoked as "markdown":
176 {{ md_source_text|markdown }}
177 """
178 if not value:
179 return ''
181 # Render Markdown
182 html = markdown(value, extensions=[
183 'def_list',
184 'fenced_code',
185 StrikethroughExtension(),
186 TableExtension(use_align_attribute=True),
187 ])
189 # If the string is not empty wrap it in rendered-markdown to style tables
190 if html:
191 html = f'<div class="rendered-markdown">{html}</div>'
193 schemes = get_config().ALLOWED_URL_SCHEMES
195 # Sanitize HTML
196 html = clean_html(html, schemes)
198 return mark_safe(html)
201@register.filter('json')
202def render_json(value):
203 """
204 Render a dictionary as formatted JSON. This filter is invoked as "json":
206 {{ data_dict|json }}
207 """
208 return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
211@register.filter('yaml')
212def render_yaml(value):
213 """
214 Render a dictionary as formatted YAML. This filter is invoked as "yaml":
216 {{ data_dict|yaml }}
217 """
218 return yaml.dump(json.loads(json.dumps(value)))
221#
222# Time & date
223#
225@register.filter()
226def isodate(value):
227 if type(value) is datetime.date:
228 text = value.isoformat()
229 return mark_safe(f'<span title="{naturalday(value)}">{text}</span>')
230 if type(value) is datetime.datetime:
231 local_value = localtime(value) if value.tzinfo else value
232 text = local_value.date().isoformat()
233 return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
234 return ''
237@register.filter()
238def isotime(value, spec='seconds'):
239 if type(value) is datetime.time:
240 return value.isoformat(timespec=spec)
241 if type(value) is datetime.datetime:
242 local_value = localtime(value) if value.tzinfo else value
243 return local_value.time().isoformat(timespec=spec)
244 return ''
247@register.filter()
248def isodatetime(value, spec='seconds'):
249 if type(value) is datetime.datetime:
250 text = f'{isodate(value)} {isotime(value, spec=spec)}'
251 else:
252 return ''
253 return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
256@register.filter
257def truncate_middle(value, length):
258 if len(value) <= length:
259 return value
261 # Calculate split points for the two parts
262 half_len = (length - 1) // 2 # 1 for the ellipsis
263 first_part = value[:half_len]
264 second_part = value[len(value) - (length - 1 - half_len):]
266 return mark_safe(f"{first_part}…{second_part}")