Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d6235679a | |||
| 4533fd6402 |
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
Binary file not shown.
BIN
mensa_app/__pycache__/urls.cpython-314.pyc
Normal file
BIN
mensa_app/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
mensa_app/__pycache__/views.cpython-314.pyc
Normal file
BIN
mensa_app/__pycache__/views.cpython-314.pyc
Normal file
Binary file not shown.
@@ -3,48 +3,7 @@ from django.contrib import admin
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
from .models import Person # Ersetze dies durch deine echten Klassennamen
|
from .models import Person # Ersetze dies durch deine echten Klassennamen
|
||||||
|
|
||||||
class PersonInlineChildren(admin.TabularInline):
|
admin.site.register(Person)
|
||||||
"""
|
|
||||||
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
|
from .models import Schulwoche # Ersetze dies durch deine echten Klassennamen
|
||||||
|
|
||||||
@@ -60,7 +19,7 @@ admin.site.register(Kategorie)
|
|||||||
|
|
||||||
from .models import Gericht # Ersetze dies durch deine echten Klassennamen
|
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
|
from .models import Menue # Ersetze dies durch deine echten Klassennamen
|
||||||
|
|
||||||
@@ -70,21 +29,3 @@ from .models import Bestellung # Ersetze dies durch deine echten Klassennamen
|
|||||||
|
|
||||||
admin.site.register(Bestellung)
|
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
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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)),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 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'])]),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 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.
@@ -3,73 +3,18 @@ from django.contrib.auth.models import User
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import datetime
|
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):
|
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'), ('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 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:
|
class Meta:
|
||||||
verbose_name_plural = "Personen"
|
verbose_name_plural = "Personen"
|
||||||
|
|
||||||
def __str__(self):
|
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})"
|
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."""
|
||||||
@@ -188,167 +133,3 @@ class Bestellung(models.Model):
|
|||||||
verbose_name = "Bestellung"
|
verbose_name = "Bestellung"
|
||||||
verbose_name_plural = "Bestellungen"
|
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
|
|
||||||
|
|||||||
55
mensa_app/templates/mensa_app/gericht_list.html
Normal file
55
mensa_app/templates/mensa_app/gericht_list.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mensa-Speisekarte</title>
|
||||||
|
<!-- Bootstrap CSS für schnelles Styling -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class='card-header bg-primary text-white text-center py-3'>
|
||||||
|
<h1><i class="bi bi-egg-fried"></i> Unsere Speisekarte</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Gericht</th>
|
||||||
|
<th>Kategorie</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for gericht in alle_gerichte %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ gericht.name }}</strong></td>
|
||||||
|
<td><span class="badge bg-info text-dark">{{ gericht.kategorie.name }}</span></td>
|
||||||
|
<td>
|
||||||
|
{% if gericht.ist_dauerangebot %}
|
||||||
|
<span class="badge bg-success">Dauerangebot</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Tagesangebot</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center text-muted">Keine Gerichte im System gefunden.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
mensa_app/urls.py
Normal file
8
mensa_app/urls.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import GerichtListView
|
||||||
|
from .views import SpeiseplanView # Achte auf den neuen Klassennamen!
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('speisekarte/', GerichtListView.as_view(), name='gericht_list'),
|
||||||
|
path('speiseplan/', SpeiseplanView.as_view(), name='speiseplan'),
|
||||||
|
]
|
||||||
@@ -1,3 +1,50 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from .models import Gericht
|
||||||
|
|
||||||
|
class GerichtListView(ListView):
|
||||||
|
model = Gericht
|
||||||
|
template_name = 'mensa_app/gericht_liste.html' # Der Pfad zum Template
|
||||||
|
context_object_name = 'alle_gerichte' # Der Name, den wir im Template nutzen
|
||||||
|
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
from .models import Menue, Gericht, SpeiseplanTag
|
||||||
|
|
||||||
|
class SpeiseplanView(TemplateView):
|
||||||
|
template_name = 'mensa_app/speiseplan.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# 1. Datum aus der URL holen (z.B. ?datum=2023-10-27)
|
||||||
|
# Wenn kein Datum angegeben ist, nehmen wir heute.
|
||||||
|
date_str = self.request.GET.get('datum')
|
||||||
|
if date_str:
|
||||||
|
try:
|
||||||
|
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
target_date = timezone.glob.now().date()
|
||||||
|
else:
|
||||||
|
target_date = timezone.now().date()
|
||||||
|
|
||||||
|
# 2. Menüs für diesen spezifischen Tag laden
|
||||||
|
# Wir suchen alle Menüs, deren Tag das target_date hat
|
||||||
|
context['target_date'] = target_date
|
||||||
|
context['menues_day'] = Menue.objects.filter(tag__datum=target_date)
|
||||||
|
|
||||||
|
# 3. Dauerangebote laden (unabhängig vom Tag)
|
||||||
|
context['dauerangebote'] = Gericht.objects.filter(ist_dauerangebot=True)
|
||||||
|
|
||||||
|
# 4. Pager-Logik: Vorherigen und nächsten Tag finden
|
||||||
|
# Wir suchen in der Tabelle SpeiseplanTag nach dem Tag davor/danach
|
||||||
|
prev_tag = SpeiseplanTag.objects.filter(datum__lt=target_date).order_by('-datum').first()
|
||||||
|
next_tag = SpeiseplanTag.objects.filter(datum__gt=target_date).order_by('datum').first()
|
||||||
|
|
||||||
|
context['prev_date'] = prev_tag.datum.strftime('%Y-%m-%d') if prev_tag else None
|
||||||
|
context['next_date'] = next_tag.datum.strftime('%Y-%m-%d') if next_tag else None
|
||||||
|
|
||||||
|
return context
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -10,13 +10,11 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
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
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
path('', include('mensa_app.urls')), # Schaltet die App-URLs frei
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user