Bilder im Datenmodell angelegt, upload, Anzeige und delete
funktionieren.
This commit is contained in:
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
BIN
media/gerichte_bilder/Nudeln_Tomatensoße3.jpeg
Normal file
BIN
media/gerichte_bilder/Nudeln_Tomatensoße3.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
BIN
media/gerichte_bilder/Nudeln_Tomatensoße4.jpeg
Normal file
BIN
media/gerichte_bilder/Nudeln_Tomatensoße4.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
BIN
media/gerichte_bilder/pasta_tomato.jpeg
Normal file
BIN
media/gerichte_bilder/pasta_tomato.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
media/gerichte_bilder/pasta_tomato2.jpeg
Normal file
BIN
media/gerichte_bilder/pasta_tomato2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
),
|
||||
]
|
||||
@@ -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')},
|
||||
),
|
||||
]
|
||||
@@ -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'])]),
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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: 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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 Original‑Dateien 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/
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user