Slugs einbauen
Ein Slug ist der Teil der URL, der eine Seite eindeutig identifiziert, und er hat ein Format, das sowohl für Benutzer als auch für Suchmaschinen leicht lesbar ist.
Der Kategorie ein Slugfeld zuweisen
Wir wollen, dass die Kategorie später auch per Slug
aufgerufen werden kann,
also zb. http://127.0.0.1/events/category/sport-und-freizeit. Dazu
definieren wir ein Slug-Feld, dessen Wert in einem späteren Schritt automatisch
generiert wird.
Um den Slug-Wert eines Feldes automatisch aus dem Namen zu generieren, überschreiben wir die save()
-Methode der Klasse Category
:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
save()
wird immer aufgerufen, wenn eine Instanz dieser Klasse gespeichert
wird. Wenn das Objekt nun gespeichert wird und bisher noch KEIN Slug
vergeben wurde, setzen wir den Slug für dieses Objekt. Der Slug basiert auf dem
name
-Attribut und wird durch die Funktion slugify
aus den
django.utils.text
importiert. slugify wandelt einen Text in eine
URL-taugliche Variante um, also keine Leerzeichen, keine Umlaute und keine
sonstigen Zeichen, die in einer URL nicht erlaubt sind.
Best Practice: Slugs
Slugs dienen als eindeutige Bezeichner, ähnlich wie IDs. Auf sie wird referenziert, über sie sind Resourcen addressierbar. Ein nachträgliches Ändern von Slugs ist problematisch, da zum Beispiel von Google schon eingelesene Links dann nicht mehr gefunden werden könnten. Aus diesem Grund müssen Slugs auch eindeutig sein (unique).
Dem Event ein Slugfeld zuweisen
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
Eindeutiger Slug
Die save-Funktion
oben hat einen Nachteil: es kann passieren, dass bei
unterschiedlichem Namen trotzdem der selbe Slug erstellt wird. So erstellt
slugify
aus oe-aa den Slug oe-aa. Gleiches passiert aber bei
öe-aa.
Wir hätten dann unterschiedliche Werte für das Name-Attribut, aber
gleichlautende Slugs. Das Programm würde einen Fehler werfen, da die unique-Constraint
verletzt würde.
Bei dem Category-Modell
oben lassen wir das jetzt mal so, da die Kategorien in
unserem Projekt nicht usergeneriert sind, sondern von der Moderation so
vorgegeben werden.
Anders wird es bei den Events sein. Dort kann es bei vielen Einträgen durchaus vorkommen, dass Events den gleichen Namen unter Umständen sogar in der gleichen Categorie haben. Und da die Event-Slugs genauso wie Event-Namen eindeutig sein müssen, müssen wir das anders machen.
Deshalb nutzen wir eine selbstgeschriebene Funktion slugify_instance_name
,
die auf Basis des Namens eines Objekts einen Slug rekursiv erstellt, bis er
eindeutig ist. Dazu wird einfach eine Zufallszahl angehängt, falls der Eintrag
schon existieren sollte.
Wir erstellen eine appübergreifende Datei unter event_manager/event_manager/utils.py
und
kopieren dort folgenden Code hinein:
import random
from django.utils.text import slugify
def slugify_instance_name(instance: object, new_slug: str = "") -> str:
"""Erstelle einen eindeutigen Slug für ein Django Model.
Es wird versucht, einen eindeutigen Slug auf Basis des name-Attributes
eines Models zu erstellen. Falls der Slug schon existiert, soll die
Funktion rekursiv aufgerufen werden, bis ein Slug erstellt ist, der noch
nicht existiert. Dazu wird die random Funktion genutzt, um eine
Zufallszahl an den Slug anzuhängen.
Der alternative Slug ist vom Format {slug-name}-RANDOM_NUMBER
Args:
instance: Django Model
new_slug: str
Returns:
slug (str)
"""
min_random = 10_000
max_random = 800_000
if new_slug:
slug = new_slug
else:
slug = slugify(instance.name)
# Prüfen, ob slug schon existiert, wenn ja, rufe funktion erneut auf
# Wir müssen die eigene Instanz natürlich exkludieren bei der
# Abfrage
cls = instance.__class__
qs = cls.objects.filter(slug=slug).exclude(id=instance.id)
if qs.exists():
random_number = random.randint(min_random, max_random)
slug = f"{slug}-{random_number}"
return slugify_instance_name(instance, new_slug=slug)
return slug
Und importieren diese Funktion in unsere event_manager/events/models.py
:
from event_manager.utils import slugify_instance_name
Um diese Funktion jetzt nutzen zu können, benötigen wir noch ein Signal.
Der
Grund ist folgender und vielleicht nicht auf den ersten Blick ersichtlich: Wenn
wir (gleich im nächsten Kapitel) einen neuen Eintrag über die Django-Administrationsoberfläche erstellen
werden, nutzen wir eine Technik namens prepopulated_fields
. Diese Technik
generiert fronteindseitig einen Slug für das Objekt.
Unsere eigene, rekursive Funktion kann allerdings von der Administration nicht genutzt werden und wir würden einen Fehler provozieren, wenn wir den Slug einfach leer lassen.
Hier kommen Signale ins Spiel….
Signale: eine von Djangos Geheimwaffe
Django Signal erlauben es bestimmten Sendern
(sender), eine Gruppe von
Empfängern
(Receivern) zu benachrichtigen, dass eine Aktion stattgefunden hat. Es handelt sich
hierbei also um System-Events, die zu gewissen Aktionen von Django automatisch
ausgelöst werden und die wir abfragen können. So können wir zum Beispiel
definieren, dass für eine Instanz ein Slug erstellt werden soll, bevor
das Objekt gespeichert wird.
|
wenn ein Django-Model instantiiert wird |
|
nachdem ein Django-Model instantiiert wurde |
|
bevor ein Model gespeichert wird |
|
nachdem ein Model gespeichert wurde |
|
bevor ein Model gelöscht wurde |
|
nachdem ein Model gelöscht wurde |
|
wenn eine Many-to-Many-Beziehung verändert |
wurde |
Django Signals und inbesondere das pre_save
sind übrigens auch der
ideale Ort, um für einen neuen User ein User-Profil zu erstellen. Das ist
einfach ein Model mit Referenz auf einen User, in dem weitere Daten wie
Hobbies, User-Foto oder Freundesliste abgebildet werden können.
Wir werden in diesem Buch nicht weiter auf Signale eingehen, da es sich um ein
zu weit führendes Thema handelt. Trotzdem werden wir ein pre_save
-Signal
für das Event-Modell nutzen, um einen Slug zu erstellen.
Wir fügen in event_manager/events/models.py
ganz unten folgenden Code ein:
@receiver(pre_save, sender=Event)
def create_slug(sender, instance, *args, **kwargs):
print("pre save wurde ausgeführt")
if not instance.slug:
instance.slug = slugify_instance_name(instance, new_slug=None)
Hier haben wir jetzt ein pre_save
-Signal erstellt. Immer, wenn ein
Event-Model gespeichert wird, wird diese Funktion aufgerufen, sie ist somit der
receiver.
Der sender
ist das Event-Model selbst und das Signal ist
pre_save
. Wir haben noch ein print-Statement in die funktion gepackt, damit
man auf der Konsole sehen kann, wann das Signal ausgeführt wird. Später werden
wir das hier natürlich rauslöschen.
Damit haben wir nun unser erstes Signal erstellt! Jetzt löschen wir die
save
-Methode wieder aus dem Event-Modell, da wir die nicht mehr brauchen
und sind somit fertig.
der richtige Ort für Signale
Ein kleiner Hinweis noch: Der richtige Ort für die receiver-Funktionen bzw. überhaupt für das Arbeiten mit Signalen ist nicht die models.py
.
Wir haben das wider besseren Wissens und der Einfachheit halber hier angelegt. Wer die Muße dazu hat, möge eine event_manager/events/signals.py
anlegen.
Dort ist der richtige Ort, die Signale für die App events
zu platzieren:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from event_manager.utils import slugify_instance_name
from .models import Event
@receiver(pre_save, sender=Event)
def create_slug(sender, instance, *args, **kwargs):
print("pre save wurde ausgeführt")
if not instance.slug:
instance.slug = slugify_instance_name(instance, new_slug=None)
um die Signale jetzt auch noch aktiv zu machen, müssen noch zwei weitere Dateien verändert werden.
In der event_manager/events/app.py
legen wir noch eine ready
-Methode an, die aufgerufen wird, wenn die Klasse geladen ist:
from django.apps import AppConfig
class EventsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "events"
def ready(self):
import event_manager.events.signals
und in die event_manager/events/__init__
-Datei wird noch folgender Eintrag hinterlegt:
default_app_config = 'event_manager.events.apps.EventsConfig'
mit
prepopulated_fields
machen wir frontendseitig einen Slug aus name
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.categories, name="categories"),
# diese Zeile ändern:
path("category/<slug:slug>", views.category_detail, name="category_detail"),
]
class EventListView(ListView):
"""http://127.0.0.1:8000/events/
http://127.0.0.1:8000/events/?category=sport
"""
model = Event
paginate_by = 3
def get_queryset(self):
queryset = Event.objects.prefetch_related("category").all()
if slug := self.request.GET.get("category"):
queryset = queryset.filter(category__slug=slug)
return queryset
Zweitens haben wir einen kleinen Filter eingebaut: Wird die URL http://127.0.0.1:8000/events/?category=sport
eingegeben, erhalten wir nur noch Events aus der Kategory Sport. Dazu filtern wir das queryset nach dem Slug der Kategorie. Wir haben also einen Kategoriefilter erstellt.
Die View für die Detailseite anpassen
Dazu öffnen wir die event_manager/events/views.py
und importieren die generische DetailView
. Dann legen wir die EventDetailView
-Klasse an.
Der Event soll ebenfalls per slug
aufgerufen werden können.
from django.views.generic.detail import DetailView
class EventDetailView(DetailView):
"""
events/event/linux-user-gathering
"""
model = Event
Unser Event ist also unter http://127.0.0.1:8000/events/event/linux-user-gathering
aufrufbar.
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/<slug:slug>", views.category_detail, name="category_detail"),
path("", views.EventListView.as_view(), name="events"),
# diese Zeile hinzufügen:
path("event/<slug:slug>", views.EventDetailView.as_view(), name="event_detail"),
]
Verlinkung auf die Detailseite
Was jetzt noch fehlt, ist eine Verlinkung auf die Detailseite von der Event-Übersichtsseite aus.
Dazu öffnen wir das Template unter events/templates/events/event_list.html
und fügen einen Hyperlink mit dem url-Tag
ein. Da event/<slug:slug>
aus
den URLs einen Slug erwarten, übergeben wir den event.pkg
.
..
<a href="{% url 'events:event_detail' event.slug %}">
<li class="list-group-item list-group-item-info">
..
</li>
</a>
Unsere Template events/templates/events/event_list.html
unter sieht jetzt so aus:
{% extends 'base.html' %}
{% load i18n %}
{% block title %}
Übersicht der Events
{%endblock%}
{% block head %}
Übersicht der Events
{%endblock%}
{% block content %}
<ul class="list-group event_box">
{% for event in object_list %}
<a href="{% url 'events:event_detail' event.slug%}">
<li class="list-group-item rounded">
<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%}
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)])
Usere EventForm Klasse unter event_manager/events/forms.py
sieht jetzt so aus:
bild stimmt noch nicht!
from django import forms
from django.core.exceptions import ValidationError
from .models import Event, Category
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = "__all__"
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ("slug", "category", "author")
widgets = {
"date": forms.DateInput(
format=("%Y-%m-%d %H:%M"), attrs={"type": "datetime-local"}
),
}
labels = {
"name": "Name des Events",
"sub_title": "Slogan",
"description": "Kurzbeschreibung",
"date": "Datum des Events",
}
def clean_sub_title(self):
sub_title = self.cleaned_data["sub_title"]
illegal = ("*", "-", ":")
if isinstance(sub_title, str) and sub_title.startswith(illegal):
raise ValidationError("Dieses Zeichen ist nicht erlaubt!")
return sub_title
Den Kategorie-Slug in der Event-Übersicht darstellen
Bisher zeigen wir nur die Kategorie in der Eventübesicht an. Wir wollen
die Ansicht aber erweitern, und den Slug der Category
auch noch
darstellen.
Leider funktioniert der Zugriff via category__slug
in der list_display
-Eigenschaft
nicht. Es kommt zu einem Fehler
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
# ... mehr Code
list_display = ("date", "slug", "author", "name", "category", "category__slug")
Um das Problem zu umgehen, schreiben wir uns eine eigene Methode,
dekorieren sie mit dem @admin.display
-Dekorator und nutzen
den Methoden-Namen in list_display
, um die Spalte anzuzeigen.
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
# mehr Code
list_display = ("date",
"slug",
"author",
"name",
"category",
"category_slug")
@admin.display(description="Kategorie Slug")
def category_slug(self, obj):
return obj.category.slug
Wir können problemlos eigene Methoden in der EventAdmin
-Klasse definieren,
und diese als Felder in der Übersicht unserer Events darstellen.
Das übergebene obj
ist im Fall von category_slug
das Event-Objekt selbst.
Die fertige EventAdmin
-Klasse sieht jetzt so aus:
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
list_display = (
"date",
"slug",
"author",
"name",
"category",
"category_slug",
)
list_filter = ("category",)
search_fields = ["name"]
actions = ["make_active", "make_inactive"]
# date_hierachy = "date"
readonly_fields = ("author",)
list_display_links = ("name", "slug")
radio_fields = {
"category": admin.HORIZONTAL,
"min_group": admin.VERTICAL,
}
@admin.action(description="Setze Events active")
def make_active(self, request, queryset):
"""Set all Entries to active."""
queryset.update(active=True)
@admin.action(description="Setze Events inactive")
def make_inactive(self, request, queryset):
"""Set all Entries to inactive."""
queryset.update(active=False)
@admin.display(description="Kategorie Slug")
def category_slug(self, obj):
return obj.category.slug