Templates ausbauen
Wir wollen jetzt das bisherige Template ausbauen und ein wenig mehr über die Django-Template-Sprache lernen. Dazu werden wir noch ein Base-Template für das Projekt entwickeln und eine Detailseite für die Kategorie-Informationen erstellen. Wir brauchen dafür natürlich wieder eine URL, eine View und ein Template.
Template-Angaben in den Settings
Wenn wir ein neues Projekt beginnen, setzen wir in der settings.py
Datei des Projekt zwei wichtige Angaben.
Templates, die das gesamte Projekt betreffen, zb. das Base-Template, müssen für alle Apps verfügbar sein. Diese Projekt-Templates liegen in einem Ordern, der in den Settings definiert wird. Für diese Angabe steht der Key “DIRS” in den Template-Settings zur Verfügung. Wir wollen unsere Projekt-Templates also unter event_manager/templates
speichern.
App-spezifische Templates, wie die Übersicht der Kategorien, speichern wir auch in der jeweiligen App, zb. event_manager/events/templates/events/category_list_simple.html
. Damit Django in den Apps nach Templates sucht, muss APP_DIRS
in den settings.py
auf True
gesetzt werden.
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "event_manager" / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
Mehr dazu unter https://docs.djangoproject.com/en/4.0/intro/tutorial03/
Die Template-Hierachie
Das Base-Template (base.html) enthält alle Elemente und Strukturen, die jede Page des Projekts haben soll. Zum Beispiel einen Headbereich mit Navigation, eine Seitenleiste, einen Footer mit Links und so weiter.
Ein Basis-Template ist das grundlegende Template, welches dann auf jeder einzelnen Seite einer Website mit spezifischeren Anweisungen erweitert wird.
Die Sub-Templates definieren ebendiese Blöcke und rendern sie in das Base-Template. Das geschieht dadurch, dass das Sub-Template das Basis-Layout erweitert (extends).
ein Base-Template für das Projekt
Unsere projektspezifischen Templates liegen also unter event_manager/templates
. Dort legen wir eine neue Datei namens base.html
an. In diese Datei kopieren wir folgenden Inhalt:
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang='{{ LANGUAGE_CODE }}'>
<head>
<meta charset="utf-8">
<title></title>
<meta name="author" content="">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>{% block head %}{% endblock %}</h1>
<div>
{% block content %}
{% endblock %}
</div>
</body>
</html>
Es handelt sich um eine einfache HTML-Struktur mit zwei sogenannten Tags
. Diese Tags definieren in diesem Fall Blöcke (block)
, in die später Content gerendert wird. Es gibt aber noch viele andere Tags und wir können natürlich auch selber eigene Tags definieren.
In den Block head
soll eine Überschrift gerendert werden, in den Block
content
wird der Inhalt kommen, also zum Beispiel die Liste der Kategorien.
Die Namen head und content sind übrigens von uns frei gewählt, wir könnten sie auch kopf und inhalt nennen.
Diese base.html
wird jetzt nicht direkt aufgerufen, sondern wir werden sie quasi vererben.
Das Base-Template erweitern
Um das Base-Template zu nutzen, ändern wir event_manager/events/templates/events/category_list_simple.html
wie folgt ab:
{% extends 'base.html' %}
{% block head%}
Übersicht der Kategorien
{% endblock %}
{% block content %}
<ul>
{% for cat in categories %}
<li>
{{cat.name}}
</li>
</a>
{% endfor %}
{% endblock %}
Durch das extends
-Tag erweitern wir unser Template jetzt und definieren den Inhalt, der in die Blöcke von base.html
gerendert werden soll. In dem Fall rendern wir Text in den head
-Block und die Liste mit den Kategorien in den content
-Block.
Bootstrap HTML
Wir wollen nun das Base-Template nochmal abändern und es ein bisschen besser formatieren. Dazu nutzen wir das Bootstrap-Framework (https://getbootstrap.com/).
Die neue base.html
Datei sieht nun so aus:
<!DOCTYPE html>
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<html lang='{{ LANGUAGE_CODE }}' class="h-100">
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<meta name="author" content="{% block author %}{% endblock %}">
<meta name="description" content="{% block description %}{% endblock %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link href="{% static 'css/style.css' %}" rel="stylesheet" />
</head>
<body class="d-flex flex-column h-100">
<header class="p-3 bg-dark text-white">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<img src="{% static 'penglogo.png' %}" style="width:50px; margin-right:10px;" />
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0" style="margin-left:120px;">
<li><a href="" class="nav-link px-2
text-white">Events</a></li>
<li><a href="" class="nav-link px-2
text-white">Kategorien</a></li>
</ul>
<form action="{% url 'events:event_search' %}" method="get" class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3">
<input name="q" required="required" type="search" class="form-control form-control-dark" placeholder="Search..." aria-label="Search">
</form>
<div class="text-end">
{% if user.is_authenticated %}
<div class="dropdown text-end">
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<img src="https://github.com/mdo.png" alt="mdo" width="32" height="32" class="rounded-circle">
</a>
<ul class="dropdown-menu text-small" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item" href="#">Event anlegen</a></li>
<!-- das Freischalten, wenn der Login und Signup implementiert wird -->
<!--
<li><a class="dropdown-item" href="{% url 'password_change' %}">Passwort ändern</a></li>
-->
<li><a class="dropdown-item" href="">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<!-- das Freischalten, wenn der Login und Signup implementiert wird -->
<!--
<li>
<a class="dropdown-item" href="{% url 'logout' %}">Sign out</a>
</li>
-->
<li>
<a class="dropdown-item" href="">Sign out</a>
</li>
</ul>
</div>
{% else %}
<!-- das Freischalten, wenn der Login und Signup implementiert wird -->
<!-- <a href="{% url 'login' %}"> -->
<a href="">
<button type="button" class="btn btn-outline-light me-2">Login</button>
</a>
<!-- das Freischalten, wenn der Login und Signup implementiert wird -->
<!-- <a href="{% url 'user:signup' %}" > -->
<a href="">
<button type="button" class="btn btn-warning">Sign-up</button>
</a>
{% endif %}
</div>
</div>
</div>
</header>
<main class="flex-shrink-0">
<div class="container">
<div class="row">
<div class="col-12" style="margin-top:25px;">
{% block head %}{% endblock %}
</div>
</div>
</div>
<div class="container">
<div class="row">
{% block content %}{% endblock %}
</div>
</div>
</main>
<footer class="footer mt-auto py-3 text-light bg-dark">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<h3 class="mb-1">Event Manager</h3>
<p class="mb-0">Neu in Django? <a href="/">Visit the Pingu homepage</a> oder <a href="https://djangoheros.spielprinzip.com">Leg los mit Django</a>.</p>
</div>
</footer>
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
statische CSS-Datei
Damit dieses Template funktioniert und richtig angezeigt wird, muss eine statische CSS-Datei angelegt werden. Dafür ein Verzeichnis für statische CSS-Dateien unter event_manager/
static/css/
erstellen und dort die Datei style.css
anlegen.
In diese neu erstelle CSS-Datei unter event_manager/
static/css/style.css
kopieren wir folgenden Inhalt.
main > .container {
padding: 20px 25px 0;
}
.event_box a{
text-decoration: none;
}
.event_box li{
margin-bottom: 15px;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
Was ist CSS?
Cascading Style Sheets
ist eine Stylesheet-Sprache für elektronische Dokumente und zusammen mit HTML
und JavaScript
eine der Kernsprachen des World Wide Webs. Sie ist ein sogenannter living standard ‚lebendiger Standard‘ und wird vom World Wide Web Consortium beständig weiterentwickelt. (Wikipedia)
statische Logo-Datei
Das Logo kann unter https://djangoheros.spielprinzip.com/_images/penglogo.png heruntergeladen und in das statische Verzeichnis unter event_manager/static/penglogo.png
kopiert werden.
Auf das HTML bzw. CSS in der Datei einzugehen, würde den Rahmen dieses Kurses sprengen.
Wir haben noch einen weiteren Block title
eingefügt, um der rerenderten Seite einen Seitentitel zu geben.
Ändern wir event_manager/events/templates/events/category_list_simple.html
noch ab:
{% extends 'base.html' %}
{% block title %}
Übersicht der Kategorien
{% endblock %}
{% block head %}
Übersicht der Kategorien
{% endblock %}
{% block content %}
<ul>
{% for cat in categories %}
<li>
{{cat.name}}
</li>
</a>
{% endfor %}
{% endblock %}
Mehr dazu unter https://docs.djangoproject.com/en/4.0/topics/templates/
Intermezzo: die Django-Template-Sprache
Wie schon erwähnt, gibt es Tags und Variablen. Tags können auf zwei Arten auftreten: einzeilige Tags oder öffnende und sich schließende Tags.
Variablen
Variablen sehen so aus: {{ Variable }}
. Wenn die Template-Engine auf eine
Variable trifft, die als Key im Kontext-Dictionary übergeben wurde, wertet sie
diese Variable aus und rendert sie in das Template. Variablennamen bestehen aus
einer beliebigen Kombination von alphanumerischen Zeichen und dem Unterstrich (“_”), dürfen jedoch nicht mit
einem Unterstrich beginnen. Falls für eine Variable kein Wert verfügbar ist,
resultiert das in keinem Fehler.
Oben im Code sehen wir die Variable {{cat.name}}
. Dort wird das name-Attribut eines Kategorie-Objekts gerendert.
Die Detailseite einer Event-Kategorie
Um von der Übersicht auf die Detailseite einer Kategorie weiterzuleiten, müssen wir die Liste verlinken. Die URL zur Detailseite zu einer Kategorie ist noch nicht festgelegt, das müssten wir vorher noch machen.
Dazu ändern wir die urlpatterns
in den event_manager/events/urls.py
ab:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.list_categories, name="categories"),
# diese Zeile hinzufügen:
path("category/<int:pk>", views.category_detail, name="category_detail"),
]
Das heisst, wenn wir http://127.0.0.1:8000/events/category/3
via Browser aufrufen, sollte uns die Detailansicht einer Kategorie ausgegeben werden.
Die View für die Detailseite der Kategorie
Die View category_detail
existiert in den views.py
noch nicht, deshalb legen wir sie an. Lade das entsprechende Objekt aus der Datenbank, und werfen eine 404 Not Found Exception, falls das Objekt nicht gefunden wird.
Die Http404 Exception muss importiert werden:
from django.http import Http404
def category_detail(request, id: int):
"""
die Detailseite einer Kategorie
http://127.0.0.1:8000/events/category/3
"""
try:
category = Company.objects.get(pk=id)
except Company.DoesNotExist:
raise Http404("Diese Kategorie existiert nicht")
return render(request, 'events/category_detail.html', {
'category': category
})
404 Fehler
Wenn eine URL nicht angezeigt werden kann, weil zum Beispiel das gesuchte Objekt nicht in der Datenbank vorhanden ist, lösen wir einen Http404-Fehler
aus. Dem User wird daraufhin eine 404-Fehlerseite angezeigt (Not found), die wir später noch als Template anlegen werden.
Falls wir in den event_manager/settings.py
DEBUG=True
gesetzt haben, bekommen wir eine ausführliche Fehlerseite angezeigt, die uns detailliert aufzeigt, wo das Problem liegt. Im Live-Modus sollte zwingend DEBUG=False
sein. Dann könnten wir unsere eigene 404-Fehlerseite
anzeigen.
Wir rendern das Kategorieobjekt category
in das Template event_manager/events/templates/events/category_detail.html
.
Das Template für die Detailseite der Kategorie
Legen wir eine neue Datei event_manager/events/templates/events/category_detail.html
im Template-Ordner der App an und kopieren folgenden Inhalt hinein:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Kategorie {{category.name}}
{%endblock%}
{% block head %}
Kategorie {{category.name}}
{%endblock%}
{% block content %}
<p>{{category.description}}</p>
<p>created at: {{category.created_at}}
{%endblock%}
Wenn wir jetzt eine Detailseite aufrufen, sollte das klappen. Dazu starten wir den Runsever mit python manage.py runserver
und navigieren auf die URL http://127.0.0.1:8000/events/category/3
.
Da sollte funktionieren. Wir wollen jetzt noch eine weitere Änderung vornehmen, bevor wir uns an die Verlinkung machen. Eine URL wie http://127.0.0.1:8000/events/category/3
ist zwar möglich, aber nicht schön.
Da wir beim Entwickeln der Models für Events und Kategorien ja schon einen Slug eingebaut haben, wollen wir den jetzt auch gleich nutzen. Wir können dann die URLs im Format http://127.0.0.1:8000/events/category/sport
aufrufen.
Dazu ändern wir die urlpatterns
in den event_manager/events/urls.py
ab:
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.list_categories, name="categories"),
# diese Zeile ändern:
path("category/<slug:slug>", views.category_detail, name="category_detail"),
]
und ändern die View event_manager/events/views.py
und ersetzen die Funktion category_detail
mit dieser hier. Am Seitenanfang importieren wir noch get_object_or_404
.
from django.shortcuts import get_object_or_404
def category_detail(request, slug: str):
"""
http://127.0.0.1:8000/events/category/sport
"""
category = get_object_or_404(Category, slug=slug)
return render(request, 'events/category_detail.html', {
'category': category
})
get_object_or_404
Für die immerwiederkehrende Aufgabe, ein Objekt aus der Datenbank zu holen und per try-except zu prüfen, ob es in der Datenbank vorhanden ist, nutzen wir ab jetzt den Shortcut get_object_or_404
, den wir aus den django.shortcuts
importieren. Wenn sich das Objekt nicht in der Datenbank befinden, wird ein 404-Fehler ausgelöst und dem User wird eine 404-Fehlerseite anzeigt.
Wir versuchen jetzt, die URL so aufzurufen: http://127.0.0.1:8000/events/category/sport
Die Detailseite verlinken
Zuletzt wollen wir noch die Verlinkung auf die Kategorie bauen:
Ändern wir dazu event_manager/events/templates/events/category_list_simple.html
noch ab.
{% extends 'base.html' %}
{% block title %}
Übersicht der Kategorien
{% endblock %}
{% block head %}
Übersicht der Kategorien
{% endblock %}
{% block content %}
<ul>
{% for cat in categories %}
<li>
<a href="{% url 'events:category_detail' cat.slug %}">{{cat.name}}</a>
</li>
</a>
{% endfor %}
{% endblock %}
Pro Schleifendurchlauf bauen wir ein Listen-Elemente li, und setzen dort einen Hyperlink zu der jeweiligen Kategorie.
Der Hyperlink wird mit einem url-Tag entwickelt, der in der Form APP-Name:PFAD-Name angeben wird, sowie dahinter die jeweiligen Parameter.
Mehr dazu in der Doku: https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#url
Verlinkungen mit dem URL-Tag
Einen templates-Tag, den wir noch nicht besprochen haben, ist der url
-Tag.
Dieser Tag ist dafür da, URLs zu generieren.
Der URL-Tag hat immer folgendes Schema:
{%url "<APP_NAME:PATH_NAME>" <PATH_ARGUMENTS (optional)> %}
APP_NAME: das entspricht der Variable
app_name
aus denurls.py
der App, also hierevents
(siehe Das Anlegen der ersten App).PATH_NAME: das enstpricht dem Name-Argument der path-Funktion aus den App-Urls.
path("category/<slug:slug>", views.category_detail, name="category_detail")
PATH_ARGUMENTS: falls unsere Route einen oder mehrere Parameter definiert hat, in unserem Fall also einen Slug, werden diese als leerzeichen-separierte Liste angegeben.
Mehr zum Thema url-Template Tag
in der Django-Doku:
https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#url
URL-Tag vs. hardkodierte URL
Wir erstellen die Verlinkungen über den url-Tag
, und das ist auch der
richtige Weg, dies zu tun. Wir könnten die Verlinkung natürlich auch direkt
hardkodiert erstellen, zb so:
<a href="/events/category/{{cat.slug }}">{{cat.slug}}</a>
Das würde gehen, gilt aber als schlechter Stil. Falls sich nämlich der
URL-Pfad der Route ändern würde, zum Beispiel von events/category
zu
events/genre
, müsste man den Link an allen Stellen, wo man ihn so
hardkodiert definiert hat, manuell ändern. Und diese Änderungen kommen häufiger vor, als man meinen würde.