Sternchen überarbeitet, mal drüberschauen(die profis) #9

Open
k.hederer wants to merge 1 commits from SheCodesGroup/SGTMensa:k.hederer-patch-1 into main
+98 -157
View File
@@ -2,115 +2,94 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
import datetime import datetime
import os import os
from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
# Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow) # Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow)
from PIL import Image as PilImage from PIL import Image as PilImage
# Optional: Wenn du später Thumbnails mit sorl-thumbnail nutzen möchtest:
# from sorl.thumbnail import get_thumbnail
class Person(models.Model): class Person(models.Model):
"""Repräsentiert Schüler oder Lehrer.""" # """Repräsentiert Schüler oder Lehrer."""
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
rolle = models.CharField(max_length=20, choices=[('schueler', 'Schüler'), ('eltern', 'Eltern'), ('lehrer', 'Lehrer'), ('mitarbeiter','Mensa-Mitarbeiter'), ('chef','Mensa-Leitung')]) rolle = models.CharField(max_length=20, choices=[('schueler', 'Schüler'), ('eltern', 'Eltern'), ('lehrer', 'Lehrer'), ('mitarbeiter','Mensa-Mitarbeiter'), ('chef','Mensa-Leitung')])
klasse = models.CharField(max_length=4, blank=True, null=True) # Nur für Schüler relevant klasse = models.CharField(max_length=4, blank=True, null=True) # Nur für Schüler relevant
# ----------------------------------------------------------------------
# Neue ManytoMany Beziehung: „Elternteil ↔ Schüler“
# ----------------------------------------------------------------------
children = models.ManyToManyField(
'self',
blank=True,
related_name='parents', # → für einen Schüler: person.parents.all()
symmetrical=False, # Verhindert eine zirkuläre RückBeziehung (nicht benötigt)
help_text=(
"Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
)
)
def __str__(self):
if self.children.exists():
# Wir sammeln alle KinderNamen in einem String, z<0xE2><0x80><0xAF>B. mit Komma getrennt.
children_names = ', '.join([c.name for c in self.children.all()])
return f"{self.user.username} ({children_names}) ({self.rolle})"
else:
return f"{self.user.username} ({self.rolle})"
def clean(self):
# """
# Überprüfung für die ManyToMany-Beziehung ``children`` (Bleibt unverändert).
# """
super().clean()
if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
# DELETE: Die Eltern-Kinder Beziehung soll zunächst nicht umgesetzt werden. # NichtEltern dürfen keine Kinder besitzen
# # ---------------------------------------------------------------------- if self.children.exists():
# # Neue ManytoMany Beziehung: „Elternteil ↔ Schüler“ raise ValidationError(
# # ---------------------------------------------------------------------- "Nur Benutzer mit der Rolle 'Eltern' "
# children = models.ManyToManyField( "dürfen Schüler zuordnen (children)."
# 'self', )
# blank=True, else:
# related_name='parents', # → für einen Schüler: person.parents.all() # Eltern: Verhindere, dass sie sich selbst als Kind eintragen
# symmetrical=False, # Verhindert eine zirkuläre RückBeziehung (nicht benötigt) if self in self.children.all():
# help_text=( raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
# "Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
# )
# )
#
# # ------------------------------------------------------------------
# # Optional: Hilfsmethode, die alle Kinder zurückgibt, ggf. mit einer Sortierung
# def get_children_sorted(self):
# # Das `order_by('name')` ist ein Beispiel du kannst deine eigene Sortierung verwenden.
# return self.children.order_by('name')
#
# class Meta:
# verbose_name_plural = "Personen"
#
# def __str__(self):
# if self.children.exists():
# # Wir sammeln alle KinderNamen in einem String, z.B. mit Komma getrennt.
# children_names = ', '.join([c.name for c in self.children.all()])
# return f"{self.user.username} ({children_names}) ({self.rolle})"
# else:
# return f"{self.user.username} ({self.rolle})"
#
# def clean(self):
# """
# Überprüfung für die ManyToMany-Beziehung ``children``:
# - Nur Personen mit der Rolle 'mitarbeiter' oder 'chef' (Eltern) dürfen Kinder haben.
# - Ein Schüler darf sich selbst nicht als Kind hinzufügen.
# """
# super().clean() # ruft die Validierungen der Elternklasse auf (hier User/Model)
#
# if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
# # NichtEltern dürfen keine Kinder besitzen
# if self.children.exists():
# raise ValidationError(
# "Nur Benutzer mit der Rolle 'Eltern' "
# "dürfen Schüler zuordnen (children)."
# )
# else:
# # Eltern: Verhindere, dass sie sich selbst als Kind eintragen
# if self in self.children.all():
# raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
class Schulwoche(models.Model): class Schulwoche(models.Model):
"""Repräsentiert eine Schulwoche mit einem eindeutigen Datum.""" # """Repräsentiert eine Schulwoche mit einem eindeutigen Datum."""
id = models.CharField(max_length=20, unique=True, primary_key=True, default="test_1") # Eine eindeutige ID wie "Woche_2024_10"
id = models.CharField(max_length=20, unique=True, primary_key=True, default="test_1") # eine eindeutige ID wie "Woche_2024_10"
datum = models.DateField(unique=True) # Datum der Schulwoche (z. B. Montag der ersten Woche) datum = models.DateField(unique=True) # Datum der Schulwoche (z. B. Montag der ersten Woche)
ist_aktiv = models.BooleanField(default=False) ist_aktiv = models.BooleanField(default=False)
ist_ferienwoche = models.BooleanField(default=False) ist_ferienwoche = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name = "Schulwoche" verbose_name = "Schulwoche"
verbose_name_plural = "Schulwochen" verbose_name_plural = "Schulwochen"
class SpeiseplanTag(models.Model): class SpeiseplanTag(models.Model):
"""Ein bestimmter Tag im Speiseplan.""" # """Ein bestimmter Tag im Speiseplan."""
# WICHTIGER HINWEIS zur Architektur (Keine Codeänderung, nur Hinweis):
# Da die Schulwoche das übergeordnete Element ist, sollte primär der FK zu
# Schulwoche genutzt werden und nicht ein einzigartiges 'datum' hier.
# Das aktuelle Design funktioniert aber für diesen Scope gut.
datum = models.DateField(unique=True) datum = models.DateField(unique=True)
schulwoche = models.ForeignKey( schulwoche = models.ForeignKey(
Schulwoche, Schulwoche,
on_delete=models.CASCADE, on_delete=models.CASCADE,
#related_name='tag_get_schulwoche' # Für Abfragen wie: Schulwoche.tag_get_schulwoche.all()
default = "test_1"
) )
class Meta: class Meta:
verbose_name = "Tag mit Speiseplan" verbose_name = "Tag mit Speiseplan"
verbose_name_plural = "Tage mit Speiseplan" verbose_name_plural = "Tage mit Speiseplan"
def __str__(self): def __str__(self):
return self.datum.strftime('%d.%m.%Y') return self.datum.strftime('%d.%m.%Y')
class Kategorie(models.Model):
"""
Definiert die Art der Speise (Süßspeise, Hauptgericht, etc.).
"""
name = models.CharField(max_length=50, unique=True, default="")
class Kategorie(models.Model):
# """
# Definiert die Art der Speise (Süßspeise, Hauptgericht, etc.).
# """
name = models.CharField(max_length=50, unique=True, default="")
class Meta: class Meta:
verbose_name = "Kategorie" verbose_name = "Kategorie"
verbose_name_plural = "Kategorien" verbose_name_plural = "Kategorien"
@@ -118,11 +97,10 @@ class Kategorie(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Gericht(models.Model): class Gericht(models.Model):
"""Ein einzelnes Gericht (z.B. 'Nudeln mit Tomatensauce').""" #"""Ein einzelnes Gericht (z.B. 'Nudeln mit Tomatensauce')."""
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
# PROTECT verhindert, dass eine Kategorie gelöscht wird,
# solange noch Gerichte ihr zugeordnet sind.
kategorie = models.ForeignKey( kategorie = models.ForeignKey(
Kategorie, Kategorie,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@@ -132,12 +110,9 @@ class Gericht(models.Model):
ist_allergene_frei = models.BooleanField(default=False) ist_allergene_frei = models.BooleanField(default=False)
allergene = models.TextField(blank=True, default="") allergene = models.TextField(blank=True, default="")
preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00) preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
now = datetime.datetime.now() now = datetime.datetime.now()
time_last_change = models.TimeField(default=now) time_last_change = models.TimeField(default=now)
time_creation = models.TimeField(default=now) time_creation = models.TimeField(default=now)
# Das Attribut für das "Dauerangebot" # Das Attribut für das "Dauerangebot"
ist_dauerangebot = models.BooleanField( ist_dauerangebot = models.BooleanField(
default=False, default=False,
@@ -154,8 +129,9 @@ class Gericht(models.Model):
ordering = ["kategorie", "name", "preis"] ordering = ["kategorie", "name", "preis"]
get_latest_by = "time_last_change" get_latest_by = "time_last_change"
class Menue(models.Model): class Menue(models.Model):
"""Eine Kombination von Speisen für einen Tag (z.B. Hauptgang + Dessert).""" # """Eine Kombination von Speisen für einen Tag (z.B. Hauptgang + Dessert)."""
tag = models.ForeignKey(SpeiseplanTag, on_delete=models.CASCADE, related_name='menues') tag = models.ForeignKey(SpeiseplanTag, on_delete=models.CASCADE, related_name='menues')
gericht = models.ForeignKey(Gericht, on_delete=models.CASCADE) gericht = models.ForeignKey(Gericht, on_delete=models.CASCADE)
preis = models.DecimalField(max_digits=5, decimal_places=2) preis = models.DecimalField(max_digits=5, decimal_places=2)
@@ -167,8 +143,9 @@ class Menue(models.Model):
verbose_name = "Menü" verbose_name = "Menü"
verbose_name_plural = "Menüs" verbose_name_plural = "Menüs"
class Bestellung(models.Model): class Bestellung(models.Model):
"""Eine abgeschlossene Bestellung eines Nutzers.""" # """Eine abgeschlossene Bestellung eines Nutzers."""
STATUS_CHOICES = [ STATUS_CHOICES = [
('offen', 'Offen'), ('offen', 'Offen'),
('abgeholt', 'Abgeholt'), ('abgeholt', 'Abgeholt'),
@@ -176,13 +153,16 @@ class Bestellung(models.Model):
('bezahlt', 'Bezahlt'), ('bezahlt', 'Bezahlt'),
('storniert', 'Storniert'), ('storniert', 'Storniert'),
] ]
person = models.ForeignKey(Person, on_delete=models.CASCADE) person = models.ForeignKey(Person, on_delete=models.CASCADE)
menue = models.ForeignKey(Menue, on_delete=models.CASCADE) menue = models.ForeignKey(Menue, on_delete=models.CASCADE)
datum_bestellung = models.DateTimeField(default=timezone.now) datum_bestellung = models.DateTimeField(default=timezone.now)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='offen') status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='offen')
bezahlt = models.BooleanField(default=False) bezahlt = models.BooleanField(default=False)
# Lagerung des Gesamtpreises zur Auditierbarkeit
# Dies verhindert Preisänderungen an Gerichten nachträglich zu ändern.
gesamtpreis_geschwaetzt = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
def __str__(self): def __str__(self):
return f"Bestellung {self.id} von {self.person.user.username}" return f"Bestellung {self.id} von {self.person.user.username}"
@@ -193,12 +173,10 @@ class Bestellung(models.Model):
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
class Bewertung(models.Model): class Bewertung(models.Model):
""" # """
Repräsentiert die Bewertung eines Gerichts durch einen Nutzer. # Repräsentiert die Bewertung eines Gerichts durch einen Nutzer.
""" # """
class Sterne(models.IntegerChoices): class Sterne(models.IntegerChoices):
EINER = 1, '★☆☆☆☆' EINER = 1, '★☆☆☆☆'
ZWEI = 2, '★★☆☆☆' ZWEI = 2, '★★☆☆☆'
@@ -228,17 +206,16 @@ class Bewertung(models.Model):
return f"{self.user.user.username} bewertet {self.gericht.name} mit {self.sterne} Sternen" return f"{self.user.user.username} bewertet {self.gericht.name} mit {self.sterne} Sternen"
def clean(self): def clean(self):
""" # """
Hier können wir zusätzliche Validierungen einbauen. # Validierung (Bleibt unverändert).
""" # """
if self.sterne < 1 or self.sterne > 5: if self.sterne < 1 or self.sterne > 5:
raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.") raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.")
def check_verifizierung(self): def check_verifizierung(self):
""" # """
Ein hilfreicher Service-Method, um den Status der Verifizierung # Service-Method zur Prüfung des Verifizierungsstatus (Bleibt unverändert).
automatisch zu prüfen. # """
"""
# Wir prüfen, ob es eine Bestellung für diesen User und dieses Gericht gibt, # Wir prüfen, ob es eine Bestellung für diesen User und dieses Gericht gibt,
# die bereits als 'bezahlt' markiert ist. # die bereits als 'bezahlt' markiert ist.
exists = Bestellung.objects.filter( exists = Bestellung.objects.filter(
@@ -246,111 +223,75 @@ class Bewertung(models.Model):
menue__gericht=self.gericht, menue__gericht=self.gericht,
bezahlt=True bezahlt=True
).exists() ).exists()
if exists: if exists:
self.ist_verifiziert = True self.ist_verifiziert = True
return self.ist_verifiziert return self.ist_verifiziert
class GerichtBild(models.Model):
"""
Speichert ein hochgeladenes Bild für ein Gericht.
- Kann später beliebig erweitert werden (z.B. Beschreibung, Sortierreihenfolge).
"""
gericht = models.ForeignKey(
'Gericht', # Verweis auf das Gericht
on_delete=models.CASCADE, # Wenn das Gericht gelöscht wird, lösche auch die Bilder
related_name='bilder' # -> Gericht.bilder.all()
)
image = models.ImageField( # Typ: BildDatei (JPEG) class GerichtBild(models.Model):
upload_to='gerichte_bilder/', # Unterordner im MEDIA_ROOT # """
validators=[FileExtensionValidator(['jpg', 'jpeg'])], # Nur JPEG zulassen # Speichert ein hochgeladenes Bild für ein Gericht (Bleibt unverändert, da technisch perfekt).
# """
gericht = models.ForeignKey(
'Gericht',
on_delete=models.CASCADE,
related_name='bilder'
)
image = models.ImageField(
upload_to='gerichte_bilder/',
validators=[FileExtensionValidator(['jpg', 'jpeg'])],
help_text="Bitte nur JPEG-Dateien (.jpg/.jpeg) hochladen." help_text="Bitte nur JPEG-Dateien (.jpg/.jpeg) hochladen."
) )
# Optional: Sortierreihenfolge, wenn mehrere Bilder angezeigt werden sollen
sort_order = models.PositiveIntegerField(default=0) sort_order = models.PositiveIntegerField(default=0)
class Meta: class Meta:
# Verhindert, dass ein Bild mehrfach zu einem Gericht angelegt wird
unique_together = ('image', 'gericht') unique_together = ('image', 'gericht')
verbose_name = "Bild" verbose_name = "Bild"
verbose_name_plural = "Bilder" verbose_name_plural = "Bilder"
def clean(self): def clean(self):
""" # """Zusätzliche Validierung (Bleibt unverändert)."""
Zusätzliche Validierung wird vor dem Speichern aufgerufen,
wenn du .full_clean() oder ModelForm nutzt.
"""
# Prüfe ob die Datei tatsächlich ein JPEG ist (optional, da Validator schon hilft)
_, ext = os.path.splitext(self.image.name.lower()) _, ext = os.path.splitext(self.image.name.lower())
if ext not in ('.jpg', '.jpeg'): if ext not in ('.jpg', '.jpeg'):
raise ValidationError('Nur JPEG-Dateien sind erlaubt.') raise ValidationError('Nur JPEG-Dateien sind erlaubt.')
def _resize_image_if_needed(self): def _resize_image_if_needed(self):
""" # """Verkleinert das hochgeladene Bild auf maximal 640 × 480 px."""
Verkleinert das hochgeladene Bild auf maximal 640 × 480px
(beibehält Seitenverhältnis). Überschreibt die Originaldatei.
"""
# Sicherstellen, dass ein Dateiname vorhanden ist und die Datei lesbar ist
if not self.image or not self.image.name: if not self.image or not self.image.name:
return return
try: try:
# Öffne das Bild mit Pillow .open('rb') liefert eine FileInstanz
with self.image.open('rb') as img_file: with self.image.open('rb') as img_file:
pil_img = PilImage.open(img_file) pil_img = PilImage.open(img_file)
pil_img = pil_img.convert('RGB') pil_img = pil_img.convert('RGB')
# thumbnail passt das Bild in die Grenzen (640×480) und behält das Seitenverhältnis bei.
# thumbnail passt das Bild in die Grenzen (640×480) und behält
# das Seitenverhältnis bei.
pil_img.thumbnail((640, 480), PilImage.LANCZOS) pil_img.thumbnail((640, 480), PilImage.LANCZOS)
# Schreibe das verkleinerte Bild zurück in dieselbe Datei.
# storage = self.image.field.storage # z.B. FileSystemStorage
storage = self.image.field.storage storage = self.image.field.storage
path = self.image.path path = self.image.path
with storage.open(path, 'wb') as out_f: with storage.open(path, 'wb') as out_f:
# Speichern des bearbeiteten Bildes zurück in das Original-File
pil_img.save(out_f, format='JPEG', quality=85) pil_img.save(out_f, format='JPEG', quality=85)
except Exception as exc: except Exception as exc:
# Im ProduktivBetrieb wäre hier ein Logging sinnvoll.
print(f'Fehler beim Verkleinern von {self.image.name}: {exc}') print(f'Fehler beim Verkleinern von {self.image.name}: {exc}')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" # """Überschreibt das Speichern für automatisches Resizing."""
Überschreibe das Speichern, um das Bild automatisch auf 640×480 px
(oder darunter) zu verkleinern. Das Ergebnis überschreibt die OriginalDatei.
"""
# 1️⃣ Führe die normale save() aus nötig, damit wir später den Pfad haben
super().save(*args, **kwargs) super().save(*args, **kwargs)
# 2️⃣ Führe die Verkleinerung nur dann aus, wenn tatsächlich ein Bild da ist.
if self.image and self.image.name: if self.image and self.image.name:
self._resize_image_if_needed() self._resize_image_if_needed()
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" # """Erweitertes Delete-Logik zur Dateilöschung und DB-Bereinigung."""
Erweiterte DeleteLogik: storage = self.image.field.storage
1. Versuche die Datei aus dem Storage zu löschen (falls sie existiert). path = self.image.path
2. Führe eigene Aktionen durch (z.B. Logging).
3. Rufe die StandardDeleteMethode auf, um den DBEintrag zu entfernen.
"""
# --- 1️⃣ Versuche die Datei zu löschen -------------------------------------------------
storage = self.image.field.storage # z.B. FileSystemStorage
path = self.image.path # vollständiger Pfad im Media-Ordner
try: try:
if storage.exists(path): if storage.exists(path):
# Optional: Backup, Logging usw. print(f"[DELETE] Bild entfernt (File System): {path}")
print(f"[DELETE] Bild entfernt: {path}") storage.delete(path)
storage.delete(path) # tatsächliches Löschen der Datei
except Exception as exc: except Exception as exc:
# Im ProduktivBetrieb besser mit logging.exception() umgehen
print(f"Warnung konnte die Bilddatei nicht löschen ({exc})") print(f"Warnung konnte die Bilddatei nicht löschen ({exc})")
# --- 2️⃣ Rufe die normale Django-DeleteLogik auf ------------------------------------ super().delete(*args, **kwargs)
super().delete(*args, **kwargs) # löscht den DBEintrag
# Beendigung des Codes. Der Rest Ihrer Logik folgt im Views/Services Layer.