einen Event via Formular hinzufügen
Wir wollen eingeloggten und am System authentifizierten Usern ermöglichen, Events mit Hilfe eines HTML-Formulars 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 Events eintragen.
Später werden wir einen Mechanismus einrichten, damit sich User über einen richtigen Login einloggen können. Erinnerung: Der Link zur Adminoberfläche ist http://127.0.0.1/admin.
In dieser Lektion wollen wir folgende klassenbasierte
Views (CBV) für die Events anlegen:
Event eintragen
eingetragenen Event editieren
Der Plan
Wir wollen folgendes bewerkstelligen: Wenn ein User auf eine
Kategorie-Detailseite, zb. unter http://127.0.0.1/events/category/3
navigiert, soll er dort einen Button vorfinden, auf dem er einen Event für
diese Kategorie eintragen kann. Beim Eintragen soll die Kategorie im Formular nicht
auswählbar sein. So soll das in etwa aussehen:
Da jedes Event einen Autor benötigt, müssen wir beim Eintragen auch einen User angeben. Auch dies soll der eingeloggte User nicht auswählen können, denn er ist ja der Autor selber.
Ein Formular entwickeln
Formular-Klasse
Für jedes Formular, dass wir darstellen wollen, benötigen wir eine
entsprechende Klasse. Die Datei, in der die Formulare üblicherweise angelegt
werden, heisst forms.py
. Diese hatten wir schon bei dem Kategorieformular
angelegt.
Dazu öffen wir die schon vorherangelegte event_manager/events/forms.py
an und befüllen sie mit folgendem Inhalt:
from django import forms
from django.core.exceptions import ValidationError
from .models import Event, Category
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = "__all__"
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. Deshalb importieren wir die beiden
Models auch.
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
Diese drei Views benötigen wir zum Umsetzen der Aktionen Anlegen, Löschen und Editieren eines Event-Objekts.
Oldschool funktionsbasierte Formularview
Bevor wir uns den moderneren klassenbasierten Aufbau ansehen, gucken wir uns die funktionsbasierte Variante an, die wir schon bei den Kategorien gesehen hatten. Ihr braucht diesen Code nicht abzutippen, da wir ihn sowieso nicht nutzen werden.
def create_event(request, category_id):
"""
/event/add/<category_id>
"""
category = 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 = category
event.author = request.user
event.save()
return redirect(event.get_absolute_url())
else:
form = EventForm()
return render(
request,
"events/event_add.html",
{"form": form},
)
Schauen wir uns diesen Code Schritt für Schritt an:
Wir erstellen eine Funktion create_event
die als Parameter das Request-Objekt und die Kategorie-ID erwartet.
Wir erinnern uns: wir befanden uns ja direkt vorher auf der Detailseite der
Kategorie und wollen diese Information nutzen, um für genau diese Kategorie
einen Event anzulegen.
Wir fragen dann zuerst mal die Kategorie ab. Existiert diese nicht, weil der User eine Kategorie
angegeben hat, die nicht existiert, wird ein 404-Fehler
ausgelöst.
category = get_object_or_404(Category, pk=category_id)
Dann prüfen wir die die HTTP-Methode.
Wenn diese NICHT POST
ist, also das Formular noch NICHT
per Submit abgesendet wurde, erstellen wir eine Instanz des Formulars und rendern dieses
in das Template. Wie das Formular im Template gerendert wird, hatten wir ja
schon bei den Kategorien gesehen. Mehr zu diesem Thema gleich weiter unten.
else:
form = EventForm()
return render(
request,
"events/event_add.html",
{"form": form},
)
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.
Trifft das zu, nutzen wir form.save
, um eine Instanz
des Models auf Basis des gefüllten Formulars zu erstellen. commit=False
zeigt an, dass es nicht in der Datenbank gespeichert werden soll.
if form.is_valid():
event = form.save(commit=False)
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. Wir
setzen also die beiden Attribute author
und category
und speichern
es ab.
Dann führen wir einen Redirect
auf die absolute URL des Eventobjects
aus (sprich, besuchen danach die Detailseite des neu angelegten Objektes).
Die get_absolute_url
hatten wir im Event-Model bereits implementiert.
event.category = category
event.author = request.user
event.save()
return redirect(event.get_absolute_url())
Tatsächlich könnte man im Redirect den expliziten Aufruf der get_absolute_url
-Methode auch weglassen, da der Default-Redirect diese Methode anspricht, wenn
sie implementiert ist.
return redirect(event)
Etwas einfacher geht es mit der generischen CreateView
.
Die Create-View
Gucken wir uns die CreateView mal genauer in der Doku an: https://ccbv.co.uk/projects/Django/ref/django.views.generic.edit/CreateView/
Folgende Attribute sind für uns erstmal von Interesse:
success_url:
: nach erfolgreichem Eintrag wird dorthin geleitettemplate_name_suffix
: Der Default Suffix ist_form
, alsoevent_form
Die View anlegen:
from django.contrib.auth.mixins import LoginRequiredMixin
class EventCreateView(LoginRequiredMixin, CreateView):
"""
create an event for a 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().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.
from django.contrib.auth.mixins import LoginRequiredMixin
class EventCreateView(LoginRequiredMixin, CreateView):
Wir definieren das Model und die benötigte Formularklasse:
model = Event
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.
def get_initial(self):
self.category = get_object_or_404(
Category,
pk=self.kwargs["category_id"]
)
Zudem implementieren wir die Methode form_valid
, die aufgerufen wird, bevor
das Objekt in der Datenbank gespeichert wird. In ihr weisen wir der
Form-Instanz die Kategorie sowie den - eingeloggten User - als Author zu.
def form_valid(self, form):
form.instance.category = self.category
form.instance.author = self.request.user
return super().form_valid(form)
Bei erfolgreichem Eintragen werden wir übrigens per default auf die neu
angelegte Detailseite des Events weitergeleitet. Das passiert, weil die
success_url
auf die Methode get_absolute_url
verweist.
Wir können success_url
auch mit einem anderen Wert definieren, wenn wir zum
Beispiel auf die Events-Übersicht weitergeleitet werden wollen:
from django.urls import reverse_lazy
class EventCreateView(LoginRequiredMixin, CreateView):
...
model = Event
form_class = EventForm
success_url = reverse_lazy('events:events')
In klassenbasierten Views müssen wir statt der reverse
- Funktion, die eine
URL-Aufösung vornimmt, die Funktion reverse_lazy
nutzen. Diese funktioniert
genauso, nur eben lazy. Der Grund ist, dass in klassenbasierten Views zur
Bootzeit des Projekts der Pfad noch nicht festgelegt ist und es zu einem Fehler
kommen würde, wenn wir hier das einfache reverse nutzen.
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/stable/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.
Mehr dazu im zweiten Teil des Buches: Die Rückkehr der Djangoheroes
.
Meht zu Mixins in der Doku: https://docs.djangoproject.com/en/ref/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 %}
<h1 class="display-6 fw-bold lh-1">Event eintragen</h1>
{%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 %}
Wir mussten immer noch die Tabellenstruktur und den form-Tag anlegen. Das werden wir gleich später noch ändern.
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.categories, name="categories"),
path("category/<int:pk>", views.category_detail, name="category_detail"),
path("", views.EventListView.as_view(), name="events"),
path("event/<int:pk>", 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.
Wichtig:
Da wir den LoginRequiredMixin genutzt hatten, müssen wir für diese Aktion eingeloggt sein. Wer das aktuell nicht ist, bitte nach http://127.0.0.1:8000/admin navigieren und einloggen. Zudem muss natürlich die ID der Kategorie existieren. Wenn es die Kategorie mit der ID 42 bei Dir nicht gibt, wähle eine passende ID aus.
So sollte das Formular zum Eintragen eines Events aktuell aussehen:
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' %}
{% 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>
<a href="{% url 'events:event_create' category.pk %}">
<button class="btn btn-primary">neues Event in {{category}} eintragen</button>
</a>
</p>
</div>
</div>
{% endblock %}
Wenn wir jetzt nach http://127.0.0.1:8000/events/category/3
navigieren, kommen wir über den Button neues Event in Sports eintragen
auf das Formular zum Eintragen eines Events.
Crispy Forms
Aktuell liegt das Formular als HTML-Tabelle vor. Das wollen wir ändern, da Tabellen nicht wirklich responsive sind und auf mobilen Geräten womöglich nicht skalieren.
Um auch gleich den Look des Formulars zu verbessern, wollen wir das Modul Crispy forms
nutzen. Hierbei handelt es sich um ein Modul, mit dem man die Arbeit mit
Formularen stark vereinfachen können.
Fügen wir das Paket inkl. dem Templatepack für Bootstrap5 der requirements.in
hinzu:
django==stable.3
# der requirements.in hinzufügen:
django-crispy-forms
crispy-bootstrap5
Kompilieren und Syncen …
(eventenv) pip-compile requirements.in
(eventenv) pip-sync requirements.txt requirements-dev.txt
Nun fügen wir es unter settings.py
den installierten Apps hinzu:
INSTALLED_APPS = [
"user",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_bootstrap5",
"crispy_forms",
"events",
]
Ebenfalls in der settings.py
setzen wir noch zwei Konfigurationskonstanten:
CRISPY_TEMPLATE_PACK = "bootstrap5"
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bootstrap5",)
Mehr zu Crispy Forms in der Doku: 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 %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">Event eintragen</h1>
{%endblock%}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-12 mb-5">
<form method="POST">
{% csrf_token %}
{{form|crispy}}
<div>
<input class="btn btn-primary" type="submit" value="Jetzt eintragen" />
</div>
</form>
<div>
</div>
</div>
{% endblock %}
Das Formular sollte jetzt deutlich schöner aussehen. Wir nutzen jetzt auch
keine Tabellen mehr, sondern moderne Div-Container. Das hat alles CrispyForms
für uns gemacht, und zwar mit einem simplen {{form|crispy}}
. Mehr zu Crispy
Forms später.
Unser Event Formular sollte nun in etwa so aussehen:
Das Formular weiter verbessern
Das Formular hat diverse Problembereiche: zuerst befinden sich einige Felder in ihm, die wir nicht darstellen wollen, zum Beispiel die Auswahl der Kategorie, denn diese Auswahl haben wir schon dadurch getroffen, dass wir vorher auf der Kategorieseite waren und den Button Event hinzufügen gedrückt hatten.
Wir wollen natürlich auch nicht, dass der Autor auswählbar ist, denn der User, der den Event angelegt, ist der Autor des Events.
Felder exkludieren
Um zu verhindern, dass Model-Felder in Formularen dargestellt werden, nutzen
wir das Attribut exclude
der ModelForm-Klasse. In unserem Fall wollen wir aus dem Event-Formular
die Felder category
und author
entfernen. Die Kategorie bzw. den
Author fügen wir kurz vor dem Eintragen in die Datenbank selbständig hinzu.
Der User soll ja nicht zusätzlich auswählen können, wer den Event eingetragen hat.
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("category", "author")
Bei den Feldern fields
und exclude
darauf achten, dass der erwartete Datentyp ein Tupel ist!
Es sei denn, man wählt den Wert __all__
, dann ist auch ein String erlaubt.
Würde man also nur die Kategorie exkludieren wollen, müsste man schreiben:
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("category",)
Wenn wir die Seite aktualisieren, sind die beiden Felder nicht mehr vorhanden:
Ein Problem ist jetzt noch die Eingabe des Datums. Es ist extrem user-unfreundlich, eine Datumseingabe über ein einfaches Textfeld abzufragen. Oft scheitert die Eingabe dann zum Beispiel am unbekannten Format.
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-Dictionary, das dem Widget entspricht.
Widgets können wir u.a. nutzen, in dem wir der Meta-Klasse des EventForms 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 oder der Safari im Iphone.
widgets = {
"date": forms.DateInput(
format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
),
}
Im Firefox sieht das Date-Widget dann so aus:
Es können natürlich auch eigene, speziellere Widgets entwickelt werden, was über den Scope dieses Buches aber deutlich hinausgeht.
Mehr zu Widgets finden sich hier: https://docs.djangoproject.com/en/stable/forms/widgets/
An dieser Stelle führen wir noch eine Feld-Validierung durch: wir prüfen, ob der
Sub-Title des Events mit einem Sternchen oder Doppelpunkt beginnt und verhindern dies, in dem wir
einen ValidationenError
erheben, falls dies passiert.
Als Methode der EventForm
- Klasse definieren wir folgende Methode:
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
-. Damit das funktioniert, dürfen wir nicht vergessen, den ValidationenError
aus django.core.exceptions
zu importieren:
from django.core.exceptions import ValidationError
Wenn wir das Formular jetzt mit einem illegalen Zeichen im Subtitle absenden, wird es an uns zurückgeschickt und das fehlerhafte Feld rot markiert:
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 neben sogenannten eingebauten Validatoren auch eigene
Validatoren zuweisen. Dies passiert in der Datei models.py
. Diese Model-Validatoren
werden auch in der Admin-Oberfläche berücksichtigt. Mehr zu diesem Thema im Kapitel Validierung.
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/stable/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
from .models import Event, Category
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = "__all__"
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("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
Wir haben noch das labels
-Feld überschrieben. Damit lässt sich das Default-Label per Feld überschreiben,
welches Django aus dem Namen des Model-Feldes generiert. Oft sind diese Namen unschön oder passen von der
Sprache nicht:
labels = {
"name": "Name des Events",
"sub_title": "Slogan",
"description": "Kurzbeschreibung",
"date": "Datum des Events",
}
Sehen wir uns das Fomular aktuell nochmal an:
Das Feld min_group
haben wir bewusst nicht überschrieben, um zu zeigen, dass mit den labels nicht jedes
Feld überschrieben werden muss. Man nimmt sich einfach die Felder raus, bei denen man das Label ändern will.
Ein Objekt updaten
Wir wollen dem User jetzt die Möglichkeit geben, Events auch upzudaten. Allerdins sollen nur die Eigentümer eines Events diesen auch updaten / löschen dürfen.
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, um
die Ausgabe entsprechend zu gestalten.
Dazu können wir prüfen, ob ein object-Objekt
im Kontext übergeben wurde.
Wenn das der Fall ist, muss es sich um den Update-Vorgang handeln.
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}
{% if object %}
Edit {{object.name}}
{% else %}
Event eintragen
{% endif %}
{%endblock%}
{% block head %}
{% if object %}
<p>
<a href="{% url 'events:event_detail' object.pk %}">zurück zum Event</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object.name}}</h1>
{% else %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">Event eintragen</h1>
{% endif %}
{%endblock%}
{% block content %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-12 mb-5">
<form method="POST">
{% csrf_token %}
{{form|crispy}}
<div>
{% if object %}
<input class="btn btn-primary" type="submit" value="Jetzt updaten" />
{% else %}
<input class="btn btn-primary" type="submit" value="Jetzt eintragen" />
{% endif %}
</div>
</form>
<div>
</div>
</div>
{% endblock %}
View erstellen für das Updaten eines Events
Unter event_manager/events/views.py
die View anlegen:
from django.contrib.auth.mixins import (
LoginRequiredMixin,
UserPassesTestMixin,
)
class UserIsOwner(UserPassesTestMixin):
def test_func(self):
return self.get_object().author == self.request.user
class EventUpdateView(UserIsOwner, UpdateView):
"""
update an event
events/event/3/update
"""
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 zum einen von der UpdateView (welches wir vorhin schon importiert hatten) und
zum anderen von dem UserIsOwner
-Mixin, welches wir extra erstellt hatten.
Dieses Mixin prüft anhand einer Vergleichsfunktion test_func
, ob die
Bedinung wahr oder falsch ist. Trifft die Bedinung nicht zu, wird ein
403 - Forbidden
-Fehler ausgelöst. Hier prüfen wir also, ob der
aktuell eingeloggte User (self.request.user
) der Autor des Events ist
(self.get_object().author
).
Nicht upgedaten werden können die Felder category und author
, wie wir das in der Formklasse schon definiert hatten.
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
Wir nutzen also für das Eintragen und das Editieren des Event-Model das gleiche
Template. Deshalb mussten wir im Template prüfen, ob das Objekt vorhanden ist.
Es spräche nichts dagegen, für jede dieser Operationen ein eigenes Template
anzulegen, zum Beisiel add_event"."html
und update_event.html
.
Um diese Templates in der View bekannt zu machen, müsste man die Variable
template_name
definieren.
class EventUpdateView(LoginRequiredMixin, UpdateView):
model = Event
form_class = EventForm
template_name = "events/update_event.html"
Das machen wir allerdings nicht, sondern wir nutzen die Variante, die vorhin schon gezeigt wurde.
Die URL
Jetzt benötien wir noch die Url, die wir unter event_manager/events/urls.py
eintragen:
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("", views.EventListView.as_view(), name="events"),
path("event/<int:pk>", 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/<int:pk>/update",
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
nehmen die Comments raus für das Editieren des Events raus:
also von:
{% comment %}
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
{% endcomment %}
nach
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
{% comment %}
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
{% endcomment %} <= rausnehmen!
Der Link zum Löschen eines Events ist noch auskommentiert, da wir diese Aktion noch nicht implementiert haben.
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' %}
{% block title %}
{{object}}
{%endblock%}
{% block head %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht aller Events</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object}}</h1>
<h3 class="display-6 lh-3">{{object.sub_title}}</h3>
<p>
am <b>{{object.date}} Uhr</b>, eingestellt von {{object.author}}
in <a href="{% url 'events:category_detail' object.category.pk %}">
{{object.category}}</a>
</p>
{% if user.is_authenticated and user == object.author %}
<span style="font-size:12px;">
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
{% comment %}
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
{% endcomment %}
</span>
{% endif %}
{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<h3>zusätzliche Infos</h3>
<ul>
<li>eingestellt am: {{object.created_at}}</li>
<li>eingestellt von: {{object.author}}</li>
<li><b>findet statt am (deutsche Zeit): {{object.date}}</b></li>
<li>Min Gruppengröße: {{object.get_min_group_display}}</li>
</ul>
{% if object.related_events.exists %}
<h3>Ähnliche Events</h3>
<ul>
{% for related in object.related_events %}
<li><a href="{{related.get_absolute_url}}">{{related.name}}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{%endblock%}
Hinweis: Wenn wir jetzt auf eine Event-Detailseite eines existierenden Events navigieren, zum Beispiel nach http://127.0.0.1:8000/events/event/39 und der aktuell eingeloggte User dem Autor des Events entspricht, sollte der Edit-Link zu sehen sein:
Ein Objekt löschen
Auch zum Löschen eines Events nutzen wir eine klassenbasierte, generische View. Der Mechanismus ist derartig, dass wir vor dem eigentlichen Löschen eine Bestätigungsseite angezeigt bekommen, ob wir das Objekt auch wirklich löschen wollen.
Zwei Dinge sind hier wichtig zu verstehen: erstens muss das eigentliche Löschen per POST abgesendet werden, dh. als Formular. Zweitens müssen wir ein Template anlegen, auf welchem das Bestätigungsformular zu finden sein wird.
URL
Kümmern wir uns zuerst um die URLs. Unter event_manager/events/urls.py
fügen wir einen neuen Eintrag zu den urlpatterns
hinzu:
path(
"event/<int:pk>/delete",
views.EventDeleteView.as_view(),
name="event_delete",
),
Das heisst, ein Aufruf auf die URL
http://127.0.0.1:8000/events/event/3/delete
bringt uns vor dem eigentlichen
Löschen erstmal auf die Besätigungsseite.
View
In event_manager/events/views.py
legen wir nun eine DeleteView
an.
Unsere EventDeleteView
erbt von DeleteView
und überschreiben wieder die
form_valid
-Methode, da wir nur das Löschen von Events nur den Usern selbst
überlassen wollen. Hier prüfen wir, ob ob der User auch der Autor ist:
from django.urls import reverse
class EventDeleteView(UserIsOwern, DeleteView):
# default Templates: company_confirm_delete
model = Event
def get_success_url(self) -> str:
return reverse("events:events")
Hier legen wir auch die success_url fest, die wir nach erfolgreichem Löschen
ansteuern wollen. Dazu nutzen wir aber die Methode get_success_url
, die wir
überschreiben. Wir hätten es aber genausogut so schreiben können:
from django.urls import reverse_lazy
class EventDeleteView(DeleteView):
model = Event
success_url = reverse_lazy("events:events")
Zur Einnerung: immer, wenn wir bei klassenbasierten Views die reverse-Funktion
nutzen wollen, müssen wir die lazy-Variante reverse_lazy
nutzen!
Der Event ist ab jetzt 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.
Wir können nun noch den Link zum Löschen des Events auf der Event-Detailseite
in event_manager/events/templates/events/event_detail.html
auskommentieren:
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
Beim erneuten Aufruf einer Event-Detailseite sollte jetzt auch der Link zum Löschen Des Events sichtbar sein. Zumindest, wenn der eingeloggte User dem Author des Events entspricht:
Aber vorsicht, uns fehlt uns noch die Bestätigungsseite, auf der der User aufgerufen wird, das Löschen des Objekts zu bestätigen. Es kann an dieser Stelle immer noch entscheiden, ob er das vielleicht doch nicht tun will.
Template
Unter event_manager/events/templates/events/event_confirm_delete.html
legen wir das Template an.
Die DeleteView hat als Default-Templatenamen immer das Schema:
<MODELNAME_LOWERCASE>_confirm_delete.html
.
{% extends 'base.html' %}
{% block head %}
<h1 class="display-6 fw-bold lh-1">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
Das Template unter event_manager/events/templates/events/event_detail.html
sieht jetzt so aus:
{% extends 'base.html' %}
{% block title %}
{{object}}
{%endblock%}
{% block head %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht aller Events</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object}}</h1>
<h3 class="display-6 lh-3">{{object.sub_title}}</h3>
<p>
am <b>{{object.date}} Uhr</b>, eingestellt von {{object.author}}
in <a href="{% url 'events:category_detail' object.category.pk %}">
{{object.category}}</a>
</p>
{% if user.is_authenticated and user == object.author %}
<span style="font-size:12px;">
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
</span>
{% endif %}
{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<section class="mb-5 mt-5">
<h3>zusätzliche Infos</h3>
<ul>
<li>eingestellt am: {{object.created_at}}</li>
<li>eingestellt von: {{object.author}}</li>
<li><b>findet statt am (deutsche Zeit): {{object.date}}</b></li>
<li>Min Gruppengröße: {{object.get_min_group_display}}</li>
</ul>
</section>
{% if object.related_events.exists %}
<section class="mb-5 mt-5">
<h3>Ähnliche Events</h3>
<ul>
{% for related in object.related_events %}
<li><a href="{{related.get_absolute_url}}">{{related.name}}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{%endblock%}