Eine Pages App erstellen
Bevor wir mit der Events-App weitermachen, wollen wir noch eine pages- App erstellen, die mehrheitlich statische Seiten wie zum Beispiel die Homepage oder das Impressum bereitstellt.
Fangen wir mal mit der Homepage an.
App installieren
Um eine App innerhalb eines Projekts anzulegen, nutzen wir wie gewohnt das Sub-Kommando startapp gefolgt von dem Namen der gewünschten App.
(eventenv) python manage.py startapp pages
und registrieren sie in den event_manager/event_manager/settings.py
INSTALLED_APPS = [
"user",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_bootstrap5",
"crispy_forms",
"events",
"pages" # << Hier die neue App registrieren!
]
Die URLS anlegen
Projekt URLs erweitern
Dazu öffnen wir erstmal die Projekt-URLs unter event_manager/event_manager/urls.py und fügen den Path zu den pages.urls ein:
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("events/", include("events.urls", namespace="events")),
path("", include("pages.urls", namespace="pages")), # <= das hier einfügen!
]
Dadurch, dass das erste Argument für die path-Funktion ein leerer String ist, können wir erreichen, dass unsere Index-Page bzw. das Impressum unter der Hauptdomain angezeigt wird.
Die URL für die Index-Seite wäre dann zum Beispiel www.example.com und für das Impressum www.example.com/imprint. Alle anderen Apps müssen wie
gewohnt mit ihrem Pfad aufgerufen werden.
App URLs anlegen
Jetzt müssen noch die URLs auf App-Ebene festgelegt werden. Dazu legen wir die Datei event_manager/pages/urls.py an und modifzieren den Inhalt wie folgt:
from django.urls import path
from . import views
app_name = "pages"
urlpatterns = [
path('', views.HomePageView.as_view(), name='home'),
]
View für die Homepage einrichten
Die View, auf die wir in den urlpatterns referenzieren, muss jetzt noch unter event_manager/pages/views.py angelegt werden. Dazu nutzen wir eine generische, klassenbasierte View namens TemplateView.
Die TemplateView ist eine generische View aus, die zur Darstellung statischer oder leicht dynamischer Inhalte verwendet wird. Sie rendert ein Template, ohne dass eigene Logik zur Verarbeitung von Formularen oder Datenbankabfragen erforderlich ist. Typischerweise wird lediglich das Attribut template_name gesetzt. Falls zusätzliche Daten an das Template übergeben werden sollen, kann die Methode get_context_data überschrieben werden. Die TemplateView eignet sich besonders für einfache Seiten wie Startseiten, Impressum oder statische Informationsseiten.
from django.views.generic.base import TemplateView
class HomePageView(TemplateView):
"""Das ist eine generische, klassenbasierte View."""
template_name = "pages/index.html"
Template für die Homepage
Wir legen in der neu erstellten pages-App zunächst das Verzeichnis templates/pages an. Anschließend erstellen wir dort das Template für die Homepage unter event_manager/pages/templates/pages/index.html und fügen folgenden Inhalt ein:
{% extends "base.html" %}
{% block title %}Homepage{% endblock %}
{% block head %}
<h1>Event Manager Startseite</h1>
{% endblock %}
Wenn wir das die URL 127.0.0.1:8000 aufrufen, sollte uns jetzt die Homepage angezeigt werden.
Auf die selbe Art könnte man nun weitere Pages wie zum Beispiel ein Impressum oder eine About-Seite anlegen. Das überlasse ich mal dem Leser als kleine Übungsaufgabe.
Den Kontext für die Homepage verändern
Wir wollen auf der Homepage nun die top 5 Kategorien auflisten, die
die meisten Events haben. Dazu müssen wir einen Filter
erstellen und die Methode get_context_data überschreiben.
from django.views.generic.base import TemplateView
from django.db.models import Count
from events.models import Category
class HomePageView(TemplateView):
template_name = "pages/index.html"
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
categories = Category.objects.annotate(
number_events=Count("events")
).order_by("-number_events")[:5]
context.update({"categories": categories})
return context
In der Methode get_context_data wird der Template-Kontext um zusätzliche Daten erweitert. Zunächst wird der bestehende Kontext der Elternklasse übernommen. Anschließend werden die Top 5 Kategorien anhand der Anzahl zugehöriger Events ermittelt. Dies geschieht über eine Aggregation mittels Count("events") und eine Sortierung in absteigender Reihenfolge. Die so berechneten Kategorien werden dem Kontext hinzugefügt und stehen im Template zur Darstellung zur Verfügung.
Dann passen wir nochmal das Homepage-template unter event_manager/pages/templates/pages/index.html an
und fügen folgenden Inhalt ein:
{% extends "base.html" %}
{% block title %}Homepage{% endblock %}
{% block head %}
<h1>Event Manager Startseite</h1>
{% endblock %}
<h2>Kategorien</h2>
<ul>
{% for category in categories %}
<li>
<a href="{% url 'events:category-detail' category.pk %}">
{{category}}
</a>
<span class="badge rounded-pill bg-primary">
{{category.number_events}}
</span>
</li>
{% endfor %}
</ul>
Kleine Übung
Wer will, kann jetzt versuchen, zusätzlich noch 5 zufällige Events auf der Startseite darzustellen.
Dazu kann die Datenbank genutzt werden, um zufällige Datensätze direkt zufällig zu sortieren, z. B. über order_by(Random()).
Die Klasse Random stammt aus django.db.models.functions. Solche Database Functions sind Wrapper um Datenbankfunktionen, die direkt auf SQL-Ebene ausgeführt werden. Statt Daten erst nach Python zu laden und dort zu verarbeiten, wird die Operation an die Datenbank delegiert. Das ist in der Regel effizienter, da die Datenbank für solche Operationen optimiert ist.
Random() entspricht dabei einer SQL-Funktion wie RANDOM() (z. B. in PostgreSQL) und sorgt dafür, dass die Ergebnismenge zufällig sortiert wird. In Kombination mit einem Slice ([:5]) lassen sich so einfach zufällige Datensätze auswählen.
Warnung
Datenverarbeitung nicht unnötig in Python durchführen
Operationen wie Filtern, Sortieren oder zufällige Auswahl sollten nach Möglichkeit in der Datenbank erfolgen und nicht in Python.
Das Laden großer Datenmengen in den Anwendungsspeicher (z. B. über list(queryset)) ist
in der Regel ineffizient und skaliert schlecht. Datenbanken sind speziell dafür optimiert,
solche Operationen performant auf SQL-Ebene auszuführen.
Faustregel: Alles, was sich mit QuerySets ausdrücken lässt, sollte auch dort umgesetzt werden.
Die Lösung könnte dann aussehen:
from django.views.generic.base import TemplateView
from django.db.models import Count
from django.db.models.functions import Random
from events.models import Category, Event
class HomePageView(TemplateView):
template_name = "pages/index.html"
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
categories = Category.objects.annotate(
number_events=Count("events")
).order_by("-number_events")[:5]
# Zufällige Auswahl von 5 Events direkt in der Datenbank
events = Event.objects.order_by(Random())[:5]
context.update({"categories": categories, "events": events})
return context
Im Template würde man die erzeugten Events dann so ausgeben:
<h2>Events</h2>
<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>
Die Abfrage der zufälligen Events erfolgt hier direkt in der Datenbank über order_by(Random()). Dadurch vermeiden wir, alle Datensätze zunächst in Python zu laden und dort weiterzuverarbeiten, was insbesondere bei größeren Datenmengen ineffizient wäre.
Zusätzlich kann es sinnvoll sein, verwandte Objekte wie author und category direkt mitzuladen, um weitere Datenbankabfragen im Template zu vermeiden:
class HomePageView(TemplateView):
template_name = "pages/index.html"
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
categories = Category.objects.annotate(
number_events=Count("events")
).order_by("-number_events")[:5]
# Zufällige Auswahl von 5 Events direkt in der Datenbank
events = (
Event.objects
.select_related("author", "category")
.order_by(Random())[:5]
)
context.update({"categories": categories, "events": events})
return context
Die Datenbank übernimmt damit sowohl die zufällige Auswahl als auch das effiziente Laden der Beziehungen, wodurch die Anzahl der Queries reduziert wird.
Die Anzahl der Datenbank-Anfragen hat sich nun drastisch reduziert: