Compare commits

...

19 Commits

Author SHA1 Message Date
mputzlocher 214113e95e Merge branch 'gui-dev' into main 2026-06-26 07:33:17 +00:00
mputzlocher 40bd15f378 Vorbereitung für Erfassung neuer Gerichte 2026-06-26 09:29:22 +02:00
mputzlocher d7461cb7d2 README.md aktualisiert
Anleitung für die Entwicklungsarbeit ergänzt
2026-06-25 07:10:32 +00:00
mputzlocher e6d1b3407b kleine Änderungen 2026-06-25 08:59:15 +02:00
mputzlocher 5d060587cb Datenbank wieder verfügbar gemacht für Testdaten 2026-06-12 12:51:27 +02:00
mputzlocher eac2457a06 README.md aktualisiert 2026-06-12 10:41:57 +00:00
mputzlocher 6ee0f9ea9d README.md aktualisiert 2026-06-12 10:38:58 +00:00
mputzlocher c3615bac76 Merge pull request 'Aktualisierung nach merge, damit Datenmodell vollständig verfügbar ist.' (#6) from main into gui-dev
Reviewed-on: #6
2026-06-12 09:19:05 +00:00
mputzlocher 0868caf35f nötige Anpassungen nach Merge 2026-06-12 11:15:53 +02:00
mputzlocher 297ee45590 Remove pycache and sqlite from version control 2026-06-12 11:06:00 +02:00
mputzlocher b85e19b94a UML-Sequenzdiagramm zum Bestellugnsprozess 2026-06-11 09:35:31 +02:00
mputzlocher 0a6ecda7a2 Neue Bestellübersicht erstellt. /bestellungen/summary 2026-06-10 10:43:53 +02:00
mputzlocher a62aed56ae Versuche die Eltern-Kind-Beziehung zu bauen, Fehler beim Erstellen der
Migration.
2026-05-20 01:02:29 +02:00
mputzlocher 5ab93129ee Bilder im Datenmodell angelegt, upload, Anzeige und delete
funktionieren.
2026-05-19 23:53:18 +02:00
mputzlocher 5e06265a2f Datenmodell für Bewertungssystem (5 stars) ergänzt. 2026-05-15 11:32:44 +02:00
mputzlocher 5d6235679a Speiseplan für einen Tag mit Pager erstellt. 2026-05-15 11:18:40 +02:00
mputzlocher 4533fd6402 Erster View und erstes Template angelegt, URL /speisekarte registriert. 2026-05-15 01:18:38 +02:00
mputzlocher bb184ff952 Merge branch 'main' of https://git.sgtlernen.de/mputzlocher/SGTMensa 2026-05-11 22:43:28 +02:00
mputzlocher 54e5bc276e ER-Diagramm hinzugefügt und Datenmodell um Schulwochen erweitert. 2026-05-11 22:42:17 +02:00
56 changed files with 1373 additions and 16 deletions
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
+5
View File
@@ -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
+94 -5
View File
@@ -6,12 +6,15 @@ Muster-Implementation durch die Lehrkraft in python3 mit Django
## To get started
## Vorbereitung der virtuellen Entwicklungsumgebung
in einem Projekt-Vezeichnis über dem eigentlichen SGTMensa-Projekt-Ordner
```bash
cd SGTMensa
python3 -m venv .venv
source .venv/bin/activate
pip install django
django-admin startproject mensa-core .
pip install pillow
```
Falls eine fish-Shell vorliegt:
@@ -19,9 +22,27 @@ Falls eine fish-Shell vorliegt:
python3 -m venv .venv
source .venv/bin/activate.fish
pip install django
django-admin startproject mensa-core .
pip install pillow
```
## Klonen des eigentlichen Projekts
```bash
git clone https://git.sgtlernen.de/mputzlocher/SGTMensa.git
```
## Starten des Servers
```bash
cd SGTMensa
python manage.py runserver
```
---
# Erweiterte Erklärung einzelner Schritte
### Schritt 1: Die Arbeitsumgebung isolieren (Virtual Environment)
Bevor du auch nur eine Zeile Code schreibst, erstelle eine eigene Umgebung. Das verhindert, dass sich verschiedene Projekte auf deinem Rechner gegenseitig stören.
@@ -42,9 +63,10 @@ Installiere nur das Nötigste. Für den Start reicht Django.
```bash
pip install django
pip install pillow
```
### Schritt 3: Das Projekt initialisieren
### Schritt 3: Das Projekt initialisieren (nur einmalig, beim ersten Start des Projekts)
Erstelle die Struktur eines Django-Projekts.
@@ -118,4 +140,71 @@ python manage.py runserver
Öffne deinen Browser unter `http://127.0.0.1:8000/admin`. Logge dich mit deinem Superuser ein. **Herzlichen Glückwunsch!** Du hast eine voll funktionsfähige Web-Anwendung, mit der du bereits Speisepläne und Bestellungen in einer Datenbank verwalten kannst.
Öffne deinen Browser unter `http://127.0.0.1:8000/admin`. Logge dich mit deinem Superuser ein. **Herzlichen Glückwunsch!** Du hast eine voll funktionsfähige Web-Anwendung, mit der du bereits Speisepläne und Bestellungen in einer Datenbank verwalten kannst.
### Schritt 9: Eigentliche Entwicklungsarbeit
Folgende Befehle werden nacheinander ausgeführt:
#### Eigenen Projektbaum auf dem lokalen Rechen auf den aktuellen Stand bringen
Öffne ein Terminal und navigiere in das Projektverzeichnis, das bereits über `git clone` abgeholt worden ist.
zum Beispiel:
```bash
cd ~/projects/SGTMensa
```
Hole den aktuellen Stand ab:
```bash
git pull
```
Aktiviere die virtuelle Entwicklungsumgebung (falls noch nicht geschehen):
```bash
source .venv/bin/activate
```
Führe notwendige Migrationen der bestehenden Datenbank aus:
```bash
python manage.py makemigrations
python manage.py migrate
```
Starte den Server des Projekts:
```bash
python manage.py runserver
```
Jetzt bist Du startklar für die Entwicklungsarbeit und solltest den aktuellen Stand im Browser unter `http://localhost:8000/` abrufen können.
#### Änderungen vornehmen
Django erkennt selbstständig Veränderungen am Quellcode und startet den Webserver neu. Achte auf Fehlermeldungen!
#### Änderungen wieder auf den git-Server einspielen
Nach einem Entwicklungsschritt solltest Du Deine Änderungen von Deinem lokalen Rechner wieder auf den git-Server hochladen,
damit Dein Team auch sieht, was Du gemacht hast, und damit Deine Ergänzungen und Änderungen wieder in den gemeinsamen
Code integriert werden können.
Nachsehen der Änderungen:
```bash
git status
```
Dies zeigt alle veränderten Dateien an.
Veränderte Dateien ins Änderungspaket aufnehmen:
```bash
git add .
```
Veränderungen bestätigen und beschreiben:
```bash
git commit -m "Deine sinnvolle Beschreibung der Änderung in wenigen Worten"
```
Veränderungen auf den git-Server hochladen:
```bash
git push
```
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+67 -2
View File
@@ -3,7 +3,54 @@ 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 JoinModel
# 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 KinderInline 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
admin.site.register(Schulwoche)
from .models import SpeiseplanTag # Ersetze dies durch deine echten Klassennamen
@@ -15,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
@@ -25,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
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
@startuml
' Teilnehmer StandardIcons
actor Student as "Student"
participant Browser as "Browser"
participant WebsiteServer as "Website Server"
database OrderDB as "Order DB"
participant OrderService as "Order Service"
participant InventoryService as "Inventory Service"
participant KitchenDisplaySystem as "Kitchen Display"
participant NotificationService as "Notification Service"
' ---------- Login ----------
Student -> Browser : Öffnet die Webseite
Browser -> WebsiteServer : GET /login
WebsiteServer --> Browser : Liefert LoginSeite
Student -> Browser : Gibt Benutzerdaten ein und schickt Formular
Browser -> WebsiteServer : POST /login\n{username, password}
WebsiteServer --> OrderService : Authentifiziere User
OrderService --> WebsiteServer : OK (SessionToken)
WebsiteServer --> Browser : Setzt SessionCookie & leitet zur Menüseite
' ---------- Menü auswählen ----------
Student -> Browser : Wählt Speisen aus dem Menü
Browser -> WebsiteServer : GET /menu\n{session}
WebsiteServer --> OrderDB : Liefert aktuelle Menüpunkte
OrderDB --> WebsiteServer : Menüdaten
WebsiteServer --> Browser : Zeigt Menü
' ---------- Artikel zum Warenkorb hinzufügen ----------
loop Für jeden gewählten Artikel
Student -> Browser : Klickt „In den Warenkorb“
Browser -> OrderService : POST /cart/add\n{session, articleID}
OrderService -> OrderDB : Update Cart (Add Item)
OrderDB --> OrderService : OK
OrderService --> Browser : Rückmeldung "Artikel hinzugefügt"
end
' ---------- Bestellung prüfen ----------
Student -> Browser : Öffnet Warenkorb und prüft Bestellübersicht
Browser -> OrderService : GET /cart\n{session}
OrderService -> OrderDB : Liefert aktuelle CartDaten
OrderDB --> OrderService : CartDetails
OrderService --> Browser : Zeigt Cart‑Übersicht
' ---------- Checkout (ohne Bezahlung) ----------
Student -> Browser : Klicke „Zur Kasse“
Browser -> WebsiteServer : GET /checkout\n{session}
WebsiteServer --> NotificationService : Sende “Bestellung anstehend” (Optional)
NotificationService --> Browser : Bestätigungsnachricht
Student -> Browser : Gibt Zahlungsinformationen für Bezahlung vor Ort ein und schickt Formular
Browser -> OrderService : POST /checkout/confirm\n{session, paymentMethod="VorOrt"}
OrderService --> OrderDB : Speichere Order & set Status “Pending Payment”
OrderDB --> OrderService : OK
OrderService --> KitchenDisplaySystem : Sende OrderDetails (Zubereitung)
KitchenDisplaySystem --> Browser : Zeigt „Bestellung in Bearbeitung“
OrderService -> InventoryService : Update Bestand
InventoryService --> OrderService : OK
OrderService --> NotificationService : Sende “Bestellung bestätigt” (Email/SMS)
' ---------- Fertigstellung ----------
Student -> Browser : Empfängt Benachrichtigung “Ihr Essen ist fertig”
Browser -> KitchenDisplaySystem : Optional „Abholung bestätigen“
@enduml
+54
View File
@@ -0,0 +1,54 @@
@startuml
entity Person {
+user: User
+rolle: CharField
+klasse: CharField
}
entity User {
+username: CharField
}
entity SpeiseplanTag {
+datum: DateField
}
entity Kategorie {
+name: CharField
}
entity Gericht {
+name: CharField
+ist_vegetarisch: BooleanField
+ist_allergene_frei: BooleanField
+allergene: TextField
+preis: DecimalField
+time_last_change: TimeField
+time_creation: TimeField
+ist_dauerangebot: BooleanField
}
entity Menue {
+tag: ForeignKey(SpeiseplanTag)
+gericht: ForeignKey(Gericht)
+preis: DecimalField
}
entity Bestellung {
+person: ForeignKey(Person)
+menue: ForeignKey(Menue)
+datum_bestellung: DateTimeField
+status: CharField
+bezahlt: BooleanField
}
Person "1" -- "1" User : 1:1
Person "1" -- "1" Gericht : 1:N
Kategorie "1" -- "*" Gericht : 1:N
SpeiseplanTag "1" -- "*" Gericht : 1:N
Menue "1" -- "*" SpeiseplanTag : N:1
Menue "1" -- "*" Gericht : N:1
Person "1" -- "*" Bestellung : N:1
Gericht "1" -- "*" Bestellung : N:1
@enduml
Binary file not shown.
+33
View File
@@ -0,0 +1,33 @@
# forms.py
from django import forms
from .models import Gericht
# Importiere alle Modelle, die für das Formular relevant sind
from .models import Kategorie
class GerichtForm(forms.ModelForm):
"""Definiert die Struktur und Validierungsregeln für das Gericht-Modell."""
class Meta:
model = Gericht
fields = [
'name',
'kategorie',
'preis',
'ist_vegetarisch',
'ist_allergene_frei',
'allergene',
'beschreibung', # Neues Feld für Zutaten/Rohstoffe
'bilder', # Das Bild-Feld
'ist_dauerangebot'
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Wir können hier zusätzliche Validierungen oder Widgets einbinden
# Beispiel: Erzwinge das Betreten der Kategorie-Auswahl
self.fields['kategorie'].required = True
# Hier könntest du auch einen Fallback für den Preis hinzufügen, falls er nicht gesetzt ist
if 'initial' in kwargs: # Wird bei GET-Requests genutzt
self.initial['preis'] = 0.00
@@ -0,0 +1,43 @@
# Generated by Django 6.0.5 on 2026-05-11 20:35
import datetime
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0002_alter_bestellung_options_alter_gericht_options_and_more'),
]
operations = [
migrations.CreateModel(
name='Schulwoche',
fields=[
('id', models.CharField(default='test_1', max_length=20, primary_key=True, serialize=False, unique=True)),
('datum', models.DateField(unique=True)),
('ist_aktiv', models.BooleanField(default=False)),
('ist_ferienwoche', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Schulwoche',
'verbose_name_plural': 'Schulwochen',
},
),
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 11, 20, 35, 15, 941417)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 11, 20, 35, 15, 941417)),
),
migrations.AddField(
model_name='speiseplantag',
name='schulwoche',
field=models.ForeignKey(default='test_1', on_delete=django.db.models.deletion.CASCADE, to='mensa_app.schulwoche'),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 6.0.5 on 2026-05-11 20:35
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0003_schulwoche_alter_gericht_time_creation_and_more'),
]
operations = [
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 5, 11, 20, 35, 44, 570572)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 5, 11, 20, 35, 44, 570572)),
),
]
@@ -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')},
},
),
]
@@ -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)),
),
]
@@ -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 = [
]
@@ -0,0 +1,38 @@
# Generated by Django 5.2.15 on 2026-06-12 10:50
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mensa_app', '0010_merge_20260612_0913'),
]
operations = [
migrations.AlterModelOptions(
name='person',
options={},
),
migrations.AlterField(
model_name='gericht',
name='time_creation',
field=models.TimeField(default=datetime.datetime(2026, 6, 12, 10, 50, 49, 206132)),
),
migrations.AlterField(
model_name='gericht',
name='time_last_change',
field=models.TimeField(default=datetime.datetime(2026, 6, 12, 10, 50, 49, 206132)),
),
migrations.AlterField(
model_name='gerichtbild',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='person',
name='rolle',
field=models.CharField(choices=[('schueler', 'Schüler'), ('eltern', 'Eltern'), ('lehrer', 'Lehrer'), ('mitarbeiter', 'Mensa-Mitarbeiter'), ('chef', 'Mensa-Leitung')], max_length=20),
),
]
+247 -5
View File
@@ -1,24 +1,101 @@
import os
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
import datetime
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 ManytoMany 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ückBeziehung (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 KinderNamen 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'):
# # NichtEltern 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):
"""Repräsentiert eine Schulwoche mit einem eindeutigen Datum."""
id = models.CharField(max_length=20, unique=True, primary_key=True, default="test_1") # eine eindeutige ID wie "Woche_2024_10"
datum = models.DateField(unique=True) # Datum der Schulwoche (z. B. Montag der ersten Woche)
ist_aktiv = models.BooleanField(default=False)
ist_ferienwoche = models.BooleanField(default=False)
class Meta:
verbose_name = "Schulwoche"
verbose_name_plural = "Schulwochen"
class SpeiseplanTag(models.Model):
"""Ein bestimmter Tag im Speiseplan."""
datum = models.DateField(unique=True)
schulwoche = models.ForeignKey(
Schulwoche,
on_delete=models.CASCADE,
#related_name='tag_get_schulwoche' # Für Abfragen wie: Schulwoche.tag_get_schulwoche.all()
default = "test_1"
)
class Meta:
verbose_name = "Tag mit Speiseplan"
verbose_name_plural = "Tage mit Speiseplan"
@@ -51,6 +128,7 @@ class Gericht(models.Model):
)
ist_vegetarisch = models.BooleanField(default=False)
ist_allergene_frei = models.BooleanField(default=False)
beschreibung = models.TextField(blank=True, null=True) # Zutaten/Rohstoffe Beschreibung (Textfeld für V1)
allergene = models.TextField(blank=True, default="")
preis = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
@@ -111,3 +189,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: BildDatei (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 × 480px
(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 FileInstanz
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 ProduktivBetrieb 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 OriginalDatei.
"""
# 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 DeleteLogik:
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 StandardDeleteMethode auf, um den DBEintrag 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 ProduktivBetrieb besser mit logging.exception() umgehen
print(f"Warnung konnte die Bilddatei nicht löschen ({exc})")
# --- 2️⃣ Rufe die normale Django-DeleteLogik auf ------------------------------------
super().delete(*args, **kwargs) # löscht den DBEintrag
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="de">
<head>
<!-- (Standard Head und Bootstrap CDN einbinden) -->
</head>
<body class="bg-light">
<div class="container mt-5">
<h1>Neue Speiseart hinzufügen</h1>
{# !!! KRITISCH WICHTIG: enctype muss für Dateiuploads stehen !!! #}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<!-- Der Standardway, alle Felder mit Bootstrap zu stylen (für Anfänger am besten) -->
{# Man kann die komplexe Struktur durch eine einfache Schleife ersetzen #}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
<!-- Fehlerhinweis des Feldes wird hier automatisch gerendert -->
{% if field.errors %}
<p class="text-danger small mt-1">{{ field.errors }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-success mt-4">Gericht Speichern & Veröffentlichen</button>
</form>
</div>
</body>
</html>
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Erfolg</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="alert alert-success shadow p-5 text-center">
<h2 class="mb-3">🎉 Erfolgreich hinzugefügt! 🎉</h2>
<p class="lead">
Das Gericht <strong>{{ objekt_name }}</strong> wurde erfolgreich in das Menüsystem übernommen.
</p>
<p>
Du kannst nun die Details überprüfen und weitere Gerichte hinzufügen.
</p>
</div>
<div class="mt-4 text-center">
<!-- Der wichtigste Button, der zum Hauptbereichen zurückführt -->
<a href="{% url 'speisekarte' %}" class="btn btn-primary btn-lg">
&larr; Zurück zur Speisekarte &amp; Verwalten
</a>
</div>
</div>
</body>
</html>
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Bestell-Zusammenfassung</title>
<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">
<!-- PAGER & DATUM -->
<div class="card shadow mb-4">
<div class="card-body text-center">
<h2 class="mb-3">Bestell-Check für den <span class="text-success">{{ target_date|date:"d.m.Y" }}</span></h2>
<div class="btn-group">
<a href="?datum={{ prev_date }}" class="btn btn-outline-secondary {% if not prev_date %}disabled{% endif %}">&laquo; Vorheriger Tag</a>
<a href="?" class="btn btn-primary">Heute</a>
<a href="?datum={{ next_date }}" class="btn btn-outline-secondary {% if not next_date %}disabled{% endif %}">Nächster Tag &raquo;</a>
</div>
</div>
</div>
<!-- DASHBOARD CARDS -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card bg-white shadow border-start border-primary border-5">
<div class="card-body">
<h6 class="text-muted">Gesamtanzahl Bestellungen</h6>
<h3 class="mb-0">{{ total_bestellungen }}</h3>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-white shadow border-start border-success border-5">
<div class="card-body">
<h6 class="text-muted">Erwarteter Umsatz</h6>
<h3 class="mb-0">{{ total_umsatz|floatformat:2 }} €</h3>
</div>
</div>
</div>
</div>
<!-- DETAILS TABELLE -->
<div class="card shadow">
<div class="card-header bg-dark text-white">Details pro Gericht</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Gericht</th>
<th>Kategorie</th>
<th class="text-center">Menge</th>
<th class="text-end">Umsatz</th>
</tr>
</thead>
<tbody>
{% for stat in summary_stats %}
<tr>
<td><strong>{{ stat.menue__gericht__name }}</strong></td>
<td><span class="badge bg-info text-dark">{{ stat.menue__gericht__kategorie__name }}</span></td>
<td class="text-center">{{ stat.anzahl }}</td>
<td class="text-end">{{ stat.umsatz|floatformat:2 }} €</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">Keine Bestellungen für diesen Tag vorhanden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="mt-3 text-end">
<a href="{% url 'speisekarte' %}" class="btn btn-link text-decoration-none">&larr; Zurück zur Speisekarte</a>
</div>
</div>
</body>
</html>
@@ -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>
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mensa Speiseplan</title>
<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">
<!-- PAGER & DATUM ANZEIGE -->
<div class="card shadow mb-4">
<div class="card-body text-center">
<h2 class="mb-3">Speiseplan für den <br><span class="text-primary">{{ target_date|date:"d.m.Y" }}</span></h2>
<div class="btn-group" role="group" aria-label="Datum Navigation">
<!-- Button Zurück -->
{% if prev_date %}
<a href="?datum={{ prev_date }}" class="btn btn-outline-secondary">&laquo; Vorheriger Tag</a>
{% else %}
<button class="btn btn-outline-light text-muted" disabled>&laquo; Vorheriger</button>
{% endif %}
<!-- Button Heute (Reset) -->
<a href="?" class="btn btn-primary">Heute</a>
<!-- Button Weiter -->
{% if next_date %}
<a href="?datum={{ next_date }}" class="btn btn-outline-secondary">Nächster Tag &raquo;</a>
{% else %}
<button class="btn btn-outline-light text-muted" disabled>Nächster &raquo;</button>
{% endif %}
</div>
</div>
</div>
<!-- TABELLE DER TAGESMENÜS -->
<div class="card shadow mb-4">
<div class="card-header bg-dark text-white">Tagesmenüs</div>
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead>
<tr>
<th>Gericht</th>
<th>Kategorie</th>
<th>Preis</th>
</tr>
</thead>
<tbody>
{% for menue in menues_day %}
<tr>
<td>{{ menue.gericht.name }}</td>
<td><span class="badge bg-info text-dark">{{ menue.gericht.kategorie.name }}</span></td>
<td>{{ menue.preis|floatformat:2 }} €</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-3 text-muted">Keine Tagesmenüs für diesen Tag geplant.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- TABELLE DER DAUERANGEBOTE -->
<div class="card shadow">
<div class="card-header bg-success text-white">Immer verfügbar (Dauerangebote)</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
{% for gericht in dauerangebote %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ gericht.name }}
<span class="badge bg-success rounded-pill">Verfügbar</span>
</li>
{% empty %}
<li class="list-group-item text-muted">Momentan keine Dauerangebote.</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
from django.urls import path
from .views import GerichtListView
from .views import SpeiseplanView # Achte auf den neuen Klassennamen!
from .views import BestellSummaryView
urlpatterns = [
path('speisekarte/', GerichtListView.as_view(), name='speisekarte'),
path('speiseplan/', SpeiseplanView.as_view(), name='speiseplan'),
path('bestellungen/summary/', BestellSummaryView.as_view(), name='bestell_summary'),
]
# ACHTUNG: Hier benötigen wir jetzt eine separate View für die Erfolgsmeldung!
# Wir nutzen hierfür ein sehr einfaches TemplateView.
from django.views.generic import TemplateView # Muss oben importiert werden
from .models import Gericht
class ErfolgTemplateView(TemplateView):
"""Zeigt einfach nur die Bestätigung an."""
template_name = 'mensa_app/add_gericht_success.html'
urlpatterns += [
# ... (vorherige Pfade bleiben) ...
path('gerichte/neu/', GerichtCreateView.as_view(), name='add_gericht'),
# NEU: Erfolgsmeldung nach erfolgreichem POST/redirect
path('erfolg/', ErfolgTemplateView.as_view(), name='success_message'),
]
+128 -1
View File
@@ -1,3 +1,130 @@
# views.py
from django.db.models import Count, Sum
from django.shortcuts import render
from django.utils import timezone
from datetime import datetime
from django.views.generic import ListView, TemplateView
from django.views.generic.edit import CreateView
from django.shortcuts import redirect # Importiere die Redirect-Funktion
from .forms import GerichtForm # Importiere dein neues Formular
from .models import Gericht, Menue, SpeiseplanTag, Bestellung
# Create your views here.
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
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
class BestellSummaryView(TemplateView):
template_name = 'mensa_app/bestell_summary.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 1. Datum aus der URL holen (wie beim Speiseplan)
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.now().date()
else:
target_date = timezone.now().date()
context['target_date'] = target_date
# 2. Aggregation: Bestellungen nach Gericht gruppieren
# Wir suchen alle Bestellungen, deren Menü am target_date stattfindet
summary_stats = (
Bestellung.objects.filter(menue__tag__datum=target_date)
.values('menue__gericht__name', 'menue__gericht__kategorie__name') # Gruppierung nach Name & Kategorie
.annotate(
anzahl=Count('id'), # Wie viele wurden bestellt?
umsatz=Sum('menue__preis') # Was macht das für einen Umsatz?
)
.order_by('menue__gericht__name')
)
context['summary_stats'] = summary_stats
# 3. Pager-Logik (identisch mit dem Speiseplan-View)
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
# 4. Gesamtzahlen für die Übersicht
context['total_bestellungen'] = Bestellung.objects.filter(menue__tag__datum=target_date).count()
context['total_umsatz'] = Bestellung.objects.filter(menue__tag__datum=target_date).aggregate(Sum('menue__preis'))['menue__preis__sum'] or 0
return context
class GerichtCreateView(CreateView):
"""
View zur Erstellung eines neuen Gerichts.
Nutzt das benutzerdefinierte GerichtForm für die gesamte Steuerung.
"""
model = Gericht
form_class = GerichtForm # Verwendet unser Custom-Formular
template_name = 'mensa_app/add_gericht.html'
def get_success_url(self):
# Die Methode wird ausgeführt, nachdem die View erfolgreich ist
objekt = self.object # Gibt uns das gerade gespeicherte Objekt zurück
return reverse('speisekarte')
def form_valid(self, form):
"""
Übersteuert die Standard-Logik:
1. Speichert zuerst das Gericht (über super()).
2. Leitet danach auf ein success/confirmation Pattern um.
"""
super().form_valid() # Das eigentliche Speichern des Objekts
# Hier ist der Haken an Django: Die View wird beim Erfolg automatisch weitergeleitet.
# Wir verwenden einen kleinen Trick, damit wir den Namen im Kontext haben.
return redirect('success_nach_gerichterstellung') # Leiten auf eine temporäre URL um
# Optional: Setzen des initialen Datenzustands (z.B. Datum, falls relevant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -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 OriginalDateien 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/
+6 -2
View File
@@ -15,8 +15,12 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
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 = [
path('admin/', admin.site.urls),
]
path('', include('mensa_app.urls')), # Schaltet die App-URLs frei
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)