Templates met Jinja2
Hier bouw je op verder
- Python Dictionaries
Een FileResponse stuurt elke keer hetzelfde bestand. Wil je een waarde uit Python op de pagina, zoals een naam, een score of straks de berichten uit je database? Dan heb je een template nodig: een HTML-bestand met open plekken, die Python invult voordat de pagina de server verlaat. Het invullen doet Jinja2, dat al met FastAPI is meegeïnstalleerd.
Een dobbelsteen op een pagina
Maak naast static een nieuwe map templates, met daarin dobbelsteen.html:
je-project/
├── main.py
├── static/
└── templates/
└── dobbelsteen.html
<!DOCTYPE html>
<html>
<body>
<h1>Dobbelsteen</h1>
<p>Je gooide een {{ worp }}.</p>
</body>
</html>
{{ worp }} is een open plek. Wat daar komt te staan, beslist Python. In je main.py komen daarvoor drie dingen bij: import random helemaal bovenaan, Request en Jinja2Templates bij de imports, en onder app.mount de regel templates = …. De bovenkant van je main.py ziet er dan zo uit:
import random
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
- Regel 10:
templatesweet waar je templates staan. Vanaf nu vraag je een template op met alleen de bestandsnaam.
Zet het endpoint onder je andere endpoints:
@app.get("/dobbelsteen")
async def dobbelsteen(request: Request):
worp = random.randint(1, 6)
return templates.TemplateResponse(
request,
"dobbelsteen.html",
{"worp": worp}
)
- Regel 2:
request: Requestvult FastAPI zelf in met het verzoek van de browser. Een template heeft dat nodig, dus je geeft hem door. - Regel 3:
random.randint(1, 6)kiest een willekeurig getal van 1 tot en met 6. - Regel 4-8:
TemplateResponsevult de template in en stuurt het resultaat als HTML terug. Eerst het verzoek, dan de bestandsnaam in de maptemplates, dan een dictionary. Elke sleutel wordt in de template een naam:"worp"vult{{ worp }}.
Voorspel: wat zie je als je de pagina drie keer herlaadt?
Steeds een ander getal, bijvoorbeeld "Je gooide een 4.", dan een 1, dan een 6. Bij elk verzoek draait de functie opnieuw, gooit opnieuw, en vult de template opnieuw in. Met een FileResponse zou je drie keer precies hetzelfde zien.
Ga naar http://127.0.0.1:8000/dobbelsteen en herlaad een paar keer.
Wat de browser krijgt
Klik met de rechtermuisknop op de pagina en kies Paginabron weergeven (of druk op Ctrl+U). Het verzoek was:
GET /dobbelsteen
En dit is het antwoord dat de browser kreeg:
<!DOCTYPE html>
<html>
<body>
<h1>Dobbelsteen</h1>
<p>Je gooide een 4.</p>
</body>
</html>
Nergens staat {{ worp }}. Jinja2 vult de template in op de server, en de browser krijgt gewone HTML. De browser weet dus niet dat er een template was; hij ziet alleen het getal.
Meer waarden meesturen
Elke sleutel in het dictionary is een naam die de template kan gebruiken. Stuur je er twee mee, dan heb je twee open plekken:
return templates.TemplateResponse(
request,
"dobbelsteen.html",
{"worp": worp, "speler": "Sam"}
)
<p>{{ speler }} gooide een {{ worp }}.</p>
Is een waarde zelf een dictionary, dan haal je er in de template een onderdeel uit met een punt:
{"speler": {"naam": "Sam", "punten": 12}}
<p>{{ speler.naam }} heeft {{ speler.punten }} punten.</p>
In Python schrijf je speler["naam"]; in een template mag speler.naam. Dat kom je weer tegen als je de berichten uit je database op een pagina zet.
Er gaat iets mis
Een lege plek op de pagina
Je gooide een .
Oorzaak: de naam in de template staat niet in het dictionary. Jinja2 geeft dan geen foutmelding, maar laat de plek leeg.
Oplossing: vergelijk de naam tussen {{ }} letter voor letter met de sleutel in het dictionary.
# FOUT: de template vraagt om worp
{"getal": worp}
# GOED
{"worp": worp}
TemplateNotFound
jinja2.exceptions.TemplateNotFound: 'dobbelsteen.html' not found in search path: 'templates'
Oorzaak: Jinja2 zoekt alleen in de map die je aan Jinja2Templates gaf, en daar staat het bestand niet.
Oplossing: zet het bestand in templates/, naast main.py, en controleer de bestandsnaam. Geef alleen de naam door, zonder templates/ ervoor.
Meer uitleg: Er gaat iets mis
Zo ziet je main.py er nu uit
Dit is je hele main.py na deze les, zonder wat je in de opdrachten maakte.
import random
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def home():
return FileResponse("static/pages/home.html")
@app.get("/info")
async def info():
return {"naam": "Jouw Naam", "leeftijd": 16}
@app.get("/pagina", response_class=HTMLResponse)
async def pagina():
return """
<!DOCTYPE html>
<html>
<head>
<title>Mijn Pagina</title>
</head>
<body>
<h1>Hallo!</h1>
<p>Dit is mijn eerste HTML pagina.</p>
</body>
</html>
"""
@app.get("/styled", response_class=HTMLResponse)
async def styled():
return """
<!DOCTYPE html>
<html>
<head>
<title>Met CSS</title>
<style>
h1 { color: blue; }
p { color: gray; }
</style>
</head>
<body>
<h1>Gestylede pagina</h1>
<p>Deze tekst is grijs.</p>
</body>
</html>
"""
@app.get("/about")
async def about():
return FileResponse("static/pages/about.html")
@app.get("/foto")
async def foto():
return FileResponse("static/pages/foto.html")
@app.get("/dobbelsteen")
async def dobbelsteen(request: Request):
worp = random.randint(1, 6)
return templates.TemplateResponse(
request,
"dobbelsteen.html",
{"worp": worp}
)
Opdrachten
Opdracht 1: Predict - Wat zie je?
Template:
<h1>Welkom {{ naam }}</h1>
<p>Leeftijd: {{ leeftijd }}</p>
<p>Klas: {{ klas }}</p>
Python:
return templates.TemplateResponse(request, "welkom.html", {
"naam": "Jan", "leeftijd": 16, "Klas": "3B"
})
Vraag: wat zie je op de pagina?
Tip
Zoek bij elke {{ }} de sleutel met precies dezelfde naam in het dictionary, letter voor letter.
Antwoord
Welkom Jan
Leeftijd: 16
Klas:
In het dictionary staat Klas met een hoofdletter, en de template vraagt om klas. Voor Jinja2 zijn dat twee verschillende namen, dus die plek blijft leeg, zonder foutmelding.
Opdracht 2: Run
Maak de map templates, het bestand dobbelsteen.html en het endpoint /dobbelsteen. Herlaad een paar keer en bekijk de paginabron.
Opdracht 3: Investigate - De oude schrijfwijze
Veel voorbeelden die je online vindt, zetten request ín het dictionary in plaats van ervoor. Probeer dat eens:
return templates.TemplateResponse("dobbelsteen.html", {"request": request, "worp": worp})
Antwoord
De browser zegt alleen Internal Server Error. Onderaan de lange foutmelding in je terminal staat TypeError: unhashable type: 'dict'.
Die melding zegt niets over templates, en dat maakt hem lastig. Het eerste argument is tegenwoordig request, dus jouw bestandsnaam belandt op de plek van request en jouw dictionary op de plek van de bestandsnaam. FastAPI probeert dan een template op te zoeken met een dictionary als naam, en dat kan niet.
Zie je deze fout in je eigen code, of in een voorbeeld dat je overneemt? Zet request vooraan.
Opdracht 4: Make - Twee dobbelstenen
Gooi met twee dobbelstenen en laat op de pagina beide worpen zien, en wat ze samen zijn: "Je gooide 3 en 5, samen 8."
Tip
Reken alles uit in Python en stuur drie waarden mee. De template telt niet zelf op; die vult alleen in.
Antwoord
templates/twee.html:
<!DOCTYPE html>
<html>
<body>
<p>Je gooide {{ eerste }} en {{ tweede }}, samen {{ totaal }}.</p>
</body>
</html>
@app.get("/twee")
async def twee(request: Request):
eerste = random.randint(1, 6)
tweede = random.randint(1, 6)
return templates.TemplateResponse(
request,
"twee.html",
{"eerste": eerste, "tweede": tweede, "totaal": eerste + tweede}
)
Opdracht 5: Make - Een profielkaart
Maak een endpoint /kaart dat een dictionary met je naam, je leeftijd en je hobby als één waarde meestuurt, en toon ze op de pagina met de punt.
Tip
Stuur {"ik": {"naam": …, "leeftijd": …, "hobby": …}} mee. In de template schrijf je dan {{ ik.naam }}.
Antwoord
templates/kaart.html:
<!DOCTYPE html>
<html>
<body>
<h1>{{ ik.naam }}</h1>
<p>Leeftijd: {{ ik.leeftijd }}</p>
<p>Hobby: {{ ik.hobby }}</p>
</body>
</html>
@app.get("/kaart")
async def kaart(request: Request):
ik = {"naam": "Sam", "leeftijd": 15, "hobby": "gamen"}
return templates.TemplateResponse(request, "kaart.html", {"ik": ik})
Je template vult nu waarden uit Python in. In Een formulier beantwoorden met een template komt die waarde van de bezoeker.