9.1 Flask

Video

Dieses Short-Video wird zu einem späteren Zeitpunkt bereitgestellt. Ersatzweise stellen wir Ihnen daher vorerst diese HTML-Seite zur Verfügung.

Die Inhalte dieser Seite stellen wir Ihnen in der Prüfung als Hilfsmittel zur Verfügung.

Transkript

AI: Ah, heute ist es soweit! Weißt du eigentlich, dass wir mit diesem Video fast am Ende unserer gemeinsamen Reise angekommen sind?

Prof: Stimmt.

AI: Ja, und was für eine Entwicklung das war! Von den ersten Schritten mit Scratch, über Speicherverwaltung in C, bis hin zu den eleganten Konzepten der objektorientierten Programmierung in Python…

Prof: Toll, ja, aber zähl jetzt bitte nicht alle Konzepte auf, die wir behandelt haben.

AI: Nein, nein. Aber irgendwie ist es auch ein bisschen schade, dass unsere gemeinsame Reise mit den Studierenden bald zu Ende geht.

Prof: Schon, darum sollten wir heute nochmal richtig Gas geben für den Endspurt.

AI: Unbedingt. Den Client-Teil von Webanwendungen haben wir mit HTML, CSS, JavaScript und dem DOM ja schon behandelt. Was uns für Web-Anwendungen noch fehlt, ist die Serverseite. Das schauen wir uns jetzt an. Heute können wir endlich zeigen, wie man mit den Bausteinen, die wir uns in den letzten Einheiten angeschaut haben, vollwertige Web-Anwendungen bauen kann.

Prof: Genau. Unser heutiges Thema, Flask, ist der Schlussstein, der Client und Server verbindet.


Nicht nur Kommandozeile

  • Python wird nicht nur für die Programmierung auf der Kommandozeile verwendet, obwohl das ein Hauptanwendungsfall ist.
  • Python enthält native Funktionalität zur Unterstützung von Netzen und mehr, sodass damit auch Backend-Systeme für Websites geschrieben werden können.

AI: Wie Sie bereits wissen, ist Python eine vielseitige Sprache. Wir haben sie bisher hauptsächlich für die Programmierung auf der Kommandozeile verwendet - denken Sie an unsere ersten Schritte mit Python.

Aber Python kann noch viel mehr! Es ist eine gute Wahl, um damit das Backend einer Website zu schreiben - also den Teil, der auf dem Server läuft und Anfragen von Clients entgegennimmt.

Aber keine Sorge, Sie müssen dafür nicht alle Details von Python’s Netzwerk-Bibliotheken kennen.


Flask

  • Web-Frameworks erleichtern das Entwickeln von Webanwendungen, indem sie die Details der Kommunikation abstrahieren und Hilfsfunktionen bereitstellen.
  • Einige der beliebtesten Frameworks für Python sind: Django, Pyramid und Flask.
  • Wir verwenden Flask, weil es leichtgewichtig und einfach zu handhaben ist, aber dennoch viele Funktionen bietet.

AI: Genau hier kommen Web-Frameworks ins Spiel. Sie sind im Grunde genommen Python-Bibliotheken, die viele der schwierigen, sich wiederholenden Aufgaben übernehmen, die beim Schreiben einer Web-Applikation anfallen.

Mit einem Web Framework müssen Sie sich nicht mehr um jedes kleine Detail kümmern. Sie können sich auf das Wesentliche konzentrieren - Ihre Anwendungslogik - und das Framework erledigt den Rest für Sie.

Es gibt viele verschiedene Web-Frameworks für Python, jedes mit seinen eigenen Stärken. Einige der beliebtesten sind Django, Pyramid und Flask.

Prof: Wir nutzen Flask. Flask ist recht leichtgewichtig. Es bietet alle Funktionen, die wir brauchen, um richtige Web-Applikationen zu bauen.

AI: Absolut! Flask ist sozusagen das Schweizer Taschenmesser unter den Python Web Frameworks - kompakt, aber vielseitig.

Aber bevor wir tiefer in Flask eintauchen, lassen Sie uns noch einmal einen Schritt zurückgehen und uns anschauen, warum wir überhaupt ein Web Framework brauchen. Dafür schauen wir uns an, was passiert, wenn wir eine Website nur mit HTML bauen.


Beispiel: Aktuelle Uhrzeit

  • Wir wissen bereits, dass wir mit HTML Websites bauen können; aber Websites, die nur reines HTML verwenden, leiden unter einer Einschränkung.
  • Stellen Sie sich vor, wir wollen eine Website erstellen, die die aktuelle Uhrzeit in Bamberg anzeigt, und zwar auf die Minute genau.

AI: Stellen Sie sich folgendes Szenario vor: Sie sind beauftragt, eine Website zu erstellen, die immer die aktuelle Uhrzeit in Bamberg anzeigt. Und zwar auf die Minute genau.

Mit dem, was wir bisher gelernt haben, könnten Sie eine HTML-Seite erstellen, die ungefähr so aussieht:

<html>
<head>
<title>Current Time in Bamberg</title>
</head>
<body>
The current time in Bamberg is 14:08
</body>
</html>

Das sieht doch ganz gut aus, oder? Aber was passiert um 14:09? Oder 14:10?

Prof: Ich sehe schon, worauf du hinaus willst. Mit reinem HTML müssten wir jede Minute manuell den HTML-Code ändern, um die Zeit zu aktualisieren. Das ist nicht nur unpraktisch, sondern auch mühsam, sobald die Website öffentlich abrufbar ist, weil wir jede Minute die aktualisierte HTML-Datei erzeugen und auf einen Webserver hochladen müssten.

