Python und Django
Willemers Informatik-Ecke
Mit Django können dynamische Webseiten unter Python entwickelt werden. Für statische Webseiten genügt ein Apache oder ein anderer Webserver. Er liefert auf die Anfrage von Firefox & Co Texte, Bilder, Musik- und Videodateien.

Für dynamische Seiten wie etwa Shops werden Seiten benötigt, die sich serverseitig dynamisch verändern, beispielsweise anhand von Datenbankinhalten. Solche Systeme der Webprogrammierung können auch in anderen Programmiersprachen umgesetzt werden.

Django stellt ein Framework dar, das Python Web-fähig macht. Dadurch wird das Python-Programm zum Webserver. Es übernimmt die Anfragen des Clients, bearbeitet sie und sendet die Ergebnisse an den Client zurück.

Django per Repository installieren

Unter Linux (Debian, Ubuntu, Linux Mint) installiert man Django mit dem Befehl:
apt-get install python3-django
Damit steht Django allen Benutzern des Systems zur Verfügung. Außerdem wird das Paket python3-venv installiert.
apt install python3-venv

Visual Code

Man kann Django mit Kommandozeilen-Tools bearbeiten. Etwas angeneher ist die Verwendung von Visual Code. Unter Ubuntu Users wird beschrieben, wie man es installiert.

Visual Code wird über den Befehl code gestartet.

code .

Durch den Punkt hinter dem Befehl wird das aktuelle Verzeichnis als Arbeitsverzeichnis gewählt. Will man ein anderes, kann dieses über den Explorer (links oben) ausgewählt werden.

Über den Menüpunkt Terminal|New Terminal kann eine Shell gestartet werden.

Environment für Projekt anlegen

Nun kann ein Environment in einem Verzeichnis erstellt werden. Darin wird sich das Projekt befinden. Es ist also auch der Ort, an dem man mit dem Befehl git init das ein Git-Projekt anlegt.

Das Django-Environment wird angelegt. Dieses enthält neben den Sourcen auch die verwendeten Tools. Aufruf:

python3 -m venv env

python3 ruft explizit die Version 3 von Python, da Python 2 nicht kompatibel ist, aber noch verbreitet ist. Aus diesem Grund steht der Aufruf python nicht auf allen Systemen (insbesondere Linux und Mac) zur Verfügung.

Das Verzeichnis env entsteht. Gern wird als Verzeichnisname auch .env verwendet. Das führt unter Nicht-Windows-Systemen dazu, dass das Environment ohne explizite Nennung verdeckt bleibt.

Aktivieren unter Linux und Mac

Im aktuellen Verzeichnis ist ein Verzeichnis namens env oder .env entstanden. Es muss noch aktiviert werden. Unter Unixoiden (Linux, Mac) rufen Sie auf:
source env/bin/activate
Wer mag, kann von hier aus auch die Entwicklungsumgebung code starten. Als Argument übergibt man den Pfad, in diesem Fall also den Punkt. Natürlich kann man auch direkt auf dem Terminal arbeiten.

Aktivieren auf Windows mit CMD.EXE

Bei Windows steht der entsprechende Skript unter env/Skripts/activate.bat.

Das Skript existiert auch in einer Power-Shell-Version an gleichem Ort mit der Erweiterung ps1. Das Skript activate sorgt dafür, dass die Pfade auf dieses Verzeichnis umgebogen werden und so erreicht wird, dass Django nur im lokalen Umfeld installiert wird.

Aktivieren auf Windows mit Power-Shell

Für die Powershell liegt ein eigener Skript parallel zu activate.bat vor. Die Powershell darf aber nicht in jeder Umgebung Skripten ausführen.

Test der Aktivierung

Nach der Aktivierung des Environment sollte auch der Python-Interpreter aus dem Environment gezogen werden. Linux und Mac können mit dem Befehl which feststellen, welche Instanz bei Aufruf eines Befehls gezogen wird.
$ which python
/home/.../env/bin/python
Windows kennt den Befehl nicht, aber den Befehl where, der wohl ähnlich funktioniert wie der Befehl whereis unter Linux. Er zählt alle Pfade auf, unter denen der Befehl aufrufbar ist. Der erste sollte sich dann im Environment befinden.

Reaktivierung

Die Aktivierung muss jedes Mal neu erfolgen, wenn eine neue Shell gestartet wird.

Installation von Django per PIP

Nun installieren Sie Django. Tatsächlich wird hier ein Großbuchstabe verwendet.
pip install Django
In diesem Verzeichnis kann nun ein Projekt eingerichtet werden.

Projekt mit startproject einrichten

Nach der Installation steht der Befehl django-admin zur Verfügung. Mit diesem erzeugt man ein Projektverzeichnis, in dem die Infrastruktur weitgehend vorbereitet wird.

Mit dem Argument startproject wird ein Projekt erzeugt. Als weiteren Parameter muss der Name des Projekts angegeben werden. Im folgenden Beispiel ist das fewo.

django-admin startproject fewo
Es entsteht ein Verzeichnis namens fewo. Darin liegt wiederum ein Verzeichnis fewo mit einigen Python-Sourcen. Ansonsten eine Datei namens manage.py.

Bei der Verwendung von Visual Studio Code sollten Sie diesen aus diesem Verzeichnis aus starten. So hat er Zugriff auf env. Ansonsten meldet er Import-Fehler auf die django-Bibliotheken.

Nun wechselt man in das Projektverzeichnis und ruft die Datei manage.py über den Python-Interpreter auf:

cd fewo
python3 manage.py runserver
Der Server startet und blockiert anschließend die Konsole. Durch die Tastenkombination [Strg]+[C] kann er beendet und die Konsole wieder freigegeben werden.

Der Server meldet, dass er unter der Portnummer 8000 zu erreichen ist. Um das zu testen, ruft man einen Browser auf und gibt als URL localhost:8000 ein. Es meldet sich die Standard-Django-Seite mit der Gratulation, dass die Installation geklappt hat.

Möglicherweise tritt auch eine Fehlermeldung auf, die besagt, dass es noch "unapplied migrations" gäbe und schlägt auch gleich die Behandlung vor:

python manage.py migrate
Es sind Python-Programme unter fewo/fewo verfügbar:

Apps als Module eines Projekts

Eine App ist ein Modul, das ein Objekt oder datenbanktechnisch eine Tabelle umfasst. Eine App bietet typischerweise eine Auflistung, eine Eingabe, eine Änderung und ein Löschen des Objekts an.

Am Beispiel des Ferienwohnungsprojekt erstellt man für den Gast, für die Wohnung und für die Buchung je eine App.

Für jede App wird später eine Klasse in models.py definiert. Für das Navigieren zwischen den Funktionen wird eine urls.py eingesetzt.

Erzeugen einer App

