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: Bild‑Datei (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 × 480 px (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 File‑Instanz 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 Produktiv‑Betrieb 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 Original‑Datei. """ # 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 Delete‑Logik: 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 Standard‑Delete‑Methode auf, um den DB‑Eintrag 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 Produktiv‑Betrieb besser mit logging.exception() umgehen print(f"Warnung – konnte die Bilddatei nicht löschen ({exc})") # --- 2️⃣ Rufe die normale Django-Delete‑Logik auf ------------------------------------ super().delete(*args, **kwargs) # löscht den DB‑Eintrag