Das Modelle ausbauen

Wir wollen das Event-zbw. Category-Model nun ausbauen, und mit weiteren Feldern anreichern. Unsere beiden Models sollen dann ungefähr so aussehen:

../_images/uml_class_3.png

Daten löschen

Wir löschen unsere Testdaten von der Shell-Übung nun aus der Datenbank.

>>> from events.models import Event, Category
>>> Event.objects.all().delete()
>>> Category.objects.all().delete()

Damit sind alle Testdaten aus der Datenbank entfernt. Wir werden keine Testdaten mehr von Hand anlegen, sondern uns später per Factory Boy Testdaten automatisch generieren lassen. Zusätzlich werden wir lernen, wie wir Testdaten auch als JSON exportieren können.

Mehr zu Factory Boy findet sich hier: https://factoryboy.readthedocs.io/en/stable/

Wenn man viele Models hat, kann die oben beschriebene Vorgehensweise mühsam sein. Wir werden im Kapitel Django Extensions Addon ein Subkommando kennenlernen, welches uns diese Arbeit womöglich erleichtert.

Das Kategorie-Modell ausbauen

Die Daten sind gelöscht, wir können uns jetzt daran machen, die Models zu ändern. Ändern wir zuerst das Category-Model in event_manager/events/models.py ab:

from django.db import models
from django.urls import reverse
from django.contrib.auth import get_user_model


User = get_user_model()


