.. _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 zwei funktions-basierte Views für das Kategorie-Model angelegt: ``list_categories()`` für das Auflisten der Kategorien und ``category_detail()`` für das Anzeigen der Kategorie-Detailseite. Wir wollen jetzt folgende ``klassenbasierte`` Views (class based views) für die Events anlegen: * Details auflisten * Detailseite eines Events Funktionsbasierte Views VS. klassenbasierten Views ==================================================== Django unterstützt zwei Arten von Views: funktionsbasierte Views (FBVs) und klassenbasierte Views (CBVs). In den ersten Versionen von Django gab es nur FBVs. Später wurden die Klassenbasierten Views eingeführt, um stets gleichen Boilerplate-Code zu vermeiden. Klassenbasierte Views sind also gut geeignet für Standard-Aufgaben. Funktionsbasierte Views sind eher geeignet für Aufgaben, für die es keine geeignete klassenbasierte View gibt. Wichtig ist: Klassenbasierte Views ersetzen Funktionsbasierte Views nicht! .. admonition:: Grundsätzlich: Die drei Aufgaben einer View Jede View muss folgende Eigenschaften haben: * sie muss callable sein (wie das eine Funktion ist) * sie muss ein Request-Objekt als erstes Argument erwarten * sie muss ein Http-Response Objekt zurückgeben oder eine Exception auslösen 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``: * für das Auflisten von Objekten eines Models * für das Anzeigen einer Detailansicht * für das Anlegen und Updaten via Formularklasse * für das Löschen eines Objektes .. 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 nicht mehr nachvollziehbar ist. .. 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. https://docs.djangoproject.com/en/4.0/topics/class-based-views/generic-display/ Event-Übersichts-Seite ======================== Schauen wir uns eine erste CBV an: Wir wollen eine Auflistung aller im System gespeicherter Events, ähnlich wie wir das bei den Kategorien gemacht hatten. Dazu öffnen wir die ``event_manager/events/views.py`` und importieren die generische ``ListView``. Die generische ListView hat die Aufgabe, alle Objekte eines Modells 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 django.views.generic.list import ListView dann erweitern wir ``ListView`` und schreiben die erste CBV: .. code-block:: python class EventListView(ListView): """http://127.0.0.1:8000/events/""" model = Event Das war's im Grunde schon. Über die Angabe von ``model`` sagen wir Django, welche Objekt wir auflisten möchten. Django erstellt daraus implizit das Queryset ``Event.objects.all()``. Die URL anlegen ----------------- 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.list_categories, name="categories"), path("category/", views.category_detail, name="category_detail"), # diese Zeile hinzufügen: path("", views.EventListView.as_view(), name="events"), ] Die Route-Angabe bleibt leer, weil wir die Auflistung unter ``http://127.0.0.1:8000/events`` haben wollen. Wie wir oben gesehen haben, muss eine View ``callable`` sein, d.h. eine Funktion. Um das zu Erreichen, nutzen wir die ``as_view()``-Methode der ListView-Klasse. Das Template für die Übersicht anlegen ------------------------------------------ Die Template-Namen sind bei generischen CBVs eine Konvention. Sie liegen unter ``events/templates/events`` und das Template im Fall einer ListView heisst ``event_list.html``. Wir legen also eine Datei ``events/templates/events/event_list.html`` an und füllen sie mit folgendem Inhalt: .. literalinclude:: ../../../src/events/event_list_1.html :language: html+django Für den besseren Look wurden noch ein paar CSS-Änderungen vorgenommen. .. admonition:: Default Templatename einer generischen CBV der Template-Name-Suffix einer ``ListView`` ist ``_list``. Der Teil davor wird aus dem Model-Namen genommen. ``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 die Variable ``template_name`` definieren template_name = 'event_liste' Alle Methoden und Attribute einer generischen CBV finden sich 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. Zwei Sachen fallen auf: erstens werden -wie erwartet- alle Events ausgegeben. Zweitens können wir in der Django Debugtoolbar unter dem Reiter "SQL" sehen, dass ca. 22 Datenbankanfragen ausgeführt wurden. Sehr schlecht. Warum ist das so? 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 im Template zu erhalten, muss Django (aus der Template-Engine heraus) 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. 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, nicht mehr reaktionsfähig wäre. **Wir können 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 und ändern das Queryset ab: .. code-block:: python class EventListView(ListView): """http://127.0.0.1:8000/events/ http://127.0.0.1:8000/events/?category=sport """ model = Event paginate_by = 3 def get_queryset(self): queryset = Event.objects.prefetch_related("category").all() if slug := self.request.GET.get("category"): queryset = queryset.filter(category__slug=slug) return queryset Drei 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 in {{event.category.name}} abfragen, wird nicht mehr jedes mal eine Anfrage an die Datenbank gestellt. Es sind jetzt deutlich weniger SQL-Anfragen in der Django-Debug-Toolbar zu sehen. Mehr zu prefetch_related unter ``_ Zweitens haben wir einen kleinen Filter eingebaut: Wird die URL ``http://127.0.0.1:8000/events/?category=sport`` eingegeben, erhalten wir nur noch Events aus der Kategory Sport. Dazu filtern wir das queryset nach dem Slug der Kategorie. Wir haben also einen Kategoriefilter erstellt. Drittens haben wir das Attribut ``paginate_by`` gesetzt. Intern wird die Anfrage nun per ``SQL LIMIT und OFFSET`` 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: .. literalinclude:: ../../../src/events/event_list_2.html :language: html+django .. admonition:: prefetch_related vs select_related Aus der Django-Doku: ``prefetch_related``: does a separate lookup for each relationship, and does the joining in Python. ``select_related``: follows foreign-key relationships, selecting additional related-object data when it executes its query. ``select_related`` sollte also eingesetzt werden, wenn es sich um eine ``one-to-many``-Relation handelt. Tatsächlich hätten wir im vorliegenden Fall also auch diese hier 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 Relation einen eigenen Lookup und baut das dann in Python zusammen. Dieser Vorgang ist bei sehr vielen Objekten etwas langsamer. 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. Der Event soll ebenfalls per ``slug`` aufgerufen werden können. .. code-block:: python from django.views.generic.detail import DetailView class EventDetailView(DetailView): """ events/event/linux-user-gathering """ model = Event Unser Event ist also unter ``http://127.0.0.1:8000/events/event/linux-user-gathering`` aufrufbar. Die URL anlegen -------------------- Dazu tragen wir eine neue URL in ``event_manager/events/urls.py`` ein: .. code-block:: python urlpatterns = [ path("categories", views.list_categories, name="categories"), path("category/", views.category_detail, name="category_detail"), path("", views.EventListView.as_view(), name="events"), # diese Zeile hinzufügen: path("event/", views.EventDetailView.as_view(), name="event_detail"), ] Das Template anlegen -------------------------- In generischen ``DetailViews`` 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: .. literalinclude:: ../../../src/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. 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): BIG = 10 SMALL = 2 MEDIUM = 5 LARGE = 20 UNLIMITED = 0 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 auf die Detailseite von der Event-Übersichtsseite aus. 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 Slug erwarten, übergeben wir den ``event.slug``. .. code-block:: html+django ..
  • ..
  • Unsere Template ``events/templates/events/event_list.html`` unter sieht jetzt so aus: .. literalinclude:: ../../../src/events/event_list_3.html :language: html+django Autor mit prefetch-related vorladen ------------------------------------- Wir haben in der finalen Version der Event-Übersitcht noch den Autor des Events in der Übersicht ausgegeben. Auch hier wird wieder pro Event eine Anfrage an die Datenbank gestellt. 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 = 3 def get_queryset(self): queryset = Event.objects.prefetch_related("category", "author").all() if slug := self.request.GET.get("category"): queryset = queryset.filter(category__slug=slug) return queryset 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. Dazu einfach ein Snippets-Verzeichnis unter ``event_manager/templates/snippets`` anlegen folgenden Code in die Datei ``paginator.html`` kopieren. ``event_manager/templates/snippets/paginator.html`` .. code-block:: html+django {% if is_paginated %}
      {% if page_obj.has_previous %}
    • « PREV
    • {% endif %} {% if page_obj.has_next %}
    • NEXT »
    • {% endif %}
    {% endif %} .. 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 zu kopieren. 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.slug)]) Wir müssen jetzt im Template nur noch ``get_absolute_url`` aufrufen und bekommen komfortabel den Link zu dieser Resource geliefert.