Bilder im Datenmodell angelegt, upload, Anzeige und delete

funktionieren.
This commit is contained in:
2026-05-19 23:53:18 +02:00
parent 5e06265a2f
commit 5ab93129ee
32 changed files with 298 additions and 3 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -19,7 +19,7 @@ admin.site.register(Kategorie)
from .models import Gericht # Ersetze dies durch deine echten Klassennamen
admin.site.register(Gericht)
# admin.site.register(Gericht)
from .models import Menue # Ersetze dies durch deine echten Klassennamen
@@ -32,3 +32,18 @@ admin.site.register(Bestellung)
from .models import Bewertung
admin.site.register(Bewertung)
from .models import GerichtBild
admin.site.register(GerichtBild)
class GerichtBildInline(admin.TabularInline):
model = GerichtBild
extra = 0 # Keine leeren Zeilen anzeigen (kann später auf 1 erhöht werden)
readonly_fields = ('image',) # Optional: Nur zur Anzeige, nicht editierbar
@admin.register(Gericht)
class GerichtAdmin(admin.ModelAdmin):
list_display = ('name', 'kategorie')
inlines = [GerichtBildInline] # Das Inline erscheint direkt unter jedem Gericht

View File

@@ -0,0 +1,52 @@
# Generated by Django 6.0.5 on 2026-05-19 20:13
import datetime
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0004_alter_gericht_time_creation_and_more'),
]
operations = [
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 13, 52, 449584)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 13, 52, 449584)),
),
migrations.CreateModel(
name='GerichtBild',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(help_text='Bitte nur JPEG-Dateien hochladen.', upload_to='gerichte_bilder/', validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg'])])),
('sort_order', models.PositiveIntegerField(default=0)),
('gericht', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bilder', to='mensa_app.gericht')),
],
),
migrations.CreateModel(
name='Bewertung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sterne', models.IntegerField(choices=[(1, '★☆☆☆☆'), (2, '★★☆☆☆'), (3, '★★★☆☆'), (4, '★★★★☆'), (5, '★★★★★')], default=3)),
('kommentar', models.TextField(blank=True, null=True)),
('datum', models.DateTimeField(auto_now_add=True)),
('ist_verifiziert', models.BooleanField(default=False, help_text='Wird automatisch auf True gesetzt, wenn eine bezahlte Bestellung vorliegt.')),
('gericht', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bewertungen', to='mensa_app.gericht')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bewertungen', to='mensa_app.person')),
],
options={
'verbose_name': 'Bewertung',
'verbose_name_plural': 'Bewertungen',
'unique_together': {('user', 'gericht')},
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.5 on 2026-05-19 20:24
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0005_alter_gericht_time_creation_and_more'),
]
operations = [
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 24, 22, 989880)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 24, 22, 989880)),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 6.0.5 on 2026-05-19 20:27
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0006_alter_gericht_time_creation_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='gerichtbild',
options={'verbose_name': 'Bild', 'verbose_name_plural': 'Bilder'},
),
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 27, 3, 59858)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 27, 3, 59858)),
),
migrations.AlterUniqueTogether(
name='gerichtbild',
unique_together={('image', 'gericht')},
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 6.0.5 on 2026-05-19 20:43
import datetime
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0007_alter_gerichtbild_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 43, 37, 765352)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 20, 43, 37, 765352)),
),
migrations.AlterField(
model_name='gerichtbild',
name='image',
field=models.ImageField(help_text='Bitte nur JPEG-Dateien (.jpg/.jpeg) hochladen.', upload_to='gerichte_bilder/', validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg'])]),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.5 on 2026-05-19 21:49
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0008_alter_gericht_time_creation_and_more'),
]
operations = [
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 21, 49, 30, 971985)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 19, 21, 49, 30, 971985)),
),
]

View File

@@ -3,6 +3,16 @@ 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)
@@ -193,3 +203,107 @@ class Bewertung(models.Model):
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: BildDatei (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×480px
(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 FileInstanz
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 ProduktivBetrieb 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 OriginalDatei.
"""
# 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 DeleteLogik:
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 StandardDeleteMethode auf, um den DBEintrag 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 ProduktivBetrieb besser mit logging.exception() umgehen
print(f"Warnung konnte die Bilddatei nicht löschen ({exc})")
# --- 2⃣ Rufe die normale Django-DeleteLogik auf ------------------------------------
super().delete(*args, **kwargs) # löscht den DBEintrag

View File

@@ -10,11 +10,13 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Ordner, in dem die OriginalDateien landen
MEDIA_URL = '/media/' # URL-Pfad für den Zugriff (bspw. http://localhost:8000/media/...)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/

View File

@@ -16,7 +16,9 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
]
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)