Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a62aed56ae | |||
| 5ab93129ee | |||
| 5e06265a2f |
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.
@@ -3,7 +3,48 @@ 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
|
||||||
|
|
||||||
admin.site.register(Person)
|
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
|
from .models import Schulwoche # Ersetze dies durch deine echten Klassennamen
|
||||||
|
|
||||||
@@ -19,7 +60,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
|
||||||
|
|
||||||
@@ -29,3 +70,21 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
@@ -3,18 +3,73 @@ 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'), ('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
|
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."""
|
||||||
@@ -133,3 +188,167 @@ 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
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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,50 +1,3 @@
|
|||||||
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,11 +10,13 @@ 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,9 +15,10 @@ 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, include
|
from django.urls import path
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include('mensa_app.urls')), # Schaltet die App-URLs frei
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user