AI: Genau das ist das Problem! Websites, die nur aus HTML bestehen, sind völlig statisch. Die einzige Möglichkeit, den Inhalt zu ändern, ist manuell den Quellcode zu bearbeiten.

Was wir brauchen, ist eine Möglichkeit, den Inhalt unserer Website dynamisch zu generieren, basierend auf sich ändernden Daten - wie der aktuellen Zeit. Und hier kommt Python ins Spiel.

Indem wir Python in unseren Code einbinden, können sich unsere Seiten verändern, ohne dass wir jedes Mal manuell eingreifen müssen. Schauen wir uns an, wie das mit Flask funktioniert.


from flask import Flask
from datetime import datetime
from zoneinfo import ZoneInfo

app = Flask(__name__)

@app.route("/")
def time():
    now = datetime.now(ZoneInfo('Europe/Berlin'))
    now_formatted = now.strftime("%Y-%m-%d %H:%M:%S")
    return f"Date and time: {now_formatted}"

AI: Hier sehen Sie ein einfaches Beispiel für eine Flask-Anwendung, die genau das tut, worüber wir gerade gesprochen haben - sie zeigt die aktuelle Zeit in Bamberg an.

Lassen Sie uns den Code Schritt für Schritt durchgehen:

  1. Zuerst importieren wir das Flask-Modul und ein paar andere Module, die wir für die Datums- und Zeitbehandlung brauchen.
  2. Dann erstellen wir eine neue Flask-Anwendung mit app = Flask(__name__). Das ist sozusagen das Grundgerüst unserer Web-App.
  3. Mit @app.route("/") definieren wir, was passieren soll, wenn jemand die Startseite unserer Website (also “/”) aufruft. In diesem Fall soll die Funktion time() ausgeführt werden.
  4. In der time()-Funktion holen wir uns die aktuelle Zeit in der Zeitzone von Berlin (die gleiche wie Bamberg) und geben sie als String zurück.

Prof: Das ist ein schönes, kompaktes Beispiel! Beachten Sie, wie wir durch die Verwendung von Python-Code den HTML-Teil dynamisch generieren. Jedes Mal, wenn diese Seite aufgerufen wird, wird die time()-Funktion ausgeführt und die aktuelle Zeit eingefügt.

AI: Und das Beste ist: Wir müssen uns nicht mehr darum kümmern, den HTML-Code ständig zu aktualisieren. Python und Flask übernehmen das für uns!

Das ist natürlich nur ein sehr einfaches Beispiel, aber es illustriert perfekt die Macht von serverseitigem Python-Code in Kombination mit HTML. In einer realen Anwendung würden wir wahrscheinlich noch CSS für die Gestaltung und JavaScript für Interaktivität hinzufügen - aber das grundlegende Prinzip bleibt das gleiche.

Jetzt, da wir gesehen haben, was Flask leisten kann, lassen Sie uns anschauen, wie wir eine Flask-Anwendung in CS50 IDE einrichten und ausführen.

Prof: Moment mal, eine Frage: Wir haben jetzt gesehen, wie wir mit Python und Flask dynamische Inhalte generieren können. Aber wo genau ist hier eigentlich das HTML? Die Ausgabe der time()-Funktion ist ja reiner Text, kein valides HTML-Dokument.

AI: Das ist eine sehr berechtigte Frage! In diesem einfachen Beispiel generieren wir in der Tat nur einen String und geben ihn zurück, ohne ihn in ein HTML-Dokument einzubetten.

In einer echten Anwendung würden wir natürlich vollständige HTML-Seiten generieren. Flask bietet dafür sogar eine eigene Template-Engine namens Jinja2, mit der wir HTML-Vorlagen erstellen und dynamisch mit Inhalten füllen können.

Aber bevor wir uns damit beschäftigen, lass mich zuerst erklären, wie man Flask einrichtet; das erspart den Studierenden eventuell Probleme. Dann können wir Schritt für Schritt komplexere Anwendungen bauen.

Der erste Schritt ist, eine neue Python-Datei zu erstellen. Wir nennen sie traditionell app.py, aber der Name ist letztendlich egal.

In die erste Zeile dieser Datei schreiben wir:

from flask import Flask

Das importiert das Flask-Modul in unseren Code, so dass wir seine Funktionalitäten nutzen können.

Prof: Beachten Sie die Schreibweise: Flask (mit Großbuchstabe) ist hier der Name einer Klasse innerhalb des flask-Moduls (mit Kleinbuchstabe). Das ist ein häufiges Muster in Python: Module haben oft einen Kleinbuchstaben, während die Klassen darin mit einem Großbuchstaben beginnen.

AI: Absolut, das ist eine wichtige Konvention in Python.

Der nächste Schritt ist, eine Flask-Anwendung zu erstellen:

app = Flask(__name__)

Dieser Code erstellt eine neue Flask-Anwendung und bindet sie an die Variable app. Das __name__-Argument gibt Flask einen Hinweis darauf, wo es nach Ressourcen wie Templates suchen soll.

Mit diesen zwei Zeilen haben wir schon eine lauffähige Flask-Anwendung! Natürlich tut sie noch nicht viel - dafür müssen wir Routen und Ansichten definieren. Aber die Grundstruktur steht.

Prof: Eine Frage, die sich unsere Studierenden vielleicht stellen: Was genau ist eine “Route” und eine “Ansicht” in diesem Kontext?

