18da57b66a
Ich habe einen Code hinzugefügt, der bewirkt, dass die Schüler bestellen können und ein Admin (der Mensabetreiber) bei Anmeldung auf die Bearbeitung der Gerichte zugreifen kann.
458 lines
18 KiB
Python
458 lines
18 KiB
Python
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) |