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!
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
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.
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
:
from django.views.generic.list import ListView
dann erweitern wir ListView
und schreiben die erste CBV:
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:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.list_categories, name="categories"),
path("category/<int:pk>", 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:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Übersicht der Events
{%endblock%}
{% block head %}
Übersicht der Events
{%endblock%}
{% block content %}
<ul class="list-group event_box">
{% for event in object_list %}
<li class="list-group-item rounded">
<b>{{event.name}}</b>
<span class="badge rounded-pill bg-primary">
{{event.category.name}}
</span>
</li>
{% endfor%}
</ul>
{%endblock%}
Für den besseren Look wurden noch ein paar CSS-Änderungen vorgenommen.
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: https://ccbv.co.uk/projects/Django/4.0/
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.
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.
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:
urlpatterns = [
path("categories", views.list_categories, name="categories"),
path("category/<slug:slug>", views.category_detail, name="category_detail"),
path("", views.EventListView.as_view(), name="events"),
# diese Zeile hinzufügen:
path("event/<slug:slug>", 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:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Event: {{object.name}}
{%endblock%}
{% block head %}
{{object.name}}
{%endblock%}
{% block content %}
<h1>{{object.name}}</h1>
<p>{{object.sub_title}}</p>
<p>Category: {{object.category}}</p>
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<p>findet statt am: {{object.date}}</p>
<p>created at: {{object.created_at}}</p>
{% if object.related_events %}
<h4>Ähnliche Events</h4>
<ul>
{% for related in object.related_events %}
<li>{{related.name}}</li>
{% endfor %}
</ul>
{% endif %}
{%endblock%}
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.
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
.
<p>Min Gruppengröße: {{object.get_min_group_display}}</p>
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/<slug:slug>
aus den URLs einen Slug erwarten, übergeben wir den event.slug
.
..
<a href="{% url 'events:event_detail' event.slug %}">
<li class="list-group-item list-group-item-info">
..
</li>
</a>
Unsere Template events/templates/events/event_list.html
unter sieht jetzt so aus:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Übersicht der Events
{%endblock%}
{% block head %}
Übersicht der Events
{%endblock%}
{% block content %}
<ul class="list-group event_box">
{% for event in object_list %}
<a href="{% url 'events:event_detail' event.slug%}">
<li class="list-group-item rounded">
<b>{{event.name}}</b>
<span class="badge rounded-pill bg-primary">
{{event.category.name}}
</span>
<p>by {{event.author}}</p>
</li>
</a>
{% endfor%}
</ul>
{% include "../snippets/paginator.html" %}
{%endblock%}
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
{% if is_paginated %}
<nav aria-label="Page navigation conatiner"></nav>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li><a href="?page={{ page_obj.previous_page_number }}" class="page-link">« PREV </a></li>
{% endif %}
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}" class="page-link"> NEXT »</a></li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
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:
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.