HTML van een bezoeker: templates escapen vanzelf
Met escape uit les 3 moet je aan elke plek denken, en in de "Er
gaat iets mis" van die les zag je hoe makkelijk je er een vergeet. Een template
van Jinja doet het voor je: alles tussen {{ }} escapet hij, zonder dat je
erom vraagt.
De lijst met een template
Maak templates/berichten_lijst.html. Het is een kleinere versie van het
template uit het recept Verversen:
deze server heeft geen detailpagina en geen verwijderknop, dus de link en het
formuliertje zijn eruit. Zonder die twee heb je de sleutels niet nodig, en loopt
de lus over de berichten alleen, met db.values() in plaats van db.items():
<ul>
{% for bericht in berichten %}
<li>{{ bericht.naam }}: {{ bericht.bericht }}</li>
{% endfor %}
</ul>
Het endpoint geeft nu het template terug, in plaats van zelf HTML te bouwen:
@app.get("/berichten/lijst")
async def berichten_lijst(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return templates.TemplateResponse(request, "berichten_lijst.html", {"berichten": alle_berichten})
- Regel 2:
Een
TemplateResponseheeft derequestnodig, net als in de andere lessen met templates. - Regel 5:
Geen f-string en geen
escapemeer: de berichten gaan naar het template, en Jinja zet ze erin.
Zet bovenaan main.py wat daarvoor nodig is. Request komt uit fastapi, dus
die zet je in de regel die er al staat. Jinja2Templates komt uit
fastapi.templating en krijgt een eigen regel:
from fastapi import FastAPI, Form, Request
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
Het antwoord na het versturen blijft één zin in een f-string, met escape(naam),
en de zoekpagina uit les 4 houdt escape(term). Voor één zin is een
template wat veel.
Doe de test opnieuw
Gooi gastenboek.db weg, start de server opnieuw, en vul als Sara weer
<b>vet</b> in.
De witruimte komt uit het template: de regels met {% for %} en {% endfor %}
worden elk een regel met alleen vier spaties. Wat Sara typte staat er, maar als
tekst, net als met escape.
Er gaat iets mis
Weer vet met een andere extensie
Je noemt het template berichten_lijst.jinja, en <b>vet</b> is weer vet.
Oorzaak: Jinja escapet in FastAPI alleen templates die op .html, .htm of
.xml eindigen. Een andere extensie zet het escapen stil uit, zonder
foutmelding of waarschuwing.
Oplossing: houd je templates op .html, en pas de naam in
TemplateResponse mee aan.
TemplateNotFound
De lijst ververst niet meer, en na herladen blijft hij leeg. lijst.py print
Internal Server Error, en in de terminal van je server eindigt de foutmelding
met:
jinja2.exceptions.TemplateNotFound: 'berichten_lijst.html' not found in search path: 'templates'
Oorzaak: het template staat niet in de map templates naast main.py, of
het heet anders dan in TemplateResponse. Je endpoint geeft dan een 500, en
htmx zet een 500 niet in de pagina: een open pagina houdt stil de oude lijst.
Oplossing: zet berichten_lijst.html in templates/, met precies de naam
uit TemplateResponse.
Opdrachten
Opdracht 1: Modify - Namen vet, veilig
Maak in de lijst de naam van elke schrijver vet, zonder dat een bezoeker zelf HTML kan invullen.
Tip
De <b> mag in het template staan. Het is jouw HTML, niet die van de bezoeker.
Antwoord
<ul>
{% for bericht in berichten %}
<li><b>{{ bericht.naam }}</b>: {{ bericht.bericht }}</li>
{% endfor %}
</ul>
De tags komen van jou, de inhoud van de bezoeker. Jinja escapet alleen wat
tussen {{ }} staat, dus de naam blijft tekst en jouw <b> blijft HTML.
Haal de <b> daarna weer weg: les 6 rekent met het template zonder vette
namen.
Opdracht 2: Predict - Aanhalingstekens
Noor vult als naam Noor in en als bericht Ik zeg "hoi".
Vraag: wat ziet een bezoeker in de lijst? En wat staat er voor dat bericht
in wat lijst.py print?
Tip
In les 3 zag je welke tekens escapen vervangt. Een " is er een
van.
Antwoord
In de lijst staat Noor: Ik zeg "hoi", met de aanhalingstekens erbij. lijst.py
geeft voor dat bericht:
<li>Noor: Ik zeg "hoi"</li>
Jinja schrijft een " als ", waar escape uit les 3 " maakt. Het
zijn twee codes voor hetzelfde teken, en de browser toont van allebei een ".
Wie gewone tekst typt, merkt dus niets van het escapen.
Zo ziet je main.py er nu uit
import time
from html import escape
from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlitedict import SqliteDict
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {"naam": naam, "bericht": bericht}
db.commit()
return HTMLResponse(f"Bedankt, {escape(naam)}. Je bericht staat in het gastenboek.")
@app.get("/berichten/lijst")
async def berichten_lijst(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return templates.TemplateResponse(request, "berichten_lijst.html", {"berichten": alle_berichten})
@app.get("/zoek")
async def zoek(term: str = ""):
gevonden = ""
with SqliteDict("gastenboek.db") as db:
for bericht in db.values():
if term.lower() in bericht["bericht"].lower():
gevonden += f"<li>{escape(bericht['naam'])}: {escape(bericht['bericht'])}</li>"
return HTMLResponse(f"<p>Je zocht naar: {escape(term)}</p><ul>{gevonden}</ul>")
Naast main.py staan static/gastenboek.html, static/js/htmx.min.js en
templates/berichten_lijst.html.
Door naar les 6: |safe, de bescherming uitzetten.