Ein Projekt kann mehrere Apps enthalten. Die Apps können aber auch in anderen Projekten wiederverwendet werden. Wir legen eine App an. Wir nennen diese gast. Sie soll sich um den Gast kümmern.
python3 manage.py startapp gast
Nun wird ein Verzeichnis gast unter dem Hauptverzeichnis fewo parallel zu dem Unterverzeichnis fewo angelegt. Sowohl das Unterverzeichnis fewo als auch das Verzeichnis gast enthalten nun diverse Python-Dateien. Davon sind besonders views.py und models.py wichtig. Daneben gibt es die Dateien tests.py, apps.py und admin.py.
fewo (Das Projekt)
  +-- fewo
  |    +-- urls.py - Pfade des Projekts
  |
  +-- gast
  |    +-- urls.py - Pfade der App (muss später angelegt werden)
  |    +-- models.py
  |    +-- views.py
  |    :
  |
  +-- manage.py

Infrastruktur der App in urls.py

Zunächst muss die Infrastruktur für den Server angelegt werden. Diese befindet sich in dem Unterverzeichnis, das den Projektnamen trägt, also hier fewo/fewo. Dort wird die Datei urls.py mit einem Editor bearbeitet. Dieses wird bei Aufruf des Servers mit der URL localhost:8000/gast die Anfragen an das Modul gast weiterleiten.

Die Projekt-urls.py

Die Datei urls.py im Verzeichnis fewo/fewo ist bereits angelegt. Dort findet man neben ausführlichen Kommentaren einige Codezeilen:
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]
Hier wird das Modul gast eingetragen. Dazu wird der Pfad gast/ eingetragen. Sobald eine URL mit diesem Pfad auftaucht, soll sich die App gast darum kümmern. Der Name der App wird in app_name angegeben.
from django.urls import path, include

app_name="gast"
urlpatterns = [
    path('admin/', admin.site.urls),
    path('gast/', include('gast.urls')),
]
Der Aufruf include sucht im Pfad gast nach einer weiteren Datei urls.py. Da es die bisher nicht gibt, muss sie im Verzeichnis gast angelegt werden. Ihre Aufgabe ist es, die Anfragen innerhalb der App zu verteilen.

Die App-urls.py

Im Unterverzeichnis gast wird eine eigene Datei namens urls.py angelegt. Als erstes wird der Leerstring definiert. Er soll alle Aufrufe der App erst einmal auf die Funktion index in der Datei views.py weiterleiten.
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
Später werden noch weitere Funktionen hinzukommen.

Verarbeitung von Anfragen

Die urls.py-Dateien leiten die Anfrage typischerweise zu views.py-Dateien. Diese ist durch den Aufruf von python3 manage.py startapp gast bereits erzeugt worden. Die Datei gast/views.py wird bearbeitet und die Funktion index definiert. Sie wird eine HTTP-Anfrage des Browsers bekommen und muss eine Antwort an den Browser erstellen. Dazu gibt sie eine HttpResponse zurück. Die einfachste Variante ist es, dem HttpResponse-Konstruktor einen String als Parameter zu übergeben.
from django.http import HttpResponse
  
def index(request):
    return HttpResponse("<h1>Hallo</h1>")
Zum Test kann nun der Server wieder gestartet werden.
python manage.py runserver
Beim Aufruf von localhost:8000/gast erscheint nun ein Hallo in großen Buchstaben. Falls aber etwas nicht korrekt konfiguriert ist, erhalten Sie auf der Konsole eine Fehlermeldung. Sie können direkt Änderungen an den Dateien vornehmen. Bei Änderungen wird die Konfiguration üblicherweise neu eingelesen.

Der Parameter Request und die Rückgabe Response

Die Funktion index erhält als Parameter die Anfrage des Clients vom Typ HttpRequest. Dieses Objekt enthält alle möglichen Attribute einer HTTP-Anfrage. Die Funktion liefert als Rückgabe HttpResponse.

Die Request

Die Request ist in django.http.HTTPRequest definiert. Sie hat die folgenden Attribute: Durch Auslesen der Request-Attribute kann ermittelt werden, was für eine Anfrage der Client an den Server sendet.

Die Response

Die Antwort wird in django.http.HTTPResponse definiert und muss von einer view-Funktion zurückgegeben werden, wie im Beispiel oben gesehen:
from django.http import HttpResponse

def index(request):
    return HttpResponse("<h1>Hallo</h1>")
Eine Response hat einige Attribute, die später noch genutzt werden.

Dynamische Response

Als Antwort können wir den String auch in einer Variablen definieren und diese an HttpResponse weitergeben.

Um dynamische Antworten zu geben, können in den String lokale Variablen eingebettet werden. Das ist natürlich auch mit den üblichen String-Manipulationen möglich. Eine häufig verwendete Sonderform für Python stellt der f-String dar.

from django.http import HttpResponse

def index(request):
    name = "James Bond"
    text = f"<h1>Hallo {name} </h1>")
    return HttpResponse(text)
Wird vor den String ein f gestellt, kann innerhalb des Strings in geschweiften Klammern eine Variable gesetzt werden, die dann an dieser Stelle ausgewertet wird.

Die Ausgabe von Listen wird immer wieder gebraucht. Hier können die Möglichkeiten von Python im Umgang mit Listen ausgiebig genutzt werden.

from django.http import HttpResponse

def index(request):
    name = "Reisegruppe 007"
    actlist = ["Sean Connery", "Roger Moore", "Timothy Dalton", "Pierce Brosnan"]
    liste = "".join([f"<li>{act}" for act in actlist])
    text =f"""
        <h1> {name} </h1>
        <ul>
        {liste}
        </ul>
    """
    return HttpResponse(text)
Die Pythonlisten können in den Text so integriert werden, dass sie in Kombination mit den UL- und LI-Kommandos von HTML eine hübsche Darstellung der Liste möglich macht.

Damit haben wir hier erreicht, dass wir mit einem Python-Programm die Antwort an den Browser dynamisch verändern können. Dabei können wir alle Möglichkeiten nutzen, die uns Python zur Verfügung stellt.

HTML-Dateien als Templates

Es ist etwas umständlich, auf jede Client-Request größere HTML-Dokumente als String zu senden. Darum wird im ersten Schritt eine Django-App dazu gebracht, eine gewöhnliche HTML-Datei auszuliefern und damit das zu tun, was jeder Web-Server als Antwort auf eine GET-Request tut.

