.. _create_event_views:
.. index::
single: View
single: klassenbasierte View
single: CBV
single: Detailseite
single: funktionsbasierte View
Views für die Events anlegen
*****************************
Bisher haben wir funktionsbasierte Views für die Kategorie angelegt:
.. csv-table:: Bisherige Views
:widths: 40, 40
``categories()``, für das Auflisten der Kategorien und
``category_detail()``, für das Anzeigen der Kategorie-Detailseite
``category_create()``, für das Anlegen einer neuen Kategorie
``category_update()``, für das Ediiteren einer Kategorie
Für die Events benötigen wir natürlich jetzt auch noch Views.
Diese wollen wir aber nicht mehr **funktionsbasiert** entwickeln, wie wir das
vorher gemacht hatten, sondern **klassenbasiert**. Aber was sind klassenbasierte
Views eigentlich?
Funktionsbasierte Views VS. klassenbasierten Views
====================================================
Django unterstützt zwei Arten von Views: **funktionsbasierte Views (FBVs)** und
**klassenbasierte Views (CBVs)**.
Klassenbasierte Views haben den Vorteil, viel von immer wiederkehrendem
Boiler-Plate-Code zu verbergen und dem Entwickler damit Arbeit abzunehmen.
Viele Aktionen in einer Web-Anwendung sind wiederkehrende Aufgaben, die immer
ähnlich ablaufen. Für diese Aufgaben eigenen sich klassenbasierte Views
hervorragend, weil sie meist genau den Use-Case abdecken.
**Wichtig ist:** Klassenbasierte Views sind nicht per se ein Ersatz für funktionsbasierte Views!
Klassenbasierte Views
========================
Im Kern sind **CBVs** Python-Klassen. Django wird mit einer Vielzahl von CBVs
ausgeliefert, die über vorkonfigurierte Funktionen verfügen, die
wiederverwendet und erweitert werden können. Diese Views werden als generische
Views bzeichnet, da sie Lösungen für immer wiederkehrende Aufgaben bieten.
Generische Klassenbasierte Views
---------------------------------
Für immer wiederkehrende Aufgaben gibt es ``generische klassenbasierte Views``, die im Package django
``django.views.generic liegen``:
* die ``ListView`` für das Auflisten von Objekten eines Models
* die ``DetailView`` für das Anzeigen einer Detailansicht
* die ``CreateView`` und ``UpdateView`` für das Anlegen und Updaten eines Models
* die ``DeleteView`` für das Löschen eines Objektes
Daneben gibt es noch eine weitere generische View, die wir in einem späteren Kapiteln kennenlernen werden,
die ``TemplateView``, deren Hauptaufgabe es ist, ein Template darzustellen.
Allen generischen Views ist gemein, dass man Methoden problemlos überschreiben und anhand Mixins
weitere Funktionalitäten anreichern kann.
.. admonition:: Nachteile einer klassenbasierten View (CBV)
Grundsätzlich arbeiten CBVs impliziter als funktionbasierte Views, in denen
alles explizit angegeben werden muss. CBVs sind somit viel schwerer zu
Debuggen, weil der Kontrollfluss oft nicht mehr nachvollziehbar ist und vieles
wie Magie erscheint.
.. admonition:: Wann klassenbasierte Views einsetzen?
Diese Frage ist schwer zu beantworten. Es gibt Entwickler, die
ausschließlich klassenbasierte Views nutzen. Andere nutzen Mischformen je
nach konkreter Problemstellung. Als Tip könnte man sagen, dass
klassenbasierte Views genommen werden sollten, wenn die generische Klasse
dem gegebenen Use-Case relativ nahe kommt. Falls aber die Klasse
umständlich umgeschrieben werden müsste, bietet es sich an, auf eine
funktionsbasierte View umzuschwenken.
Bevor wir unsere erste klassenbasierte View erstellen, hier nur noch kurz eine Übersicht
der Dateinnamen der html-Templates, die wir in den Views einsetzen werden:
.. csv-table:: generische Templatenamen des Event-Models
:widths: 40, 40
``event_list.html``, für die ListView
``event_detail.html``, für das Anzeigen der Event-Detailseite
``event_form``, für das Anzeigen des Formulars (bei Update und Create)
Wenn wir Beispielsweise eine DetailView für das Event-Model erstellen, heisst das
Template per default ``event_detail.html``.
**Mehr zum Thema generische Views:**
``_
Event-Übersichts-Seite
========================
Schauen wir uns eine erste **CBV** an: Wir wollen eine Auflistung aller im System
gespeicherten Events, ähnlich wie wir das bei den Kategorien gemacht hatten.
Dazu öffnen wir die ``event_manager/events/views.py`` und importieren zuerst die
generische ``ListView``. Die generische ListView hat die Aufgabe, alle Objekte
eines Models aus der Datenbank zu lesen und an ein Template weiterzureichen.
Die View anlegen
---------------------
Zuerst importieren wir die ``ListView`` aus ``django.views.generic.list``:
.. code-block:: python
from .models import Event, Category
from django.views.generic.list import ListView
dann erben wir von ``ListView`` und schreiben unsere erste eigene CBV:
.. code-block:: python
class EventListView(ListView):
"""http://127.0.0.1:8000/events/"""
model = Event
Eine gute Konvention ist es, die eigene View immer nach dem Schema aufzubauen,
dass der neue, klassenbasierte Viewname aus dem Namen des entsprechenden Models
und dem Typ der View zu einem neuen Namen zusammengefügt wird.
Eine Detail-View für die Kategorien würde folglich ``CategoryDetailView``
lauten. So kommt man später nicht durcheinander.
Das war's im Grunde schon. Über die Definition des Attributs ``model`` teilen
wir Django mit, welche Objekt wir auflisten möchten. Django erstellt daraus
implizit das Queryset ``Event.objects.all()``, es werden also per default alle
Objekte geladen. Keine Sorge, das Queryset kann man auch überschreiben, wenn
man nicht alle Objekte haben möchte.
Die URL anlegen
-----------------
Nun benötigen wir eine URL für die Event-Übersicht. Dazu tragen wir eine neue URL in
``event_manager/events/urls.py`` ein:
.. 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"),
path("category//update/", views.category_update, name="category_update"),
# diese Zeile hinzufügen:
path("", views.EventListView.as_view(), name="events"),
]
Die Route-Angabe in der path-Funktion bleibt leer, weil wir die Auflistung unter
``http://127.0.0.1:8000/events`` angezeigt bekommen wollen, also im Root der Event-App.
Die ``as_view()``-Methode ist der Einstiegspunkt für Django in eine klassenbasierte View.
Beim Nutzen von klassenbasierten Views müssen wir das in Zukunft immer so in den ``urls.py`` eintragen.
Das Template für die Übersicht anlegen
------------------------------------------
Wir legen ``events/templates/events/event_list.html`` an und füllen sie mit folgendem Inhalt:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_list_1.html
:language: html+django
Im Template-Code sehen wir die Variable ``object_list``, über die wir iterieren. Das ist der generische
Bezeichner für das Queryset, auf das wir im Template Zugriff haben.
Falls man den Kontextnamen der Variablen ``object_list`` überschreiben möchte, könnte man das
über das Attribut ``context_object_name`` machen:
.. code-block:: python
class EventListView(ListView):
"""http://127.0.0.1:8000/events/"""
model = Event
context_object_name = "events"
Dann könnte man im ``event_list.html`` statt ``object_list`` selbsterklärender
das Wort ``events`` schreiben. Wer will, kann es jetzt gerne ausprobieren.
.. admonition:: Default Templatename einer generischen CBV
der Template-Name-Suffix einer ``ListView`` ist ``_list``. Der vordere Teil
wird aus dem klein geschriebenen Model-Namen abgeleitet. ``event_list`` ist
also der Defaultname unseren **List-View-Templates** für das Event-Model.
Wir können allerdings auch eigene Templatenamen vergeben, müssen dafür dann
nur das Attribut ``template_name`` definieren:
.. code-block:: python
class EventListView(ListView):
"""http://127.0.0.1:8000/events/"""
model = Event
template_name = 'events/event_liste.html'
**Alle Methoden und Attribute der generischen ListView finden sich u.a. hier:**
``_
Wir starten den Runserver und rufen ``http://127.0.0.1:8000/events/`` auf.
Auf unserer Übersichtsseite werden jetzt alle in der Datenbank verfügbaren Events ausgegeben.
.. image:: /images/events_overview_1.png
Zwei Sachen fallen auf: erstens werden -wie erwartet- alle Events ausgegeben. Das können unter Umständen
sehr viele sein, was problematisch ist.
Zweitens können wir in der **Django Debugtoolbar** unter dem Reiter **SQL** sehen,
dass viele, und zudem sehr ähnlich lautende Datenbankanfragen ausgeführt wurden. Das ist
natürlich schlecht für die Performance.
Für jede Iteration über die ``object_list`` (die im Template eine Liste aller Events
darstellen), wurde ``{{event.category.name}}`` ausgeführt, um den Namen der Kategorie für jedes Event
anzuzeigen. Um aber den Namen der Kategorie im Template zu erhalten, muss Django (aus der Template-Engine heraus)
pro Event eine SQL-Anfrage starten. Das ist natürlich extrem unperformant, denn jede
Anfrage an die Datenbank kostet Zeit. Wir wollen die Anzahl der
Anfragen, die an die Datenbank gerichtet werden, aber möglichst klein halten.
**SQL Anfragen ohne Ende, schlecht für die Performance:**
.. image:: /images/events_overview_debugtoolbar.png
Hätten wir also 5000 Events im System, würden hier 5000 Datenbankanfragen gestartet, die das System so
stark ausbremsen würden, dass es selbst im Produktivbetrieb unter idealen Server-Bedingungen
eventuell nicht mehr reaktionsfähig wäre.
**Wir können und müssen das verhindern!**
Prefetch-Related
-------------------------
Um das Problem mit den SQL-Anfragen zu umgehen, können wir unseren View etwas
umbauen. Wir überschreiben die ``get_queryset``-Methode der ``EventListView``
und ändern das Rückgabe-Queryset dieser View wie folgt ab:
.. code-block:: python
class EventListView(ListView):
"""http://127.0.0.1:8000/events/
"""
model = Event
paginate_by = 10
def get_queryset(self):
return Event.objects.prefetch_related("category").all()
**Zwei Dinge sind hier passiert:**
**Erstens** haben wir mit ``prefetch_related("category")`` alle Daten, die die Kategorie betreffen, vorgeladen.
Dh. wenn wir nun den Namen der Kategorie im Template mit ``{{event.category.name}}`` abfragen, wird nicht mehr
jedes mal eine Anfrage an die Datenbank gestellt.
Es sind jetzt auch deutlich weniger SQL-Anfragen in der Django-Debug-Toolbar zu sehen.
**Mehr zu prefetch_related unter:**
``_
**Zweitens** haben wir das Attribut ``paginate_by`` auf 10 gesetzt. Intern wird
die Anfrage nun per **SQL LIMIT und OFFSET** per Seitenaufruf auf 10 Events
limitiert.
Wir müssen jetzt, um eine ordentliche Pagination zu gewährleisten, nur noch das Template ``event_list.html`` anpassen und den Code für die Pagination einbauen:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_list_2.html
:language: html+django
Die eingebaute Pagination ermöglicht uns das Vor- und Zurückblättern im Event-Katalog.
.. image:: /images/events_overview_pagination.png
.. admonition:: prefetch_related vs select_related
``select_related`` sollte also eingesetzt werden, wenn es sich um eine **one-to-many**-Beziehung handelt.
Tatsächlich hätten wir im vorliegenden Fall also auch diese Methode nehmen können.
Select_Related macht einen echten **INNER JOIN** auf der Datenbank.
``prefetch_related`` macht vor allem Sinn bei einem **umgekehrten Foreign-Key**
oder bei einer **Many-to-Many**- Abfrage. Diese Funktion erstellt für jede
Beziehung einen eigenen Lookup und baut das Ergebnis in Python zusammen. Dieser
Vorgang ist bei sehr vielen Objekten etwas langsamer.
Ein erneuter Check der Event-Übersichtseite in der Debug-Toolbar zeigt die Optimierung, die
``prefech_related`` gebracht hat.
.. image:: /images/events_overview_debugtoolbar2.png
Event-Detail-Seite
========================
Machen wir uns an die Event-Detail-Seite. Dazu nutzen wir wieder ein generisches CBV, diesmal eine ``DetailView``.
Die View für die Detailseite anlegen
---------------------------------------
Dazu öffnen wir die ``event_manager/events/views.py`` und importieren die generische ``DetailView``.
Dann legen wir die ``EventDetailView``-Klasse an.
.. code-block:: python
from django.views.generic.detail import DetailView
class EventDetailView(DetailView):
"""
events/event/3
"""
model = Event
Unser Event soll unter ``http://127.0.0.1:8000/events/event/3`` aufrufbar sein.
Die URL anlegen
--------------------
Dazu tragen wir eine neue URL in ``event_manager/events/urls.py`` ein:
.. code-block:: python
urlpatterns = [
path("categories", views.categories, name="categories"),
path("category/", views.category_detail, name="category_detail"),
path("category/create", views.category_create, name="category_create"),
path("category//update/", views.category_update, name="category_update"),
path("", views.EventListView.as_view(), name="events"),
# diese Zeile hinzufügen:
path("event/", views.EventDetailView.as_view(), name="event_detail"),
]
Das Template anlegen
--------------------------
In der generischen ``DetailView`` ist der Template-Suffix ``_detail``. Unser Template heisst also ``event_detail``
und muss unter ``events/templates/events/event_detail.html`` angelegt werden.
Fügen wir folgenden Code ein:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_detail_1.html
:language: html+django
Im Template verweisen wir jetzt auch auf die Funktion ``related_events`` des Event-Objekts und zeigen die
Events auch an, falls verfügbar. Dazu nutzen wir den ``if-Tag`` der Django Template Sprache in Kombination
mit dem Aufruf der ``exists``-Funktion.
Die Links zum Editieren und Löschen des Events sind aktuell noch mithilfe des
``comment-Tags`` auskommentiert und werden später, wenn die Views implementiert
sind, freigeschaltet.
Wenn wir mit unserem Browser auf die URL navigieren, können wir einen Blick auf
die neue Event-Detailseite werfen:
.. image:: /images/event_detailseite_1.png
Wenn wir nur ``object.min_group`` ausgeben würden, würden wir die Werte des Choices
erhalten. Das sind aber Integer-Werte, weil wir das in ``event_manager/events/models.py`` so ja auch definiert hatten.
.. code-block:: python
class Group(models.IntegerChoices):
SMALL = 2, "kleine Gruppe"
MEDIUM = 5, "mittelgroße Gruppe"
BIG = 10, "große Gruppe"
LARGE = 20, "sehr große Gruppe"
UNLIMITED = 0, "ohne Begrenzung"
Mit dem Schema ``get_FELDNAME_display()`` kommen wir auch die Keys des Labels, in diesem Fall zum Beispiel ``small``.
.. code-block:: html+django
Min Gruppengröße: {{object.get_min_group_display}}
Verlinkung auf die Detailseite
------------------------------------
Was jetzt noch fehlt, ist eine Verlinkung von der Event-Übersichtsseite auf die Event-Detailseite.
Jeder Event in der List soll anklickbar sein.
Dazu öffnen wir das Template unter ``events/templates/events/event_list.html``
und fügen einen Hyperlink mit dem ``url-Tag`` ein. Da ``event/`` aus
den URLs einen Integer erwartet, übergeben wir den Primarykey: ``event.pk``.
Zur Erinnerung, so sieht der URL-Eintrag aus den urlpatterns für die Event-Detailseite aus:
.. code-block:: python
urlpatterns = [
[..]
path("event/", views.EventDetailView.as_view(), name="event_detail"),
]
Im Template müssen wir den URL-Tag so nutzen:
.. code-block:: html+django
..
Unsere Template ``events/templates/events/event_list.html`` sieht jetzt so aus:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/event_list_3.html
:language: html+django
.. image:: /images/events_overview_final.png
Autor mit prefetch-related vorladen
-------------------------------------
Wir haben in der finalen Version der Event-Übersicht noch den Autor des Events in der Übersicht ausgegeben.
Auch hier wird wieder pro Event eine Anfrage an die Datenbank gestellt, weil auch der Autorname
nicht Teil der Event-Tabelle ist.
Das resultiert allerdings wieder in unnötigen SQL-Anfragen:
.. image:: /images/events_overview_author.png
Und auch hier gilt wieder: ``prefetch-related`` to the rescue. Wir ändern also die ``prefetch_related``-Methode
der ``EventListView`` unter ``event_manager/events/views.py`` ab und laden auch noch den ``author`` vor:
.. code-block:: python
class EventListView(ListView):
model = Event
paginate_by = 10
def get_queryset(self):
return Event.objects.prefetch_related("category", "author").all()
Paginator
---------------
Wie wir weiterhin in der finalen Version der Event-Übersitcht unter
``events/templates/events/event_list.html`` sehen können, haben wir die Logik
für den Paginator ausgelagert und per ``include``-Tag eingebunden.
Dazu einfach ein Snippets-Verzeichnis unter ``event_manager/templates/snippets`` anlegen
und folgenden Code in die Datei ``paginator.html`` kopieren. Die Snippets können also
in jeder App genutzt werden, wo sie benötigt werden.
Wir legen also die Datei an ``event_manager/templates/snippets/paginator.html``
und kopieren folgenden Templatecode hinein:
.. code-block:: html+django
{% if is_paginated %}
{% endif %}
Dieser Schnipsel zeigt die Next- und Previous Buttons an, wenn die Pagination
eingeblendet werden soll. Wir prüfen hier zunächst mit ``is_paginated``, ob überhaupt eine
Pagination nötig und möglich ist. Falls eine Möglichkeit zur Pagination angezeigt werden soll,
, prüfen wir mit ``page_obj.has_next``, ob es es eine nächste Seite zum Umblättern gibt und falls das zutrifft,
wird der entsprechende Button eingeblendet.
Ebenso machen wir das mit den Vorgängerseiten (``page_obj.has_previous``).
.. admonition:: Snippets
Es ist eine gute Idee, Teile der Templates, die immer wiederkehren, in
Snippet-Ordner auszulagern und per ``include`` in das Template zur Laufzeit
inkludieren. Somit läuft man weniger Gefahr, Code permanent zu kopieren und dem **DRY-Prinzip** [#dry]_
treu zu bleiben.
Starten wir den Runserver und navigieren nach
``https://128.0.0.1:8000/events``, um die Übersicht aller Events zu sehen.
Absolute URL zur Detailseite
--------------------------------
Eine Sache müssen wir jetzt noch machen. Wir müssen für das Event-Model eine
Methode implementieren, die die absolute URL zu einer Instanz der Klasse
zurückgibt. Wir werden diese später noch brauchen, wenn wir ein neues
Event-Objekt mit einem Formular einfügen und daraufhin auf dieses Objekt
weitergeleitet werden wollen.
Wir öffnen also die Datei ``event_manager/events/models.py`` und fügen der
Event-Klasse folgendes hinzu:
.. code-block:: python
from django.urls import reverse
..
..
class Event(DateMixin):
..
..
def get_absolute_url(self):
return reverse("events:event_detail", args=[str(self.pk)])
Wir müssen jetzt im Template nur noch ``get_absolute_url`` aufrufen und
bekommen komfortabel den Link zu dieser Resource geliefert, ohne jedesmal wieder darüber nachdenken
zu müssen, wie der Link zur Detailseite eigentlich konstuiert werden muss.
Wer Lust hat, kann die Methode auch in dem Kategorie-Model vornehmen und dann die beiden Views für die Kategorie Views entsprechend abändern:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/views/category_update_1.py
:language: python
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/views/category_add_1.py
:language: python
Wenn für ein Model die Methode ``get_absolute_url`` implementiert ist,
reicht es aus, bei redirect nur das Objekt zu übergeben, wenn nach dem
Speichern auf die Detailseite des Models weitergeleitet werden soll.
Optional: Das Projekt in der Version v0.2 kopieren
-----------------------------------------------------
Es ist eine Menge passiert bisher und oft schleicht sich mal der Fehlerteufel
ein. Wer das Projekt an dieser Stelle kopieren oder klonen möchte, kann dies
jetzt tun:
Version v0.2 via github clonen
......................................
.. code-block:: bash
git clone -b v0.2 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.2 als zip-Datei runterladen
...........................................
``_
Version v0.2 als tar-Datei runterladen
............................................
``_
.. [#dry] ``_