Compare commits

..

308 Commits

Author SHA1 Message Date
ben8
d56c9d3419 Kritik zusammengefasst 2026-02-07 11:33:28 +01:00
ben8
543ae87a14 sliden verbessert 2026-02-07 11:14:43 +01:00
ben8
7e22e818ef Merge branch '166-detail-quiz-uberarbeiten' into 'master'
Resolve "Detail quiz überarbeiten"

Closes #166

See merge request enrichment-2024/qivip!123
2026-01-17 14:47:46 +00:00
ben8
81dce0e2bd Design Update #166 2026-01-17 15:47:15 +01:00
ben8
45dc8bd18d Merge branch '165-fragen-reihenfolge-verschieben' into 'master'
Resolve "Fragen Reihenfolge verschieben"

Closes #165

See merge request enrichment-2024/qivip!122
2026-01-17 12:55:52 +00:00
ben8
0d21d0b94b #165 change question_order 2026-01-17 13:55:00 +01:00
ben8
022a59cc7b Merge branch '93-idee-quiz-kritisieren' into 'master'
Resolve "Idee: QUIZ "kritisieren""

Closes #93

See merge request enrichment-2024/qivip!121
2026-01-17 10:47:32 +00:00
ben8
0b23f6bf9d rename 2026-01-17 11:46:20 +01:00
ben8
fb225dc6f3 critizise Quiz 2026-01-17 11:11:34 +01:00
ben8
1fb0958daf #93 critisize-Quiz 2026-01-17 11:10:47 +01:00
ben8
cf5b428031 Merge branch '163-auf-ahnliche-quizze-verweisen' into 'master'
Resolve "Auf ähnliche Quizze verweisen"

Closes #163

See merge request enrichment-2024/qivip!120
2025-12-03 14:21:59 +00:00
ben8
b596655e43 #163 Verweis auf andere Quizze 2025-12-03 15:21:26 +01:00
ben8
334d2aa8dd Merge branch '162-ubersicht-auf-startseite-hinzufugen' into 'master'
Resolve "Übersicht auf Startseite hinzufügen"

Closes #162

See merge request enrichment-2024/qivip!119
2025-11-27 18:05:52 +00:00
ben8
9c06c81cb1 #162 Info über qivip 2025-11-27 19:05:14 +01:00
ben8
bd3c1555ae Abstand verändert 2025-11-16 16:46:01 +01:00
ben8
ccb8c4350d Merge branch '158-design-quiz-formular' into 'master'
Resolve "Design Quiz Formular"

Closes #158

See merge request enrichment-2024/qivip!115
2025-11-16 15:38:45 +00:00
ben8
f6382069d0 #158 Design fix 2025-11-16 16:38:15 +01:00
ben8
d65f5d8927 Merge branch '157-genauere-punktevergabe-bei-der-kartenfunktion' into 'master'
Resolve "Genauere Punktevergabe bei der Kartenfunktion!"

Closes #157

See merge request enrichment-2024/qivip!114
2025-11-15 17:54:52 +00:00
ben8
5897ec03c7 Genauigkeit bei Punkten für Karte 2025-11-15 18:54:12 +01:00
ben8
63d801d62b Hinweis zur Quiz-Statistik 2025-11-15 17:32:18 +01:00
ben8
f0f10593b4 Merge branch '156-fehler-bei-punktevergabe-bei-der-schatzfunktion' into 'master'
Resolve "Fehler bei Punktevergabe bei der Schätzfunktion"

Closes #156

See merge request enrichment-2024/qivip!113
2025-11-15 16:22:10 +00:00
ben8
147f702a39 #156 fix 2025-11-15 17:21:31 +01:00
ben8
53c7199895 Merge branch '155-quiz-design-anpassen' into 'master'
Resolve "Quiz Design anpassen!"

Closes #155

See merge request enrichment-2024/qivip!112
2025-11-15 16:03:15 +00:00
ben8
0371fc6ade #155 design for credits 2025-11-15 17:00:17 +01:00
ben8
1bbafefa71 Merge branch '154-anzahl-wie-oft-ein-quiz-gespielt-worden-ist' into 'master'
Resolve "Anzahl, wie oft ein Quiz gespielt worden ist."

Closes #154

See merge request enrichment-2024/qivip!111
2025-11-14 16:25:26 +00:00
ben8
304cf0a01b #154-counter_played_quizzes 2025-11-14 17:24:51 +01:00
ben8
92214d3bda Merge branch '85-fragentyp-ort-suchen' into 'master'
Resolve "Fragentyp: Ort suchen"

Closes #85

See merge request enrichment-2024/qivip!103
2025-11-01 18:41:37 +00:00
ben8
ce0b8305b4 geopy and geographiclib zu Paketen hinzugefügt 2025-11-01 19:40:48 +01:00
ben8
ee823629b9 Teilnehmer_Koordinaten-einzeichnen 2025-11-01 17:09:42 +01:00
ben8
57d325df17 small_fix 2025-11-01 15:43:35 +01:00
ben8
4f320d5c8b Fix 2025-10-31 20:26:33 +01:00
ben8
bd3c956e19 style 2025-10-27 12:37:47 +01:00
ben8
7e892571fb Design-Anpassung (Formular) 2025-10-24 19:52:58 +02:00
ben8
63107a00e6 Merge branch '85-fragentyp-ort-suchen' of edugit.org:enrichment-2024/qivip into 85-fragentyp-ort-suchen 2025-10-24 18:47:46 +02:00
juhn
0d098c38eb adjust map score tolerance 2025-10-24 18:24:59 +02:00
juhnsa
fa544d8228 Fix errors 2025-10-24 18:24:59 +02:00
juhnsa
e816c3f75a First Iteration of Map Question Type 2025-10-24 18:24:59 +02:00
ben8
bb36b3e71f Merge branch '153-footer-fix' into 'master'
Resolve "Footer fix"

Closes #153

See merge request enrichment-2024/qivip!110
2025-10-24 16:17:42 +00:00
ben8
27a5a78e10 fix footer 2025-10-24 18:17:11 +02:00
juhn
657b81cd46 adjust map score tolerance 2025-10-24 16:03:16 +02:00
juhnsa
07b9ddf745 Fix errors 2025-10-24 16:03:16 +02:00
juhnsa
ded3328d06 First Iteration of Map Question Type 2025-10-24 16:03:06 +02:00
ben8
760ba5be06 Paket_Liste überarbeitet 2025-10-21 18:14:42 +02:00
ben8
46052d136b Merge branch '64-footer-muss-unten-angezeigt-werden-2' into 'master'
Resolve "Footer muss unten angezeigt werden"

Closes #64

See merge request enrichment-2024/qivip!109
2025-10-21 15:48:32 +00:00
ben8
3701442e8b Footer überarbeitet 2025-10-21 17:37:13 +02:00
ben8
c21363e2ac Merge branch '152-css' into 'master'
Resolve "css"

Closes #152

See merge request enrichment-2024/qivip!108
2025-10-09 17:27:08 +00:00
ben8
35d5884156 design-fix 2025-10-09 18:13:51 +02:00
nik8
a5337b1fde Merge branch '151-der-text-ist-teilweise-nicht-mehr-zu-sehen-im-erstellmodus' into 'master'
Resolve "Der Text ist teilweise nicht mehr zu sehen!!! Im Erstellmodus!"

Closes #151

See merge request enrichment-2024/qivip!107
2025-09-27 17:07:13 +00:00
nik8
3b37bd8338 Fix_Schrift + request bei requirements 2025-09-27 19:06:44 +02:00
ben8
df4f7e92bc Merge branch '141-richtig-schones-cooles-siegerpodest-2' into 'master'
Resolve "Richtig schönes, cooles Siegerpodest."

Closes #141

See merge request enrichment-2024/qivip!106
2025-07-29 17:02:51 +00:00
ben8
d03c99b8e3 schönes Siegerpodest (und bessere Punkteübersicht) 2025-07-29 19:01:20 +02:00
ben8
c9ddecfba7 Punktevergabe fix Fragetyp:order 2025-07-24 21:50:18 +02:00
ben8
2da75fdb52 podest_1.Version 2025-07-24 21:02:41 +02:00
ben8
86a4ecc351 Merge branch '148-schatzen-fragentyp' into 'master'
Resolve "Schätzen - Fragentyp"

Closes #148

