Ga naar hoofdinhoud

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

    Een TemplateResponse heeft de request nodig, net als in de andere lessen met templates.

  2. Regel 5:

    Geen f-string en geen escape meer: 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.

Controlevraag
Je maakt een pagina /nieuwste met een eigen template nieuwste.html. Daarin staan de naam en het bericht van de laatste bezoeker, elk tussen dubbele accolades. Wat zet je in Python om de naam en het bericht, voordat ze naar het template gaan?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

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 &#34;hoi&#34;</li>

Jinja schrijft een " als &#34;, waar escape uit les 3 &quot; 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.