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.

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/category_list_simple.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/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.

Tags

Tags sehen so aus: {% tag %}. 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.

Im Code oben siehen wir mehrere sich öffnende-und schließende Tags, zb. den for-Tag. Er definiert einen Loop, ähnlich wie man das von Python kennt. Wenn der For-Loop beendet ist, wird er mit endfor geschlossen.

Beispiele für Tags

For - Tag

{% for company in companies %}
    <li>{{ company.name }}</li>
{% endfor %}

if - else - Tag

{% if events %}
    Events vorhanden
{% else %}
    keine Events vorhanden!
{% endif %}

if - elif - else (hier mit einer Filter-Direktive)

{% if companies|length >= 5 %}
    viele Firmen
{% elif companies|length > 3 %}
    mittelviele Firmen
{% else %}
    wenige Firmen
{% endif %}

Mehr zu Tags und Variablen in der Doku: https://docs.djangoproject.com/en/4.0/ref/templates/language/

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 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("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.