Custom Template Tags & Filters
Buraq lets you register custom globals, filters, and tests for Jinja2 templates using simple decorators — no tag library files, no {% load %}, no boilerplate.
Why Buraq’s approach is better
Section titled “Why Buraq’s approach is better”Step 1 — create templatetags.py in your app:
from buraq.template import register
@register.globaldef format_price(amount): return f"${amount:,.2f}"Step 2 — use it anywhere, no imports needed:
{{ format_price(product.price) }}{{ format_price(product.price, "$") }}That’s it. Buraq auto-discovers the file at startup and registers everything globally.
Step 1 — create the directory and __init__.py:
myapp/└── templatetags/ ├── __init__.py ← required, even if empty └── myapp_tags.pyStep 2 — instantiate Library() and decorate:
from django import template
register = template.Library()
@register.simple_tagdef format_price(amount): return f"${amount:,.2f}"Step 3 — {% load %} in every template that uses it:
{% load myapp_tags %}{% format_price product.price %}Forget {% load %} in one template → silent failure at render time.
Why Buraq wins:
- Simpler — 1 file, 1 decorator, no
{% load %}ever vs 5 steps (directory,__init__.py, tag file,Library(),{% load %}in every template) - Safer — registration errors are caught at startup, not silently at render time when a template forgets
{% load %} - Faster — no
{% load %}parsing on every request; all tags registered once intoenv.globalsat startup - More powerful —
@register.globalexposes a full Python callable with natural argument and keyword-argument support ({{ fn(a, b, key=val) }});simple_tagcannot be called with keyword args as naturally in template syntax
Create a templatetags.py file inside any app listed in INSTALLED_APPS:
myapp/├── models.py├── views.py├── urls.py└── templatetags.py ← create thisBuraq auto-discovers and imports every templatetags.py at startup. Everything registered inside is available in all templates immediately.
Globals
Section titled “Globals”A global is a callable available anywhere in a template — called like a function.
from buraq.template import register
@register.globaldef format_price(amount, currency="$"): return f"{currency}{amount:,.2f}"
@register.globaldef now(): from buraq.utils.timezone import now as _now return _now()<span>{{ format_price(product.price) }}</span><span>{{ format_price(product.price, currency="€") }}</span><footer>Generated at {{ now() }}</footer>Filters
Section titled “Filters”A filter transforms a value via the | pipe syntax.
from buraq.template import register
@register.filterdef truncate(value, length=100): value = str(value) return value[:length] + "…" if len(value) > length else value
@register.filterdef currency(amount, symbol="$"): return f"{symbol}{float(amount):,.2f}"
@register.filterdef initials(name): return "".join(w[0].upper() for w in name.split() if w){{ product.description|truncate(150) }}{{ order.total|currency("£") }}{{ user.full_name|initials }}A test is used in {% if x is mytest %} conditions.
from buraq.template import register
@register.testdef even(value): return int(value) % 2 == 0
@register.testdef published(post): return post.published and post.published_at is not None{% if loop.index is even %} <tr class="alt-row">{% endif %}
{% if article is published %} <span class="badge">Live</span>{% endif %}Safe output (disable auto-escaping)
Section titled “Safe output (disable auto-escaping)”By default Jinja2 auto-escapes output. If your function returns trusted HTML, mark it safe with is_safe=True:
from buraq.template import register
@register.global(is_safe=True)def icon(name): return f'<svg class="icon icon-{name}"><use href="#{name}"/></svg>'
@register.filter(is_safe=True)def highlight(value, term): return str(value).replace(term, f"<mark>{term}</mark>"){{ icon("search") }} {# rendered as raw HTML, not escaped #}{{ article.body|highlight(query) }}Custom name
Section titled “Custom name”Use name= to register the function under a different name in templates:
from buraq.template import register
@register.global(name="static")def static_url(path): from buraq.conf import settings return f"{settings.STATIC_URL}{path}"
@register.filter(name="nl2br", is_safe=True)def newline_to_br(value): return str(value).replace("\n", "<br>")<img src="{{ static('images/logo.png') }}">{{ post.body|nl2br }}Accessing request context
Section titled “Accessing request context”Globals and filters are plain Python functions — pass request explicitly from the view if needed, or read context from contextvars:
@register.globaldef active_language(): from buraq.utils.translation import get_language return get_language() # reads from LocaleMiddleware's ContextVarReference
Section titled “Reference”| Decorator | Template usage | Description |
|---|---|---|
@register.global |
{{ fn(args) }} |
Callable available everywhere |
@register.filter |
{{ value|fn(args) }} |
Transforms a value |
@register.test |
{% if x is fn %} |
Boolean check |
All decorators accept:
| Option | Type | Description |
|---|---|---|
name |
str |
Override the name used in templates |
is_safe |
bool |
Wrap output in Markup to skip auto-escaping |
Built-in filters
Section titled “Built-in filters”Buraq registers 37 filters and 10 globals automatically — no {% load %} needed.
They live in buraq/template/builtins.py and are installed via
register_builtins(env) at startup. Jinja’s own 54 filters are there too, so
lower, join, length, slice and the rest work under the names you expect.
Built-in globals
Section titled “Built-in globals”Called, not written as tags — Jinja has no tag-registration syntax.
| Global | Usage |
|---|---|
url(name, **params) |
{{ url("post-detail", pk=post.id) }} |
static(path) |
{{ static("css/site.css") }} — hashed when manifest storage is on |
csrf_token(request) / csrf_input(request) |
the token, and a ready-made hidden input |
now(fmt) |
{{ now("Y-m-d") }} |
firstof(*values, default="") |
{{ firstof(user.nickname, user.name, default="Anonymous") }} |
widthratio(value, max, width) |
{{ widthratio(votes, total, 100) }} — a bar’s width |
querystring(request, **changes) |
{{ querystring(request, page=2) }} — keeps the current filters |
cycle(*values) |
alternating row classes |
regroup(items, key) |
{% set groups = regroup(people, "city") %} |
ifchanged(value) |
true only when the value differs from last time |
spaceless(html) |
strips whitespace between tags |
get_messages(request) |
the flash messages for this request |
firstof earns its place over Jinja’s |default, which only catches an
undefined name — not the empty string a blank form field or missing column
actually is. querystring keeps the filters and page a visitor already has
while changing one of them, which is otherwise a rebuild of the whole string
by hand:
<a href="{{ querystring(request, page=page.next) }}">Next</a>Pass None to drop a parameter, or a list to repeat one.
Date & time
Section titled “Date & time”| Filter | Usage |
|---|---|
date |
{{ dt|date("Y-m-d") }} — full Django format-code set |
time |
{{ dt|time("H:i:s") }} |
timesince |
{{ dt|timesince }} → "2 hours, 5 minutes" |
timeuntil |
{{ dt|timeuntil }} → "3 days" |
| Filter | Usage |
|---|---|
truncatechars(n) |
{{ text|truncatechars(50) }} |
truncatewords(n) |
{{ text|truncatewords(10) }} |
wordcount |
{{ body|wordcount }} |
capfirst |
{{ name|capfirst }} |
addslashes |
{{ val|addslashes }} |
slugify |
{{ title|slugify }} |
linenumbers |
{{ code|linenumbers }} |
pluralize |
{{ n|pluralize }} or {{ n|pluralize("y,ies") }} |
yesno |
{{ flag|yesno("yes,no,maybe") }} |
default_if_none |
{{ val|default_if_none("—") }} |
phone2numeric |
{{ "HELLO"|phone2numeric }} → "43556" |
floatformat(n) |
{{ 3.14159|floatformat(2) }} → "3.14" |
stringformat(spec) |
{{ 5|stringformat("03d") }} → "005" — Python’s % syntax without the % |
add(n) |
{{ 4|add(6) }} → 10; joins lists and strings when they are not numbers |
divisibleby(n) |
{{ 9|divisibleby(3) }} → True |
get_digit(n) |
{{ 123456789|get_digit(2) }} → 8 — counted from the right |
escapeseq |
{{ items|escapeseq|join(", ") }} — escapes each item, not the joined string |
safeseq |
{{ items|safeseq|join(", ") }} — marks each item safe |
HTML (marked safe — output not auto-escaped)
Section titled “HTML (marked safe — output not auto-escaped)”| Filter | Usage |
|---|---|
linebreaks |
{{ body|linebreaks }} |
linebreaksbr |
{{ body|linebreaksbr }} |
urlize |
{{ text|urlize }} |
escapejs |
var x = "{{ val|escapejs }}" |
json_script(id) |
{{ data|json_script("app-data") }} |
filesizeformat |
{{ size|filesizeformat }} → "1.2 MB" |