einen Event via Formualar hinzufügen
Wir wollen -eingeloggten- Usern ermöglichen, Events via einem Formular einzutragen. Da wir zur Zeit noch keinen Authentifizierungsmechanismus implementiert haben, loggen wir uns über die Django-Adminoberfläche ein. Wir sind damit ordentlich authentifziert und können dann Events eintragen. Später werden wir noch einen Mechanismus einrichten, damit sich auch andere User einloggen können.
Wir wollen jetzt folgende klassenbasierte
Views (class based views) für die Events anlegen:
* Event eintragen
* eingetragenen Event updaten
Der Plan
Wir wollen folgendes bewerkstelligen: Wenn ein User auf eine Kategorie-Detailseite, zb. unter http://127.0.0.1/events/category/sport
navigiert, soll er dort einen Button vorfinden, auf dem er einen Event für diese Kategorie eintragen kann.
Ein Formular entwickeln
Folgende Dinge sind nötig, um ein Formular zu implementieren:
eine Formular-Klasse, die von
ModelForm
erbtdie URL, die auf das Formular führt
eine View, die das Formular anzeigt
ein Template, welches das Formular anzeigt
Formular-Klasse
Für jedes Formular, dass wir darstellen wollen, benötigen wir eine entsprechende Klasse.
Dazu legen wir eine Datei event_manager/events/forms.py
an und befüllen sie mit folgendem Inhalt:
from django import forms
from django.core.exceptions import ValidationError
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
Die Klasse EventForm
erbt von forms.ModelForm
. Django erstellt also aus dem Model ein entsprechendes Formular.
Zusätzlich importieren wir auch noch from django.core.exceptions import ValidationError
, damit wir das Formular auch Validieren können.
Formular-View
Um das Formular anzeigen zu können, müssen wir unter event_manager/events/views.py
eine View entwickeln. Dazu nutzen wir wieder eine generische klassenbasierte View:
Wir importieren folgende generische Views sowie unsere eben erstelle Formularklasse:
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from .forms import EventForm
Oldschool funktionsbasierte Formularview
Bevor wir uns den moderneren klassenbasierten Aufbau ansehen, gucken wir uns die etwas ältere Methode an, die immer noch genutzt werden kann und bei komplexeren Prozessen der klassenbasierten View auch vorzuziehen ist.
def create_event(request, category_id):
"""
/event/add/<category_id>
"""
cat = get_object_or_404(Category, pk=category_id)
if request.method == "POST":
form = EventForm(request.POST or None)
if form.is_valid():
event = form.save(commit=False)
event.category = cat
event.author = request.user
event.save()
return redirect(event.get_absolute_url())
else:
form = EventForm()
return render(
request,
"events/event_add.html",
{"form": form},
)
Wir erstellen eine Funktion create_event
die als Parameter das Request-Objekt und die Kategorie-ID erwartet. Wir fragen zuerst die Kategorie ab (cat = get_object_or_404(Category, pk=category_id)
) und prüfen dann die HTTP-Methode.
Wenn die HTTP-Methode NICHT POST
ist, also das Formular noch NICHT abgesendet wurde, erstellen wir eine Instanz des Formulars und rendern dieses in das Template. Ansonsten erstellen wir eine Instanz des Formulars und füllen es mit den Formulardaten, prüfen dann mit form.is_valid()
ob es valide ist und speichern es mit Kategorie und Autor ab. Dann führen wir einen Redirect
auf die absolute URL des Eventobjects aus (sprich, besuchen danach die Detailseite des neu angelegten Objektes).
event = form.save(commit=False)
erstellt ein Objekt auf Basis des gefüllten Formulars, commit=False
zeigt aber an, dass es nicht gespeichert werden soll. Denn bevor wir es speichern, müssen wir noch die Kategorie (die aus der URL extrahiert wird) und den Autor, der ja der eingeloggte User ist, angeben.
Etwas einfacher geht es mit der CreateView
.
Die Create-View
Gucken wir uns die CreateView mal genauer an: <https://ccbv.co.uk/projects/Django/4.0/django.views.generic.edit/CreateView/>_
Folgende Attribute sind für uns erstmal von Interesse:
success_url: nach erfolgreichem Eintrag wird dorthin geleitet
template_name_suffix. Der Default Suffix ist ‘_form’, also ‘event_form’
Die View anlegen:
class EventCreateView(LoginRequiredMixin, CreateView):
"""
create an event for certain category
events/event/create/3
"""
model = Event
form_class = EventForm
def form_valid(self, form):
form.instance.category = self.category
form.instance.author = self.request.user
return super(EventCreateView, self).form_valid(form)
def get_initial(self):
self.category = get_object_or_404(
Category,
pk=self.kwargs["category_id"]
)
Wir erben also von CreateView und nutzen wieder einen Mixin, diesemal den LoginRequiredMixin
. Der User muss also eingeloggt sein, um diese View zu sehen. Wir definieren das Model model=Event
und die Formklasse mit form_class=EventForm
.
Dazu implementieren wir noch zwei Methoden: get_initial
wird immer zuerst aufgerufen, wenn diese View aufgerufen wird. In ihr prüfen wir, ob die übergebene Kategorie überhaupt existiert und lösen einen 404-Fehler
aus, falls dies nicht der Fall sein sollte.
Zudem implementieren wir form_valid
, die aufgerufen wird, wenn das Formular abgesendet wird. In ihr weisen wir der Form-Instanz die Kategorie sowie den -eingeloggten user- als Author zu.
Mixins
Das Salz in der Suppe von klassenbasierten Views sind Mixins. So ermöglicht zum Beispiel der soeben genutze LoginRequiredMixin
, dass der User, der diese View anfordert, eingeloggt sein muss. Genauer genommen bestehen unsere komfortablen generischen Views auf vielen anderen Mixin-Klassen, die genutzt werden:
https://docs.djangoproject.com/en/4.0/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView
Wir können unseren Views aber auch noch andere Mixins hinzufügen: zum Beispiel den UserPassesTestMixin
, der gewährleistet, dass der User einen Test besteht, zb. sein Name die Zeichenkette “Klaus” beinhaltet. Oder der PermissionRequiredMixin
der prüft, ob der User die entsprechenden Rechte hat.
https://docs.djangoproject.com/en/4.0/topics/class-based-views/mixins/
Das Template für das Formular
Fehlt noch das Template für das Formular:
Wir legen unter folgende Datei an event_manager/events/templates/events/event_form.html
und fügen folgenden Code hinzu:
{% extends 'base.html' %}
{% block title %}
Event eintragen
{%endblock%}
{% block head %}
Event eintragen
{%endblock%}
{% block content %}
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
<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 %}
In {{form}}
wird das Formular gerendert, der {% csrf_token %}
dient der Sicherheit. Mehr zum CSRF-Token hier: https://docs.djangoproject.com/en/4.0/ref/csrf/
Die URL
Wir fügen jetzt die URL in die event_manager/events/urls.py
ein:
urlpatterns = [
path("hello_pingus", views.hello_pingus, name="hello_pingus"),
path("categories", views.list_categories, name="categories"),
path("category/<slug:slug>", views.category_detail, name="category_detail"),
path("", views.EventListView.as_view(), name="events"),
path(
"event/<slug:slug>",
views.EventDetailView.as_view(),
name="event_detail"
),
# diese Zeile hinzufügen:
path(
"event/create/<int:category_id>",
views.EventCreateView.as_view(),
name="event_create",
),
]
Unter folgender URL können wir jetzt einen Event eintragen: http://127.0.0.1:8000/events/event/create/42
.
Verlinkung von der Kategorie-Detailseite aus
Wir wollen die Möglichkeit schaffen, von der Kategorie-Detailseite aus einen Event einzustellen. Dazu öffnen wir das Template für die Kategorie-Detailseite unter event_manager/events/templates/events/category_detail.html
und fügen folgenden Code ein:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Kategorie {{category.name}}
{%endblock%}
{% block head %}
Kategorie {{category.name}}
{%endblock%}
{% block content %}
<p>{{category.description}}</p>
<p>created at: {{category.created_at}}
<p>
<a href="{% url 'events:event_create' category.pk %}">neues Event eintragen</a>
</p>
{%endblock%}
Wenn wir jetzt nach http://127.0.0.1:8000/events/category/sport
navigieren, kommen wir über den Link unten auf das Formular.
Crispy Forms
Um den Look des Formulars zu verbessern, installieren wir mit aktiviertem Environment via pip das Modul Crispy forms
bzw. das Crispy-Bootstrap5-Paket.
Dieses benötigen wir, um mit Bootstrap5
zu arbeiten.
$ (eventenv) pip install django-crispy-forms
$ (eventenv) pip install crispy-bootstrap5
und fügen es unter settings.py
den installierten Apps hinzu:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_bootstrap5",
"crispy_forms",
]
# ebenfall in die settings.py
CRISPY_TEMPLATE_PACK = "bootstrap5"
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
https://django-crispy-forms.readthedocs.io/en/latest/install.html
jetzt ändern wir das Template event_manager/events/templates/events/event_form.html
wie folgt ab:
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
Event eintragen
{%endblock%}
{% block head %}
Event eintragen
{%endblock%}
{% block content %}
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
<form method="POST">
{% csrf_token %}
<table>
{{form|crispy}}
<tr>
<th>
Formular absenden:
</th>
<td>
<input class="btn btn-primary" type="submit" value="Jetzt eintragen" />
</td>
</tr>
</table>
</form>
{% endblock %}
Das Formular sollte jetzt deutlich schöner aussehen.
Das Formular weiter verbessern
Das Formular hat diverse Problembereich: zuerst befinden sich einige Felder in ihm, die wir nicht darstellen wollen, zum Beispiel die Auswahl der Kategorie, denn diese Auswahl haben wir schon getroffen. Wir wollen auch nicht, dass der Autor auswählbar ist, denn dies wäre eine Sicherheitslücke. Zudem wollen wir verhindern, dass der Slug manuell eingegeben werden kann.
Felder exkludieren
Um zu verhindern, dass Model-Felder in Formularen dargestellt werden, nutzen wir das Attribut exclude
. In unserem Fall wollen wir aus dem Event-Formular die Felder slug
, category
und author
entfernen. Der Slug wird automatisch beim Speichern des Formulars erstellt, und die Kategorie bzw. den Author fügen wir selbständig hinzu. Der User soll ja nicht auswählen können, wer den Event eingetragen hat.
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("slug", "category", "author")
Ein Problem ist jetzt noch die Eingabe des Datums. Um diese zu optimieren, werden wir ein Widget verwenden, um ein Kalenderwidget statt eines einfachen Textfeldes zu erzeugen.
Widgets
ein Widget
ist Djangos Darstellung eines HTML-Eingabeelements. Das Widget handhabt das Rendern des HTML und das Extrahieren von Daten aus einem GET/POST-Wörterbuch, das dem Widget entspricht.
Widgets können wir u.a. nutzen, in dem wir der Form-Klasse das widgets-dictionary
für die Felder definieren, die wir überschreiben wollen.
Wir wollen für das Feld date
statt eines Textfeldes eine DateInput
-Komponente erzeugen. Diese Datums-Komponente ist browserspezifisch, Firefox stellt den Kalender also anders da als Google Chrome.
widgets = {
"date": forms.DateInput(
format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
),
}
Es können natürlich auch eigene, speziellere Widgets entwickelt werden, was über den Scope dieses Buches hinausgeht.
Mehr zu Widgets finden sich hier: <https://docs.djangoproject.com/en/4.0/ref/forms/widgets/>_
An dieser Stelle führen wir noch eine Validierung durch: wir prüfen, ob der
Sub-Title des Events mit Sonderzeichen beginnt und verhindern dies, in dem wir
das einen ValidationenError
erheben, falls dies passiert.
def clean_sub_title(self) -> str:
sub_title = self.cleaned_data["sub_title"]
illegal = ("*", "-", ":")
if isinstance(sub_title, str) and sub_title.startswith(illegal):
raise ValidationError("Dieses Zeichen ist nicht erlaubt!")
return sub_title
Auf das Dict self.cleaned_data
kann man gezielt per Key zugreifen. Dort befinden sich alle Einträge, aus denen der User-Input in Python-Objekte umgewandelt wurde und die save-Methode
überstanden haben. Wir können einen ValidationError erheben und damit das Formular zum User zurücksenden lassen.
Der Rückgabewert der Methode clean_sub_title
ist der neue Inhalt des Feldes sub_title
-.
Formulareingabe-Validierung
In Django kann die Eingabevalidierung an mehreren Ebenen stattfinden, je nachdem, was man benötigt. Zum Beispiel wollen wir verhindern, dass Events mit einem Namen kleiner als zwei Zeichen eingegeben werden oder ungültige Email-Adressen.
Auf Model-Ebene
Wir können Model-Feldern (auch eigene) Validatoren zuweisen. Dies passiert in der Datei models.py
. Diese Model-Validatoren werden auch in der Admin-Oberfläche berücksichtigt.
Auf Formular-Ebene
Wir können direkt in der Form-Klasse Felder validieren. Dazu nutzen wir entweder die Methode clean_<FELDNAME>
um gezielt Funktionen zur Feldüberprüfungen zu schreiben oder die Funktion clean
, um Felder zu überprüfen, die voneinander abhängen.
Die Formular-Validatoren werden NICHT in der Admin-Oberfläche berücksichtigt.
Mehr dazu unter: <https://docs.djangoproject.com/en/4.0/ref/forms/validation/#cleaning-a-specific-field-attribute>_
Auf View-Ebene man könnte auch in den Views die Eingabe validieren, davon wird jedoch abgeraten.
Ablauf des Validierungsprozesses siehe https://stackoverflow.com/questions/38394078/django-form-validation-overview-quick
Usere EventForm Klasse unter event_manager/events/forms.py
sieht jetzt so aus:
from django import forms
from django.core.exceptions import ValidationError
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("slug", "category", "author")
widgets = {
"date": forms.DateInput(
format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
),
}
labels = {
"name": "Name des Events",
"sub_title": "Slogan",
"description": "Kurzbeschreibung",
"date": "Datum des Events",
}
def clean_sub_title(self):
sub_title = self.cleaned_data["sub_title"]
illegal = ("*", "-", ":")
if isinstance(sub_title, str) and sub_title.startswith(illegal):
raise ValidationError("Dieses Zeichen ist nicht erlaubt!")
return sub_title
Ein Objekt updaten
Wir wollen dem User jetzt die Möglichkeit geben, Events auch upzudaten.
nur die Eigentümer eines Events dürfen updaten / löschen
Template erstellen für Update Event
Für das Update-Formular nutzen wir ebenfalls das schon bei EventCreate
genutzt event_form.html
. Dazu müssen wir im Template allerdings unterscheiden, ob wir gerade die Create-View aufrufen oder die Update-View. Dazu können wir prüfen, ob ein object-Objekt
im Kontext übergeben wurde.
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
{% if object %}
Event {{object.name}} updaten
{% else %}
Event eintragen
{% endif %}
{%endblock%}
{% block head %}
{% if object %}
Event {{object.name}} updaten
{% else %}
Event eintragen
{% endif %}
{%endblock%}
{% block content %}
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
<form method="POST">
{% csrf_token %}
<table>
{{form|crispy}}
<tr>
<th>
Formular absenden:
</th>
<td>
<input class="btn btn-primary" type="submit" value="Jetzt speichern" />
</td>
</tr>
</table>
</form>
{% endblock %}
View erstellen für das Updaten eines Events
Unter event_manager/events/views.py
die View anlegen:
from django.core.exceptions import PermissionDenied
class EventUpdateView(LoginRequiredMixin, UpdateView):
"""
update an event
events/event/update/3
"""
model = Event
form_class = EventForm
def form_valid(self, form):
if form.instance.author != self.request.user:
raise PermissionDenied(_("Zugriff ist leider nicht erlaubt"))
return super(EventUpdateView, self).form_valid(form)
Wir erben also von UpdateView (welches wir vorhin schon importiert hatten) und importieren auch noch die PermissionDenied
-Exception aus den django.core.exceptions
. Wieder nutzen wir den LoginRequiredMixin
. Der User muss also eingeloggt sein, um das Event-Model updaten zu dürfen.
Nicht upgedaten werden können die Felder category, author und slug
, wie wir das in der Formklasse schon definiert hatten.
In der form_valid
-Methode prüfen wir, ob der Autor des Events auch dem User entspricht. Falls das nicht der Fall sein sollte, erheben wir eine PermissionDenied
-Exception, die in einem 403 Forbidden
-Error für den User resultiert.
Eigene Fehlerseiten
Für folgende HTTP-Statuscodes lassen sich unter event_manager/templates
Fehlerseiten anlegen:
404 dazu eine eigene
404.html
anlegen500 dazu eine eigene
500.html
anlegen403 dazu eine eigene
403.html
anlegen
Die URL
Wir fügen jetzt die URL in die event_manager/events/urls.py
ein:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.list_categories, name="categories"),
path("category/<slug:slug>", views.category_detail, name="category_detail"),
path("", views.EventListView.as_view(), name="events"),
path(
"event/<slug:slug>",
views.EventDetailView.as_view(),
name="event_detail"
),
path(
"event/create/<int:category_id>",
views.EventCreateView.as_view(),
name="event_create"
),
# diese Zeile hinzufügen:
path(
"event/update/<int:pk>",
views.EventUpdateView.as_view(),
name="event_update"
),
]
Verlinkung von der Events-Detailseite
Um das Update-Formular aufrufen zu können, benötigen wir eine Verlinkung.
Dazu öffnen wir das Template für die Event Detailseite event_manager/events/templates/events/event_detail.html
und fügen folgenden Code hinzu:
{% if user.is_authenticated and user == object.author %}
<p><a href="{% url 'events:update_event' object.id %}">Event editieren</a></p>
{% endif %}
Wir prüfen also, ob der User eingeloggt ist und wenn ja, ob er auch Autor des Events ist. Nur dann darf er das Formular auch editieren und nur dann wir der Link dazu angezeigt.
Das Template unter event_manager/events/templates/events/event_detail.html
sieht jetzt so aus:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Event: {{object.name}}
{%endblock%}
{% block head %}
{{object.name}}
{%endblock%}
{% block content %}
<h1>{{object.name}}</h1>
<p>{{object.sub_title}}</p>
<p>Category: {{object.category}}</p>
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<p>findet statt am: {{object.date}}</p>
<p>created at: {{object.created_at}}</p>
{% if object.related_events %}
<h4>Ähnliche Events</h4>
<ul>
{% for related in object.related_events %}
<li>{{related.name}}</li>
{% endfor %}
</ul>
{% endif %}
<p><a href="{% url 'events:update_event' object.id %}">Event editieren</a></p>
{%endblock%}
Ein Objekt löschen
URL
path(
"event/delete/<int:pk>",
views.EventDeleteView.as_view(),
name="event_delete",
),
View
class EventDeleteView(DeleteView):
# default Templates: company_confirm_delete
model = Event
def form_valid(self, form):
if form.instance.author != self.request.user:
raise PermissionDenied(_("Zugriff ist leider nicht erlaubt"))
return super(EventUpdateView, self).form_valid(form)
def get_success_url(self) -> str:
return reverse("events:events")
Event ist nur löschbar, wenn der eingeloggte User auch der Autor ist. Nach erfolgreichem Löschen wird auf http://127.0.0.1/events/events
weitergeleitet.
Template
Unter event_manager/events/templates/events/event_confirm_delete.html
legen wir das Template an.
{% extends 'base.html' %}
{% block head %}
<h1>Event löschen: {{object.name}}</h1>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>Willst du das Event sicher löschen? "{{ object }}"?</p>
<input type="submit" class="btn btn-primary" value="Ja, weg damit!">
</form>
{% endblock %}
Event-Detailseite
und modifizieren die Event-Detailsieite
{% if user.is_authenticated and user == object.author %}
<p
<a href="{% url 'events:update_event' object.id %}">Event editieren</a><br>
<a href="{% url 'events:delete_event' object.id %}">Event löschen</a><br>
</p>
{% endif %}
Das Template unter event_manager/events/templates/events/event_detail.html
sieht jetzt so aus:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Event: {{object.name}}
{%endblock%}
{% block head %}
{{object.name}}
{%endblock%}
{% block content %}
<h1>{{object.name}}</h1>
<p>{{object.sub_title}}</p>
<p>Category: {{object.category}}</p>
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<p>findet statt am: {{object.date}}</p>
<p>created at: {{object.created_at}}</p>
{% if object.related_events %}
<h4>Ähnliche Events</h4>
<ul>
{% for related in object.related_events %}
<li>{{related.name}}</li>
{% endfor %}
</ul>
{% endif %}
<p><a href="{% url 'events:update_event' object.id %}">Event editieren</a></p>
{%endblock%}