Die 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:
Daten löschen
Wir löschen unsere Testdaten von der Shell-Übung nun aus der Datenbank. Wir öffnen nun also die Django Shell mit folgendem Befehl:
uv run manage.py shell
und löschen die Daten mit folgendem Kommando:
>>> 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 jetzt schon mal 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.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, "kleine Gruppe"
Wir wollen den Namen der Kategorie
eindeutighaben, deshalb setzen wir die name-Eigenschaft aufunique=True. Falls ein gleichlautender Name eingetragen wird, quittiert das die Datenbank mit einemIntegrity 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.
Mehr zu Meta-Klassen hier:
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 Darstellung für Benutzer verständlich ist, versehen wir die einzelnen Werte mit einem sprechenden Label, z. B. „große Gruppe“. Dieses Label wird z. B. in Formularen oder im Admin-Interface angezeigt.
Choice-Fields
Auswahlfelder werden in modernen Django-Anwendungen als Klassen definiert, die
von models.IntegerChoices oder models.TextChoices erben. Diese Klassen
basieren intern auf dem Enum-Modul der Python-Standardbibliothek.
Jeder Eintrag besteht dabei aus einem gespeicherten Wert und einem lesbaren Label.
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, definieren wir eine 1:n-Beziehung zwischen Event und Benutzer. Dafür verwenden wir einen Foreign Key auf das im Projekt verwendete User-Model.
Wichtig ist, dass wir nicht direkt das Standardmodell (django.contrib.auth.models.User) importieren, sondern über get_user_model() darauf zugreifen. Der Grund: Django erlaubt es, das User-Model auszutauschen. Wenn wir hart auf das Standardmodell referenzieren, würde unser Code in solchen Fällen brechen.
Mit get_user_model() erhalten wir immer das tatsächlich konfigurierte User-Model, egal ob Standard oder angepasst.
Nur eingeloggte Benutzer sollen später Events erstellen dürfen. Der aktuell angemeldete User wird dann automatisch als Autor gesetzt.
Im weiteren Verlauf des Buches werden wir selbst ein eigenes User-Model definieren. Durch die Verwendung von get_user_model() bleibt unser Code bereits jetzt kompatibel damit.
from django.contrib.auth import get_user_model
# Liefert das aktuell im Projekt konfigurierte User-Model
User = get_user_model()
class Event(models.Model):
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="events"
)
Moment mal. Was ist denn eigentlich ein User-Model?
Das User-Model ist ein ganz normales Django-Model mit vordefinierten Feldern wie username und password.
Dafür existiert automatisch eine entsprechende Datenbanktabelle.
Zusätzlich bringt das User-Model zentrale Funktionen wie Authentifizierung,
Passwort-Hashing und Benutzerverwaltung bereits mit.
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.
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.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, "kleine Gruppe"
MEDIUM = 5, "mittelgroße Gruppe"
BIG = 10, "große Gruppe"
LARGE = 20, "sehr große Gruppe"
UNLIMITED = 0, "ohne Begrenzung"
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.
uv run manage.py createsuperuser
Benutzername: admin
E-Mail-Adresse: hello@blablub.de
Password:
Password (again):
Superuser created successfully.
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 unser Model verändert haben, müssen wir erneut eine Datenbankmigration durchführen. Zunächst erzeugen wir die neue Migrationsdatei für die App events:
uv run manage.py makemigrations events
Beim Ausführen dieses Befehls tritt nun ein Problem auf:
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:
Was ist hier das Problem?
Wir haben dem bestehenden Model ein neues Feld author hinzugefügt, das nicht null sein darf. Die zugehörige Datenbanktabelle existiert jedoch bereits und enthält möglicherweise schon Datensätze.
Die Datenbank steht nun vor folgendem Problem:
Für neue Datensätze ist klar, wie das Feld gefüllt wird
Für bereits existierende Datensätze gibt es keinen Wert für das neue Feld
Da null nicht erlaubt ist, benötigt Django einen Default-Wert, um die bestehenden Zeilen der Tabelle zu aktualisieren.
Ohne diese Angabe kann die Migration nicht durchgeführt werden, da die Datenbank sonst in einen inkonsistenten Zustand geraten würde.
Was passiert in der Datenbank?
Vor der Änderung sah unsere Tabelle vereinfacht so aus:
id | name | date
-------------------------
1 | Konzert | 2026-01-01
2 | Workshop | 2026-02-10
Nun fügen wir das neue Feld author hinzu:
id | name | date | author
------------------------------------------
1 | Konzert | 2026-01-01 | ???
2 | Workshop | 2026-02-10 | ???
Für die bereits vorhandenen Datensätze existiert jedoch kein Wert für author.
Da das Feld nicht null sein darf, kann die Datenbank diese „Lücken“ nicht akzeptieren.
Django verlangt daher einen Default-Wert, um diese fehlenden Einträge zu füllen und die Migration konsistent durchführen zu können.
Uns bleiben zwei Möglichkeiten: Entweder wir wählen Option 2 und machen das Feld temporär nullable (null=True), sodass bestehende Datensätze keinen Wert benötigen. Da der author jedoch fachlich kein optionales Feld sein soll, verwerfen wir diese Lösung.
Stattdessen entscheiden wir uns für Option 1: Wir definieren einen einmaligen (one-off) Default-Wert, der ausschließlich während der Migration verwendet wird, um die bestehenden Datensätze zu befüllen.
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.
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 einmalig für die schon bestehenden Felder und nicht für zukünftige Einträge.
uv run 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.