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

1import datetime 

2import json 

3import re 

4 

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 

14 

15from netbox.config import get_config 

16from utilities.html import clean_html, foreground_color 

17from utilities.markdown import StrikethroughExtension 

18from utilities.string import title 

19 

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) 

38 

39register = template.Library() 

40 

41 

42# 

43# General 

44# 

45 

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. 

52 

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 '' 

57 

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) 

64 

65 

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) 

73 

74 

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. 

80 

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)}' 

90 

91 

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) 

98 

99 

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). 

105 

106 Args: 

107 model: A Django model class or instance 

108 attr: The attribute name 

109 """ 

110 return getattr(model._meta, attr, '') 

111 

112 

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 

120 

121 return mark_safe('<span class="text-muted">&mdash;</span>') 

122 

123 

124@register.filter() 

125def split(value, separator=','): 

126 """ 

127 Wrapper for Python's `split()` string method. 

128 

129 Args: 

130 value: A string 

131 separator: String on which the value will be split 

132 """ 

133 return value.split(separator) 

134 

135 

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') 

142 

143 

144# 

145# Content types 

146# 

147 

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) 

154 

155 

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 

165 

166 

167# 

168# Rendering 

169# 

170 

171@register.filter('markdown', is_safe=True) 

172def render_markdown(value): 

173 """ 

174 Render a string as Markdown. This filter is invoked as "markdown": 

175 

176 {{ md_source_text|markdown }} 

177 """ 

178 if not value: 

179 return '' 

180 

181 # Render Markdown 

182 html = markdown(value, extensions=[ 

183 'def_list', 

184 'fenced_code', 

185 StrikethroughExtension(), 

186 TableExtension(use_align_attribute=True), 

187 ]) 

188 

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>' 

192 

193 schemes = get_config().ALLOWED_URL_SCHEMES 

194 

195 # Sanitize HTML 

196 html = clean_html(html, schemes) 

197 

198 return mark_safe(html) 

199 

200 

201@register.filter('json') 

202def render_json(value): 

203 """ 

204 Render a dictionary as formatted JSON. This filter is invoked as "json": 

205 

206 {{ data_dict|json }} 

207 """ 

208 return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True) 

209 

210 

211@register.filter('yaml') 

212def render_yaml(value): 

213 """ 

214 Render a dictionary as formatted YAML. This filter is invoked as "yaml": 

215 

216 {{ data_dict|yaml }} 

217 """ 

218 return yaml.dump(json.loads(json.dumps(value))) 

219 

220 

221# 

222# Time & date 

223# 

224 

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 '' 

235 

236 

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 '' 

245 

246 

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>') 

254 

255 

256@register.filter 

257def truncate_middle(value, length): 

258 if len(value) <= length: 

259 return value 

260 

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):] 

265 

266 return mark_safe(f"{first_part}&hellip;{second_part}")