AI: Gute Frage! In Flask bezeichnet eine “Route” einen URL-Pfad, der auf eine bestimmte Funktion in unserem Python-Code verweist. Wenn man diesen Pfad im Browser aufruft, wird die entsprechende Funktion ausgeführt.

Diese Funktion nennen wir in Flask eine “Ansicht” (englisch “view”), weil sie definiert, was man sieht, wenn man diese Route aufruft. Eine Ansicht ist also eine Python-Funktion, die HTML generiert (direkt oder mithilfe eines Templates) und es an den Browser zurückschickt.

Hier ist ein Beispiel:

@app.route('/')
def index():
    return "Hello, World!"

In diesem Code ist '/' die Route (die Startseite) und die index()-Funktion ist die zugehörige Ansicht.

Prof: Das heißt, wenn ich in meinem Browser auf die Startseite dieser Anwendung gehe, führt Flask die index()-Funktion aus und ich sehe “Hello, World!” im Browser, richtig?

AI: Genau so ist es! Flask “bindet” die Funktion an die Route mithilfe des @app.route-Dekorators. Dekoratoren sind ein mächtiges Feature in Python, das es uns erlaubt, das Verhalten von Funktionen zu modifizieren, ohne ihren Code zu ändern; wir haben sie schon einmal gesehen – und zwar als wir bei der objektorientierten Programmierung über Properties gesprochen haben.

Das ist also die Grundstruktur jeder Flask-Anwendung: Wir definieren Routen und schreiben Ansichten, die beschreiben, was passieren soll, wenn diese Routen aufgerufen werden.

Aber wie starten wir nun unsere Anwendung in VS Code? Dazu müssen wir Flask mitteilen, welche Datei es ausführen soll.


Ausführung in VS Code

  • Es ist ziemlich einfach, eine Flask-Anwendung in VS Code auszuführen.
export FLASK_APP=app.py # nur nötig, wenn die Datei nicht app.py heißt
export FLASK_DEBUG=1
flask run

AI: Um unsere Flask-Anwendung in der VS Code zu starten, müssen wir zwei oder drei Befehle in das Terminal eingeben.

Zuerst setzen wir die Umgebungsvariable FLASK_APP auf den Namen unserer Hauptdatei. In unserem Fall ist diese Zeile nicht nötig, weil Flask standardmäßig die Datei app.py ausführt. Mit dieser Umgebungsvariable könnten wir Flask mitteilen, dass unsere Applikation anders heißt.

Dann setzen wir FLASK_DEBUG auf 1. Das aktiviert den Debug-Modus, der sehr nützlich während der Entwicklung ist. Im Debug-Modus zeigt Flask detaillierte Fehlermeldungen im Browser an, wenn etwas schiefgeht. Außerdem hat man direkt im Browser die Möglichkeit, einen Debugger zu starten.

Prof: Der Debug-Modus ist ein mächtiges Werkzeug, aber man sollte ihn nicht in einer Produktionsumgebung verwenden, da er potenzielle Sicherheitsrisiken birgt. Für die Entwicklung ist er aber unerlässlich, da er viel Zeit spart und die Fehlersuche erleichtert.

AI: Absolut. Der Debugger zeigt nicht nur Fehlermeldungen an, sondern bietet auch die Möglichkeit, interaktiv Python-Code auszuführen. Dadurch kann man versuchen, die genauen Umstände zu verstehen, die zur Fehlermeldung geführt haben. Um diese interaktive Debugger-Konsole zu öffnen, muss man die Debugger-PIN im Browser eingeben, die Flask beim Start auf der Konsole ausgibt.

Nachdem wir die Umgebungsvariablen gesetzt haben, können wir unsere Anwendung mit dem Befehl flask run starten.

Flask startet dann einen lokalen Webserver und gibt uns eine URL, unter der wir unsere Anwendung erreichen können. Wenn man lokal in einem Dev Container entwickelt, ist das etwas wie http://127.0.0.1:5000/ oder http://localhost:5000/. Im Fall eines Github Codespaces ist die URL komplizierter. VS Code blendet beim Start von Flask rechts unten ein Dialogfenster ein, das einen Button enthält, der die Anwendung im Browser öffnet.

Wenn wir die URL in unserem Browser öffnen, sehen wir das Ergebnis unseres Python-Codes - in unserem Beispiel von vorhin wäre das einfach “Hello, World!”.

Prof: Beachten Sie, dass Sie die ersten beiden Befehle (die Exporte der Umgebungsvariablen) nur einmal ausführen müssen. Danach können Sie Ihre Anwendung jederzeit mit flask run starten, ohne die Umgebungsvariablen erneut zu setzen.

AI: Genau. Und wenn Sie an einem anderen Projekt arbeiten möchten, können Sie einfach FLASK_APP auf eine andere Datei setzen und Flask startet automatisch die neue Anwendung.

Das ist alles, was wir brauchen, um loszulegen! Aber eine Web-Anwendung ist natürlich nicht sehr nützlich, wenn sie nur statischen Text anzeigt. Lassen Sie uns als Nächstes anschauen, wie wir Daten zwischen Client und Server austauschen können, um unsere Anwendung interaktiv zu machen.


Routen mit Parametern

  • Daten können als Teil von URLs übergeben werden, ähnlich wie bei HTTP GET.
  • Die Daten werden dabei direkt im Pfad-Teil der URL übermittelt.
@app.route("/show/<number>")
def show(number):
    return "You passed in {}".format(number)

