Les 3 · Module 1 · Flask-basis
Jinja-templates en overerving
Haal de HTML uit je Python-code: render Jinja-templates met variabelen, lussen en filters, en geef elke pagina dezelfde basislayout met overerving.
🎯 Na deze les kun je
- een template renderen met render_template en er variabelen aan meegeven
- in Jinja lussen, voorwaarden en filters gebruiken
- een basistemplate maken en die met extends en block uitbreiden
- uitleggen waarom Jinja automatisch escapet en wanneer dat misgaat
Na les 2 staat je HTML als f-strings in app.py. Dat werkt voor één regel, maar een bon met een tabel,
een formulier en een menu wordt zo snel onleesbaar. Je ziet niet meer waar Python ophoudt en HTML begint,
en je moet zelf aan escape() denken. Jinja is de template-taal van Flask: je schrijft gewone HTML-bestanden
met een paar gaten erin, en Flask vult die gaten met jouw gegevens.
In deze les verhuist alle HTML van de kassa naar templates. Aan het eind heeft elke pagina dezelfde kop en hetzelfde menu, en staat dat maar op één plek.
Waar we gebleven waren
Je hebt kassa-web/app.py met de routes /, /totaal/..., /fooi/..., /kassa/toevoegen, /bon en
/kassa/leegmaken. De bon is een lijst bon met tuples (product, prijs). Start de server weer:
cd kassa-web && source .venv/bin/activate
flask --app app run --debug
De map templates
Flask zoekt templates standaard in een map templates naast app.py:
kassa-web/
├── app.py
└── templates/
└── bon.html
Maak templates/bon.html:
<!doctype html>
<html lang="nl">
<head><meta charset="utf-8"><title>Bon</title></head>
<body>
<h1>Bon</h1>
<ul>
{% for product, prijs in regels %}
<li>{{ product }}: {{ "%.2f"|format(prijs) }} euro</li>
{% else %}
<li>De bon is nog leeg.</li>
{% endfor %}
</ul>
<p>Totaal: {{ "%.2f"|format(totaal) }} euro</p>
</body>
</html>
En vervang de route in app.py:
from flask import render_template
@app.route("/bon")
def bon_tonen():
totaal = sum((prijs for _, prijs in bon), Decimal("0"))
return render_template("bon.html", regels=bon, totaal=totaal)
render_template leest het bestand, vult het met de keyword arguments die je meegeeft en geeft een string
terug. Die string wordt de body van het antwoord.
De drie soorten haakjes
| Syntax | Betekenis | Voorbeeld |
|---|---|---|
{{ … }} |
expressie: waarde invullen | {{ product }} |
{% … %} |
statement: lus, voorwaarde, blok | {% for x in lijst %} … {% endfor %} |
{# … #} |
commentaar, komt niet in de HTML | {# TODO: korting #} |
Een for in Jinja kent een {% else %}: dat deel verschijnt als de lijst leeg is. Binnen een lus heb je
loop.index (vanaf 1), loop.first en loop.last.
Voorspel eerst: wat toont dit stukje als regels twee items heeft?
{% for product, prijs in regels %}
{{ loop.index }}/{{ loop.length }} {{ product|upper }}{% if not loop.last %},{% endif %}
{% endfor %}
Uitkomst
Bij [("koffie", 2.80), ("thee", 2.50)] krijg je 1/2 KOFFIE, 2/2 THEE (met wat witruimte ertussen).
loop.length is het aantal items, |upper is een filter en na het laatste item komt geen komma.
Filters
Een filter bewerkt een waarde met |. Je kunt ze achter elkaar zetten.
| Filter | Doet | Voorbeeld → resultaat |
|---|---|---|
upper, lower, title |
hoofdletters | "thee"|title → Thee |
length |
lengte | regels|length → 2 |
default("…") |
waarde als iets ontbreekt | naam|default("gast") |
format |
printf-opmaak | "%.2f"|format(2.8) → 2.80 |
join(", ") |
lijst samenvoegen | ["a","b"]|join(", ") |
truncate(20) |
inkorten | lange omschrijving afkappen |
Je kunt ook een eigen filter maken. Een bedrag in Nederlandse notatie is handig voor de kassa:
@app.template_filter("euro")
def euro(bedrag):
return f"€ {bedrag:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
Nu schrijf je in elke template {{ totaal|euro }} en krijg je € 1.234,50.
Automatisch escapen
Zet maar eens via het formulier het product <b>gratis</b> op de bon. Je ziet de tekens <b> letterlijk:
Jinja zet < om naar < in elke {{ … }}. Je hoeft escape() dus niet meer zelf aan te roepen. Dat werkt
alleen voor bestanden die eindigen op .html, .htm, .xml of .xhtml.
Met {{ tekst|safe }} zet je escapen uit. Gebruik dat nooit voor iets wat een gebruiker heeft ingevuld.
Eén |safe op een productnaam en je hebt weer het XSS-lek uit les 2.
Overerving: één basislayout
Elke pagina heeft dezelfde <head>, kop en menu. Kopiëren is foutgevoelig. Met template-overerving
(template inheritance) schrijf je die één keer in templates/base.html:
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>{% block titel %}Kassa{% endblock %} · kassa-web</title>
</head>
<body>
<nav>
<a href="{{ url_for('home') }}">Start</a>
<a href="{{ url_for('toevoegen') }}">Toevoegen</a>
<a href="{{ url_for('bon_tonen') }}">Bon</a>
</nav>
<main>
{% block inhoud %}{% endblock %}
</main>
</body>
</html>
Een pagina erft daarvan en vult alleen de blokken in. templates/bon.html wordt:
{% extends "base.html" %}
{% block titel %}Bon{% endblock %}
{% block inhoud %}
<h1>Bon</h1>
<table>
{% for product, prijs in regels %}
<tr><td>{{ product }}</td><td>{{ prijs|euro }}</td></tr>
{% else %}
<tr><td colspan="2">De bon is nog leeg.</td></tr>
{% endfor %}
</table>
<p><strong>Totaal: {{ totaal|euro }}</strong></p>
<form method="post" action="{{ url_for('leegmaken') }}"><button>Bon leegmaken</button></form>
{% endblock %}
url_for werkt ook in templates. {{ super() }} in een blok neemt de inhoud van het ouderblok over,
bijvoorbeeld {% block titel %}Bon – {{ super() }}{% endblock %}.
Stukjes hergebruiken met include en macro's
{% include "_menu.html" %}plakt een ander template op die plek. Een underscore voor de naam is een gewoonte voor "stukje, geen hele pagina".- Een macro is een functie in Jinja, handig voor formuliervelden:
{# templates/_velden.html #}
{% macro veld(naam, label, waarde="") %}
<label>{{ label }} <input name="{{ naam }}" value="{{ waarde }}"></label>
{% endmacro %}
{% from "_velden.html" import veld %}
{{ veld("product", "Product") }}
{{ veld("prijs", "Prijs", "0,00") }}
Typische fouten
jinja2.exceptions.TemplateNotFound: bon.html: de map heet niet preciestemplates, of hij staat niet naastapp.py.UndefinedError: 'totaal' is undefinedin een filter: je bent vergetentotaal=…mee te geven aanrender_template. Een losse{{ onbekend }}geeft gewoon niets, pas een bewerking erop faalt.TemplateSyntaxError: Unexpected end of template: een{% endfor %},{% endif %}of{% endblock %}ontbreekt.
Oefenen
- Maak
templates/home.htmldie erft vanbase.htmlen op de startpagina het aantal regels op de bon toont ("Er staan 3 producten op de bon"). Gebruik het filterlength. - Zet het formulier van
/kassa/toevoegenintemplates/toevoegen.html. Toon de foutmelding uit les 2 (oefening 4) alleen als er een is, met{% if fout %}. - Markeer in de bontabel elke regel boven de € 5,00 met
class="duur". Gebruik{% if %}binnen hetclass-attribuut. - Maak een macro
bedragveldvoor een prijsveld metinputmode="decimal"en gebruik die in het formulier.
Modelantwoord oefening 1
@app.route("/")
def home():
return render_template("home.html", aantal=len(bon))
{% extends "base.html" %}
{% block inhoud %}
<h1>Kassa</h1>
<p>Er {{ "staat" if aantal == 1 else "staan" }} {{ aantal }} product{{ "" if aantal == 1 else "en" }} op de bon.</p>
{% endblock %}
Je kunt ook regels=bon meegeven en {{ regels|length }} schrijven. Beide werken; het eerste houdt de template simpel.
Modelantwoord oefening 2
{% extends "base.html" %}
{% block titel %}Toevoegen{% endblock %}
{% block inhoud %}
{% if fout %}<p class="fout">{{ fout }}</p>{% endif %}
<form method="post">
<label>Product <input name="product" value="{{ product|default('') }}"></label>
<label>Prijs <input name="prijs" value="{{ prijs|default('') }}"></label>
<button>Toevoegen</button>
</form>
{% endblock %}
In de route: return render_template("toevoegen.html", fout=fout, product=product, prijs=prijs_tekst), 400
bij een fout en render_template("toevoegen.html") bij een GET. De ingevulde waarden blijven staan, zodat de
gebruiker niet alles opnieuw hoeft te typen.
Modelantwoord oefening 3
<tr{% if prijs > 5 %} class="duur"{% endif %}>
prijs is een Decimal; vergelijken met het getal 5 mag gewoon.
Modelantwoord oefening 4
{% macro bedragveld(naam, label, waarde="") %}
<label>{{ label }}
<input name="{{ naam }}" value="{{ waarde }}" inputmode="decimal" pattern="[0-9]+([,.][0-9]{1,2})?">
</label>
{% endmacro %}
pattern helpt de gebruiker in de browser, maar vervangt de controle op de server niet: iemand met curl
slaat de browser gewoon over.
Lokaal oefenen
Deze cursus heeft geen live lab: Flask, een database en een webserver passen niet in de kleine lab-container.
Je werkt in je eigen kassa-web-map. Controleer jezelf met flask --app app routes en met curl -i op elke pagina.
Samenvatting
- Templates staan in
templates/en render je metrender_template("naam.html", var=waarde). {{ }}vult in (en escapet automatisch),{% %}stuurt (lus, if, block), filters bewerken met|.base.htmlmet{% block %}+{% extends %}geeft alle pagina's dezelfde layout.- In les 4 krijgt die layout CSS en een logo via statische bestanden.