Template Tags¶
Added in: Milestone 2
Django-admin-deux provides custom template tags for common admin UI patterns.
Loading Template 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:
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="<script>alert('xss')</script>">
Column Header Icons¶
render_column_header_icons¶
Purpose: Render icons in table column headers (e.g., sort indicators, help icons).
Signature:
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:
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?
querystringworks for links (preserves query params in href)query_params_as_hidden_inputsworks 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:
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¶
- Column Header Icons Guide - Using column header icons
- Sidebar Widgets Guide - Using sidebar widgets
- Search Guide - Search functionality
- Actions Guide - Understanding the action system
- Django 5.0 querystring tag - Built-in tag