In der Datei settings.py des Projekts wird die Konfiguration der App eingetragen. Dadurch wird auch dort nach dem Templates-Ordner gesucht.
INSTALLED_APPS = [
     'gast.apps.GastConfig',
     'django.contrib.admin', ...
In der apps.py der App ist eine Config-Klasse automatisch definiert.
from django.apps import AppConfig

class GastConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "gast"

Die View (views.py) der App kann nun angepasst werden, um die Datei index.html zu laden.

from django.shortcuts import render

def index(request):
    return render(request, "gast/index.html")
Die Funktion render leitet die Request an die HTML-Datei weiter. Auf diese Weise kann die HTML-Datei Informationen über die Anfrage erhalten.

Vererbung von Templates

Oft sind einige Teile der Webseiten gleich. Anstatt diese jedes Mal neu einzutippen oder mit Copy-Paste zu übernehmen, kann eine Basis-Seite definiert werden, die das Standardverhalten vorgibt. Nennen wir diese Seite base.html.
<html>
    <head>
        <title>Mein Titel</title>
    </head>
    <body>
        {% block header %}
        <h1>Eine einfache Überschrift</h1>
        {% endblock header %}
        {% block main %}
        Hier könnte Ihre Werbung stehen!
        {% endblock main %}       
    </body>
</html>
Nun kann eine andere Webseite wie etwas index.html davon erben. Dazu muss das Verzeichnis mit angegeben werden, das unter templates steht, hier im Beispiel meineapp.
{% extends  'meineapp/base.html' %}
{% block main %}
    <h2>Hier ist index.html</h2>
    {% for menus in data.items %}
        <h2>{{day}}</h2>
        <ul>
        {% for menu in menus %}
          {{ menu.name }} <br>
          {{ menu.art }} - {{ menu.location }} - {{ menu.price }}
          <p>
        {% endfor %}
      </ul>
    {% endfor %}
{% endblock main %}
Da der block header von index.html nicht überschrieben wird, wird "Eine einfache Überschrift" erscheinen so wie auch "Mein Titel" im Titel. Da aber block main überschrieben wurde, wird man den Werbungshinweis in index.html nicht sehen.

Tags im Templates

Für statische Webseiten benötigen wir kein Django. Das könnte jeder gängige Webserver auch allein. Interessant wird es, wenn Daten in den HTML-Code gezaubert werden können. Dazu kann dem render-Aufruf in der Funktion in views.py ein weiterer Parameter übergeben werden, der ein Dictionary der gewünschten Daten enthält.
def index(request):
    gastlist = ["Sean Connery", "Roger Moore", "Timothy Dalton", "Pierce Brosnan"]
    gastcontext = {
        "liste": gastlist,
        "spruch": "Geschüttelt nicht gerührt"
    }
    return render(request, "gast/index.html", gastcontext)
Über dieses Dictionary können beliebige Datenstrukturen eingebettet werden. Als Beispiel haben wir hier unter dem Namen spruch einen String und unter dem Namen liste die Liste von Gästen erstellt.

Diese Daten können innerhalb des HTML-Dokuments zugegriffen werden. Sehr einfach ist der Zugriff auf den String in spruch. Eine Variable kann zugegriffen werden, indem sie mit doppelt geschweiften Klammern umgeben wird.

<html>
    <body>
        <h1>Gästeliste</h1>
        {{spruch}}
        </ul>
    </body>
</html>

Das for-Tag

Um auf die Liste zugreifen zu können, wird eine Schleife benötigt. Hier wird ein Python-for eingesetzt. Um es zu kennzeichnen, wird es mit geschweifter Klammer und Prozentzeichen umgeben. Innerhalb des HTML-Codes gibt es aber keine Einrückung. Darum muss das Ende der Schleife mit {% endfor %} gekennzeichnet werden.

<html>
    <body>
        <h1>Gästeliste</h1>
        {{spruch}}
        <ul>
        {% for gast in liste %}
        <li>{{gast}}</li>
        {% endfor %}
        </ul>
    </body>
</html>
Auf die Variable der for-Schleife kann innerhalb des HTML-Dokuments genauso zugegriffen werden, wie es zuvor schon mit der Variablen spruch funktionierte.

Über die Variable kann mittels eines Punktes auf Elemente zugegriffen werden. Beispiel:

{{ element.name }}

Das if-Tag

Neben dem {% for ... %} gibt es auch ein {% if ... %}. Damit können Bedingungen abgefragt werden. Auch hier muss ein End-Tag {% endif %} definiert werden.
{% if status == s %}
    <h4>Status!</h4>
{% endif %}

Formulare

Der typische Weg, wie Anwender aus einer Webseite Daten an den Server senden kann, erfolgt über HTML-Formulare. Ein HTML-Formular wird durch das Tag <form ...> eingeleitet. Darin sind zwei Attribute wesentlich.

Formular ruft Django per GET

Für die Eingabe in ein Formular erstellen wir unter templates/gast eine neue Datei namens eingabe.html. Sie soll später einfach über die URL localhost:8000/gast/eingabe aufgerufen werden. Dazu müssen aber noch einige Arbeiten erfolgen.

Mit dem folgenden Formular wird eine GET-Anfrage an insert gesendet. Diese Funktion soll später die Verarbeitung der eingegebenen Daten übernehmen.

<form action="insert" method="GET">
    <label>Name: </label> <input type="text" name="name"><br>
    <label>E-Mail</label> <input type="text" name="mail">
    <input type="submit" value="ok">
</form>
In der Datei urls.py der App gast muss nun ein Eintrag für eingabe existieren, damit die URL localhost:8000/gast/eingabe funktioniert. Die action lautet auf insert. Und auch diese muss in der urls.py hinterlegt werden, damit die Funktion gefunden werden kann.
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('eingabe', views.eingabe),
    path('insert', views.insert),
]
Und dann muss es auch eine Funktion eingabe und eine Funktion insert in der views.py der App geben. Die Funktion eingabe wird einfach die HTML-Datei eingabe.html aufrufen darstellen, so wie es bei index schon gemacht wurde. Die Funktion insert soll dann beim Klick auf den Submit-Button aktiv werden. Hier soll erst einmal nur ein Text auftauchen, damit wir wissen, dass sie erreicht wurde.
from django.http import HttpResponse
from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, "gast/index.html")

def eingabe(request):
    return render(request, "gast/eingabe.html")

def insert(request):
    return HttpResponse("Insert")
Nun kann das Eingabeformular über die URL localhost:8000/gast/eingabe aufgerufen werden. Sobald auf den Submit-Button geklickt wird, erscheint das Wort Insert im Browser.

Da die Eingabemaske die Methode GET verwendet, sieht man die Eingaben auch in der URL:

http://localhost:8000/gast/insert?name=Hugo&mail=hm%40m.de

Durch den Zugriff auf die Variable request und deren GET können die eingegebenen Werte auch in der Funktion insert ausgelesen werden.

def insert(request):
    name = request.GET.get('name', '')
    mail  = request.GET.get('mail','')
    return HttpResponse("Name: " + name + " E-Mail: " + mail)
Nun erscheinen die Eingaben auf dem Browser-Fenster.

Formular-POST und das CSRF-Cookie

Wird dasselbe Formular per POST versendet, wird Django melden, dass dies nicht geht, weil das CSRF-Cookie (Cross Site Request Forgery) nicht gesetzt sei.

