Abschnitt 18 · Lektion 5
Ein Formular
Ein Suchfeld hängt seinen Wert an die Adresse, /suche?wort=krume. Fürs Suchen ist das richtig,
denn so lässt sich eine Suche als Link weitergeben. Fürs Speichern nicht. Eine Adresse wird neu
geladen, als Lesezeichen abgelegt und verschickt, und jedes Mal würde wieder gespeichert. Alles, was
auf dem Server etwas ändert, wird deshalb anders verschickt.
POST statt GET
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Kategorien</title>
</head>
<body>
<h1>Kategorien</h1>
<ul>
{% for kategorie in kategorien %}
<li>{{ kategorie }}</li>
{% endfor %}
</ul>
<h2>Neue Kategorie</h2>
{% if fehler %}
<p class="fehler">{{ fehler }}</p>
{% endif %}
<form method="post" action="/kategorien">
<input name="name" value="{{ name }}">
<button type="submit">Anlegen</button>
</form>
</body>
</html> from flask import Flask, render_template, request
app = Flask(__name__)
kategorien = ["Lebensmittel", "Wohnen", "Freizeit"]
@app.get("/")
def uebersicht():
return render_template("kategorien.html", kategorien=kategorien)
@app.post("/kategorien")
def anlegen():
kategorien.append(request.form["name"])
return render_template("kategorien.html", kategorien=kategorien) Mit <form method="post" action="/kategorien"> geht das Formular mit dem Verfahren POST an
/kategorien. Die Felder stehen dann nicht in der Adresse, sondern im Rumpf der Anfrage, wie die
Daten in einer Antwort aus Abschnitt 17. Jedes Feld hat einen Namen, name="name", und sein Wert
ist das, was jemand hineingetippt hat. Im Reiter Netzwerk siehst du die Anfrage samt Rumpf:
name=Reisen.
In Flask nimmt @app.post("/kategorien") die Anfrage an, das Gegenstück zu get. request.form
ist für den Rumpf das, was request.args für die Adresse ist, ein Dictionary mit den Feldern. Die
Funktion hängt den Namen an die Liste und zeigt die Seite neu.
Wenn du im Browser eine Kategorie anlegst, steht sie danach in der Liste, und in der Adresszeile
steht /kategorien. Die Seite vor dir ist die Antwort auf den POST, und die merkt sich ein Browser.
Zu Hause wiederholt F5 deshalb den POST. Der Browser fragt vorher, ob er die Daten noch einmal
senden soll. Wer Ja sagt, hat die Kategorie danach zweimal in der Liste.
Hier lädt F5 die ganze Lektion neu, und Seite neu laden holt die Adresse mit GET. Darauf kommt bei
/kategorien ein 405 Method Not Allowed zurück, weil es dort für GET keine Route gibt. Wie das
Wiederholen aussieht, zeigt deshalb die Befehlsleiste. Der Befehl dort schickt denselben POST, den
der Browser schicken würde, und zählt, wie oft „Reisen“ danach in der Liste steht. Beim zweiten Mal
steht dort eins mehr.
Nach dem Speichern weiterleiten
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Kategorien</title>
</head>
<body>
<h1>Kategorien</h1>
<ul>
{% for kategorie in kategorien %}
<li>{{ kategorie }}</li>
{% endfor %}
</ul>
<h2>Neue Kategorie</h2>
{% if fehler %}
<p class="fehler">{{ fehler }}</p>
{% endif %}
<form method="post" action="/kategorien">
<input name="name" value="{{ name }}">
<button type="submit">Anlegen</button>
</form>
</body>
</html> from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
kategorien = ["Lebensmittel", "Wohnen", "Freizeit"]
@app.get("/")
def uebersicht():
return render_template("kategorien.html", kategorien=kategorien)
@app.post("/kategorien")
def anlegen():
kategorien.append(request.form["name"])
return redirect(url_for("uebersicht")) Nach dem Speichern schickt die Funktion deshalb keine Seite, sondern eine Weiterleitung
(redirect). redirect(…) antwortet mit dem Statuscode 302 und einer Kopfzeile Location, in der
steht, wo es weitergeht. Der Browser fragt diese Adresse dann von selbst ab, und zwar mit GET. Die
letzte Anfrage ist danach ein GET auf /, und beim Neuladen wird nur der wiederholt.
Dieses Muster heißt Post/Redirect/Get, und fast jedes Formular im Netz macht es so. Nach dem
Abschicken wechselt die Adresse auf /, und Seite neu laden holt die Liste, so oft du willst.
Der Befehl in der Befehlsleiste schickt das Formular von Hand ab und zeigt die Antwort, die der
Browser nie zu sehen bekommt, weil er ihr sofort folgt: HTTP/1.1 302 FOUND und darunter
Location: /.
url_for("uebersicht") macht aus dem Namen der Funktion ihre Adresse, hier also /. Das ist der
Endpunkt, der Name, unter dem Flask jede Route kennt. Natürlich ginge auch redirect("/"). Mit
url_for stimmt die Weiterleitung aber auch dann noch, wenn die Startseite irgendwann eine andere
Adresse bekommt.
Erst prüfen, dann speichern
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Kategorien</title>
</head>
<body>
<h1>Kategorien</h1>
<ul>
{% for kategorie in kategorien %}
<li>{{ kategorie }}</li>
{% endfor %}
</ul>
<h2>Neue Kategorie</h2>
{% if fehler %}
<p class="fehler">{{ fehler }}</p>
{% endif %}
<form method="post" action="/kategorien">
<input name="name" value="{{ name }}">
<button type="submit">Anlegen</button>
</form>
</body>
</html> from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
kategorien = ["Lebensmittel", "Wohnen", "Freizeit"]
@app.get("/")
def uebersicht():
return render_template("kategorien.html", kategorien=kategorien)
@app.post("/kategorien")
def anlegen():
name = request.form.get("name", "").strip()
if not name:
fehler = "Bitte gib einen Namen ein."
elif name in kategorien:
fehler = f"{name} gibt es schon."
else:
kategorien.append(name)
return redirect(url_for("uebersicht"))
return render_template("kategorien.html", kategorien=kategorien, fehler=fehler, name=name), 400 In ein Formular kann jeder tippen, was er will. Auch nichts. Deshalb wird wie in Lektion 9.3 erst geprüft und dann verwendet.
request.form.get("name", "") liest das Feld mit einem Standardwert, und strip() nimmt
Leerzeichen vorn und hinten weg. Danach kommen zwei Fragen. Ist der Name leer? Gibt es ihn schon?
Erst wenn beides nicht zutrifft, wird gespeichert und weitergeleitet.
Sonst zeigt die Funktion das Formular noch einmal, mit einer Meldung darüber. Der eingetippte Name steht dabei wieder im Feld, damit niemand alles neu tippen muss.
return render_template(…), 400 gibt dabei zwei Dinge zurück, die Seite und einen Statuscode. 400
heißt „Bad Request“, die Anfrage war also nicht in Ordnung. Im Browser sieht die Seite trotzdem ganz
normal aus. Der Status ist für Programme, die deine Anwendung fragen, und für den Reiter Netzwerk.
Ausprobieren lässt sich das im Browser mit einem leeren Namen und mit einem, den es schon gibt.
Weg nach dem Neustart
Die neuen Kategorien stehen in einer Liste im Speicher des Servers, wie der Zähler vom Anfang des Abschnitts. Nach jeder Änderung am Code startet der Server neu, und in der Liste stehen wieder nur die drei Kategorien vom Anfang. Für eine Übung ist das in Ordnung. Für ein echtes Haushaltsbuch nicht.
Der nächste Schritt wäre die Datenbank aus Abschnitt 16. In der Route stünde dann statt append ein
INSERT, und die Ausgaben überlebten jeden Neustart.
Übrigens ändert append die Liste, ohne dem Namen kategorien einen neuen Wert zu geben. Deshalb
braucht es auch hier kein global.
Ein Feld, das nicht ankommt
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Kategorien</title>
</head>
<body>
<h1>Kategorien</h1>
<ul>
{% for kategorie in kategorien %}
<li>{{ kategorie }}</li>
{% endfor %}
</ul>
<h2>Neue Kategorie</h2>
{% if fehler %}
<p class="fehler">{{ fehler }}</p>
{% endif %}
<form method="post" action="/kategorien">
<input name="name" value="{{ name }}">
<button type="submit">Anlegen</button>
</form>
</body>
</html> from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
kategorien = ["Lebensmittel", "Wohnen", "Freizeit"]
@app.get("/")
def uebersicht():
return render_template("kategorien.html", kategorien=kategorien)
@app.post("/kategorien")
def anlegen():
kategorien.append(request.form["kategorie"])
return redirect(url_for("uebersicht")) Hier liest die Funktion request.form["kategorie"], aber das Feld im Formular heißt name. Wenn du
das Formular abschickst, bekommst du eine Seite mit der Überschrift Bad Request und dem Satz
The browser (or proxy) sent a request that this server could not understand. Übersetzt heißt das,
der Browser hat eine Anfrage geschickt, die der Server nicht versteht.
Im Terminal steht dazu kein Traceback. Flask hält ein fehlendes Feld für einen Fehler des Browsers
und antwortet mit 400, ohne zu sagen, welches Feld fehlt. Wenn nach dem Abschicken eines Formulars
„Bad Request“ dasteht, lohnt es sich, die Namen in name="…" mit denen in request.form[…] zu
vergleichen, Groß und klein eingeschlossen. Mit get bekämst du stattdessen den Standardwert und
könntest selbst entscheiden, was dann passiert.
Verwandt damit ist das 405 Method Not Allowed aus dem ersten Beispiel. Das kommt, wenn eine
Anfrage mit einem Verfahren ankommt, für das es an dieser Adresse keine Route gibt, etwa ein
Formular mit POST an eine Adresse, die nur get kennt.
Was die Aufgabe will
Für neue Ausgaben gibt es das Formular und die Seiten dazu schon. Es fehlt die Route, die das Formular annimmt, prüft, speichert und weiterleitet. Beim Prüfen wird das Formular dafür ein paarmal abgeschickt, mit guten und mit schlechten Eingaben, und danach wird auf der Startseite nachgesehen.
Zum Mitnehmen
Bei einem Formular mit method="post" stehen die Felder im Rumpf der Anfrage, wo @app.post("/kategorien") sie annimmt und request.form.get("name", "") eines davon liest. Erst wird geprüft, dann gespeichert, und danach führt return redirect(url_for("uebersicht")) auf eine andere Seite, damit ein Neuladen nichts doppelt einträgt. Passt etwas nicht, kommt mit return render_template(…), 400 das Formular samt Meldung noch einmal.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.