mitmario.dev

Ein Formular

Python Sandbox 5 Min Lesezeit 4 BeispieleLektion 5 von 7

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

Ein Formular mit POST
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

Nach dem Speichern weiterleiten
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

Erst prüfen, dann speichern
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

Ein Feld, das nicht ankommt
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, kostenlos

Zu 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.