Views für die Events anlegen
Bisher haben wir funktionsbasierte Views für die Kategorie angelegt:
|
für das Auflisten der Kategorien und |
|
für das Anzeigen der Kategorie-Detailseite |
|
für das Anlegen einer neuen Kategorie |
|
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 Modelsdie
DetailView
für das Anzeigen einer Detailansichtdie
CreateView
undUpdateView
für das Anlegen und Updaten eines Modelsdie
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.
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.
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:
|
für die ListView |
|
für das Anzeigen der Event-Detailseite |
|
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: https://docs.djangoproject.com/en/ref/topics/class-based-views/generic-display/
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
:
from .models import Event, Category
from django.views.generic.list import ListView
dann erben wir von ListView
und schreiben unsere erste eigene CBV:
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:
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"),
path("category/<int:pk>/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:
{% extends 'base.html' %}
{% block title %}
Event Übersicht
{% endblock %}
{% block head%}
<h1 class="display-6 fw-bold lh-1">Event Übersicht</h1>
<p>Tolle Events und Aktivitäten!</p>
{% endblock %}
{% block content %}
<ul class="list-group event_box">
{% for event in object_list %}
<li class="list-group-item rounded">
<small><span>am {{event.date}}</span></small><br>
<b>{{event.name}}</b>
<span class="badge rounded-pill bg-primary">{{event.category.name}}</span>
</li>
{% endfor%}
</ul>
{%endblock%}
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:
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.
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 Attributtemplate_name
definieren:
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: https://ccbv.co.uk/projects/Django/stable/django.views.generic.list/ListView/
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. 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:
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!
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.
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:
urlpatterns = [
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"),
path("category/<int:pk>/update/", views.category_update, name="category_update"),
path("", views.EventListView.as_view(), name="events"),
# diese Zeile hinzufügen:
path("event/<int:pk>", 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:
{% extends 'base.html' %}
{% block title %}
{{object}}
{%endblock%}
{% block head %}
<p>
<a href="{% url 'events:events' %}">zurück zur Übersicht aller Events</a><br>
</p>
<h1 class="display-6 fw-bold lh-1">{{object}}</h1>
<h3 class="display-6 lh-3">{{object.sub_title}}</h3>
<p>
am <b>{{object.date}} Uhr</b>, eingestellt von {{object.author}}
in <a href="{% url 'events:category_detail' object.category.pk %}">
{{object.category}}</a>
</p>
{% if user.is_authenticated and user == object.author %}
<span style="font-size:12px;">
{% comment %}
<a href="{% url 'events:event_update' object.id %}">editieren</a> |
<a href="{% url 'events:event_delete' object.id %}">löschen</a>
{% endcomment %}
</span>
{% endif %}
{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-sm-12">
<h3>Beschreibung</h3>
<p>{{object.description}}</p>
<h3>zusätzliche Infos</h3>
<ul>
<li>eingestellt am: {{object.created_at}}</li>
<li>eingestellt von: {{object.author}}</li>
<li><b>findet statt am (deutsche Zeit): {{object.date}}</b></li>
<li>Min Gruppengröße: {{object.get_min_group_display}}</li>
</ul>
{% if object.related_events.exists %}
<h3>Ähnliche Events</h3>
<ul>
{% for related in object.related_events %}
<li><a href="{{related.get_absolute_url}}">{{related.name}}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{%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 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:
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):
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
.
<p>Min Gruppengröße: {{object.get_min_group_display}}</p>
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/<int:pk>
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:
urlpatterns = [
[..]
path("event/<int:pk>", views.EventDetailView.as_view(), name="event_detail"),
]
Im Template müssen wir den URL-Tag so nutzen:
..
<a href="{% url 'events:event_detail' event.pk %}">
<li class="list-group-item list-group-item-info">
..
</li>
</a>
Unsere Template events/templates/events/event_list.html
sieht jetzt so aus:
{% extends 'base.html' %}
{% block title %}
Event Übersicht
{% endblock %}
{% block head%}
<h1 class="display-6 fw-bold lh-1">Event Übersicht</h1>
<p>Tolle Events und Aktivitäten!</p>
{% endblock %}
{% block content %}
<ul class="list-group event_box">
{% for event in object_list %}
<a href="{% url 'events:event_detail' event.pk %}">
<li class="list-group-item rounded">
<small><span>am {{event.date}}</span></small><br>
<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 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:
{% 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">« ZURÜCK </a></li>
{% endif %}
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}" class="page-link"> VOR »</a></li>
{% endif %}
</ul>
</nav>
</div>
{% 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
).
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 1
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:
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:
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(category)
return render(
request,
"events/category_update.html",
{"form": form},
)
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."""
if request.method == "POST":
form = CategoryForm(request.POST)
if form.is_valid():
category = form.save()
return redirect(category)
else:
form = CategoryForm()
return render(
request,
"events/category_create.html",
{"form": form},
)
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
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:
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
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.2.zip
Version v0.2 als tar-Datei runterladen
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.2.tar.gz