See merge request enrichment-2024/qivip!105
2025-07-21 17:08:59 +00:00
ben8
07633dcaf9 Schätzfunktion( (mit Schieberegler) #148 2025-07-21 18:56:17 +02:00
ben8
885eb1e528 Merge branch '147-quiztyp-reihenfolge' into 'master'
Resolve "Quiztyp - Reihenfolge"

Closes #147

See merge request enrichment-2024/qivip!104
2025-07-20 12:07:18 +00:00
ben8
9397da4cfd Reihenfolge_Fragetyp(und Lizenzenz_Dateien) 2025-07-20 14:06:52 +02:00
ben8
f778107c30 Reihenfolge-Fragetyp (und Lizenzenübersicht) 2025-07-20 14:05:51 +02:00
ben8
826af8beb2 Merge branch '146-pdf-erstellen-fur-quiz' into 'master'
Resolve "PDF - erstellen für Quiz"

Closes #146

See merge request enrichment-2024/qivip!102
2025-07-13 11:32:02 +00:00
ben8
f80e35a0d0 fix-Multiple-choice (verursacht bei Änderung - darkMode) 2025-07-13 13:31:25 +02:00
ben8
736db1c480 PDF 2025-07-13 13:12:45 +02:00
ben8
4ac5dd794b Merge branch '145-dark-mode' into 'master'
Resolve "Dark mode"

Closes #145

See merge request enrichment-2024/qivip!101
2025-07-11 14:57:56 +00:00
ben8
d17d41e07a dark_mode 2025-07-11 16:54:00 +02:00
ben8
7f845b8f36 Merge branch '24-rahmen-um-die-quiz-in-der-bibliothek-farbe-and-die-kategorie' into 'master'
Resolve "Rahmen um die Quiz in der Bibliothek Farbe and die Kategorie."

Closes #24

See merge request enrichment-2024/qivip!100
2025-07-04 11:57:25 +00:00
ben8
37f4a7b868 Kategorie_mit_Farbpunkt 2025-07-04 13:56:32 +02:00
ben8
d929e6b5dd weiter Button in überspringen umbenannt 2025-06-29 17:45:42 +02:00
ben8
6e63e0876a optisches Update - Schrift 2025-06-29 17:43:28 +02:00
ben8
d907390652 credits_verbessert 2025-06-29 17:11:04 +02:00
ben8
31e9618e1c credits_darstellung_verbessert 2025-06-29 17:10:25 +02:00
ben8
73345bf6b2 fix_zu published_at 2025-06-29 16:27:32 +02:00
ben8
6c7e4770d6 fix_published_date_bei_Kopie 2025-06-29 16:26:20 +02:00
ben8
0fe5c0cac3 credits 2025-06-27 16:29:00 +02:00
ben8
c912b63d8d neuer Slogan 2025-06-27 16:18:36 +02:00
ben8
177c200467 sinnvolle Sortierung 2025-06-27 15:53:46 +02:00
ben8
2a06add37e fix 2025-06-27 15:37:45 +02:00
ben8
fd3464711b Merge branch '143-zeitangabe-fur-quizze' into 'master'
Resolve "Zeitangabe für Quizze"

Closes #143

See merge request enrichment-2024/qivip!98
2025-06-27 13:19:14 +00:00
ben8
93e166ece0 Bewertung_fix und Veröffentlichungszeit 2025-06-27 15:17:27 +02:00
juhnsa
096349c989 Standardmaessig 4 statt 2 Optionen bei Multiple Choice Fragen 2025-06-27 13:47:14 +02:00
ben8
1014ea37ec Merge branch '140-umstrukturierung-der-library' into 'master'
Resolve "Umstrukturierung der Library"

Closes #140

See merge request enrichment-2024/qivip!97
2025-06-23 12:55:20 +00:00
ben8
e65cd408eb improved_library_structure#140 2025-06-23 14:54:46 +02:00
ben8
d2274960cb richtige Anzeige der Punkte fix zu #139 2025-06-14 15:21:29 +02:00
ben8
016c253bc4 Merge branch '139-frage-bei-doppelklick-wird-falschlich-ubersprungen' into 'master'
Resolve "Frage bei Doppelklick wird fälschlich übersprungen!"

Closes #139

See merge request enrichment-2024/qivip!96
2025-06-14 13:15:18 +00:00
ben8
9f0d80bc41 weiterButton Fragen-richtig überspringen und #139 (gelöst) 2025-06-14 15:14:54 +02:00
ben8
f8dad9f0bd Merge branch '138-zahlen-bei-anmeldung-wie-viele-quizze-fragen-erstellt' into 'master'
Resolve "Zahlen bei Anmeldung -- wie viele Quizze, Fragen erstellt"

Closes #138

See merge request enrichment-2024/qivip!95
2025-06-14 12:24:30 +00:00
ben8
d55c4babc0 Erfolge#138 2025-06-14 14:24:05 +02:00
ben8
4875ed9676 Merge branch '137-credits-auch-fur-fragen' into 'master'
Resolve "Credits auch für Fragen"

Closes #137

See merge request enrichment-2024/qivip!94
2025-06-13 18:36:48 +00:00
ben8
7fcad195bd Credits bei Fragen 2025-06-13 20:36:06 +02:00
ben8
78fbdc9cc6 Credits-bei Fragen 2025-06-13 20:35:51 +02:00
ben8
4e399a5423 Credits-verbessert (Link und Darstellung) 2025-06-13 18:04:44 +02:00
ben8
c56d850dfa Merge branch '90-404-seite-desinen-2' into 'master'
Resolve "404 Seite designen"

Closes #90

See merge request enrichment-2024/qivip!40
2025-06-13 15:55:29 +00:00
ben8
136fa7864a customized-404-Seite#90 2025-06-13 17:53:02 +02:00
ben8
e916c010fe Merge branch '136-wenn-kein-bild-uberschrift-auf-blauem-hintergrund-bei-den-quizzen' into 'master'
Resolve "Wenn kein Bild: Überschrift auf blauem Hintergrund bei den Quizzen!"

Closes #136

See merge request enrichment-2024/qivip!93
2025-06-13 15:44:13 +00:00
ben8
809a4495d5 Schrift-auf_Quiz#136 2025-06-13 17:43:55 +02:00
ben8
5ca48f9287 Merge branch '132-in-der-anzeige-wenn-man-sich-die-antwortmoglichketen-anzeigt-da-man-auf-das-auge-geklickt-hat' into 'master'
Resolve "In der Anzeige wenn man sich die Antwortmöglichketen anzeigt da man auf das Auge geklickt hat auch die Bilder darstellen!"

Closes #132

See merge request enrichment-2024/qivip!92
2025-06-13 15:30:19 +00:00
ben8
16471141b2 pictures_and_numbers(detail_quiz)#132 2025-06-13 17:29:43 +02:00
ben8
f008d932ab Merge branch '135-design' into 'master'
Resolve "Design"

Closes #135

See merge request enrichment-2024/qivip!91
2025-06-08 17:51:51 +00:00
ben8
431e1c07c8 design#135 2025-06-08 19:51:10 +02:00
ben8
c41d9cc9b5 Merge branch '131-style-optimieren' into 'master'
Resolve "Style optimieren!"

Closes #131

See merge request enrichment-2024/qivip!90
2025-06-07 17:28:37 +00:00
ben8
fec37580ed style_überarbeitet#131 2025-06-07 19:28:17 +02:00
Frank Poetzsch-Heffter
b06ed09492 Merge branch '130-anpassungen-fur-produktivserverbetrieb' into 'master'
Änderungen zum Produktivbetrieb

Closes #130

See merge request enrichment-2024/qivip!89
2025-06-03 15:40:09 +00:00
Frank Poetzsch-Heffter
1771cd0d77 Änderungen zum Produktivbetrieb 2025-06-03 17:28:51 +02:00
ben8
609a3a423a Merge branch '129-faqs-auch-in-der-db-bearbeiten-konnen' into 'master'
Resolve "Faqs auch in der DB bearbeiten können!"

Closes #129

See merge request enrichment-2024/qivip!87
2025-06-01 17:34:56 +00:00
ben8
aea07295d8 Faqs über DB hinzufügen! 2025-06-01 19:34:21 +02:00
ben8
3bfca331f2 Merge branch '127-suchfunktion-verbessern' into 'master'
Resolve "Suchfunktion verbessern"

Closes #127

See merge request enrichment-2024/qivip!85
2025-06-01 14:16:49 +00:00
ben8
dcee37f295 Optimierte Suchvorschläge 2025-06-01 16:16:25 +02:00
ben8
78bf2d296f Merge branch '126-kein-zugriff-auch-privates-quiz' into 'master'
Resolve "kein Zugriff auch privates Quiz"

Closes #126

See merge request enrichment-2024/qivip!84
2025-06-01 13:54:39 +00:00
ben8
a78cbcc10e privates Quiz sichern 2025-06-01 15:54:15 +02:00
ben8
c245f42821 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-05-23 19:39:14 +02:00
ben8
0c8223ec83 maximale Bildgröße 2025-05-23 19:39:10 +02:00
ben8
3056a8b389 Merge branch '125-counter-der-punkte-js-animation' into 'master'
Resolve "Counter der Punkte (js animation)"

Closes #125

See merge request enrichment-2024/qivip!83
2025-05-23 17:24:36 +00:00
ben8
f30c984342 counter für Punkte und Optikverbesserungen 2025-05-23 19:22:41 +02:00
ben8
8f6bfd2aad QR Code Größe 2025-05-19 18:42:39 +02:00
ben8
2268035e58 Merge branch '124-qr-code' into 'master'
Resolve "QR Code"

Closes #124

See merge request enrichment-2024/qivip!82
2025-05-19 16:31:54 +00:00
ben8
59c3e5d90c QR Code 2025-05-19 18:31:17 +02:00
ben8
8efd22034c Merge branch '123-reaktion-auf-die-antworten-3' into 'master'
Resolve "Reaktion auf die antworten"

Closes #123

See merge request enrichment-2024/qivip!81
2025-05-18 17:54:09 +00:00
ben8
d02109e474 Sprüche nach Antwort 2025-05-18 19:53:33 +02:00
Frank Poetzsch-Heffter
49f5e0bad9 python3 core/manage.py to python django/manage.py 2025-05-18 09:35:58 +00:00
Frank Poetzsch-Heffter
321487e7c8 Merge branch '120-produktivumgebung-vorbereiten' into 'master'
Resolve "Produktivumgebung vorbereiten"

Closes #120

See merge request enrichment-2024/qivip!77
2025-05-18 09:23:25 +00:00
Frank Poetzsch-Heffter
b318237f50 dotenv eingerichtet 2025-05-18 11:18:51 +02:00
nik8
198905232c Merge branch '110-hintergrundmusik' into 'master'
Resolve "Hintergrundmusik?"

Closes #110

See merge request enrichment-2024/qivip!76
2025-05-17 12:57:58 +00:00
nik8
43f81a5ae7 Musik 2025-05-17 14:55:41 +02:00
nik8
c66e22a436 Merge branch '121-teilnehmer-nicht-uberdecken' into 'master'
Resolve "Teilnehmer nicht überdecken"

Closes #121

See merge request enrichment-2024/qivip!78
2025-05-17 10:46:24 +00:00
nik8
fbec1fed05 Scrollbar in Lobby 2025-05-17 12:46:03 +02:00
ben8
169f9b41a1 Merge branch '119-design' into 'master'
Resolve "Design"

Closes #119

See merge request enrichment-2024/qivip!75
2025-05-17 10:11:27 +00:00
ben8
75e25fb3df fix und Balkenanimation 2025-05-17 12:08:25 +02:00
nik8
e1d4f145f0 UPDATE SLOGAN 2025-05-17 11:42:01 +02:00
7eb2a80656 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-05-10 15:36:45 +02:00
3aaf684b0e Datenschutzhinweise in Register und New Quiz 2025-05-10 15:35:34 +02:00
ben8
9199678b4b Merge branch '94-langfristig-unnotige-packages-code-etc-entfernen' into 'master'
Resolve "Langfristig: Unnötige Packages/Code etc. entfernen!"

Closes #94

See merge request enrichment-2024/qivip!73
2025-05-10 12:41:49 +00:00
ben8
5d8f3ed820 Unnötiges zum Teil entfernt! 2025-05-10 14:41:31 +02:00
a5d4e75040 Rechtschreibfehler im FAQ 2025-05-10 14:37:26 +02:00
7e4bb05c74 Datenschutz edit 2025-05-10 14:25:58 +02:00
1bb2666784 Datenschutzerklaerung 2025-05-10 14:23:07 +02:00
nik8
c05c3fd4a0 faqs_update4 2025-05-10 13:53:55 +02:00
nik8
c9a1342f1d faqs_update3 2025-05-10 13:52:34 +02:00
nik8
991488790a faqs_update2 2025-05-10 13:44:13 +02:00
ben8
c014003062 Merge branch '114-suchleiste-vorschlage-anzeigen' into 'master'
Resolve "Suchleiste - Vorschläge anzeigen!"

Closes #114

See merge request enrichment-2024/qivip!70
2025-05-10 10:53:30 +00:00
nik8
8133eaa439 faqs_update 2025-05-10 12:17:17 +02:00
38a2ffcaff CSS Fix 2025-05-10 12:11:50 +02:00
ben8
cfa95a84e7 Merge branch '117-faqs' into 'master'
Resolve "FAQs"

Closes #117

See merge request enrichment-2024/qivip!72
2025-05-10 10:02:47 +00:00
ben8
64e89d3749 Merge branch '81-uberarbeitung-des-kontos-fur-umbenennung-und-co' into 'master'
Resolve "Überarbeitung des Kontos für Umbenennung und co."

Closes #81

See merge request enrichment-2024/qivip!65
2025-05-10 09:45:26 +00:00
ben8
2708be3177 faqs 2025-05-10 11:34:39 +02:00
ben8
647b45d8a6 Merge branch '115-fehler-eingabe-funktion' into 'master'
Resolve "Fehler Eingabe Funktion"

Closes #115

See merge request enrichment-2024/qivip!71
2025-05-09 13:54:49 +00:00
ben8
c8e2817b07 Antwort_abschicken_ENTER+Design_verbessert 2025-05-09 15:53:34 +02:00
ben8
01e7909e3a fix Eingabe 2025-05-08 21:27:21 +02:00
dfee602d54 CSS Fixes for Login / Logout Button 2025-05-07 15:26:05 +02:00
ben8
0062f88167 Suchleiste Vorschläge 2025-05-04 21:28:19 +02:00
ben8
de3d3dd00c Hinweis, wenn Url nicht mehr gültig! 2025-05-03 17:20:10 +02:00
ben8
85ba8bc9cb Merge branch '113-suchleiste-optimieren' into 'master'
Resolve "Suchleiste optimieren"

Closes #113

See merge request enrichment-2024/qivip!68
2025-05-03 10:10:35 +00:00
ben8
a8103b4a20 kein_Verspringen_der_Suchleiste fix #113 2025-05-03 12:10:06 +02:00
ben8
ba3e89f9f7 Hinweis_Quiz_zu_favoriten_hinzugefügt 2025-05-03 11:44:22 +02:00
ben8
dbc457b21d Merge branch '75-idee-neuer-spielmodus-eingabe' into 'master'
Resolve "IDEE: Neuer Spielmodus: Eingabe"

Closes #75

See merge request enrichment-2024/qivip!67
2025-05-02 17:30:06 +00:00
ben8
a6cac52ba6 Eingabe_Funktion 2025-05-02 17:06:29 +02:00
ben8
5f10c2a57b Umbenennung Funktion in Passwort vergessen? bei Login! 2025-04-30 17:08:55 +02:00
ben8
19092b092d fix_change_password 2025-04-27 12:02:53 +02:00
ben8
2ca623bf33 Passwort-zurücksetzen, Passwort-ändern, Account-löschen! 2025-04-24 12:41:36 +02:00
ben8
a7c42ecf13 Merge branch '108-bilderupload-verbessern' into 'master'
Resolve "Bilderupload verbessern!"

Closes #108

See merge request enrichment-2024/qivip!60
2025-04-23 15:30:51 +00:00
ben8
cc8cc9ad74 Bild_löschen 2025-04-23 16:13:14 +02:00
ben8
0c8a8d3062 sinnvolleres_speichern 2025-04-23 16:11:08 +02:00
ben8
a021e7561a Bildspeichern_DB_fix 2025-04-23 13:02:28 +02:00
ben8
9321b44056 Bild_anzeige_in_modi_fix 2025-04-23 12:16:18 +02:00
Juhn-Vitus Saß
873b50103d Revert breaking changes.
Revert "Add missing file for collect static"

This reverts commit b823909386.
2025-04-23 12:00:50 +02:00
ben8
41cecf98dc fix_Bilder 2025-04-23 11:52:32 +02:00
Juhn-Vitus Saß
ac5859a666 Move Media Root Definition 2025-04-23 10:57:14 +02:00
Juhn-Vitus Saß
779762c2a3 WhiteNoise Configuration 2025-04-23 10:50:39 +02:00
Juhn-Vitus Saß
da78060415 Serve media in Production 2025-04-23 10:45:15 +02:00
Juhn-Vitus Saß
b823909386 Add missing file for collect static 2025-04-22 22:41:40 +02:00
Juhn-Vitus Saß
82f0cd5191 Correct Package name 2025-04-22 20:05:11 +02:00
Juhn-Vitus Saß
1069dd1cad Nachricht für keine Bewertung auch bei fremden Quizzen anzeigen 2025-04-22 19:36:16 +02:00
Juhn-Vitus Saß
905da6737c Merge branch '89-pop-up-menu-fur-mobile' into 'master'
Resolve "Pop-up Menü für Mobile"

Closes #89

See merge request enrichment-2024/qivip!64
2025-04-22 17:14:14 +00:00
Juhn-Vitus Saß
6eed8107ac feat(navbar): Implementiere responsive Navbar mit mobilem Menü
- Füge Hamburger-Menü für mobile Ansichten hinzu
- Positioniere Suchleiste zentral auf dem Desktop
- Implementiere animierte Transitions beim Öffnen/Schließen des Menüs
- Entferne untere Border-Radius der Navbar bei geöffnetem Menü für nahtloses Erscheinungsbild
- Optimiere Layout für verschiedene Bildschirmgrößen
- Behalte Designsprache und Farbschema bei (blau/weiß)
2025-04-22 19:13:55 +02:00
ben8
95e4428943 fix_Bilder 2025-04-22 19:11:31 +02:00
Juhn-Vitus Saß
bad257f341 Merge branch '83-auch-bei-dem-orginalquiz-sollte-der-autor-stehen' into 'master'
Resolve "Auch bei dem Orginalquiz sollte der Autor stehen!"

Closes #83

See merge request enrichment-2024/qivip!63
2025-04-22 16:55:42 +00:00
Juhn-Vitus Saß
eaf709ed4d Verbesserte Ansicht des Ursprungs und der Editoren 2025-04-22 18:54:59 +02:00
Juhn-Vitus Saß
8ca470ad54 Merge branch '71-bewertungssystem-ueberarbeiten' into 'master'
Resolve "Bewertungssystem ueberarbeiten"

Closes #71

See merge request enrichment-2024/qivip!62
2025-04-22 16:33:02 +00:00
Juhn-Vitus Saß
af919b9ea5 README updaten 2025-04-22 18:31:51 +02:00
Juhn-Vitus Saß
154110e510 Bewertungen verstecken wenn keine vorhanden und Möglichkeit zum deaktivieren von Bewertungen 2025-04-22 18:28:50 +02:00
Juhn-Vitus Saß
a16c97c4f3 Merge branch '70-siegerpodest-am-spielende' into 'master'
Resolve "Siegerpodest am Spielende"

Closes #70 and #72

See merge request enrichment-2024/qivip!61
2025-04-22 15:57:55 +00:00
Juhn-Vitus Saß
01562c6ad8 Konfetti animation
Fix #72
2025-04-22 17:57:27 +02:00
ben8
50cd95daf1 Uploadfunktion! 2025-04-22 17:52:34 +02:00
ben8
1a4b9e0d6b Hübscheres_Bilder_Upload+Zurücksetzfunktion 2025-04-22 17:51:19 +02:00
Juhn-Vitus Saß
867c839202 Abhängigkeiten erneuern 2025-04-22 14:24:26 +02:00
Juhn-Vitus Saß
8d8e211c72 Merge branch '73-lobby-ueberarbeiten' into 'master'
Resolve "Lobby ueberarbeiten"

Closes #73

See merge request enrichment-2024/qivip!56
2025-04-22 10:30:35 +00:00
Juhn-Vitus Saß
6da095c35e Overflow entfernen 2025-04-22 12:29:44 +02:00
Juhn-Vitus Saß
2dc9a077ee Benachrichtigungen fixen mit Redis 2025-04-22 12:21:47 +02:00
Juhn-Vitus Saß
881bc3d6c1 Merge branch '79-filter-problem-der-filter-soll-sich-nicht-schliessen-wenn-man-auf-filtern-klickt' into 'master'
Resolve "Filter Problem! Der Filter soll sich nicht schließen wenn man auf Filtern klickt."

Closes #79

See merge request enrichment-2024/qivip!55
2025-04-22 09:47:05 +00:00
Juhn-Vitus Saß
b1e438229a Filter bleibt offen wenn man auf filtern drückt 2025-04-22 11:46:44 +02:00
Juhn-Vitus Saß
97e4fa9505 Merge branch '82-bei-dem-kopieren-wird-momentan-noch-die-bewertung-ubernommen' into 'master'
Resolve "Bei dem Kopieren wird momentan noch die Bewertung übernommen!"

Closes #82

See merge request enrichment-2024/qivip!54
2025-04-21 19:20:30 +00:00
Juhn-Vitus Saß
3981dc31da Bewertung und Anzahl an Bewertungen zurücksetzen 2025-04-21 21:20:07 +02:00
Juhn-Vitus Saß
7b86b0791c Merge branch '104-kein-rauskicken-nach-30-sekunden' into 'master'
Resolve "kein rauskicken nach 30 Sekunden"

Closes #104 and #105

See merge request enrichment-2024/qivip!53
2025-04-21 11:36:28 +00:00
Juhn-Vitus Saß
7faf931fb2 - Entfernen des automatischen Kick-Mechanismus
- Fix #105
2025-04-21 13:35:51 +02:00
Juhn-Vitus Saß
9245f928dc Fix: Error when creating question without image 2025-04-21 11:54:30 +02:00
Frank Poetzsch-Heffter
1f5a177060 typo corrected 2025-04-21 06:34:31 +00:00
Juhn-Vitus Saß
ebbe4f7193 Merge branch '23-bilder-fur-fragen-und-co' into 'master'
Resolve "Bilder für Fragen und co."

Closes #23

See merge request enrichment-2024/qivip!49
2025-04-18 19:30:15 +00:00
Juhn-Vitus Saß
acd07bfb80 Merge branch 'master' into '23-bilder-fur-fragen-und-co'
# Conflicts:
#   django/templates/play/game/question_host.html
2025-04-18 19:29:59 +00:00
Juhn-Vitus Saß
d824816be8 Bilder für Fragen 2025-04-18 21:26:40 +02:00
ben8
2c563b3471 Merge branch '107-favoritensystem-verbessern' into 'master'
Resolve "Favoritensystem verbessern"

Closes #107

See merge request enrichment-2024/qivip!52
2025-04-18 16:47:37 +00:00
ben8
4122f0631b Favorit entfernen-in_DB_löschen 2025-04-18 18:47:10 +02:00
ben8
2372cf7146 Merge branch '106-quizze-mit-null-fragen-sollen-nicht-gespielt-werden-konnen' into 'master'
Resolve "Quizze mit null Fragen sollen nicht gespielt werden können!"

Closes #106

See merge request enrichment-2024/qivip!48
2025-04-18 16:33:34 +00:00
ben8
6305af9879 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:27:37 +02:00
ben8
176a3cc15c Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:27:27 +02:00
ben8
fa9bb45d3f Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:12:25 +02:00
ben8
cc449997dc Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:12:13 +02:00
ben8
e813660722 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:11:35 +02:00
ben8
1d6fab919e Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:11:07 +02:00
ben8
17195a221f Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:10:50 +02:00
ben8
3fc08cbd4e Merge branch 'cherry-pick-d4252c12' into 'master'
Merge branch '103-verschiedene-lernmodi' into 'master'

Closes #103

See merge request enrichment-2024/qivip!51
2025-04-18 18:07:03 +02:00
ben8
4cf6f45daf Merge branch 'cherry-pick-d4252c12' into 'master'
Merge branch '103-verschiedene-lernmodi' into 'master'

Closes #103

See merge request enrichment-2024/qivip!51
2025-04-18 14:13:18 +00:00
ben8
d2ad0f041f Edit question_host.html 2025-04-18 14:11:57 +00:00
ben8
bcdfebf791 Merge branch '103-verschiedene-lernmodi' into 'master'
Resolve "verschiedene Lernmodi"

Closes #103

See merge request enrichment-2024/qivip!47

(cherry picked from commit d4252c1297)

5b4c76a1 Zwischenstand Lernmodus
4209dcbc Merge branch 'master' into 103-verschiedene-lernmodi
9fb9ab91 Lernfunktion
e22ea409 Lernmodus verbessert und Testmodus entfernt
2052a702 Merge branch 'master' into '103-verschiedene-lernmodi'

Co-authored-by: ben8 <ben_ollech@bessermailer.de>
2025-04-18 14:00:29 +00:00
ben8
937fd4a2e4 Merge branch 'revert-d4252c12' into 'master'
Revert "Merge branch '103-verschiedene-lernmodi' into 'master'"

See merge request enrichment-2024/qivip!50
2025-04-18 13:51:58 +00:00
ben8
a9343f285a Revert "Merge branch '103-verschiedene-lernmodi' into 'master'"
This reverts merge request !47
2025-04-18 13:48:23 +00:00
ben8
d4252c1297 Merge branch '103-verschiedene-lernmodi' into 'master'
Resolve "verschiedene Lernmodi"

Closes #103

See merge request enrichment-2024/qivip!47
2025-04-18 13:28:25 +00:00
ben8
2052a702f0 Merge branch 'master' into '103-verschiedene-lernmodi'
# Conflicts:
#   django/templates/play/game/question_host.html
2025-04-18 13:28:04 +00:00
ben8
a0292ce409 Startbutton-min.1Frage 2025-04-18 15:25:29 +02:00
Juhn-Vitus Saß
73cd52ea3d Merge branch '76-die-lange-der-zeit-pro-frage-anpassen' into 'master'
Resolve "Die Länge der Zeit Pro Frage anpassen."

Closes #76

See merge request enrichment-2024/qivip!36
2025-04-18 11:01:34 +00:00
Juhn-Vitus Saß
ffe636c6c7 Merge branch 'master' into '76-die-lange-der-zeit-pro-frage-anpassen'
# Conflicts:
#   django/library/forms.py
2025-04-18 11:00:52 +00:00
ben8
e22ea40910 Lernmodus verbessert und Testmodus entfernt 2025-04-18 12:48:35 +02:00
Juhn-Vitus Saß
e269264f03 Front End einbindung von Zeitauswahl 2025-04-18 12:05:50 +02:00
ben8
9fb9ab9198 Lernfunktion 2025-04-14 23:39:43 +02:00
ben8
4209dcbc2d Merge branch 'master' into 103-verschiedene-lernmodi 2025-04-14 20:26:34 +02:00
ben8
70be6396aa Merge branch '101-zwischenseite-nach-spielen-z-b-lernmodus-klassisches-moderieren' into 'master'
Resolve "Zwischenseite nach Spielen z.B. Lernmodus, klassisches Moderieren, ..."

Closes #101

See merge request enrichment-2024/qivip!45
2025-04-14 18:24:48 +00:00
ben8
5b4c76a1c8 Zwischenstand Lernmodus 2025-04-14 20:23:47 +02:00
ben8
9d9e1527f4 Merge branch '102-verbesserung-der-anti-spring-funktion-bei-library' into 'master'
Resolve "Verbesserung der "Anti-Spring" Funktion bei library!"

Closes #102

See merge request enrichment-2024/qivip!46
2025-04-14 13:12:44 +00:00
ben8
70741a5a34 #102 fix Springen 2025-04-14 15:12:16 +02:00
ben8
8022602fc5 Zwischenauswahl_Spielmodus 2025-04-14 14:44:55 +02:00
ben8
f0dcc2baad vorrübergehende_Lösung-Suchleiste 2025-04-13 18:30:45 +02:00
ben8
2221c99ad7 Merge branch '99-suchleiste-design' into 'master'
Resolve "Suchleiste design abstand"

Closes #99

See merge request enrichment-2024/qivip!43
2025-04-13 16:17:21 +00:00
ben8
d84b44252e Darstellung Suchleiste 2025-04-13 18:16:58 +02:00
ben8
5a719de8ee Merge branch '55-quiz-zu-deinen-favoriten' into 'master'
Resolve "Quiz zu deinen Favoriten"

Closes #55

See merge request enrichment-2024/qivip!39
2025-04-13 15:33:20 +00:00
ben8
7c8c142e34 Richtige Sortierung! 2025-04-13 17:31:38 +02:00
ben8
093b0a7f83 Merge branch 'master' into 55-quiz-zu-deinen-favoriten 2025-04-13 17:07:11 +02:00
ben8
a4442fe12e Merge branch '98-betreten-form-reparieren' into 'master'
Resolve "Betreten form reparieren!"

Closes #98

See merge request enrichment-2024/qivip!42
2025-04-13 15:01:52 +00:00
ben8
a59df8dd62 #98 plus besser Darstellung der Suchleiste 2025-04-13 17:01:28 +02:00
ben8
80390de6dc Merge branch '96-suchleiste-uberall-anzeigen' into 'master'
Resolve "Suchleiste überall anzeigen"

Closes #96

See merge request enrichment-2024/qivip!41
2025-04-13 11:10:27 +00:00
ben8
5f05964b1b Suchleiste_überall #96 2025-04-13 13:09:39 +02:00
ben8
7e321af48c 404 Fehler Seite-vorbereitet! 2025-04-13 12:44:12 +02:00
ben8
d53240c710 Favoriten_nur_wenn_angemeldet! 2025-04-11 20:29:55 +02:00
ben8
1f3b551b94 Favoriten 2025-04-11 19:31:12 +02:00
ben8
2a7e39bbd0 Favoritenfunktion! #55 2025-04-11 18:18:34 +02:00
ben8
04ce8104fe Fehler_behonben 2025-04-10 20:29:28 +02:00
ben8
7ce0e6e9dd Zwischenstand_Favoriten 2025-04-10 19:55:11 +02:00
ben8
b0782868b8 Merge branch '10-die-tags-loschen' into 'master'
Resolve "die "Tags" Löschen"

Closes #10

See merge request enrichment-2024/qivip!38
2025-04-10 14:10:13 +00:00
ben8
a39c944f10 tags_gelöcscht+status_versteckt_entfernt 2025-04-10 16:09:51 +02:00
ben8
655e2a2e5d Merge branch '67-spiel-starten-button-in-den-bearbeitungsmodus-hinzufugen' into 'master'
Resolve "Spiel Starten Button in den Bearbeitungsmodus hinzufügen!!!!!!!!!!!!!!!!!!"

Closes #67

See merge request enrichment-2024/qivip!37
2025-04-10 13:54:02 +00:00
ben8
087d4cdddd #67 Spiel_button_beiEditInterface_und_kldesignchanges 2025-04-10 15:47:47 +02:00
ben8
77714d22a9 Zeit_bisher_im_Backend #76 2025-04-10 14:43:43 +02:00
nik8
f405016f96 zwischenstand 2025-04-09 14:22:46 +02:00
ben8
145d339ae1 Merge branch '80-kleine-design-verbesserungen' into 'master'
Resolve "kleine Design Verbesserungen!"

Closes #80

See merge request enrichment-2024/qivip!35
2025-04-07 17:37:50 +00:00
ben8
4e6de6fe85 Design 2025-04-07 19:36:42 +02:00
juhnsa
c4489c2301 Ergaenzung zu #68 2025-04-07 17:36:19 +02:00
ben8
bb60197c46 Merge branch '68-design-quizkopieren-plus-hinweis-wenn-quiz-kopiert' into 'master'
Resolve "Design Quizkopierbutton"

Closes #68

See merge request enrichment-2024/qivip!32
2025-04-07 15:15:13 +00:00
Juhn-Vitus Saß
a89847f818 Merge branch '45-fehlermeldungen-in-play-app-wenn-spiel-nicht-gefunden-wurde' into 'master'
Resolve "Fehlermeldungen in Play App, wenn Spiel nicht gefunden wurde"

Closes #45

See merge request enrichment-2024/qivip!33
2025-04-07 15:01:34 +00:00
juhnsa
071e76b3af Error message when quiz not found 2025-04-07 17:00:20 +02:00
Juhn-Vitus Saß
295c3aa6d5 Merge branch 'initial-game-logic' into 'master'
Initial game logic

Closes #33, #43, #52, and #66

See merge request enrichment-2024/qivip!31
2025-04-07 14:00:03 +00:00
ben8
2fab7683e1 hübscherer_button 2025-04-07 15:27:27 +02:00
Juhn-Vitus Saß
f264d10cb8 Ignore Missing Files 2025-04-06 22:24:15 +02:00
Juhn-Vitus Saß
95f2a82532 Exclude t-input as it causes problems when deployed 2025-04-06 22:15:10 +02:00
Juhn-Vitus Saß
d9f3d07dd7 Filter fix 2025-04-06 20:18:30 +02:00
Juhn-Vitus Saß
cf7a2fb8bb UI Refactoring and Fix of image issue when adding an image to a quiz 2025-04-06 19:49:46 +02:00
Juhn-Vitus Saß
3f07ce4ec7 Initial Game Logic 2 2025-04-06 15:57:42 +02:00
Juhn-Vitus Saß
3637edce33 Initial Game Logic 2025-04-06 15:57:27 +02:00
Juhn-Vitus Saß
cd2e138f66 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-05 19:12:29 +02:00
Juhn-Vitus Saß
0062b1f9c9 Ignore Staticfiles Dir 2025-04-05 19:12:25 +02:00
Juhn-Vitus Saß
a3ac9c474a Merge branch '42-websockets-in-lobby-zur-aktualisierung-der-spielerliste' into 'master'
Resolve "Websockets in Lobby zur aktualisierung der Spielerliste"

Closes #42

See merge request enrichment-2024/qivip!25
2025-04-05 14:23:27 +00:00
Juhn-Vitus Saß
783ae44ce1 Update Requirements and README 2025-04-05 16:22:50 +02:00
Juhn-Vitus Saß
0068e162f7 feat: Add real-time quiz lobby functionality
- Implement WebSocket-based lobby system for quiz participants
- Add lobby page with real-time participant updates
- Create LobbyConsumer for WebSocket communication
- Add routing configuration for WebSocket connections
- Update requirements with WebSocket dependencies
- Add development and production server documentation

This change enables real-time quiz lobbies where participants can
join and wait for the quiz to start, with instant updates for all
connected users.
2025-04-05 16:22:30 +02:00
ben2
b0b4724b4a Merge branch '38-zuruck-buttons-wenn-notig' into 'master'
Resolve "Zurück Buttons, wenn nötig"

Closes #38

See merge request enrichment-2024/qivip!30
2025-04-05 12:52:42 +00:00
nik8
ad74ec8f29 Merge branch '65-wenn-man-auf-den-bildlink-klickt-wird-man-auf-einen-fehler-weitergefuhrt' into 'master'
Resolve "Wenn man auf den Bildlink klickt wird man auf einen Fehler weitergeführt!"

Closes #65

See merge request enrichment-2024/qivip!29
2025-04-05 12:51:21 +00:00
Ben
00109d4694 Abbrechen button 2025-04-05 14:50:59 +02:00
nik8
0a91d70cd3 Bild_fehler_behoben 2025-04-05 14:50:33 +02:00
nik8
1cd6d66b85 Bilder anzeigen 2025-04-05 14:33:46 +02:00
Frank Poetzsch-Heffter
3ef1abf35e Ändere \ in / in Windows-Tailwind-Pfad 2025-04-05 12:23:53 +00:00
nik8
e29b954438 Merge branch '62-antwortenanzeige-bearbeiten' into 'master'
Resolve "Antwortenanzeige bearbeiten"

Closes #62

See merge request enrichment-2024/qivip!28
2025-04-05 11:41:49 +00:00
nik8
8c3b365e0b Antworten zeigen/verstecken 2025-04-05 13:40:55 +02:00
ben2
33b0e256c4 Merge branch '63-autor' into 'master'
Resolve "Autor!"

Closes #63

See merge request enrichment-2024/qivip!27
2025-04-05 10:57:49 +00:00
Ben
21713498b2 Bearbeitung hizugefügt 2025-04-05 12:56:25 +02:00
Juhn-Vitus Saß
8d1489f513 Creator auskommentieren 2025-04-05 12:17:34 +02:00
nik8
cbd02bece8 Merge branch 'testbranhc' 2025-04-05 12:12:31 +02:00
nik8
e135ff945f Kopie eines Quizzes (Datenbank) 2025-04-05 12:07:56 +02:00
Juhn-Vitus Saß
f3591bb9da Merge branch '48-quiz-fragen-templates-erstellen' into 'master'
Resolve "Quiz Fragen Templates erstellen"

Closes #48

See merge request enrichment-2024/qivip!15
2025-04-05 09:08:01 +00:00
nik8
fbb0787aa9 Design_Änderung_Rahmen 2025-03-28 22:29:23 +01:00
nik8
16fcaf6346 Lupe-Symbol ersetzt 2025-03-28 21:55:25 +01:00
nik8
8a6d337392 Symbole vereinheitlicht 2025-03-22 15:16:20 +01:00
ben2
5f4fd4345e Merge branch '60-refactoring-views-py-try-accept-loschen' into 'master'
Resolve "Refactoring: Views.py try accept löschen"

Closes #60

See merge request enrichment-2024/qivip!23
2025-03-22 14:01:23 +00:00
Laurenz Wolff
7e93165c58 Überflüssiger Code entfernt 2025-03-22 14:59:34 +01:00
nik8
419160d144 Merge branch '59-uniocodes-austauschen' into 'master'
Resolve "Uniocodes Austauschen"

Closes #59

See merge request enrichment-2024/qivip!22
2025-03-22 13:58:25 +00:00
nik8
0c6580e732 Ersetzen der Symbole 2025-03-22 14:58:01 +01:00
nik8
8f4b5d581d Merge branch '54-filter-fur-library' into 'master'
Resolve "Filter für library"

Closes #54

See merge request enrichment-2024/qivip!21
2025-03-22 11:59:25 +00:00
nik8
809d857e94 Swiper_package 2025-03-22 12:58:59 +01:00
Frank Poetzsch-Heffter
98142c91f3 Pfad für tailwind-Dateien angepasst 2025-03-22 11:36:25 +00:00
nik8
93b2675b9e Zwischenstand 2025-03-22 12:21:39 +01:00
nik8
157daae740 struktur_library 2025-03-20 21:34:17 +01:00
nik8
f6459965e0 Suchfunktion_update 2025-03-18 17:22:42 +01:00
nik8
a3331543f2 Filter_library 2025-03-17 18:28:12 +01:00
nik8
9d71f2bbf3 Merge branch '50-in-der-handy-ansicht-ist-qivip-zu-dicht-an-dem-text-darunter-home-seite' into 'master'
Resolve "In der Handy Ansicht ist "qivip" zu dicht an dem Text darunter (Home Seite)"

Closes #50

See merge request enrichment-2024/qivip!20
2025-03-17 12:28:07 +00:00
nik8
8cceecf1bd Abstand_qivip_slogan 2025-03-17 13:27:18 +01:00
nik8
c330c4bc70 Merge branch '49-beim-speichern-eines-neuen-quiz-richtig-weiterfuhrung-nicht-das-man-zweilmal-auf-speichern' into 'master'
Resolve "Beim Speichern eines neuen Quiz richtig weiterführung, nicht das man zweilmal auf speichern drücken muss..."

Closes #49

See merge request enrichment-2024/qivip!19
2025-03-17 12:14:18 +00:00
nik8
26bf2a2c55 unnötige_Zeile_entfernt 2025-03-17 13:13:47 +01:00
nik8
86958c6d16 Weiterleitung_nach_Quiz_erstellen_gefixt 2025-03-17 13:12:04 +01:00
nik8
83a3d1584a Merge branch '47-weiterleitung-nach-registrierung-fixen' into 'master'
Resolve "Weiterleitung nach Registrierung fixen"

Closes #47

See merge request enrichment-2024/qivip!18
2025-03-17 11:41:31 +00:00
nik8
a07c66ad1a Weiterleitung_Registrierung_gefixt_autom_Login_nach_Regestrierung 2025-03-17 12:39:20 +01:00
nik8
956e69b56f Moderieren_link_zu_library 2025-03-16 18:17:16 +01:00
juhnsa
db71ea2a5b Farben aendern 2025-03-16 17:54:31 +01:00
nik8
526e486fd3 Text in Box 2025-03-16 17:53:48 +01:00
juhnsa
b149cbcfee Components verfuegbar machen 2025-03-16 17:49:49 +01:00
nik8
12c5cec7a1 Merge branch '51-im-impressum-einen-kasten-machen-schoner' into 'master'
Resolve "Im Impressum einen Kasten machen (schöner)"

Closes #51

See merge request enrichment-2024/qivip!17
2025-03-16 16:41:59 +00:00
juhnsa
d2375e136a Templates fuer Fragen 2025-03-16 17:08:38 +01:00
166 changed files with 13193 additions and 528 deletions

6
.gitignore vendored
View File

@@ -1,6 +1,10 @@
__pycache__
.venv
.env
db.sqlite3
tailwindcss.exe
t-style.css
dump.rdb
dump.rdb
media
node_modules
staticfiles

View File

@@ -7,6 +7,7 @@
3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts
@@ -40,23 +41,69 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
```
7. Die Datenbank migrieren:
```sh
python3 core/manage.py migrate
python django/manage.py migrate
```
## Starten des Servers
### Entwicklungsserver starten
Um den Server zu starten, verwende einen der folgenden Befehle:
Für die Entwicklung mit WebSocket-Unterstützung verwenden wir Daphne:
- Standardport (8000):
```sh
python3 core/manage.py runserver
```
- Bestimmten Port (z. B. 9000):
```sh
python3 core/manage.py runserver 9000
```
1. Aktiviere die virtuelle Umgebung:
```sh
source .venv/bin/activate # Linux
.venv\Scripts\activate # Windows
```
2. Starte den Daphne-Server:
```sh
# Im Verzeichnis 'django'
daphne -b 127.0.0.1 -p 8000 core.asgi:application
```
3. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Alternativ kannst du den Django-Entwicklungsserver verwenden, wenn du keine WebSocket-Funktionalität benötigst:
```sh
python3 core/manage.py runserver
```
### Produktionsserver starten
Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP als auch WebSocket-Verbindungen unterstützt:
1. Aktiviere die virtuelle Umgebung wie oben beschrieben
2. Sammle die statischen Dateien:
```sh
python3 core/manage.py collectstatic
```
3. Starte den Daphne-Server:
```sh
daphne -b 0.0.0.0 -p 8000 core.asgi:application
```
- `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces
- `-p 8000`: Port (anpassbar)
4. Cronjob für Bereinigung der Spiele in der Datenbank
- Zum bearbeiten der Cronjobs:
```sh
crontab -e
```
- Dann `0 0 * * * /path/to/venv/bin/python /path/to/your_project/manage.py cleanup_games` einfügen, damit um Mitternacht alle Spiele gelöscht werden
5. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
@@ -100,10 +147,10 @@ npx @tailwindcss/cli -i <INPUT-DATEI> -o <OUTPUT-DATEI> --watch --minify
```sh
# unter Linux mit Tailwind Binärdatei:
tailwindcss -i core/static/homepage/t-input.css -o core/static/homepage/t-style.css --watch --minify
npx tailwindcss -i static/css/t-input.css -o static/css/t-style.css --watch --minify
# unter Windows mit NPM:
npx @tailwindcss/cli -i .\core\static\homepage\t-input.css -o .\core\static\homepage\t-style.css --watch --minify
# unter Windows mit NPM (in bash mit '/' statt '\'):
npx @tailwindcss/cli -i ./static/css/t-input.css -o ./static/css/t-style.css --watch --minify
```
## Issue - Merge Request - Merge
@@ -112,6 +159,23 @@ Hier der vereinbarte Arbeitsablauf:
1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird.
2. Merge Request anlegen - dadurch wird der Bransch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
## Konfiguration
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen |
| --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
## Lizenzen für node_modules
1. Installation: npm install license-checker --save-dev
npx license-checker --markdown > THIRD_PARTY_LICENSES.md
## für Python Packete:
pip-licenses --format=markdown > THIRD_PARTY_LICENSES_PYTHON.md

View File

@@ -0,0 +1,45 @@
| Name | Version | License |
|---------------------------|--------------|---------------------------------------------------------|
| Automat | 25.4.16 | MIT License |
| Django | 5.1.13 | BSD License |
| Twisted | 25.5.0 | MIT License |
| asgiref | 3.10.0 | BSD License |
| attrs | 25.4.0 | UNKNOWN |
| autobahn | 25.10.1 | UNKNOWN |
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
| cffi | 2.0.0 | UNKNOWN |
| channels | 4.2.2 | BSD License |
| channels_redis | 4.3.0 | BSD |
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
| charset-normalizer | 3.4.4 | MIT |
| colorama | 0.4.6 | BSD License |
| constantly | 23.10.4 | MIT License |
| cryptography | 46.0.3 | UNKNOWN |
| daphne | 4.1.2 | BSD License |
| django-autocomplete-light | 3.12.1 | MIT License |
| django-cleanup | 9.0.0 | MIT License |
| geographiclib | 2.1 | MIT |
| geopy | 2.4.1 | MIT License |
| hyperlink | 21.0.0 | MIT License |
| idna | 3.11 | UNKNOWN |
| incremental | 24.7.2 | MIT License |
| msgpack | 1.1.2 | UNKNOWN |
| pillow | 11.1.0 | CMU License (MIT-CMU) |
| pyOpenSSL | 25.3.0 | Apache Software License |
| pyasn1 | 0.6.1 | BSD License |
| pyasn1_modules | 0.4.2 | BSD License |
| pycparser | 2.23 | BSD License |
| pypng | 0.20220715.0 | MIT License |
| python-dotenv | 1.1.1 | BSD License |
| qrcode | 7.4.2 | BSD License |
| redis | 6.4.0 | MIT License |
| reportlab | 4.2.5 | BSD License |
| requests | 2.32.5 | Apache Software License |
| service-identity | 24.2.0 | MIT License |
| sqlparse | 0.5.3 | BSD License |
| txaio | 25.9.2 | UNKNOWN |
| typing_extensions | 4.15.0 | UNKNOWN |
| tzdata | 2025.2 | Apache Software License |
| urllib3 | 2.5.0 | UNKNOWN |
| whitenoise | 6.6.0 | MIT License |
| zope.interface | 8.0.1 | Zope Public License |

View File

@@ -0,0 +1,7 @@
Name und Anschrift des Verantwortlichen
//NAME HIER EINSETZEN//
//ADRESSE HIER EINSETZEN//
//E-MAIL HIER EINSETZEN//

Binary file not shown.

View File

@@ -6,11 +6,15 @@ from django import forms
class RegisterForm(UserCreationForm):
class Meta:
model=User
fields = ['username','password1','password2']
fields = ['username','password1','password2', "email"]
username = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
)
email = forms.EmailField(
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
)
password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'})
)
@@ -19,4 +23,3 @@ class RegisterForm(UserCreationForm):
)