Der Hintergrund ist, dass der Browser von einem Tab zum anderen springen kann (Cross Site Request) und vielleicht auf diesem bereits angemeldet ist. Auf diese Weise könnte diese Anmeldung benutzt werden, ohne authentifiert zu sein. Um solche Angriffe zu verhindern, verfügt Django über einen CSRF-Mechanismus per Cookie. Dieser kann mit der Tag {% csrf_token %} im FORM eingetragen werden.

<form method=POST ...
    {% csrf_token %}
    <input ....
</form>
Auf der Empfangsseite muss natürlich auf POST statt auf GET zugegriffen werden.
def insert(request):
    name = request.POST.get('name', '')
    mail  = request.POST.get('mail','')
    return HttpResponse("Name: " + name + " E-Mail: " + mail)
Das Ergebnis ist das gleiche wie bei GET. Allerdings werden die Daten nun im BODY übertragen und sind dann bei HTTPS verschlüsselt. Außerdem ist die Größe nicht wie bei GET beschränkt.

Umgang mit Datenbanken

Die Daten einer Web-Anwendung müssen in der Regel gespeichert werden. Datenbanken werden im Projekt in der Datei settings.py definiert. Hier wird definiert, welche Datenbank zum Einsatz kommt, ob beispielsweise PostreSQL oder auch MySQL. Der einfachste Weg führt zu SQLite, das von Python direkt zugegriffen werden kann. Aus dieseem Grund ist hier SQLite auch bereits eingetragen.
# settings.py im Projektordner
# ...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
Hinter ENGINE wird der Datenbanktreiber aufgeführt. Für die anderen Datenbanken müsste hier stehen: Die weiteren Abläufe unterscheiden sich nicht, egal welche Datenbank zum Einsatz kommt.

Definition eines Modells

Im Gegensatz zu vielen anderen Systemen setzt Django nicht auf die Einbettung von SQL-Befehlen, sondern verwendet ORM (Object Relation Mapping), also die Modellierung der Datenbanktabellen durch Objekte im objektorientierten Sinn.

Die Klassen dieser Objekte werden in der Datei models.py der App definiert. Für einen Gast enthält die Klasse an Attributen:

Hinzu kommt eine Methode namens __str__ mit je zwei Unterstrichen vor und nach dem Namen. Sie wird immer dann aufgerufen, wenn ein Objekt der Klasse als Text dargestellt werden soll. Sie sollte also einen String zurückgeben, der aussagekräftig den Inhalt des Objekts darstellt.

Die Klasse wird zum Modell, indem Sie die Basisklasse models.Model erweitert.

# models.py der App
from django.db import models

class Gast(models.Model):
    name    = models.CharField(max_length=100)
    adresse = models.CharField(max_length=100)
    telefon = models.CharField(max_length=50)
    email   = models.EmailField()

    def __str__(self) -> str:
        return f"{self.name}, Adresse {self.adresse}, Tel {self.telefon}, E-Mail {self.email}"
Datenbanken benötigen im Gegensatz zu Python klare Ansagen bezüglich der Typen ihrer Spalten. Darum müssen diese in der Klasse erwähnt werden. Beispiele für gültige Typen: Eine komplette Liste der Feldtypen befindet sich in der Django-Dokumentation:

Datenbankerzeugung: Migration

Um aus dieser Definition ein nutzbares Modell zu erzeugen, wird eine Migration erzeugt.
python manage.py makemigrations gast
Als Antwort auf den Befehl erscheint:
Migrations for 'gast':
  gast/migrations/0001_initial.py
    - Create model Gast
Es entsteht auf der oberen Projektebene die Datei db.sqlite3, sofern Sie SQLite verwenden.

Bevor man mit der Datenbank arbeiten kann, muss sie explizit migriert werden. Das ist auch erforderlich, wenn sich am Modell irgendetwas ändert.

python manage.py migrate

Erzeugen von Objekten in der Datenbank

Das Datenbankmodell kann über eine Shell bedient werden. Dazu muss das Modell der App importiert werden. Im folgenden Beispiel wird ein Objekt des Modells unter dem Namen gast erzeugt und kann mit der Methode save in der Datenbank gespeichert werden.

Ohne dass dies explizit erwähnt wird, wird automatisch eine Spalte id vom Typ integer angelegt, die einen primären Schlüssel darstellt.

python manage.py shell
>>> from gast.models import Gast
>>> gast = Gast(name="Anton Meier", adresse="Quarkmuehle", telefon="089-112", email="anton@m.de")
>>> gast.save()
>>> gast
<Gast: Anton Meier, Adresse Quarkmuehle, Tel 089-112, E-Mail anton@m.de>
>>>
Durch Erzeugung eines neuen Gastobjekts und anschließendem Aufruf der Methode save können nun beliebig viele Gäste in die Datenbank eingetragen werden.

Anfragen

Über den Modellnamen Gast und objects kann auf die Elemente der Datenbank zugegrifen werden. So liefert all() alle Einträge der Datenbanktabelle.
>>> Gast.objects.all()
Das zeigt alle Einträge in der Datenbank an. Werden rechteckige Klammern angehängt, kann auf die Elemente wie bei einer Liste zugegriffen werden.
>>> Gast.objects.all()[0]
Mit der Methode get können Elemente anhand der Daten gesucht werden.
>>> Gast.objects.get(id=1)
>>> Gast.objects.get(adresse="Hamburg")

Filter

Analog zu der WHERE-Klausel in SQL können bei Anfragen in Django Filter eingesetzt werden. Diese werden an objects angehängt.

>>> Gast.objects.filter(adresse__contains="amb")
<QuerySet [<Gast: Anke Evers, Adresse Hamburg, Tel 220, E-Mail a@e.de>]>

Als Parametervariable wird die Klassenvariable, gefolgt von zwei Unterstrichen und dem Filter-Lookup gesetzt. Das Filter-Lookup ist hier contains (enthält): Es kann startswith (beginnt mit), gt (größer), lt (kleiner), in (in Liste enthalten) oder week (Kalenderwoche) lauten.

Die folgende URL verweist auf eine ausführlichere Beschreibung der Lookups:

Änderungen

Änderungen an einem Objekt können durch save() zu einer Änderung in der Datenbank führen, sofern die id nicht geändert wird.
>>> g = Gast.objects.get(adresse="Hamburg")
>>> g.telefon="220"
>>> g.save()

Löschen

Durch Anhängen der Methode delete() an eine Anfrage können alle gefundenen Einträge aus der Datenbank gelöscht werden.

Views für die Datenverwaltung

Für die Verwaltung von Daten werden unterschiedliche Bildschirme für das Erfassen oder die Darstellung benötigt. Diese werden typischerweise durch Funktionen in der Datei views.py in der App realisiert.

index: Liste der Objekte

Die bisher schon verwendete Funktion index wird meist für eine Übersicht verwendet, also einer Liste der Objekte.

Nach wie vor steht in der Datei models.py das Model für einen Gast.

from django.db import models