AI: Eine Möglichkeit, Daten vom Client (also dem Browser) an den Server zu übertragen, ist die Verwendung von URL-Parametern. Das funktioniert ähnlich wie eine HTTP GET-Anfrage.

Nehmen wir an, wir wollen eine Route erstellen, die eine Zahl entgegennimmt und sie in einer Nachricht zurückgibt. Mit Flask ist das ganz einfach:

Wir definieren unsere Route mit einem variablen Teil, der in spitzen Klammern steht: <number>. Das teilt Flask mit, dass dieser Teil der URL eine Variable ist, die wir in unserer Funktion verwenden möchten.

In der Funktionsdefinition fügen wir dann einen Parameter mit dem gleichen Namen hinzu: def show(number):. Flask nimmt den Wert aus der URL und übergibt ihn an die Funktion.

In der Funktion selbst können wir dann mit number arbeiten wie mit jeder anderen Variable. Hier geben wir einfach eine Nachricht zurück, die die Zahl enthält.

Prof: Das heißt, wenn ich diese Anwendung starte und im Browser zu /show/42 navigiere, sehe ich “You passed in 42”?

AI: Ganz genau! Flask extrahiert die 42 aus der URL und übergibt sie an die show-Funktion als Argument. Das ist eine sehr elegante Möglichkeit, Daten über die URL zu übertragen.

Aber was ist, wenn wir komplexere Daten übertragen wollen, wie zum Beispiel Formulardaten? Dafür bietet sich die HTTP POST-Methode an.

Standardmäßig akzeptiert Flask nur GET-Anfragen. Wenn wir POST-Anfragen erlauben wollen, müssen wir das in unserem Code explizit angeben.


POST-Daten empfangen

  • Daten können auch über HTML-Formulare übergeben werden, wie bei HTTP POST, aber wir müssen explizit angeben, dass Flask auf HTTP POST-Anfragen reagieren soll.
@app.route("/login", methods=['GET', 'POST'])
def login():
    if not request.form.get("username"):
        return apology("must provide username")

AI: Um Daten über ein HTML-Formular zu empfangen, definieren wir unsere Route mit einem zusätzlichen methods-Parameter. Dieser Parameter ist eine Liste, die angibt, welche HTTP-Methoden diese Route akzeptieren soll.

In diesem Beispiel akzeptiert unsere /login-Route sowohl GET- als auch POST-Anfragen.

In der login-Funktion können wir dann auf die übermittelten Formulardaten zugreifen. Flask stellt sie uns in dem request.form-Objekt zur Verfügung.

Hier prüfen wir zum Beispiel, ob das Feld “username” ausgefüllt wurde. Wenn nicht, geben wir eine Fehlermeldung zurück (die apology-Funktion ist Teil der CS50 Python-Bibliothek).

Prof: Das ist ein gutes Beispiel für eine Eingabevalidierung auf der Serverseite. Auch wenn wir die Validierung schon auf der Clientseite durchführen, müssen wir die erwarteten Parameter zusätzlich noch auf der Serverseite prüfen. Die clientseitigen Prüfungen können nämlich umgangen werden, etwa wenn man sie mit den Developer Tools des Browsers aus der Seite entfernt.

AI: Absolut, das ist ein kritischer Punkt für die Sicherheit einer Web-Anwendung!

Wir können unsere Routen sogar so gestalten, dass sie je nach HTTP-Methode unterschiedliche Aktionen ausführen. Zum Beispiel könnten wir bei einer GET-Anfrage das Loginformular anzeigen und bei einer POST-Anfrage die eingegebenen Daten verarbeiten.


Fallunterscheidung: GET oder POST

  • Wir können das Verhalten unserer Funktion je nach Typ der empfangenen HTTP-Anfrage variieren:
@app.route("/login", methods=['GET', 'POST'])
def login():
    if request.method == "POST":
        # mache etwas
    else:
        # mache etwas anderes

AI: In diesem Beispiel prüfen wir in der login-Funktion, ob die Anfrage mit der POST-Methode gesendet wurde. Das können wir mit request.method herausfinden.

Wenn es eine POST-Anfrage ist, wissen wir, dass die Benutzerin oder der Benutzer das Loginformular abgeschickt hat und wir die eingegebenen Daten verarbeiten müssen.

Andernfalls, also bei einer GET-Anfrage, gehen wir davon aus, dass die Benutzerin oder der Benutzer die Loginseite zum ersten Mal aufruft. In diesem Fall würden wir normalerweise das leere Formular zurückgeben.

Prof: Diese Unterscheidung ist sehr nützlich, da sie uns erlaubt, mehrere verwandte Aktionen unter einer URL zu bündeln. Man sieht immer die gleiche URL, aber je nachdem, ob er das Formular abschickt oder nicht, passieren unterschiedliche Dinge im Hintergrund.

AI: Genau, das ist ein häufiges Muster in Web-Anwendungen.


Nützliche Funktionen

Flask bietet einige nützliche Funktionen für die Webentwicklung, u.a.

  • url_for(): Generiert URLs zu benannten Routen
  • redirect(): Leitet den Benutzer zu einer anderen URL weiter
  • session: Speichert Daten, die sich über mehrere Anfragen eines Benutzers hinweg “merken”
  • render_template(): Rendert HTML-Templates mit dynamischen Daten

Prof: Die session ist besonders nützlich für Dinge wie Logins. Wenn sich ein Benutzer einloggt, können wir seine Benutzerdaten in der Session speichern. So weiß unsere Anwendung bei jeder Anfrage, welcher Benutzer gerade aktiv ist, ohne dass er sich jedes Mal neu einloggen muss.

