Conditional Redaction in Django Templates
2025-10-16Today I needed to redact names of users and some other sensitive strings from a significant part of a Django
application. After starting to litter template code with {% if allow_unredacted %}...{% else %}...{% endif %}
statements, I came to my senses and implemented a Custom Django Template Tag. That tag
automatically redacts content based on the request. Nothing crazy, maybe it inspires you to implement something similar?
The Starting Point
There were many templates along the lines of
Some action by <span class="fw-semibold">{{ event.actor|user_display_name }}</span>.
Dataset delivered from <span>{{ dataset.supplier.name }}</span>.
To be safe I wanted the values to be hidden by default and only reveal them, if
- the actor is the authenticated user or
- the user has other groups than the one named Consultant.
The Redaction Approach
I wanted to redact the text by rendering a replacement string and apply the .redacted CSS class.
.redacted {
filter: blur(8px);
user-select: none;
pointer-events: none;
}
That results in the following HTML, which renders as a blurred bar. Sneaky people using the DOM inspector to look up the value are left in the dark, too.
Some action by <span class="fw-semibold redacted">Nope Joe</span>.
Dataset delivered from <span class="redacted">Nope Joe</span>.
The Template Tag
Migrating the code to a custom template tag allowed me to uniformly redact users and arbitrary strings based on the current request and business requirements. First I made sure that we actually have access a request available to prevent any undefined behaviour later.
Continuing the flow, I am checking the users group membership and override the permission, if we get a user instance,
which actually is the user themself. As a small bonus I was able to apply the user_display_name within the tag, which
again spared some repetition in the templates.
Most importantly: based on the permission check, I replaced the value to some dummy string and added the .redacted CSS
class.
from typing import Union
from django import template
from django.contrib.auth.models import User
from django.utils.html import format_html
from django_backoffice.templatetags.backoffice_tags import user_display_name
from thetool.groups.group_consultant import GroupConsultant
register = template.Library()
@register.simple_tag(takes_context=True)
def conditionally_redacted(context, value: Union[str, User], *css_classes):
request = context.get("request")
if not request:
raise Exception(
"Pass a request to all templates containing the conditionally_redacted template tag"
)
allow_unredacted = not (GroupConsultant.is_only_group(request) or request.thetool_groups == [])
if isinstance(value, User):
allow_unredacted = allow_unredacted or value == request.user
value = user_display_name(value)
if not allow_unredacted:
css_classes += ("redacted",)
value = "Nope Joe"
return format_html(
"<span{}>{}</span>",
format_html(' class="{}"', " ".join(css_classes)) if css_classes else "",
value,
)
Using that template tag all redaction is done behind the scenes. Neat.
{% load common %}
Some action by {% conditionally_redacted event.actor "fw-semibold" %}.
Dataset delivered from {% conditionally_redacted dataset.supplier.name %}.
The Happy Dennis
I am happy about that implementation as I can easily assert its logic in unit tests and do not have to scatter the same checks over and over again through different views and templates.
© 2025 Dennis Stritzke
Code samples are public domain unless otherwise noted.