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ück‑Beziehung (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 Kinder‑Namen 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'): # Nicht‑Eltern 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.