diff --git a/db.sqlite3 b/db.sqlite3 index f8ad6e9..490c930 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/media/gerichte_bilder/Nudeln_Tomatensoße3.jpeg b/media/gerichte_bilder/Nudeln_Tomatensoße3.jpeg new file mode 100644 index 0000000..03285e4 Binary files /dev/null and b/media/gerichte_bilder/Nudeln_Tomatensoße3.jpeg differ diff --git a/media/gerichte_bilder/Nudeln_Tomatensoße4.jpeg b/media/gerichte_bilder/Nudeln_Tomatensoße4.jpeg new file mode 100644 index 0000000..b7b04d4 Binary files /dev/null and b/media/gerichte_bilder/Nudeln_Tomatensoße4.jpeg differ diff --git a/media/gerichte_bilder/pasta_tomato.jpeg b/media/gerichte_bilder/pasta_tomato.jpeg new file mode 100644 index 0000000..4c36e4f Binary files /dev/null and b/media/gerichte_bilder/pasta_tomato.jpeg differ diff --git a/media/gerichte_bilder/pasta_tomato2.jpeg b/media/gerichte_bilder/pasta_tomato2.jpeg new file mode 100644 index 0000000..92ac142 Binary files /dev/null and b/media/gerichte_bilder/pasta_tomato2.jpeg differ diff --git a/mensa_app/__pycache__/__init__.cpython-314.pyc b/mensa_app/__pycache__/__init__.cpython-314.pyc index ac18d10..46c3669 100644 Binary files a/mensa_app/__pycache__/__init__.cpython-314.pyc and b/mensa_app/__pycache__/__init__.cpython-314.pyc differ diff --git a/mensa_app/__pycache__/admin.cpython-314.pyc b/mensa_app/__pycache__/admin.cpython-314.pyc index de80568..8bd90d7 100644 Binary files a/mensa_app/__pycache__/admin.cpython-314.pyc and b/mensa_app/__pycache__/admin.cpython-314.pyc differ diff --git a/mensa_app/__pycache__/apps.cpython-314.pyc b/mensa_app/__pycache__/apps.cpython-314.pyc index 7d5b56e..13e3337 100644 Binary files a/mensa_app/__pycache__/apps.cpython-314.pyc and b/mensa_app/__pycache__/apps.cpython-314.pyc differ diff --git a/mensa_app/__pycache__/models.cpython-314.pyc b/mensa_app/__pycache__/models.cpython-314.pyc index e0fd0fa..7bf82a8 100644 Binary files a/mensa_app/__pycache__/models.cpython-314.pyc and b/mensa_app/__pycache__/models.cpython-314.pyc differ diff --git a/mensa_app/admin.py b/mensa_app/admin.py index 223d240..4a2b7fa 100644 --- a/mensa_app/admin.py +++ b/mensa_app/admin.py @@ -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 + diff --git a/mensa_app/migrations/0005_alter_gericht_time_creation_and_more.py b/mensa_app/migrations/0005_alter_gericht_time_creation_and_more.py new file mode 100644 index 0000000..24c0940 --- /dev/null +++ b/mensa_app/migrations/0005_alter_gericht_time_creation_and_more.py @@ -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')}, + }, + ), + ] diff --git a/mensa_app/migrations/0006_alter_gericht_time_creation_and_more.py b/mensa_app/migrations/0006_alter_gericht_time_creation_and_more.py new file mode 100644 index 0000000..d6aa715 --- /dev/null +++ b/mensa_app/migrations/0006_alter_gericht_time_creation_and_more.py @@ -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)), + ), + ] diff --git a/mensa_app/migrations/0007_alter_gerichtbild_options_and_more.py b/mensa_app/migrations/0007_alter_gerichtbild_options_and_more.py new file mode 100644 index 0000000..eadf7c4 --- /dev/null +++ b/mensa_app/migrations/0007_alter_gerichtbild_options_and_more.py @@ -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')}, + ), + ] diff --git a/mensa_app/migrations/0008_alter_gericht_time_creation_and_more.py b/mensa_app/migrations/0008_alter_gericht_time_creation_and_more.py new file mode 100644 index 0000000..eb3c974 --- /dev/null +++ b/mensa_app/migrations/0008_alter_gericht_time_creation_and_more.py @@ -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'])]), + ), + ] diff --git a/mensa_app/migrations/0009_alter_gericht_time_creation_and_more.py b/mensa_app/migrations/0009_alter_gericht_time_creation_and_more.py new file mode 100644 index 0000000..c51dd5b --- /dev/null +++ b/mensa_app/migrations/0009_alter_gericht_time_creation_and_more.py @@ -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)), + ), + ] diff --git a/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc b/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc index 42952ee..190f65f 100644 Binary files a/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc and b/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0002_alter_bestellung_options_alter_gericht_options_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0002_alter_bestellung_options_alter_gericht_options_and_more.cpython-314.pyc index 973cd09..cae7689 100644 Binary files a/mensa_app/migrations/__pycache__/0002_alter_bestellung_options_alter_gericht_options_and_more.cpython-314.pyc and b/mensa_app/migrations/__pycache__/0002_alter_bestellung_options_alter_gericht_options_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0003_schulwoche_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0003_schulwoche_alter_gericht_time_creation_and_more.cpython-314.pyc index a82d32d..680e53d 100644 Binary files a/mensa_app/migrations/__pycache__/0003_schulwoche_alter_gericht_time_creation_and_more.cpython-314.pyc and b/mensa_app/migrations/__pycache__/0003_schulwoche_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0004_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0004_alter_gericht_time_creation_and_more.cpython-314.pyc index f97feed..c33415e 100644 Binary files a/mensa_app/migrations/__pycache__/0004_alter_gericht_time_creation_and_more.cpython-314.pyc and b/mensa_app/migrations/__pycache__/0004_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0005_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0005_alter_gericht_time_creation_and_more.cpython-314.pyc new file mode 100644 index 0000000..fd40f79 Binary files /dev/null and b/mensa_app/migrations/__pycache__/0005_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0006_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0006_alter_gericht_time_creation_and_more.cpython-314.pyc new file mode 100644 index 0000000..97ffdcf Binary files /dev/null and b/mensa_app/migrations/__pycache__/0006_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0007_alter_gerichtbild_options_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0007_alter_gerichtbild_options_and_more.cpython-314.pyc new file mode 100644 index 0000000..564b9c6 Binary files /dev/null and b/mensa_app/migrations/__pycache__/0007_alter_gerichtbild_options_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0008_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0008_alter_gericht_time_creation_and_more.cpython-314.pyc new file mode 100644 index 0000000..51ee7bd Binary files /dev/null and b/mensa_app/migrations/__pycache__/0008_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/0009_alter_gericht_time_creation_and_more.cpython-314.pyc b/mensa_app/migrations/__pycache__/0009_alter_gericht_time_creation_and_more.cpython-314.pyc new file mode 100644 index 0000000..9d6fef3 Binary files /dev/null and b/mensa_app/migrations/__pycache__/0009_alter_gericht_time_creation_and_more.cpython-314.pyc differ diff --git a/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc b/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc index 2acbc8d..cc7d1ea 100644 Binary files a/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc and b/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/mensa_app/models.py b/mensa_app/models.py index c24c43e..99d39ad 100644 --- a/mensa_app/models.py +++ b/mensa_app/models.py @@ -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 diff --git a/mensa_core/__pycache__/__init__.cpython-314.pyc b/mensa_core/__pycache__/__init__.cpython-314.pyc index c6998ec..f6c0a1c 100644 Binary files a/mensa_core/__pycache__/__init__.cpython-314.pyc and b/mensa_core/__pycache__/__init__.cpython-314.pyc differ diff --git a/mensa_core/__pycache__/settings.cpython-314.pyc b/mensa_core/__pycache__/settings.cpython-314.pyc index ddbc640..ec7042a 100644 Binary files a/mensa_core/__pycache__/settings.cpython-314.pyc and b/mensa_core/__pycache__/settings.cpython-314.pyc differ diff --git a/mensa_core/__pycache__/urls.cpython-314.pyc b/mensa_core/__pycache__/urls.cpython-314.pyc index 262e0fa..980ac10 100644 Binary files a/mensa_core/__pycache__/urls.cpython-314.pyc and b/mensa_core/__pycache__/urls.cpython-314.pyc differ diff --git a/mensa_core/__pycache__/wsgi.cpython-314.pyc b/mensa_core/__pycache__/wsgi.cpython-314.pyc index b95635b..ebd799d 100644 Binary files a/mensa_core/__pycache__/wsgi.cpython-314.pyc and b/mensa_core/__pycache__/wsgi.cpython-314.pyc differ diff --git a/mensa_core/settings.py b/mensa_core/settings.py index b562651..f4fbc3b 100644 --- a/mensa_core/settings.py +++ b/mensa_core/settings.py @@ -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/ diff --git a/mensa_core/urls.py b/mensa_core/urls.py index 29c5021..9bf908e 100644 --- a/mensa_core/urls.py +++ b/mensa_core/urls.py @@ -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)