.. _working_with_forms: .. index:: single: Formular single: Eintragen single: Update single: Formclass single: is_valid single: Validierung single: Verlinken single: Crispy Forms single: Bootstrap 5 single: Eintragen`` einen Event via Formualar hinzufügen ************************************ Wir wollen -eingeloggten- Usern ermöglichen, Events via einem Formular einzutragen. Da wir zur Zeit noch keinen Authentifizierungsmechanismus implementiert haben, loggen wir uns über die Django-Adminoberfläche ein. Wir sind damit ordentlich authentifziert und können dann Events eintragen. Später werden wir noch einen Mechanismus einrichten, damit sich auch andere User einloggen können. Wir wollen jetzt folgende ``klassenbasierte`` Views (class based views) für die Events anlegen: * Event eintragen * eingetragenen Event updaten Der Plan ============================= Wir wollen folgendes bewerkstelligen: Wenn ein User auf eine Kategorie-Detailseite, zb. unter ``http://127.0.0.1/events/category/sport`` navigiert, soll er dort einen Button vorfinden, auf dem er einen Event für diese Kategorie eintragen kann. Ein Formular entwickeln ============================= Folgende Dinge sind nötig, um ein Formular zu implementieren: * eine Formular-Klasse, die von ``ModelForm`` erbt * die URL, die auf das Formular führt * eine View, die das Formular anzeigt * ein Template, welches das Formular anzeigt Formular-Klasse ----------------------------------------------------- Für jedes Formular, dass wir darstellen wollen, benötigen wir eine entsprechende Klasse. Dazu legen wir eine Datei ``event_manager/events/forms.py`` an und befüllen sie mit folgendem Inhalt: .. literalinclude:: ../../../src/events/event_form_class_1.py Die Klasse ``EventForm`` erbt von ``forms.ModelForm``. Django erstellt also aus dem Model ein entsprechendes Formular. Zusätzlich importieren wir auch noch ``from django.core.exceptions import ValidationError``, damit wir das Formular auch Validieren können. Formular-View ----------------------------------------------------- Um das Formular anzeigen zu können, müssen wir unter ``event_manager/events/views.py`` eine View entwickeln. Dazu nutzen wir wieder eine generische klassenbasierte View: Wir importieren folgende generische Views sowie unsere eben erstelle Formularklasse: .. code-block:: python from django.views.generic.edit import CreateView, DeleteView, UpdateView from .forms import EventForm Oldschool funktionsbasierte Formularview ----------------------------------------------------- Bevor wir uns den moderneren klassenbasierten Aufbau ansehen, gucken wir uns die etwas ältere Methode an, die immer noch genutzt werden kann und bei komplexeren Prozessen der klassenbasierten View auch vorzuziehen ist. .. code-block:: python def create_event(request, category_id): """ /event/add/ """ cat = get_object_or_404(Category, pk=category_id) if request.method == "POST": form = EventForm(request.POST or None) if form.is_valid(): event = form.save(commit=False) event.category = cat event.author = request.user event.save() return redirect(event.get_absolute_url()) else: form = EventForm() return render( request, "events/event_add.html", {"form": form}, ) Wir erstellen eine Funktion ``create_event`` die als Parameter das Request-Objekt und die Kategorie-ID erwartet. Wir fragen zuerst die Kategorie ab (``cat = get_object_or_404(Category, pk=category_id)``) und prüfen dann die HTTP-Methode. Wenn die HTTP-Methode NICHT ``POST`` ist, also das Formular noch NICHT abgesendet wurde, erstellen wir eine Instanz des Formulars und rendern dieses in das Template. Ansonsten erstellen wir eine Instanz des Formulars und füllen es mit den Formulardaten, prüfen dann mit ``form.is_valid()`` ob es valide ist und speichern es mit Kategorie und Autor ab. Dann führen wir einen ``Redirect`` auf die absolute URL des Eventobjects aus (sprich, besuchen danach die Detailseite des neu angelegten Objektes). ``event = form.save(commit=False)`` erstellt ein Objekt auf Basis des gefüllten Formulars, ``commit=False`` zeigt aber an, dass es nicht gespeichert werden soll. Denn bevor wir es speichern, müssen wir noch die Kategorie (die aus der URL extrahiert wird) und den Autor, der ja der eingeloggte User ist, angeben. Etwas einfacher geht es mit der ``CreateView``. Die Create-View ----------------------------------------------------- Gucken wir uns die CreateView mal genauer an: `_ 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 class EventCreateView(LoginRequiredMixin, CreateView): """ create an event for certain category events/event/create/3 """ model = Event form_class = EventForm def form_valid(self, form): form.instance.category = self.category form.instance.author = self.request.user return super(EventCreateView, self).form_valid(form) def get_initial(self): self.category = get_object_or_404( Category, pk=self.kwargs["category_id"] ) Wir erben also von CreateView und nutzen wieder einen Mixin, diesemal den ``LoginRequiredMixin``. Der User muss also eingeloggt sein, um diese View zu sehen. Wir definieren das Model ``model=Event`` und die Formklasse mit ``form_class=EventForm``. Dazu implementieren wir noch zwei Methoden: ``get_initial`` wird immer zuerst aufgerufen, wenn diese View aufgerufen wird. In ihr prüfen wir, ob die übergebene Kategorie überhaupt existiert und lösen einen ``404-Fehler`` aus, falls dies nicht der Fall sein sollte. Zudem implementieren wir ``form_valid``, die aufgerufen wird, wenn das Formular abgesendet wird. In ihr weisen wir der Form-Instanz die Kategorie sowie den -eingeloggten user- als Author zu. .. 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. https://docs.djangoproject.com/en/4.0/topics/class-based-views/mixins/ Das Template für das Formular ============================= Fehlt noch das Template für das Formular: Wir legen unter folgende Datei an ``event_manager/events/templates/events/event_form.html`` und fügen folgenden Code hinzu: .. literalinclude:: ../../../src/events/event_form_template_1.html In ``{{form}}`` wird das Formular gerendert, der ``{% csrf_token %}`` dient der Sicherheit. Mehr zum CSRF-Token hier: ``_ 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.list_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: ``http://127.0.0.1:8000/events/event/create/42``. Verlinkung von der Kategorie-Detailseite aus ----------------------------------------------------- Wir wollen die Möglichkeit schaffen, von der Kategorie-Detailseite aus einen Event einzustellen. Dazu öffnen wir das Template für die Kategorie-Detailseite unter ``event_manager/events/templates/events/category_detail.html`` und fügen folgenden Code ein: .. code-block:: html+django {% extends 'base.html' %} {% load i18n %} {% block title %} Kategorie {{category.name}} {%endblock%} {% block head %} Kategorie {{category.name}} {%endblock%} {% block content %}