class DateMixin(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Category(DateMixin):
    """Eine Kategorie für einen Event."""

    name = models.CharField(max_length=100, unique=True)
    sub_title = models.CharField(max_length=200, null=True, blank=True)
    description = models.TextField(null=True, blank=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name


class Event(DateMixin):
    """Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""

    class Group(models.IntegerChoices):
  • Wir wollen den Namen der Kategorie eindeutig haben, deshalb setzen wir die name-Eigenschaft auf unique=True. Falls ein gleichlautender Name eingetragen wird, quittiert das die Datenbank mit einem Integrity Error.

Zuletzt wollen wir die Klasse noch mit einer Meta-Klasse anreichern.

class Meta:
    ordering = ["name"]

Wenn wir jetzt alle Objekte mit Category.objects.all() aufrufen, werden die Einträge nun per default nach name sortiert, und nicht mehr nach der Reihenfolge des Einfügens in der Datenbank.

Meta-Klassen

Mit Meta-Klassen können wir unser Model mit Meta-Daten anreichern. Sortier-Reihenfolge, Plural-Namen in der Administrationsoberfläche, Tabellennamen, Permissions und vieles mehr könnten wir hier definieren.

Mehr zu Meta-Klassen hier: * https://docs.djangoproject.com/en/ref/topics/db/models/#meta-options * https://docs.djangoproject.com/en/stable/models/options/

Das Event-Modell ausbauen

Ändern wir nun auch noch das Event-Modell in event_manager/events/models.py ab.

Wir wollen für die Events die Mindest-Gruppengröße (an Personen) als Select-Feld angeben, also zb. kleine Gruppe (ab 2 Personen), große Gruppe (ab 20 Personen) und so weiter. Dafür schreiben wir die innere Klasse Group und erben von models.IntegerChoices. Dann weisen wir einem neuen IntegerField namens min-group diese choices zu.

class Group(models.IntegerChoices):
    SMALL = 2, "kleine Gruppe"
    MEDIUM = 5, "mittelgroße Gruppe"
    BIG = 10, "große Gruppe"
    LARGE = 20, "sehr große Gruppe"
    UNLIMITED = 0, "ohne Begrenzung"

min_group = models.IntegerField(choices=Group.choices)

Damit die Ausgabe der Labels nicht ganz so kurzatmig klingt, hängen wir noch ein sprechendes Label wie zum Beispiel große Gruppe an.

Choice-Fields

Auswahlfelder legt man in modernen Django-Applikationen als Klasse an, die von models.IntegerChoices bzw. models.StringChoices erbt. Diese beiden Klassen basieren intern auf dem Enum- Modul der Python Standard-Bibliothek.

Mehr zu Python Enum findet sich in der Python Doku: https://docs.python.org/3/library/enum.html

Wir setzen das Name-Feld ebenfalls unique.

name = models.CharField(max_length=100, unique=True)

Da jedes Event von einem Autor erstellt wird, sezten wir jetzt einen Foreign Key auf unser user.User Model und definieren damit eine 1-n Beziehnung. Wer immer gerade im System angemeldet ist, kann einen Event erstellen. Nur eingeloggte User sollen später Events erstellen können. Dazu hatten wir ganz oben schon das User-Model importiert.

from django.contrib.auth import get_user_model
...

# Das ist das im Projekt gentutzte User-Model:
User = get_user_model()

...

# unser Event-Model setzt einen Foreign-Key auf das User-Model.
author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="events"
    )

Das related_name-Attribut ist wie schon bei dem ForeignKey auf die Kategorie auf die Bezeichnung events gesetzt. Der related_name ermöglicht uns den Zugriff quasi von der anderen Seite aus, d.h. der Zugriff von einem User-Objekt auf seine Events.

Der Zugriff erfolgt über den Related Manager, der im Grunde ähnlich funktioniert, wie der Manager (siehe auch die Model API).

Wir können also später alle Events, die ein User mit der ID 1 eingestellt hat, zum Beispiel wie folgt abfragen:

>>> # user Objekt abfragen
>>> user_1 = User.objects.get(pk=1)
>>>
>>> # der Related Manager
>>> user_1.events
>>>
>>> # die all()-Methode des Related Managers aufrufen
>>> # d.h. alle Events, die ein User eingestellt hat
>>> user_1.events.all()

Klassen-Beziehungen

Wir können in Django 1-1 (One-to-One), 1-n (One-to-Many) und n-m (Many-to-Many) Beziehungen abbilden. Eine 1-1 Beziehung wäre zum Beispiel ein User-Profil, eine 1-n Beziehung ein User, der viele Events hat und eine n-m Beziehung wären zum Beispiel Tags, die Events zugeordnet werden können. Jeder Event hat mehrere Tags und jedes Tag kann mehreren Events zugeordnet werden.

../_images/1_N.png

Eine 1 zu N Beziehungen, wie sie unter Pinguinen populär ist.

Last but not least fügen wir auch hier noch eine Meta-Klasse ein:

class Meta:
    ordering = ["name"]

Die Models der App Event

So sieht unsere Datei event_manager/events/models.py nun aus:

from django.db import models
from django.urls import reverse
from django.contrib.auth import get_user_model


User = get_user_model()


class DateMixin(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Category(DateMixin):
    """Eine Kategorie für einen Event."""

    name = models.CharField(max_length=100, unique=True)
    sub_title = models.CharField(max_length=200, null=True, blank=True)
    description = models.TextField(null=True, blank=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name


class Event(DateMixin):
    """Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""

    class Group(models.IntegerChoices):
        SMALL = 2
        MEDIUM = 5
        BIG = 10
        LARGE = 20
        UNLIMITED = 0

    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(null=True, blank=True)
    date = models.DateTimeField()
    sub_title = models.CharField(max_length=200, null=True, blank=True)
    is_active = models.BooleanField(default=True)
    min_group = models.IntegerField(choices=Group.choices)
    category = models.ForeignKey(
        Category, on_delete=models.CASCADE, related_name="events"
    )
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="events")

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name

Einen Superuser anlegen

Bevor wir das Model migrieren können, müssen wir erstmal einen Superuser anlegen. Denn einen User benötigen wir zumindest, dem wir Events zuweisen können.

Was ist ein Superuser?

Der Superuser ist quasi der Admin der Applikation. Er hat besitzt neben dem staff-Status, der es erlaubt, auf das Backend zuzugreifen, standardmäßig auch alle Rechte.

Admin User anlegen

Wir rufen auf der Shell folgendes Manage-Commando auf und geben die entsprechenden Daten ein. Hinweis: die eingetippten Passwörter werden aus Sicherheitsgründen auf der Shell nicht angezeigt.

python manage.py createsuperuser

Benutzername: admin
E-Mail-Adresse: hello@blablub.de
Password:
Password (again):
Superuser created successfully.

Nicht wundern: bei den beiden Password-Eingaben erscheint aus Sicherheitsgründen keine Ausgabe.

Best Practice: Superuser

Wir müssen den Superuser nicht mit Realdaten anlegen, dh. die einzugebende Email-Adresse muss aktuell noch nicht existieren. Auf der Entwicklungsplattform, also zb. dem lokalen Rechner, liegen nur Testdaten, die hin und wieder auch gelöscht werden. Reale Userdaten liegen ausschließlich auf der Live-Umgebung. Wir werden später auch noch Testuser mit einer Faker-Factory anlegen, um einige Testuser im System zu haben.

Wichtig Die Zugangsdaten für den neu angelegten Adminuser sollte man sich natürlich merken! Das ist allen voran der Nutzername und das Password.

Migrationen erstellen und durchführen

Nachdem wir unsere Models verändert haben, müssen wir jetzt wieder eine Datenbankmigration durchführen. Wir können jetzt mit dem Befehl die Migrationsdatei für die App Events erstellen:

python manage.py makemigrations events

Da schon Felder in der Datenbanktabelle zu finden sind und potentiell Daten in diesen stehen, benötigt Django Default-Werte, die in diese schon bestehenden Felder eingetragen werden können. Deshalb kommt auch gleich diese Meldung:

It is impossible to add a non-nullable field 'author' to event without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.
Select an option:

Django teilt uns also mit, dass ein Feld namens author, welches nicht leer sein darf, nicht hinzugefügt werden kann, ohne einen Wert anzugeben, der in die bereits existierenden Einträge eingefügt werden soll.

MISSING IMAGE: Zeichnung Tabelle mit Spalten und Fragezeichen.

Uns bleiben zwei Möglichkeiten: entweder, wir wählen 2) und machen die Felder nullable, dh. setzen im Event-Model für den author null=True. Da aber der Author kein optionales Feld sein soll, wählen wir die andere Variante: 1.

