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:
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 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: * 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.
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.