Skip to content

Template Tags

Added in: Milestone 2

Django-admin-deux provides custom template tags for common admin UI patterns.

Loading Template Tags

{% load djadmin_tags %}

Query Parameter Preservation

query_params_as_hidden_inputs

Purpose: Convert query parameters to hidden input fields for form submission.

Problem: When a form uses method="get", the form submission overrides the action URL's query string. This causes loss of other query parameters (filters, search, ordering, etc.).

Solution: Convert existing query parameters to hidden input fields, excluding the ones the form manages.

Signature:

{% query_params_as_hidden_inputs *exclude %}

Parameters: - *exclude - Variable number of parameters to exclude from hidden inputs - Accepts: strings, lists, dict_keys (from form.fields), or any iterable

Returns: Safe HTML string with hidden input fields

Example 1: Search Form

Preserve filters and ordering when searching:

{% load djadmin_tags %}

<form method="get" action="">
    {# Exclude 'search' (form manages it) and 'page' (reset to page 1) #}
    {% query_params_as_hidden_inputs 'search' 'page' %}

    <input type="search" name="search" value="{{ request.GET.search|default:'' }}">
    <button type="submit">Search</button>
</form>

Generated HTML (if URL is ?category=1&ordering=price&page=2):

<input type="hidden" name="category" value="1">
<input type="hidden" name="ordering" value="price">
<!-- 'page' excluded, 'search' excluded -->

Example 2: Filter Form

Preserve search and ordering when filtering:

{% load djadmin_tags %}

<form method="get" action="">
    {# Exclude filter fields (form manages them) and reset pagination #}
    {% query_params_as_hidden_inputs 'page' filterset.form.fields %}

    {{ filterset.form.as_p }}
    <button type="submit">Apply Filters</button>
</form>

Generated HTML (if URL is ?search=laptop&ordering=price&page=2&category=1):

<input type="hidden" name="search" value="laptop">
<input type="hidden" name="ordering" value="price">
<!-- 'page' excluded, 'category' excluded (in filterset.form.fields) -->

Example 3: Multiple Exclusions

Pass multiple parameters or iterables:

{% load djadmin_tags %}

{# Exclude individual parameters #}
{% query_params_as_hidden_inputs 'page' 'search' 'ordering' %}

{# Exclude parameters from dict_keys #}
{% query_params_as_hidden_inputs 'page' form.fields %}

{# Exclude parameters from list #}
{% query_params_as_hidden_inputs 'page' exclude_list %}

Multiple Values

Handles parameters with multiple values (e.g., tags=python&tags=django):

{# URL: ?tags=python&tags=django&category=1 #}
{% query_params_as_hidden_inputs 'category' %}

{# Generates: #}
<input type="hidden" name="tags" value="python">
<input type="hidden" name="tags" value="django">

HTML Safety

Values are properly escaped to prevent XSS:

# URL: ?name=<script>alert('xss')</script>
{% query_params_as_hidden_inputs %}

# Generates (escaped):
<input type="hidden" name="name" value="&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;">

Column Header Icons

render_column_header_icons

Purpose: Render icons in table column headers (e.g., sort indicators, help icons).

Signature:

{% render_column_header_icons column_name %}

Parameters: - column_name (str) - The field name of the column

Returns: Rendered HTML for all icons registered for this column

Example:

{% load djadmin_tags %}

<table>
    <thead>
        <tr>
            {% for column in columns %}
                <th>
                    {{ column.label }}
                    {% render_column_header_icons column.field %}
                </th>
            {% endfor %}
        </tr>
    </thead>
</table>

Generated HTML (with sorting enabled):

<th>
    Product Name
    <span class="column-header-icons">
        <a href="?ordering=name"
           title="Sort by name"
           class="column-header-icon">
            <svg>...</svg> <!-- Sort icon -->
        </a>
    </span>
</th>

With Multiple Icons:

If multiple icons are registered for a column (e.g., sort + help), they render in order:

<th>
    Price
    <span class="column-header-icons">
        <a href="?ordering=price" class="column-header-icon" title="Sort by price">
            <svg>...</svg> <!-- Sort icon (order=10) -->
        </a>
        <a href="/help/price" class="column-header-icon" title="Help about pricing">
            <svg>...</svg> <!-- Help icon (order=100) -->
        </a>
    </span>
</th>

No Icons:

If no icons are registered for the column, nothing is rendered:

{% render_column_header_icons 'description' %}
<!-- Renders: (nothing) -->

Implementation Details

query_params_as_hidden_inputs Implementation

# djadmin/templatetags/djadmin_tags.py

@register.simple_tag(takes_context=True)
def query_params_as_hidden_inputs(context, *exclude):
    """
    Convert query parameters to hidden input fields.

    Handles multiple value types for exclusions:
    - Strings: 'page', 'search'
    - dict_keys: form.fields
    - Lists/tuples: ['page', 'search']
    """
    request = context.get('request')
    if not request:
        return ''

    # Flatten all exclusions into a set
    exclude_set = set()
    for item in exclude:
        if isinstance(item, str):
            exclude_set.add(item)
        elif hasattr(item, '__iter__'):
            exclude_set.update(item)

    # Build hidden inputs for non-excluded parameters
    inputs = []
    for key in request.GET.keys():
        if key not in exclude_set:
            for value in request.GET.getlist(key):
                escaped_value = escape(value)
                inputs.append(
                    f'<input type="hidden" name="{key}" value="{escaped_value}">'
                )

    return mark_safe('\n'.join(inputs))

render_column_header_icons Implementation

# djadmin/templatetags/djadmin_tags.py

@register.inclusion_tag('djadmin/includes/_column_header_icons.html', takes_context=True)
def render_column_header_icons(context, column_name):
    """
    Render icons for a column header.

    Gets icons from view.column_header_icons and evaluates callables.
    """
    view = context.get('view')
    if not view or not hasattr(view, 'column_header_icons'):
        return {'icons': []}

    # Get icons for this column from the view
    all_icons = getattr(view, 'column_header_icons', [])

    # Evaluate callables and filter by condition
    icons = []
    for icon_config in all_icons:
        # Check condition
        if icon_config.condition and not icon_config.condition(view, column_name):
            continue

        # Evaluate callables
        icon = {
            'icon_template': icon_config.icon_template(view, column_name),
            'url': icon_config.url(view, column_name),
            'title': icon_config.title(view, column_name),
            'css_class': icon_config.css_class,
        }
        icons.append(icon)

    # Sort by order attribute
    icons.sort(key=lambda i: getattr(i, 'order', 100))

    return {'icons': icons}

Inclusion Template:

{# djadmin/templates/djadmin/includes/_column_header_icons.html #}
{% if icons %}
    <span class="column-header-icons">
        {% for icon in icons %}
            <a href="{{ icon.url }}"
               title="{{ icon.title }}"
               class="column-header-icon {{ icon.css_class }}">
                {% include icon.icon_template %}
            </a>
        {% endfor %}
    </span>
{% endif %}

Django 5.0+ Requirement

The {% querystring %} tag (used in pagination and clear buttons) requires Django 5.0+:

{# Pagination links - uses built-in querystring tag #}
<a href="{% querystring page=2 %}">Page 2</a>

{# Clear button - uses built-in querystring tag #}
<a href="{% querystring search=None page=None %}">Clear</a>

Why not use query_params_as_hidden_inputs everywhere?

  • querystring works for links (preserves query params in href)
  • query_params_as_hidden_inputs works for forms (form submission overrides query string)

They're complementary: - Links{% querystring %} - Forms{% query_params_as_hidden_inputs %}

Variable Assignment

assign

Purpose: Simple variable assignment for cleaner template logic.

Signature:

{% assign value as variable_name %}

Parameters: - value - Any value to assign (can be variable, literal, filter result, etc.)

Returns: The value passed to it

Example 1: Conditional Assignment

Assign different values based on conditions without using {% with %} inside {% if %}:

{% load djadmin_tags %}

{% if object %}
    {% filter_record_actions actions object as filtered_actions %}
{% else %}
    {% assign actions as filtered_actions %}
{% endif %}

{% for action in filtered_actions %}
    {{ action.label }}
{% endfor %}

Example 2: Filter Results

Assign the result of a filter operation:

{% load djadmin_tags %}

{% assign items|length as item_count %}
<p>Total items: {{ item_count }}</p>

Why {% assign %} Instead of {% with %}?

Django doesn't allow {% with %} inside {% if %} blocks (block nesting issues). The {% assign %} tag provides a cleaner alternative:

{# ❌ BAD - Causes block nesting errors #}
{% if condition %}
    {% with value as var %}
        ...
    {% endwith %}
{% else %}
    {% with other_value as var %}
        ...
    {% endwith %}
{% endif %}

{# ✅ GOOD - Clean and works correctly #}
{% if condition %}
    {% assign value as var %}
{% else %}
    {% assign other_value as var %}
{% endif %}
{{ var }}

Action Rendering

Action Button Partial

Template: djadmin/includes/_action_buttons.html

Purpose: Reusable partial for rendering action buttons consistently across all views.

Usage:

{% include "djadmin/includes/_action_buttons.html" with actions=action_list [object=obj] [include_current=False] [default_css_class="primary"] [use_list_items=False] %}

Parameters: - actions (required) - List of action instances to render - object (optional) - Model instance for per-object permission filtering - include_current (optional, default=False) - Whether to show current action - default_css_class (optional) - Fallback CSS class if action doesn't specify one - use_list_items (optional, default=False) - Wrap each action in <li> tags

Context Requirements: The following variables are automatically inherited from parent context: - action_namespace - For URL generation - action - Current action (for exclusion logic) - model_admin - For permission filtering - request - For permission filtering

Example 1: Edit View Footer (Record Actions)

{# djadmin/templates/djadmin/actions/edit.html #}
<footer>
  <button type="submit">Save Changes</button>
  <a href="{{ list_url }}">Cancel</a>

  {% if filtered_record_actions %}
    <menu>
      {% include "djadmin/includes/_action_buttons.html" with actions=filtered_record_actions object=object %}
    </menu>
  {% endif %}
</footer>

Example 2: List View Top Bar (General Actions)

{# djadmin/templates/djadmin/{app}/{model}_list.html or actions/list.html #}
<menu>
  {% include "djadmin/includes/_action_buttons.html" with actions=filtered_general_actions default_css_class="primary" use_list_items=True %}
</menu>

Example 3: List View Per-Row (Record Actions with Permission Filtering)

{# djadmin/templates/djadmin/{app}/{model}_list.html or actions/list.html #}
{% if record_actions %}
  <td data-label="Actions">
    <menu>
      {# Passing object= triggers automatic permission filtering #}
      {% include "djadmin/includes/_action_buttons.html" with actions=record_actions object=obj use_list_items=True %}
    </menu>
  </td>
{% endif %}

How Permission Filtering Works:

When object parameter is provided, the partial automatically uses the {% filter_record_actions %} tag to filter actions based on per-object permissions:

{# Inside _action_buttons.html #}
{% if object %}
    {# Per-object filtering for record actions #}
    {% filter_record_actions actions object as actions_to_render %}
{% else %}
    {# No filtering for general actions #}
    {% assign actions as actions_to_render %}
{% endif %}

URL Generation:

The partial handles URL generation automatically, supporting both: - Actions with pre-resolved URLs (action.url is set) - Actions requiring URL generation via template tags

{# Inside _action_buttons.html #}
{% if action_item.url %}
    {# Use pre-resolved URL #}
    <a href="{{ action_item.url }}">{{ action_item.label }}</a>
{% else %}
    {# Generate URL from action_namespace and url_name #}
    <a href="{% url action_namespace|add:':'|add:action_item.url_name pk=object.pk %}">
        {{ action_item.label }}
    </a>
{% endif %}

Benefits:

Single source of truth - All action button rendering in one place ✅ Automatic permission filtering - Pass object= for per-object checks ✅ Consistent styling - All action buttons render identically ✅ Easy maintenance - Update once, applies everywhere ✅ Flexible - Works for general, bulk, and record actions

See Also