Bei 1 definieren wir ad hoc einen one-off Default-Wert für die Migration.

Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.
Select an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>>

Wir tippen jetzt wieder 1 ein, denn das enstpricht der ID des Userobjektes, welches wir gerade vorhin mit createsuperuser angelegt hatten. Hier aufpassen, dass es dieses Userobjekt auch tatsächlich gibt.

Nun müssen wir noch einen Default-Value für das min_group Feld eintragen. Da wählen wir erst wieder 1 und im zweiten Schritt den Defaultwert 10.

>>> 1
It is impossible to add a non-nullable field 'min_group' to event without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.
Please an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>> 10
Migrations for 'events':
  events/migrations/0002_alter_category_options_alter_event_options_and_more.py
    - Change Meta options on category
    - Change Meta options on event
    - Add field author to event
    - Add field min_group to event
    - Alter field name on category
    - Alter field name on event

Jetzt können wir die Datenbank-Migration durchführen. Wenn wir die entsprechende Migrationsdatei angucken, sehen wir, dass dort in der Migrationsvorschrift ein default-Wert steht. Dieser gilt aber nur für die schon bestehenden Felder und nicht für zukünftige Einträge.

python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, events, sessions, user
Running migrations:
  Applying events.0002_alter_category_options_alter_event_options_and_more... OK

In unserem SQLBrowser sollten wir jetzt die aktualisierte Tabelle events_event finden.

../_images/event_model_final.png