Ga naar hoofdinhoud

Templates met Jinja2

Hier bouw je op verder

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")
  1. Regel 10:

    templates weet 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}
)
  1. Regel 2:

    request: Request vult FastAPI zelf in met het verzoek van de browser. Een template heeft dat nodig, dus je geeft hem door.

  2. Regel 3:

    random.randint(1, 6) kiest een willekeurig getal van 1 tot en met 6.

  3. Regel 4-8:

    TemplateResponse vult de template in en stuurt het resultaat als HTML terug. Eerst het verzoek, dan de bestandsnaam in de map templates, 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.