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:

../_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:

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 geleitet

  • template_name_suffix: Der Default Suffix ist _form, also event_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:

../_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:

{% 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.

../_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:

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:

../_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.

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:

../_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.

../_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.

widgets = {
        "date": forms.DateInput(
            format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
        ),
    }

Im Firefox sieht das Date-Widget dann so aus:

../_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: 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:

../_images/event_form_3.png

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:

../_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.

{% 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 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.

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:

../_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:

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:

../_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:

<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 %}
../_images/event_detailseite_2_confirm_delete.png

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%}