Notes 9

Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.

Willkommen!

  • In den vergangenen Wochen haben Sie zahlreiche Programmiersprachen, Techniken und Strategien kennen gelernt.
  • In der Tat war dieser Kurs weniger ein C-Kurs oder Python-Kurs als vielmehr ein Programmierkurs, sodass Sie in Zukunft selbständig den Trends folgen und sich neue Sprachen oder Konzepte beibringen können.
  • In den letzten Wochen haben Sie gelernt, wie man programmiert.
  • Heute werden wir von HTML und CSS dazu übergehen, HTML, CSS, Python und JavaScript zu kombinieren, damit Sie damit Ihre eigenen Webanwendungen erstellen können.
  • Gerade die in dieser Woche erworbenen Fähigkeiten sind sicher für das ein oder andere Abschlussprojekt nützlich.

http-server

  • Bis zu diesem Zeitpunkt war das HTML, das Sie sahen, vorformuliert und statisch.

  • Wenn Sie in der Vergangenheit eine Seite besuchten, lud der Browser eine HTML-Seite herunter, die Sie dann anzeigen konnten. Solcze Seiten werden als statische Seiten bezeichnet, da das, was in der HTML-Datei steht, genau das ist, was der Benutzer sieht und auf die Client-Seite, d. h. auf seinen Internet-Browser, herunterlädt.

  • Wenn wir hingegen von dynamischen Seiten sprechen, meinen wir damit die Fähigkeit eines Webservers, mit Python oder anderen Sprachen (wie PHP, JavaScript oder TypeScript) HTML-Dateien on-the-fly zu erstellen – sodass nicht alle, die die Seiten besuchen, unbedingt auch die gleichen Inhalte sehen. Es handelt sich dabei also um Webseiten, die auf der Server-Seite durch Code erzeugt werden, der mit den Eingaben oder dem Verhalten der Benutzer arbeitet.

  • Sie haben in der Vergangenheit http-server verwendet, um Ihre Webseiten bereitzustellen. Heute werden wir einen neuen Server einsetzen, der die URLs in eingehenden HTTP-Anfragen analysiert und – das ist neu – auf der Grundlage der URL und der übergebenen Parameter verschiedene Aktionen durchführen kann.

  • In der letzten Woche haben Sie z.B. folgende URLs gesehen:

    https://www.example.com/folder/file.html

    Beachten Sie, dass file.html eine HTML-Datei in einem Ordner mit dem Namen folder auf example.com ist.

Flask

  • Diese Woche stellen wir die Möglichkeit vor, mit Routen wie https://www.example.com/route?key=value zu arbeiten, bei denen bestimmte Funktionen auf dem Server über die in der URL angegebenen Schlüssel und Werte ausgeführt werden können.

  • Flask ist eine Open-Source-Bibliothek, die es Ihnen ermöglicht, dynamische Webanwendungen in Python zu programmieren. Flask wird auch als Framework bezeichnet, weil es bestimmte Konventionen und Regeln vorgibt, an denen man sich bei der Programmierung orientiert, um von den Vorarbeit zu profitieren, die die Bibliothek leistet.

  • Sie können einen Flask-Webserver starten, indem Sie flask run in Ihrem Terminalfenster ausführen.

  • Dazu benötigen Sie eine Datei namens app.py. Die Datei app.py enthält Code, der Flask mitteilt, wie deine Webanwendung ausgeführt werden soll.

  • Üblicherweise legt man auch eine Datei namens requirements.txt an, wenn man mit Flask arbeitet. Die Datei requirements.txt enthält eine Liste der Bibliotheken, die für die Ausführung einer Python-Anwendung erforderlich sind. In unserem Fall wird dies eben Flask sein.

    • Die Datei requirements.txt ist so etwas wie eine Zutatenliste, in der alle Python-Bibliotheken, die ein Projekt benötigt, aufgelistet werden. Wenn Sie Code mit anderen teilem oder ihn auf einem anderen Computer einsetzen, hilft diese Datei dabei, automatisch alle notwendigen Bibliotheken mit den richtigen Versionen zu installieren. Im CS50-Codespace und im darauf basierenden Dev-Container ist Flask bereits vorinstalliert, sodass Sie dort keine requirements.txt anlegen. Anders verhält es sich, wenn Sie auf Ihrem eigenen Rechner außerhalb des Codespaces entwickeln oder Ihre Anwendung auf einem produktiven Webserver bereitstellen würden. Dann wäre die Datei hilfreich.

    • Hier ist ein Beispiel für die Datei requirements.txt:

      Flask

      Beachten Sie, dass in dieser Datei nur Flask erscheint. Das liegt daran, dass für unsere Projekte in dieser Vorlesung zumindest anfangs nur Flask benötigt wird. Wenn Ihre Projekte komplexer werden, können Sie weitere Bibliotheken in diese Datei aufnehmen, wobei jeder Bibliotheksname in einer eigenen Zeile steht. Dies hilft, den Überblick über den gesamten externen Code zu behalten, von dem Ihre Anwendung abhängt.

    • Wenn Sie Flask auf Ihrem eigenen Rechner einrichten würden, könnten Sie automatisch alle benötigten Bibliotheken installieren, indem Sie diesen Befehl in Ihrem Terminal ausführen:

      pip install -r requirements.txt

      Dies weist das Python-Paket-Installationsprogramm (pip) an, die Datei requirements.txt zu lesen (-r) und alles zu installieren, was darin aufgeführt ist.

    • Für eine umfassende Anleitung zum Einrichten von Python-Projekten und zur Verwaltung von Abhängigkeiten sollten Sie sich den offiziellen Python Packaging User Guide ansehen. Jetzt können Sie aber sofort mit der Erstellung Ihrer Flask-Anwendung in der CS50-Umgebung weitermachen, da dort alles bereits eingerichtet ist.

  • Hier ist eine sehr einfache Flask-Anwendung in app.py:

    # Says hello to world by returning a string of text
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    @app.route("/") # a decorator
    def index():
        return "hello, world"

    Beachten Sie, dass dieser Code Flask anweist, auf HTTP-Anfragen, die über die /-Route eingehen, mit dem Aufruf der Funktion index() zu reagieren, die einfach den Text hello, world zurückgibt. Was auch immer von einer Funktion zurückgegeben wird, wird an den Browser gesendet.

    Die Syntax “@app.route” wird Ihnen vielleicht aus dem Short über Objektorientierte Programmierung bekannt vorkommen. Es ist ein Decorator, der es erlaubt, eine Funktion prägnant zu annotieren. Decorators werden in Flask verwendet, um zu definieren, wie der Webserver auf eingehende HTTP-Anfragen antwortet, basierend auf dem Pfad-Teil (in diesem Zusammen auch Route genannt) der URL. Hinter den Kulissen generiert der Decorator den notwendigen Python-Code, der prüft, ob eine eingehende HTTP-Anfrage mit dem an @app.route(...) übergebenen Argument übereinstimmt und ruft dann die mit dem Decorator annotierte Funktion auf. Wenn die Funktion zurückkehrt, kümmert sich der vom Decorator erzeugte Code darum, den zurückgegebenen String an den Browser zu senden.

    Diese Verwendung von Decorators ist ein weiteres Beispiel für das Prinzip der Abstraktion – wir müssen uns nicht mit den Details der Interaktion mit dem Browser auf einer niedrigen Ebene beschäftigen.

  • Neben reinem Text (Plaintext) können wir auch Code erstellen, der HTML zurückgibt:

    # Says hello to world by returning a string of HTML
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        return '<html><body>hello</body></html>'

    Beachten Sie, dass hier kein einfacher Text, sondern HTML zurückgegeben wird.

