Compare commits
18 Commits
a62aed56ae
...
gui-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| e0bd01fa88 | |||
| 1b1e6c1553 | |||
| f42ca76066 | |||
| 60a046939b | |||
| 214113e95e | |||
| 40bd15f378 | |||
| d7461cb7d2 | |||
| e6d1b3407b | |||
| 5d060587cb | |||
| eac2457a06 | |||
| 6ee0f9ea9d | |||
| c3615bac76 | |||
| 0868caf35f | |||
| 297ee45590 | |||
| b85e19b94a | |||
| 0a6ecda7a2 | |||
| 5d6235679a | |||
| 4533fd6402 |
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -119,3 +141,70 @@ 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.
|
||||
|
||||
|
||||
### 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
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB 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.
+30
-28
@@ -3,33 +3,35 @@ from django.contrib import admin
|
||||
# Register your models here.
|
||||
from .models import Person # Ersetze dies durch deine echten Klassennamen
|
||||
|
||||
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)
|
||||
# DELTE: Die Option, Kinder zuzuordnen soll zunächst nicht umgesetzt werden.
|
||||
# class PersonInlineChildren(admin.TabularInline):
|
||||
# """
|
||||
# Inline für das Feld ``children`` (Schüler eines Elternteils).
|
||||
# Nur bei Eltern (Rolle Mitarbeit/Chef) sichtbar.
|
||||
# """
|
||||
# model = Person.children.through # Das durchschnittliche Join‑Model
|
||||
# verbose_name = 'Kind'
|
||||
# verbose_name_plural = 'Kinder'
|
||||
# extra = 0 # Keine leeren Zeilen anzeigen
|
||||
#
|
||||
# # ----------------------------------------------------------
|
||||
# # **WICHTIG:** Hier wird der zu verwendende FK explizit genannt
|
||||
# # ----------------------------------------------------------
|
||||
# fk_name = 'person' # <-- legt fest, welcher FK gemeint ist
|
||||
#
|
||||
# def has_add_permission(self, request, obj=None):
|
||||
# # Erlaube Hinzufügen nur für Eltern (Rolle Eltern)
|
||||
# return obj and obj.rolle in ('eltern')
|
||||
#
|
||||
# def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
# """
|
||||
# Verhindere, dass ein Benutzer sich selbst als Kind hinzufügt.
|
||||
# """
|
||||
# if db_field.name == "person":
|
||||
# # Hier handelt es sich um den FK auf die Person (das Ziel)
|
||||
# kwargs["queryset"] = Person.objects.exclude(pk=self.instance.pk)
|
||||
# return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
@admin.register(Person)
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
@@ -37,8 +39,8 @@ class PersonAdmin(admin.ModelAdmin):
|
||||
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
|
||||
# # ---- 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):
|
||||
"""
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
@startuml
|
||||
|
||||
' Teilnehmer – Standard‑Icons
|
||||
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 Login‑Seite
|
||||
Student -> Browser : Gibt Benutzerdaten ein und schickt Formular
|
||||
Browser -> WebsiteServer : POST /login\n{username, password}
|
||||
WebsiteServer --> OrderService : Authentifiziere User
|
||||
OrderService --> WebsiteServer : OK (Session‑Token)
|
||||
WebsiteServer --> Browser : Setzt Session‑Cookie & 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 Cart‑Daten
|
||||
OrderDB --> OrderService : Cart‑Details
|
||||
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 Order‑Details (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
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
'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,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,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 = [
|
||||
]
|
||||
+38
@@ -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),
|
||||
),
|
||||
]
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-26 08:55
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mensa_app', '0011_alter_person_options_alter_gericht_time_creation_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gericht',
|
||||
name='beschreibung',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gericht',
|
||||
name='time_creation',
|
||||
field=models.TimeField(default=datetime.datetime(2026, 6, 26, 8, 55, 13, 383962)),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gericht',
|
||||
name='time_last_change',
|
||||
field=models.TimeField(default=datetime.datetime(2026, 6, 26, 8, 55, 13, 383962)),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+52
-51
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
import datetime
|
||||
|
||||
import os
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import FileExtensionValidator
|
||||
# Pillow ist für die Bildbearbeitung zuständig (installiere per pip install pillow)
|
||||
@@ -19,55 +17,57 @@ class Person(models.Model):
|
||||
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
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Neue Many‑to‑Many Beziehung: „Elternteil ↔ Schüler“
|
||||
# ----------------------------------------------------------------------
|
||||
children = models.ManyToManyField(
|
||||
'self',
|
||||
blank=True,
|
||||
related_name='parents', # → für einen Schüler: person.parents.all()
|
||||
symmetrical=False, # Verhindert eine zirkuläre Rück‑Beziehung (nicht benötigt)
|
||||
help_text=(
|
||||
"Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
|
||||
)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Optional: Hilfsmethode, die alle Kinder zurückgibt, ggf. mit einer Sortierung
|
||||
def get_children_sorted(self):
|
||||
# Das `order_by('name')` ist ein Beispiel – du kannst deine eigene Sortierung verwenden.
|
||||
return self.children.order_by('name')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Personen"
|
||||
|
||||
def __str__(self):
|
||||
if self.children.exists():
|
||||
# Wir sammeln alle Kinder‑Namen in einem String, z. B. mit Komma getrennt.
|
||||
children_names = ', '.join([c.name for c in self.children.all()])
|
||||
return f"{self.user.username} ({children_names}) ({self.rolle})"
|
||||
else:
|
||||
return f"{self.user.username} ({self.rolle})"
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Überprüfung für die ManyToMany-Beziehung ``children``:
|
||||
- Nur Personen mit der Rolle 'mitarbeiter' oder 'chef' (Eltern) dürfen Kinder haben.
|
||||
- Ein Schüler darf sich selbst nicht als Kind hinzufügen.
|
||||
"""
|
||||
super().clean() # ruft die Validierungen der Elternklasse auf (hier User/Model)
|
||||
|
||||
if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
|
||||
# Nicht‑Eltern dürfen keine Kinder besitzen
|
||||
if self.children.exists():
|
||||
raise ValidationError(
|
||||
"Nur Benutzer mit der Rolle 'Eltern' "
|
||||
"dürfen Schüler zuordnen (children)."
|
||||
)
|
||||
else:
|
||||
# Eltern: Verhindere, dass sie sich selbst als Kind eintragen
|
||||
if self in self.children.all():
|
||||
raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
|
||||
# DELETE: Die Eltern-Kinder Beziehung soll zunächst nicht umgesetzt werden.
|
||||
# # ----------------------------------------------------------------------
|
||||
# # Neue Many‑to‑Many Beziehung: „Elternteil ↔ Schüler“
|
||||
# # ----------------------------------------------------------------------
|
||||
# children = models.ManyToManyField(
|
||||
# 'self',
|
||||
# blank=True,
|
||||
# related_name='parents', # → für einen Schüler: person.parents.all()
|
||||
# symmetrical=False, # Verhindert eine zirkuläre Rück‑Beziehung (nicht benötigt)
|
||||
# help_text=(
|
||||
# "Eltern können hier die ihnen zugewiesenen Schüler hinzufügen. Für Schüler bleibt dieses Feld leer."
|
||||
# )
|
||||
# )
|
||||
#
|
||||
# # ------------------------------------------------------------------
|
||||
# # Optional: Hilfsmethode, die alle Kinder zurückgibt, ggf. mit einer Sortierung
|
||||
# def get_children_sorted(self):
|
||||
# # Das `order_by('name')` ist ein Beispiel – du kannst deine eigene Sortierung verwenden.
|
||||
# return self.children.order_by('name')
|
||||
#
|
||||
# class Meta:
|
||||
# verbose_name_plural = "Personen"
|
||||
#
|
||||
# def __str__(self):
|
||||
# if self.children.exists():
|
||||
# # Wir sammeln alle Kinder‑Namen in einem String, z. B. mit Komma getrennt.
|
||||
# children_names = ', '.join([c.name for c in self.children.all()])
|
||||
# return f"{self.user.username} ({children_names}) ({self.rolle})"
|
||||
# else:
|
||||
# return f"{self.user.username} ({self.rolle})"
|
||||
#
|
||||
# def clean(self):
|
||||
# """
|
||||
# Überprüfung für die ManyToMany-Beziehung ``children``:
|
||||
# - Nur Personen mit der Rolle 'mitarbeiter' oder 'chef' (Eltern) dürfen Kinder haben.
|
||||
# - Ein Schüler darf sich selbst nicht als Kind hinzufügen.
|
||||
# """
|
||||
# super().clean() # ruft die Validierungen der Elternklasse auf (hier User/Model)
|
||||
#
|
||||
# if self.rolle in ('mitarbeiter', 'chef', 'lehrer'):
|
||||
# # Nicht‑Eltern dürfen keine Kinder besitzen
|
||||
# if self.children.exists():
|
||||
# raise ValidationError(
|
||||
# "Nur Benutzer mit der Rolle 'Eltern' "
|
||||
# "dürfen Schüler zuordnen (children)."
|
||||
# )
|
||||
# else:
|
||||
# # Eltern: Verhindere, dass sie sich selbst als Kind eintragen
|
||||
# if self in self.children.all():
|
||||
# raise ValidationError("Ein Benutzer darf nicht gleichzeitig Elternteil und eigener Kind sein.")
|
||||
|
||||
|
||||
|
||||
@@ -128,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)
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Neues Gericht hinzufügen</title>
|
||||
<!-- Bootstrap CSS einbinden -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- OPTIONAL: Füge hier deinen eigenen, kleinen Custom-CSS-Link hinzu! -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container mt-5 mb-5">
|
||||
<!-- Der Haupttitel muss prominent sein und signalisiert den Zweck der Seite -->
|
||||
<header class="mb-4 border-bottom pb-2">
|
||||
<h1>🥗 Neues Gericht Speiseplan hinzufügen</h1>
|
||||
<p class="text-muted">Bitte fülle alle notwendigen Felder aus, um das Gericht zu veröffentlichen.</p>
|
||||
</header>
|
||||
|
||||
<!-- Die Hauptkarte für das Formular -->
|
||||
<div class="card shadow p-4 mb-5" id="gericht-form-container">
|
||||
{# ⚠️ Wichtig: enctype bleibt erhalten! #}
|
||||
<form method="post" enctype="multipart/upload">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- ==================== BLOCK 1: GRUNDDATEN (Kategorie, Name, Preis) ==================== -->
|
||||
<div class="card mb-4 shadow-sm border-primary">
|
||||
<div class="card-header bg-primary text-white p-3">Grunddaten & Identifikation</div>
|
||||
<div class="card-body row g-3">
|
||||
|
||||
<!-- Name (col-md-4) -->
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}<p class="text-danger small mt-1">{{ form.name.errors }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Kategorie (Dropdown) -->
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
{{ form.kategorie }}
|
||||
{% if form.kategorie.errors %}<p class="text-danger small mt-1">{{ form.kategorie.errors }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Preis (Money Input) -->
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
{{ form.preis }}
|
||||
{% if form.preis.errors %}<p class="text-danger small mt-1">{{ form.preis.errors }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ==================== BLOCK 2: DETAILS & STATUS (Allergene, Flags) ==================== -->
|
||||
<div class="card mb-4 shadow-sm border-warning">
|
||||
<div class="card-header bg-warning text-dark p-3">Details und Statusprüfung</div>
|
||||
<div class="card-body row g-3">
|
||||
|
||||
<!-- Switches (Checkboxen) - Nehmen wenig Platz ein -->
|
||||
<div class="col-md-4 col-lg-2 mb-3">
|
||||
{% if form.ist_vegetarisch %}
|
||||
{{ form.ist_vegetarisch }} <label for="{{ form.ist_vegetarisch.id_for_label }}" class="form-check-label">Vegetarisch</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-lg-2 mb-3">
|
||||
{% if form.ist_allergene_frei %}
|
||||
{{ form.ist_allergene_frei }} <label for="{{ form.ist_allergene_frei.id_for_label }}" class="form-check-label">Allergenfrei</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Dauerangebot -->
|
||||
<div class="col-md-4 col-lg-2 mb-3">
|
||||
{{ form.ist_dauerangebot }}
|
||||
<label for="{{ form.ist_dauerangebot.id_for_label }}" class="form-check-label">Dauerangebot</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== BLOCK 3: TEXTBEDARF (Beschreibung, Allergene) ==================== -->
|
||||
<div class="card mb-4 shadow-sm border-info">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title text-info mb-3">Beschreibung & Nährstoffe</h6>
|
||||
|
||||
<!-- Allergene (TextArea) -->
|
||||
<div class="mb-3">
|
||||
{{ form.allergene }}
|
||||
{% if form.allergene.errors %}<p class="text-danger small mt-1">{{ form.allergene.errors }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung (TextArea) -->
|
||||
<div class="mb-3">
|
||||
{{ form.beschreibung }}
|
||||
{% if form.beschreibung.errors %}<p class="text-danger small mt-1">{{ form.beschreibung.errors }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ==================== BLOCK 4: BILDER & Upload (Media) ==================== -->
|
||||
<div class="card mb-5 shadow-sm border-secondary">
|
||||
<div class="card-header bg-secondary text-white p-3">Mediendateien hochladen</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
{{ form.bilder }}
|
||||
{% if form.bilder.errors %}<p class="text-danger small mt-1">{{ form.bilder.errors }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Submit Button Container -->
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-lg btn-success">Gericht Speichern & Veröffentlichen</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</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">
|
||||
← Zurück zur Speisekarte & 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 %}">« 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 »</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">← 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">« Vorheriger Tag</a>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-light text-muted" disabled>« 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 »</a>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-light text-muted" disabled>Nächster »</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>
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.urls import path
|
||||
from .views import GerichtListView
|
||||
from .views import SpeiseplanView # Achte auf den neuen Klassennamen!
|
||||
from .views import BestellSummaryView
|
||||
from .views import GerichtCreateView
|
||||
|
||||
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'),
|
||||
]
|
||||
+144
-1
@@ -1,3 +1,146 @@
|
||||
# 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, reverse # 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):
|
||||
"""
|
||||
Die korrigierte Version der Form-Validierung.
|
||||
Das 'form' Argument wird hier explizit als zweites Argument deklariert, um den TypeError zu verhindern.
|
||||
"""
|
||||
# 1.!!! WICHTIG: Das Aufrufen von super().form_valid() MUSS der allererste Befehl sein!
|
||||
# Django speichert damit das Hauptobjekt (das Gericht) und übergibt es als self.object
|
||||
super().form_valid(form)
|
||||
|
||||
# Jetzt sind wir sicher, dass das Objekt gespeichert wurde und Zugriff auf das neu erstellte Objekt haben:
|
||||
newly_created_dish = self.object
|
||||
|
||||
# 2. Bilder verarbeiten (Der kritische Schritt nach der Speicherung)
|
||||
uploaded_files = self.request.FILES.getlist('bilder') # Achtung: self.request wird hier genutzt!
|
||||
|
||||
if uploaded_files:
|
||||
for file in uploaded_files:
|
||||
try:
|
||||
GerichtBild.objects.create(
|
||||
gericht=newly_created_dish,
|
||||
image=file,
|
||||
sort_order=0 # Setze eine Standardreihenfolge
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"WARNUNG beim Speichern des Bildes {file.name}: {e}")
|
||||
|
||||
# Wir müssen danach explizit den Return-Wert festlegen, damit Django weitergeleitet werden kann.
|
||||
return super().form_valid(form)
|
||||
|
||||
# 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
@@ -15,10 +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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user