Conditional Redaction in Django Templates

2025-10-16

Today 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 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&nbsp;Joe</span>.
Dataset delivered from <span class="redacted">Nope&nbsp;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&nbsp;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.