Files
SGTMensa/mensa_app/models.py
T
k.hederer b7df490ea8 mensa_app/models.py aktualisiert
neuer branch für überarbeiten code
2026-06-25 07:12:20 +00:00

297 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
import datetime
import os
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
# Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow)
from PIL import Image as PilImage
class Person(models.Model):
# """Repräsentiert Schüler oder Lehrer."""
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')])
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'):
# 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):
# """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"
datum = models.DateField(unique=True) # Datum der Schulwoche (z. B. Montag der ersten Woche)
ist_aktiv = models.BooleanField(default=False)
ist_ferienwoche = models.BooleanField(default=False)
class Meta:
verbose_name = "Schulwoche"
verbose_name_plural = "Schulwochen"
class SpeiseplanTag(models.Model):
# """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)
schulwoche = models.ForeignKey(
Schulwoche,
on_delete=models.CASCADE,
)
class Meta:
verbose_name = "Tag mit Speiseplan"
verbose_name_plural = "Tage mit Speiseplan"
def __str__(self):
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 Meta:
verbose_name = "Kategorie"
verbose_name_plural = "Kategorien"
def __str__(self):
return self.name
class Gericht(models.Model):
#"""Ein einzelnes Gericht (z.B. 'Nudeln mit Tomatensauce')."""
name = models.CharField(max_length=100)
kategorie = models.ForeignKey(
Kategorie,
on_delete=models.PROTECT,
related_name='gerichte'
)
ist_vegetarisch = models.BooleanField(default=False)
ist_allergene_frei = models.BooleanField(default=False)
allergene = models.TextField(blank=True, default="")
preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
now = datetime.datetime.now()
time_last_change = models.TimeField(default=now)
time_creation = models.TimeField(default=now)
# Das Attribut für das "Dauerangebot"
ist_dauerangebot = models.BooleanField(
default=False,
help_text="Wenn aktiviert, ist dieses Gericht immer verfügbar und nicht an einen spezifischen Tag gebunden."
)
def __str__(self):
return f"{self.name} ({self.kategorie.name})"
class Meta:
verbose_name = "Gericht"
verbose_name_plural = "Gerichte"
permissions = [("can_create_Gericht", "Kann neues Gericht anlegen")]
ordering = ["kategorie", "name", "preis"]
get_latest_by = "time_last_change"
class Menue(models.Model):
# """Eine Kombination von Speisen für einen Tag (z.B. Hauptgang + Dessert)."""
tag = models.ForeignKey(SpeiseplanTag, on_delete=models.CASCADE, related_name='menues')
gericht = models.ForeignKey(Gericht, on_delete=models.CASCADE)
preis = models.DecimalField(max_digits=5, decimal_places=2)
def __str__(self):
return f"{self.tag.datum}: {self.gericht.name} ({self.preis}€)"
class Meta:
verbose_name = "Menü"
verbose_name_plural = "Menüs"
class Bestellung(models.Model):
# """Eine abgeschlossene Bestellung eines Nutzers."""
STATUS_CHOICES = [
('offen', 'Offen'),
('abgeholt', 'Abgeholt'),
('verwaist', 'Nicht abgeholt'),
('bezahlt', 'Bezahlt'),
('storniert', 'Storniert'),
]
person = models.ForeignKey(Person, on_delete=models.CASCADE)
menue = models.ForeignKey(Menue, on_delete=models.CASCADE)
datum_bestellung = models.DateTimeField(default=timezone.now)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='offen')
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):
return f"Bestellung {self.id} von {self.person.user.username}"
class Meta:
verbose_name = "Bestellung"
verbose_name_plural = "Bestellungen"
from django.core.exceptions import ValidationError
class Bewertung(models.Model):
# """
# Repräsentiert die Bewertung eines Gerichts durch einen Nutzer.
# """
class Sterne(models.IntegerChoices):
EINER = 1, '★☆☆☆☆'
ZWEI = 2, '★★☆☆☆'
DREI = 3, '★★★☆☆'
VIER = 4, '★★★★☆'
FUENF = 5, '★★★★★'
user = models.ForeignKey('Person', on_delete=models.CASCADE, related_name='bewertungen')
gericht = models.ForeignKey('Gericht', on_delete=models.CASCADE, related_name='bewertungen')
sterne = models.IntegerField(choices=Sterne.choices, default=3)
kommentar = models.TextField(blank=True, null=True)
datum = models.DateTimeField(auto_now_add=True)
# Das Feld für die Verifizierung
ist_verifiziert = models.BooleanField(
default=False,
help_text="Wird automatisch auf True gesetzt, wenn eine bezahlte Bestellung vorliegt."
)
class Meta:
# Verhindert, dass ein Nutzer dasselbe Gericht mehrfach bewertet
unique_together = ('user', 'gericht')
verbose_name = "Bewertung"
verbose_name_plural = "Bewertungen"
def __str__(self):
return f"{self.user.user.username} bewertet {self.gericht.name} mit {self.sterne} Sternen"
def clean(self):
# """
# Validierung (Bleibt unverändert).
# """
if self.sterne < 1 or self.sterne > 5:
raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.")
def check_verifizierung(self):
# """
# Service-Method zur Prüfung des Verifizierungsstatus (Bleibt unverändert).
# """
# Wir prüfen, ob es eine Bestellung für diesen User und dieses Gericht gibt,
# die bereits als 'bezahlt' markiert ist.
exists = Bestellung.objects.filter(
person=self.user,
menue__gericht=self.gericht,
bezahlt=True
).exists()
if exists:
self.ist_verifiziert = True
return self.ist_verifiziert
class GerichtBild(models.Model):
# """
# 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."
)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
unique_together = ('image', 'gericht')
verbose_name = "Bild"
verbose_name_plural = "Bilder"
def clean(self):
# """Zusätzliche Validierung (Bleibt unverändert)."""
_, ext = os.path.splitext(self.image.name.lower())
if ext not in ('.jpg', '.jpeg'):
raise ValidationError('Nur JPEG-Dateien sind erlaubt.')
def _resize_image_if_needed(self):
# """Verkleinert das hochgeladene Bild auf maximal 640 × 480 px."""
if not self.image or not self.image.name:
return
try:
with self.image.open('rb') as img_file:
pil_img = PilImage.open(img_file)
pil_img = pil_img.convert('RGB')
# thumbnail passt das Bild in die Grenzen (640×480) und behält das Seitenverhältnis bei.
pil_img.thumbnail((640, 480), PilImage.LANCZOS)
storage = self.image.field.storage
path = self.image.path
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)
except Exception as exc:
print(f'Fehler beim Verkleinern von {self.image.name}: {exc}')
def save(self, *args, **kwargs):
# """Überschreibt das Speichern für automatisches Resizing."""
super().save(*args, **kwargs)
if self.image and self.image.name:
self._resize_image_if_needed()
def delete(self, *args, **kwargs):
# """Erweitertes Delete-Logik zur Dateilöschung und DB-Bereinigung."""
storage = self.image.field.storage
path = self.image.path
try:
if storage.exists(path):
print(f"[DELETE] Bild entfernt (File System): {path}")
storage.delete(path)
except Exception as exc:
print(f"Warnung konnte die Bilddatei nicht löschen ({exc})")
super().delete(*args, **kwargs)
# Beendigung des Codes. Der Rest Ihrer Logik folgt im Views/Services Layer.