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:

from django import forms
from .models import Category


class CategoryForm(forms.ModelForm):
    class Meta:
        model = Category
        fields = "__all__"

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:

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:

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:

default Darstellung von Model-Feldern in HTML-Formularen

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: https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#field-types

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:

from django.shortcuts import get_object_or_404, render, reverse, redirect
from .forms import CategoryForm
from .models import Event, Category
from .forms import CategoryForm


def category_create(request):
    """Eine View zum Hinzufügen einer Kategorie.

    http://127.0.0.1:8000/events/category/create

    """
    if request.method == "POST":
        form = CategoryForm(request.POST or None)
        if form.is_valid():
            category = form.save()
            return redirect("events:category_detail", pk=category.pk)
    else:
        form = CategoryForm()
    return render(
        request,
        "events/category_create.html",
        {"form": form},
    )

Zuerst importieren wir uns unsere neu erstellte Formularklasse und die redirect-Funktion in die views.py

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.

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.

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.

return render(
        request,
        "events/category_create.html",
        {"form": form},
)

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: https://docs.djangoproject.com/en/stable/ref/urlresolvers/

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:

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"),
    # 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:

{% extends 'base.html' %}

{% block title %}
neue Kategorie hinzufügen
{% endblock %}


{% block head%}

<a href="{% url 'events:categories' %}">zurück zur Übersicht</a><br>

<h1 class="display-6 fw-bold lh-1">neue Kategorie hinzufügen</h1>
{% endblock %}

{% block content %}

    <form method="POST">
    {% csrf_token %}
    <table>
    {{form}}
    <tr>
        <th>
            Formular absenden:
        </th>
        <td>
            <input type="submit" value="Jetzt eintragen" />
        </td>
    </tr>
    </table>
    </form>

{% endblock %}

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: https://docs.djangoproject.com/en/stable/csrf/

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.

{% extends 'base.html' %}

{% block title %}
Übersicht der Kategorien
{% endblock %}

{% block head %}
<h1 class="display-6 fw-bold lh-1">Kategorien</h1>
{% endblock %}

{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">

<ul class="list-group event_box">
{% for category in categories %}

<a href="{% url 'events:category_detail' category.pk %}">
<li class="list-group-item rounded">
    {{category}}
    <span class="badge rounded-pill bg-primary">{{category.num_of_events}}</span>
</li>
</a>
{% endfor %}
</ul>

<p>
<a href="{% url 'events:category_create' %}">neue Kategorie hinzufügen</a>
</p>
</div>
</div>
{% endblock %}

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.

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()
../_images/category_overview_mit_link.png

Wenn wir den Entwicklungsserver starten und nach http://127.0.0.1:8000/events/categories navigieren, sollte unsere Kategorieübersicht so aussehen:

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

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

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("category/create", views.category_create, name="category_create"),
    # diese Zeile hinzufügen:
    path("category/<int:pk>/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: http://127.0.0.1:8000/events/category/3/update

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:

def category_update(request, pk):
    """View zum Ändern einer Kategorie.

    http://127.0.0.1:8000/events/category/7/update
    """
    instance = get_object_or_404(Category, pk=pk)
    form = CategoryForm(request.POST or None, instance=instance)

    if form.is_valid():
        category = form.save()
        return redirect("events:category_detail", pk=category.pk)

    return render(
        request,
        "events/category_update.html",
        {"form": form},
    )

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:

{% 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>
</div>
</div>
{% endblock %}

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.

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

{% extends 'base.html' %}

{% block title %}
Kategorie bearbeiten
{% endblock %}


{% block head%}
<a href="{% url 'events:categories' %}">zurück zur Übersicht</a><br>

<h1 class="display-6 fw-bold lh-1">Kategorie bearbeiten</h1>
{% endblock %}


{% block content %}
<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 %}

Das Template stellt das Formular wie schon vorher in einer Tabelle da und bietet einen Submit-Button zum Absenden.

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

../_images/category_update_2.png