2026-02-20 22:08:04 +01:00

14 KiB
Raw Permalink Blame History

name: django-templates description: Creates Django 6.0 HTML templates with partials, HTMX integration, and modern best practices. Use for template creation, refactoring, or HTMX endpoints. argument-hint: [template-name] [--partial|--htmx|--base] allowed-tools: Read, Write, Edit, Glob, Grep

Django 6.0 HTML Templates

Generate production-ready Django 6.0.2 templates with Template Partials, HTMX integration, CSP support, and modern best practices for the league-planner project.

When to Use

  • Creating new HTML templates with Django 6.0 features
  • Refactoring templates to use Template Partials
  • Building HTMX-powered dynamic components
  • Setting up base templates with proper block structure
  • Implementing reusable template fragments

Prerequisites

  • Django 6.0+ installed
  • Templates directory configured in settings
  • For HTMX: django-htmx package (optional but recommended)

Instructions

Step 1: Analyze Request

Parse $ARGUMENTS to determine:

  • Template name: The target template file
  • Type flag:
    • --partial: Create reusable partial fragments
    • --htmx: HTMX-enabled template with partial endpoints
    • --base: Base template with block structure
    • (none): Standard template

Step 2: Check Existing Templates

# Find existing templates in league-planner
find . -path "*/templates/*.html" -type f

Review the project's template structure and naming conventions.

Step 3: Generate Template

Apply the appropriate pattern from the examples below.


Django 6.0 Template Features

Template Partials (NEW in 6.0)

Define reusable fragments without separate files:

{# Define a partial #}
{% partialdef card %}
    <div class="card">
        <h3>{{ title }}</h3>
        <p>{{ content }}</p>
    </div>
{% endpartialdef %}

{# Render the partial multiple times #}
{% partial card %}
{% partial card %}

Inline Option - Render immediately AND save for reuse:

{% partialdef filter_controls inline %}
    <form method="get" class="filters">
        {{ filter_form.as_p }}
        <button type="submit">Filter</button>
    </form>
{% endpartialdef %}

{# Can still reuse later #}
{% partial filter_controls %}

Accessing Partials from Views (HTMX Pattern)

Render only a specific partial:

# views.py
from django.shortcuts import render

def update_component(request, pk):
    obj = MyModel.objects.get(pk=pk)
    # Render ONLY the partial named "item_row"
    return render(request, "myapp/list.html#item_row", {"item": obj})

forloop.length (NEW in 6.0)

Access total loop count:

{% for item in items %}
    <div>
        {{ item.name }}
        <span class="counter">({{ forloop.counter }}/{{ forloop.length }})</span>
    </div>
{% endfor %}

querystring Tag Improvements

Build query strings cleanly:

{# Basic usage #}
<a href="?{% querystring page=1 %}">First Page</a>

{# Modify current query params #}
<a href="?{% querystring page=page.next_page_number %}">Next</a>

{# Remove a parameter #}
<a href="?{% querystring filter=None %}">Clear Filter</a>

{# Multiple mappings (NEW in 6.0) #}
<a href="?{% querystring base_params extra_params page=1 %}">Reset</a>

CSP Nonce Support

For inline scripts with Content Security Policy:

{# In settings: add 'django.template.context_processors.csp' #}

<script nonce="{{ csp_nonce }}">
    // Safe inline script
</script>

Patterns & Best Practices

Pattern 1: Base Template with Blocks

{# templates/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}League Planner{% endblock %}</title>

    {% block extra_css %}{% endblock %}
</head>
<body>
    {% block navbar %}
        {% include "includes/navbar.html" %}
    {% endblock %}

    <main class="container">
        {% block messages %}
            {% if messages %}
                {% for message in messages %}
                    <div class="alert alert-{{ message.tags }}">{{ message }}</div>
                {% endfor %}
            {% endif %}
        {% endblock %}

        {% block content %}{% endblock %}
    </main>

    {% block footer %}
        {% include "includes/footer.html" %}
    {% endblock %}

    {% block extra_js %}{% endblock %}
</body>
</html>

Pattern 2: List Template with Partials

{# templates/scheduler/scenario_list.html #}
{% extends "base.html" %}

{% block title %}Szenarien{% endblock %}

{% block content %}
<div class="scenario-list">
    <h1>Szenarien</h1>

    {# Define row partial for HTMX updates #}
    {% partialdef scenario_row %}
        <tr id="scenario-{{ scenario.pk }}">
            <td>{{ scenario.name }}</td>
            <td>{{ scenario.season }}</td>
            <td>{{ scenario.get_status_display }}</td>
            <td>
                <a href="{% url 'scenario-detail' scenario.pk %}"
                   class="btn btn-sm btn-primary">Details</a>
                <button hx-delete="{% url 'scenario-delete' scenario.pk %}"
                        hx-target="#scenario-{{ scenario.pk }}"
                        hx-swap="outerHTML"
                        hx-confirm="Wirklich löschen?"
                        class="btn btn-sm btn-danger">
                    Löschen
                </button>
            </td>
        </tr>
    {% endpartialdef %}

    <table class="table">
        <thead>
            <tr>
                <th>Name</th>
                <th>Saison</th>
                <th>Status</th>
                <th>Aktionen</th>
            </tr>
        </thead>
        <tbody id="scenario-table-body">
            {% for scenario in scenarios %}
                {% partial scenario_row %}
            {% empty %}
                <tr><td colspan="4">Keine Szenarien vorhanden.</td></tr>
            {% endfor %}
        </tbody>
    </table>
</div>
{% endblock %}

Pattern 3: HTMX-Powered Form

{# templates/scheduler/scenario_form.html #}
{% extends "base.html" %}

{% block content %}
<div class="form-container">
    <h1>{% if scenario.pk %}Szenario bearbeiten{% else %}Neues Szenario{% endif %}</h1>

    {# Form partial for HTMX validation #}
    {% partialdef scenario_form inline %}
        <form method="post"
              hx-post="{% url 'scenario-create' %}"
              hx-target="#form-container"
              hx-swap="innerHTML">
            {% csrf_token %}

            {% for field in form %}
                <div class="form-group {% if field.errors %}has-error{% endif %}">
                    <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                    {{ field }}
                    {% if field.errors %}
                        <span class="error">{{ field.errors.0 }}</span>
                    {% endif %}
                    {% if field.help_text %}
                        <small class="help-text">{{ field.help_text }}</small>
                    {% endif %}
                </div>
            {% endfor %}

            <div class="form-actions">
                <button type="submit" class="btn btn-primary">
                    {% if scenario.pk %}Speichern{% else %}Erstellen{% endif %}
                </button>
                <a href="{% url 'scenario-list' %}" class="btn btn-secondary">Abbrechen</a>
            </div>
        </form>
    {% endpartialdef %}
</div>
{% endblock %}

Pattern 4: Detail Page with Inline Editing

{# templates/scheduler/scenario_detail.html #}
{% extends "base.html" %}

{% block content %}
<article class="scenario-detail">
    {# Header partial - editable via HTMX #}
    {% partialdef scenario_header inline %}
        <header id="scenario-header">
            <h1>{{ scenario.name }}</h1>
            <p class="meta">
                Saison: {{ scenario.season }} |
                Status: {{ scenario.get_status_display }} |
                Erstellt: {{ scenario.created_at|date:"d.m.Y H:i" }}
            </p>
            <button hx-get="{% url 'scenario-edit-header' scenario.pk %}"
                    hx-target="#scenario-header"
                    hx-swap="outerHTML"
                    class="btn btn-sm btn-outline">
                Bearbeiten
            </button>
        </header>
    {% endpartialdef %}

    {# Games list partial #}
    {% partialdef games_list %}
        <section id="games-list" class="games">
            <h2>Spiele ({{ games|length }}/{{ games|length }})</h2>
            <ul>
                {% for game in games %}
                    <li>
                        {{ game.home_team }} vs {{ game.away_team }}
                        <span class="date">{{ game.date|date:"d.m.Y" }}</span>
                    </li>
                {% endfor %}
            </ul>
        </section>
    {% endpartialdef %}

    {% partial games_list %}
</article>
{% endblock %}

Pattern 5: Modal with Partial

{# templates/includes/modal.html #}
{% partialdef modal_container %}
    <div id="modal-container"
         class="modal {% if show %}show{% endif %}"
         hx-swap-oob="true">
        {% if show %}
            <div class="modal-backdrop" hx-get="" hx-target="#modal-container"></div>
            <div class="modal-content">
                {% block modal_header %}
                    <header class="modal-header">
                        <h3>{{ modal_title }}</h3>
                        <button hx-get="" hx-target="#modal-container" class="close">&times;</button>
                    </header>
                {% endblock %}

                {% block modal_body %}
                    {{ modal_content }}
                {% endblock %}

                {% block modal_footer %}
                    <footer class="modal-footer">
                        <button hx-get="" hx-target="#modal-container" class="btn btn-secondary">
                            Abbrechen
                        </button>
                        {% block modal_actions %}{% endblock %}
                    </footer>
                {% endblock %}
            </div>
        {% endif %}
    </div>
{% endpartialdef %}

Pattern 6: Pagination with querystring

{# templates/includes/pagination.html #}
{% if page_obj.has_other_pages %}
<nav class="pagination" aria-label="Seitennavigation">
    <ul>
        {% if page_obj.has_previous %}
            <li>
                <a href="?{% querystring page=1 %}">« Erste</a>
            </li>
            <li>
                <a href="?{% querystring page=page_obj.previous_page_number %}"> Zurück</a>
            </li>
        {% endif %}

        <li class="current">
            Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}
        </li>

        {% if page_obj.has_next %}
            <li>
                <a href="?{% querystring page=page_obj.next_page_number %}">Weiter </a>
            </li>
            <li>
                <a href="?{% querystring page=page_obj.paginator.num_pages %}">Letzte »</a>
            </li>
        {% endif %}
    </ul>
</nav>
{% endif %}

HTMX View Pattern

For HTMX endpoints that render partials:

# views.py
from django.shortcuts import render, get_object_or_404
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET"])
def scenario_row(request, pk):
    """Render single scenario row partial for HTMX."""
    scenario = get_object_or_404(Scenario, pk=pk)
    # Syntax: "template.html#partial_name"
    return render(request, "scheduler/scenario_list.html#scenario_row", {
        "scenario": scenario
    })

@require_http_methods(["DELETE"])
def scenario_delete(request, pk):
    """Delete scenario and return empty response for HTMX swap."""
    scenario = get_object_or_404(Scenario, pk=pk)
    scenario.delete()
    # Return empty string - HTMX will remove the row
    return HttpResponse("")

@require_http_methods(["GET", "POST"])
def scenario_edit_header(request, pk):
    """Inline edit scenario header via HTMX."""
    scenario = get_object_or_404(Scenario, pk=pk)

    if request.method == "POST":
        form = ScenarioHeaderForm(request.POST, instance=scenario)
        if form.is_valid():
            form.save()
            # Return the display partial
            return render(request, "scheduler/scenario_detail.html#scenario_header", {
                "scenario": scenario
            })
    else:
        form = ScenarioHeaderForm(instance=scenario)

    # Return edit form partial
    return render(request, "scheduler/scenario_edit_header.html", {
        "scenario": scenario,
        "form": form
    })

Template Organization (league-planner)

templates/
├── base.html                    # Main base template
├── includes/
│   ├── navbar.html
│   ├── footer.html
│   ├── pagination.html
│   └── modal.html
├── scheduler/
│   ├── scenario_list.html       # With partials for rows
│   ├── scenario_detail.html     # With partials for sections
│   ├── scenario_form.html       # With form partial
│   └── _scenario_row.html       # Standalone partial (legacy)
├── draws/
│   └── ...
└── qualifiers/
    └── ...

Common Pitfalls

  • Partial nicht gefunden: Partial-Namen müssen exakt matchen, keine Leerzeichen
  • Context fehlt: Partials erben den Context - stelle sicher, dass Variablen verfügbar sind
  • HTMX Swap-Probleme: Verwende IDs für präzises Targeting (hx-target="#element-id")
  • CSP Violations: Inline-Styles/Scripts brauchen nonce="{{ csp_nonce }}" wenn CSP aktiv
  • N+1 in Templates: Verwende select_related/prefetch_related in Views, nicht im Template
  • forloop.length Performance: Bei großen Listen kann dies teuer sein (zählt vorab)

Deprecation Warning

⚠️ urlize Filter: Default-Protokoll wechselt von HTTP zu HTTPS in Django 7.0. Setze URLIZE_ASSUME_HTTPS = True in settings.py für Vorwärtskompatibilität.


Verification

Test the template:

# Check for template syntax errors
python manage.py check --deploy

# Render template in shell
python manage.py shell
>>> from django.template.loader import render_to_string
>>> html = render_to_string('scheduler/scenario_list.html', {'scenarios': []})
>>> print(html)

# Test partial rendering
>>> html = render_to_string('scheduler/scenario_list.html#scenario_row', {'scenario': scenario})