Templates

  • Zur Verbesserung unserer Anwendung können wir auch HTML auf der Grundlage von Templates bereitstellen, indem wir einen Ordner mit dem Namen templates und in diesem Ordner eine Datei mit dem Namen index.html mit dem folgenden Code erstellen:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <title>hello</title>
        </head>
    
        <body>
            hello, {{ name }}
        </body>
    
    </html>

    Beachten Sie die doppelten geschweiften Klammern {{ name }}: das ist ein Platzhalter für etwas, das der Flask-Server zur Laufzeit mit einem anderen String ersetzen wird. Die von Flask verwendete Templating-Sprache, die hier zum Einsatz kommt, heißt Jinja.

  • Erstellen Sie dann in demselben Ordner, in dem sich der Ordner template befindet, eine Datei namens app.py und fügen Sie den folgenden Code hinzu:

    # Uses request.args.get
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    
    @app.route("/")
    def index():
        name = request.args.get("name", "world")
        return render_template("index.html", name=name)

    Beachten Sie, dass dieser Code app als die Flask-Anwendung definiert. Dann wird mit dem bereits bekannten Decorator eine /-Route angelegt, die den Inhalt von index.html an den Browser schickt. Wichtig: beim Aufruf von render_template wird ein benanntes Argument name verwendet. Der Wert, der an das name-Argument übergeben wird, steht in der Variablen name, die von der Funktion request.args.get befüllt wurde – und zwar mit dem Wert, der in der URL beim Schlüssel-Wert-Paar stand, das den Schlüssel name hat. Diesen Wert könnte beispielsweise eine Benutzerin oder ein Benutzer in der URL angegeben haben (etwa mit .../?name=Petra). Wenn kein Name angegeben wurde, soll die get(…)-Funktion ersaztweise world zurückliefern.

  • Sie können diese Webanwendung ausführen, indem Sie flask run in ein Terminalfenster eingeben. Wenn Flask dann nicht startet, vergewissern Sie sich, dass Ihre Syntax in jeder der oben genannten Dateien korrekt ist (haben Sie alle Funktionen importiert, die Sie nutzen?) und dass Ihre Dateien wie folgt organisiert sind:

    /templates
        index.html
    app.py
    requirements.txt
  • Sobald der Server läuft, werden Sie von VS Code aufgefordert, auf einen Link zu klicken, um die URL zu Ihrer Anwendung im Browser zu öffnen. Wenn Sie dies getan haben, versuchen Sie, ?name=[Ihr Name] an die Basis-URL in der Adresszeile Ihres Browsers anzuhängen. Ihr Name sollte dann auf der Seite erscheinen, d.h., der Server hat dynamisch auf Ihre Eingabe reagiert!

Forms

  • Die meisten Menschen werden nicht gewillt sein, ihre Daten direkt in die Adressleiste einzugeben. Stattdessen erwarten sie Formularfelder auf Webseiten. Dazu müssen wir index.html wie folgt ändern:

    <!DOCTYPE html>
    
    <html lang="en">
        <head>        
            <title>hello</title>
        </head>
        <body>
            <form action="/greet" method="get">
                <input name="name" type="text">
                <button type="submit">Greet</button>
            </form>
        </body>
    </html>

    Beachten Sie, dass jetzt ein Formular erstellt wird, das den Namen des aufnimmt und ihn dann an eine Route namens /greet weitergibt. Was auch immer als Wert des name-Attributs im input-Tag angegeben wird (hier ist der Wert ebenfalls name), wird vom Browser verwendet, um den vom Benutzer eingegebenen Text dem Server als Teil eines Key-Value-Paares bereitzustellen.

  • Weiterhin müssen wir app.py wie folgt ändern:

    # Adds a form, second route
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    @app.route("/greet")
    def greet():
        return render_template("greet.html",
            name=request.args.get("name", "world"))

    Beachten Sie, dass die Standard-Route unser Formular anzeigt, in das der Benutzer seinen Namen eingeben kann. Die Route /greet wird den name an das Template greet.html weitergeben (das aber noch nicht existiert).

  • Um die Implementierung des Beispiels abzuschließen, benötigen Sie noch ein Template für greet.html im Ordner templates:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>hello</title>
        </head>
        <body>
            hello, {{ name }}
        </body>
    </html>

    Beachten Sie, dass dieses Template nun die Begrüßung des Benutzers wiedergibt, gefolgt von seinem Namen.

Templates

  • Unsere beiden Webseiten, index.html und greet.html, sind sich sehr ähnlich und enthalten dupliziertes Markup. Wäre es nicht schön, wenn wir uns nur um die Unterschiede kümmern müssten, aber nicht um das überall gleiche Layout?

  • Diesen Wunsch können wir uns mit Templates leicht erfüllen. Templates können nämlich verschachtelt werden Erstellen Sie zunächst ein neues Template mit dem Namen layout.html und schreiben Sie den Code wie folgt:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>hello</title>
        </head>
        <body>
            {% block body %}{% endblock %}
        </body>
    </html>

    Beachten Sie, dass das {% block body %}...{% endblock %} ein Platzhalter ist, der es uns erlaubt, an dieser Stelle anderen Code aus anderen HTML-Templates einzufügen.

  • Ändern Sie dann Ihre index.html wie folgt, um davon Gebrauch zu machen:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/greet" method="get">
            <input autofocus name="name" type="text">
            <button type="submit">Greet</button>
        </form>
    
    {% endblock %}

    Beachten Sie, dass die Zeile {% extends "layout.html" %} dem Server mitteilt, wo er das Layout dieser Seite abrufen kann. Der Teil {% block body %}{% endblock %} legt fest, was in layout.html an der entsprechenden Stelle eingefügt werden soll. Flask sucht in layout.html dann nach dem zugehörigen body-Block und ersetzt ihn durch den entsprechenden body-Block-Code aus index.html.

  • Ändern Sie schließlich greet.html wie folgt:

    {% extends "layout.html" %}
    
    {% block body %}
        hello, {{ name }}
    {% endblock %}

    Beachten Sie, dass dieser Code kürzer und kompakter ist.

Request-Methoden

  • Es gibt Szenarien, in denen es nicht gut ist, die Methode GET zu verwenden. Zum Beispiel würden Benutzernamen und Passwörter in der URL auftauchen, wenn ein Benutzer ein Anmeldeformular via GET abschicken würde.

  • Dieses Problem lässt sich mit der Methode POST lösen. Dazu ändern wir die Datei app.py wie folgt:

    # Switches to POST
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    @app.route("/greet", methods=["POST"])
    def greet():
        return render_template("greet.html",
            name=request.form.get("name", "world"))

    Beachten Sie, dass POST zur Route /greet hinzugefügt wurde und dass wir dann request.form.get anstelle von request.args.get verwenden müssen.

  • Neben der unsichtbaren Übermittlung der Daten hat POST den Vorteil, dass der Browser die Formulardaten nicht einfach erneut übermittelt, wenn Sie die Seite neu laden (z.B. mit F5). Er zeigt dann eine Warnung an, ob Sie das wirklich tun wollen. Der Grund für dieses Verhalten ist, dass POST (und einige andere HTTP-Methoden, auf die wir hier nicht eingehen) insbesondere dazu dient, etwas auf dem Server zu verändern, zum Beispiel einen Datensatz hinzuzufügen, zu ändern oder zu löschen. Sie sollten niemals GET für solche Operationen verwenden. GET ist nur zum Lesen gedacht, d.h. der serverseitige Code sollte bei GERT keine Änderungen an Daten oder Systemen vornehmen.

  • Unser Programm kann weiter verbessert werden, indem man eine einzige Route für GET und POST verwendet. Dazu ändern Sie app.py wie folgt (nicht in der Vorlesung gezeigt):

    # Uses a single route
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    @app.route("/", methods=["GET", "POST"])
    def index():
        if request.method == "POST":
            return render_template("greet.html",
                name=request.form.get("name", "world"))
        return render_template("index.html")

    Beachten Sie, dass nun sowohl GET als auch POST in einem einzigen Routing verwendet werden. Daher wird nun request.method verwendet, um die Verarbeitung auf der Grundlage der vom Benutzer verwendeten Methode korrekt durchzuführen.

  • Dazu passend ändern Sie Ihre index.html wie folgt:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/" method="post">
            <input name="name" type="text">
            <button type="submit">Greet</button>
        </form>
    
    {% endblock %}

    Beachten Sie, dass die action geändert wurde.

  • Nun gibt es noch einen Fehler im Code. Wenn jemand keinen Namen in das Formular eingibt, zeigt unsere Implementierung “Hallo” ohne Namen an. Wir können unseren Code verbessern, indem wir app.py wie folgt bearbeiten:

    # Moves default value to template
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    @app.route("/", methods=["GET", "POST"])
    def index():
        if request.method == "POST":
            return render_template("greet.html"
                name=request.form.get("name"))
        return render_template("index.html")

    Beachten Sie, dass wir nunname=request.form.get("name")) verwenden, also keinen Default-Wert setzen, wenn name im Formular leer ist.

  • Ändern Sie schließlich greet.html wie folgt:

    {% extends "layout.html" %}
    
    {% block body %}
    
        hello,
        {% if name %}
            {{ name }}
        {% else %}
            world
        {% endif %}
    
    {% endblock %}

    Vorher stand in dieser Datei hello, {{ Name }}. Jinja-Templates bieten uns wie hier gezeigt die Möglichkeit, mit einfachen bedingten Anweisungen zu beeinflussen, was angezeigt werden soll.

Frosh IMs

  • Wir schauen uns nun an, wie man eine einfache Webanwendungen erzeugt. Unser Projekt heißt “Frosh IMs” oder froshims, eine Webanwendung, die es Studierenden ermöglichen soll, sich für Sportangebote zu registrieren (Frosh: Anspielung auf Freshmen, IM steht für intramural sports: (Hoch-)Schulsport).

  • Schließen Sie alle Code-Fenster und erstellen Sie einen neuen Ordner, indem Sie mkdir froshims in ein Terminalfenster eingeben. Geben Sie dann cd froshims ein, um in diesen Ordner zu wechseln. Erstellen Sie darin ein Verzeichnis namens templates, indem Sie mkdir templates eingeben.

  • Führen Sie code requirements.txt aus und schreiben Sie dort hinein:

    Flask

    Sie sehen: Wiezuvor ist Flask erforderlich, um unsere Anwendung auszuführen.

  • Geben Sie schließlich code app.py ein und schreiben Sie folgenden Code:

    # Implements a registration form using a select menu, validating sport server-side
    
    from flask import Flask, render_template, request
    
    app = Flask(__name__)
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    @app.route("/register", methods=["POST"])
    def register():
    
        # Validate submission
        if not request.form.get("name") or request.form.get("sport") not in SPORTS:
            return render_template("failure.html")
    
        # Confirm registration
        return render_template("success.html")

    Beachten Sie, dass eine Fehlermeldung angezeigt wird, wenn das Feld name oder sport nicht korrekt ausgefüllt ist.

  • Nun erstellen Sie eine Datei index.html im Ordner templates mit folgendem Inhalt:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Register</h1>
        <form action="/register" method="post">
            <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
            <select name="sport">
                <option disabled selected value="">Sport</option>
                {% for sport in sports %}
                    <option value="{{ sport }}">{{ sport }}</option>
                {% endfor %}
            </select>
            <button type="submit">Register</button>
        </form>
    {% endblock %}
  • Weiterhin legen wir die Datei layout.html an (ebenfalls in templates), indem wir code templates/layout.html eingeben und folgendes hineinschreiben:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>froshims</title>
        </head>
    
        <body>
            {% block body %}{% endblock %}
        </body>
    
    </html>
  • Nun erstellen wir ein Template mit dem Namen success.html:

    {% extends "layout.html" %}
    
    {% block body %}
        You are registered!
    {% endblock %}
  • Erstellen Sie schließlich im Template-Ordner eine Datei mit dem Namen failure.html:

    {% extends "layout.html" %}
    
    {% block body %}
        You are not registered!
    {% endblock %}
  • Führen Sie flask run aus und testen Sie die Anwendung in diesem Stadium.

Radio Buttons

  • Die bisherige Applikation verwendet ein Dropdown-Menü zur Auswahl der Sportart. Wir können stattdessen auch Radio-Buttons verwenden, indem wir index.html wie folgt ändern:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Register</h1>
        <form action="/register" method="post">
            <input name="name" type="text">
            {% for sport in sports %}
                <input name="sport" type="radio" value="{{ sport }}"> {{ sport }}
            {% endfor %}
            <button type="submit">Register</button>
        </form>
    {% endblock %}

    Beachten Sie, dass type in radio geändert wurde und statt select- und option-Tags nun input-Tags verwendet werden. Beachten Sie auch, dass alle erzeugten Radio-Buttons denselben name haben, nämlich “sport”. Auf der Serverseite erhalten wir die ausgewählte Sportart dann über den Key “sport”.

  • Wenn Sie erneut flask run ausführen, können Sie sehen, wie sich die Anwendung nun verändert hat.

  • Wir passen app.py nun weiter an, um die Anmeldungen zu speichern, vorerst ganz einfach direkt in einem Dictionary im Programm:

    # Implements a registration form, storing registrants in a dictionary, with error messages
    
    # NEW: redirect
    from flask import Flask, redirect, render_template, request
    
    app = Flask(__name__)
    
    REGISTRANTS = {}
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    @app.route("/register", methods=["POST"])
    def register():
    
        # Validate name
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="Missing name")
    
        # Validate sport
        sport = request.form.get("sport")
        if not sport:
            return render_template("error.html", message="Missing sport")
        if sport not in SPORTS:
            return render_template("error.html", message="Invalid sport")
    
        # Remember registrant
        REGISTRANTS[name] = sport
    
        # Confirm registration by instructing the browser
        # retrieve the registrants route that will render
        # the list of registrations (why? so our register
        # function does not duplicate the rendering logic)
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        return render_template("registrants.html", registrants=REGISTRANTS)

    Beachten Sie, dass ein Dictionary namens REGISTRANTS verwendet wird, um mittels REGISTRANTS[name] die von der Person, die den Namen name eingegeben hat, ausgewählte Sportart zu speichern. Beachten Sie auch, dass registrants=REGISTRANTS das Wörterbuch an das Template weitergibt. Schließlich ist zu beachten, dass die Funktion register das Ergebnis eines Aufrufs an redirect() zurückgibt, der den Browser anweist, eine weitere Anfrage zu senden, diesmal unter Verwendung der angegebenen URL (/registrants). Dieser Ansatz ist eine gängige Praxis und ein Beispiel für Separation of Concerns.

  • Zusätzlich erzeugen wir noch ein Template error.html:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Error</h1>
        <p>{{ message }}</p>
        <img alt="Grumpy Cat" src="/static/cat.jpg">
    {% endblock %}
  • Erstellen Sie außerdem ein neues Template mit dem Namen registrants.html:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Registrants</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Sport</th>
                </tr>
            </thead>
            <tbody>
                {% for name in registrants %}
                    <tr>
                        <td>{{ name }}</td>
                        <td>{{ registrants[name] }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}

    Beachten Sie, dass {% for name in registrants %}...{% endfor %} über alle Registrierungen iteriert. Diese von Jinja angebotene Möglichkeit, direkt im Template über Listen oder Dictionary-Keys zu iterieren, ist hier sehr nützlich.

  • Erstellen Sie schließlich einen Ordner namens static im selben Ordner wie app.py. Laden Sie dort z.B. die folgende Datei hoch: cat.jpg.

  • Führen Sie nun flask run aus und spielen Sie mit der Anwendung.

  • Sie haben nun eine dynamische Webanwendung! Allerdings gibt es einige Sicherheitslücken! Da vieles auf der Client-Seite läuft, könnte ein Angreifer den HTML-Code ändern und unsere Website hacken. Das Anmelden zu mehreren Sportarten ist nicht möglich bzw. erfordert mehrmaliges Ausfüllen des Formulars. Und außerdem bleiben die Daten nicht erhalten, wenn der Flask-Server heruntergefahren wird. Wie könnten wir erreichen, dass unsere Daten auch nach einem Neustart des Servers erhalten bleiben?

