.. _setup_templates:
.. index::
single: Templates
single: Django Template Sprache
single: APP_DIRS
single: Verlinkungen
single: URL-Tag
single: Twig
single: statische Dateien
single: base.html
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. Jede andere Ort wäre aber auch denkbar.
App-spezifische Templates, wie die Übersicht der Kategorien, speichern wir auch
in der jeweiligen App, zb.
``event_manager/events/templates/events/categories.html``. Damit
Django in den Apps nach Templates sucht, muss ``APP_DIRS`` in den
``settings.py`` auf ``True`` gesetzt werden.
.. code-block:: python
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 ``_
.. admonition:: 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 ebenfalls diese Blöcke, diesmal allerdings mit
Inhalt gefüllt, und rendern sie in das Base-Template. Das geschieht dadurch, dass das Sub-Template das
Basis-Layout erweitert ``(extends)``. Durch geschickte Schachtelung, können
tiefe Template-Hierachien geschaffen werden.
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:
.. literalinclude:: ../../../src/events/base_html_1.html
:language: html+django
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.
Der LANGUAGE_CODE ist der Code, den wir in der settings.py für die aktuelle
Sprache eingetragen hatten. Er wäre an dieser Stelle aktuell noch nicht
notwendig, aber wir nutzen ihn trotzdem schon mal. Für diesen Zweck hatten wir
``{% load i18n %}`` ins Template geladen.
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 nicht direkt aufgerufen, sondern wir werden sie quasi vererben.
.. admonition:: Tags und Variablen
die Django-Template-Sprache definiert zwei Typen von Anweisungen: ``Tags
und Variablen``. Tags bieten Funktionalität während Variablen Platzhalter
sind, in die Werte gerendert werden. Beides hatten wir in
``event_manager/events/templates/events/categories.html`` schon
gesehen.
Tags haben dir Form ``{% TAG %}`` während Variblen durch die doppelten
geschweiften Klammern definiert werden ``{{ VARIABLE }}``. Mehr dazu
später.
Das Base-Template erweitern
=========================================
Um das Base-Template zu nutzen, ändern wir ``event_manager/events/templates/events/categories.html`` wie folgt ab:
.. code-block:: html+django
{% extends 'base.html' %}
{% block head%}
Übersicht der Kategorien
{% endblock %}
{% block content %}
{% for cat in categories %}
{{cat.name}}
{% 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.
Wenn wir jetzt mit ``python manage.py runserver`` den Entwicklungsserver
starten und auf die Kategorie-Übersicht navigieren (), sehen wir im Ergebnis
keinen Unterschied zu vorher. Ist ja auch klar, wir hatten am Style an sich
nichts verändert.
Bootstrap HTML
------------------
Wir wollen nun das Base-Template nochmal abändern und es ein bisschen besser formatieren. Dazu nutzen wir das Bootstrap-Framework (``_).
Die neue ``base.html`` Datei sieht nun so aus:
.. rli::
https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/templates/events/base_html_2.html
:language: html+django
In dem Template finden sich einige Template-Kommentare, die mit dem
``comment``-Tag umgesetzt wurden. Insbesonder wurden damit HTML-Elemente und
Verlinkungen auskommentiert, die aktuell noch zu einem Fehler führen würden, da
die entsprechenden Pfade in den ``urls.py`` - Dateien nicht angelegt wurden:
.. code-block:: html+django
{% comment %}
{% endcomment %}
In diesem Beispiel sieht man, dass der Login-Button auskommentiert wurde. Den
Kommentar werden wir entfernen, wenn wir die Authentifizierung implementieren.
Den Code kann man sich hier bequem runterladen:
`Code Base HTML: `_
statische CSS-Datei
--------------------
Damit dieses Template später funktioniert und richtig angezeigt wird, muss eine
statische CSS-Datei angelegt werden. Dafür werden wir unter
``event_manager/static`` ein Verzeichnis für statische Dateien anlegen.
Dort erstellen wir noch ein Verzeichnis für CSS-Dateien unter ``event_manager/static/css/`` und legen dort die Datei
``style.css`` an.
In diese neu erstelle CSS-Datei unter ``event_manager/static/css/style.css`` kopieren wir folgenden Inhalt.
.. code-block:: css
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: ref;
}
.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;
}
ul {
padding-left: 0px;
list-style-type: none;
margin-left: 0px;
}
ul li a {
text-decoration: none;
}
.card a {
text-decoration: none;
}
.card h3,
.card p {
color: #333;
}
**Der aktuelle Verzeichnisbaum sieht aktuell so aus**:
.. code-block:: bash
├── event_manager
│ ├── db.sqlite3
│ ├── event_manager
│ │ ├── settings.py
│ │ ├── templates
│ │ │ └── base.html
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── events
│ ├── manage.py
│ ├── static
│ │ ├── css
│ │ │ └── style.css
│ │ └── images
│ │ └── penglogo.png
│ └── user
│ ├── models.py
│ └── views.py
├── README.md
├── requirements-dev.in
├── requirements-dev.txt
├── requirements.in
└── requirements.txt
Das neu angelegte ``static`` - Verzeichnis liegt also auf der gleichen Ebene, wie
das ``manage.py``. Dort befinden sich aktuell die ``css/style.css`` und das
Logo.
.. admonition:: 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 ``_ heruntergeladen
und in das statische Verzeichnis unter ``event_manager/static/images/penglogo.png`` kopiert werden.
Auf das HTML bzw. CSS in der Datei einzugehen, würde den Rahmen dieses Buches
sprengen. Im Netz finden sich aber unzählige Tutorials, um schnell einen
Überblick über CSS zu erhalten.
Im base-Template haben wir noch einen weiteren Block ``title`` eingefügt, um der rerenderten
Seite einen Seitentitel zu geben. Dieser Seitentitel wird zum Beispiel in dem
entsprechenden Brower-Tab angezeigt.
Ändern wir ``event_manager/events/templates/events/categories.html`` noch ab:
.. code-block:: html+django
{% extends 'base.html' %}
{% block title %}
Übersicht der Kategorien
{% endblock %}
{% block head %}
Übersicht der Kategorien
{% endblock %}
{% block content %}
{% for cat in categories %}
{{cat.name}}
{% endfor %}
{% endblock %}
Mehr dazu unter ``_
Wenn wir jetzt die Seite unter ``_ sehen wir zwar ein verändertes
Layout, allerdings fehlt das Logo. Es fällt auch auf, dass die Datei
``style.css`` nicht geladen werden konnte. Damit diese beiden Resourcen gefunden werden, müssen wir in der
``settings.py`` noch etwas ändern. Das machen wir aber im nächsten Kapitel
(siehe **statische Dateien einbinden**).
Intermezzo: die Django-Template-Sprache
=========================================
Um das Arbeiten mit Templates zu erleichtern, haben wir gesehen, dass uns
Django die Möglichkeit gibt, direkt im Template über Listen zu iterieren oder Variablen
auszugeben. Und dazu haben wir die sogenannte **Django-Template-Sprache** genutzt.
Solche Template-Engines gibt es in den meisten Web-Frameworks in ähnlicher Art
und Weise, zum Beispiel das bekannte ``Twig`` aus dem PHP-Framework Symphony
[#symphony]_.
Wozu noch eine neue Sprache?
--------------------------------
Wenn wir Daten an den User ausgeben wollen, gehört diese Aufgabe zur
sogenannten Präsentationsebene. Die Datenverarbeitung in den Views und Models
ist eher auf der Applikationslogik zu finden. Generell gilt: die Formatierung
und optische Aufbereitung von Daten zum Zweck der Präsentation darf nicht in
der Appikationsebene vorgenommen werden. Wir wollen Logik und Ausgabe strikt getrennt
halten.
Variablen
-------------
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. Variablen sehen so aus: ``{{ Variable }}``.
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.
Wir haben in den Variablen via der Dot-Notation also auch ganz wie gewohnt Zugriff auf die Attribute
eines Objekts, wie wir das von Python gewohnt sind.
Filter
----------
Der Inhalt der Variablen kann im Template an Filter übergeben werden, die vom
Prinzip her ähnlich aufgebaut sind, wie der Pipe-Operator auf der Unix - Shell.
Ein Filter kann über diesen Operator an die Variable angehängt und deren
Ausgabe verändern, zum Beispiel den Beschreiungstext in Kleinbuchstaben ausgeben:
.. code-block:: html+django
{{ company.description|lower }}
.. csv-table:: Wichtige Filter in den Django-Templates
:widths: 20, 60
``capfirst``, enspricht dem Capitalize aus Python
``date``, zur schönen Formatierung von Datumsangaben
``default``, Der Defaultwert einer leeren Variable
``escape``, konvertiert Zeichen in HTML-Entsprechungen
``linebreaks``, Konvertiert Newlines in HTML-Breaks
``pluralize``, pluralisiert Wörter
``striptags``, löscht HTML-Tags
Filter lassen sich auch **chainen,** dh. verketten. Der fertige Filter reicht den
Output an den nächsten Filter in der Reihe weiter:
.. code-block:: html+django
{{ company.description|striptags|lower|capfirst }}
So wird die Beschreibung erst von HTML-Tags bereinigt, dann in Kleinbuchstaben
unmgewandelt um schließlich via capfirst alles wieder zu Kapitalisieren. Erst
dann wird die Variable in den HTML-Code gerendert.
Der Vollständigkeitshalber sei noch erwähnt, dass es nicht sonderlich schwierig
ist, Filter selber zu erstellen. Filter sind nichts anderes als
Python-Funktionen, die man registrieren und laden muss, um sie im Template zu
nutzen. Mehr dazu findet sich in der Doku.
**Weitere Built-in Filter finden sich hier:**
https://docs.djangoproject.com/en/stable/ref/templates/builtins/#filter
Tags
---------
Tags sind komplexer als Variablen: Einige erstellen Text in der Ausgabe, andere
steuern den Fluss durch Ausführen von Schleifen oder Logik und einige laden
externe Informationen in die Vorlage, die von späteren Variablen verwendet
werden sollen. Tags sehen so aus: {% tag %}. Es gibt Tags, die aus einem
öffnenden und schließenden Teil bestehen sowie einzelstehende Tags.
Wir hatten im Code bisher den ``for`` - Tag gesehen.
**For - Tag**
.. code-block:: html+django
{% for company in companies %}
{{ company.name }}
{% endfor %}
**if - else - Tag**
Natürlich können wir auch den Kontrollfluss mit Bedingungen regeln:
.. code-block:: html+django
{% if categories.exist %}
Es sind Kategorien vorhanden
{% else %}
Es befinden sich keine Kategorien im System!
{% endif %}
**if - elif - else (hier mit der Filter-Direktive length)**
Wie wir sehen können, lassen sich sogar arithmetische Operatoren in der
Django-Template-Sprache nutzen:
.. code-block:: html+django
{% if categories|length > 5 %}
Es binden sich mehr als 5 Kategorien im System
{% elif companies|length > 3 %}
Es binden sich mehr als 3 Kategorien im System
{% else %}
Es binden sich wenige Kategorien im System
{% endif %}
Die Django-Template-Sprache ist eine ziemlich komplexe Sprache und bietet
unzählige Möglichkeiten, Daten zu präsentieren.
**Mehr zu Tags und Variablen in der Doku:**
``_
So sieht unsere Seite zur Zeit aus:
.. image:: /images/category_overview_neu.png
Die Detailseite einer Kategorie
=========================================
Um von der Kategorie-Übersicht auf die Detailseite einer Kategorie
weiterzuleiten, müssen wir die Liste verlinken. Die URL zur Detailseite zu
einer Kategorie ist allerdings noch nicht festgelegt, das müssten wir machen,
bevor wir die Verlinkung einbauen können.
Dazu ändern wir die ``urlpatterns`` in den ``event_manager/events/urls.py`` ab:
.. code-block:: python
urlpatterns = [
path("hello_world", views.hello_world, name="hello_world"),
path("categories", views.categories, name="categories"),
# diese Zeile hinzufügen:
path("category/", 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:
.. code-block:: python
from django.shortcuts import render
from django.http import Http404
from .models import Event, Category
def category_detail(request, pk: int):
"""
die Detailseite einer Kategorie
http://127.0.0.1:8000/events/category/3
"""
try:
category = Category.objects.get(pk=pk)
except Category.DoesNotExist:
raise Http404("Diese Kategorie existiert nicht")
return render(request, 'events/category_detail.html', {
'category': category
})
.. admonition:: 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:
.. code-block:: html+django
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Kategorie {{category}}
{%endblock%}
{% block head %}
{%endblock%}
Wir definieren hier unter anderem einen Link, der uns zurück zur Übersichtsseite
führt. Wie solche Verlinkungen in Django funktionieren, sehen wir gleich weiter
unten genauer.
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``. Falls hier ein Not-Found Error
kommt, muss womöglich eine andere ID gewählt werden (dazu in der
Admin-Oberfläche nachschauen, welche Kategorien vorhanden sind).
.. image:: /images/category_detail_1.png
Prüfung ob Vorhanden und der 404-Fehler
-----------------------------------------------
In der Funktion ``category_detail`` hatten wir im try-except-Block geprüft,
ob das Selektieren des Objekts über die übergebenen ID keinen Fehler schmeisst,
und hatten im Erfolgsfall das Objekt in category gespeichert. Im Fehlerfall
hatten wir mit ``raise`` eine ``Http404``-Exception ausgelöst.
Dieser Fall, also das Prüfen des Vorhandenseins eines Objektes, ist ein so
häufiger Use-Case, dass es dafür einen Shortcut gibt: die Funktion
``get_object_or_404``. Die macht das, was wir vorhin umständlich gemacht
hatten, in einer Zeile.
Wir ä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`` aus den Shortcuts.
.. code-block:: python
from django.shortcuts import render, get_object_or_404
from django.http import Http404
def category_detail(request, pk: int):
"""
http://127.0.0.1:8000/events/category/3
"""
category = get_object_or_404(Category, pk=pk)
return render(request, 'events/category_detail.html', {
'category': category
})
.. admonition:: 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/3
Die Detailseite verlinken
-----------------------------------------------
Zuletzt wollen wir noch die Verlinkung auf die Kategorie bauen.
Ändern wir dazu ``event_manager/events/templates/events/categories.html`` noch ab.
.. code-block:: html+django
{% extends 'base.html' %}
{% block title %}
Übersicht der Kategorien
{% endblock %}
{% block head %}
{% 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 angegeben, der in der Form
**APP-Name:PFAD-Name** angeben wird, sowie dahinter die jeweiligen Parameter.
Mehr dazu in der Doku:
``_
Die Verlinkungen sollten jetzt blau hinterlegt sein:
.. image:: /images/category_overview_verlinkt.png
Verlinkungen mit dem URL-Tag
-----------------------------------------------
Einen Template-Tag, den wir noch nicht besprochen haben, ist der ``url``-Tag.
Dieser Tag ist dafür da, URLs zu generieren. Zum Erstellen der URL sind zwei
Dinge wichtig: der ``app_name`` in der urls.py und der Name des Pfades, so wie
er in der ``path``-Funktion der urlpatterns festgelegt wurde.
Der URL-Tag hat immer folgendes Schema:
..........................................
{% url "<**APP_NAME:PATH_NAME**>" <**PATH_ARGUMENTS** (optional)> %}
* ``APP_NAME:`` das entspricht der Variable ``app_name`` aus den ``urls.py`` der
App, also hier ``events`` (siehe **Das Anlegen der ersten App**).
* ``PATH_NAME:`` das enstpricht dem ``name``-Argument der path-Funktion aus den
App-Urls.
* PATH_ARGUMENTS: falls unsere Route einen oder mehrere Parameter definiert
hat, in unserem Fall also einen Primary Key, werden diese als Leerzeichen-separierte Liste angegeben.
.. code-block:: python
path("category/", views.category_detail, name="category_detail")
Mehr zum Thema ``url-Template Tag`` in der Django-Doku:
``_
.. admonition:: 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:
``{{cat}}``
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.
Wenn wir jetzt den ``runserver`` starten, sollten wir unter ``http://127.0.0.1:8000/events/categories`` die Kategorien in einer Liste sehen.
.. [#symphony] https://twig.symfony.com/