View File

@@ -1,7 +1,8 @@
from django.urls import path # type: ignore
from django.urls import path, reverse_lazy # type: ignore
from . import views
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import LoginView
from django.contrib.auth import views as auth_views
app_name = 'accounts' # Namensraum zum verhindern von Konflikten zwischen Apps
#
@@ -13,4 +14,22 @@ urlpatterns = [
path('logout/', LogoutView.as_view(next_page='accounts:login'), name='logout'),
path('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'),
path('register/', views.register, name='register'),
]
path('password_reset/', auth_views.PasswordResetView.as_view(success_url=reverse_lazy('accounts:password_reset_done')), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('password_change/', views.password_changed, name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('delete_account/', views.deleteAccount, name="delete_account"),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html',
success_url=reverse_lazy('accounts:password_reset_complete')
),
name='password_reset_confirm'
),
]

View File

@@ -1,19 +1,71 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from .forms import RegisterForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.contrib import messages
from django.contrib.auth import logout
from library.models import QivipQuiz, QivipQuestion
from django.db.models import Count
# Create your views here.
@login_required
def home(request):
return render(request, 'accounts/home.html')
own_critisized_quizzes = QivipQuiz.objects.annotate(
criticism_count=Count('criticism_quiz')
).filter(criticism_count__gt=0).filter(user_id=request.user)
my_quizzes_number=QivipQuiz.objects.filter(user_id=request.user).count()
my_questions_number = QivipQuestion.objects.filter(quiz_id__user_id=request.user).count()
context = {
'own_critisized_quizzes':own_critisized_quizzes,
'my_quizzes_number': my_quizzes_number,
'my_questions_number': my_questions_number,
}
return render(request, 'accounts/home.html', context=context)
def register(response):
if response.method == "POST":
form = RegisterForm(response.POST)
if form.is_valid():
form.save()
return redirect("home")
user=form.save()
login(response, user) #automatischer Login nach Registrierung
return redirect("accounts:home")
else:
form = RegisterForm()
return render(response, "accounts/register.html", {"form":form})
return render(response, "accounts/register.html", {"form":form})
from django.contrib.auth import views as auth_views
@login_required
def password_changed(request):
if request.method == 'POST':
form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
user = form.save()
#update_session_auth_hash(request, user) #Hier wäre der User nach Passwortänderung noch angemeldet!
logout(request)
messages.success(request, "Das Passwort wurde erfolgreich geändert!")
return redirect('accounts:home')
else:
form = PasswordChangeForm(user=request.user)
return render(request, 'registration/password_change_form.html', {'form': form})
@login_required
def deleteAccount(request):
if request.method == 'POST':
user=request.user
logout(request)
user.delete()
messages.success(request, "Der Account wurde erfolgreich gelöscht!")
return redirect('homepage:home')
return render(request, 'registration/delete_account_confirm.html')

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class ComponentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'components'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,14 +0,0 @@
from django.urls import path # type: ignore
from . import views
app_name = 'components' # Namensraum zum verhindern von Konflikten zwischen Apps
#
# Wichtig: Damit Links funktionieren müssen diese so eingebunden werden: {% url 'accounts:login' %} statt {% url 'login' %}
#
urlpatterns = [
path('<int:pk>', views.play, name='play'),
path('ranking/<int:pk>', views.show_rank, name='ranking')
]

