Sternchen überarbeitet, mal drüberschauen(die profis) #9
+98
-157
@@ -2,115 +2,94 @@ from django.db import models
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from django.db import models
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.core.validators import FileExtensionValidator
|
||||||
# Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow)
|
# Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow)
|
||||||
from PIL import Image as PilImage
|
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):
|
class Person(models.Model):
|
||||||
"""Repräsentiert Schüler oder Lehrer."""
|
# """Repräsentiert Schüler oder Lehrer."""
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
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')])
|
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
|
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'):
|
||||||
# DELETE: Die Eltern-Kinder Beziehung soll zunächst nicht umgesetzt werden.
|
# Nicht‑Eltern dürfen keine Kinder besitzen
|
||||||
# # ----------------------------------------------------------------------
|
if self.children.exists():
|
||||||
# # Neue Many‑to‑Many Beziehung: „Elternteil ↔ Schüler“
|
raise ValidationError(
|
||||||
# # ----------------------------------------------------------------------
|
"Nur Benutzer mit der Rolle 'Eltern' "
|
||||||
# children = models.ManyToManyField(
|
"dürfen Schüler zuordnen (children)."
|
||||||
# 'self',
|
)
|
||||||
# blank=True,
|
else:
|
||||||
# related_name='parents', # → für einen Schüler: person.parents.all()
|
# Eltern: Verhindere, dass sie sich selbst als Kind eintragen
|
||||||
# symmetrical=False, # Verhindert eine zirkuläre Rück‑Beziehung (nicht benötigt)
|
if self in self.children.all():
|
||||||
# help_text=(
|
raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
|
||||||
# "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):
|
class Schulwoche(models.Model):
|
||||||
"""Repräsentiert eine Schulwoche mit einem eindeutigen Datum."""
|
# """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"
|
||||||
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)
|
datum = models.DateField(unique=True) # Datum der Schulwoche (z. B. Montag der ersten Woche)
|
||||||
|
|
||||||
ist_aktiv = models.BooleanField(default=False)
|
ist_aktiv = models.BooleanField(default=False)
|
||||||
ist_ferienwoche = models.BooleanField(default=False)
|
ist_ferienwoche = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Schulwoche"
|
verbose_name = "Schulwoche"
|
||||||
verbose_name_plural = "Schulwochen"
|
verbose_name_plural = "Schulwochen"
|
||||||
|
|
||||||
|
|
||||||
class SpeiseplanTag(models.Model):
|
class SpeiseplanTag(models.Model):
|
||||||
"""Ein bestimmter Tag im Speiseplan."""
|
# """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)
|
datum = models.DateField(unique=True)
|
||||||
|
|
||||||
schulwoche = models.ForeignKey(
|
schulwoche = models.ForeignKey(
|
||||||
Schulwoche,
|
Schulwoche,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
#related_name='tag_get_schulwoche' # Für Abfragen wie: Schulwoche.tag_get_schulwoche.all()
|
|
||||||
default = "test_1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Tag mit Speiseplan"
|
verbose_name = "Tag mit Speiseplan"
|
||||||
verbose_name_plural = "Tage mit Speiseplan"
|
verbose_name_plural = "Tage mit Speiseplan"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.datum.strftime('%d.%m.%Y')
|
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 Kategorie(models.Model):
|
||||||
|
# """
|
||||||
|
# Definiert die Art der Speise (Süßspeise, Hauptgericht, etc.).
|
||||||
|
# """
|
||||||
|
name = models.CharField(max_length=50, unique=True, default="")
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Kategorie"
|
verbose_name = "Kategorie"
|
||||||
verbose_name_plural = "Kategorien"
|
verbose_name_plural = "Kategorien"
|
||||||
@@ -118,11 +97,10 @@ class Kategorie(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Gericht(models.Model):
|
class Gericht(models.Model):
|
||||||
"""Ein einzelnes Gericht (z.B. 'Nudeln mit Tomatensauce')."""
|
#"""Ein einzelnes Gericht (z.B. 'Nudeln mit Tomatensauce')."""
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
# PROTECT verhindert, dass eine Kategorie gelöscht wird,
|
|
||||||
# solange noch Gerichte ihr zugeordnet sind.
|
|
||||||
kategorie = models.ForeignKey(
|
kategorie = models.ForeignKey(
|
||||||
Kategorie,
|
Kategorie,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@@ -132,12 +110,9 @@ class Gericht(models.Model):
|
|||||||
ist_allergene_frei = models.BooleanField(default=False)
|
ist_allergene_frei = models.BooleanField(default=False)
|
||||||
allergene = models.TextField(blank=True, default="")
|
allergene = models.TextField(blank=True, default="")
|
||||||
preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
|
preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
time_last_change = models.TimeField(default=now)
|
time_last_change = models.TimeField(default=now)
|
||||||
time_creation = models.TimeField(default=now)
|
time_creation = models.TimeField(default=now)
|
||||||
|
|
||||||
# Das Attribut für das "Dauerangebot"
|
# Das Attribut für das "Dauerangebot"
|
||||||
ist_dauerangebot = models.BooleanField(
|
ist_dauerangebot = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@@ -154,8 +129,9 @@ class Gericht(models.Model):
|
|||||||
ordering = ["kategorie", "name", "preis"]
|
ordering = ["kategorie", "name", "preis"]
|
||||||
get_latest_by = "time_last_change"
|
get_latest_by = "time_last_change"
|
||||||
|
|
||||||
|
|
||||||
class Menue(models.Model):
|
class Menue(models.Model):
|
||||||
"""Eine Kombination von Speisen für einen Tag (z.B. Hauptgang + Dessert)."""
|
# """Eine Kombination von Speisen für einen Tag (z.B. Hauptgang + Dessert)."""
|
||||||
tag = models.ForeignKey(SpeiseplanTag, on_delete=models.CASCADE, related_name='menues')
|
tag = models.ForeignKey(SpeiseplanTag, on_delete=models.CASCADE, related_name='menues')
|
||||||
gericht = models.ForeignKey(Gericht, on_delete=models.CASCADE)
|
gericht = models.ForeignKey(Gericht, on_delete=models.CASCADE)
|
||||||
preis = models.DecimalField(max_digits=5, decimal_places=2)
|
preis = models.DecimalField(max_digits=5, decimal_places=2)
|
||||||
@@ -167,8 +143,9 @@ class Menue(models.Model):
|
|||||||
verbose_name = "Menü"
|
verbose_name = "Menü"
|
||||||
verbose_name_plural = "Menüs"
|
verbose_name_plural = "Menüs"
|
||||||
|
|
||||||
|
|
||||||
class Bestellung(models.Model):
|
class Bestellung(models.Model):
|
||||||
"""Eine abgeschlossene Bestellung eines Nutzers."""
|
# """Eine abgeschlossene Bestellung eines Nutzers."""
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('offen', 'Offen'),
|
('offen', 'Offen'),
|
||||||
('abgeholt', 'Abgeholt'),
|
('abgeholt', 'Abgeholt'),
|
||||||
@@ -176,13 +153,16 @@ class Bestellung(models.Model):
|
|||||||
('bezahlt', 'Bezahlt'),
|
('bezahlt', 'Bezahlt'),
|
||||||
('storniert', 'Storniert'),
|
('storniert', 'Storniert'),
|
||||||
]
|
]
|
||||||
|
|
||||||
person = models.ForeignKey(Person, on_delete=models.CASCADE)
|
person = models.ForeignKey(Person, on_delete=models.CASCADE)
|
||||||
menue = models.ForeignKey(Menue, on_delete=models.CASCADE)
|
menue = models.ForeignKey(Menue, on_delete=models.CASCADE)
|
||||||
datum_bestellung = models.DateTimeField(default=timezone.now)
|
datum_bestellung = models.DateTimeField(default=timezone.now)
|
||||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='offen')
|
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='offen')
|
||||||
bezahlt = models.BooleanField(default=False)
|
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):
|
def __str__(self):
|
||||||
return f"Bestellung {self.id} von {self.person.user.username}"
|
return f"Bestellung {self.id} von {self.person.user.username}"
|
||||||
|
|
||||||
@@ -193,12 +173,10 @@ class Bestellung(models.Model):
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class Bewertung(models.Model):
|
class Bewertung(models.Model):
|
||||||
"""
|
# """
|
||||||
Repräsentiert die Bewertung eines Gerichts durch einen Nutzer.
|
# Repräsentiert die Bewertung eines Gerichts durch einen Nutzer.
|
||||||
"""
|
# """
|
||||||
|
|
||||||
class Sterne(models.IntegerChoices):
|
class Sterne(models.IntegerChoices):
|
||||||
EINER = 1, '★☆☆☆☆'
|
EINER = 1, '★☆☆☆☆'
|
||||||
ZWEI = 2, '★★☆☆☆'
|
ZWEI = 2, '★★☆☆☆'
|
||||||
@@ -228,17 +206,16 @@ class Bewertung(models.Model):
|
|||||||
return f"{self.user.user.username} bewertet {self.gericht.name} mit {self.sterne} Sternen"
|
return f"{self.user.user.username} bewertet {self.gericht.name} mit {self.sterne} Sternen"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
# """
|
||||||
Hier können wir zusätzliche Validierungen einbauen.
|
# Validierung (Bleibt unverändert).
|
||||||
"""
|
# """
|
||||||
if self.sterne < 1 or self.sterne > 5:
|
if self.sterne < 1 or self.sterne > 5:
|
||||||
raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.")
|
raise ValidationError("Die Bewertung muss zwischen 1 und 5 Sternen liegen.")
|
||||||
|
|
||||||
def check_verifizierung(self):
|
def check_verifizierung(self):
|
||||||
"""
|
# """
|
||||||
Ein hilfreicher Service-Method, um den Status der Verifizierung
|
# Service-Method zur Prüfung des Verifizierungsstatus (Bleibt unverändert).
|
||||||
automatisch zu prüfen.
|
# """
|
||||||
"""
|
|
||||||
# Wir prüfen, ob es eine Bestellung für diesen User und dieses Gericht gibt,
|
# Wir prüfen, ob es eine Bestellung für diesen User und dieses Gericht gibt,
|
||||||
# die bereits als 'bezahlt' markiert ist.
|
# die bereits als 'bezahlt' markiert ist.
|
||||||
exists = Bestellung.objects.filter(
|
exists = Bestellung.objects.filter(
|
||||||
@@ -246,111 +223,75 @@ class Bewertung(models.Model):
|
|||||||
menue__gericht=self.gericht,
|
menue__gericht=self.gericht,
|
||||||
bezahlt=True
|
bezahlt=True
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
self.ist_verifiziert = True
|
self.ist_verifiziert = True
|
||||||
return self.ist_verifiziert
|
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)
|
class GerichtBild(models.Model):
|
||||||
upload_to='gerichte_bilder/', # Unterordner im MEDIA_ROOT
|
# """
|
||||||
validators=[FileExtensionValidator(['jpg', 'jpeg'])], # Nur JPEG zulassen
|
# 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."
|
help_text="Bitte nur JPEG-Dateien (.jpg/.jpeg) hochladen."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional: Sortierreihenfolge, wenn mehrere Bilder angezeigt werden sollen
|
|
||||||
sort_order = models.PositiveIntegerField(default=0)
|
sort_order = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
# Verhindert, dass ein Bild mehrfach zu einem Gericht angelegt wird
|
|
||||||
unique_together = ('image', 'gericht')
|
unique_together = ('image', 'gericht')
|
||||||
verbose_name = "Bild"
|
verbose_name = "Bild"
|
||||||
verbose_name_plural = "Bilder"
|
verbose_name_plural = "Bilder"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
# """Zusätzliche Validierung (Bleibt unverändert)."""
|
||||||
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())
|
_, ext = os.path.splitext(self.image.name.lower())
|
||||||
if ext not in ('.jpg', '.jpeg'):
|
if ext not in ('.jpg', '.jpeg'):
|
||||||
raise ValidationError('Nur JPEG-Dateien sind erlaubt.')
|
raise ValidationError('Nur JPEG-Dateien sind erlaubt.')
|
||||||
|
|
||||||
def _resize_image_if_needed(self):
|
def _resize_image_if_needed(self):
|
||||||
"""
|
# """Verkleinert das hochgeladene Bild auf maximal 640 × 480 px."""
|
||||||
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:
|
if not self.image or not self.image.name:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Öffne das Bild mit Pillow – .open('rb') liefert eine File‑Instanz
|
|
||||||
with self.image.open('rb') as img_file:
|
with self.image.open('rb') as img_file:
|
||||||
pil_img = PilImage.open(img_file)
|
pil_img = PilImage.open(img_file)
|
||||||
pil_img = pil_img.convert('RGB')
|
pil_img = pil_img.convert('RGB')
|
||||||
|
# thumbnail passt das Bild in die Grenzen (640×480) und behält das Seitenverhältnis bei.
|
||||||
# thumbnail passt das Bild in die Grenzen (640×480) und behält
|
|
||||||
# das Seitenverhältnis bei.
|
|
||||||
pil_img.thumbnail((640, 480), PilImage.LANCZOS)
|
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
|
storage = self.image.field.storage
|
||||||
path = self.image.path
|
path = self.image.path
|
||||||
|
|
||||||
with storage.open(path, 'wb') as out_f:
|
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)
|
pil_img.save(out_f, format='JPEG', quality=85)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Im Produktiv‑Betrieb wäre hier ein Logging sinnvoll.
|
|
||||||
print(f'Fehler beim Verkleinern von {self.image.name}: {exc}')
|
print(f'Fehler beim Verkleinern von {self.image.name}: {exc}')
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
# """Überschreibt das Speichern für automatisches Resizing."""
|
||||||
Ü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)
|
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:
|
if self.image and self.image.name:
|
||||||
self._resize_image_if_needed()
|
self._resize_image_if_needed()
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
# """Erweitertes Delete-Logik zur Dateilöschung und DB-Bereinigung."""
|
||||||
Erweiterte Delete‑Logik:
|
storage = self.image.field.storage
|
||||||
1. Versuche die Datei aus dem Storage zu löschen (falls sie existiert).
|
path = self.image.path
|
||||||
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:
|
try:
|
||||||
if storage.exists(path):
|
if storage.exists(path):
|
||||||
# Optional: Backup, Logging usw.
|
print(f"[DELETE] Bild entfernt (File System): {path}")
|
||||||
print(f"[DELETE] Bild entfernt: {path}")
|
storage.delete(path)
|
||||||
|
|
||||||
storage.delete(path) # tatsächliches Löschen der Datei
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Im Produktiv‑Betrieb besser mit logging.exception() umgehen
|
|
||||||
print(f"Warnung – konnte die Bilddatei nicht löschen ({exc})")
|
print(f"Warnung – konnte die Bilddatei nicht löschen ({exc})")
|
||||||
|
|
||||||
# --- 2️⃣ Rufe die normale Django-Delete‑Logik auf ------------------------------------
|
super().delete(*args, **kwargs)
|
||||||
super().delete(*args, **kwargs) # löscht den DB‑Eintrag
|
|
||||||
|
|
||||||
|
# Beendigung des Codes. Der Rest Ihrer Logik folgt im Views/Services Layer.
|
||||||
Reference in New Issue
Block a user