Beetje Coderen

Les 5 · Module 1 · Flask-basis

Formulieren verwerken en valideren

Maak formulieren robuust: valideer op de server, toon foutmeldingen per veld, bewaar ingevulde waarden en gebruik Flask-WTF met CSRF-bescherming.

⏱ 90 minuten leestijd + oefenen

Illustratie: een formulierdocument met een groen schild en een vinkje, symbool voor gecontroleerde invoer

🎯 Na deze les kun je

  • uitleggen waarom validatie op de server verplicht is, ook met HTML-validatie
  • een formulier met Flask-WTF definiëren met validators
  • foutmeldingen per veld tonen en ingevulde waarden laten staan
  • flash-berichten gebruiken na een geslaagde actie

Het formulier van de kassa controleert nu met de hand of product en prijs kloppen. Bij twee velden gaat dat nog, maar elk nieuw veld betekent nieuwe if-regels, foutteksten en het terugzetten van waarden. In deze les leer je het patroon dat elke webapp gebruikt: valideren op de server, fouten per veld tonen, daarna doorsturen met een melding. Je doet het eerst zelf, daarna met Flask-WTF, dat ook meteen CSRF-bescherming geeft.

Waarom op de server?

required, type="number" en pattern in HTML helpen de gebruiker, maar zijn geen beveiliging. Iedereen kan met curl of de ontwikkelaarstools elk verzoek sturen dat hij wil. De regel is daarom:

Browservalidatie is voor gemak. Servervalidatie is voor correctheid.

Het patroon met de hand

from flask import flash

def lees_bedrag(tekst):
    try:
        bedrag = Decimal(tekst.strip().replace(",", "."))
    except (InvalidOperation, AttributeError):
        return None
    return bedrag if bedrag >= 0 else None

@app.route("/kassa/toevoegen", methods=["GET", "POST"])
def toevoegen():
    fouten, waarden = {}, {}
    if request.method == "POST":
        waarden = request.form
        product = request.form.get("product", "").strip()
        prijs = lees_bedrag(request.form.get("prijs", ""))
        if not product:
            fouten["product"] = "Vul een product in."
        elif len(product) > 40:
            fouten["product"] = "Maximaal 40 tekens."
        if prijs is None:
            fouten["prijs"] = "Vul een geldig bedrag in, bijvoorbeeld 2,80."
        if not fouten:
            bon.append((product, prijs))
            flash(f"{product} staat op de bon.")
            return redirect(url_for("bon_tonen"))
    status = 400 if fouten else 200
    return render_template("toevoegen.html", fouten=fouten, waarden=waarden), status

flash() bewaart een bericht in de sessie tot de volgende pagina het ophaalt. Daarvoor heeft Flask een SECRET_KEY nodig (meer in les 7):

app.config["SECRET_KEY"] = "alleen-voor-ontwikkeling"

En in base.html:

{% with berichten = get_flashed_messages() %}
  {% for b in berichten %}<p class="melding">{{ b }}</p>{% endfor %}
{% endwith %}

Voorspel eerst: je stuurt product= (leeg) en prijs=-1. Hoeveel foutmeldingen zie je, en welke statuscode?

Uitkomst

Twee meldingen (product en prijs) en status 400 Bad Request. Er wordt niets aan de bon toegevoegd en er volgt geen redirect. De ingevulde waarden komen terug via waarden.

Flask-WTF: formulieren als klasse

pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, DecimalField, SubmitField
from wtforms.validators import DataRequired, Length, NumberRange

class ProductForm(FlaskForm):
    product = StringField("Product", validators=[DataRequired("Vul een product in."), Length(max=40)])
    prijs = DecimalField("Prijs", places=2, use_locale=False,
                         validators=[DataRequired("Vul een prijs in."), NumberRange(min=0)])
    opslaan = SubmitField("Toevoegen")

@app.route("/kassa/toevoegen", methods=["GET", "POST"])
def toevoegen():
    form = ProductForm()
    if form.validate_on_submit():
        bon.append((form.product.data, form.prijs.data))
        flash(f"{form.product.data} staat op de bon.")
        return redirect(url_for("bon_tonen"))
    return render_template("toevoegen.html", form=form), (400 if form.errors else 200)

validate_on_submit() is True als het een POST is en alle validators slagen, inclusief de CSRF-controle. De template:

