diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9031276 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +*.sqlite3 + diff --git a/.kdev4/SGTMensa.kdev4 b/.kdev4/SGTMensa.kdev4 new file mode 100644 index 0000000..7c48fae --- /dev/null +++ b/.kdev4/SGTMensa.kdev4 @@ -0,0 +1,5 @@ +[Buildset] +BuildItems=@Variant(\x00\x00\x00\t\x00\x00\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00S\x00G\x00T\x00M\x00e\x00n\x00s\x00a) + +[Project] +VersionControlSupport=kdevgit diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index ce6b742..0000000 Binary files a/db.sqlite3 and /dev/null 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..7e7f769 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..dc181cb Binary files /dev/null and b/media/gerichte_bilder/pasta_tomato2.jpeg differ diff --git a/mensa_app/__pycache__/__init__.cpython-312.pyc b/mensa_app/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d854d3c..0000000 Binary files a/mensa_app/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/__init__.cpython-314.pyc b/mensa_app/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 46c3669..0000000 Binary files a/mensa_app/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/admin.cpython-312.pyc b/mensa_app/__pycache__/admin.cpython-312.pyc deleted file mode 100644 index d91dd7d..0000000 Binary files a/mensa_app/__pycache__/admin.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/admin.cpython-314.pyc b/mensa_app/__pycache__/admin.cpython-314.pyc deleted file mode 100644 index a5ef4df..0000000 Binary files a/mensa_app/__pycache__/admin.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/apps.cpython-312.pyc b/mensa_app/__pycache__/apps.cpython-312.pyc deleted file mode 100644 index 0c46525..0000000 Binary files a/mensa_app/__pycache__/apps.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/apps.cpython-314.pyc b/mensa_app/__pycache__/apps.cpython-314.pyc deleted file mode 100644 index 13e3337..0000000 Binary files a/mensa_app/__pycache__/apps.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/models.cpython-312.pyc b/mensa_app/__pycache__/models.cpython-312.pyc deleted file mode 100644 index a884738..0000000 Binary files a/mensa_app/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/models.cpython-314.pyc b/mensa_app/__pycache__/models.cpython-314.pyc deleted file mode 100644 index 010479e..0000000 Binary files a/mensa_app/__pycache__/models.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/tests.cpython-312.pyc b/mensa_app/__pycache__/tests.cpython-312.pyc deleted file mode 100644 index 5efc8ba..0000000 Binary files a/mensa_app/__pycache__/tests.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/urls.cpython-314.pyc b/mensa_app/__pycache__/urls.cpython-314.pyc deleted file mode 100644 index b35e61b..0000000 Binary files a/mensa_app/__pycache__/urls.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/__pycache__/views.cpython-314.pyc b/mensa_app/__pycache__/views.cpython-314.pyc deleted file mode 100644 index 22b19f4..0000000 Binary files a/mensa_app/__pycache__/views.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/admin.py b/mensa_app/admin.py index 3bb21ae..5b8db43 100644 --- a/mensa_app/admin.py +++ b/mensa_app/admin.py @@ -3,7 +3,50 @@ from django.contrib import admin # Register your models here. from .models import Person # Ersetze dies durch deine echten Klassennamen -admin.site.register(Person) + +# DELTE: Die Option, Kinder zuzuordnen soll zunächst nicht umgesetzt werden. +# class PersonInlineChildren(admin.TabularInline): +# """ +# Inline für das Feld ``children`` (Schüler eines Elternteils). +# Nur bei Eltern (Rolle Mitarbeit/Chef) sichtbar. +# """ +# model = Person.children.through # Das durchschnittliche Join‑Model +# verbose_name = 'Kind' +# verbose_name_plural = 'Kinder' +# extra = 0 # Keine leeren Zeilen anzeigen +# +# # ---------------------------------------------------------- +# # **WICHTIG:** Hier wird der zu verwendende FK explizit genannt +# # ---------------------------------------------------------- +# fk_name = 'person' # <-- legt fest, welcher FK gemeint ist +# +# def has_add_permission(self, request, obj=None): +# # Erlaube Hinzufügen nur für Eltern (Rolle Eltern) +# return obj and obj.rolle in ('eltern') +# +# def formfield_for_foreignkey(self, db_field, request, **kwargs): +# """ +# Verhindere, dass ein Benutzer sich selbst als Kind hinzufügt. +# """ +# if db_field.name == "person": +# # Hier handelt es sich um den FK auf die Person (das Ziel) +# kwargs["queryset"] = Person.objects.exclude(pk=self.instance.pk) +# return super().formfield_for_foreignkey(db_field, request, **kwargs) + +@admin.register(Person) +class PersonAdmin(admin.ModelAdmin): + list_display = ('user', 'rolle', 'klasse') + search_fields = ['user__username', 'klasse'] + ordering = ('rolle',) + + # # ---- Inline für Eltern (Rolle Mitarbeit/Chef) ---- + # inlines = [PersonInlineChildren] # Zeigt das Kinder‑Inline nur bei passenden Rollen an + + def has_change_permission(self, request, obj=None): + """ + Optional: Verhindere, dass ein Schüler seine eigene Rolle oder die Zuordnung ändert. + """ + return super().has_change_permission(request, obj) from .models import Schulwoche # Ersetze dies durch deine echten Klassennamen @@ -19,7 +62,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 @@ -29,3 +72,21 @@ from .models import Bestellung # Ersetze dies durch deine echten Klassennamen 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_bestellung_id_alter_gericht_id_and_more.py b/mensa_app/migrations/0005_alter_bestellung_id_alter_gericht_id_and_more.py new file mode 100644 index 0000000..6db99ad --- /dev/null +++ b/mensa_app/migrations/0005_alter_bestellung_id_alter_gericht_id_and_more.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.14 on 2026-05-15 09:28 + +import datetime +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='bestellung', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='gericht', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='gericht', + name='time_creation', + field=models.TimeField(default=datetime.datetime(2026, 5, 15, 9, 28, 5, 292454)), + ), + migrations.AlterField( + model_name='gericht', + name='time_last_change', + field=models.TimeField(default=datetime.datetime(2026, 5, 15, 9, 28, 5, 292454)), + ), + migrations.AlterField( + model_name='kategorie', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='menue', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='person', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='speiseplantag', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.CreateModel( + name='Bewertung', + fields=[ + ('id', models.AutoField(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/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/0010_merge_20260612_0913.py b/mensa_app/migrations/0010_merge_20260612_0913.py new file mode 100644 index 0000000..2f16081 --- /dev/null +++ b/mensa_app/migrations/0010_merge_20260612_0913.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.14 on 2026-06-12 09:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mensa_app', '0005_alter_bestellung_id_alter_gericht_id_and_more'), + ('mensa_app', '0009_alter_gericht_time_creation_and_more'), + ] + + operations = [ + ] diff --git a/mensa_app/migrations/__pycache__/0001_initial.cpython-312.pyc b/mensa_app/migrations/__pycache__/0001_initial.cpython-312.pyc deleted file mode 100644 index 2bc9c39..0000000 Binary files a/mensa_app/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc b/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc deleted file mode 100644 index 190f65f..0000000 Binary files a/mensa_app/migrations/__pycache__/0001_initial.cpython-314.pyc and /dev/null 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 deleted file mode 100644 index cae7689..0000000 Binary files a/mensa_app/migrations/__pycache__/0002_alter_bestellung_options_alter_gericht_options_and_more.cpython-314.pyc and /dev/null 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 deleted file mode 100644 index 680e53d..0000000 Binary files a/mensa_app/migrations/__pycache__/0003_schulwoche_alter_gericht_time_creation_and_more.cpython-314.pyc and /dev/null 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 deleted file mode 100644 index c33415e..0000000 Binary files a/mensa_app/migrations/__pycache__/0004_alter_gericht_time_creation_and_more.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/migrations/__pycache__/__init__.cpython-312.pyc b/mensa_app/migrations/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index bdc1992..0000000 Binary files a/mensa_app/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc b/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index cc7d1ea..0000000 Binary files a/mensa_app/migrations/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/mensa_app/models.py b/mensa_app/models.py index 605e782..88062d2 100644 --- a/mensa_app/models.py +++ b/mensa_app/models.py @@ -3,17 +3,74 @@ 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'), ('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 - class Meta: - verbose_name_plural = "Personen" - def __str__(self): - return f"{self.user.username} ({self.rolle})" +# 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): @@ -133,3 +190,167 @@ class Bestellung(models.Model): 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 diff --git a/mensa_core/__pycache__/__init__.cpython-312.pyc b/mensa_core/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 52c5866..0000000 Binary files a/mensa_core/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/__init__.cpython-314.pyc b/mensa_core/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index f6c0a1c..0000000 Binary files a/mensa_core/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/settings.cpython-312.pyc b/mensa_core/__pycache__/settings.cpython-312.pyc deleted file mode 100644 index c09f901..0000000 Binary files a/mensa_core/__pycache__/settings.cpython-312.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/settings.cpython-314.pyc b/mensa_core/__pycache__/settings.cpython-314.pyc deleted file mode 100644 index 4d35f6d..0000000 Binary files a/mensa_core/__pycache__/settings.cpython-314.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/urls.cpython-312.pyc b/mensa_core/__pycache__/urls.cpython-312.pyc deleted file mode 100644 index f3f1d72..0000000 Binary files a/mensa_core/__pycache__/urls.cpython-312.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/urls.cpython-314.pyc b/mensa_core/__pycache__/urls.cpython-314.pyc deleted file mode 100644 index 05686aa..0000000 Binary files a/mensa_core/__pycache__/urls.cpython-314.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/wsgi.cpython-312.pyc b/mensa_core/__pycache__/wsgi.cpython-312.pyc deleted file mode 100644 index 7526c10..0000000 Binary files a/mensa_core/__pycache__/wsgi.cpython-312.pyc and /dev/null differ diff --git a/mensa_core/__pycache__/wsgi.cpython-314.pyc b/mensa_core/__pycache__/wsgi.cpython-314.pyc deleted file mode 100644 index ebd799d..0000000 Binary files a/mensa_core/__pycache__/wsgi.cpython-314.pyc and /dev/null 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 da1dfc9..c7c1527 100644 --- a/mensa_core/urls.py +++ b/mensa_core/urls.py @@ -16,8 +16,11 @@ Including another URLconf """ from django.contrib import admin from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('mensa_app.urls')), # Schaltet die App-URLs frei -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +