.. _create_catogeries: .. index:: single: Kategorieformular single: funktionsbasierte View Formulare für die Kategorie anlegen ************************************ Bisher haben wir zwei funktionsbasierte Views für das Kategorie-Model angelegt: ``categories()`` für das Auflisten der Kategorien und ``category_detail()`` für das Anzeigen der Kategorie-Detailseite. Wir wollen jetzt in der Lage sein, neue Kategorien einzutragen und bestehende zu editieren. Dazu benötigen wir eine Formularklasse, und jeweils URLs, Templates und Views zum Anlegen und Editieren. Das Formular ============================= HTML-Formulare zu entwickeln ist eine aufwendige und sorgfältig testbare Angelegenheit. Es sei denn, man arbeitet mit Django. Django bietet out of the box einfache Möglichkeiten, aus Modellen entsprechende Formulare zu generieren. Um in Django eine Formularmöglichkeit zu implementieren, sind mindestens diese 4 folgenden Schritte notwendig: * eine Formular-Klasse, die von ``ModelForm`` erbt * eine View, die das Formular anzeigt * die URL, die auf die View führt, die das Formular anzeigt * ein Template, welches das Formular anzeigt Die Klasse forms.ModelForm ------------------------------ Dazu legen wir eine Datei ``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_0.py :language: python Die Klasse ``CategoryForm`` erbt von ``forms.ModelForm``. Django erstellt also aus dem Model ein entsprechendes Formular. Deshalb importieren wir die beiden Models auch. Um das zu bewerkstelligen, definieren wir über die ``model`` Eigenschaft der Meta-Klasse das Model, auf dessen Basis das Formular erstellt werden soll: .. code-block:: python class Meta: model = Category fields = "__all__" In der Meta-Klasse werden wir später noch einige andere definieren, wie wir sehen werden. Die ``fields``- Angabe hier spezifiziert zum Beispiel die Model-Felder, die wir im Formular später sehen wollen. ``__all__`` steht offensichtlich für alle Felder, wir könnten aber auch Felder in einem Tupel angeben: .. code-block:: python class Meta: model = Category fields = "name", "sub_title" **Hier eine kleine Warnung:** Wenn wir im Model Felder als mandatory festgelegt hatten, und wir diese Felder im Formular ignorieren, werden wir früher oder später in ein Problem laufen. Wir müssen also vor dem Speichern den Feldern irgendeinen Wert zuweisen. Wie das geht, werden wir später noch sehen, wenn wir einen Event mit einem eingeloggten Autor anlegen. Default Rendering der Modelfelder ------------------------------------- Wenn das Formular im Template gerendert wird, sind das die Default-HTML-Elemente der wichtigsten Model-Felder: .. csv-table:: default Darstellung von Model-Feldern in HTML-Formularen :widths: 40, 40 ``CharField``, Text-Inputfeld ``CharField mit choices``, Select-Box ``Integerfield``, Number-Inputfeld ``Integerfield mit choices``, Select-Box ``DateField``, Text-Inputfeld ``TextField``, Text-Area ``ForeignKey``, Select-Box ``BooleanField``, Check-Box **Eine Übersicht aller Model- und Formfelder findet sich hier:** ``_ Wir können für jedes Feld auch speziell festlegen, in welcher Form das Feld gerendert werden soll. Dieses Verhalten wird in den ``widgets``-Eigenschaft der ``Meta``- Klasse festgelegt. Mehr zu Widgets später. View zum Anlegen einer Kategorie ================================== Wir benötigen zum Anlegen der Kategorie jetzt natürlich eine View, die wir ``category_create`` nennen. Die View wird sowohl per ``GET`` über einen Link sowie nach dem Absenden des Formulars per ``POST`` aufgerufen. Auf diese beiden Ereignisse müssen wir in der View reagieren: Öffnen wir nun ``event_manager/events/views.py`` und fügen folgende funktionale View ein: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/views/category_add_0.py :language: python Zuerst importieren wir uns unsere neu erstellte Formularklasse und die redirect-Funktion in die ``views.py`` .. code-block:: python from django.shortcuts import get_object_or_404, render, reverse, redirect from .models import Event, Category from .forms import CategoryForm Die View reagiert wie gesagt auf zwei Ereignisse: wenn die HTTP-Request-Methode ``POST`` ist, wird eine neue Formular-Instanz mit den aus dem Formular gesendeten POST-Daten erstellt. .. code-block:: python if request.method == "POST": form = CategoryForm(request.POST) if form.is_valid(): category = form.save() return ( redirect("events:category_detail", kwargs={"id": self.category.id}), ) Wir prüfen mit ``form.is_valid()``, ob die Formulardaten auch valide sind, und speichern das Formluar dann ab. Dabei wird uns als Rückgabewert von ``save()`` auch gleich eine neue Category-Instanz geliefert, die wir für einen Redirect auf die entsprechende Kategorie-Detailseite nutzen. Dazu erwartet die Funktion ``redirect`` den Namen der Route, wie wir das schon im Template gesehen hatten, und zum Auflösen der Route auch noch das nötige Argument (siehe Verlinkung mit dem URL-TAG). Im anderen Fall, also wenn die Methode ``GET`` war, wird einfach ein leeres Formular erstellt. .. code-block:: python form = CategoryForm() Zuletzt geben wir der ``render`` - Funktion wie gewohnt noch das Request-Objekt, den Pfad zum (noch nicht existenten) ``category_create``-Template und den Kontext, der in diesem Fall aus dem Formular besteht. .. code-block:: python return render( request, "events/category_create.html", {"form": form}, ) .. admonition:: URL-Auflösung Wie wir gesehen hatten, können wir der Funktion redirect (und auch reverse) einen String wie "events:category_detail" übergeben, der dann wie gewohnt nach ``app_name`` und ``path_name`` aufgelöst wird. So ähnlich hatten wir das ja schon im Template mit dem url-Tag gesehen. **Mehr zum Thema Url-Resolving hier:** ``_ Die nötigen URLs ============================= Wir benötigen eine URL für das Erstellen einer Kategorie. Unten auf der Kategorie-Übersicht soll sich ein Link befinden, der den User auffordert, eine neue Kategorie anzulegen. Dieser führt dann auf unser neues Formular. Dazu öffnen wir ``event_manager/events/urls.py`` und verändern die urlpatterns: .. 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"), # diese Zeile hinzufügen: path("category/create", views.category_create, name="category_create"), ] Der neue Eintrag hat den Namen ``category_create`` und referenziert auf die ``category_create`` - Funktion in den ``views.py``. Das Template ============================= Das Template erstellen wir unter ``event_manager/events/templates/events`` und legen dort die Datei ``category_create.html`` an: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/category_form_template_0.html :language: html+django Wir legen ein HTML-Formular mit der HTTP-Methode POST an, wie das in Formularen üblich ist. Innerhalb des Formulars definieren wir einen HTML-Table und einen Submit-Button. In ``{{form}}`` wird das Formular in einer altmodische Tabellenstruktur gerendert, was heute aus Grund aus responsivem Design mehrheitlich nicht mehr zeitgemäß ist. Das werden wir später ändern. Der ``{% csrf_token %}`` rendert ein unsichtbares Hidden-Feld mit einem Einmal-Token, der von Django in der CSRF-Middleware geprüft wird und uns vor Cross-Site-Request-Forgery Attacken schützen soll. Ist der Token falsch, werden die Formulardaten als potientiell unsicher eingestuft und der Request wird abgelehnt. Mehr zum CSRF-Token hier: ``_ .. admonition:: Was ist ein CSRF? Cross-Site Request Forgery (CSRF) ist ein Angriff, der einen Endbenutzer dazu zwingt, unerwünschte Aktionen in einer Webanwendung auszuführen, bei der er gerade authentifiziert ist. Mit von Social Engineering (z. B. durch das Versenden eines Links per E-Mail oder Chat) kann ein Angreifer den Benutzer einer Webanwendung dazu bringen, Aktionen nach Wahl des Angreifers auszuführen. Das Formular verlinken ============================= Wir sind jetzt fast fertig. Nun müssen wir nur noch die Vormularseite verlinken. Dazu soll es einen Link auf der Kategorie-Übersichtsseite geben, der dies ermöglicht. Dazu öffnen wir ``event_manager/events/templates/events/categories.html`` und fügen in das Template die Verlinkung ein. .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/categories_2.html :language: html+django Hier sind ein ein paar Dinge passiert: zuerst einmal haben wir ein paar Bootstrap-spezifische Styles eingefügt, wie zum Beispiel die h1-Überschrift. Dann rufen wir eine Methode des Category-Objekts auf, die wir noch gar nicht kennen: ``num_of_events``. Diese Methode liefert uns für jede Kategorie die Anzahl an Events, die ihr zugeordnet sind, und zeigt sie in einem blauen Bootstrap-Pillow an. Interessant ist an dieser Stelle, dass der Methodenaufruf ohne die runden Klammern erfolgt, wie das sonst üblich ist. Damit dieser Code funktioniert, müssen wir noch die Methode in das Kategorie-Model einbauen. Wir öffen ``event_manager/events/models.py`` und fügen unterhalb der String-Methode die Methode ``num_of_events`` hinzu. .. code-block:: python class Category(DateMixin): """Eine Kategorie für einen Event.""" name = models.CharField(max_length=100, unique=True) sub_title = models.CharField(max_length=200, null=True, blank=True) description = models.TextField(null=True, blank=True) class Meta: ordering = ["name"] def __str__(self): return self.name def num_of_events(self): """Die Anzahl der Events einer Kategorie.""" return self.events.count() .. image:: /images/category_overview_mit_link.png Wenn wir den Entwicklungsserver starten und nach ``_ navigieren, sollte unsere Kategorieübersicht so aussehen: .. image:: /images/category_add_0.png Das Formular testen und absenden ---------------------------------- Wir sollten das Formular jetzt ausfüllen und absenden können. Nach erfolgreichem Absenden werden wir auf die Detailseite der neu angelegten Kategorie geleitet. Dort gibt es auch eine Verlinkung zur Übersicht, die wir vorhin angelegt hatten. .. image:: /images/category_add_1_angelegt.png Eine Kategorie updaten ============================= Bisher sind wir noch nicht in der Lage, eine Kategorie zu editieren. Das wollen wir nachholen, und ein Update-Formular anlegen. Der Prozess ist im Grunde der gleiche, wie beim Anlegen. Wir benötigen wieder eine **View**, die **URLs**, und ein **Template**. Da es sich bei unserem Formular um ein ``ModelForm`` handelt, welches das Kategorie-Model abbildet, benötigen wir für die Update-Methode nicht extra noch eine eigene Form-Klasse. URLs für das Editieren einer Kategorie ----------------------------------------- Dazu öffnen wir wieder die ``event_manager/events/urls.py``: .. 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("category/create", views.category_create, name="category_create"), # diese Zeile hinzufügen: path("category//update", views.category_update, name="category_update" ), ] Der neue Eintrag hat den Namen ``category_update`` und referenziert auf die ``category_update``- Funktion in den ``views.py``, die wir jetzt gleich anlegen werden. Die URL match zum Beispiel auf diese Anfrage: ``_ Noch kommt es hier allerdings noch zu einem Fehler, da die View und das Template noch nicht implementiert sind. Die Kategorie-Update View ----------------------------------------- Legen wir die View jetzt an. Unter ``event_manager/events/views.py`` tragen wir die neue View ein: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/views/category_update_0.py :language: python Zuerst holen wir uns anhand des Primary Keys ``pk`` die ``Kategorie-Instanz,`` die geändert werden soll, aus der Datenbank und lösen einen 404-Fehler aus, falls das Objekt nicht (mehr) vorhanden sein sollte. In einem nächsten Schritt erstellen wir ein neues ``form`` auf Basis der geladenen Kategorie. Wurde die View per HTTP-POST angesprochen, füllen wir das Formular mit den Werten aus dem HTML-Formular. Wenn der Formularinhalt ``valide`` ist, speichern wir das Objekt in der Datenbank ab und führen einen Redirect auf die Detailseite der Kategorie aus. Andernfalls leiten wir im Fehlerfall auf das Formular zurück, damit der User seine Eingabedaten eingeben oder überprüfen kann. Verlinkung von der Kategorie-Detailseite ----------------------------------------- Wir wollen auf der Detailseite der Kategorie einen Link setzen, der uns die Möglichkeit gibt, die Kategorie zu editieren. Der Link soll allderings nur sichtbar sein, wenn der User am System eingeloggt ist. Dazu nutzen wir im Template den ``if-Tag`` und lesen das ``user``-Objekt aus. Das User-Objekt steht uns in jedem Template pauschal zur Verfügung. **Hinweis**: Einen Link zu verbergen ist keine Sicherheitsmaßnahme, sondern in diesem Fall nur ein erster Schritt in Richtung Erhöhung der Benutzerfreundlichkeit. Die schreibenden Zugriff auf die Kategorie und die Events sollen später nur noch für eingeloggte User funktionieren. Dazu öffen wir das Template der Kategorie-Detailseite unter ``event_manager/events/templates/events/category_detail.html`` und fügen einen Link ein zum Editieren der Kategorie ein: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/category_detail_3.html :language: python Nebenbei haben wir noch etwas am Style geändert, dass der Look ein bisschen besser wird. Wenn wir nun die Detailseite einer Kategorie besuchen, finden wir einen Link, der uns auf das Updaten der Kategorie führt. Dazu müssen wir allerdings eingeloggt sein, sonst sieht man den Link nicht. .. image:: /images/category_detail_2.png Das Template für die Update-View ----------------------------------------- Last but not least muss das eigentliche Formular gerendert werden. Dazu legen wir ein Template unter ``event_manager/events/templates/events/category_update.html`` an und fügen den HTML-Code hinzu: .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/category_update_0.html :language: html+django Das Template stellt das Formular wie schon vorher in einer Tabelle da und bietet einen Submit-Button zum Absenden. .. image:: /images/category_update_1.png Damit sind wir in diesem Kapitel fertig. Wir haben bis auf das Löschen einer Kategorie alle anderen ``CRUD-Operationen`` umgesetzt. Wer Lust hat, kann gerne noch ein wenig am Layout rumspielen, bevor es mit den Views für die Events weitergeht. .. image:: /images/category_update_2.png