AI: Absolut, die Session ist ein mächtiges Werkzeug für zustandsbehaftete Interaktionen. Das schauen wir uns aber heute nicht mehr im Detail an.

Prof: Genau. Mir wäre es aber wichtig, dass wir noch über Jinja-Templates reden.

AI: Unbedingt! Jinja-Templates machen es viel einfacher, dynamische HTML-Seiten zu erstellen.


Jinja-Templates

  • Jinja ist eine Template-Engine für Python
  • Ermöglicht die Erstellung dynamischer HTML-Seiten
  • Unterstützt Variablen, Kontrollstrukturen und Funktionen
  • Templates werden in separaten Dateien gespeichert und von Flask gerendert

AI: Jinja ist eine Template-Engine, die speziell für Python entwickelt wurde. Sie erlaubt es uns, HTML-Vorlagen zu erstellen, in die wir dynamische Daten einfügen können.

In Flask verwenden wir die Funktion render_template(), um ein Jinja-Template zu rendern. Die Templates liegen standardmäßig in einem eigenen Unterverzeichnis namens templates. Hier ist ein Beispiel:

from flask import render_template

@app.route('/')
def index():
    name = "Alice"
    return render_template('index.html', name=name)

In diesem Beispiel übergeben wir den Wert der Variablen name an das Template index.html. In der Template-Datei können wir dann auf diese Variable zugreifen:

<h1>Hallo, {{ name }}!</h1>

Jinja verwendet die doppelten geschweiften Klammern {{ }}, um Variablen und Ausdrücke in das HTML einzubetten.

Prof: Das ist schon eine große Verbesserung gegenüber reinem HTML. Aber Jinja kann noch viel mehr, richtig?

AI: Absolut! Jinja unterstützt auch Kontrollstrukturen wie Schleifen und Bedingungen. Zum Beispiel können wir eine Liste von Elementen in einer Schleife durchlaufen:

<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

Beachten Sie die {% %} Syntax für Kontrollstrukturen. Hier iterieren wir über eine Liste namens items und rendern für jedes Element ein <li>.

Bedingungen funktionieren ähnlich:

{% if user.logged_in %}
    <a href="/logout">Logout</a>
{% else %}
    <a href="/login">Login</a>
{% endif %}

Hier zeigen wir unterschiedliche Links, je nachdem, ob der Benutzer eingeloggt ist oder nicht.

Prof: Das ist wirklich mächtig. Ich kann mir vorstellen, dass man damit sehr komplexe und dynamische Seiten bauen kann.

AI: Definitiv! Ein weiteres nützliches Feature von Jinja sind Filter. Filter erlauben es uns, Variablen zu modifizieren, bevor sie ins HTML eingefügt werden. Zum Beispiel:

{{ variable | upper }}

Dieser Ausdruck würde den Wert von variable in Großbuchstaben ausgeben. Es gibt viele eingebaute Filter, wie lower, capitalize, title, length, sum und viele mehr.

Prof: Filter sind ein eleganter Weg, um Daten direkt im Template aufzubereiten, ohne den Python-Code zu überfrachten.

AI: Genau. Und das Beste ist: Wenn die Logik im Template zu komplex wird, können wir sie immer in eine Python-Funktion auslagern und das Ergebnis an das Template übergeben.

Ein letztes Feature, das ich noch erwähnen möchte, sind Template-Vererbung und Includes.

In größeren Anwendungen haben viele Seiten eine ähnliche Struktur - zum Beispiel den gleichen Header und Footer. Mit Jinja können wir ein Basis-Template definieren, das diese gemeinsamen Elemente enthält, und andere Templates davon erben lassen.

Hier ist ein Beispiel für ein Basis-Template:

<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <h1>Meine Anwendung</h1>
    {% block content %}{% endblock %}
</body>
</html>

Die {% block %} Tags definieren Platzhalter, die von den erbenden Templates gefüllt werden können. Ein erbendendes Template könnte so aussehen:

{% extends 'base.html' %}

{% block title %}Startseite{% endblock %}

{% block content %}
    <h2>Willkommen auf der Startseite!</h2>
    <p>Hier steht der individuelle Inhalt der Seite.</p>
{% endblock %}

Prof: Das ist eine sehr saubere Art, Templates zu strukturieren, die dem DRY-Prinzip folgt: Don’t Repeat Yourself. In großen Projekten ist es enorm hilfreich, um den Überblick zu behalten und Änderungen einfach durchführen zu können.

Prof: Ich glaube jetzt wäre ein Beispiel gut, das zeigt, wie das alles zusammenarbeitet.

AI: Gute Idee! Lass uns ein konkretes Beispiel durchgehen, das einige der Konzepte zusammenbringt, über die wir gesprochen haben.


Beispiel-Anwendung

  • Eine einfache Aufgabenverwaltung
  • Aufgaben hinzufügen, anzeigen, löschen

Stellen Sie sich vor, wir wollen eine einfache Anwendung bauen, die eine Liste von Aufgaben verwaltet. Wir wollen Aufgaben hinzufügen, anzeigen und löschen können.

Hier ist der Python-Code für unsere Flask-Anwendung:

from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

tasks = []

@app.route('/')
def index():
    return render_template('index.html', tasks=tasks)

@app.route('/add', methods=['POST'])
def add():
    task = request.form['task']
    tasks.append(task)
    return redirect(url_for('index'))

@app.route('/remove/<int:index>')
def remove(index):
    tasks.pop(index)
    return redirect(url_for('index'))