Persistenz, UUIDs, JSON

  • Der Reihe nach! Wir wollen es erst einmal möglich machen, dass man sich bei mehreren Sportarten auf einmal registrieren kann – dazu können wir statt Radio-Buttons Checkboxen verwenden. Der Unterschied besteht darin, dass der Browser bei Radio-Buttons erzwingt, dass nur höchstens ein Optionsfeld ausgewählt werden kann, bei Checkboxen hat man hingegen freie Auswahl. Wir bekommen dann auf der Serverseite also entweder keine, eine oder mehrere Sportarten auf einmal zurückgeliefert. Darauf müssen wir auf der Serverseite anders reagieren als bisher. Bei den Radio-Buttons haben wir ja entweder keine oder genau eine Sportart übermittelt bekommen.

  • Um dies zu erreichen, können wir die Vorlage index.html wie folgt ändern:

    {% for sport in sports %}
        <input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }}
    {% endfor %}

    Beachten Sie, dass der Typ der Input-Tags nun checkbox ist. Interessant: Der name aller Checkboxen ist derselbe: sport. Das bedeutet, dass der Browser mehrere Schlüssel-Wert-Paare mit demselben Schlüssel (sport) senden wird, je ein Paar für jedes angekreuzte Kontrollkästchen. Wir müssen den serverseitigen Code daher anpassen, um damit zurechtzukommen (folgt gleich).

  • Weiterhin benötigen wir eine Lösung, mit der wir die Registrierungen dauerhaft speichern können (Persistenz). Dadurch überleben die Daten einen Server-Neustart.

    • Da wir in diesem Kurs keine SQL-Datenbanken behandeln, greifen wir auf einen einfachen dateibasierten Mechanismus zurück, um Daten zu persistieren.
    • Die Grundidee besteht darin, beim Absenden des Formulars alle Sportarten, für die sich eine Person angemeldet hat, zusammen mit dem eingegebenen Namen in eine Datei zu schreiben, also jedes Mal eine neue Datei anzulegen, wenn das Formular abgesendet wird.
  • Wie sollten wir diese Dateien nennen?

UUIDs

  • Ein gängiger Ansatz besteht darin, jeder Datei einen zufällig generierten, eindeutigen Namen zu geben. Für den Namen können wir eine sogenannte UUID (universally unique identifier) verwenden.
    • Dieser Vorschlag mag auf den ersten Blick seltsam erscheinen, er ist aber einfacher als viele Alternativen, die naheliegender erscheinen. Wir könnten uns zum Beispiel ja auch dafür entscheiden, als Dateiname den Namen zu verwenden, den die Person beim Absenden ins Formular eingegeben hat. Das Problem hierbei ist, dass der Name Zeichen enthalten könnte, die in Dateinamen nicht verwendet werden dürfen oder Sicherheitsprobleme verursachen könnten (z.B. wenn eine böswillige Person absichtlich einen relativen Pfad wie “../../” in ihrem Namen eingeben würde). Um das zu verhindern müssten wir die Eingaben genau analysieren und bereinigen – das lenkt aber von unserem eigentlichen Ziel ab!
    • Die Verwendung von UUIDs ist auch besser als die Verwendung eines einfachen Integer-Zählers, der immer um eins erhöht wird, sobald sich ein Benutzer registriert. Wir müssten dann jedes Mal, wenn ein neuer Datensatz gespeichert werden soll, den nächsten Wert des Zählers ermitteln, indem wir uns z.B. die Liste der bisherigen Dateien besorgen, damit den aktuellen maximalen Zählerwert bestimmen und diesen dann um eins erhöhen.
    • Das ist nicht nur etwas ineffizient, sondern wir müssten auch Grenzfälle berücksichtigen, zum Beispiel die Situation, dass sich mehrere Benutzer exakt gleichzeitig anmelden (ob nun absichtlich oder unabsichtlich). Wir müssten sicherstellen, dass es nicht passieren kann, dass in diesem Fall der Zählerwert doppelt verwendet wird, etwa weil parallele Anfragen vom Webserver (nahezu) gleichzeitig verarbeitet werden. Solche Situationen nennt man auch Race Conditions (wer schneller ist, gewinnt – und überschreibt möglicherweise vorherige Änderungen ohne es zu merken). Damit verbunden sind spannende Informatik-Probleme (Concurrency, Gleichzeitigkeit), die wir in diesem Kurs aber nicht behandeln. Da UUIDs eindeutig sind, gehen wir diesen Problemen damit einfach aus dem Weg.

