Formulare für die Kategorie anlegen
Bisher haben wir zwei funktionsbasierte Views für das Kategorie-Model
angelegt: categories()
für das Auflisten der Kategorien und
category_detail()
für das Anzeigen der Kategorie-Detailseite.
Wir wollen jetzt in der Lage sein, neue Kategorien einzutragen und bestehende zu editieren. Dazu benötigen wir eine Formularklasse, und jeweils URLs, Templates und Views zum Anlegen und Editieren.
Das Formular
HTML-Formulare zu entwickeln ist eine aufwendige und sorgfältig testbare Angelegenheit. Es sei denn, man arbeitet mit Django. Django bietet out of the box einfache Möglichkeiten, aus Modellen entsprechende Formulare zu generieren.
Um in Django eine Formularmöglichkeit zu implementieren, sind mindestens diese 4 folgenden Schritte notwendig:
eine Formular-Klasse, die von
ModelForm
erbteine View, die das Formular anzeigt
die URL, die auf die View führt, die das Formular anzeigt
ein Template, welches das Formular anzeigt
Die Klasse forms.ModelForm
Dazu legen wir eine Datei event_manager/events/forms.py
an und befüllen sie mit folgendem Inhalt:
from django import forms
from .models import Category
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = "__all__"
Die Klasse CategoryForm
erbt von forms.ModelForm
. Django erstellt also aus
dem Model ein entsprechendes Formular. Deshalb importieren wir die beiden
Models auch. Um das zu bewerkstelligen, definieren wir über die model
Eigenschaft der Meta-Klasse das Model, auf dessen Basis das Formular erstellt
werden soll:
class Meta:
model = Category
fields = "__all__"
In der Meta-Klasse werden wir später noch einige andere definieren, wie wir
sehen werden. Die fields
- Angabe hier spezifiziert zum Beispiel die
Model-Felder, die wir im Formular später sehen wollen. __all__
steht
offensichtlich für alle Felder, wir könnten aber auch Felder in einem Tupel
angeben:
class Meta:
model = Category
fields = "name", "sub_title"
Hier eine kleine Warnung: Wenn wir im Model Felder als mandatory festgelegt hatten, und wir diese Felder im Formular ignorieren, werden wir früher oder später in ein Problem laufen. Wir müssen also vor dem Speichern den Feldern irgendeinen Wert zuweisen. Wie das geht, werden wir später noch sehen, wenn wir einen Event mit einem eingeloggten Autor anlegen.
Default Rendering der Modelfelder
Wenn das Formular im Template gerendert wird, sind das die Default-HTML-Elemente der wichtigsten Model-Felder:
|
Text-Inputfeld |
|
Select-Box |
|
Number-Inputfeld |
|
Select-Box |
|
Text-Inputfeld |
|
Text-Area |
|
Select-Box |
|
Check-Box |
Eine Übersicht aller Model- und Formfelder findet sich hier: https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#field-types
Wir können für jedes Feld auch speziell festlegen, in welcher Form das Feld
gerendert werden soll. Dieses Verhalten wird in den widgets
-Eigenschaft der Meta
- Klasse
festgelegt. Mehr zu Widgets später.
View zum Anlegen einer Kategorie
Wir benötigen zum Anlegen der Kategorie jetzt natürlich eine View, die wir
category_create
nennen. Die View wird sowohl per GET
über einen Link sowie nach dem Absenden des Formulars per
POST
aufgerufen. Auf diese beiden Ereignisse müssen wir in der View reagieren:
Öffnen wir nun event_manager/events/views.py
und fügen folgende funktionale View ein:
from django.shortcuts import get_object_or_404, render, reverse, redirect
from .forms import CategoryForm
from .models import Event, Category
from .forms import CategoryForm
def category_create(request):
"""Eine View zum Hinzufügen einer Kategorie.
http://127.0.0.1:8000/events/category/create
"""
if request.method == "POST":
form = CategoryForm(request.POST or None)
if form.is_valid():
category = form.save()
return redirect("events:category_detail", pk=category.pk)
else:
form = CategoryForm()
return render(
request,
"events/category_create.html",
{"form": form},
)
Zuerst importieren wir uns unsere neu erstellte Formularklasse und die
redirect-Funktion in die views.py
from django.shortcuts import get_object_or_404, render, reverse, redirect
from .models import Event, Category
from .forms import CategoryForm
Die View reagiert wie gesagt auf zwei Ereignisse: wenn die HTTP-Request-Methode POST
ist, wird eine neue Formular-Instanz mit den aus dem Formular gesendeten
POST-Daten erstellt.
if request.method == "POST":
form = CategoryForm(request.POST)
if form.is_valid():
category = form.save()
return (
redirect("events:category_detail", kwargs={"id": self.category.id}),
)
Wir prüfen mit form.is_valid()
, ob die Formulardaten auch valide sind, und
speichern das Formluar dann ab. Dabei wird uns als Rückgabewert von save()
auch gleich eine neue
Category-Instanz geliefert, die wir für einen Redirect auf die entsprechende
Kategorie-Detailseite nutzen. Dazu erwartet die Funktion redirect
den Namen der
Route, wie wir das schon im Template gesehen hatten, und zum Auflösen der Route
auch noch das nötige Argument (siehe Verlinkung mit dem URL-TAG).
Im anderen Fall, also wenn die Methode GET
war, wird einfach ein leeres Formular erstellt.
form = CategoryForm()
Zuletzt geben wir der render
- Funktion wie gewohnt noch das Request-Objekt, den
Pfad zum (noch nicht existenten) category_create
-Template und den Kontext,
der in diesem Fall aus dem Formular besteht.
return render(
request,
"events/category_create.html",
{"form": form},
)
URL-Auflösung
Wie wir gesehen hatten, können wir der Funktion redirect (und auch reverse)
einen String wie „events:category_detail“ übergeben, der dann wie gewohnt
nach app_name
und path_name
aufgelöst wird. So ähnlich hatten wir
das ja schon im Template mit dem url-Tag gesehen.
Mehr zum Thema Url-Resolving hier: https://docs.djangoproject.com/en/stable/ref/urlresolvers/
Die nötigen URLs
Wir benötigen eine URL für das Erstellen einer Kategorie. Unten auf der Kategorie-Übersicht soll sich ein Link befinden, der den User auffordert, eine neue Kategorie anzulegen. Dieser führt dann auf unser neues Formular.
Dazu öffnen wir event_manager/events/urls.py
und verändern die urlpatterns:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.categories, name="categories"),
path("category/<int:pk>", views.category_detail, name="category_detail"),
# diese Zeile hinzufügen:
path("category/create", views.category_create, name="category_create"),
]
Der neue Eintrag hat den Namen category_create
und referenziert auf die
category_create
- Funktion in den views.py
.
Das Template
Das Template erstellen wir unter event_manager/events/templates/events
und
legen dort die Datei category_create.html
an:
{% extends 'base.html' %}
{% block title %}
neue Kategorie hinzufügen
{% endblock %}
{% block head%}
<a href="{% url 'events:categories' %}">zurück zur Übersicht</a><br>
<h1 class="display-6 fw-bold lh-1">neue Kategorie hinzufügen</h1>
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
<table>
{{form}}
<tr>
<th>
Formular absenden:
</th>
<td>
<input type="submit" value="Jetzt eintragen" />
</td>
</tr>
</table>
</form>
{% endblock %}
Wir legen ein HTML-Formular mit der HTTP-Methode POST an, wie das in Formularen üblich ist. Innerhalb des Formulars definieren wir einen HTML-Table und einen Submit-Button.
In {{form}}
wird das Formular in einer altmodische Tabellenstruktur
gerendert, was heute aus Grund aus responsivem Design mehrheitlich nicht mehr zeitgemäß ist.
Das werden wir später ändern.
Der {% csrf_token %}
rendert ein unsichtbares Hidden-Feld mit einem
Einmal-Token, der von Django in der CSRF-Middleware geprüft wird und uns vor
Cross-Site-Request-Forgery Attacken schützen soll.
Ist der Token falsch, werden die Formulardaten als potientiell unsicher eingestuft und der Request wird abgelehnt.
Mehr zum CSRF-Token hier: https://docs.djangoproject.com/en/stable/csrf/
Was ist ein CSRF?
Cross-Site Request Forgery (CSRF) ist ein Angriff, der einen Endbenutzer dazu zwingt, unerwünschte Aktionen in einer Webanwendung auszuführen, bei der er gerade authentifiziert ist. Mit von Social Engineering (z. B. durch das Versenden eines Links per E-Mail oder Chat) kann ein Angreifer den Benutzer einer Webanwendung dazu bringen, Aktionen nach Wahl des Angreifers auszuführen.
Das Formular verlinken
Wir sind jetzt fast fertig. Nun müssen wir nur noch die Vormularseite verlinken. Dazu soll es einen Link auf der Kategorie-Übersichtsseite geben, der dies ermöglicht.
Dazu öffnen wir event_manager/events/templates/events/categories.html
und
fügen in das Template die Verlinkung ein.
{% extends 'base.html' %}
{% block title %}
Übersicht der Kategorien
{% endblock %}
{% block head %}
<h1 class="display-6 fw-bold lh-1">Kategorien</h1>
{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">
<ul class="list-group event_box">
{% for category in categories %}
<a href="{% url 'events:category_detail' category.pk %}">
<li class="list-group-item rounded">
{{category}}
<span class="badge rounded-pill bg-primary">{{category.num_of_events}}</span>
</li>
</a>
{% endfor %}
</ul>
<p>
<a href="{% url 'events:category_create' %}">neue Kategorie hinzufügen</a>
</p>
</div>
</div>
{% endblock %}
Hier sind ein ein paar Dinge passiert: zuerst einmal haben wir ein paar Bootstrap-spezifische Styles eingefügt, wie zum Beispiel die h1-Überschrift.
Dann rufen wir eine Methode des Category-Objekts auf, die wir noch gar nicht
kennen: num_of_events
. Diese Methode liefert uns für jede Kategorie die
Anzahl an Events, die ihr zugeordnet sind, und zeigt sie in einem
blauen Bootstrap-Pillow an. Interessant ist an dieser Stelle, dass der
Methodenaufruf ohne die runden Klammern erfolgt, wie das sonst üblich ist.
Damit dieser Code funktioniert, müssen wir noch die Methode in das Kategorie-Model
einbauen. Wir öffen event_manager/events/models.py
und fügen unterhalb der
String-Methode die Methode num_of_events
hinzu.
class Category(DateMixin):
"""Eine Kategorie für einen Event."""
name = models.CharField(max_length=100, unique=True)
sub_title = models.CharField(max_length=200, null=True, blank=True)
description = models.TextField(null=True, blank=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
def num_of_events(self):
"""Die Anzahl der Events einer Kategorie."""
return self.events.count()
Wenn wir den Entwicklungsserver starten und nach http://127.0.0.1:8000/events/categories navigieren, sollte unsere Kategorieübersicht so aussehen:
Das Formular testen und absenden
Wir sollten das Formular jetzt ausfüllen und absenden können. Nach erfolgreichem Absenden werden wir auf die Detailseite der neu angelegten Kategorie geleitet. Dort gibt es auch eine Verlinkung zur Übersicht, die wir vorhin angelegt hatten.
Eine Kategorie updaten
Bisher sind wir noch nicht in der Lage, eine Kategorie zu editieren. Das wollen wir nachholen, und ein Update-Formular anlegen. Der Prozess ist im Grunde der gleiche, wie beim Anlegen. Wir benötigen wieder eine View, die URLs, und ein Template.
Da es sich bei unserem Formular um ein ModelForm
handelt, welches das Kategorie-Model abbildet, benötigen wir für die
Update-Methode nicht extra noch eine eigene Form-Klasse.
URLs für das Editieren einer Kategorie
Dazu öffnen wir wieder die event_manager/events/urls.py
:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.categories, name="categories"),
path("category/<int:pk>", views.category_detail, name="category_detail"),
path("category/create", views.category_create, name="category_create"),
# diese Zeile hinzufügen:
path("category/<int:pk>/update",
views.category_update,
name="category_update"
),
]
Der neue Eintrag hat den Namen category_update
und referenziert auf die
category_update
- Funktion in den views.py
, die wir jetzt gleich anlegen
werden. Die URL match zum Beispiel auf diese Anfrage:
http://127.0.0.1:8000/events/category/3/update
Noch kommt es hier allerdings noch zu einem Fehler, da die View und das Template noch nicht implementiert sind.
Die Kategorie-Update View
Legen wir die View jetzt an. Unter event_manager/events/views.py
tragen wir die neue View ein:
def category_update(request, pk):
"""View zum Ändern einer Kategorie.
http://127.0.0.1:8000/events/category/7/update
"""
instance = get_object_or_404(Category, pk=pk)
form = CategoryForm(request.POST or None, instance=instance)
if form.is_valid():
category = form.save()
return redirect("events:category_detail", pk=category.pk)
return render(
request,
"events/category_update.html",
{"form": form},
)
Zuerst holen wir uns anhand des Primary Keys pk
die Kategorie-Instanz,
die geändert
werden soll, aus der Datenbank und lösen einen 404-Fehler aus, falls das Objekt
nicht (mehr) vorhanden sein sollte.
In einem nächsten Schritt erstellen wir ein neues form
auf Basis der
geladenen Kategorie. Wurde die View per HTTP-POST angesprochen, füllen wir das
Formular mit den Werten aus dem HTML-Formular.
Wenn der Formularinhalt valide
ist, speichern wir das Objekt in der Datenbank
ab und führen einen Redirect auf die Detailseite der Kategorie aus.
Andernfalls leiten wir im Fehlerfall auf das Formular zurück, damit der User seine Eingabedaten eingeben oder überprüfen kann.
Verlinkung von der Kategorie-Detailseite
Wir wollen auf der Detailseite der Kategorie einen Link setzen, der uns die
Möglichkeit gibt, die Kategorie zu editieren. Der Link soll allderings nur
sichtbar sein, wenn der User am System eingeloggt ist. Dazu nutzen wir im
Template den if-Tag
und lesen das user
-Objekt aus. Das User-Objekt steht uns
in jedem Template pauschal zur Verfügung.
Hinweis: Einen Link zu verbergen ist keine Sicherheitsmaßnahme, sondern in diesem Fall nur ein erster Schritt in Richtung Erhöhung der Benutzerfreundlichkeit. Die schreibenden Zugriff auf die Kategorie und die Events sollen später nur noch für eingeloggte User funktionieren.
Dazu öffen wir das Template der Kategorie-Detailseite unter
event_manager/events/templates/events/category_detail.html
und fügen einen
Link ein zum Editieren der Kategorie ein:
{% extends 'base.html' %}
{% block title %}
{{category.name}}
{% endblock %}
{% block head %}
<p>
<a href="{% url 'events:categories' %}">zurück zur Übersicht aller Kategorien</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{category.name}}</h1>
<h3 class="display-6 lh-3">{{category.sub_title}}</h3>
{% if user.is_authenticated %}
<span style="font-size:12px;">
<a href="{% url 'events:category_update' category.pk %}">editieren</a>
</span>
{% endif %}
{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">
<p>{{category.description}}</p>
</p>
</div>
</div>
{% endblock %}
Nebenbei haben wir noch etwas am Style geändert, dass der Look ein bisschen besser wird.
Wenn wir nun die Detailseite einer Kategorie besuchen, finden wir einen Link, der uns auf das Updaten der Kategorie führt. Dazu müssen wir allerdings eingeloggt sein, sonst sieht man den Link nicht.
Das Template für die Update-View
Last but not least muss das eigentliche Formular gerendert werden. Dazu legen wir ein Template unter
event_manager/events/templates/events/category_update.html
an und fügen den
HTML-Code hinzu:
{% extends 'base.html' %}
{% block title %}
Kategorie bearbeiten
{% endblock %}
{% block head%}
<a href="{% url 'events:categories' %}">zurück zur Übersicht</a><br>
<h1 class="display-6 fw-bold lh-1">Kategorie bearbeiten</h1>
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
<table>
{{form}}
<tr>
<th>
Formular absenden:
</th>
<td>
<input class="btn btn-primary" type="submit" value="Jetzt eintragen" />
</td>
</tr>
</table>
</form>
{% endblock %}
Das Template stellt das Formular wie schon vorher in einer Tabelle da und bietet einen Submit-Button zum Absenden.
Damit sind wir in diesem Kapitel fertig. Wir haben bis auf das Löschen einer
Kategorie alle anderen CRUD-Operationen
umgesetzt. Wer Lust hat, kann gerne
noch ein wenig am Layout rumspielen, bevor es mit den Views für die Events
weitergeht.