class Gast(models.Model):
    name    = models.CharField(max_length=100)
    adresse = models.CharField(max_length=100)
    telefon = models.CharField(max_length=50)
    email   = models.EmailField()

    def __str__(self) -> str:
        return f"{self.name}, Adresse {self.adresse}, Tel {self.telefon}, E-Mail {self.email}"
In der Datei views.py ist die Funktion index definiert. Sie holt alle Einträge aus der Datenbank, schiebt sie dem context unter und lässt die Datei index.html anzeigen.
from django.shortcuts import render
from . import models

def index(request):
    gastlist = models.Gast.objects.all()
    gastcontext = {
        "liste": gastlist,
        "spruch": "Geschüttelt nicht gerührt"
    }
    return render(request, "gast/index.html", gastcontext)
Prinzipiell hat sich gegebenüber der bisherigen Funktion index nichts geändert, nur dass die Liste nun nicht in der Funktion statisch angelegt wird, sondern aus der Datenbanktabelle geholt wird.

Die Datei index.html befindet sich immer noch im App-Verzeichnis templates/gast und benötigt keine Änderungen. Sie wertet die übergebene Variable gaeste in einer for-Schleife aus, die daraus die li-Elemente einer ul erzeugt.

<html>
    <body>
        <h1>Gästeliste</h1>
        {{spruch}}
        <ul>
            {% for gast in gaeste %}
            <li> {{gast}} </li>
            {% endfor %}
        </ul>
    </body>
</html>

Detailanzeige eines einzelnen Objekts

Um von der Übersicht auf die Anzeige eines einzelnen Gastes zu kommen, wird jeder li-Eintrag um einen Link erweitert. Er soll die id des jeweils angezeigten Gastes an die Funktion übergeben, die einen einzelnen Gast anzeigt.
<ul>
    {% for gast in gaeste %}
    <li> {{gast}}
         <a href="{% url 'gast:detail' gast.id %}">Details</a></li>
    {% endfor %}
</ul>

Die URL gast:detail in der index.html zeigt auf eine URL, die in der Datei urls.py aufgeführt werden muss. Hier muss auch erwähnt werden, dass die Funktion die id als Parameter benötigt. Diese wurde hinter der URL als gast.id angegeben und ist eine ganze Zahl, also ein int.

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('detail/<int:id>', views.detail, name='detail'),
]
Um die Details eines einzelnen Gastes anzuzeigen, wird in der views.py eine zusätzliche Funktion detail definiert. Diese erhält als weiteren Parameter die id des zu betrachtenden Objekts.
from django.shortcuts import render
from . import models

def index(request):
    gaeste = models.Gast.objects.all()
    context = { 'gaeste': gaeste }
    return render(request, "gast/index.html", context)

def detail(request, id):
    gast = models.Gast.objects.get(id=id)
    context = { 'gast': gast }
    return render(request, "gast/detail.html", context)
Nun muss noch die Template-Seite als detail.html in den Ordner templates/gast erstellt werden. Auf dieser Seite werden die einzelnen Elemente eines Gastes angezeigt. wird ein Link zu der Gästeliste gesetzt.
<html>
    <body>
        <h1>Gast</h1>
        {{gast.name}}<br>
        {{gast.adresse}}<br>
        {{gast.telefon}}<br>
        {{gast.email}}<p>
        <p>
        <a href="{% url 'gast:index' %}">Liste</a>
        </p>
    </body>
</html>
Es ist auch möglich, nur die Variable gast anzugeben. Dann würde der Text ausgegeben, den die Gast-Methode __str__ zurückgibt. <h1>Gast</h1> {{gast}} <a href="{% url 'gast:index' %}">Liste</a>

Anlegen eines Gastes

In der Datei index.html kann ein Link erzeugt werden, der auf eine Maske für die Eingabe eines neuen Gastes verweist. Diese Maske wird dann durch die Funktion eingabe erstellt, die wir oben schon einmal verwendet haben.
<a href="{% url 'gast:eingabe' %}">Neuer Gast</a>

Dazu muss eingabe in der Datei urls.py der App eingetragen werden, wo sich die Funktion für das Anlegen eines Gastes befindet. Da die Funktion über einen Link mit gast:eingabe aufgerufen wird, muss der dritte Parameter name gesetzt sein.

from django.urls import path
from . import views

app_name="gast"
urlpatterns = [
    path('', views.index, name='index'),
    path('detail/<int:id>', views.detail, name='detail'),
    path('eingabe/', views.eingabe, name='eingabe'),
]
Nun kann in der Datei views.py wie gehabt eine Funktion für den Aufruf des Templates erstellt werden, die eingabe heißen soll. Hier ändert sich noch nichts gegenüber der vorigen Funktion eingabe.
def eingabe(request):
    return render(request, "gast/eingabe.html")
Das Template eingabe.html enthält ein Formular, dessen FORM-Tag eine ACTION besitzt, die wiederum auf die Funktion insert verweist, die den Inhalt des Formulars in die Datenbank schafft.
<form action="{% url 'gast:insert' %}" method="POST">
    {% csrf_token %}
    <label>Name: </label> <input type="text" name="name"><br>
    <label>Adr:</label> <input type="text" name="adr"><br>
    <label>Tel.:</label> <input type="text" name="tel" id="tel"><br>
    <label>Mail:</label> <input type="text" name="mail" id="mail"><br>
    <input type="submit" value="ok">
</form>
Auch die Funktion insert muss in der Datei urls.py erwähnt werden.
from django.urls import path
from . import views

app_name="gast"
urlpatterns = [
    path('', views.index, name='index'),
    path('detail/<int:id>', views.detail, name='detail'),
    path('eingabe/', views.eingabe, name='eingabe'),
    path('insert/', views.insert),
]
Sie trifft dann auf die Funktion insert in der Datei views.py. Sie liest wie gehabt die POST-Informationen aus, schafft sie dieses Mal aber in die Datenbank, indem ein gast-Objekt angelegt wird und darüber save aufgerufen wird.
from django.shortcuts import render, redirect, reverse
from . import models
# ...
def insert(request):
    name = request.POST.get("name")
    adr = request.POST.get("adr")
    tel = request.POST.get("tel")
    mail = request.POST.get("mail")
    gast = models.Gast(name=name, adresse=adr, telefon=tel, email=mail)
    gast.save()
    return redirect(reverse("gast:detail", args=[gast.id]))
Nach dem Einfügen wird der Detail-Bildschirm wieder aufgerufen, um den Neuantrag zu zeigen. Dort befindet sich der Link zur Liste in index.html.

Pflichtfelder und Eingabevalidierung