JSON

  • Nachdem wir das Problem der Wahl eines Dateinamens gelöst haben, müssen wir ein strukturiertes Datenformat festlegen, um die Registrierungen zu speichern.

  • Zu diesem Zweck werden wir die JavaScript Object Notation oder JSON verwenden, eine computerfreundliche und menschenlesbare Methode, um Daten zu speichern. JSON ist ein gängiges Format, mit dem wir ein Dictionary mit Schlüsseln und Werten in Textform speichern können, wobei als Werte neben Zahlen und Strings auch Listen und Dictionaries möglich sind.

  • Eine JSON-Datei könnte beispielsweise so aussehen.

    {
        "id": "uuid-here",
        "name": "student-name",
        "sports": ["sport1", "sport2"]
    }
  • Um die bisher beschriebenen Änderungen umzusetzen, müssen Sie folgende Schritte nachvollziehen.

  • Ändern Sie zunächst die Datei `index.html wie folgt:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Register</h1>
        <form action="/register" method="post">
            <input autofocus name="name" placeholder="Name" type="text">
            {% for sport in sports %}
                <input name="sport" type="checkbox" value="{{ sport }}">
                {{ sport }}
            {% endfor %}
            <button type="submit">Register</button>
        </form>
    {% endblock %}
  • Stellen Sie sicher, dass layout.html wie folgt aussieht:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <title>Registration Admin Panel</title>
        </head>
    
        <body>
            {% block body %}{% endblock %}
        </body>
    
    </html>
  • Stellen Sie sicher, dass error.html wie folgt aussieht:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Error</h1>
        <p>{{ message }}</p>
        <img alt="Grumpy Cat" src="/static/cat.jpg">
    {% endblock %}
  • Ändern Sie die Datei registrants.html wie folgt:

    {% extends "layout.html" %}
    
    {% block body %}
        <h1>Registrants</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Sport</th>
                </tr>
            </thead>
            <tbody>
                {% for registrant in registrants %}
                    <tr>
                        <td>{{ registrant.name }}</td>
                        <td>{{ registrant.sport }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
  • Und nun kommen wir zum interessantesten Teil. Ändern Sie app.py wie folgt:

    import json  # for JSON file creation and reading
    import os    # for file operations
    import uuid  # for generating UUIDs
    from flask import Flask, redirect, render_template, request
    from glob import glob     # allows us to enumerate the JSON files
    from pathlib import Path  # makes path handling easier
    
    app = Flask(__name__)
    
    # This is the directory where we will store our JSON files:
    DATA_DIR = Path("data")  # will be a subdirectory in our app's folder
    DATA_DIR.mkdir(exist_ok=True)  # create it / make sure it exists
    
    # same as before
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    # same as before
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    
    # changes here
    @app.route("/register", methods=["POST"])
    def register():
        # Validate name
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="Missing name")
    
        # NEW: we get the *list* of all checked sports with getlist()
        sports = request.form.getlist("sport")
    
        # Validate sports
        if not sports:
            return render_template("error.html", message="Missing sport")
        for sport in sports:
            if sport not in SPORTS:
                return render_template("error.html", message="Invalid sport")
    
        # NEW: create registrant dict in a local variable and
        # store UUID in the dict so that everything is kept together
        registrant = {
            "id": str(uuid.uuid4()),  # e.g. "f8a39c33-6d7b-444a-a561-4737ead1da69"
            "name": name,
            "sports": sports          # attention: sports is now a list
        }
    
        # Convert the dict to JSON string and save it to a file
        # with the UUID as the filename (.json extension).
        # Syntax is explained below.
        with open(DATA_DIR / f"{registrant['id']}.json", "w") as f:
            json.dump(registrant, f)
    
        # redirect the browser to show list of registrations
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        # We want to re-use the existing template.
        # This template expects a *flat list of registrations*
        # in the variable registrations that looks like this:
        # each element of the list is a dictionary with a
        # key-value pair "name" and a key-value pair "sport".
        # This means there are multiple elements if a user
        # has registered for multiple sports. Our registrant
        # dict is not in this format. it contains a key "name"
        # and a key "sports", where sports is a list!
        # We have to flatten this data structure so that it
        # matches the template's expectation.
    
        flat_registrants = []
        # iterate over all JSON files with glob()
        for file_path in DATA_DIR.glob("*.json"):
            # open the current JSON file
            with open(file_path) as f:
                # re-create a dict based on the JSON data
                reg = json.load(f)
                # flatten the sports list
                for sport in reg["sports"]:
                    flat_registrants.append({
                        "name": reg["name"],
                        "sport": sport
                    })
    
        return render_template("registrants.html", registrants=flat_registrants)

    Die register-Route nimmt den Namen und die Sportarten aus dem Registrierungsformular entgegen und speichert sie in einem Dictionary. Das Dictionary wird dann in eine JSON-Datei geschrieben (“persistiert”), deren Dateiname aus einer UUID besteht.

  • In obigem Code werden zwei interessante Konzepte verwendet:
    • with open(...) as f: Die with-Anweisung in Python ist ein Kontext-Manager, der den korrekten Umgang mit Ressourcen wie Dateien, Netzwerk- oder Datenbankverbindungen sicherstellt, indem er deren Öffnen und Schließen automatisch verwaltet. Wenn with mit Dateien wie in with open(...) as f: verwendet wird, wird die geöffnete Datei automatisch geschlossen, wenn der Block, den der Kontext-Manager einleitet, verlassen wird – selbst wenn es in diesem Block zu Fehlern kommt. Dieses Konstrukt ist daher besser als das manuelle Öffnen und Schließen von Dateien.
    • DATA_DIR / 'name.json' verbindet zwei Pfade miteinander (erzeugt also z.B. “data/name.json” unter Linux oder “data\name.json” unter Windows). Wie funktioniert das? Objekte können das Verhalten von Operatoren mit Dunder Methods (dunder steht für double underscore) ändern. In diesem Fall implementiert das Path-Objekt die Dunder-Method __truediv__, die das Verhalten des / Operators festlegt. Dieser Operator kann dann verwendet werden, um Pfadkomponenten zu verbinden. Dadurch wird der Code etwas besser lesbar.
  • Sie können nun flask run ausführen und die Anwendung ausprobieren. Sie sollten wie zuvor dazu in der Lage sein, Registrierungen zu erstellen. Wenn Sie den Flask-Server mit Strg-C beenden und erneut starten, gehen die bereits gespeicherten Daten nicht verloren. Untersuchen Sie auch die JSON-Dateien im data-Verzeichnis!

Abmeldungen ermöglichen

  • Bislang können wir nur neue Registrierungen vornehmen, aber keine Abmeldungen. Wir werden nun unsere Anwendung erweitern, um diese Funktionalität einzubauen.

  • Bevor wir Änderungen vornehmen, machen wir uns Gedanken mit welchem Design wir das bewerkstelligen können – denken Sie mit! Das ist eine Gelegenheit, Ihre Problemlösungsfähigkeiten zu trainieren.

    • Eine einfache Möglichkeit, die Funktionalität zu implementieren, ist zum Beispiel die Bereitstellung eines Buttons zur Abmeldung hinter jedem Eintrag in der Liste der Registrierungen.

    • Wir können unser Wissen nutzen, um mit dem Button ein Formular abzusenden, das eine neue deregister-Route ansteuert – mit POST da das Löschen einer Registrierung ja Änderungen auf der Server-Seite vornimmt.

    • Welche Informationen benötigen wir auf der Server-Seite?

    • Erstens müssen wir wissen, welche Person betroffen ist, da wir den Inhalt der entsprechenden Datei ändern müssen. Am leichtesten fällt uns das, wenn wir die UUID der Person an unsere deregister-Route weitergeben würden. Dazu müssten wir die UUID an der Stelle griffbereit haben, wo wir das Formular mit dem Deregister-Button erzeugen.

    • Zweitens müssen wir unserer Deregister-Funktion mitteilen, welche Sportart aus der Liste der registrierten Sportarten entfernt werden soll.

    • Es sind zwei Teilprobleme zu lösen:

      • Schauen Sie sich zunächst an, ob wir diese Informationen im registrants-html-Template aktuell schon zur Verfügung haben oder wie wir sie uns verfügbar machen können.
      • Nehmen wir an, wir hätten die beiden Informationen griffbereit, wenn wir die Tabelleneinträge erzeugen. Wie könnten wir sie elegant an die Server-Seite übertragen? Das wissen Sie noch nicht.
    • Eine mögliche Lösung zeigt folgender Code für registrants.html:

      {% extends "layout.html" %}
      
      {% block body %}
          <h1>Registrants</h1>
          <table>
              <thead>
                  <tr>
                      <th>Name</th>
                      <th>Sport</th>
                      <th></th>
                  </tr>
              </thead>
              <tbody>
                  {% for registrant in registrants %}
                      <tr>
                          <td>{{ registrant.name }}</td>
                          <td>{{ registrant.sport }}</td>
                          <td>
                              <form action="/deregister" method="post">
                                  <input name="id" type="hidden" value="{{ registrant.id }}">
                                  <input name="sport" type="hidden" value="{{ registrant.sport }}">
                                  <button type="submit">Deregister</button>
                              </form>
                          </td>
                      </tr>
                  {% endfor %}
              </tbody>
          </table>
      {% endblock %}

      Sie sehen hier, wie wir Formularfelder vom Typ “hidden” verwenden können, um Informationen an den Server zu übermitteln. Diese Felder werden im Browser nicht angezeigt. Beachten Sie auch, dass dieses Template erwartet, dass die Liste der Registrierungen einen zusätzlichen Key id enthält, den unsere derzeitige Implementierung noch nicht einfügt – der aber leicht hinzuzufügen sein wird, da wir die UUID glücklicherweise nicht nur als Dateiname nutzen, sondern zusätzlich auch im Dictionary in den JSON-Dateien speichern!

  • Schauen wir uns die geänderte app.py an:

    import json  # for JSON file creation and reading
    import os    # for file operations
    import uuid  # for generating UUIDs
    from flask import Flask, redirect, render_template, request
    from glob import glob     # allows us to enumerate the JSON files
    from pathlib import Path  # makes path handling easier
    
    app = Flask(__name__)
    
    # This is the directory where we will store our JSON files:
    DATA_DIR = Path("data")  # will be a subdirectory in our app's folder
    DATA_DIR.mkdir(exist_ok=True)  # create it / make sure it exists
    
    # same as before
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
    ]
    
    
    # No changes here
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS)
    
    # No changes here
    @app.route("/register", methods=["POST"])
    def register():
        # …
    
    # NEW
    @app.route("/deregister", methods=["POST"])
    def deregister():
        # obtain UUID from hidden form field
        id = request.form.get("id")
        # defensive programming:
        # make no assumptions (check before proceeding)
        if id:
            file_path = DATA_DIR / f"{id}.json"
            # defensive programming, again
            if file_path.exists():
                # OK, we found the user's data file
                # We have to read it, remove one of
                # the sport entries and save it back.
                with open(file_path) as f:
                    reg = json.load(f)
                    reg["sports"].remove(request.form.get("sport"))
                    with open(file_path, "w") as g:
                        json.dump(reg, g)
    
        # let the browser fetch the (updated) list
        return redirect("/registrants")
    
    
    # Only one line changed here
    @app.route("/registrants")
    def registrants():
        flat_registrants = []
        for file_path in DATA_DIR.glob("*.json"):
            with open(file_path) as f:
                reg = json.load(f)
                # Flatten sports list
                for sport in reg["sports"]:
                    flat_registrants.append({
                        "id": reg["id"],  # NEW
                        "name": reg["name"],
                        "sport": sport
                    })
    
        return render_template("registrants.html", registrants=flat_registrants)

    Beachten Sie, wie wir nun auch die id an das Template übergeben. Beachten Sie auch, dass der Code zum Entfernen einer Registrierung der Idee des Defensiven Programmierens (siehe Short zur Fehlerbehandlung) folgt, da er zunächst prüft, ob der Parameter id und die Datei existieren, bevor er fortfährt. Dadurch werden Exceptions oder Fehlerzustände vermieden, die den Server zum Absturz bringen oder zu Datenverlust führen könnten.

Cookies and Sessions

  • Die bisherige Implementierung von froshims ist höchstens zur Administration des Sportangebots nützlich. Im aktuellen Zustand könnten wir die Anwendung nicht auf einem öffentlichen Server bereitstellen, auf dem sich Studierende selbst für Angebote registrieren könnten – schließlich könnte jede Person die Daten von allen anderen einsehen und ändern!
    • Böswillige könnten Anmeldungen im Namen anderer Personen erzeugen oder diese über den “Deregistrieren”-Button abmelden.
    • Im Web muss man sich daher üblicherweise auf Webseiten anmelden und hat nur Zugriff auf die eigenen Daten. Wie funktioniert das?
  • Eine wichtige Basis für solche Angebote sind HTTP-Cookies. Cookies sind Zeichenfolgen, die auf Ihrem Rechner gespeichert werden. Mit Cookies lässt sich das Konzept einer Sitzung (engl. Session) umsetzen. Innerhalb einer Session teilt Ihr Rechner dem Server dann bei jeder Anfrage mit: “Ich habe mich vorhin auf dieser Seite angemeldet, du kennst mich unter folgender Nummer: …”
    • Im Zuge des Logins, also des Anmeldeprozesses (meist mittels Benutzername und Passwort), sendet der Server ein Cookie an den Browser, das eine zufällige und eindeutige Sitzungs-ID enthält, etwa in Form einer UUID oder einer anderen langen zufälligen Zeichenkette. Der Server merkt sich, für welches Benutzerkonto er diese Session-ID erzeugt hat. Der Browser sendet sie in allen weiteren HTTP-Anfragen an den Server. Dies erlaubt es dem Server, die HTTP-Anfragen verschiedener Benutzer voneinander zu unterscheiden und ihnen Seiten mit unterschiedlichen Inhalten zu schicken, sodass jede Person nur das sieht, was für sie bestimmt ist.

    • Um ein Cookie in einem Browser zu speichern, sendet der Server einen Set-Cookie-Header als Teil einer HTTP-Antwort. Bei allen folgenden HTTP-Anfragen übermittelt der Browser dieses Cookie dann wie im folgenden Beispiel zu sehen:

      GET / HTTP/2
      Host: accounts.google.com
      Cookie: session=93ddcdc3-51f8-4e29-a960-e688bf2518cd

      Hier ist zu sehen, dass eine Session-ID mit einem bestimmten Wert übermittelt wird, die diese Sitzung repräsentiert.

Einfaches Beispiel

  • Bevor wir Sessions in unsere froshims-Anwendung integrieren, betrachten wir ein einfacheres Beispiel.

  • Wir werden Sessions in einer neuen, einfachen Flask-Anwendung implementieren. Dazu erstellen Sie einen Ordner namens login mit folgenden Dateien (nicht gezeigt).

  • Zuerst erstellen Sie eine Datei namens requirements.txt:

    Flask
    Flask-Session

    Beachten Sie, dass wir zusätzlich zu Flask auch Flask-Session einbinden, die zur Unterstützung von Login-Sitzungen erforderlich ist.

  • Zweitens: Erstellen Sie in einem Unterordner templates eine Datei mit dem Namen layout.html:

    <!DOCTYPE html>
    
    <html lang="en">
    
        <head>
            <meta name="viewport" content="initial-scale=1, width=device-width">
            <title>login</title>
        </head>
    
        <body>
            {% block body %}{% endblock %}
        </body>
    
    </html>

    Dadurch erhalten wir wieder ein einfaches Layout mit einem Titel und einem Textkörper.

  • Drittens: Erstellen Sie im Ordner templates eine Datei mit dem Namen index.html:

    {% extends "layout.html" %}
    
    {% block body %}
    
        {% if name %}
            You are logged in as {{ name }}. <a href="/logout">Log out</a>.
        {% else %}
            You are not logged in. <a href="/login">Log in</a>.
        {% endif %}
    
    {% endblock %}

    Beachten Sie, dass hier geprüft wird, ob session["name"] existiert (weiter unten in app.py näher erläutert). Wenn ja, wird eine Willkommensnachricht angezeigt. Wenn nicht, wird empfohlen, eine Seite zum Einloggen aufzurufen.

  • Viertens: Erstellen Sie eine Datei mit dem Namen login.html:

    {% extends "layout.html" %}
    
    {% block body %}
    
        <form action="/login" method="post">
            <input type="text">
            <button type="submit">Log In</button>
        </form>
    
    {% endblock %}

    Dies ist eine einfache Anmeldeseite.

  • Erstellen Sie schließlich die Datei app.py:

    from flask import Flask, redirect, render_template, request, session
    from flask_session import Session
    
    # Configure app
    app = Flask(__name__)
    
    # Configure session
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    
    @app.route("/")
    def index():
        return render_template("index.html", name=session.get("name"))
    
    
    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            session["name"] = request.form.get("name")
            return redirect("/")
        return render_template("login.html")
    
    
    @app.route("/logout")
    def logout():
        session.clear()
        return redirect("/")

    Wichtig sind zunächst die zusätzlichen import-Anweisungen am Anfang der Datei, insbesondere für session. Damit können wir Sessions in unserer Anwendung komfortabel nutzen. Beachten Sie weiterhin, wie session["name"] in den Routen login und logout verwendet wird. Die login-Route wird den angegebenen Login-Namen ermitteln und ihn in einem Session-Dictionary speichern. Die dort abgelegten Daten bleiben während der Sitzungsdauer erhalten, sodass wir bei späteren HTTP-Anfragen darauf Zugriff haben. In der Route logout wird das Ausloggen realisiert, indem session geleert wird.

  • Sicherheitshinweis: Dies ist ein vereinfachtes Beispiel zu Lernzwecken. Echte Anwendungen sollten (1) keine eigene Authentifizierungslogik implementieren, (2) etablierte Bibliotheken (wie Flask-Login) verwenden, (3) Passwörter sicher speichern (Verschlüsselung oder Hashing sind oft nicht ausreichend!), (4) HTTPS verwenden und (5) darüber hinaus weitere bewährte Sicherheitsverfahren befolgen. Besuchen Sie den Kurs “PSI-IntroSP-B: Einführung in Sicherheit und Datenschutz”, um mehr darüber zu erfahren!

  • Mit der Session-Abstraktion können Sie sicherstellen, dass nur eine bestimmte Person Zugriff auf bestimmte Daten und Funktionen in unserer Anwendung hat.

  • Details zu Sessions in Flask stehen in der Flask-Dokumentation.

Sessions in Frosh IMs

  • Jetzt sind Sie gut darauf vorbereitet, unsere froshims-App um Sessions zu erweitern. Dafür müssen wir ein paar Änderungen an app.py vornehmen:

    import json
    import os
    import uuid
    from flask import Flask, redirect, render_template, request, session
    from flask_session import Session
    from glob import glob
    from pathlib import Path
    from datetime import datetime
    
    app = Flask(__name__)
    
    # NEW: Configure session
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    # Ensure data directory exists
    DATA_DIR = Path("data")
    DATA_DIR.mkdir(exist_ok=True)
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
        ]
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS,
                               name=session.get("username"))
    
    # NEW
    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            # Note: In a real application, you would verify
            # passwords here. Do not implement your own
            # authentication system. Use established libraries
            # and follow security best practices!
            session["username"] = request.form.get("name")
            session["history"] = []  # used to keep track of user's activities
            return redirect("/")
        return render_template("login.html")
    
    
    # NEW: forget the user's session to log them out
    @app.route("/logout")
    def logout():
        session.clear()
        return redirect("/")
    
    
    @app.route("/register", methods=["POST"])
    def register():
        # NEW: Ensure user is logged in
        if not session.get("username"):
            return redirect("/login")
    
        # Validate name
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="Missing name")
    
        # Validate sports
        sports = request.form.getlist("sport")
        if not sports:
            return render_template("error.html", message="Missing sport")
        for sport in sports:
            if sport not in SPORTS:
                return render_template("error.html", message="Invalid sport")
    
        # Create registrant with UUID
        registrant = {
            "id": str(uuid.uuid4()),
            "username": session["username"],  # NEW
            "name": name,
            "sports": sports
        }
    
        # Save to JSON file
        with open(DATA_DIR / f"{registrant['id']}.json", "w") as f:
            json.dump(registrant, f)
    
        # NEW: Keep track of user's activities
        session["history"].append({
            "time": datetime.now().strftime("%H:%M:%S"),
            "action": "You registered for " + ", ".join(sports)
        })
    
        return redirect("/registrants")
    
    
    @app.route("/deregister", methods=["POST"])
    def deregister():
        # NEW: Ensure user is logged in
        if not session.get("username"):
            return redirect("/login")
    
        id = request.form.get("id")
        sport = request.form.get("sport")
        if id:
            file_path = DATA_DIR / f"{id}.json"
            if file_path.exists():
                with open(file_path) as f:
                    reg = json.load(f)
                    # NEW: check that the id received from the
                    # browser (hidden form field) actually matches
                    # the currently logged-in user. If not, the
                    # user tampered with the id and intends to
                    # modify a file of someone else. That is not
                    # allowed!
                    if reg["username"] == session["username"]:
                        reg["sports"].remove(sport)
                        with open(file_path, "w") as g:
                            json.dump(reg, g)
                    else:
                        return render_template("error.html", message="Not authorized")
    
        # NEW: again, keep track of their activities
        session["history"].append({
            "time": datetime.now().strftime("%H:%M:%S"),
            "action": f"You de-registered from {sport}"
        })
    
        return redirect("/registrants")
    
    
    @app.route("/registrants")
    def registrants():
        # NEW: Ensure user is logged in
        if not session.get("username"):
            return redirect("/login")
    
        # Collect the user's registrations
        flat_registrants = []
        for file_path in DATA_DIR.glob("*.json"):
            with open(file_path) as f:
                reg = json.load(f)
                # NEW: Only show user's registrations
                if reg["username"] == session["username"]:
                    # Flatten sports list for template compatibility
                    for sport in reg["sports"]:
                        flat_registrants.append({
                            "id": reg["id"],
                            "name": reg["name"],
                            "sport": sport
                        })
    
        return render_template("registrants.html", registrants=flat_registrants)

    Beachten Sie die Kommentare im Quellcode, die erklären, was passiert.

  • Als nächstes müssen wir layout.html ändern, um einen Aktivitätsverlauf anzuzeigen, den wir in der neuen Version der Anwendung einbauen werden:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>login</title>
        </head>
        <body>
            {% block body %}{% endblock %}
            <footer>
                <h3>Your activities during this session</h3>
                {% if session.history %}
                    <ul>
                    {% for entry in session.history %}
                        <li>{{ entry.time }}: {{ entry.action }}</li>
                    {% endfor %}
                    </ul>
                {% else %}
                    No activity yet.
                {% endif %}
            </footer>
        </body>
    </html>
  • Das Template index.html muss ebenfalls geändert werden:

    {% extends "layout.html" %}
    
    {% block body %}
        {% if name %}
    
            <h1>Hello</h1>
            <p>You are logged in as {{ name }}. <a href="/logout">Log out</a>.</p>
    
            <a href="/registrants">Show your current registrations</a>
    
            <h2>Add registration</h1>
            <form action="/register" method="post">
                <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
                {% for sport in sports %}
                    <input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }}
                {% endfor %}
                <button type="submit">Register</button>
            </form>
    
        {% else %}
    
            You are not logged in. <a href="/login">Log in here</a>.
    
        {% endif %}
    {% endblock %}
  • Und wir brauchen jetzt auch ein neues Template login.html:

    {% extends "layout.html" %}
    
    {% block body %}
    
    <form action="/login" method="post">
        <input autofocus name="name" placeholder="Name" type="text">
        <button type="submit">Log In</button>
    </form>
    
    {% endblock %}
  • Starten Sie den Flask-Server und probieren Sie die Anwendung aus.

  • Wenn Sie keine besonderen Vorkehrungen treffen, übermitteln alle Tabs und Browser-Fenster ein einmal gesetztes Cookie an den Server – zwei verschiedene Benutzer können sie so nicht simulieren.

  • Um die Auswirkung von Sessions zu sehen, benötigen Sie zwei verschiedene Browser. Oder Sie verwenden ein “Private-Browsing-Fenster” oder den “Inkognito-Modus”. Solchermaßen erstellte Browser-Fenster teilen sich die Cookies nicht.

  • Öffnen Sie dann die Website in beiden Browsern, melden Sie sich mit unterschiedlichen Benutzernamen an und registrieren Sie sich für Sportarten. Die in den beiden Browsern angezeigten Informationen werden unterschiedlich sein. Untersuchen Sie danach den Inhalt des Verzeichnisses data, um zu sehen, dass nun auch der Anmeldename in den JSON-Dateien abgespeichert wird.

APIs und asynchrone Aktualisierung

  • Moderne Websites sind viel dynamischer als unser Beispielprojekt. Sie sind in der Lage, sich automatisch im Hintergrund (asynchron) zu aktualisieren, ohne dass ein Mensch eingreifen muss.

  • In der Regel ruft dazu JavaScript-Code, der Teil der gerade geöffneten Webseite ist, URLs auf, die zu einer API (Application Programming Interface) gehören, die von einem Webserver bereitgestellt wird. Die über die API abgerufenen Daten werden vom Skript verarbeitet, um Teile des DOM zu verändern.

  • Auf diese Weise kann ein Teil der offenen Webseite ohne Interaktion im Hintergrund aktualisiert werden, also ohne dass die Seite neu geladen werden muss.

  • Vielen APIs verwenden als Datenformat für die Kommunikation JSON. JSON kann nicht nur – wie bereits gezeigt – in Python, sondern auch in JavaScript leicht verarbeitet werden.

  • Auf die technischen Details der asynchronen Programmierung in JavaScript gehen wir in diesem Kurs nicht näher ein. Wir möchten Ihnen aber eine letzte Erweiterung der Anwendung froshims zeigen, um Ihnen ein Gefühl für asynchrones Programmieren und die Nutzung von APIs zu geben.

  • Wir werden die Anwendung so erweitern, dass im Registrierungsformular hinter den Sportarten steht, wie viele Registrierungen es für die jeweilige Sportart bereits gibt. Es ist ja durchaus üblich, dass die Anzahl der Plätze bei solchen Angeboten beschränkt ist. Und da in Stoßzeiten sehr viele Personen versuchen, sich zu registrieren, soll dieser Zähler sich kontinuierlich aktualisieren solange das Registrierungsformular angezeigt wird.

  • Diese Funktion werden wir mit JavaScript und einer einfachen API umsetzen.

  • Unsere API besteht aus einem Endpunkt (bzw. einer Route), die für alle Sportarten die aktuellen Anmeldezahlen zurückliefert.

  • Das Skript wird die API in regelmäßigen Abständen aufrufen und die entsprechenden Teile der Website aktualisieren.

  • Dazu müssen Sie zwei Änderungen an der Datei app.py vornehmen: Einen neuen Import am Anfang und eine neue Route ganz am Ende – der Rest ist unverändert:

    import json
    import os
    import uuid
    from flask import Flask, redirect, render_template, request, session
    from flask import jsonify  # NEW
    from flask_session import Session
    from glob import glob
    from pathlib import Path
    from datetime import datetime
    
    app = Flask(__name__)
    
    # Configure session
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_TYPE"] = "filesystem"
    Session(app)
    
    # Ensure data directory exists
    DATA_DIR = Path("data")
    DATA_DIR.mkdir(exist_ok=True)
    
    SPORTS = [
        "Basketball",
        "Soccer",
        "Ultimate Frisbee"
        ]
    
    @app.route("/")
    def index():
        return render_template("index.html", sports=SPORTS, name=session.get("username"))
    
    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            session["username"] = request.form.get("name")
            session["history"] = []
            return redirect("/")
        return render_template("login.html")
    
    @app.route("/logout")
    def logout():
        session.clear()
        return redirect("/")
    
    @app.route("/register", methods=["POST"])
    def register():
        if not session.get("username"):
            return redirect("/login")
    
        name = request.form.get("name")
        if not name:
            return render_template("error.html", message="Missing name")
    
        sports = request.form.getlist("sport")
        if not sports:
            return render_template("error.html", message="Missing sport")
        for sport in sports:
            if sport not in SPORTS:
                return render_template("error.html", message="Invalid sport")
    
        registrant = {
            "id": str(uuid.uuid4()),
            "username": session["username"],
            "name": name,
            "sports": sports
        }
    
        with open(DATA_DIR / f"{registrant['id']}.json", "w") as f:
            json.dump(registrant, f)
    
        session["history"].append({
            "time": datetime.now().strftime("%H:%M:%S"),
            "action": "You registered for " + ", ".join(sports)
        })
    
        return redirect("/registrants")
    
    
    @app.route("/deregister", methods=["POST"])
    def deregister():
        if not session.get("username"):
            return redirect("/login")
    
        id = request.form.get("id")
        sport = request.form.get("sport")
        if id:
            file_path = DATA_DIR / f"{id}.json"
            if file_path.exists():
                with open(file_path) as f:
                    reg = json.load(f)
                    if reg["username"] == session["username"]:
                        reg["sports"].remove(sport)
                        with open(file_path, "w") as g:
                            json.dump(reg, g)
                    else:
                        return render_template("error.html", message="Not authorized")
    
        session["history"].append({
            "time": datetime.now().strftime("%H:%M:%S"),
            "action": f"You de-registered from {sport}"
        })
    
        return redirect("/registrants")
    
    @app.route("/registrants")
    def registrants():
        if not session.get("username"):
            return redirect("/login")
    
        flat_registrants = []
        for file_path in DATA_DIR.glob("*.json"):
            with open(file_path) as f:
                reg = json.load(f)
                # Only show user's registrations
                if reg["username"] == session["username"]:
                    # Flatten sports list for template compatibility
                    for sport in reg["sports"]:
                        flat_registrants.append({
                            "id": reg["id"],
                            "name": reg["name"],
                            "sport": sport
                        })
    
        return render_template("registrants.html", registrants=flat_registrants)
    
    # NEW: this is an API endpoint that returns JSON instead of HTML
    @app.route("/counts")
    def counts():
        counts = {}  # a dict for the counts: key = sport, value = count
        for sport in SPORTS:
            count = 0
            for file_path in DATA_DIR.glob("*.json"):
                with open(file_path) as f:
                    reg = json.load(f)
                    if sport in reg["sports"]:
                        count += 1
            counts[sport] = count
        return jsonify(counts)  # jsonify returns the required JSON string 
  • Außerdem muss die Liste der Registrierungen (die in index.html angezeigt wird) wie folgt erweitert werden:

    {% extends "layout.html" %}
    
    {% block body %}
    
        {% if name %}
    
            <h1>Hello</h1>
            <p>You are logged in as {{ name }}. <a href="/logout">Log out</a>.</p>
    
            <a href="/registrants">Show your current registrations</a>
    
            <h2>Add registration</h1>
            <form action="/register" method="post">
                <input autofocus name="name" placeholder="Name" type="text">
                {% for sport in sports %}
                    <input name="sport" type="checkbox" value="{{ sport }}">
                    {{ sport }}
                    (<span class="count" id="count-{{ loop.index0 }}">Loading...</span>)
                {% endfor %}
    
                <button type="submit">Register</button>
            </form>
    
            <script>
                let sports = {{ sports|tojson }};  // tojson: Jinja filter that converts a Python list to JS array syntax
    
                // function for live updates
                async function updateCounts() {
                    // contact /counts API endpoint on server (returns JSON)
                    let response = await fetch('/counts');
                    let counts = await response.json();
    
                    // loop over sports and query for count-0, count-1, count-2, etc.
                    // this corresponds to the id of the html element above that needs
                    // to be updated
                    for (let i = 0; i < sports.length; i++) {
                        document.querySelector('#count-' + i).innerHTML =
                            `${counts[sports[i]]} registered`;
                    }
                }
    
                // Update initially and every few seconds
                updateCounts();
                setInterval(updateCounts, 5000);  // Every 5 seconds
            </script>
        {% else %}
    
            You are not logged in. <a href="/login">Log in here</a>.
    
        {% endif %}
    
    {% endblock %}

    Hier passiert viel auf kleinem Raum! Die span-Elemente mit der ID count-x sind unsere Platzhalter, die die jeweilige Anzahl aufnehmen. loop.index0 ist eine Jinja-Funktion, die den Schleifenindex der for-Schleife zurückgibt (beginnend mit der 0 im ersten Durchlauf), d.h. in der ersten Iteration der for-Schleife 0 in der zweiten 1 und in der dritten 2. Wir werden also der Reihe nach count-0, count-1, count-2 als IDs für die span Elemente generieren. Diese ID-Selektoren werden von in der Webseite enthaltenen JavaScript verwendet, um das richtige Element zu finden. Das Ansprechen über die Indizes gelingt natürlich nur, wenn die Sportarten im HTML-Dokument in der gleichen Reihenfolge auftreten wie sie von der API geliefert werden. Das ist hier gewährleistet, weil wir das Sportarten-Array, über das wir im Skript iterieren, über einen Jinja-Filter direkt aus der Python-Liste erzeugen.

  • Vielleicht fragen Sie sich: Könnten wir nicht einfach sport anstelle von loop.index0 verwenden und dessen Inhalt an count- anhängen? Nein, denn sport kann Zeichen enthalten, die in ID-Selektoren nicht erlaubt sind (z.B. die Leerzeichen in “Ultimate Frisbee”).

  • Um zu sehen, dass die dynamische Aktualisierung funktioniert, öffnen Sie die Registierungsseite gleichzeitig mit zwei Browsern. Wenn Sie sich in einem Browser für eine Sportart an- oder abmelden, werden die Zählerstände im anderen Browser automatisch innerhalb von fünf Sekunden aktualisiert.

  • Das Beispiel veranschaulicht, wie Webseiten konstruiert sind, die asynchrone Aktualisierungen enthalten.

  • Wir erwarten von Ihnen in der Prüfung nicht, dass Sie selbst asynchrone JavaScript-Funktionen (async/await) schreiben. Diese Technik kann aber für Ihr Abschlussprojekt nützlich sein.

Zusammenfassend

In dieser Vorlesung haben Sie gelernt, wie man Flask zur Erstellung von Webanwendungen einsetzt. Insbesondere haben wir besprochen …

  • Flask
  • Formulare (Dropdowns, Radio Buttons, Checkboxen, Hidden Form Fields)
  • Templates und Layouts
  • Request-Methoden GET und POST
  • UUIDs
  • JSON zum Persistieren von Daten
  • Cookies und Sessions
  • einfache APIs
  • einfache JSON-Verarbeitung in JavaScript

Dies war die letzte Präsenzvorlesung von Inf-Einf-B. Viel Spaß beim Programmieren!