508 lines
14 KiB
Markdown
508 lines
14 KiB
Markdown
---
|
||
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">×</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})
|
||
```
|