Die Eingaben des Benutzers müssen auf Konsistenz geprüft werden. Pflichtfelder können bereits in HTML5 mit der Option required bzw. required=true bei jedem input-Element festgelegt werden. Versucht der Anwender einen Submit, ohne das Feld auszufüllen, wird er auf sein Versäumnis hingewiesen.
<input type="text" name="adr" id="adr" required>
Anstatt den Browser prüfen zu lassen, kann auch das Backend die Validität prüfen. Zunächst stellen wir fest, ob alle Felder eingegeben wurden. Dann können wir das required aus dem Formular entfernen.
def insert(request):
    name = request.POST.get("name")
    adr = request.POST.get("adr")
    tel = request.POST.get("tel")
    mail = request.POST.get("mail")
    if name and adr and tel and mail:
        # Alle Felder sind gefüllt
        b = models.Gast(name=name, adresse=adr, telefon=tel)
        b.save()
        return redirect(reverse("gast:detail", args=[gast.id]))
    else:
        # Da fehlt ein Feld. Zurück in die Eingabemaske
        return render(request, "gast/eingabe.html",{"error":"Alle Eingabefelder füllen"})
Die Fehlervariable error kann direkt in der Eingabemaske dargestellt werden. Sofern das Feld error besetzt ist, wird es angezeigt.
<html>
    <body>
        <h1>Gast erfassen</h1>
        <form action="{% url 'gast:insert' %}" method="post">
            {% csrf_token %}
            {% if error %}
                <p>Fehler: {{error}}</p>
            {% endif %}
            Name: <input type="text" name="name" id="name"><br>
            Adr.: <input type="text" name="adr" id="adr"><br>
            Tel.: <input type="text" name="tel" id="tel"><br>
            Mail: <input type="text" name="mail" id="mail"><br>
            <input type="submit" value="OK">
        </form>
    </body>
</html>
Nun können auch weitere Prüfungen in der Funktion in views.py erfolgen und diese angezeigt werden.
def insert(request):
    name = request.POST.get("name")
    adr = request.POST.get("adr")
    tel = request.POST.get("tel")
    mail = request.POST.get("mail")
    if name and adr and tel and mail:
        # Alle Felder sind gefüllt
        b = models.Gast(name=name, adresse=adr, telefon=tel)
        b.save()
        return redirect(reverse("gast:detail", args=[gast.id]))
    elif not "@" in mail:
        return render(request, "gast/eingabe.html",{"error":"Mail-Adresse nicht korrekt"})
    elif len(tel) < 5:
        return render(request, "gast/eingabe.html",{"error":"Telefonnummer zu kurz"})
    else:
        # Da fehlt ein Feld. Zurück in die Eingabemaske
        return render(request, "gast/eingabe.html",{"error":"Alle Eingabefelder füllen"))
Um die bisher eingegebenen Werte auch bei einer Fehlermeldung zu erhalten, müssen diese per context wieder zurück an die Maske übertragen werden. Im Fehlerfall wird dann das context-Dictionary um das Feld error erweitert.
def insert(request):
    name = request.POST.get("name")
    adr = request.POST.get("adr")
    tel = request.POST.get("tel")
    mail = request.POST.get("mail")
    context = {'name': name, 'adr': adr, 'tel':tel, 'mail':mail}
    if name and adr and tel and mail:
        # Alle Felder sind gefüllt
        b = models.Gast(name=name, adresse=adr, telefon=tel)
        b.save()
        return redirect(reverse("gast:detail", args=[gast.id]))
    elif not "@" in mail:
        context.update({'error':"Mail-Adresse nicht korrekt"})
        return render(request, "gast/eingabe.html",context)
    elif len(tel) < 5:
        context.update({"error":"Telefonnummer zu kurz"})
        return render(request, "gast/eingabe.html", context)
    else:
        # Da fehlt ein Feld. Zurück in die Eingabemaske
        context.update({"error":"Alle Eingabefelder füllen"})
        return render(request, "gast/eingabe.html", context)
Nun sind die bisherigen Eingaben im context. Damit diese in der Maske erscheinen, muss den input-Feldern die Tag value hinzugefügt werden.
<html>
    <body>
        <h1>Gast erfassen</h1>
        <form action="{% url 'gast:insert' %}" method="post">
            {% csrf_token %}
            {% if error %}
                <p>Fehler: {{error}}</p>
            {% endif %}
            Name: <input type="text" name="name" id="name" value={{name}}><br>
            Adr.: <input type="text" name="adr" id="adr" value={{adr}}><br>
            Tel.: <input type="text" name="tel" id="tel" value={{tel}}><br>
            Mail: <input type="text" name="mail" id="mail" value={{mail}}><br>
            <input type="submit" value="OK">
        </form>
    </body>
</html>

Mehrere Tabellen/Objekte

In den meisten Fällen wird es nicht bei einer Tabelle bleiben. Ein Gast ist aus Sicht eines Vermieters nur interessant, wenn er eine Wohnung bucht. Wir schaffen darum eine weitere Tabelle Buchung. Jede Buchung gehört zu genau einem Gast. Ein Gast darf aber gern mehrfach buchen. Man spricht von einer 1:n-Beziehung.

In der Welt der Datenbanktabellen erreicht man solch eine Beziehung, indem die Buchung einen Eintrag für den eindeutigen Schlüssel des Gastes enthält, der bucht. Man spricht von einem Fremdschlüssel oder foreign key.

class Buchung(models.Model):
    gast = models.ForeignKey(Gast, on_delete=models.DO_NOTHING)
    wohnung = models.CharField(max_length=30)
    von = models.DateField()
    bis = models.DateField()
    preis = models.DecimalField(decimal_places=8, max_digits=11)

    def __str__(self) -> str:
        return f"{self.von} bis {self.bis} Gast {self.gast.name} Wohnung {self.wohnung.bez}"
Die Wohnung wird als reiner Text in der Buchung eingetragen. Für eine einfache Anwendung mag das reichen, aber wenn der Vermieter mehrere Wohnungen hat oder vermittelt, dann sollte für die Wohnung eine eigene Tabelle angelegt werden. Diese kann dann auch die Anschrift und andere Details enthalten.

class Wohnung(models.Model):
    bez = models.CharField(max_length=30)
    adr = models.CharField(max_length=70)
    preis = models.DecimalField(decimal_places=8, max_digits=11)
    preis_hs = models.DecimalField(decimal_places=8, max_digits=11)

    def __str__(self) -> str:
        return self.bez
Neu sind hier die Zahlenwerte. Da es sich um Preise handelt, ist es sinnvoll, diese nicht binär, sondern als dezimale Werte zu verwenden.

Gast und Wohnung werden durch eine Buchung verbunden. Ein Gast kann nicht nur eine Wohnungen buchen. Er kann gern jedes Jahr wiederkommen und wieder eine Wohnung buchen. Und auch eine Wohnung ist nicht nur für einen Gast da, sondern wird im Laufe der Zeit von mehreren Gästen gebucht werden. Man spricht von einer m:n-Beziehung.