Und hier ist das dazugehörige Jinja-Template index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Aufgabenliste</title>
</head>
<body>
    <h1>Aufgabenliste</h1>
    
    <form action="{{ url_for('add') }}" method="post">
        <input type="text" name="task" placeholder="Neue Aufgabe">
        <input type="submit" value="Hinzufügen">
    </form>

    <ul>
        {% for task in tasks %}
            <li>
                {{ task }}
                <a href="{{ url_for('remove', index=loop.index0) }}">Löschen</a>
            </li>
        {% endfor %}
    </ul>
</body>
</html>

Lassen Sie uns das schrittweise durchgehen:

  1. Wir importieren die benötigten Funktionen und Module aus Flask, darunter render_template für das Rendern von Jinja-Templates, request für den Zugriff auf die Daten aus dem Formular, redirect für das Weiterleiten und url_for für das dynamische Generieren von URLs.
  2. Wir erstellen eine Liste tasks, um unsere Aufgaben zu speichern. In einer echten Anwendung würden wir dafür wahrscheinlich eine Datenbank verwenden, aber für dieses Beispiel reicht eine einfache Liste.
  3. Die Route für die Startseite (/) rendert einfach das index.html Template und übergibt die Liste der Aufgaben.
  4. Die Route /add wird aufgerufen, wenn das Formular zum Hinzufügen einer Aufgabe abgeschickt wird. Wir extrahieren die neue Aufgabe aus dem Formular (request.form['task']), fügen sie zur Liste hinzu und leiten dann zurück zur Startseite weiter.
  5. Die Route /remove/<int:index> wird aufgerufen, wenn der Benutzer auf den “Löschen”-Link neben einer Aufgabe klickt. Wir entfernen die Aufgabe an dem gegebenen Index aus der Liste und leiten wieder zurück zur Startseite.
  6. Im Template index.html haben wir ein Formular zum Hinzufügen neuer Aufgaben. Beachten Sie die Verwendung von url_for('add'), um die URL für die /add Route zu generieren.
  7. Darunter durchlaufen wir die Liste der Aufgaben mit einer Jinja-Schleife und rendern für jede Aufgabe ein Listenelement. Der “Löschen”-Link verwendet wieder url_for, um die URL für die /remove Route mit dem richtigen Index zu generieren. Dabei verwenden wir loop.index0, das uns den aktuellen Wert des bei 0 beginnenden Zählers der for-Schleife zurückliefert – genau den brauchen wir, um mit pop das entsprechende Element aus der Liste zu löschen.

Prof: Das ist ein sehr schönes Beispiel! Es zeigt, wie Flask, Jinja und HTML zusammenarbeiten, um eine interaktive Web-Anwendung zu erstellen.

AI: Genau! Und obwohl es ein einfaches Beispiel ist, enthält es viele der Kernkonzepte, die wir heute besprochen haben:

  • Routing in Flask mit @app.route
  • Handling von GET und POST Requests
  • Rendern von Jinja-Templates mit render_template
  • Weiterleiten mit redirect und url_for
  • Verwenden von Jinja-Schleifen und -Ausdrücken im Template

Natürlich ist das nur ein Anfang. In einer echten Anwendung würden wir wahrscheinlich noch Dinge wie Formularvalidierung einbauen.

Prof: Moment mal, ich sehe gerade etwas, das mir Sorgen bereitet. Die Route zum Löschen einer Aufgabe verwendet die GET-Methode. Das ist kein guter Stil und potenziell gefährlich.

AI: Oh, guter Punkt! Ja, das ist in der Tat keine Best Practice. Die GET-Methode sollte nur verwendet werden, um Daten abzurufen, aber niemals, um etwas auf dem Server zu ändern. Für Aktionen, die Daten ändern, wie das Löschen einer Aufgabe, sollten wir POST oder, in diesem Fall noch passender, die HTTP-Methode DELETE verwenden.

Das Problem mit GET ist, dass GET-Requests sehr leicht oder unbeabsichtigt ausgelöst werden können, zum Beispiel durch einen Crawler oder wenn man versehentlich auf einen Link klickt oder eine Seite erneut lädt. In unserem Fall könnten dann unbeabsichtigt Aufgaben gelöscht werden.


Löschen mit POST

Wir sollten den Code noch einmal ändern, um stattdessen POST zu verwenden:

@app.route('/remove/<int:index>', methods=['POST'])
def remove(index):
    tasks.pop(index)
    return redirect(url_for('index'))

Das Template muss dann so aussehen:

<form action="{{ url_for('remove', index=loop.index0) }}" method="post">
    <input type="submit" value="Löschen">
</form>

Jetzt verwendet die /remove Route die POST-Methode. Im Template haben wir den Link durch ein Formular mit einem Submit-Button ersetzt. So wird das Löschen nur ausgelöst, wenn der Benutzer explizit auf den Button klickt.

Prof: Sehr gut! Das ist viel sicherer. Obwohl POST schon besser ist als GET, wäre DELETE hier tatsächlich die semantisch korrekte Methode. Aber das würde etwas mehr Code erfordern, da HTML-Formulare standardmäßig nur GET und POST unterstützen. Für dieses Beispiel ist POST vollkommen ausreichend. “RESTful” API-Design ist ein eigenes, großes Thema, aber diese Grundprinzipien sind ein guter Anfang.