{{category.description}}

created at: {{category.created_at}}

neues Event eintragen

{%endblock%} Wenn wir jetzt nach ``http://127.0.0.1:8000/events/category/sport`` navigieren, kommen wir über den Link unten auf das Formular. Crispy Forms ============================= Um den Look des Formulars zu verbessern, installieren wir mit aktiviertem Environment via pip das Modul ``Crispy forms`` bzw. das Crispy-Bootstrap5-Paket. Dieses benötigen wir, um mit ``Bootstrap5`` zu arbeiten. .. code-block:: bash $ (eventenv) pip install django-crispy-forms $ (eventenv) pip install crispy-bootstrap5 und fügen es unter ``settings.py`` den installierten Apps hinzu: .. code-block:: python INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "crispy_bootstrap5", "crispy_forms", ] # ebenfall in die settings.py CRISPY_TEMPLATE_PACK = "bootstrap5" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" https://django-crispy-forms.readthedocs.io/en/latest/install.html jetzt ändern wir das Template ``event_manager/events/templates/events/event_form.html`` wie folgt ab: .. literalinclude:: ../../../src/events/event_form_template_2.html Das Formular sollte jetzt deutlich schöner aussehen. Das Formular weiter verbessern ================================ Das Formular hat diverse Problembereich: zuerst befinden sich einige Felder in ihm, die wir nicht darstellen wollen, zum Beispiel die Auswahl der Kategorie, denn diese Auswahl haben wir schon getroffen. Wir wollen auch nicht, dass der Autor auswählbar ist, denn dies wäre eine Sicherheitslücke. Zudem wollen wir verhindern, dass der Slug manuell eingegeben werden kann. Felder exkludieren ----------------------- Um zu verhindern, dass Model-Felder in Formularen dargestellt werden, nutzen wir das Attribut ``exclude``. In unserem Fall wollen wir aus dem Event-Formular die Felder ``slug``, ``category`` und ``author`` entfernen. Der Slug wird automatisch beim Speichern des Formulars erstellt, und die Kategorie bzw. den Author fügen wir selbständig hinzu. Der User soll ja nicht auswählen können, wer den Event eingetragen hat. .. code-block:: python class EventForm(forms.ModelForm): class Meta: model = Event fields = "__all__" exclude = ("slug", "category", "author") Ein Problem ist jetzt noch die Eingabe des Datums. Um diese zu optimieren, werden wir ein Widget verwenden, um ein Kalenderwidget statt eines einfachen Textfeldes zu erzeugen. Widgets ------------ ein ``Widget`` ist Djangos Darstellung eines HTML-Eingabeelements. Das Widget handhabt das Rendern des HTML und das Extrahieren von Daten aus einem GET/POST-Wörterbuch, das dem Widget entspricht. Widgets können wir u.a. nutzen, in dem wir der Form-Klasse das ``widgets-dictionary`` für die Felder definieren, die wir überschreiben wollen. Wir wollen für das Feld ``date`` statt eines Textfeldes eine ``DateInput``-Komponente erzeugen. Diese Datums-Komponente ist browserspezifisch, Firefox stellt den Kalender also anders da als Google Chrome. .. code-block:: python widgets = { "date": forms.DateInput( format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"} ), } Es können natürlich auch eigene, speziellere Widgets entwickelt werden, was über den Scope dieses Buches hinausgeht. Mehr zu Widgets finden sich hier: `_ An dieser Stelle führen wir noch eine Validierung durch: wir prüfen, ob der Sub-Title des Events mit Sonderzeichen beginnt und verhindern dies, in dem wir das einen ``ValidationenError`` erheben, falls dies passiert. .. 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``-. .. 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 (auch eigene) Validatoren zuweisen. Dies passiert in der Datei ``models.py``. Diese Model-Validatoren werden auch in der Admin-Oberfläche berücksichtigt. **Auf Formular-Ebene** Wir können direkt in der Form-Klasse Felder validieren. Dazu nutzen wir entweder die Methode ``clean_`` 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: .. literalinclude:: ../../../src/events/event_form_class_2.py Ein Objekt updaten ============================= Wir wollen dem User jetzt die Möglichkeit geben, Events auch upzudaten. * nur die Eigentümer eines Events dürfen updaten / löschen Template erstellen für Update Event ------------------------------------------------------------ Für das Update-Formular nutzen wir ebenfalls das schon bei ``EventCreate`` genutzt ``event_form.html``. Dazu müssen wir im Template allerdings unterscheiden, ob wir gerade die Create-View aufrufen oder die Update-View. Dazu können wir prüfen, ob ein ``object-Objekt`` im Kontext übergeben wurde. .. literalinclude:: ../../../src/events/event_form_template_3.html View erstellen für das Updaten eines Events ------------------------------------------------------------ Unter ``event_manager/events/views.py`` die View anlegen: .. code-block:: python from django.core.exceptions import PermissionDenied class EventUpdateView(LoginRequiredMixin, UpdateView): """ update an event events/event/update/3 """ model = Event form_class = EventForm def form_valid(self, form): if form.instance.author != self.request.user: raise PermissionDenied(_("Zugriff ist leider nicht erlaubt")) return super(EventUpdateView, self).form_valid(form) Wir erben also von UpdateView (welches wir vorhin schon importiert hatten) und importieren auch noch die ``PermissionDenied``-Exception aus den ``django.core.exceptions``. Wieder nutzen wir den ``LoginRequiredMixin``. Der User muss also eingeloggt sein, um das Event-Model updaten zu dürfen. Nicht upgedaten werden können die Felder ``category, author und slug``, wie wir das in der Formklasse schon definiert hatten. In der ``form_valid``-Methode prüfen wir, ob der Autor des Events auch dem User entspricht. Falls das nicht der Fall sein sollte, erheben wir eine ``PermissionDenied``-Exception, die in einem ``403 Forbidden``-Error für den User resultiert. .. 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 Die URL ------------------------------------ Wir fügen jetzt die URL in die ``event_manager/events/urls.py`` ein: .. code-block:: python urlpatterns = [ path("hello_world", views.hello_world, name="hello_world"), path("categories", views.list_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 fügen folgenden Code hinzu: .. code-block:: html+django {% if user.is_authenticated and user == object.author %}