class Buchung(models.Model):
    gast = models.ForeignKey(Gast, on_delete=models.DO_NOTHING)
    wohnung = models.ForeignKey(Wohnung, on_delete=models.CASCADE)
    von = models.DateField()
    bis = models.DateField()
    preis = models.DecimalField(decimal_places=8, max_digits=11)

    def __str__(self) -> str:
        return f"{self.von} bis {self.bis} Gast {self.gast.name} Wohnung {self.wohnung.bez}"
Datenbanktechnisch handelt es sich bei Buchung um eine Relation, da sie die Beziehung zwischen den Entities Gast und Wohnung beschreibt. Aber auch eine Relationstabelle darf neben den beiden Fremdschlüsseln weitere Spalten besitzen.

Die Forms von Django

Wenn ein Model für die Datenstruktur erstellt wurde, kann dieses für die Erstellung eines Forms verwendet werden und vereinfacht damit die Erstellung der HTML-Forms. Die Datei models.py mit der Klasse Gast wird hier vorausgesetzt.

Es wird eine neue Datei forms.py in der App angelegt. Diese bindet die Django-Klasse ModelForm und das lokal definierte Modell ein.

from django.forms import ModelForm
from .models import Gast

class GastForm(ModelForm):
    class Meta:
        model = Gast
        fields = ('name', 'adresse','telefon','email')
Nun wird die Funktion eingabe in views.py auf die Forms angepasst. Dazu muss die Form aus forms.py importiert werden. In der Funktion eingabe wird ein form aus der GastForm erstellt und diese als dritter Parameter an render übergeben.
from .forms import GastForm

# ...

def eingabe(request):
    form = GastForm()
    return render(request, "gast/eingabe.html", {'form': form})
Nun kann die HTML-Datei entrümpelt werden. Im templates/gast-Verzeichnis sieht die eingabe.html nun so aus.
<html>
    <body>
        <h1>Gast erfassen</h1>
        <form method="post" action="{% url 'gast:insert' %}" >
            {% csrf_token %}
            {{ form }}
            <button type="submit" class="save btn btn-default">Erfassen</button>
        </form>
    </body>
</html>
Hier erscheinen alle Eingabefelder in einer Zeile. Wenn statt {{ form }} die Variante {{ form.p_as }} verwendet wird, erscheint jedes Eingabeelement in einer eigenen Zeile, also untereinander.

Das Eingabeformular von Wohnung kann ähnlich realisiert werden, da die Tabelle ebenfalls keine Fremdschlüssel enthält.

Form mit Fremdschlüssel

Die Form basiert immer auf einem Model, also sehen wir uns noch einmal das Model in der Datei models.py an:
class Buchung(models.Model):
    gast = models.ForeignKey(Gast, on_delete=models.DO_NOTHING)
    wohnung = models.ForeignKey(Wohnung, on_delete=models.CASCADE)
    von = models.DateField()
    bis = models.DateField()
    preis = models.DecimalField(decimal_places=8, max_digits=11)

    def __str__(self) -> str:
        return f"{self.von} bis {self.bis} Gast {self.gast.name} Wohnung {self.wohnung.bez}"
Darauf wird eine Form namens BuchForm in der Datei forms.py definiert.
from django.forms import ModelForm, DateInput
# ...
class BuchForm(ModelForm):
    class Meta:
        model = Buchung
        fields = ('von', 'bis', 'preis', 'gast', 'wohnung')
        widgets = {
            'von': DateInput(attrs={'class':'form-control', 'type':'date'}),
             'bis': DateInput(attrs={'class':'form-control', 'type':'date'}), 
        }
Für die Fremdschlüssel wird anscheinend also ein komplettes Objekt und nicht nur die ID herangezogen.

Für die Datumsfelder sind die widgets besonders interessant. Ohne diese würde Django für die Datumsfelder ganz normale Texteingaben erstellen. Der Benutzer müsste dort ein zulässiges Datumsformat eingeben, ohne darauf hingewiesen zu werden, wie dieses aussieht.

Das DateInput-Element sorgt dafür, dass das Datumsformat angezeigt und beim Klicken eines Feldes ein Auswahldialog aufgeht, das die Auswahl des Tages aus einem Kalender erlaubt.

Um eine Buchung zu erfassen, wird eine Funktion zum Aufruf der Formular-Webseite (buchform) benötigt und eine weitere Funktion für die spätere Auswertung (buchinsert). Beides sollte gleich in der urls.py vorgemerkt werden.

# urls.py der App
urlpatterns = [
# ...
    path('buchform/', views.buchform, name='buchform'),
    path('buchinsert/', views.buchinsert, name='buchinsert'),
]
Zunächst wird buchform aufgerufen, um das Buchungsformular anzuzeigen.
def buchform(request):
    gast = models.Gast.objects.all()
    wohnung = models.Wohnung.objects.all()
    form = BuchForm()
    context = { 'gast': gast, 'wohnung': wohnung, 'form': form }
    return render(request, "gast/buchform.html", context)
Hier werden also alle Gast- und Wohnungselemente übergeben. Das HTML-Template buchform.html zeigt einfach nur die Form.
<html>
    <body>
        <h1>Buchen</h1>
            <form method="post" action="{% url 'gast:buchinsert' %}" >
                {% csrf_token %}
                {{ form }}
                <button type="submit" class="save btn btn-default">Erfassen</button>
            </form>
    </body>
</html>
Sowohl für gast als auch für wohnung wird in dem Formular eine Klappbox angezeigt, in der ein Element ausgewählt werden kann. Solange man übersichtlich viele Wohnungen und nur wenige Stammgäste hat, ist diese Vorgehensweise durchaus praktikabel.

Die action der form leitet weiter auf die Funktion buchinsert. Sobald der Anwender also das Formular ausgefüllt hat, geht es dort weiter. Diese Funktion liest aus der POST-Liste der Request die einzelnen Felder aus. Dabei wird deutlich, dass für Gast und Wohnung nicht das komplette Objekt, sondern nur die id übertragen wird.

In der Annahme, dass der Anwender beim Ausfüllen des Formulars schön artig war, können aus den ids die Objekte gewonnen werden und mit den anderen Eingaben über den Konstruktor zur Erzeugung einer Buchung genutzt werden. Durch den anschließenden Aufruf der Methode save() wird alles in die Datenbank geschrieben.

def buchinsert(request):
    von = request.POST.get("von")
    bis = request.POST.get("bis")
    preis = request.POST.get("preis")
    gid = request.POST.get("gast")
    gast = models.Gast.objects.get(id=gid)
    wid = request.POST.get("wohnung")
    wohnung = models.Wohnung.objects.get(id=wid)
    buchung = models.Buchung(von=von, bis=bis, preis=preis, gast=gast, wohnung=wohnung)
    buchung.save()
    return redirect(reverse("gast:detail", args=[gast.id]))

Django Admin

