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'), ('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 # DELETE: Die Eltern-Kinder Beziehung soll zunächst nicht umgesetzt werden. # # ---------------------------------------------------------------------- # # Neue Many‑to‑Many 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." # ) # ) # # # ------------------------------------------------------------------ # # 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 Kinder‑Namen 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'): # # 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.""" 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 # yourappname/admin.py from django.contrib import admin from .models import Kategorie, Gericht, Schulwoche, SpeiseplanTag, Menue, Bestellung, Bewertung, GerichtBild # 1. Kategorien registrieren @admin.register(Kategorie) class KategorieAdmin(admin.ModelAdmin): list_display = ('name',) search_fields = ('name',) # 2. Gerichte (Das wichtigste Modell für den Betreiber!) @admin.register(Gericht) class GerichtAdmin(admin.ModelAdmin): list_display = ('name', 'kategorie', 'ist_vegetarisch', 'ist_dauerangebot') list_filter = ['kategorie', 'is_vegetarisch', 'is_allergene_frei'] search_fields = ['name'] # 3. Bilder (Optional, aber hilfreich) @admin.register(GerichtBild) class GerichtBildAdmin(admin.ModelAdmin): list_display = ('gericht', 'image') list_filter = ['gericht'] # Hier können Sie eventuell das Bild beim Bearbeiten vorab anzeigen lassen # 4. Speiseplan-Verwaltung (Schulwochen und Tage) @admin.register(Schulwoche) class SchulwocheAdmin(admin.ModelAdmin): list_display = ('id', 'datum', 'ist_aktiv') search_fields = ('id',) # Hier könnten Sie weitere Modelle wie Menue, Bestellung usw. registrieren, # je nachdem, welchen Zugriff der Betreiber auf diese Bereiche benötigt. # yourappname/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required # Stellt sicher, dass nur eingeloggte Nutzer bestellen können from .models import SpeiseplanTag, Menue, Bestellung, Person from datetime import date import django.db @login_required def place_order_view(request): # 1. Überprüfen, ob Daten über POST (Formular) gesendet wurden if request.method == 'POST': # Wir nehmen an, dass das Formular Datum und Benutzer-ID sendet. datum_str = request.POST.get('datum') if not datum_str: return render(request, 'order/error.html', {'message': "Bitte wählen Sie ein Datum."}) try: bestellungsdatum = date.fromisoformat(datum_str) # Angenommen, das Format ist YYYY-MM-DD except ValueError: return render(request, 'order/error.html', {'message': "Ungültiges Datumsformat."}) # 2. Speiseplan für dieses Datum finden try: speisetag = SpeiseplanTag.objects.get(datum=bestellungsdatum) except SpeiseplanTag.DoesNotExist: return render(request, 'order/error.html', {'message': f"Für den {bestellungsdatum} ist kein Menü verfügbar."}) # 3. Das verfügbare Menü für diesen Tag ermitteln (Hier vereinfacht!) try: # Beispiel: Wir holen uns nur das erste gefundene Menü des Tages menue_item = Menue.objects.filter(tag=speisetag).select_related('gericht').first() if not menue_item: return render(request, 'order/error.html', {'message': "Für dieses Datum ist kein Haupt-Menü konfiguriert."}) except Exception as e: # Bei einem Fehler bei der Datenbankabfrage return render(request, 'order/error.html', {'message': f"Fehler beim Laden des Menüs: {e}"}) # 4. Bestellung erstellen (Der Kernprozess) user = Person.objects.get(pk=request.user.person_id) # Zugriff auf die Person-Details des eingeloggten Nutzers try: new_bestellung = Bestellung.objects.create( person=user, menue=menue_item, # Das Menü für den Tag datum_bestellung=timezone.now(), # Zeitpunkt der Bestätigung status='offen' # Standardmäßig offen ) return redirect('order:confirmation_success') # Weiterleitung zu einer Erfolgsseite except Exception as e: # Fehlerbehandlung, z.B., wenn das Menü veraltet ist oder die Person fehlt return render(request, 'order/error.html', {'message': f"Ein Bestellfehler ist aufgetreten: {e}"}) # 5. Fallback: Wenn Methode GET war (Anzeige des Formulars) speisetag = SpeiseplanTag.objects.filter(datum__gte=date.today()).order_by('datum').first() if not speisetag: return render(request, 'order/form.html', {'error': "Kein zukünftiger Speiseplan gefunden."}) # Hier würden Sie ein Formular mit dem Datum und weiteren Optionen (z.B. Abholzeit) vorbelegen. context = { 'speisetag': speisetag, 'menue_verfuegbar': True # ... etc. } return render(request, 'order/form.html', context)