.. _slugify: .. index:: single: Slug single: slugify 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``: .. code-block:: python 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. .. admonition:: 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 =================================== .. code-block:: python 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: .. code-block:: python 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``: .. code-block:: python 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.... .. admonition:: 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. .. csv-table:: Django bietet sieben eingebaute Signale: :widths: 20, 60 ``pre_init``, wenn ein Django-Model instantiiert wird ``post_init``, nachdem ein Django-Model instantiiert wurde ``pre_save``, bevor ein Model gespeichert wird ``post_save``, nachdem ein Model gespeichert wurde ``pre_delete``, bevor ein Model gelöscht wurde ``post_delete``, nachdem ein Model gelöscht wurde ``m2m_changed``, 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: .. code-block:: python @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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python urlpatterns = [ path("hello_world", views.hello_world, name="hello_world"), path("categories", views.categories, name="categories"), # diese Zeile ändern: path("category/", views.category_detail, name="category_detail"), ] .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python urlpatterns = [ path("categories", views.categories, name="categories"), path("category/", views.category_detail, name="category_detail"), path("", views.EventListView.as_view(), name="events"), # diese Zeile hinzufügen: path("event/", 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/`` aus den URLs einen Slug erwarten, übergeben wir den ``event.pkg``. .. code-block:: html+django ..
  • ..
  • Unsere Template ``events/templates/events/event_list.html`` unter sieht jetzt so aus: .. literalinclude:: ../../../src/events/event_list_3.html :language: html+django 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: .. code-block:: python 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! .. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/forms/event_form_class_3.py 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 .. code-block:: python @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. .. code-block:: python @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: .. code-block:: python @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