Das Admin-Konto von Django ist von vornherein eingerichtet. Es existiert bereits ein urlpattern dafür.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path('buchform/', views.buchform, name='buchform'),
    ...
    path('admin/', admin.site.urls),
]
Um diesen Administrator anzulegen muss createsuperuser gestartet werden.
python manage.py createsuperuser
Nach dem Start des Servers durch python manage.py runserver kann das Administrationskonto über die URL \url{http://localhost:8000/admin} erreicht werden.

Benutzer und Gruppen verwalten

Im Administrationsdialog lassen sich Benutzer und Gruppen eintragen.

Durch den Klick auf +Add hinter Users kann ein Benutzer angelegt werden. Es wird ein Benutzername und ein Passwort benötigt. Dabei prüft Django sehr genau, dass das Passwort dem Benutzer nicht zu ähnlich ist und auch mindestens acht Buchstaben groß ist.

In einem weiteren Dialog können weitere Informationen für den Benutzer definiert werden, insbesondere seine Berechtigung, bestimmte Modelle zu bearbeiten.

Verwaltung der Modelle per Admindialog

Um die Möglichkeiten des Admin-Zugangs zu erweitern, wird die Datei admin.py in der App erweitert. Dort können die eigenen Modelle eingetragen werden. Dazu wird aus der lokalen models.py das Modell Gast eingetragen.
# admin.py
from django.contrib import admin
from .models import Gast
# Register your models here.
admin.site.register(Gast)
Nun kann im Adminbereich auch die App GAST mit ihrem Modell Gast bearbeitet werden. Diese erscheint unterhalb der Benutzerverwaltung. Klickt man auf das Modell, erscheint rechts eine Liste der bisherigen Gäste, die hier sehr einfach bearbeitet, ergänzt oder gelöscht werden können. Die Elemente der Liste wird durch die __str__-Methode dargestellt.


Abbildung: Administrationsbildschirm mit Modell

Die Darstellung der Admin-Modelle kann durch Vererbung erweitert werden. In der folgenden admin.py wird die Buchung durch eine BuchAdmin ersetzt. Die Klasse erweitert admin.ModelAdmin und trägt in fields die Feldnamen und in list_display diejenigen Elemente von fields, die in der Listendarstellung auftreten sollen. Dabei wird der Fremdschlüssel gast wieder durch die __str__-Methode, diesmal von Gast dargestellt.

from django.contrib import admin
from .models import Gast, Buchung, Wohnung
# Register your models here.
admin.site.register(Gast)
admin.site.register(Wohnung)

class BuchAdmin(admin.ModelAdmin):
    fields = ('gast', 'wohnung', 'von', 'bis')
    list_display = ('gast', 'wohnung', 'von', 'bis')

#admin.site.register(Buchung)
admin.site.register(Buchung, BuchAdmin)
Klickt man in der Liste von BuchAdmin auf ein Element, wird dieses in einem Bearbeitungsmodus dargestellt. Das Element gast ist dann eine Klappbox, über die ein anderes Element von Gast ausgewählt werden kann.

Ein weiteres Element von BuchAdmin kann readonly_fields sein. Dieses sollte alle Felder enthalten, die nicht änderbar sein sollen, sondern nur lesbar. Das betrifft beispielsweise Datumsfelder, die automatisch gesetzt werden.

Mit dem Element list_filter kann die Liste durch Setzen eines Filters eingegrenzt werden, mit dem Element search_fields kann in den angeführten Spalten gesucht werden.

Für alle diese Elemente gilt, dass sie ein Tupel oder eine Liste darstellen müssen. Ist nur ein Element vorhanden, muss ein Komma dahinter gesetzt werden.

Authentifizierung

Wenn jemand einen Gast eintragen will, sollte er dazu auch berechtigt sein. Dazu kann ein Login eingefordert werden. Einloggen kann sich natürlich nur jemand, der zuvor als Benutzer angemeldet wurde. Das kann beispielsweise über die Django-Administration erfolgen.

Berechtigungen

Für die verschiedenen Modelle mus die Zugriffsberechtigung geklärt werden. Django ist da großzügig. Jeder darf alles. Unterteilt ist das in add, change, delete und view. Dieser Fähigkeit wird der App-Name mit einem Punkt vorangestellt und hinter einem Unterstrich das Modell angehängt, also gast.change_buchung, um die Buchung in der App ändern zu dürfen.

In einer Anwendung würde zunächst der Benutzer angefordert und dann mit has_perm geprüft, ob die Berechtigung vorliegt.

from django.contrib.auth.models import User, Permission

user = User.objects.get(username = "wirt")
if user.has_perm(gast.add_wohnung'):
In der Anwendung kann mit authenticate geprüft werden, ob ein Benutzer registriert ist.
from django.contrib.auth import authenticate

user = authenticate(username='wirt', password='geheim')
if user is not None:
    user.set_password('ganzgeheim')
    user.save()

User einer Request abfragen

def tudochwas(request):
   if request.user.is_authenticated:
       return HttpResponse("Ach, Du bist's, "+user.username)
   else:
       return HttpResponse("Nur Mitglieder!")
from django.contrib.auth.decorators import login_required

@login_required
def eingabe(request):
Ein Aufrufer wird nun auf die Seite accounts/login umgeleitet. Diese muss erst angelegt werden. Darum muss in der urls.py des Projekts (nicht der App) ein Eintrag dafür angelegt werden.
# ...
from django.contrib.auth import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('gast/', include("gast.urls")),
    path('accounts/login/', views.LoginView.as_view(), name='login'),
]
Das Template für die Login-Seite befindet sich in einer Datei login.html, die unter templates in einem eingabe anzulegendem Verzeichnis registration abgelegt wird.
{% if form.errors %}
<p>Benutzer und Passwort passen nicht.</p>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="Anmelden" />
<input type="hidden" name="next" value="{{ next }}" />
</form>

Zugriff auf APIs von REST-Datenquellen

Für einen Zugriff auf Web-Interface-APIs gibt es die Python-Lib requests. Diese muss explizit installiert werden.
pip install requests
Nun kann über requests ein Zugriff auf eine URL ausgeführt werden.
from django.http import HttpResponse
import requests

def index(request):
    r = requests.get('https://official-joke-api.appspot.com/random_joke')
    if r.status_code == 200:
        js = r.json()
        frage = js.get("setup")
        antwort = js.get("punchline")
        text = "<h2>"+frage+"</h2><h3>"+antwort+"</h3>"
        response = HttpResponse(text)
    else:
        response = HttpResponse(f"Fehler mit Status-Code {response.status_code}")
    return response
Das Beispiel zeigt, wie eine URL per GET geladen wird.

JSON-Struktur

Bei dem Ergebnis handelt es sich um eine JSON-Struktur. Mit dem Befehl curl kann man diese sehen (der besseren Lesbarkeit umbrochen):
$ curl https://official-joke-api.appspot.com/random_joke
{"id":187,"type":"general","setup":"What did the late
tomato say to the early tomato?","punchline":"I’ll ketch up"}
$ 
Hier kann man sehen, dass Fragen hinter der Variablen setup und Antworten hinter punchline stehen.

Links und Literatur