.. _app_extend:
.. index::
single: Model
single: related name
single: CharField
single: choices
single: Foreign Key
single: 1:N
single: BooleanField
single: Migrationen
single: createsuperuser
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:
.. image:: /images/uml_class_3.png
Daten löschen
===================================
Wir löschen unsere Testdaten von der Shell-Übung nun aus der Datenbank.
.. code-block:: python
>>> 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:**
``_
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:
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/2_events.py
:language: python
:lines: 1-34
* 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.
.. code-block:: python
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.
.. admonition:: 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:
* ``_
* ``_
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.
.. code-block:: python
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.
.. admonition:: 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:**
``_
Wir setzen das Name-Feld ebenfalls ``unique``.
.. code-block:: python
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.
.. code-block:: python
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:
.. code-block:: python
>>> # 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()
.. admonition:: 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.
.. figure:: /images/1_N.png
:scale: 150%
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:
.. code-block:: python
class Meta:
ordering = ["name"]
Die Models der App Event
------------------------------
So sieht unsere Datei :file:`event_manager/events/models.py` nun aus:
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/2_events.py
:language: python
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.
.. admonition:: 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.
.. code-block:: bash
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.
.. admonition:: 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:
.. code-block:: bash
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:
.. code-block:: bash
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.
.. code-block:: bash
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.
.. code-block:: bash
>>> 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.
.. code-block:: python
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.
.. image:: /images/event_model_final.png