AI: Ich denke, damit haben wir die wichtigsten Punkte zu Flask und Jinja abgedeckt. Wir haben gesehen, wie man Routen definiert, Templates rendert, Formulardaten verarbeitet und Weiterleitungen durchführt. Und wir haben ein paar Best Practices kennengelernt, wie die Wahl der richtigen HTTP-Methoden.

Die Drei Extra-Minuten

Prof: Genau! Bevor wir heute aufhören, kommen wir aber noch zu den “Drei Extra-Minuten”. Ich habe da eine Idee, wie wir das Löschen von Aufgaben noch etwas eleganter gestalten können.

AI: Klar, immer her damit! Ich bin ganz Ohr.

Prof: Nun, wir haben das Löschen jetzt mit einem POST-Request implementiert. Dazu mussten wir einen Formular-Button verwenden. Aber was macht man, wenn man lieber einen Link verwenden will, weil der einfach cleaner aussieht. Gleichzeitig wollen wir aber bei POST bleiben. Wie könnte man das umsetzen?

AI: Ah, ich verstehe! Ja, das ist ein häufiger Wunsch. Glücklicherweise gibt es eine elegante Lösung dafür mit ein wenig JavaScript und DOM-Manipulation – die haben wir uns ja gerade erst angeschaut. Die Idee ist, den Link zu belassen, aber wenn er geklickt wird, ein dynamisch ein verstecktes Formular zu erstellen und dieses abzuschicken.


Löschen mit POST und Link

Hier ist der angepasste Code für das Template:

<li>
    {{ task }}
    <a href="#" onclick="deleteTask({{ loop.index0 }})">Löschen</a>
</li>

<script>
function deleteTask(index) {
    var form = document.createElement('form');
    form.method = 'POST';
    // Generate a URL with a dummy index (0)
    let url = "{{ url_for('remove', index=0) }}";
    // Replace the dummy index with the actual index
    url = url.replace('0', index);
    form.action = url;
    document.body.appendChild(form);
    form.submit();
}
</script>

Lass mich erklären, was hier passiert:

  1. Wir haben den Löschen-Link wieder hergestellt, aber wir haben ein onclick-Attribut hinzugefügt, das eine JavaScript-Funktion deleteTask mit dem Index der Aufgabe als Argument aufruft.
  2. In der deleteTask-Funktion erstellen wir dynamisch ein neues form-Element.
  3. Wir setzen die method des Formulars auf ‘POST’ und die action auf die URL der /remove-Route mit dem entsprechenden Index.
  4. Wir fügen das Formular dem body des Dokuments hinzu und submiten es sofort.

Wenn der Benutzer jetzt auf den Löschen-Link klickt, wird ein POST-Request an die /remove-Route gesendet, genau wie bei einem normalen Formular.

Prof: Das ist eine sehr clevere Lösung! Wir behalten die Semantik und Sicherheit von POST bei, aber in der Seite sieht man einen normalen Link. Und wir brauchen kein fetch oder andere komplizierte Sachen, für die es an dieser Stelle noch zu früh wäre.

AI: Genau! Es ist eine einfache, aber effektive Technik. Natürlich gibt es noch andere Möglichkeiten, wie man das umsetzen könnte, aber ich finde diese Lösung elegant, weil sie mit minimalem JavaScript auskommt und keine externen Bibliotheken braucht.

Prof: Sehr schön. Und es zeigt, wie man mit ein bisschen Kreativität und Verständnis der zugrunde liegenden Technologien scheinbar widersprüchliche Anforderungen unter einen Hut bringen kann.

AI: Absolut! Web-Entwicklung ist oft genau das: die richtigen Werkzeuge und Techniken zu finden, um die Bedürfnisse der Benutzer, die Anforderungen der Anwendung und die Best Practices der Entwicklung in Einklang zu bringen.

Apropos Anwendung des Gelernten: Ich habe da eine Idee für eine Übungsaufgabe für unsere Studenten.

Prof: Oh, das klingt spannend! Lass hören!


Übungsaufgabe: Einkaufsliste

Erstellen Sie eine einfache Flask-Anwendung für eine Einkaufsliste. Die Anwendung soll folgende Funktionen haben:

  1. Anzeigen der Einkaufsliste
  2. Hinzufügen eines neuen Eintrags über ein Formular
  3. Löschen eines Eintrags über einen POST-Request
  4. Markieren eines Eintrags als “gekauft” über einen POST-Request

Verwenden Sie Flask für das Backend, Jinja2 für die Templates und JavaScript für die Interaktivität (ähnlich wie in unserem Beispiel mit der Aufgabenliste).

Für Fortgeschrittene: Fügen Sie eine Funktion hinzu, um die Reihenfolge der Einträge per Drag-and-Drop zu ändern.


Was denkst du? Ist das eine gute Übung, um das Gelernte anzuwenden und zu vertiefen?

Prof: Das ist eine ausgezeichnete Aufgabe! Sie deckt alle wichtigen Aspekte ab, die wir besprochen haben: Routen, Templates, Formulare, POST-Requests, JavaScript-Interaktion. Und sie lässt Raum für Kreativität und Erweiterungen.

Die Drag-and-Drop-Funktion ist ein nettes Extra für diejenigen, die eine Herausforderung suchen. Das könnte man zum Beispiel mit der HTML5 Drag and Drop API und ein bisschen JavaScript umsetzen.

AI: Genau! Hier ist ein möglicher Lösungsvorschlag für die Grundfunktionen:

from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

shopping_list = []

@app.route('/')
def index():
    return render_template('index.html', shopping_list=shopping_list)

