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

508 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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
```bash
# 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:
```django
{# 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:
```django
{% 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:
```python
# 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:
```django
{% for item in items %}
<div>
{{ item.name }}
<span class="counter">({{ forloop.counter }}/{{ forloop.length }})</span>
</div>
{% endfor %}
```
### querystring Tag Improvements
Build query strings cleanly:
```django
{# 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:
```django
{# 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
```django
{# 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
```django
{# 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
```django
{# 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
```django
{# 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
```django
{# 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
```django
{# 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:
```python
# 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:
```bash
# 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})
```