View File

@@ -1,9 +0,0 @@
from django.shortcuts import render
# Create your views here.
def play(request,pk): #
return render(request, 'components/answer.html', {"pk": pk}) #
def show_rank(request,pk): #
return render(request, 'components/ranking.html', {"pk": pk}) #

View File

@@ -0,0 +1,3 @@
{
"ENABLE_RATING_SYSTEM": true
}

View File

@@ -8,9 +8,21 @@ https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django.setup()
application = get_asgi_application()
from play.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})

View File

@@ -10,30 +10,42 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import json, os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR ENTWICKLUNG
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# Support env variables from .env file if defined
env_path = load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(env_path)
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
#SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
#DEBUG = False
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
# Konfigurationsdatei für Projekt
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
QIVIP_CONFIG = json.load(config_file)
# Application definition
INSTALLED_APPS = [
'library.apps.LibraryConfig',
'homepage.apps.HomepageConfig',
'components.apps.ComponentsConfig',
'accounts.apps.AccountsConfig',
'play',
'django.contrib.admin',
@@ -42,10 +54,16 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'django_cleanup',
'django.contrib.humanize',
'dal',
'dal_select2',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -73,6 +91,17 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6380)],
},
},
}
# Database
@@ -85,6 +114,15 @@ DATABASES = {
}
}
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -121,11 +159,32 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/account/login/'
LOGIN_URL = '/account/login/'
import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien
# Email Settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'postoffice.katharineum.de'
EMAIL_PORT = 587 # 25
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'qivip-mail@katharineum.de'
EMAIL_HOST_PASSWORD = '<password>'
EMAIL_USE_SSL = False
DEFAULT_FROM_EMAIL = 'qivip-mail@katharineum.de'