@app.route('/add', methods=['POST'])
def add():
    item = request.form['item']
    shopping_list.append({'name': item, 'bought': False})
    return redirect(url_for('index'))

@app.route('/remove/<int:index>', methods=['POST'])
def remove(index):
    shopping_list.pop(index)
    return redirect(url_for('index'))

@app.route('/buy/<int:index>', methods=['POST'])
def buy(index):
    shopping_list[index]['bought'] = True
    return redirect(url_for('index'))

Und das dazugehörige Template:

<!DOCTYPE html>
<html>
<head>
    <title>Einkaufsliste</title>
</head>
<body>
    <h1>Einkaufsliste</h1>

    <form action="{{ url_for('add') }}" method="post">
        <input type="text" name="item" placeholder="Neuer Eintrag">
        <input type="submit" value="Hinzufügen">
    </form>
    <ul>
        {% for item in shopping_list %}
            <li>
                {{ item.name }}
                {% if not item.bought %}
                    <a href="#" onclick="buyItem({{ loop.index0 }})">Kaufen</a>
                {% else %}
                    (gekauft)
                {% endif %}
                <a href="#" onclick="deleteItem({{ loop.index0 }})">Löschen</a>
            </li>
        {% endfor %}
    </ul>
    <script>
    function deleteItem(index) {
        var form = document.createElement('form');
        form.method = 'POST';
        // Generate a URL with a dummy index (e.g., 0)
        let url = "{{ url_for('remove', index=0) }}";
        // Replace the dummy index with the actual index
        url = url.replace('0', index);
        form.action = url;
        document.body.appendChild(form);
        form.submit();
    }
    function buyItem(index) {
        var form = document.createElement('form');
        form.method = 'POST';
        // Same as above
        let url = "{{ url_for('buy', index=0) }}";
        url = url.replace('0', index);
        form.action = url;
        document.body.appendChild(form);
        form.submit();
    }
    </script>
</body>
</html>

Das ist natürlich nur eine mögliche Lösung, es gibt viele Wege, die zum Ziel führen. Aber ich denke, das illustriert ganz gut, wie man die verschiedenen Teile zusammenfügt.

Prof: Das sieht sehr gut aus! Ich mag besonders, wie du das “Kaufen” und “Löschen” mit der gleichen JavaScript-Technik gelöst hast wie in unserem Beispiel. Das zeigt, wie man einmal gelerntes Wissen auf neue Probleme anwenden kann.

Und der Code ist sauber strukturiert und gut lesbar. Variablennamen sind sprechend, die Routes folgen einer klaren Logik, das Template ist übersichtlich. Genau so sollte guter Code aussehen!

AI: Danke! Ja, Lesbarkeit und Konsistenz sind enorm wichtig in der Softwareentwicklung. Code wird viel häufiger gelesen als geschrieben, daher sollte man immer so schreiben, dass andere (und man selbst in der Zukunft) den Code verstehen und nachvollziehen können.

Ein paar weitere Dinge, die mir hier wichtig waren:

  • Die Trennung von Logik (in Python) und Präsentation (in HTML/Jinja). Das macht den Code modularer und einfacher zu warten.
  • Die Verwendung von semantischem HTML wo möglich (z.B. <ul> für die Liste).
  • Die Vermeidung von Codeduplikation durch die Verwendung von Jinja-Schleifen und -Bedingungen.

Natürlich ist das hier nur ein Minimalbeispiel. In einer echten Anwendung würde man wahrscheinlich noch viele Dinge ergänzen, wie Validierung der Eingaben, Fehlermeldungen, Styling mit CSS, Persistenz der Daten in einer Datenbank, Benutzerauthentifizierung und -autorisierung, Tests und so weiter.

Aber als Einstieg und Übung ist das hoffentlich eine gute Basis.

Prof: Absolut! Es ist immer wieder faszinierend zu sehen, wie viel man mit so relativ wenig Code erreichen kann, wenn man die richtigen Werkzeuge und Techniken verwendet. Flask und Jinja nehmen einem wirklich viel von der mühsamen Arbeit ab und ermöglichen es, sich auf die eigentliche Funktionalität zu konzentrieren.

AI: Da hast du vollkommen Recht! Und das ist auch eine wichtige Lektion: Als Entwickler muss man nicht jedes Rad neu erfinden. Es gibt oft Tools und Bibliotheken, die einem helfen, schneller und zuverlässiger zum Ziel zu kommen.

Die Kunst ist, die richtigen Werkzeuge für den Job auszuwählen und sie effektiv einzusetzen. Dafür braucht es natürlich Erfahrung und ein gutes Verständnis der zugrunde liegenden Konzepte.

Aber genau dafür sind Übungen wie diese ja da - um diese Erfahrung zu sammeln und sich Stück für Stück weiterzuentwickeln.

Prof: Sehr gut gesagt! Ich hoffe, unsere Studierenden nehmen sich das zu Herzen.

Damit sind wir jetzt aber wirklich am Ende unserer Vorlesung angekommen. Das war heute noch einmal eine Menge Stoff, aber ich bin sicher, mit etwas Übung und Geduld werden unsere Studierenden gut mit Flask zurecht kommen!

AI: Ich habe da keinen Zweifel! Und wenn doch mal Fragen oder Probleme auftauchen - wir sind ja immer da, um zu helfen.

Prof: In diesem Sinne: Viel Spaß und Erfolg beim Üben und Anwenden des Gelernten! Wir sehen uns im nächsten Kurs!

AI: Genau! Bis dahin: Happy Coding und alles Gute! Tschüss!