<form method="post" novalidate>
  {{ form.hidden_tag() }}
  {% for veld in [form.product, form.prijs] %}
    <p>{{ veld.label }} {{ veld() }}
      {% for f in veld.errors %}<span class="fout">{{ f }}</span>{% endfor %}</p>
  {% endfor %}
  {{ form.opslaan() }}
</form>

form.hidden_tag() zet een verborgen veld csrf_token in het formulier. Zonder dat token weigert Flask-WTF het formulier. Waarom dat belangrijk is, zie je in les 14.

DecimalField kent geen komma. Wil je 2,80 accepteren, maak dan een eigen veld dat in process_formdata de komma vervangt (oefening 3). Met novalidate zet je de browservalidatie uit, zodat je tijdens het testen de servermeldingen ziet.

Typische fouten

  • RuntimeError: A secret key is required: SECRET_KEY ontbreekt (nodig voor flash en CSRF).
  • Formulier doet niets, geen fouten: {{ form.hidden_tag() }} vergeten. De CSRF-fout staat in form.errors["csrf_token"]; toon form.errors even om het te zien.
  • curl krijgt altijd 400: klopt, curl heeft geen CSRF-token. Test formulieren in de browser of in pytest (les 16), waar je CSRF uit kunt zetten.

Oefenen

  1. Voeg een veld aantal toe (geheel getal, 1 t/m 99, standaard 1) en zet aantal × prijs op de bon.
  2. Toon na het leegmaken van de bon een flash-bericht "De bon is leeg." en geef flash-berichten een categorie (flash(…, "succes")) die je als CSS-class gebruikt.
  3. Schrijf een veld KommaDecimalField dat ook 2,80 accepteert.
  4. Voeg een eigen validator toe die het product test weigert met de melding "Kies een echt product".
Modelantwoord oefening 1
from wtforms import IntegerField
aantal = IntegerField("Aantal", default=1, validators=[NumberRange(min=1, max=99)])

In de route: bon.append((f"{form.aantal.data}× {form.product.data}", form.prijs.data * form.aantal.data)). Later, met een database, bewaar je aantal en stukprijs apart; nu volstaat de tuple.

Modelantwoord oefening 2
flash("De bon is leeg.", "succes")
{% for cat, b in get_flashed_messages(with_categories=true) %}
  <p class="melding {{ cat }}">{{ b }}</p>
{% endfor %}

Zonder categorie is die "message".

Modelantwoord oefening 3
class KommaDecimalField(DecimalField):
    def process_formdata(self, valuelist):
        if valuelist:
            valuelist = [valuelist[0].replace(",", ".")]
        super().process_formdata(valuelist)

Je past alleen de ruwe tekst aan en laat de rest (omzetten, foutmelding) aan DecimalField.

Modelantwoord oefening 4
from wtforms.validators import ValidationError

class ProductForm(FlaskForm):
    ...
    def validate_product(self, veld):
        if veld.data.strip().lower() == "test":
            raise ValidationError("Kies een echt product")

Een methode validate_<veldnaam> wordt automatisch als extra validator van dat veld gebruikt.

Lokaal oefenen

Geen live lab. Test in de browser met novalidate aan, zodat je de servermeldingen ziet. Probeer bewust lege velden, letters in de prijs, een negatieve prijs en een productnaam van 41 tekens.

Samenvatting

  • Valideer altijd op de server; HTML-validatie is gemak.
  • Patroon: POST → valideren → fouten tonen met status 400 of opslaan, flash en redirect.
  • Flask-WTF geeft formulierklassen, validators en CSRF-tokens; validate_on_submit() doet alles in één keer.
  • Volgende les: de groeiende app.py opdelen met blueprints.

📝 Quiz: formulieren verwerken en valideren

5 vragen · 70% nodig om te slagen

1. Je formulier heeft `required` en `pattern`. Moet je nog op de server valideren?
2. Welke statuscode past bij een formulier met validatiefouten dat opnieuw getoond wordt?
3. Wat heeft flash() nodig om te werken?
4. Wanneer geeft form.validate_on_submit() True?
5. Je formulier doet niets en toont geen fouten bij de velden. Wat is een waarschijnlijke oorzaak?

Dit was de laatste gratis les

Je hebt les 1–5 van Webapps met Flask gratis gevolgd. Wil je verder? Met één aankoop (€ 49,00, eenmalig) krijg je de overige 15 lessen, de live labs en het certificaat. Geen abonnement.

Cursus kopen en verdergaan