View File

@@ -14,6 +14,8 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.conf import settings
from django.contrib import admin # type: ignore
from django.urls import path, include
@@ -24,3 +26,6 @@ urlpatterns = [
path('play/', include('play.urls')),
path('library/', include('library.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,3 +1,12 @@
from django.contrib import admin
from .models import QivipFaq, QivipDescription
# Register your models here.
class QivipFaqAdmin(admin.ModelAdmin):
list_display = ('question','answer',)
class QivipDescriptionAdmin(admin.ModelAdmin):
list_display = ('title','description',)
# Registrierung der Modelle im Admin
admin.site.register(QivipFaq, QivipFaqAdmin)
admin.site.register(QivipDescription, QivipDescriptionAdmin)

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-06-01 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='QivipFAQs',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=200)),
('answer', models.CharField(max_length=1000)),
],
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-06-01 17:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('homepage', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='QivipFAQs',
new_name='QivipFaq',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.7 on 2025-06-01 17:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0002_rename_qivipfaqs_qivipfaq'),
]
operations = [
migrations.AlterField(
model_name='qivipfaq',
name='answer',
field=models.TextField(max_length=1000),
),
migrations.AlterField(
model_name='qivipfaq',
name='question',
field=models.TextField(max_length=200),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.1.7 on 2025-11-27 17:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0003_alter_qivipfaq_answer_alter_qivipfaq_question'),
]
operations = [
migrations.CreateModel(
name='QivipInfos',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField(max_length=200)),
('description', models.TextField(max_length=1000)),
],
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-11-27 17:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('homepage', '0004_qivipinfos'),
]
operations = [
migrations.RenameModel(
old_name='QivipInfos',
new_name='QivipInfo',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-11-27 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0005_rename_qivipinfos_qivipinfo'),
]
operations = [
migrations.CreateModel(
name='QivipDescription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField(help_text='Funktion von qivip', max_length=200)),
('description', models.TextField(help_text='Beschreibung derFunktion von qivip', max_length=1000)),
],
),
migrations.DeleteModel(
name='QivipInfo',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 17:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0006_qivipdescription_delete_qivipinfo'),
]
operations = [
migrations.AlterField(
model_name='qivipdescription',
name='description',
field=models.TextField(help_text='Beschreibung der Funktion von qivip', max_length=1000),
),
]

View File

@@ -1,3 +1,10 @@
from django.db import models
# Create your models here.
class QivipFaq(models.Model):
question = models.TextField(max_length=200)
answer = models.TextField(max_length=1000)
class QivipDescription(models.Model): # Was kann man mit qivip machen ...
title = models.TextField(max_length=200, help_text="Funktion von qivip")
description= models.TextField(max_length=1000, help_text="Beschreibung der Funktion von qivip")

View File

@@ -1,9 +1,13 @@
from django.shortcuts import render, redirect
from .models import QivipFaq, QivipDescription
def home(request):
return render(request, 'homepage/home.html')
faqs=QivipFaq.objects.all()
descriptions=QivipDescription.objects.all()
context={"faqs": faqs, "descriptions": descriptions}
return render(request, 'homepage/home.html', context)
def impress(request):
return render(request, 'homepage/impress.html')
def privacy(request):
return render(request, 'homepage/privacy.html')
return render(request, 'homepage/privacy.html')

View File

@@ -1,15 +1,25 @@
from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag
from .models import QivipQuiz, QivipQuestion,QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating, QuizCriticism
# Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen
autocomplete_fields = ("reference_quizzes",)
class QuizCriticismAdmin(admin.ModelAdmin):
list_display = ('quiz','question_criticism', 'user','criticism') # Welche Felder sollen in der Übersicht angezeigt werden
search_fields = ('quiz','question_criticism', 'user','criticism') # Suchfelder
list_filter=('quiz','question_criticism', 'user','criticism')
# Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('quiz_id', 'data', 'creation_date', 'update_date')
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')
search_fields = ('data',)
list_filter = ('quiz_id',)
@@ -19,13 +29,47 @@ class QuizCategoryAdmin(admin.ModelAdmin):
search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen
# Für das Tag Modell
class TagAdmin(admin.ModelAdmin):
list_display = ('name',) # Zeige nur den Namen des Tags
search_fields = ('name',)
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
class QuizImageAdmin(admin.ModelAdmin):
list_display = ('image',)
class QuestionImageAdmin(admin.ModelAdmin):
list_display = ('image',)
from django import forms
from .models import QuizCategory
class QuizCategoryForm(forms.ModelForm):
color = forms.CharField(
widget=forms.TextInput(attrs={'type': 'color'})
)
class Meta:
model = QuizCategory
fields = '__all__'
from django.utils.html import format_html
@admin.register(QuizCategory)
class QuizCategoryAdmin(admin.ModelAdmin):
form = QuizCategoryForm
list_display = ('name', 'color_preview', 'color')
def color_preview(self, obj):
return format_html(
'<div style="width: 24px; height: 24px; background-color: {}; border: 2px solid #000;"></div>',
obj.color
)
color_preview.short_description = "Farbe"
# Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(QuizCriticism, QuizCriticismAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -1,12 +1,61 @@
from django import forms
from .models import QivipQuiz, QivipQuestion
from .models import QivipQuiz, QivipQuestion, QuizCategory
from dal import autocomplete
class QuizForm(forms.ModelForm):
class Meta:
model = QivipQuiz
fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"]
fields = ['name', 'description', 'status', 'category','difficulty','reference_quizzes','credits']
widgets = {
'reference_quizzes': autocomplete.ModelSelect2Multiple(
url='library:quiz-autocomplete',
forward=['pk']
)
}
def __init__(self, *args, **kwargs):
super(QuizForm, self).__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({
})
self.fields["reference_quizzes"].queryset = QivipQuiz.objects.filter(
status="öffentlich").exclude(pk=self.instance.pk)
self.fields["reference_quizzes"].label_from_instance = lambda obj: f"{obj.name} (ID: {obj.pk}) (User: {obj.user_id})"
class QuestionForm(forms.ModelForm):
class Meta:
model = QivipQuestion
fields = ['quiz_id', 'data']
fields = ['quiz_id', 'data','time_per_question']
from django import forms
class QuizFilterForm(forms.Form):
search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'list': 'quiz-names',
}))
min_amount_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
max_amount_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
categories = forms.ModelChoiceField(
queryset=QuizCategory.objects.filter().distinct(),
required=False,
label="Kategorie",
widget=forms.Select(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full dark:bg-[#2a2f3a] dark:text-white'
}))

View File

