.. _working_with_forms: .. index:: single: Formular single: Eintragen single: Update single: Formclass single: is_valid single: Validierung single: Crispy Forms single: Bootstrap 5 single: Eintragen 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 ``_. 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: .. image:: /images/category_detail_3.png 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: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/forms/event_form_class_1.py :language: python 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: .. code-block:: python 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. .. code-block:: python def create_event(request, category_id): """ /event/add/ """ 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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: ``_ 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: .. code-block:: python 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. .. code-block:: python from django.contrib.auth.mixins import LoginRequiredMixin class EventCreateView(LoginRequiredMixin, CreateView): Wir definieren das Model und die benötigte Formularklasse: .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python 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. .. admonition:: 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: ``_ 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:** ``_ 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: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_form_template_0.html :language: html+django 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: .. code-block:: python urlpatterns = [ path("hello_pingus", views.hello_pingus, name="hello_pingus"), path("categories", views.categories, name="categories"), path("category/", views.category_detail, name="category_detail"), path("", views.EventListView.as_view(), name="events"), path("event/", views.EventDetailView.as_view(), name="event_detail"), # diese Zeile hinzufügen: path( "event/create/", views.EventCreateView.as_view(), name="event_create", ), ] Unter folgender URL können wir jetzt einen Event eintragen: ``_. **Wichtig:** Da wir den LoginRequiredMixin genutzt hatten, müssen wir für diese Aktion eingeloggt sein. Wer das aktuell nicht ist, bitte nach ``_ 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: .. image:: /images/event_form_0.png 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: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/category_detail_3b.html :language: python 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. .. image:: /images/category_detail_3.png 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: .. code-block:: shell django==stable.3 # der requirements.in hinzufügen: django-crispy-forms crispy-bootstrap5 Kompilieren und Syncen ... .. code-block:: shell (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: .. code-block:: python 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: .. code-block:: python CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = ("bootstrap5",) **Mehr zu Crispy Forms in der Doku:** ``_ jetzt ändern wir das Template ``event_manager/events/templates/events/event_form.html`` wie folgt ab: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_form_template_1.html :language: html+django 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: .. image:: /images/event_form_1.png 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. .. code-block:: python 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: .. code-block:: python class EventForm(forms.ModelForm): class Meta: model = Event fields = "__all__" exclude = ("category",) Wenn wir die Seite aktualisieren, sind die beiden Felder nicht mehr vorhanden: .. image:: /images/event_form_2.png 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. .. image:: /images/event_form_2_textdatefield.png 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. .. code-block:: python widgets = { "date": forms.DateInput( format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"} ), } Im Firefox sieht das Date-Widget dann so aus: .. image:: /images/event_form_2_date.png 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:** ``_ 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: .. code-block:: python 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: .. code-block:: python 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: .. image:: /images/event_form_3.png .. admonition:: 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_`` 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:** ``_ **Auf View-Ebene** man könnte auch in den Views die Eingabe validieren, davon wird jedoch abgeraten. **Ablauf des Validierungsprozesses** siehe ``_ Usere EventForm Klasse unter ``event_manager/events/forms.py`` sieht jetzt so aus: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/forms/event_form_class_2.py 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: .. code-block:: python labels = { "name": "Name des Events", "sub_title": "Slogan", "description": "Kurzbeschreibung", "date": "Datum des Events", } Sehen wir uns das Fomular aktuell nochmal an: .. image:: /images/event_form_4.png 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. .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_form_template_3.html :language: html+django View erstellen für das Updaten eines Events ------------------------------------------------------------ Unter ``event_manager/events/views.py`` die View anlegen: .. code-block:: python 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. .. admonition:: Eigene Fehlerseiten Für folgende HTTP-Statuscodes lassen sich unter ``event_manager/templates`` Fehlerseiten anlegen: * 404 dazu eine eigene ``404.html`` anlegen * 500 dazu eine eigene ``500.html`` anlegen * 403 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. .. code-block:: python 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: .. code-block:: python urlpatterns = [ path("hello_world", views.hello_world, name="hello_world"), path("categories", views.categories, name="categories"), path("category/", views.category_detail, name="category_detail"), path("", views.EventListView.as_view(), name="events"), path("event/", views.EventDetailView.as_view(), name="event_detail"), path( "event/create/", views.EventCreateView.as_view(), name="event_create" ), # diese Zeile hinzufügen: path( "event//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: .. code-block:: html+django {% comment %} editieren | löschen {% endcomment %} nach .. code-block:: html+django editieren | {% comment %} löschen {% 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: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_detail_2.html :language: html+django **Hinweis:** Wenn wir jetzt auf eine Event-Detailseite eines existierenden Events navigieren, zum Beispiel nach ``_ und der aktuell eingeloggte User dem Autor des Events entspricht, sollte der Edit-Link zu sehen sein: .. image:: /images/event_detailseite_2_edit.png 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: .. code-block:: python path( "event//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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: html+django editieren | löschen 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: .. image:: /images/event_detailseite_2_delete.png 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: ``_confirm_delete.html``. .. code-block:: html+django {% extends 'base.html' %} {% block head %}

Event löschen: {{object.name}}

{% endblock %} {% block content %}
{% csrf_token %}

Willst du das Event sicher löschen? "{{ object }}"?

{% endblock %} .. image:: /images/event_detailseite_2_confirm_delete.png Event-Detailseite --------------------- Das Template unter ``event_manager/events/templates/events/event_detail.html`` sieht jetzt so aus: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_detail_3.html :language: html+django Navigation ============================================ Aktuell ist es noch nicht möglich, die beiden Links in der Navigationsleiste zu bedienen und direkt auf die Events bzw. Kategorien zu kommen. Das wollen wir jetzt nachholen: .. image:: /images/navigation_links_events.png Dazu öffnen wir das base-Template unter ``event_manager/event_manager/templates/base.html`` und verlinken die beiden Links Events und Kategorien (ca. in Zeile 33): vorher: .. code-block:: html+django
  • Events
  • Kategorien
  • nachher: .. code-block:: html+django
  • Events
  • Kategorien
  • Nach einem Serverneustart sollten die beiden Verlinkungen in der oberen Navigationsleiste auf die Events und Kategorie-Übersichtsseiten zeigen. Events der Kategorie anzeigen -------------------------------- Wir sind mit den Views für Events und Kategorien vorerst fast fertig. Das einzige, was wir jetzt noch tun wollen, ist es, auf den Kategorie-Detailseiten die Events anzuzeigen, die dieser Kategorie zugeordnet sind. Dafür müssen wir nichts weiter tun, als im Template der Kategorie-Detailseite unter ``event_manager/events/templates/events/category_detail.html`` die Events über den Related Manager auszugeben: .. code-block:: html+django
      {% for e in category.events.all %}
    • {{e.name}}
    • {% empty %}
    • In dieser Kategorie sind keine Events vorhanden.
    • {% endfor %}
    Das Template für die Kategorie sieht jetzt so aus: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/category_detail_4.html :language: html+django .. image:: /images/category_detail_5.png Falls einer Kategorie keine Events zugeordnet sind, kommt eine entsprechende Meldung: .. image:: /images/category_detail_4.png Dafür hatten wir den ``for-empty`` - Tag im Template genutzt, der uns auf eine einfache Art und Weise ermöglicht, eine Meldung auszugeben, wenn das Queryset leer sein sollte. Möchte man den ganzen Block inklusiver der Überschrift nur anzeigen, wenn auch tatsächlich Events vorhanden sind, müsste man den ganzen Block nochmal in einen ``if`` - Tag packen: .. code-block:: html+django {% if category.events.all.exists %}

    Events aus der Kategorie {{category}}

      {% for e in category.events.all %}
    • {{e.name}}
    • {% empty %}
    • In dieser Kategorie sind keine Events vorhanden.
    • {% endfor %}
    {% endif %} Optional: Das Projekt in der Version v0.3 kopieren ----------------------------------------------------- Wie immer kann man sich den Zwischenstand klonen, falls beim Mitschreiben was schiefgegangen ist. Version v0.3 via github clonen ...................................... .. code-block:: bash git clone -b v0.3 git@github.com:realcaptainsolaris/event_project.git und dann nicht vergessen, ein virtuelles Environment anzulegen und zu migrieren und danach noch ein paar Testdaten zu generieren: .. code-block:: bash python -m venv .envs/eventenv pip install pip-tools pip-sync requirements.txt requirements-dev.txt python manage.py migrate python manage.py createsuperuser python manage.py create_user -n 10 python manage.py create_events --events 20 --categories 5 python manage.py runserver Version v0.3 als zip-Datei runterladen ........................................... ``_ Version v0.3 als tar-Datei runterladen ............................................ ``_