Files
SGTMensa/mensa_app/models.py

310 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.db import models
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
# Optional: Wenn du später Thumbnails mit sorl-thumbnail nutzen möchtest:
# from sorl.thumbnail import get_thumbnail
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'), ('lehrer', 'Lehrer'), ('mitarbeiter','Mensa-Mitarbeiter'), ('chef','Mensa-Leitung')])
klasse = models.CharField(max_length=4, blank=True, null=True) # Nur für Schüler relevant
class Meta:
verbose_name_plural = "Personen"
def __str__(self):
return f"{self.user.username} ({self.rolle})"
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."""
datum = models.DateField(unique=True)
schulwoche = models.ForeignKey(
Schulwoche,
on_delete=models.CASCADE,
#related_name='tag_get_schulwoche' # Für Abfragen wie: Schulwoche.tag_get_schulwoche.all()
default = "test_1"
)
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)
# PROTECT verhindert, dass eine Kategorie gelöscht wird,
# solange noch Gerichte ihr zugeordnet sind.
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)
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):
"""
Hier können wir zusätzliche Validierungen einbauen.
"""
if self.sterne < 1 or self.sterne > 5:
raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.")
def check_verifizierung(self):
"""
Ein hilfreicher Service-Method, um den Status der Verifizierung
automatisch zu prüfen.
"""
# 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.
- 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)
upload_to='gerichte_bilder/', # Unterordner im MEDIA_ROOT
validators=[FileExtensionValidator(['jpg', 'jpeg'])], # Nur JPEG zulassen
help_text="Bitte nur JPEG-Dateien (.jpg/.jpeg) hochladen."
)
# Optional: Sortierreihenfolge, wenn mehrere Bilder angezeigt werden sollen
sort_order = models.PositiveIntegerField(default=0)
class Meta:
# Verhindert, dass ein Bild mehrfach zu einem Gericht angelegt wird
unique_together = ('image', 'gericht')
verbose_name = "Bild"
verbose_name_plural = "Bilder"
def clean(self):
"""
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())
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×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:
return
try:
# Öffne das Bild mit Pillow .open('rb') liefert eine FileInstanz
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)
# Schreibe das verkleinerte Bild zurück in dieselbe Datei.
# storage = self.image.field.storage # z.B. FileSystemStorage
storage = self.image.field.storage
path = self.image.path
with storage.open(path, 'wb') as out_f:
pil_img.save(out_f, format='JPEG', quality=85)
except Exception as exc:
# Im ProduktivBetrieb wäre hier ein Logging sinnvoll.
print(f'Fehler beim Verkleinern von {self.image.name}: {exc}')
def save(self, *args, **kwargs):
"""
Ü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)
# 2⃣ Führe die Verkleinerung nur dann aus, wenn tatsächlich ein Bild da ist.
if self.image and self.image.name:
self._resize_image_if_needed()
def delete(self, *args, **kwargs):
"""
Erweiterte DeleteLogik:
1. Versuche die Datei aus dem Storage zu löschen (falls sie existiert).
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:
if storage.exists(path):
# Optional: Backup, Logging usw.
print(f"[DELETE] Bild entfernt: {path}")
storage.delete(path) # tatsächliches Löschen der Datei
except Exception as exc:
# Im ProduktivBetrieb besser mit logging.exception() umgehen
print(f"Warnung konnte die Bilddatei nicht löschen ({exc})")
# --- 2⃣ Rufe die normale Django-DeleteLogik auf ------------------------------------
super().delete(*args, **kwargs) # löscht den DBEintrag