@@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from play.models import QuizGame
from django.utils import timezone
class Command(BaseCommand):
help = 'Bereinigt beendete Spiele'
def handle(self, *args, **kwargs):
expired_games = QuizGame.objects.filter(
updated_at__lt=timezone.now() - timezone.timedelta(minutes=5))
deleted_count = expired_games.count()
expired_games.delete()
self.stdout.write(f'{deleted_count} alte Spiele gelöscht.')

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-21 17:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0022_alter_qivipquiz_description_alter_qivipquiz_name'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='quiz_images/'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-03-22 10:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0023_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-03 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0024_alter_qivipquiz_image'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, default='Das Quiz wurde modifiziert von:', editable=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-04 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0025_qivipquiz_modified_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, editable=False),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.1.7 on 2025-04-04 13:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0026_alter_qivipquiz_modified_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.1.7 on 2025-04-04 13:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0027_qivipquiz_creator'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-04 16:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0028_alter_qivipquiz_creator'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.7 on 2025-04-05 09:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0029_qivipquiz_credits'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='creator',
),
migrations.RemoveField(
model_name='qivipquiz',
name='modified_description',
),
migrations.AddField(
model_name='qivipquiz',
name='base_quiz',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_quizzes', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-05 17:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0030_remove_qivipquiz_creator_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='quiz_id',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.1.7 on 2025-04-05 19:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0031_alter_qivipquestion_quiz_id'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='average_rating',
field=models.FloatField(default=3.0),
),
migrations.AddField(
model_name='qivipquiz',
name='rating_count',
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name='QuizRating',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('participant_id', models.CharField(max_length=200)),
('rating', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')])),
('created_at', models.DateTimeField(auto_now_add=True)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='library.qivipquiz')),
],
options={
'unique_together': {('quiz', 'participant_id')},
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180), ('nicht gesetzt', 'nicht gesetzt')], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-04-10 13:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='tags',
),
migrations.DeleteModel(
name='Tag',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-10 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_remove_qivipquiz_tags_delete_tag'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='status',
field=models.CharField(choices=[('öffentlich', 'Öffentlich'), ('privat', 'Privat')], default='öffentlich', max_length=10),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=3),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquiz_status'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.1.7 on 2025-04-10 16:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0035_qivipquiz_favorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
migrations.CreateModel(
name='QuizFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('favorite', models.BooleanField(default=False)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.qivipquiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'quiz')},
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-10 16:05
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0036_remove_qivipquiz_favorite_quizfavorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='QuizFavorite',
new_name='QivipQuizFavorite',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-10 17:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0037_rename_quizfavorite_qivipquizfavorite'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='quiz',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-11 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0038_alter_qivipquizfavorite_quiz'),
]
operations = [
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-11 13:15
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0039_qivipquizfavorite_favorite_date'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description], verbose_name='Beschreibung'),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.7 on 2025-04-11 13:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0040_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='library.quizcategory', verbose_name='Kategorie'),
),
migrations.AlterField(
model_name='qivipquiz',
name='difficulty',
field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13, verbose_name='Schwierigkeit'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=75, verbose_name='Name des Quizzes'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-11 13:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.qivipquizfavorite'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-04-11 15:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0042_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-11 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0043_remove_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquizfavorite',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquiz',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-11 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0044_remove_qivipquizfavorite_favorite_date_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-11 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0045_remove_qivipquiz_favorite_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0035_alter_qivipquestion_time_per_question'),
('library', '0046_alter_qivipquizfavorite_favorite_date'),
]
operations = [
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0047_merge_20250418_1334'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, height_field=900, max_length=200, null=True, upload_to='question_images/', verbose_name='Bild', width_field=1000),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-18 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0048_qivipquestion_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.1.7 on 2025-04-22 16:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0049_alter_qivipquestion_image'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.QuizImage'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-04-23 12:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0050_questionimage_quizimages_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-04-23 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0051_alter_qivipquestion_image_alter_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.DeleteModel(
name='QuestionImage',
),
migrations.DeleteModel(
name='QuizImage',
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.1.7 on 2025-04-23 13:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0052_alter_qivipquestion_image_alter_qivipquiz_image_and_more'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-06-13 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0053_questionimage_quizimage_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-06-26 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0054_qivipquestion_credits'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='published_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-06-27 12:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0055_qivipquiz_published_at'),
]
operations = [
migrations.AlterUniqueTogether(
name='quizrating',
unique_together=set(),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-02 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0056_alter_quizrating_unique_together'),
]
operations = [
migrations.AddField(
model_name='quizcategory',
name='color',
field=models.CharField(default='#000000', help_text='Farbe als Hex-Code, z.B. #ff0000', max_length=7),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.7 on 2025-11-14 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0057_quizcategory_color'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0058_qivipquiz_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='single_player_or_multiple',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0059_remove_qivipquiz_single_player_or_multiple'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, related_name='similar_to', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0060_qivipquiz_similar_quizzes'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 19:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0061_alter_qivipquiz_similar_quizzes'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, to='library.qivipquiz', verbose_name='Verweis auf andere Quizze'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-12-03 11:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0062_alter_qivipquiz_similar_quizzes'),
]
operations = [
migrations.RenameField(
model_name='qivipquiz',
old_name='similar_quizzes',
new_name='reference_quizzes',
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-12-30 13:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0063_rename_similar_quizzes_qivipquiz_reference_quizzes'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='QuizCriticism',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('criticism', models.CharField(max_length=256, verbose_name='Kritik')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criticism_question', to='library.qivipquestion')),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criticism_quiz', to='library.qivipquiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('quiz', 'user', 'question')},
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-12-30 13:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0064_quizcriticism'),
]
operations = [
migrations.AddField(
model_name='quizcriticism',
name='creation_date',
field=models.DateTimeField(auto_now_add=True, default='2025-12-30 12:00:00'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-12-30 13:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0065_quizcriticism_creation_date'),
]
operations = [
migrations.RemoveField(
model_name='quizcriticism',
name='creation_date',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-12-30 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0066_remove_quizcriticism_creation_date'),
]
operations = [
migrations.AddField(
model_name='quizcriticism',
name='creation_date',
field=models.DateTimeField(auto_now_add=True, default='2025-12-30 12:00:00'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-12-31 11:56
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0067_quizcriticism_creation_date'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameField(
model_name='quizcriticism',
old_name='question',
new_name='question_criticism',
),
migrations.AlterUniqueTogether(
name='quizcriticism',
unique_together={('quiz', 'user', 'question_criticism', 'criticism')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-12-31 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0068_rename_question_quizcriticism_question_criticism_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizcriticism',
name='question_criticism',
field=models.CharField(max_length=1024),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-12-31 14:03
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0069_alter_quizcriticism_question_criticism'),
]
operations = [
migrations.AddField(
model_name='quizcriticism',
name='question',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='library.qivipquestion'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-12-31 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0070_quizcriticism_question'),
]
operations = [
migrations.AddField(
model_name='quizcriticism',
name='id_question',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2026-01-17 11:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0071_quizcriticism_id_question'),
]
operations = [
migrations.AlterModelOptions(
name='qivipquestion',
options={'ordering': ['order']},
),
migrations.AddField(
model_name='qivipquestion',
name='order',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -3,11 +3,20 @@ from django.contrib.auth.models import User
from django.utils.text import slugify
import uuid
from django.core.exceptions import ValidationError
from django.utils import timezone
class QuizImage(models.Model):
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
class QuestionImage(models.Model):
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
class QivipQuiz(models.Model):
STATUS_VALUES = {
"öffentlich": "Öffentlich",
"versteckt": "Versteckt",
#"versteckt": "Versteckt",
"privat": "Privat",
}
DIFFICULTY_VALUES = {
@@ -19,48 +28,161 @@ class QivipQuiz(models.Model):
"nicht gesetzt":"nicht gesetzt",
}
def validate_description(value):
if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ForeignKey(
QuizImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="quizzes"
)
reference_quizzes = models.ManyToManyField(
'self',
blank=True,
symmetrical=False,
verbose_name="Verweis auf andere Quizze"
)
quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus= models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus_players= models.IntegerField(null=True, blank=True, default=0)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
base_quiz = models.ForeignKey('self', on_delete=models.SET_NULL,blank=True, null=True, related_name='child_quizzes')
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich")
category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
name = models.CharField(max_length=75)
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...")
difficulty= models.CharField(max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
category = models.ForeignKey('QuizCategory',verbose_name="Kategorie", related_name='quiz', on_delete=models.CASCADE)
name = models.CharField(max_length=75,verbose_name="Name des Quizzes")
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...", verbose_name="Beschreibung")
difficulty= models.CharField(verbose_name="Schwierigkeit", max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
credits=models.TextField(blank=True, editable=True)
average_rating = models.FloatField(default=3.0)
rating_count = models.IntegerField(default=0)
published_at = models.DateTimeField(null=True, blank=True) # manuell gesetzt, wenn veröffentlicht
def is_published(self):
return self.published_at is not None and self.published_at <= timezone.now()
def __str__(self):
return self.name
class QivipQuizFavorite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # Verknüpfung zu User
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='favorites') # Verknüpfung zu Quiz
favorite = models.BooleanField(default=False) # Favoritenstatus
favorite_date = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
# Create your models here.
class QivipQuestion(models.Model):
TIME_VALUES = {
"10": 10,
"15": 15,
"30": 30,
"60": 60,
"90": 90,
"120": 120,
"180": 180,
}
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='question')
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions')
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
data = models.TextField()
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ForeignKey(
QuestionImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="questions"
)
credits=models.TextField(blank=True, editable=True)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order'] # Fragen nach Reihenfolge sortieren
def __str__(self):
return self.data[:50]
class Tag(models.Model):
name = models.CharField(max_length=100, unique=True)
class QuizCriticism(models.Model):
quiz= models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='criticism_quiz')
user = models.ForeignKey(User, on_delete=models.CASCADE)
question_criticism=models.CharField(max_length=1024)
criticism=models.CharField(max_length=256, verbose_name="Kritik")
creation_date = models.DateTimeField(auto_now_add=True)
question = models.ForeignKey(QivipQuestion, on_delete=models.CASCADE,null=True, blank=True)
id_question=models.IntegerField(null=True, blank=True)
class Meta:
unique_together = ('quiz', 'user', 'question_criticism','criticism')
def __str__(self):
return self.name
return f"{self.question_criticism}"
class QuizRating(models.Model):
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='ratings')
participant_id = models.CharField(max_length=200)
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
created_at = models.DateTimeField(auto_now_add=True)
#class Meta:
#unique_together = ('quiz', 'participant_id')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Update quiz average rating (immer, auch bei Updates)
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()
"""
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Update quiz average rating
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()"""
class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True)
slug = models.SlugField(unique=True, blank=True)
color= models.CharField(
max_length=7,
default="#000000",
help_text="Farbe als Hex-Code, z.B. #ff0000"
)
def save(self, *args, **kwargs):
if not self.slug:

View File

@@ -1,10 +1,15 @@
from django.urls import path
from . import views
from django.conf import settings
from . import views
from django.conf.urls.static import static
from .views import QuizAutocomplete
app_name = 'library'
urlpatterns = [
path('', views.overview_quiz, name='overview_quiz'),
path('my/', views.overview_quiz_my, name='overview_quiz_my'),
path('favorites/', views.overview_quiz_favorites, name='overview_quiz_favorites'),
path('new/', views.new_quiz, name='new_quiz'),
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
@@ -12,4 +17,18 @@ urlpatterns = [
path('question/new/', views.new_question, name='new_question'),
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
path('critique/delete/<int:critique_id>/<int:quiz_id>', views.delete_critique, name='delete_critique'),
path('quiz/reorder/', views.reorder_questions, name='reorder_questions'),
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
path(
"autocomplete/quiz",
QuizAutocomplete.as_view(),
name="quiz-autocomplete"
),
]

View File

@@ -1,35 +1,414 @@
from django.utils import timezone
import uuid
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.contrib import messages
from .models import QivipQuiz, QivipQuestion
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCriticism, QuizImage
from .forms import QuizForm, QuestionForm
import json
from django.core.paginator import Paginator
from .models import QivipQuiz
from .forms import QuizFilterForm
from django.db.models import Count
from django.contrib.auth.models import User
from urllib.parse import urlparse, urlunparse
# Übersicht aller Quizze
def overview_quiz(request):
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib import colors
from django.http import HttpResponse
import json
from datetime import datetime
from reportlab.platypus import Image
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.pagesizes import A4
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
import json
from io import BytesIO
import requests
from django.core.files.storage import default_storage
from PIL import Image as PILImage, ImageOps
from django.conf import settings
import os
from django.utils.html import escape
from .models import QivipQuiz
from django.db.models import Value, CharField
from django.db.models.functions import Concat
# views.py
from dal import autocomplete
from .models import QivipQuiz
class QuizAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = QivipQuiz.objects.filter(status="öffentlich")
pk = self.forwarded.get('pk', None)
if pk:
qs = qs.exclude(pk=pk)
if self.q:
qs = qs.annotate(
search_text=Concat(
'name',
Value(' (ID: '), 'pk', Value(') (User: '), 'user_id', Value(')'),
output_field=CharField()
)
).filter(search_text__icontains=self.q)
return qs
def get_result_label(self, result):
# z.B. Name + ID + User
return f"{result.name} (ID: {result.pk}) (User: {result.user_id})"
def quiz_names_json(request):
form = QuizFilterForm(request.GET)
if form.is_valid():
search = form.cleaned_data.get('search')
names = QivipQuiz.objects.all().annotate(max_amount_questions=Count('questions')).filter(max_amount_questions__gte=1, status='öffentlich', name__icontains=search)
if request.user.is_authenticated:
names = names | QivipQuiz.objects.filter(
user_id=request.user.id,
name__icontains=search
)
names=list(names.values_list('name', flat=True).distinct())
return JsonResponse(names, safe=False)
def reorder_questions(request):
if request.method == "POST":
data = json.loads(request.body)
for item in data['order']:
QivipQuestion.objects.filter(id=item['id']).update(order=item['position'])
return JsonResponse({'status': 'ok'})
return JsonResponse({'status': 'error'}, status=400)
def apply_quiz_filters(queryset, form):
if not form.is_valid():
return queryset
queryset = queryset.annotate(question_count=Count('questions'))
search = form.cleaned_data.get('search')
username = form.cleaned_data.get('user')
min_questions = form.cleaned_data.get('min_amount_questions')
max_questions = form.cleaned_data.get('max_amount_questions')
category = form.cleaned_data.get('categories')
filters = {}
if search:
filters['name__icontains'] = search
if min_questions is not None:
filters['question_count__gte'] = min_questions
if max_questions is not None:
filters['question_count__lte'] = max_questions
if category is not None:
filters['category'] = category
if username:
try:
user = User.objects.get(username=username)
filters['user_id'] = user.id
except User.DoesNotExist:
return queryset.none()
return queryset.filter(**filters)
def critisize_quiz(request, pk):
quiz= get_object_or_404(QivipQuiz, pk=pk)
if request.method == "POST":
question_id = request.POST.get("question_id") or "whole_quiz"
if question_id!="whole_quiz":
question = quiz.questions.get(id=question_id)
data = json.loads(question.data)
question_text = data["question"]
else:
question_text="Quiz(formular) kritisiert"
question = None
question_id = None
critic_type = request.POST.get("critic")
CRITICISM_MAP = {
"content_error": "inhaltlicher Fehler",
"spelling_error": "Rechtschreibfehler oder Grammatikfehler",
"logic_error": "umständliche oder unsinnige Formulierung",
"inappropriate_error": "unangemessen",
"not_completed_error": "unvollständig",
"source_error": "Quellen unvollständig",
"quiz_error": "Quiz als \"öffentliches Quiz\" ungeeignet"}
exists = QuizCriticism.objects.filter(
id_question=question_id,
question=question,
quiz=quiz,
user=request.user,
question_criticism=question_text,
criticism=CRITICISM_MAP[critic_type],
).exists()
if exists:
messages.error(request, "Diese Kritik wurde bereits abgegeben.") # nur Kritik abgeben, wenn noch nicht erstellt
else:
QuizCriticism.objects.create(
question=question,
id_question=question_id,
quiz=quiz,
user=request.user,
question_criticism=question_text,
criticism=CRITICISM_MAP[critic_type],
)
def delete_critique(request,critique_id,quiz_id):
critique = get_object_or_404(QuizCriticism, id=critique_id)
critique.delete()
return redirect('library:detail_quiz', pk=quiz_id)
def get_critisized_quizzes():
critisized_quizzes = QivipQuiz.objects.annotate(
criticism_count=Count('criticism_quiz')
).filter(criticism_count__gt=0)
return critisized_quizzes
def overview_quiz(request, connect="all"):
critisized_quizzes = get_critisized_quizzes()
form = QuizFilterForm(request.GET)
user = request.user if request.user.is_authenticated else None
# Meine Quizzeze mit mind. 1 Frage
filter_my_quizzes = QivipQuiz.objects.none()
if user:
filter_my_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
user_id=user.id,
max_amount_questions__gte=0
)
# Öffentliche Alle mit mind. 1 Frage
filter_all_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
status='öffentlich',
max_amount_questions__gte=1
)
if user:
filter_all_quizzes = filter_all_quizzes.exclude(user_id=user.id)
# Favoriten mit mind. 1 Frage
favorite_quiz_ids = []
favorite_quizzes = QivipQuiz.objects.none()
if user:
favorite_quiz_ids = QivipQuizFavorite.objects.filter(user=user, favorite=True).values_list('quiz', flat=True)
favorite_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
id__in=favorite_quiz_ids,
max_amount_questions__gte=1
)
# Suche anwenden, falls vorhanden
filter_my_quizzes = apply_quiz_filters(filter_my_quizzes, form)
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form)
favorite_quizzes = apply_quiz_filters(favorite_quizzes, form)
if request.user.is_authenticated:
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form) | apply_quiz_filters(filter_my_quizzes, form)
# Treffer zählen
total_my = filter_my_quizzes.count()
total_all = filter_all_quizzes.count()
total_favorite = favorite_quizzes.count()
total_all_found=total_my + total_all + total_favorite
# Paginierung
paginator_my = Paginator(filter_my_quizzes.order_by('-creation_date'), 8)
paginator_all = Paginator(filter_all_quizzes.order_by('-published_at'), 8)
paginator_fav = Paginator(favorite_quizzes.order_by('-published_at'), 8)
my_page = request.GET.get('my_page', 1)
all_page = request.GET.get('all_page', 1)
fav_page = request.GET.get('favorite_page', 1)
try:
quizzes = QivipQuiz.objects.filter(user_id=request.user)
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').exclude(user_id=request.user).filter(question__isnull=False).distinct()
return render(request, 'library/overview_quiz.html', {'quizzes': quizzes, 'all_quizzes': all_quizzes})
my_quizzes = paginator_my.page(my_page)
except:
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct()
return render(request, 'library/overview_quiz.html', {'quizzes': None, 'all_quizzes': all_quizzes})
my_quizzes = paginator_my.page(1)
try:
all_quizzes = paginator_all.page(all_page)
except:
all_quizzes = paginator_all.page(1)
try:
favorite_quizzes = paginator_fav.page(fav_page)
except:
favorite_quizzes = paginator_fav.page(1)
context = {
'critisized_quizzes':critisized_quizzes,
'form': form,
'my_quizzes': my_quizzes,
'all_quizzes': all_quizzes,
'favorite_quizzes': favorite_quizzes,
'total_my': total_my,
'total_all': total_all,
'total_favorite': total_favorite,
'total_all_found':total_all_found,
'show_search': True,
'favorite_quiz_ids': list(favorite_quiz_ids),
'url_my': '/library/my/',
'url_all': '/library/',
'url_favorite': '/library/favorites/',
}
query_params = request.GET.copy()
querydict_my = query_params.copy()
querydict_all = query_params.copy()
querydict_fav = query_params.copy()
# Entferne die jeweiligen Seiten-Parameter, damit wir sie neu setzen können
querydict_my.pop('my_page', None)
querydict_all.pop('all_page', None)
querydict_fav.pop('favorite_page', None)
# Füge die "bereinigten" Querystrings dem context hinzu
context.update({
'querystring_my': querydict_my.urlencode(),
'querystring_all': querydict_all.urlencode(),
'querystring_favorite': querydict_fav.urlencode(),
})
if connect=="all":
context.update({"link":"library:overview_quiz"})
return render(request, 'library/overview_quiz.html', context)
elif connect=="my":
context.update({"link":"library:overview_quiz_my"})
return render(request, 'library/overview_quiz_my.html', context)
elif connect=="favorite":
context.update({"link":"library:overview_quiz_favorites"})
return render(request, 'library/overview_quiz_favorite.html', context)
def overview_quiz_my(request):
return overview_quiz(request, connect="my")
def overview_quiz_favorites(request):
return overview_quiz(request, connect="favorite")
@login_required
def favorite_quiz(request, pk):
quiz= get_object_or_404(QivipQuiz, pk=pk)
favorite = QivipQuizFavorite.objects.filter(user=request.user, quiz=quiz).first()
if favorite:
favorite.favorite=not favorite.favorite
if favorite.favorite==True:
favorite.favorite_date = timezone.now()
favorite.save()
else:
favorite.favorite_date = None
favorite.delete()
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich von den Favoriten entfernt!"
messages.error(request, statement)
else:
favorite=QivipQuizFavorite.objects.create(user=request.user, quiz=quiz, favorite=True, favorite_date=timezone.now())
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich zu den Favoriten hinzugefügt!"
messages.success(request,statement )
return redirect(request.META.get('HTTP_REFERER', 'library:overview_quiz'))
# Neues Quiz erstellen
@login_required
def new_quiz(request):
if request.method == 'POST':
form = QuizForm(request.POST)
form = QuizForm(request.POST, request.FILES)
if form.is_valid():
quiz = form.save(commit=False)
status = form.cleaned_data.get('status')
if status == 'öffentlich' and not quiz.published_at:
quiz.published_at = timezone.now()
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
quiz.user_id = request.user
#quiz.creator = request.user
quiz.save()
form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
return redirect('library:edit_quiz', pk=quiz.pk)
#form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
return redirect('library:detail_quiz', pk=quiz.pk)
else:
form = QuizForm()
return render(request, 'library/form.html', {'form': form})
@@ -39,39 +418,135 @@ def new_quiz(request):
def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz)
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid():
if 'reset_image' in request.POST:
quiz.image = None
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
quiz = form.save(commit=False)
# Beispiel: Status aus dem Formular holen
status = form.cleaned_data.get('status')
if status == 'öffentlich' and not quiz.published_at:
quiz.published_at = timezone.now()
form.save()
return redirect('library:overview_quiz')
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk)
else:
form = QuizForm(instance=quiz)
return render(request, 'library/form.html', {'form': form})
# Quiz löschen
@login_required
def delete_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
quiz.delete()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen
@login_required
def detail_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
critisize_quiz(request, pk)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz)
# Parse JSON data for each question
for question in questions:
if question.data:
question.data = json.loads(question.data)
context = {
'quiz': quiz,
'questions': questions
'questions': questions,
'detail':show_answers,
}
return render(request, 'library/detail_quiz.html', context)
if quiz.status!="privat" or quiz.user_id==request.user:
return render(request, 'library/detail_quiz.html', context)
else:
return redirect('library:overview_quiz')
@login_required
def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
new_quiz = original_quiz # Kopie erstellen (aber Achtung: Noch gleiche Referenz!)
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
new_quiz.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie"
new_quiz.base_quiz_id = pk
new_quiz.rating_count = 0
new_quiz.average_rating = 3.0
new_quiz.quiz_played_Lernmodus = 0
new_quiz.quiz_played_Moderiermodus = 0
new_quiz.quiz_played_Moderiermodus_players = 0
if original_quiz.status=="öffentlich":
new_quiz.published_at = timezone.now()
else:
new_quiz.published_at = None
new_quiz.save() # Speichern als neues Objekt
# Optional: Beschreibung anpassen
# Alle zugehörigen Fragen kopieren
questions = QivipQuestion.objects.filter(quiz_id=pk)
for question in questions:
question.uuid = uuid.uuid4()
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
question.quiz_id = original_quiz # Verknüpfe mit dem neuen Quiz
question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!")
return redirect('library:detail_quiz', pk=new_quiz.pk)
# Übersicht aller Fragen
@login_required
@@ -90,6 +565,7 @@ def question_list(request):
def new_question(request):
question_type = request.GET.get('type')
quiz_id = request.GET.get('quiz_id')
question_data = None
if not quiz_id:
messages.error(request, 'Quiz ID muss angegeben werden.')
@@ -101,12 +577,12 @@ def new_question(request):
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz')
if question_type not in ['multiple_choice', 'true_false']:
if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']:
base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
if request.method == 'POST':
question_text = request.POST.get('question', '')
question_text = request.POST.get('question', 'Kein Fragentext angegeben')
# Handle different question types
if question_type == 'true_false':
@@ -119,7 +595,18 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'multiple_choice':
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'multiple_choice' :
options = []
i = 1
# Limit to maximum 6 options
@@ -142,11 +629,78 @@ def new_question(request):
'question': question_text,
'options': options
}
elif question_type == 'order' :
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
is_correct = request.POST.get(f'correct_{i}') == '1'
options.append({
'order': i,
'value': request.POST.get(f'option_{i}'),
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': question_text,
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'map':
target_location = request.POST.get('location')
tolerance_radius = request.POST.get('tolerance_radius')
json_data = {
'type': question_type,
'question': question_text,
'target_location': target_location,
'tolerance_radius': tolerance_radius
}
question = QivipQuestion()
question.data = json.dumps(json_data)
question.quiz_id = quiz
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except:
pass
question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save()
#return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk)
else:
# Initialize empty question data for new questions
@@ -159,6 +713,19 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': False}
]
}
standard_time_per_question = 30
elif question_type == 'input':
question_data = {
'type': question_type,
'question': '',
'options': [
]
}
standard_time_per_question = 30
elif question_type == 'multiple_choice':
question_data = {
'type': question_type,
@@ -168,12 +735,51 @@ def new_question(request):
{'value': '', 'is_correct': False}
]
}
standard_time_per_question = 30
elif question_type == 'order':
question_data = {
'type': question_type,
'question': '',
'options': [
{'value': ''},
{'value': ''}
]
}
standard_time_per_question = 30
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': '',
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
standard_time_per_question = 30
elif question_type == 'map':
question_data = {
'type': question_type,
'question': '',
'target_location': "",
'tolerance_radius': 90000
}
standard_time_per_question = 30
template_name = f'library/question/question_{question_type}.html'
context = {
'question': question_data,
'quiz_id': quiz_id,
'quiz': quiz
'quiz': quiz,
'standard_time_per_question': standard_time_per_question,
}
return render(request, template_name, context)
@@ -182,6 +788,8 @@ def new_question(request):
@login_required
def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
if 'reset_ques_image' in request.POST:
question.image = None
if question.quiz_id.user_id != request.user:
return redirect('library:question_list')
@@ -189,6 +797,7 @@ def edit_question(request, pk):
try:
question_data = json.loads(question.data) if question.data else {}
question_type = question_data.get('type', 'multiple_choice')
question_image = question.image
# For true/false questions, get the correct answer from the options
if question_type == 'true_false':
@@ -205,6 +814,13 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'map':
question_data = {
'type': question_type,
'question': question_data.get('question', ''),
'target_location': question_data.get('target_location', ''),
'tolerance_radius': question_data.get('tolerance_radius', 90000)
}
else:
# For multiple choice questions
question_data.setdefault('type', question_type)
@@ -233,7 +849,13 @@ def edit_question(request, pk):
if request.method == 'POST':
question_text = request.POST.get('question', '')
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except Exception:
pass
# Handle different question types
if question_type == 'true_false':
correct_answer = request.POST.get('correct_answer') == 'true'
@@ -245,6 +867,17 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer,'is_correct':True}
]
}
elif question_type == 'multiple_choice':
options = []
i = 1
@@ -273,19 +906,90 @@ def edit_question(request, pk):
'options': options
}
elif question_type == 'order':
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
options.append({
'order': i,
'value': request.POST.get(f'option_{i}')
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
# Validate maximum 6 options
if len(options) > 6:
messages.error(request, 'Maximal 6 Optionen sind erlaubt.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': question_text,
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'map':
target_location = request.POST.get('location')
tolerance_radius = request.POST.get('tolerance_radius')
json_data = {
'type': question_type,
'question': question_text,
'target_location': target_location,
'tolerance_radius': tolerance_radius
}
question.data = json.dumps(json_data)
question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else:
template_name = f'library/question/question_{question_type}.html'
context = {
'credits':question.credits,
'question': {'data': question_data}, # Wrap in data structure expected by template
'quiz_id': question.quiz_id.pk,
'quiz': question.quiz_id
'quiz': question.quiz_id,
'time_per_question': question.time_per_question,
'image': question_image,
}
return render(request, template_name, context)
# Frage löschen
@login_required
def delete_question(request, pk):
@@ -297,5 +1001,11 @@ def delete_question(request, pk):
if request.method == 'POST':
question.delete()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
return render(request, 'library/delete_confirmation.html', {'object': question})

1489
django/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
django/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "django",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
}
}

528
django/play/consumers.py Normal file
View File

@@ -0,0 +1,528 @@
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from datetime import timedelta
from django.urls import reverse
from .models import QuizGame, QuizGameParticipant
class GameConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.game_group_name = f'game_{self.join_code}'
# Join game group
await self.channel_layer.group_add(
self.game_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
self.game_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_answer',
'answer': answer
}
)
elif message_type == 'get_answer_stats':
stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({
'type': 'answer_stats',
'stats': stats
}))
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'next_question':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.finish_game()
elif message_type == 'advance_to_scores':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scores()
async def participant_answer(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_answer',
'answer': event['answer']
}))
async def participant_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_list_update',
'participants': event['participants']
}))
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def verify_host(self, host_id):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
return quiz_game.host_id == host_id
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def save_answer(self, participant_id, answer_index, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
# Check if answer is correct
is_correct = False
if answer_index >= 0: # -1 means timeout
is_correct = current_question.data['options'][answer_index]['is_correct']
# Calculate score based on correctness and time
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / 30000 # 30 seconds max
score = int(base_score * (0.5 + 0.5 * time_factor))
participant.last_score=score
participant.score += score
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def get_answer_stats(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
# Count answers for each option
stats = {}
for participant in participants:
if participant.last_answer is not None:
stats[participant.last_answer] = stats.get(participant.last_answer, 0) + 1
return stats
except QuizGame.DoesNotExist:
return {}
@database_sync_to_async
def get_participants(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
return [
{
'id': p.participant_id,
'display_name': p.display_name,
'score': p.score
}
for p in participants
]
except QuizGame.DoesNotExist:
return []
@database_sync_to_async
def _advance_to_next_question(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz = quiz_game.quiz_id
# Move to next question
quiz_game.current_question_index += 1
# Check if we've reached the end
if quiz_game.current_question_index >= quiz.questions.count():
quiz_game.current_state = 'finished'
quiz_game.save()
return 'finished'
else:
# Update game state and start time
quiz_game.current_state = 'question'
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return 'question'
except QuizGame.DoesNotExist:
return None
async def advance_to_next_question(self):
result = await self._advance_to_next_question()
if result == 'finished':
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
elif result == 'question':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'next_question',
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scores'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scores(self):
success = await self._show_scores()
if success:
# Notify clients to redirect to scores page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scores',
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _finish_game(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def finish_game(self):
success = await self._finish_game()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
class LobbyConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.room_group_name = f'game_{self.join_code}'
self.heartbeat_task = None
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Start heartbeat check
self.heartbeat_task = asyncio.create_task(self.check_participants_heartbeat())
# Send current participants list
participants = await self.get_participants()
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
async def disconnect(self, close_code):
# Cancel heartbeat task
if self.heartbeat_task:
self.heartbeat_task.cancel()
try:
await self.heartbeat_task
except asyncio.CancelledError:
pass
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
async def game_state_update(self, event):
# Send game state update to WebSocket
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
elif message_type == 'submit_answer':
participant_id = text_data_json.get('participant_id')
answer = text_data_json.get('answer')
time_remaining = text_data_json.get('time_remaining')
if participant_id:
score = await self._process_answer(participant_id, answer, time_remaining)
await self.send(text_data=json.dumps({
'type': 'answer_processed',
'score': score
}))
elif message_type == 'next_question':
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
next_state = await self._advance_game_state()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': next_state['action'],
'redirect_url': next_state['redirect_url']
}
)
async def participant_list_update(self, event):
participants = event['participants']
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
@database_sync_to_async
def get_participants(self):
game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=game)
return [{'id': str(p.participant_id), 'name': p.display_name} for p in participants]
@database_sync_to_async
def _update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
participant.save() # This will update last_heartbeat due to auto_now=True
except QuizGameParticipant.DoesNotExist:
pass
@database_sync_to_async
def _is_host(self, host_id):
try:
return QuizGame.objects.filter(join_code=self.join_code, host_id=host_id).exists()
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
@database_sync_to_async
def _process_answer(self, participant_id, answer, time_remaining):
game = QuizGame.objects.get(join_code=self.join_code)
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
question = game.quiz_id.questions.all()[game.current_question_index]
# Load question data
question_data = json.loads(question.data)
correct_answer = question_data.get('correct_answer')
# Calculate score based on correctness and time
score = 0
if answer == correct_answer:
# Base score for correct answer + bonus for speed
score = 1000 + int(time_remaining * 10) # 10 points per remaining second
participant.last_score= score
participant.score += score
participant.save()
return score
@database_sync_to_async
def _advance_game_state(self):
game = QuizGame.objects.get(join_code=self.join_code)
total_questions = game.quiz_id.questions.count()
if game.current_state == 'question':
game.current_state = 'scores'
game.save()
return {
'action': 'show_scores',
'redirect_url': f'/play/game/{self.join_code}/scores'
}
elif game.current_state == 'scores':
if game.current_question_index + 1 < total_questions:
game.current_state = 'question'
game.current_question_index += 1
game.question_start_time = timezone.now()
game.save()
return {
'action': 'next_question',
'redirect_url': f'/play/game/{self.join_code}/{game.current_question_index}'
}
else:
game.current_state = 'finished'
game.save()
return {
'action': 'game_finished',
'redirect_url': f'/play/game/{self.join_code}/finished'
}
@database_sync_to_async
def _remove_inactive_participants(self):
# Remove participants who haven't sent a heartbeat in the last 30 seconds
timeout = timezone.now() - timedelta(seconds=30)
quiz_game = QuizGame.objects.get(join_code=self.join_code)
QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
last_heartbeat__lt=timeout
).delete()
async def check_participants_heartbeat(self):
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds
await self._remove_inactive_participants()
# Send updated participant list
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in heartbeat check: {e}')
await asyncio.sleep(10) # Wait before retrying

View File

@@ -0,0 +1,3 @@
from .game import GameConsumer
__all__ = ['GameConsumer']

View File

@@ -0,0 +1,721 @@
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from django.urls import reverse
from play.models import QuizGame, QuizGameParticipant, QuizAnswer,QivipQuiz
from django.conf import settings
from geopy.distance import geodesic
class GameConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.game_group_name = f'game_{self.join_code}'
# Join game group
await self.channel_layer.group_add(
self.game_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
self.game_group_name,
self.channel_name
)
# Check if game should be deleted
await self.cleanup_game()
@database_sync_to_async
def get_question_data(self):
quiz_game = QuizGame.objects.get(join_code=self.join_code)
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
return json.loads(current_question.data)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'ping':
# Respond with pong to keep connection alive
await self.send(text_data=json.dumps({
'type': 'pong'
}))
return
if message_type == 'leave_game':
participant_id = text_data_json.get('participant_id')
if participant_id:
try:
# Get participant info before deletion
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(participant_id=participant_id)
display_name = participant.display_name
join_code = self.join_code
# Delete participant
await database_sync_to_async(participant.delete)()
# Update participants list
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
# Send redirect to home
await self.send(text_data=json.dumps({
'type': 'redirect',
'url': '/'
}))
except QuizGameParticipant.DoesNotExist:
pass
return
if message_type == 'submit_rating':
participant_id = text_data_json['participant_id']
rating = text_data_json['rating']
success = await self.save_rating(participant_id, rating)
if success:
await self.send(text_data=json.dumps({
'type': 'rating_confirmed'
}))
elif message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0)
question_data = await self.get_question_data()
if question_data['type'] == "map":
await self.save_answer_map(participant_id, answer, time_remaining)
else:
await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_answer',
'answer': answer
}
)
elif message_type == 'start_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'get_answer_stats':
stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({
'type': 'answer_stats',
'stats': stats,
}))
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'next_question':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'winner_podest':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_winner_podest()
elif message_type == 'scoreboard':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scoreboard()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.finish_game()
elif message_type == 'advance_to_scores':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scores()
async def participant_answer(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_answer',
'answer': event['answer']
}))
async def participant_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_list_update',
'participants': event['participants']
}))
async def player_left(self, event):
await self.send(text_data=json.dumps({
'type': 'player_left',
'player_name': event['player_name'],
'was_kicked': event.get('was_kicked', False)
}))
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def verify_host(self, host_id):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
return quiz_game.host_id == host_id
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def save_answer_map(self, participant_id, answer_location, time_remaining):
answer_index = 0
try:
# Get all necessary data first
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
target_location = question_data['target_location']
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
# Parse coordinates if they are strings
def parse_coords(coord):
if isinstance(coord, str):
try:
# Handle both comma and space separated coordinates
if ',' in coord:
lat, lng = map(float, [x.strip() for x in coord.split(',')])
else:
# Try splitting by space if no comma
parts = coord.strip().split()
if len(parts) >= 2:
lat, lng = map(float, parts[:2])
else:
raise ValueError("Invalid coordinate format")
return (lat, lng)
except (ValueError, AttributeError) as e:
print(f"Error parsing coordinates {coord}: {e}")
return None
elif isinstance(coord, (list, tuple)) and len(coord) >= 2:
return (float(coord[0]), float(coord[1]))
return None
# Parse the coordinates
target_location = parse_coords(target_location)
answer_location_coords = answer_location
answer_location = parse_coords(answer_location)
if not target_location or not answer_location:
print("Invalid coordinates, cannot calculate score")
is_correct = False
score = 0
else:
# Configuration - Make these easily adjustable
MAX_DISTANCE_KM = 50 # Maximum distance for scoring (in km)
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
try:
# Calculate distance
distance_km = geodesic(answer_location, target_location).kilometers
tolerance_km = float(tolerance_radius) / 1000 # Convert meters to kilometers
print(f"Distance: {distance_km} km, Tolerance: {tolerance_km} km")
# Calculate base score with non-linear decrease
if distance_km <= tolerance_km:
# Direct hit - full points + bonus
base_score = 1000 + DIRECT_HIT_BONUS
is_correct = True
else:
if distance_km > MAX_DISTANCE_KM:
# Beyond max distance - 0 points
base_score = 0
is_correct = False
else:
# Non-linear decrease using exponential falloff
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
base_score = 1000 * (1 - falloff)
is_correct = True
# Apply time penalty (reduces score based on time taken)
if is_correct and base_score > 0:
# Calculate time taken as a fraction of total time (0-1)
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
max_time_sec = int(current_question.time_per_question)
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
else:
final_score = base_score # 0 points for incorrect answers
# Apply max score limit after all calculations (including bonus)
final_score = min(final_score, 1200) # Cap at 1200 (1000 + 200 bonus)
score = int(round(final_score))
except Exception as e:
print(f"Error calculating score: {e}")
is_correct = False
score = 0
# Save the answer
answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant,
question_index=quiz_game.current_question_index,
defaults={
'answer_index': answer_index,
'is_correct': is_correct,
'score': score,
'time_remaining': time_remaining
}
)
if not created:
answer_obj.answer_index = answer_index
answer_obj.is_correct = is_correct
answer_obj.score = score
answer_obj.time_remaining = time_remaining
answer_obj.save()
participant.last_score = score
participant.score += score
participant.coordinate_answer= answer_location_coords
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def save_answer(self, participant_id, answer, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
is_correct = False
answer_index = None
answers_order = None
answer_text = None
if question_type in ["multiple_choice", "true_false"]:
# answer ist hier eine Integer-Antwort (Index)
answer_index = int(answer)
if answer_index >= 0:
is_correct = question_data['options'][answer_index].get('is_correct', False)
elif question_type == "input":
user_input = str(answer).strip().lower().replace(" ", "")
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
is_correct = (user_input == correct_value)
answer_text = user_input
answer_index = 0 if is_correct else 1
elif question_type == "order":
# answer ist hier eine Liste von Strings mit der Reihenfolge
answers_order = answer
correct_sequence = sorted(question_data["options"], key=lambda x: x["order"])
correct_values = [opt["value"] for opt in correct_sequence]
is_correct = (answers_order == correct_values)
elif question_type == "guess":
try:
user_val = float(answer)
correct_val = float(question_data['options'][0].get('value', 0))
tolerance = float(question_data.get('tolerance', 0))
percentage = 1 - abs(user_val - correct_val) / (abs(correct_val) + 1e-5)
score_percentage= min(round(percentage, 3), 1.0)
if score_percentage>= abs(tolerance/100):
is_correct=True
else:
is_correct=False
print(score_percentage)
except ValueError:
is_correct = False # falls keine Zahl eingegeben wurde
answer_index = 0 if is_correct else 1
# Score berechnen
try:
if score_percentage==None:
score_percentage=1
except:
score_percentage=1
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / int(current_question.time_per_question) / 1000
score = int(base_score * (0.75 + 0.25 * time_factor))*score_percentage
print(score)
# Antwort speichern oder aktualisieren
answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant,
question_index=quiz_game.current_question_index,
defaults={
'answer_index': answer_index,
'answers_order': answers_order,
'is_correct': is_correct,
'score': score,
'time_remaining': time_remaining
}
)
if not created:
answer_obj.answer_index = answer_index
answer_obj.answers_order = answers_order
answer_obj.is_correct = is_correct
answer_obj.score = score
answer_obj.time_remaining = time_remaining
answer_obj.save()
# Teilnehmer aktualisieren
participant.last_score = score
participant.score += score
participant.last_answer=answer_index
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def get_answer_stats(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
answers = QuizAnswer.objects.filter(
participant__quiz_game=quiz_game,
question_index=quiz_game.current_question_index
)
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
stats = {}
for single_answer in answers:
if question_type == "order":
correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])]
if single_answer.answers_order== correct_answer:
key = 0 # richtig
else:
key = 1 # falsch
stats[key] = stats.get(key, 0) + 1
else:
key = str(single_answer.answer_index if single_answer.answer_index is not None else "unanswered")
stats[key] = stats.get(key, 0) + 1
return stats
except:
pass
@database_sync_to_async
def save_rating(self, participant_id, rating):
from django.db import models
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
return False
from library.models import QuizRating
try:
game = QuizGame.objects.get(join_code=self.join_code)
participant = QuizGameParticipant.objects.get(
quiz_game=game,
participant_id=participant_id
)
# Update or create rating
"""
rating_obj, created = QuizRating.objects.get_or_create(
quiz=game.quiz_id,
participant_id=participant_id,
defaults={'rating': rating}
)
"""
rating_obj=QuizRating.objects.create(
quiz=game.quiz_id,
participant_id=participant_id,
rating=rating)
rating_obj.rating = rating
rating_obj.save()
return True
except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist):
return False
@database_sync_to_async
def cleanup_game(self):
try:
game = QuizGame.objects.get(join_code=self.join_code)
active_participants = QuizGameParticipant.objects.filter(
quiz_game=game
).count()
if active_participants == 0:
game.delete()
except QuizGame.DoesNotExist:
pass
@database_sync_to_async
def get_participants(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
return [
{
'id': p.participant_id,
'display_name': p.display_name,
'score': p.score
}
for p in participants
]
except QuizGame.DoesNotExist:
return []
@database_sync_to_async
def _advance_to_next_question(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz = quiz_game.quiz_id
# Move to next question
quiz_game.current_question_index += 1
# Check if we've reached the end
if quiz_game.current_question_index >= quiz.questions.count():
quiz_game.current_state = 'finished'
quiz_game.save()
return 'finished'
else:
# Update game state and start time
quiz_game.current_state = 'question'
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return 'question'
except QuizGame.DoesNotExist:
return None
async def advance_to_next_question(self):
result = await self._advance_to_next_question()
if result == 'finished':
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
elif result == 'question':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'next_question',
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
elif result == 'winner_podest':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
elif result == 'scoreboard':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scores'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scores(self):
success = await self._show_scores()
if success:
# Notify clients to redirect to scores page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scores',
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def counter_played_quizzes(self): # Zähler, wie oft ein Quiz gespielt worden ist.
quiz = QuizGame.objects.get(join_code=self.join_code).quiz_id
quiz_game = QuizGame.objects.get(join_code=self.join_code)
if quiz_game.single_player_or_multiple=="multiple":
quiz.quiz_played_Moderiermodus+=1
active_participants = QuizGameParticipant.objects.filter(
quiz_game=quiz_game
).count()
quiz.quiz_played_Moderiermodus_players+=active_participants
else:
quiz.quiz_played_Lernmodus+=1
quiz.save()
@database_sync_to_async
def _finish_game(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def finish_game(self):
success = await self._finish_game()
await self.counter_played_quizzes()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_winner_podest(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'winner_podest'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_winner_podest(self):
success = await self._show_winner_podest()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scoreboard(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scoreboard'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scoreboard(self):
success = await self._show_scoreboard()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)

View File

@@ -0,0 +1,235 @@
from channels.generic.websocket import AsyncWebsocketConsumer
import json
import asyncio
from ..models import QuizGame, QuizGameParticipant
from channels.db import database_sync_to_async
import redis
# Dictionary to track inactive check tasks and active connections per game
game_check_tasks = {}
game_connections = {}
class LobbyConsumer(AsyncWebsocketConsumer):
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.room_group_name = f'lobby_{self.join_code}'
# Get participant ID from session
self.participant_id = self.scope['session'].get('participant_id')
# Track connection
if self.join_code not in game_connections:
game_connections[self.join_code] = set()
game_connections[self.join_code].add(self.channel_name)
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Send initial participants list
await self.update_participants()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Remove from connection tracking
if self.join_code in game_connections:
game_connections[self.join_code].discard(self.channel_name)
if not game_connections[self.join_code]:
# Last connection closed
del game_connections[self.join_code]
if self.join_code in game_check_tasks:
# Cancel the inactive check task
game_check_tasks[self.join_code].cancel()
del game_check_tasks[self.join_code]
async def receive(self, text_data):
data = json.loads(text_data)
message_type = data.get('type')
print(f'Received message type: {message_type}')
if message_type == 'ping':
# Respond with pong to keep connection alive
await self.send(text_data=json.dumps({
'type': 'pong'
}))
return
elif message_type in ['leave_game', 'kick_player']:
participant_id = data.get('participant_id')
if participant_id:
try:
# Get participant info before deletion
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(participant_id=participant_id)
display_name = participant.display_name
# For kick_player, verify that the request comes from the host
if message_type == 'kick_player':
game = await database_sync_to_async(lambda: participant.quiz_game)()
# Get host_id from message
host_id = data.get('host_id')
print(f'Kick attempt - Game host_id: {game.host_id}, Message host_id: {host_id}')
if not host_id:
print('No host_id in message')
return
if str(game.host_id) != str(host_id):
print(f'Host ID mismatch: {game.host_id} != {host_id}')
return
# Also verify against cookie as backup
cookie_host_id = self.scope.get('cookies', {}).get('host_id')
if not cookie_host_id or str(cookie_host_id) != str(host_id):
print(f'Cookie host_id mismatch: {cookie_host_id} != {host_id}')
return
# Delete the participant
await database_sync_to_async(participant.delete)()
# Notify other players
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'player_left',
'player_name': display_name,
'was_kicked': message_type == 'kick_player',
'participant_id': participant_id
}
)
# Update participants list
await self.update_participants()
# If it's the kicked player's connection, send them home
if participant_id == self.participant_id:
await self.send(text_data=json.dumps({
'type': 'redirect',
'url': '/'
}))
except QuizGameParticipant.DoesNotExist:
pass
elif message_type == 'update_participants':
await self.update_participants()
elif message_type == 'start_game':
host_id = data.get('host_id')
if host_id:
success = await self.start_game(host_id)
if success:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/question'
}
)
async def get_participants_list(self):
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code)
participants = await database_sync_to_async(lambda: list(
game.participants
.values('participant_id', 'display_name')
))()
# Zustand laden
try:
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
# Alte Teilnehmer aus Redis holen
data = r.get(f"last_participants_{self.room_group_name}")
prev_names = set(json.loads(data)) if data else set()
# Aktuelle Teilnehmer aus dem Argument extrahieren
current_names = set(p['display_name'] for p in participants)
# Neue Teilnehmer bestimmen
new_participants = current_names - prev_names
# Benachrichtigung für neue Teilnehmer versenden
for name in new_participants:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'player_joined',
'player_name': name
}
)
# Teilnehmerliste in Redis aktualisieren
r.set(f"last_participants_{self.room_group_name}", json.dumps(list(current_names)))
except Exception as e:
print(f"Error in send_join_notifications: {e}")
self.last_participants = participants
return participants
except QuizGame.DoesNotExist:
return []
async def update_participants(self):
participants = await self.get_participants_list()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participants_list_update',
'participants': participants
}
)
async def participants_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participants_list_update',
'participants': event['participants']
}))
@database_sync_to_async
def start_game(self, host_id):
try:
game = QuizGame.objects.get(join_code=self.join_code)
if str(game.host_id) == str(host_id):
from django.utils import timezone
game.current_state = 'question'
game.question_start_time = timezone.now()
game.save()
return True
except QuizGame.DoesNotExist:
pass
return False
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
async def player_joined(self, event):
await self.send(text_data=json.dumps({
'type': 'player_joined',
'player_name': event['player_name']
}))
async def player_left(self, event):
message = {
'type': 'player_left',
'player_name': event['player_name'],
'was_kicked': event.get('was_kicked', False)
}
if event.get('was_kicked') and event.get('participant_id'):
message['participant_id'] = event['participant_id']
await self.send(text_data=json.dumps(message))

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.7 on 2025-04-05 10:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0003_quizgameparticipant_participant_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='quizgame',
name='host_user',
),
migrations.AddField(
model_name='quizgame',
name='host_id',
field=models.CharField(default=None, max_length=200, unique=True),
preserve_default=False,
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-05 12:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0004_remove_quizgame_host_user_quizgame_host_id'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_heartbeat',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.7 on 2025-04-05 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0005_quizgameparticipant_last_heartbeat'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='current_question_index',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('finished', 'Beendet')], default='lobby', max_length=20),
),
migrations.AddField(
model_name='quizgame',
name='question_start_time',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-05 18:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0006_quizgame_current_question_index_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizgameparticipant',
name='quiz_game',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='play.quizgame'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.7 on 2025-04-05 18:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0007_alter_quizgameparticipant_quiz_game'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_answer',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quizgameparticipant',
name='last_answer_correct',
field=models.BooleanField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-04-05 20:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0008_quizgameparticipant_last_answer_and_more'),
]
operations = [
migrations.CreateModel(
name='QuizAnswer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_index', models.IntegerField()),
('answer_index', models.IntegerField()),
('is_correct', models.BooleanField()),
('score', models.IntegerField()),
('time_remaining', models.IntegerField()),
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='play.quizgameparticipant')),
],
options={
'unique_together': {('participant', 'question_index')},
},
),
]

Some files were not shown because too many files have changed in this diff Show More