Event editieren

{% endif %} Wir prüfen also, ob der User eingeloggt ist und wenn ja, ob er auch Autor des Events ist. Nur dann darf er das Formular auch editieren und nur dann wir der Link dazu angezeigt. Das Template unter ``event_manager/events/templates/events/event_detail.html`` sieht jetzt so aus: .. literalinclude:: ../../../src/events/event_detail_2.html Ein Objekt löschen ============================= URL ------- .. code-block:: python path( "event/delete/", views.EventDeleteView.as_view(), name="event_delete", ), View ------ .. code-block:: python class EventDeleteView(DeleteView): # default Templates: company_confirm_delete model = Event def form_valid(self, form): if form.instance.author != self.request.user: raise PermissionDenied(_("Zugriff ist leider nicht erlaubt")) return super(EventUpdateView, self).form_valid(form) def get_success_url(self) -> str: return reverse("events:events") Event ist nur löschbar, wenn der eingeloggte User auch der Autor ist. Nach erfolgreichem Löschen wird auf ``http://127.0.0.1/events/events`` weitergeleitet. Template ----------- Unter ``event_manager/events/templates/events/event_confirm_delete.html`` legen wir das Template an. .. 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 %} Event-Detailseite --------------------- und modifizieren die Event-Detailsieite .. code-block:: bash {% if user.is_authenticated and user == object.author %}

Event editieren
Event löschen

{% endif %} Das Template unter ``event_manager/events/templates/events/event_detail.html`` sieht jetzt so aus: .. literalinclude:: ../../../src/events/event_detail_2.html