Compare commits

..

192 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
ben8
7e321af48c 404 Fehler Seite-vorbereitet! 2025-04-13 12:44:12 +02:00
126 changed files with 9186 additions and 1515 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
__pycache__ __pycache__
.venv .venv
.env
db.sqlite3 db.sqlite3
tailwindcss.exe tailwindcss.exe
t-style.css t-style.css

View File

@@ -7,6 +7,7 @@
3. [Superuser erstellen](#superuser-erstellen) 3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung) 4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge) 5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts ## Initialisierung des Projekts
@@ -40,7 +41,7 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
``` ```
7. Die Datenbank migrieren: 7. Die Datenbank migrieren:
```sh ```sh
python3 core/manage.py migrate python django/manage.py migrate
``` ```
## Starten des Servers ## Starten des Servers
@@ -61,6 +62,13 @@ Für die Entwicklung mit WebSocket-Unterstützung verwenden wir Daphne:
daphne -b 127.0.0.1 -p 8000 core.asgi:application 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: Alternativ kannst du den Django-Entwicklungsserver verwenden, wenn du keine WebSocket-Funktionalität benötigst:
```sh ```sh
python3 core/manage.py runserver python3 core/manage.py runserver
@@ -84,6 +92,19 @@ Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP
- `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces - `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces
- `-p 8000`: Port (anpassbar) - `-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! Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
## Superuser erstellen ## Superuser erstellen
@@ -141,3 +162,20 @@ Hier der vereinbarte Arbeitsablauf:
2. Merge Request anlegen - dadurch wird der Branch 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. 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 RegisterForm(UserCreationForm):
class Meta: class Meta:
model=User model=User
fields = ['username','password1','password2'] fields = ['username','password1','password2', "email"]
username = forms.CharField( username = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'}) widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
) )
email = forms.EmailField(
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
)
password1 = forms.CharField( password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'}) 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 . import views
from django.contrib.auth.views import LogoutView from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import LoginView 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 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('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('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'),
path('register/', views.register, name='register'), 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

@@ -2,11 +2,31 @@ from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate from django.contrib.auth import login, authenticate
from .forms import RegisterForm 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. # Create your views here.
@login_required @login_required
def home(request): 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): def register(response):
if response.method == "POST": if response.method == "POST":
@@ -18,4 +38,34 @@ def register(response):
else: else:
form = RegisterForm() 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

@@ -10,30 +10,42 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import json, os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR ENTWICKLUNG
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # 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! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
#DEBUG = False
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*'] 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 # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'library.apps.LibraryConfig', 'library.apps.LibraryConfig',
'homepage.apps.HomepageConfig', 'homepage.apps.HomepageConfig',
'components.apps.ComponentsConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'play', 'play',
'django.contrib.admin', 'django.contrib.admin',
@@ -44,6 +56,9 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels', 'channels',
'django_cleanup', 'django_cleanup',
'django.contrib.humanize',
'dal',
'dal_select2',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -79,12 +94,16 @@ WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application' ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
'default': { "default": {
'BACKEND': 'channels.layers.InMemoryChannelLayer' "BACKEND": "channels_redis.core.RedisChannelLayer",
} "CONFIG": {
"hosts": [("127.0.0.1", 6380)],
},
},
} }
# Database # Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
@@ -144,7 +163,7 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration # WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js'] WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files # Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None WHITENOISE_MISSING_FILE_ERRNO = None
@@ -159,3 +178,13 @@ import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien 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

@@ -25,7 +25,6 @@ urlpatterns = [
path('', include('homepage.urls')), path('', include('homepage.urls')),
path('play/', include('play.urls')), path('play/', include('play.urls')),
path('library/', include('library.urls')), path('library/', include('library.urls')),
path('components/', include('components.urls'))
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@@ -1,3 +1,12 @@
from django.contrib import admin from django.contrib import admin
from .models import QivipFaq, QivipDescription
# Register your models here. # 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 from django.db import models
# Create your models here. # 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 django.shortcuts import render, redirect
from .models import QivipFaq, QivipDescription
def home(request): 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): def impress(request):
return render(request, 'homepage/impress.html') return render(request, 'homepage/impress.html')
def privacy(request): def privacy(request):
return render(request, 'homepage/privacy.html') return render(request, 'homepage/privacy.html')

View File

@@ -1,14 +1,25 @@
from django.contrib import admin from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuizCategory from .models import QivipQuiz, QivipQuestion,QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating, QuizCriticism
# Für das QivipQuiz Modell # Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin): 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 search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen 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 # Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin): class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date') list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')
search_fields = ('data',) search_fields = ('data',)
list_filter = ('quiz_id',) list_filter = ('quiz_id',)
@@ -23,8 +34,42 @@ class QuizCategoryAdmin(admin.ModelAdmin):
class QivipQuizFavoriteAdmin(admin.ModelAdmin): class QivipQuizFavoriteAdmin(admin.ModelAdmin):
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden 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 # Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin) admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin) admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin) admin.site.register(QuizCriticism, QuizCriticismAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin) admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -1,10 +1,33 @@
from django import forms from django import forms
from .models import QivipQuiz, QivipQuestion from .models import QivipQuiz, QivipQuestion, QuizCategory
from dal import autocomplete
class QuizForm(forms.ModelForm): class QuizForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description','image', 'status', 'category','difficulty','credits'] 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 QuestionForm(forms.ModelForm):
class Meta: class Meta:
@@ -15,17 +38,24 @@ from django import forms
class QuizFilterForm(forms.Form): class QuizFilterForm(forms.Form):
search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={ search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'list': 'quiz-names',
})) }))
min_amout_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={ 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' 'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
})) }))
max_amout_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={ 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' 'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
})) }))
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={ user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full' '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,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,6 +3,15 @@ from django.contrib.auth.models import User
from django.utils.text import slugify from django.utils.text import slugify
import uuid import uuid
from django.core.exceptions import ValidationError 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): class QivipQuiz(models.Model):
STATUS_VALUES = { STATUS_VALUES = {
@@ -27,10 +36,27 @@ class QivipQuiz(models.Model):
if value.strip() == "In dem Quiz geht es um ...": if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.") raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/ 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) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
base_quiz = models.ForeignKey('self', on_delete=models.SET_NULL,blank=True, null=True, related_name='child_quizzes') 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) creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
@@ -42,6 +68,10 @@ class QivipQuiz(models.Model):
credits=models.TextField(blank=True, editable=True) credits=models.TextField(blank=True, editable=True)
average_rating = models.FloatField(default=3.0) average_rating = models.FloatField(default=3.0)
rating_count = models.IntegerField(default=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): def __str__(self):
return self.name return self.name
@@ -56,6 +86,7 @@ class QivipQuizFavorite(models.Model):
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
# Create your models here. # Create your models here.
class QivipQuestion(models.Model): class QivipQuestion(models.Model):
@@ -74,10 +105,37 @@ class QivipQuestion(models.Model):
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
data = models.TextField() 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") time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True) 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): def __str__(self):
return self.data[:50] return self.data[:50]
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 f"{self.question_criticism}"
class QuizRating(models.Model): class QuizRating(models.Model):
@@ -86,9 +144,22 @@ class QuizRating(models.Model):
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)]) rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
class Meta: #class Meta:
unique_together = ('quiz', 'participant_id') #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): def save(self, *args, **kwargs):
is_new = self.pk is None is_new = self.pk is None
super().save(*args, **kwargs) super().save(*args, **kwargs)
@@ -100,12 +171,18 @@ class QuizRating(models.Model):
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg'] avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0 quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings quiz.rating_count = total_ratings
quiz.save() quiz.save()"""
class QuizCategory(models.Model): class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
slug = models.SlugField(unique=True, blank=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): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:

View File

@@ -3,10 +3,13 @@ from django.urls import path
from django.conf import settings from django.conf import settings
from . import views from . import views
from django.conf.urls.static import static from django.conf.urls.static import static
from .views import QuizAutocomplete
app_name = 'library' app_name = 'library'
urlpatterns = [ urlpatterns = [
path('', views.overview_quiz, name='overview_quiz'), 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('new/', views.new_quiz, name='new_quiz'),
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'), path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'), path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
@@ -16,5 +19,16 @@ urlpatterns = [
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'), path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'), path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_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,12 +1,12 @@
from django.utils import timezone from django.utils import timezone
import uuid import uuid
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.contrib import messages from django.contrib import messages
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCriticism, QuizImage
from .forms import QuizForm, QuestionForm from .forms import QuizForm, QuestionForm
import json import json
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -14,153 +14,335 @@ from .models import QivipQuiz
from .forms import QuizFilterForm from .forms import QuizFilterForm
from django.db.models import Count from django.db.models import Count
from django.contrib.auth.models import User from django.contrib.auth.models import User
from urllib.parse import urlparse, urlunparse
# Übersicht aller Quizze # Übersicht aller Quizze
def overview_quiz(request): from reportlab.lib.pagesizes import A4
# Filter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
form = QuizFilterForm(request.GET) from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
try: from reportlab.lib import colors
favorite_quizzes_data = QivipQuizFavorite.objects.filter(user=request.user, favorite=True).order_by("-favorite_date") from django.http import HttpResponse
favorite_quiz_ids = favorite_quizzes_data.values_list('quiz', flat=True) import json
favorite_quizzes = QivipQuiz.objects.filter(id__in=favorite_quiz_ids) from datetime import datetime
except: from reportlab.platypus import Image
favorite_quizzes_data = QivipQuizFavorite.objects.none()
favorite_quizzes = QivipQuizFavorite.objects.none()
favorite_quiz_ids = favorite_quizzes.none()
favorite_quizzes = QivipQuiz.objects.none()
# Initialize querysets from reportlab.lib import colors
if request.user.is_authenticated: from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions')) from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
favorite_quizzes = favorite_quizzes.filter(id__in=favorite_quiz_ids).annotate(max_amout_questions=Count('questions')) from reportlab.lib.pagesizes import A4
filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user) from django.http import HttpResponse
else: from django.shortcuts import get_object_or_404
favorite_quizzes = QivipQuiz.objects.none() import json
filter_my_quizzes = QivipQuiz.objects.none() from io import BytesIO
filter_other_quizzes = QivipQuiz.objects.all() 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):
# Apply common filters to other quizzes def get_queryset(self):
filter_other_quizzes = filter_other_quizzes.annotate(max_amout_questions=Count('questions')) qs = QivipQuiz.objects.filter(status="öffentlich")
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1, 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(): if form.is_valid():
search = form.cleaned_data.get('search') search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amout_questions= form.cleaned_data.get('max_amout_questions')
min_amout_questions= form.cleaned_data.get('min_amout_questions') 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
)
filters = {} names=list(names.values_list('name', flat=True).distinct())
if username:
try:
user = User.objects.get(username=username)
filters['user_id'] = user.id
except User.DoesNotExist:
favorite_quizzes = QivipQuiz.objects.none()
filter_my_quizzes = QivipQuiz.objects.none()
filter_other_quizzes=QivipQuiz.objects.none()
if search:
filters['name__icontains'] = search
if min_amout_questions:
filters['max_amout_questions__gte'] = min_amout_questions
if max_amout_questions:
filters['max_amout_questions__lte'] = max_amout_questions
try:
filter_other_quizzes = filter_other_quizzes.filter(**filters)
except:
filter_other_quizzes=QivipQuiz.objects.none()
try:
filter_my_quizzes = filter_my_quizzes.filter(**filters)
except:
filter_my_quizzes=QivipQuiz.objects.none()
try:
favorite_quizzes = favorite_quizzes.filter(**filters)
except:
favorite_quizzes=QivipQuiz.objects.none()
"""
if search: # Suche nach Namen
filter_other_quizzes = filter_other_quizzes.filter(name__icontains=search)
filter_my_quizzes = filter_my_quizzes.filter(name__icontains=search)
if min_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=min_amout_questions)
filter_my_quizzes = filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions) if filter_my_quizzes else QivipQuiz.objects.none()
if max_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__lte=max_amout_questions)
try:
filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__lte=max_amout_questions)
except:
filter_my_quizzes = QivipQuiz.objects.none()
if username:
try:
user = User.objects.get(username=username) # Benutzer anhand des Namens holen
filter_other_quizzes = filter_other_quizzes.filter(user_id=user.id)
filter_my_quizzes = filter_my_quizzes.filter(user_id=user.id) # Nach der user_id filtern
except User.DoesNotExist:
filter_other_quizzes = QivipQuiz.objects.none() # Falls User nicht existiert, leere Query zurückgeben
filter_my_quizzes = QivipQuiz.objects.none()
try:
filter_other_quizzes=filter_other_quizzes.exclude(user_id=request.user)
except:
filter_other_quizzes=filter_other_quizzes
"""
return JsonResponse(names, safe=False)
# Pagination for my quizzes
my_quizzes_paginator = Paginator(filter_my_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
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: try:
my_quizzes = my_quizzes_paginator.page(request.GET.get('my_page', 1)) user = User.objects.get(username=username)
except: filters['user_id'] = user.id
my_quizzes = my_quizzes_paginator.page(1) except User.DoesNotExist:
return queryset.none()
# Pagination for other quizzes return queryset.filter(**filters)
other_quizzes_paginator = Paginator(filter_other_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
other_quizzes = other_quizzes_paginator.page(request.GET.get('other_page', 1))
except:
other_quizzes = other_quizzes_paginator.page(1)
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)
try:
favorite_quizzes = favorite_quizzes.filter(
favorites__user=request.user, # Filtere nach dem User
favorites__favorite=True # Filtere nach favorisierten Quizzes
).order_by('-favorites__favorite_date')
except:
pass
# Pagination for favorite_quizzes def get_critisized_quizzes():
favorite_quizzes_paginator = Paginator(favorite_quizzes, 8) # 8 quizzes per page critisized_quizzes = QivipQuiz.objects.annotate(
try: criticism_count=Count('criticism_quiz')
favorite_quizzes = favorite_quizzes_paginator.page(request.GET.get('my_page', 1)) ).filter(criticism_count__gt=0)
except: return critisized_quizzes
favorite_quizzes = favorite_quizzes_paginator.page(1)
context = {
'show_search': True,
'favorite_quiz_ids': favorite_quiz_ids,
'favorite_quizzes':favorite_quizzes,
'my_quizzes': my_quizzes,
'other_quizzes': other_quizzes,
'form': form,
}
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:
my_quizzes = paginator_my.page(my_page)
except:
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) return render(request, 'library/overview_quiz.html', context)
elif connect=="my":
# Neues Quiz erstellen 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 @login_required
def favorite_quiz(request, pk): def favorite_quiz(request, pk):
quiz= get_object_or_404(QivipQuiz, pk=pk) quiz= get_object_or_404(QivipQuiz, pk=pk)
@@ -169,13 +351,11 @@ def favorite_quiz(request, pk):
if favorite: if favorite:
favorite.favorite=not favorite.favorite favorite.favorite=not favorite.favorite
if favorite.favorite==True: if favorite.favorite==True:
favorite.favorite_date = timezone.now() favorite.favorite_date = timezone.now()
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich zu den Favoriten hinzugefügt!"
messages.success(request,statement )
favorite.save() favorite.save()
else: else:
favorite.favorite_date = None favorite.favorite_date = None
@@ -185,7 +365,9 @@ def favorite_quiz(request, pk):
else: else:
QivipQuizFavorite.objects.create(user=request.user, quiz=quiz, favorite=True, favorite_date=timezone.now()) 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')) return redirect(request.META.get('HTTP_REFERER', 'library:overview_quiz'))
@@ -193,11 +375,35 @@ def favorite_quiz(request, pk):
# Neues Quiz erstellen # Neues Quiz erstellen
@login_required @login_required
def new_quiz(request): def new_quiz(request):
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, request.FILES) form = QuizForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
quiz = form.save(commit=False) 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.user_id = request.user
#quiz.creator = request.user #quiz.creator = request.user
quiz.save() quiz.save()
#form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags) #form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
@@ -212,10 +418,40 @@ def new_quiz(request):
def edit_quiz(request, pk): def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz, files=request.FILES) form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid(): 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() form.save()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=pk) return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk) #return modified(request, pk)
@@ -229,12 +465,27 @@ def delete_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
quiz.delete() 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 redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz}) return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen # Quiz anzeigen
def detail_quiz(request, pk): 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) quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true' show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz) questions = QivipQuestion.objects.filter(quiz_id=quiz)
@@ -248,12 +499,17 @@ def detail_quiz(request, pk):
'questions': questions, 'questions': questions,
'detail':show_answers, '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 @login_required
def copy_quiz(request, pk): def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=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)
@@ -264,9 +520,18 @@ def copy_quiz(request, pk):
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt 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.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie" # Optional: Name anpassen new_quiz.name += " - Kopie"
new_quiz.base_quiz_id = pk new_quiz.base_quiz_id = pk
#new_quiz.creator = original_quiz.creator or request.user 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 new_quiz.save() # Speichern als neues Objekt
# Optional: Beschreibung anpassen # Optional: Beschreibung anpassen
# Alle zugehörigen Fragen kopieren # Alle zugehörigen Fragen kopieren
@@ -276,6 +541,7 @@ def copy_quiz(request, pk):
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird 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.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.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save() question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!") messages.success(request, "Quiz wurde erfolgreich kopiert!")
@@ -299,6 +565,7 @@ def question_list(request):
def new_question(request): def new_question(request):
question_type = request.GET.get('type') question_type = request.GET.get('type')
quiz_id = request.GET.get('quiz_id') quiz_id = request.GET.get('quiz_id')
question_data = None
if not quiz_id: if not quiz_id:
messages.error(request, 'Quiz ID muss angegeben werden.') messages.error(request, 'Quiz ID muss angegeben werden.')
@@ -310,13 +577,12 @@ def new_question(request):
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.') messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz') 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') base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}') return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
if request.method == 'POST': if request.method == 'POST':
question_text = request.POST.get('question', '') question_text = request.POST.get('question', 'Kein Fragentext angegeben')
question_image = request.FILES.get('question_image', None)
# Handle different question types # Handle different question types
if question_type == 'true_false': if question_type == 'true_false':
@@ -329,7 +595,18 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': not correct_answer} {'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 = [] options = []
i = 1 i = 1
# Limit to maximum 6 options # Limit to maximum 6 options
@@ -352,12 +629,76 @@ def new_question(request):
'question': question_text, 'question': question_text,
'options': options '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 = QivipQuestion()
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.quiz_id = quiz question.quiz_id = quiz
question.image = question_image 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') question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save() question.save()
#return modified(request, pk=quiz.pk) #return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:detail_quiz', pk=quiz.pk)
@@ -373,6 +714,18 @@ def new_question(request):
] ]
} }
standard_time_per_question = 30 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': elif question_type == 'multiple_choice':
question_data = { question_data = {
'type': question_type, 'type': question_type,
@@ -383,7 +736,44 @@ def new_question(request):
] ]
} }
standard_time_per_question = 30 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' template_name = f'library/question/question_{question_type}.html'
context = { context = {
'question': question_data, 'question': question_data,
@@ -398,6 +788,8 @@ def new_question(request):
@login_required @login_required
def edit_question(request, pk): def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user) 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: if question.quiz_id.user_id != request.user:
return redirect('library:question_list') return redirect('library:question_list')
@@ -422,6 +814,13 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer} {'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: else:
# For multiple choice questions # For multiple choice questions
question_data.setdefault('type', question_type) question_data.setdefault('type', question_type)
@@ -451,7 +850,9 @@ def edit_question(request, pk):
if request.method == 'POST': if request.method == 'POST':
question_text = request.POST.get('question', '') question_text = request.POST.get('question', '')
try: try:
question_image = request.FILES['question_image'] question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except Exception: except Exception:
pass pass
@@ -466,6 +867,17 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer} {'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': elif question_type == 'multiple_choice':
options = [] options = []
i = 1 i = 1
@@ -494,16 +906,80 @@ def edit_question(request, pk):
'options': options '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.data = json.dumps(json_data)
question.time_per_question = request.POST.get('time_per_question', '30') question.time_per_question = request.POST.get('time_per_question', '30')
question.image = question_image try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save() 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 modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk) return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else: else:
template_name = f'library/question/question_{question_type}.html' template_name = f'library/question/question_{question_type}.html'
context = { context = {
'credits':question.credits,
'question': {'data': question_data}, # Wrap in data structure expected by template 'question': {'data': question_data}, # Wrap in data structure expected by template
'quiz_id': question.quiz_id.pk, 'quiz_id': question.quiz_id.pk,
'quiz': question.quiz_id, 'quiz': question.quiz_id,
@@ -525,5 +1001,11 @@ def delete_question(request, pk):
if request.method == 'POST': if request.method == 'POST':
question.delete() 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 redirect('library:detail_quiz', pk=question.quiz_id.pk)
return render(request, 'library/delete_confirmation.html', {'object': question}) return render(request, 'library/delete_confirmation.html', {'object': question})

1488
django/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,11 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"swiper": "^11.2.6" "@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
} }
} }

View File

@@ -129,6 +129,7 @@ class GameConsumer(AsyncWebsocketConsumer):
time_factor = time_remaining / 30000 # 30 seconds max time_factor = time_remaining / 30000 # 30 seconds max
score = int(base_score * (0.5 + 0.5 * time_factor)) score = int(base_score * (0.5 + 0.5 * time_factor))
participant.last_score=score
participant.score += score participant.score += score
participant.last_answer_correct = is_correct participant.last_answer_correct = is_correct
participant.save() participant.save()
@@ -460,7 +461,7 @@ class LobbyConsumer(AsyncWebsocketConsumer):
if answer == correct_answer: if answer == correct_answer:
# Base score for correct answer + bonus for speed # Base score for correct answer + bonus for speed
score = 1000 + int(time_remaining * 10) # 10 points per remaining second score = 1000 + int(time_remaining * 10) # 10 points per remaining second
participant.last_score= score
participant.score += score participant.score += score
participant.save() participant.save()
return score return score

View File

@@ -4,80 +4,11 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from django.db import models from play.models import QuizGame, QuizGameParticipant, QuizAnswer,QivipQuiz
from datetime import timedelta from django.conf import settings
from geopy.distance import geodesic
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
# Dictionary to track inactive check tasks per game
game_check_tasks = {}
class GameConsumer(AsyncWebsocketConsumer): class GameConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
game_group_name = f'game_{join_code}'
while True:
try:
await asyncio.sleep(30) # Check every 30 seconds
try:
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(minutes=1)
# Get active and inactive participants
all_participants = await database_sync_to_async(lambda: list(
game.participants.all().values('participant_id', 'display_name', 'last_heartbeat')
))()
# Konvertiere datetime zu ISO Format String
for p in all_participants:
if p['last_heartbeat']:
p['last_heartbeat'] = p['last_heartbeat'].isoformat()
active_participants = [p for p in all_participants if p['last_heartbeat'] and timezone.datetime.fromisoformat(p['last_heartbeat']) >= cutoff_time]
inactive_participants = [p for p in all_participants if not p['last_heartbeat'] or timezone.datetime.fromisoformat(p['last_heartbeat']) < cutoff_time]
# Remove inactive participants
if inactive_participants:
for p in inactive_participants:
# Benachrichtige andere über den gekickten Spieler
await channel_layer.group_send(
game_group_name,
{
'type': 'player_left',
'player_name': p['display_name'],
'was_kicked': True
}
)
# Lösche den inaktiven Teilnehmer
await database_sync_to_async(QuizGameParticipant.objects.filter(
participant_id=p['participant_id']
).delete)()
# Broadcast update to all clients
if active_participants or inactive_participants:
await channel_layer.group_send(
game_group_name,
{
'type': 'participant_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players in game: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in game inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
async def connect(self): async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code'] self.join_code = self.scope['url_route']['kwargs']['join_code']
@@ -90,9 +21,6 @@ class GameConsumer(AsyncWebsocketConsumer):
) )
await self.accept() await self.accept()
# Ensure inactive check is running for this game
await self.start_inactive_check(self.join_code, self.channel_layer)
async def disconnect(self, close_code): async def disconnect(self, close_code):
# Leave game group # Leave game group
@@ -104,6 +32,13 @@ class GameConsumer(AsyncWebsocketConsumer):
# Check if game should be deleted # Check if game should be deleted
await self.cleanup_game() 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): async def receive(self, text_data):
text_data_json = json.loads(text_data) text_data_json = json.loads(text_data)
message_type = text_data_json['type'] message_type = text_data_json['type']
@@ -145,21 +80,6 @@ class GameConsumer(AsyncWebsocketConsumer):
except QuizGameParticipant.DoesNotExist: except QuizGameParticipant.DoesNotExist:
pass pass
return return
if message_type == 'heartbeat':
participant_id = text_data_json.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Update participants list after heartbeat
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
return
if message_type == 'submit_rating': if message_type == 'submit_rating':
participant_id = text_data_json['participant_id'] participant_id = text_data_json['participant_id']
@@ -175,8 +95,13 @@ class GameConsumer(AsyncWebsocketConsumer):
answer = text_data_json['answer'] answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0) time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
await self.save_answer(participant_id, answer, time_remaining) 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 # Notify host about the new answer
await self.channel_layer.group_send( await self.channel_layer.group_send(
@@ -196,7 +121,7 @@ class GameConsumer(AsyncWebsocketConsumer):
stats = await self.get_answer_stats() stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({
'type': 'answer_stats', 'type': 'answer_stats',
'stats': stats 'stats': stats,
})) }))
elif message_type == 'update_participants': elif message_type == 'update_participants':
@@ -213,8 +138,18 @@ class GameConsumer(AsyncWebsocketConsumer):
host_id = text_data_json['host_id'] host_id = text_data_json['host_id']
if await self.verify_host(host_id): if await self.verify_host(host_id):
await self.advance_to_next_question() 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': elif message_type == 'finish_game':
host_id = text_data_json['host_id'] host_id = text_data_json['host_id']
if await self.verify_host(host_id): if await self.verify_host(host_id):
await self.finish_game() await self.finish_game()
@@ -250,17 +185,6 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': event.get('redirect_url') 'redirect_url': event.get('redirect_url')
})) }))
async def update_participant_heartbeat(self, participant_id):
"""Update the last heartbeat timestamp for a participant."""
try:
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(
participant_id=participant_id
)
participant.last_heartbeat = timezone.now()
await database_sync_to_async(participant.save)()
except QuizGameParticipant.DoesNotExist:
pass
@database_sync_to_async @database_sync_to_async
def verify_host(self, host_id): def verify_host(self, host_id):
try: try:
@@ -270,29 +194,107 @@ class GameConsumer(AsyncWebsocketConsumer):
return False return False
@database_sync_to_async @database_sync_to_async
def save_answer(self, participant_id, answer_index, time_remaining): def save_answer_map(self, participant_id, answer_location, time_remaining):
answer_index = 0
try: try:
# Get all necessary data first
participant = QuizGameParticipant.objects.get( participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code, quiz_game__join_code=self.join_code,
participant_id=participant_id participant_id=participant_id
) )
quiz_game = participant.quiz_game quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[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)
target_location = question_data['target_location']
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
# Check if answer is correct
is_correct = False
if answer_index >= 0: # -1 means timeout
question_data = json.loads(current_question.data)
is_correct = question_data['options'][answer_index]['is_correct']
# Calculate score based on correctness and time # Parse coordinates if they are strings
score = 0 def parse_coords(coord):
if is_correct: if isinstance(coord, str):
base_score = 1000 try:
time_factor = time_remaining / 30000 # 30 seconds max # Handle both comma and space separated coordinates
score = int(base_score * (0.5 + 0.5 * time_factor)) 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)
# Update or create answer in QuizAnswer table
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( answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant, participant=participant,
question_index=quiz_game.current_question_index, question_index=quiz_game.current_question_index,
@@ -311,36 +313,170 @@ class GameConsumer(AsyncWebsocketConsumer):
answer_obj.time_remaining = time_remaining answer_obj.time_remaining = time_remaining
answer_obj.save() answer_obj.save()
participant.last_score = score
participant.score += score participant.score += score
participant.coordinate_answer= answer_location_coords
participant.last_answer_correct = is_correct participant.last_answer_correct = is_correct
participant.save() participant.save()
return True return True
except (QuizGameParticipant.DoesNotExist, IndexError): except (QuizGameParticipant.DoesNotExist, IndexError):
return False 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 @database_sync_to_async
def get_answer_stats(self): def get_answer_stats(self):
try: try:
quiz_game = QuizGame.objects.get(join_code=self.join_code) 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:
# Count answers for each option in the current question
stats = {}
answers = QuizAnswer.objects.filter(
participant__quiz_game=quiz_game,
question_index=quiz_game.current_question_index
)
for answer in answers: if question_type == "order":
if answer.answer_index >= 0: # Ignore timeouts (-1) correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])]
stats[answer.answer_index] = stats.get(answer.answer_index, 0) + 1
if single_answer.answers_order== correct_answer:
return stats key = 0 # richtig
except QuizGame.DoesNotExist: else:
return {} 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 @database_sync_to_async
def save_rating(self, participant_id, rating): 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 from library.models import QuizRating
try: try:
game = QuizGame.objects.get(join_code=self.join_code) game = QuizGame.objects.get(join_code=self.join_code)
@@ -350,15 +486,22 @@ class GameConsumer(AsyncWebsocketConsumer):
) )
# Update or create rating # Update or create rating
"""
rating_obj, created = QuizRating.objects.get_or_create( rating_obj, created = QuizRating.objects.get_or_create(
quiz=game.quiz_id, quiz=game.quiz_id,
participant_id=participant_id, participant_id=participant_id,
defaults={'rating': rating} defaults={'rating': rating}
) )
"""
rating_obj=QuizRating.objects.create(
quiz=game.quiz_id,
participant_id=participant_id,
rating=rating)
if not created: rating_obj.rating = rating
rating_obj.rating = rating rating_obj.save()
rating_obj.save()
return True return True
except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist): except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist):
@@ -368,21 +511,17 @@ class GameConsumer(AsyncWebsocketConsumer):
def cleanup_game(self): def cleanup_game(self):
try: try:
game = QuizGame.objects.get(join_code=self.join_code) game = QuizGame.objects.get(join_code=self.join_code)
# Get all active participants (heartbeat within last minute)
from django.utils import timezone
active_participants = QuizGameParticipant.objects.filter( active_participants = QuizGameParticipant.objects.filter(
quiz_game=game, quiz_game=game
last_heartbeat__gte=timezone.now() - timezone.timedelta(minutes=1)
).count() ).count()
# If no active participants and game is finished for more than 5 minutes, delete it if active_participants == 0:
if active_participants == 0 and game.current_state == 'finished': game.delete()
if game.question_start_time and (timezone.now() - game.question_start_time).total_seconds() > 300:
game.delete()
except QuizGame.DoesNotExist: except QuizGame.DoesNotExist:
pass pass
@database_sync_to_async @database_sync_to_async
def get_participants(self): def get_participants(self):
try: try:
@@ -444,6 +583,26 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code}) '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 @database_sync_to_async
def _show_scores(self): def _show_scores(self):
@@ -467,11 +626,35 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code}) '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 @database_sync_to_async
def _finish_game(self): def _finish_game(self):
try: try:
quiz_game = QuizGame.objects.get(join_code=self.join_code) quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished' quiz_game.current_state = 'finished'
quiz_game.save() quiz_game.save()
return True return True
@@ -480,6 +663,7 @@ class GameConsumer(AsyncWebsocketConsumer):
async def finish_game(self): async def finish_game(self):
success = await self._finish_game() success = await self._finish_game()
await self.counter_played_quizzes()
if success: if success:
# Notify clients to redirect to finished page # Notify clients to redirect to finished page
await self.channel_layer.group_send( await self.channel_layer.group_send(
@@ -490,3 +674,48 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code}) '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

@@ -3,53 +3,15 @@ import json
import asyncio import asyncio
from ..models import QuizGame, QuizGameParticipant from ..models import QuizGame, QuizGameParticipant
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
import redis
# Dictionary to track inactive check tasks and active connections per game # Dictionary to track inactive check tasks and active connections per game
game_check_tasks = {} game_check_tasks = {}
game_connections = {} game_connections = {}
class LobbyConsumer(AsyncWebsocketConsumer): class LobbyConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
room_group_name = f'lobby_{join_code}'
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds to match frontend heartbeat
# Get participants list and broadcast update
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
# Get current active participants with full info
active_participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time)
.values('participant_id', 'display_name')
))()
# Send update to all clients
await channel_layer.group_send(
room_group_name,
{
'type': 'participants_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players()) r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
async def connect(self): async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code'] self.join_code = self.scope['url_route']['kwargs']['join_code']
@@ -63,9 +25,6 @@ class LobbyConsumer(AsyncWebsocketConsumer):
game_connections[self.join_code] = set() game_connections[self.join_code] = set()
game_connections[self.join_code].add(self.channel_name) game_connections[self.join_code].add(self.channel_name)
# Start inactive check task if not already running
await self.start_inactive_check(self.join_code, self.channel_layer)
# Join room group # Join room group
await self.channel_layer.group_add( await self.channel_layer.group_add(
self.room_group_name, self.room_group_name,
@@ -107,11 +66,6 @@ class LobbyConsumer(AsyncWebsocketConsumer):
})) }))
return return
if message_type == 'heartbeat':
participant_id = data.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Don't update participants list after heartbeat - let the background task handle it
elif message_type in ['leave_game', 'kick_player']: elif message_type in ['leave_game', 'kick_player']:
participant_id = data.get('participant_id') participant_id = data.get('participant_id')
if participant_id: if participant_id:
@@ -180,36 +134,32 @@ class LobbyConsumer(AsyncWebsocketConsumer):
} }
) )
@database_sync_to_async
def update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
from django.utils import timezone
participant.last_heartbeat = timezone.now()
participant.save()
return True
except QuizGameParticipant.DoesNotExist:
return False
async def get_participants_list(self): async def get_participants_list(self):
try: try:
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code) game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code)
# Only return participants that have been seen in the last minute
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
participants = await database_sync_to_async(lambda: list( participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time) game.participants
.values('participant_id', 'display_name') .values('participant_id', 'display_name')
))() ))()
# Send join notification for new participants # Zustand laden
try: try:
prev_names = set(p['display_name'] for p in self.last_participants) if hasattr(self, 'last_participants') else set() 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) current_names = set(p['display_name'] for p in participants)
# Neue Teilnehmer bestimmen
new_participants = current_names - prev_names new_participants = current_names - prev_names
# Benachrichtigung für neue Teilnehmer versenden
for name in new_participants: for name in new_participants:
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.room_group_name, self.room_group_name,
@@ -218,9 +168,12 @@ class LobbyConsumer(AsyncWebsocketConsumer):
'player_name': name '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: except Exception as e:
print(f'Error sending join notifications: {e}') print(f"Error in send_join_notifications: {e}")
self.last_participants = participants self.last_participants = participants
return participants return participants
except QuizGame.DoesNotExist: except QuizGame.DoesNotExist:

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-21 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0010_alter_quizgameparticipant_last_heartbeat'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-22 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0011_quizgame_updated_at'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_score',
field=models.IntegerField(default=0, verbose_name='Letzte_Punkte'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.7 on 2025-07-18 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0012_quizgameparticipant_last_score'),
]
operations = [
migrations.AddField(
model_name='quizanswer',
name='answer_text',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='quizanswer',
name='answers_order',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='quizanswer',
name='answer_index',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-25 16:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0013_quizanswer_answer_text_quizanswer_answers_order_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet')], default='lobby', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-28 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0014_alter_quizgame_current_state'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet'), ('scoreboard', 'Scoreboard')], default='lobby', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-01 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0015_alter_quizgame_current_state'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='coordinate_answer',
field=models.CharField(blank=True, max_length=256, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.7 on 2025-11-14 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0016_quizgameparticipant_coordinate_answer'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-14 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0017_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-11-14 14:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('play', '0018_quizgame_single_player_or_multiple'),
]
operations = [
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Lernmodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
),
migrations.RemoveField(
model_name='quizgame',
name='single_player_or_multiple',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0019_remove_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -10,15 +10,19 @@ class QuizGame(models.Model):
('lobby', 'In Lobby'), ('lobby', 'In Lobby'),
('question', 'Frage läuft'), ('question', 'Frage läuft'),
('scores', 'Punkteübersicht'), ('scores', 'Punkteübersicht'),
('finished', 'Beendet') ('winner_podest','Siegerpodest'),
('finished', 'Beendet'),
('scoreboard', 'Scoreboard'),
] ]
single_player_or_multiple=models.CharField(max_length=20, blank=True)
host_id = models.CharField(max_length=200, unique=True) host_id = models.CharField(max_length=200, unique=True)
join_code = models.CharField(max_length=6, unique=True, blank=True) join_code = models.CharField(max_length=6, unique=True, blank=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE) quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE)
current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby') current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby')
current_question_index = models.IntegerField(default=0) current_question_index = models.IntegerField(default=0)
question_start_time = models.DateTimeField(null=True, blank=True) question_start_time = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.join_code: if not self.join_code:
@@ -42,9 +46,11 @@ class QuizGameParticipant(models.Model):
display_name = models.CharField(verbose_name="Anzeigename", max_length=15) display_name = models.CharField(verbose_name="Anzeigename", max_length=15)
quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants") quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants")
score = models.IntegerField(verbose_name="Punkte", default=0) score = models.IntegerField(verbose_name="Punkte", default=0)
last_score = models.IntegerField(verbose_name="Letzte_Punkte", default=0)
avatar = models.CharField(max_length=200, blank=True, default="") avatar = models.CharField(max_length=200, blank=True, default="")
last_heartbeat = models.DateTimeField(auto_now_add=True) last_heartbeat = models.DateTimeField(auto_now_add=True)
last_answer = models.IntegerField(null=True, blank=True) last_answer = models.IntegerField(null=True, blank=True)
coordinate_answer = models.CharField(null=True, blank=True,max_length=256)
last_answer_correct = models.BooleanField(null=True, blank=True) last_answer_correct = models.BooleanField(null=True, blank=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -61,7 +67,9 @@ class QuizGameParticipant(models.Model):
class QuizAnswer(models.Model): class QuizAnswer(models.Model):
participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers') participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers')
question_index = models.IntegerField() question_index = models.IntegerField()
answer_index = models.IntegerField() answer_index = models.IntegerField(null=True, blank=True) # für MC, TF
answer_text = models.TextField(null=True, blank=True) # für Input
answers_order = models.JSONField(null=True, blank=True) # NEU für "order"
is_correct = models.BooleanField() is_correct = models.BooleanField()
score = models.IntegerField() score = models.IntegerField()
time_remaining = models.IntegerField() time_remaining = models.IntegerField()

View File

@@ -12,6 +12,8 @@ urlpatterns = [
path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'), path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'),
path('game/<str:join_code>/scores', views.scores, name='scores'), path('game/<str:join_code>/scores', views.scores, name='scores'),
path('game/<str:join_code>/finished', views.finished, name='finished'), path('game/<str:join_code>/finished', views.finished, name='finished'),
path('game/<str:join_code>/winner_podest', views.winner_podest, name='winner_podest'),
path('game/<str:join_code>/scoreboard', views.scoreboard, name='scoreboard'),
path('select_mode/<str:join_code>', views.select_mode, name='select_mode'), path('select_mode/<str:join_code>', views.select_mode, name='select_mode'),
path('selected_mode/<str:join_code>', views.selected_mode, name='selected_mode'), path('selected_mode/<str:join_code>', views.selected_mode, name='selected_mode'),
] ]

View File

@@ -2,32 +2,52 @@ from django.utils import timezone
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404, reverse from django.shortcuts import render, redirect, get_object_or_404, reverse
from play.models import QuizGame, QuizGameParticipant from play.models import QuizGame, QuizGameParticipant
from library.models import QivipQuiz, QivipQuestion from library.models import QivipQuiz
from django.http import HttpResponseRedirect, HttpResponseNotFound from django.http import HttpResponseRedirect, HttpResponseNotFound
from django.contrib import messages from django.contrib import messages
from django.conf import settings
import json import json
import qrcode
import base64
from io import BytesIO
from django.shortcuts import render
# Create your views here. # Create your views here.
def lobby(request, join_code): def lobby(request, join_code):
relative_url = reverse("play:create_participant", kwargs={"join_code": join_code})
full_url = request.build_absolute_uri(relative_url)
qr = qrcode.make(full_url)
buffer = BytesIO()
qr.save(buffer, format="PNG")
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id) quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
context = { context = {
'quiz': quiz, 'quiz': quiz,
'quiz_game': quiz_game, 'quiz_game': quiz_game,
"qr_code_base64": img_base64,
} }
mode = request.GET.get('mode','default') mode = request.GET.get('mode','default')
request.session['mode'] = mode request.session['mode'] = mode
if "host_id" in request.COOKIES: if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id'] host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id: if host_id == quiz_game.host_id:
context['host_id'] = host_id context['host_id'] = host_id
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context) return render(request, 'play/lobby.html', context=context)
elif mode=="learn": elif mode=="learn":
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
if not "participant_id" in request.COOKIES: if not "participant_id" in request.COOKIES:
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
@@ -37,56 +57,62 @@ def lobby(request, join_code):
if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save() participant.save()
except QuizGameParticipant.DoesNotExist: except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
context['participant'] = participant context['participant'] = participant
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context) return render(request, 'play/lobby.html', context=context)
def select_mode(request,join_code): def select_mode(request,join_code):
return render(request, 'play/select_mode.html', {'join_code': join_code}) return render(request, 'play/select_mode.html', {'join_code': join_code})
def create_participant(request, join_code): def create_participant(request, join_code):
mode = request.GET.get('mode','default') mode = request.GET.get('mode','default')
request.session['mode'] = mode request.session['mode'] = mode
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if mode!="learn": if mode!="learn":
if request.method == 'GET' and "participant_id" in request.COOKIES: if "participant_id" in request.COOKIES:
# Prüfe ob der Teilnehmer noch existiert # Prüfe ob der Teilnehmer noch existiert
participant_id = request.COOKIES['participant_id'] participant_id = request.COOKIES['participant_id']
try: try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id) participant = QuizGameParticipant.objects.get(participant_id=participant_id)
if participant.quiz_game.join_code != join_code: if participant.quiz_game.join_code != join_code:
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game participant.score = 0 # Reset score when joining new game
participant.save() participant.last_score = 0
return render(request, 'play/initialize_participant.html', {'participant': participant, 'join_code': quiz_game.join_code}) participant.save()
except QuizGameParticipant.DoesNotExist: return redirect('play:lobby', join_code=join_code)
# Cookie löschen wenn Teilnehmer nicht mehr existiert except QuizGameParticipant.DoesNotExist:
response = HttpResponseRedirect(request.path) # Cookie löschen wenn Teilnehmer nicht mehr existiert
response.delete_cookie('participant_id') response = HttpResponseRedirect(request.path)
return response response.delete_cookie('participant_id')
if request.method == 'POST':
display_name = request.POST.get('display_name')
if not display_name:
return render(request, 'play/initialize_participant.html', {
'join_code': join_code,
'error': 'Bitte geben Sie einen Namen ein'
})
participant = QuizGameParticipant()
participant.display_name = display_name
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
response.set_cookie('participant_id', participant.participant_id, max_age=3600)
return response return response
return render(request, 'play/initialize_participant.html', {'join_code': join_code}) if request.method == 'POST':
display_name = request.POST.get('display_name')
if not display_name:
return render(request, 'play/initialize_participant.html', {
'join_code': join_code,
'error': 'Bitte geben Sie einen Namen ein'
})
participant = QuizGameParticipant()
participant.display_name = display_name
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.last_score = 0
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
response.set_cookie('participant_id', participant.participant_id, max_age=3600)
return response
return render(request, 'play/initialize_participant.html', {'join_code': join_code})
else: else:
messages.error(request, "Beitritt nicht möglich.") messages.error(request, "Beitritt nicht möglich.")
return render(request, 'play/join_game.html') return render(request, 'play/join_game.html')
@@ -121,25 +147,7 @@ def join_game(request):
messages.error(request, "Dieses Spiel existiert nicht.") messages.error(request, "Dieses Spiel existiert nicht.")
return render(request, 'play/join_game.html') return render(request, 'play/join_game.html')
def finished(request, join_code):
try:
game = QuizGame.objects.get(join_code=join_code)
participant = None
participant_id = request.session.get('participant_id')
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=game,
participant_id=participant_id
).first()
context = {
'join_code': join_code,
'is_host': str(game.host_id) == str(request.session.get('host_id')),
'participant_id': participant_id if participant else None
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
def scores(request, join_code): def scores(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -181,7 +189,51 @@ def scores(request, join_code):
'total_questions': len(questions) 'total_questions': len(questions)
}) })
def scoreboard(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'scoreboard':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/scoreboard.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
"""
def finished(request, join_code): def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state # Verify game state
@@ -213,6 +265,84 @@ def finished(request, join_code):
'is_host': is_host, 'is_host': is_host,
'participant_id': participant_id if participant else None 'participant_id': participant_id if participant else None
}) })
"""
def winner_podest(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'winner_podest':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/winner_podest.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if quiz_game.current_state != 'finished':
return redirect('play:lobby', join_code=join_code)
try:
game = QuizGame.objects.get(join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Get participant if exists
participant_id = request.COOKIES.get('participant_id')
participant = None
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
participant_id=participant_id
).first()
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
context = {
'join_code': join_code,
'is_host': is_host,
'participants': participants,
'participant_id': participant_id if participant else None,
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
def question(request, join_code): def question(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -289,14 +419,39 @@ def waiting_room(request, join_code):
def selected_mode(request, join_code): def selected_mode(request, join_code):
mode = request.GET.get('mode','default') mode = request.GET.get('mode','default')
request.session['mode'] = mode request.session['mode'] = mode
quiz_game = QuizGame.objects.get(join_code=join_code)
if mode=="learn":
quiz_game.single_player_or_multiple="single"
else:
quiz_game.single_player_or_multiple="multiple"
quiz_game.save()
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id) quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete() QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete()
# Hier werden die Fragen gemischt!
import random
questions = quiz_game.quiz_id.questions.all()
for question in questions:
try:
data = json.loads(question.data)
if (data.get('type') == 'order' or data.get('type') == 'multiple_choice') and 'options' in data:
random.shuffle(data['options'])
question.data = json.dumps(data)
question.save(update_fields=['data'])
except Exception:
pass
context = { context = {
'quiz': quiz, 'quiz': quiz,
'quiz_game': quiz_game, 'quiz_game': quiz_game,
@@ -316,6 +471,7 @@ def selected_mode(request, join_code):
participant.participant_id=host_id participant.participant_id=host_id
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Initialize score participant.score = 0 # Initialize score
participant.last_score = 0
participant.save() participant.save()
quiz_game.current_state = "question" quiz_game.current_state = "question"
quiz_game.question_start_time = timezone.now() quiz_game.question_start_time = timezone.now()
@@ -323,5 +479,5 @@ def selected_mode(request, join_code):
return redirect('play:question',join_code) return redirect('play:question',join_code)
return redirect('play:lobby', join_code=join_code)
return render(request, 'play/lobby.html', context=context)

View File

@@ -1,8 +1,17 @@
@import 'tailwindcss'; @import 'tailwindcss';
.select2-container {
@apply font-black!;
}
qp-container { qp-container {
@apply max-w-xl w-full mx-auto; @apply max-w-xl w-full mx-auto;
} }
.dark-form label {
@apply dark:text-white!;
}
nav div ul.qp-nav-list li { nav div ul.qp-nav-list li {
@apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200; @apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200;
@@ -17,7 +26,7 @@ a.qp-a-button-small {
.register input{@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black w-full;} .register input{@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black w-full;}
.input-group { .input-group {
@apply flex flex-col gap-4 max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg; @apply flex flex-col gap-4 max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg;
} }
.input-group form { .input-group form {
@@ -53,6 +62,34 @@ a.qp-a-button-small {
} }
.field-group form p {
@apply flex flex-col gap-2;
}
.field-group form p label {
@apply text-gray-900 font-semibold text-sm;
}
.field-group form p input {
@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p select {
@apply p-3 rounded-md bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p textarea {
@apply p-3 rounded-xl bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 min-h-[100px];
}
.field-group form p button {
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105
transition duration-200 font-black shadow-md hover:shadow-lg;
}
ul.messages li { ul.messages li {
@@ -121,4 +158,13 @@ div.qp-question-container {
div.qp-question-container img { div.qp-question-container img {
@apply max-w-[50vw] max-h-[40vh] mx-auto; @apply max-w-[50vw] max-h-[40vh] mx-auto;
}
div#mapquestion-container {
display: flex;
justify-content: center;
}
div#map {
@apply md:w-[600px] lg:w-[900px] h-[400px]
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

35
django/templates/404.html Normal file
View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-2xl mx-auto mt-20 p-6 bg-white shadow-xl rounded-2xl text-center dark:bg-transparent">
<div class="flex justify-center items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-10 h-10 text-red-600">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<h1 class=" text-4xl font-extrabold text-red-600">404 Fehler</h1>
</div>
<div class="my-6 flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-24 h-24 text-gray-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
</svg>
</div>
<h2 class="text-2xl font-semibold text-gray-800 mb-2 dark:text-white ">Seite nicht gefunden</h2>
<p class="text-lg text-gray-600 mb-6 dark:text-white">Ups, die angeforderte Seite wurde nicht gefunden. Überprüfen Sie die URL oder kehren Sie zur Startseite zurück.</p>
<div class="flex justify-center">
<a href="{% url 'homepage:home' %}"
class="bg-blue-600 hover:bg-blue-700 transition-colors text-white px-6 py-3 rounded-full text-lg font-semibold shadow-lg">
Zur Startseite
</a>
</div>
</div>
{% endblock %}

View File

@@ -3,19 +3,59 @@
{% block content %} {% block content %}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12 mx-auto px-10">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent! dark:text-white! ">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-full mb-4 flex-col items-center justify-between">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1>
<form action="{% url 'accounts:logout' %}" method="post"> <form action="{% url 'accounts:logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="bg-blue-600 text-white p-3 rounded-full font-black" type="submit">Abmelden</button> <button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 cursor-pointer" type="submit">Abmelden</button>
</form> </form>
<button class="text-center text-sm w-full mt-4 font-light text-blue-600"><a href="{% url 'accounts:password_change' %}">Passwort ändern</a></button>
<button class="text-center text-sm w-full mt-4 font-light text-red-600"><a href="{% url 'accounts:delete_account' %}">Account löschen</a></button>
<!-- Kritik eigener Quizze -->
{% if own_critisized_quizzes %}
<div class="border-2 border-red-600 p-6 rounded-xl shadow-inner bg-red-50 flex flex-col gap-4 items-start mt-8 dark:bg-transparent">
<div class="flex items-center gap-2 text-xl font-semibold text-red-800 dark:text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
Kritik
</div>
<p class="text-xs opacity-50 dark:text-white">
Diese Quizze wurden von anderen Nutzern kritisiert, da sie als fehlerhaft oder nicht angemessen
wahrgenommen wurden. Bitte überprüfen und verbessern Sie Ihre Quizze so schnell wie möglich.</p>
{% for own_critisized_quiz in own_critisized_quizzes %}
<a href="{% url 'library:detail_quiz' own_critisized_quiz.id %}"> <span class="font-bold text-blue-600 hover:underline">{{ own_critisized_quiz }}</span> </a>
{% endfor %}
</div>
{% endif %}
<div class="border-2 border-blue-600 p-6 rounded-xl shadow-inner bg-blue-50 flex flex-col gap-4 items-start mt-8 dark:bg-transparent">
<div class="flex items-center gap-2 text-xl font-semibold text-blue-800 dark:text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-yellow-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" />
</svg>
Erfolge
</div>
<ul class="list-disc pl-6 text-base text-gray-700 dark:text-white">
<li><b>{{ my_quizzes_number }}</b> Quizze erstellt</li>
<li><b>{{ my_questions_number }}</b> Fragen insgesamt erstellt</li>
</ul>
</div>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
<h2 class="text-2xl text-center p-4 font-black">Anmeldung</h2> <h2 class="text-2xl text-center p-4 font-black dark:text-white">Anmeldung</h2>
<form class="space-y-4" method="post"> <form class="space-y-4" method="post">
{% if form.errors %} {% if form.errors %}
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center"> <div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">
@@ -24,9 +24,10 @@
<div class="items-center w-full"> <div class="items-center w-full">
<input class=" w-full p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black" type="password" name="password" id="password" required placeholder="PASSWORT"> <input class=" w-full p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black" type="password" name="password" id="password" required placeholder="PASSWORT">
</div> </div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Anmelden</button> <button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 cursor-pointer" type="submit" class="login-button">Anmelden</button>
</form> </form>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:register' %}" class="register_link">Noch kein Konto?</a></button> <button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:register' %}" class="register_link">Noch kein Konto?</a></button>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:password_reset' %}" class="register_link">Passwort vergessen?</a></button>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -4,7 +4,7 @@
<div class="grid place-content-center h-120 mt-12 mb-12"> <div class="grid place-content-center h-120 mt-12 mb-12">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
<h2 class="text-2xl text-center p-4 font-black">Registrieren</h2> <h2 class="text-2xl text-center p-4 font-black dark:text-white">Registrieren</h2>
<form class=" space-y-4" method="post"> <form class=" space-y-4" method="post">
{% if form.errors %} {% if form.errors %}
@@ -37,7 +37,14 @@
{{ form.password2 }} {{ form.password2 }}
</div> </div>
<div class="register">
{{ form.email }}
</div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
<label class=''>Mit der Erstellung ihres Kontos stimmen Sie der <a class="text-blue-600" href="/privacy">Datenschutzerklärung </a> zu.</label>
</div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Registrieren</button> <button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Registrieren</button>
</form> </form>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:login' %}" class="register_link">Bereits ein Konto?</a></button> <button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:login' %}" class="register_link">Bereits ein Konto?</a></button>

View File

@@ -1,19 +1,26 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de" class="dark:bg-gray-900 h-full">
<head> <head>
<link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}"> <link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/t-style.css' %}"> <link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
<title>qivip</title> <title>qivip</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head> </head>
<body> <body class="flex flex-col min-h-screen">
<main class="grow">
{% include 'partials/_nav.html' %} {% include 'partials/_nav.html' %}
{% block content%}{% endblock %} {% block content%}{% endblock %}
{% block faqs%}{% endblock %}
</main>
{% include 'partials/_footer.html' %} {% include 'partials/_footer.html' %}
<!-- Toast Container --> <!-- Toast Container -->
<div id="toast-container" class="fixed top-4 right-4 z-50"></div> <div id="toast-container" class="fixed top-4 right-4 z-50"></div>
@@ -66,9 +73,12 @@
}, duration); }, duration);
} }
}; };
if (window.location.pathname !== "/library/") {
localStorage.setItem("meine_seite_scroll", 0);}
</script> </script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
<style>html {
overflow-y: scroll;
}</style>
</body> </body>
</html> </html>

View File

@@ -1,22 +0,0 @@
{% extends 'components/base.html' %}
{% load static %}
{% block title %}Antwort{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % -->
{% if user.is_authenticated %}
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">1</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-indigo-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">2</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-yellow-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">3</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">4</button>
<!-- TODO wieder einfügen, wenn Bedingung gestetzt -->
{% else %}
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">wahr</button>
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">falsch</button>
<!--TODO wieder einfügen, wenn Bedingung gestetzt plus -->
{% endif %}
{% endblock %}

View File

@@ -1,45 +0,0 @@
{% load static %}
<!doctype html>
<html class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body class="dark:bg-gray-900 h-full m-0">
<div style="height: calc(12vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.55rem,5vw,1.5rem)]" > Frage: Wie hoch ist der Eiffelturm insgesamt?
</div>
<div class=" dark:bg-gray-900 flex justify-end gap-2 " style="height: calc(12vh);">
<span class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto ml-0.75 text-white bg-blue-400 mb-6 h-12 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center">Zeit: 60s</span>
<div class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto mr-0.75 bg-gray-500 text-white h-12 mb-6 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center ">Deine Punkte: 60</div>
</div>
<div class="mt-3 flex justify-center -z-1 " style="height: calc(37vh);"> <img class="object-cover " src="{% static 'components/Ausgabe.png' %}" alt="Bild"> </div>
<div class="w-full grid grid-cols-2 p-2 h-full" style="height: calc(38vh);min-height: 50px;">
{% block content %}
{% endblock %}
</div>
<div class="absolute bottom-0 right-0">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
</span>
</div>
 </body>
</html>

View File

@@ -1,63 +0,0 @@
{% load static %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<!doctype html>
<html class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body class=" dark:bg-gray-900 w-full justify-center h-full m-0">
<div style="height: calc(10vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.75rem,5vw,1.5rem)]" >Ranking
</div>
<div class="m-4 flex justify-end font-bold">Frage 1/10</div>
<div class="text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(255, 208, 0),rgb(216, 151, 21))" class="text-white !important px-4 rounded-sm">1. Platz</div>
<div class="text-stone-900 ">6000 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold p-4 flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(102, 122, 122),rgb(194, 186, 186))" class="text-white !important px-4 rounded-sm">2. Platz</div>
<div class="text-stone-900 ">5000 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(211, 91, 17),rgb(219, 168, 109))" class="text-white !important px-4 rounded-sm">3. Platz</div>
<div class="text-stone-900 ">600 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] items-center flex justify-around font-bold border-blue-600 border-4 space-x-2 mt-2 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div>4. Platz</div>
<div>200 Punkte</div>
<div>Person XY</div>
</div>
<div class="absolute bottom-0 right-0">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
</span>
</div>
 </body>
</html>

View File

@@ -4,12 +4,84 @@
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4"> <div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
<div class="text-center"> <div class="text-center">
<h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2> <h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2>
<h3 class="mt-4 italic font-extralight sm:text-2xl text-md">Interaktives Lernen neu definiert.</h3> <h3 class="mt-4 italic font-light sm:text-4xl text-md bg-gradient-to-r from-pink-500 via-purple-600 to-blue-700 bg-clip-text text-transparent">weil Lernen auch Spaß <br> machen kann!</h3>
</div> </div>
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md"> <div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md dark:bg-gray-900 dark:border-0">
<a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a> <a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a>
<a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a> <a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a>
<a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a> <a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a>
</div> </div>
</div> </div>
{% endblock %}
{% block faqs %}
<style>
details[open] summary svg {
transform: rotate(180deg);
}
</style>
{% if descriptions %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Was kann qivip?</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Mit qivip kann man:
</p>
<div class="space-y-2 text-gray-700 dark:text-white ">
{% for description in descriptions %}
<p>
<li class="ml-4"><span class="font-bold">{{ description.title }}</span>&nbsp;{{ description.description }}</li>
</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if faqs %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">FAQs</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<div class="input-group !max-w-4xl tracking-wide dark:!bg-gray-900 dark:!text-white">
<h1 class="font-black text-blue-600 flex justify-between gap-2 lg:text-lg">
FAQs - häufig gestellte Fragen
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
</svg></h1>
</div>
{% for faq in faqs %}
<div class="input-group mt-2.5 !max-w-4xl hover:bg-gray-50 dark:!bg-gray-900 dark:!text-white ">
<details>
<summary class="flex justify-between cursor-pointer font-bold">{{ faq.question }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</summary>
<p class="text-blue-600">{{ faq.answer }}</p>
</details>
</div>
{% endfor %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content%} {% block content%}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white ">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-120">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Impressum</b></h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Impressum</b></h1>
@@ -12,7 +12,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent ">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
{% block content%} {% block content%}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12 ">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-120">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1>
@@ -12,7 +12,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:!bg-transparent ">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -3,15 +3,15 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-lg mx-auto mt-10"> <div class="max-w-lg mx-auto mt-10">
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
<div class="text-center"> <div class="text-center">
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg> </svg>
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2> <h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-white">
{% if object.quiz_id %} {% if object.quiz_id %}
Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
{% else %} {% else %}

View File

@@ -1,66 +1,88 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load static %}
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
<div class="container mx-auto px-6">
<div class="flex flex-wrap space-y-2 items-center justify-between mb-8 dark:text-white">
<div class="container mx-auto">
<h1 class="text-2xl mt-4 font-bold dark:text-white trucate">
{{ quiz.name }}
</h1>
<div class="container mx-auto px-4">
<div class="flex flex-wrap space-y-2 items-center justify-between mb-6">
<h1 class="text-2xl font-bold mr-4 ">{{ quiz.name }}</h1>
<div class=" flex flex-wrap space-x-2 ">
<div class="flex flex-wrap">
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</a>
{% endif %}
</div>
<div class="flex flex-wrap gap-2">
<a href="{% url 'library:copy_quiz' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" />
</svg>
Quiz kopieren
</a>
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% endif %}
</div>
</div>
</div>
{% if quiz.description %}
<p class="text-gray-600 mb-4">{{ quiz.description }}</p>
{% endif %} <div class="flex items-center justify-center lg:justify-start gap-4 mt-4 flex-wrap">
<a href="{% url 'library:copy_quiz' quiz.id %}"
class="inline-flex bg-green-100 text-green-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
duplizieren
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
</a>
{% if quiz.questions.count %}
<a href="{% url 'play:create_game' quiz.id %}"
class="inline-flex bg-blue-100 text-blue-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
spielen
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
</svg>
</a>
{% endif %}
{% if quiz.user_id == request.user %}
<!-- Edit -->
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="inline-flex bg-orange-100 text-orange-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
bearbeiten
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
<!-- Delete -->
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="inline-flex bg-red-100 text-red-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
löschen
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</a>
{% endif %}
</div>
</div>
</div>
<summary class="list-none">
<div class="flex items-center gap-3 mt-12">
<p class="text-lg font-bold text-blue-600 dark:text-white">Fragen des Quizzes</p>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</summary>
<details open>
<div class="flex justify-end mb-4"> <div class="flex justify-end mb-4">
{% if detail == True %} {% if detail == True %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false"> <a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false">
<div class="flex items-center mr-2"> <div class="flex items-center mr-2 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
</svg> </svg>
@@ -71,7 +93,7 @@
{% else %} {% else %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true"> <a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true">
<div class="flex items-center mr-2"> <div class="flex items-center mr-2 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
@@ -85,73 +107,217 @@
</div> </div>
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md">
<div class="border-b border-gray-200 p-4">
<div class="flex justify-between items-center flex-wrap"> <div class="bg-white rounded-xl shadow-md border-2 border-blue-600 dark:!bg-transparent dark:text-white">
<h2 class="text-xl font-semibold">Fragen</h2> <div class="border-b border-gray-200 p-4">
<div class="flex space-x-2 flex-wrap"> <div class="flex justify-between items-center flex-wrap">
{% if quiz.user_id == request.user %} <!-- Titel -->
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> <h2 class="text-xl font-semibold">
Multiple Choice Fragen
</a> {% for question in questions %}{% if forloop.last %}({{ forloop.counter }}) {% endif %}{% endfor %}
{% endif %} </h2>
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Wahr/Falsch <!-- Neue Frage Dropdown -->
</a> {% if quiz.user_id == request.user %}
{% endif %} <div class="relative inline-block text-left mt-2 sm:mt-0">
</div> <button type="button" class="inline-flex justify-center w-full rounded-md border border-blue-600 shadow-sm px-4 py-2 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" id="menu-button" aria-expanded="true" aria-haspopup="true">
</div> Neue Frage
<!-- Pfeil nach unten -->
<svg class="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.584l3.71-4.354a.75.75 0 011.08 1.04l-4.25 5a.75.75 0 01-1.08 0l-4.25-5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</button>
<!-- Dropdown Menu -->
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none hidden group" id="dropdown-menu">
<div class="py-1 flex flex-col" role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
<a href="{% url 'library:new_question' %}?type=input&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Eingabe</a>
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Multiple Choice</a>
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Wahr/Falsch</a>
<a href="{% url 'library:new_question' %}?type=order&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Reihenfolge</a>
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Schätzen</a>
<a href="{% url 'library:new_question' %}?type=map&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Karte</a>
</div>
</div> </div>
</div>
{% endif %}
</div>
</div>
<!-- Dropdown Toggle Script -->
<script>
const btn = document.getElementById('menu-button');
const menu = document.getElementById('dropdown-menu');
btn.addEventListener('click', () => {
menu.classList.toggle('hidden');
});
window.addEventListener('click', (e) => {
if (!btn.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
</script>
{% if questions %} {% if questions %}
<div class="divide-y divide-gray-200"> <div class="divide-y divide-gray-200" id="questions-list">
{% for question in questions %} {% for question in questions %}
<div class="w-full rounded-xl p-4 hover:bg-gray-50">
<div data-question-id="{{ question.id }}" class="question-item w-full rounded-xl p-4 hover:bg-gray-50 dark:hover:bg-[#2a2f3a] dark:text-white">
{% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %} {% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %}
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<p class="question-counter pr-4 ">{{ forloop.counter }}.</p>
<div class="flex-grow"> <div class="flex-grow">
<p class="font-medium">{{ question.data.question }}</p> <p class="font-medium">{{ question.data.question }}</p>
<div class="mt-1"> <div class="mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800{% else %}bg-purple-100 text-purple-800{% endif %}"> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'map' %}bg-orange-100 text-orange-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% else %}Wahr/Falsch{% endif %} {% if question.data.type == 'multiple_choice' %}Multiple Choice
{% elif question.data.type == 'input' %}Eingabe
{% elif question.data.type == 'map' %}Karte
{% elif question.data.type == 'true_false' %}Wahr/Falsch
{% elif question.data.type == 'order' %}Reihenfolge
{% else %}Schätzen
{% endif %}
</span> </span>
</div> </div>
<div class="mt-2"> <div class="mt-2">
{% if detail %} {% if detail %}
{% if question.data.type == 'multiple_choice' %} {% if question.data.type == 'multiple_choice' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 "> <ul class="space-y-1 ">
{% for option in question.data.options %} {% for option in question.data.options|dictsort:"order" %}
<li class="flex items-center text-sm">
<li class="flex items-center text-sm ">
{% if option.is_correct %} {% if option.is_correct %}
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> </svg>
<span class="font-medium text-green-700">{{ option.value }}</span> <span class="font-medium text-green-700">{{ option.value }}</span>
{% else %} {% else %}
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-gray-400 mr-1.5 dark:text-white " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
</svg> </svg>
<span class="text-gray-600">{{ option.value }}</span> <span class="text-gray-600 dark:text-white">{{ option.value }}</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %}
</ul>
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% elif question.data.type == 'map' and detail %}
<div class="mt-3">
<div class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">Zielort:</span>
<span id="target-location-{{ forloop.counter }}">
{% if question.data.target_location %}
{{ question.data.target_location }}
{% else %}
Kein Zielort festgelegt
{% endif %}
</span>
</div>
{% if question.data.target_location %}
<script>
// Format the coordinates to 4 decimal places
document.addEventListener('DOMContentLoaded', function() {
const targetEl = document.getElementById('target-location-{{ forloop.counter }}');
if (targetEl) {
const coords = targetEl.textContent.trim().split(',');
if (coords.length === 2) {
const lat = parseFloat(coords[0]).toFixed(4);
const lng = parseFloat(coords[1]).toFixed(4);
targetEl.textContent = `${lat}, ${lng}`;
}
}
});
</script>
{% endif %}
{% if question.data.tolerance_radius %}
<div class="text-sm text-gray-600 dark:text-gray-300 mt-1">
<span class="font-medium">Toleranzradius:</span> {{ question.data.tolerance_radius }} Meter
</div>
{% endif %}
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4" />
{% endif %}
</div>
{% elif question.data.type == 'order' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 ">
{% for option in question.data.options|dictsort:"order" %}
<li class="flex items-center text-sm">
{{ forloop.counter }}. &nbsp;
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% elif question.data.type == 'input' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 ">
{% for option in question.data.options %}
<li class="flex items-center text-sm">
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li>
{% endfor %}
</ul>
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% elif question.data.type == 'guess' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 ">
{% for option in question.data.options %}
<li class="flex items-center text-sm">
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li>
{% endfor %}
</ul>
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% else %}
<div class="w-full gap-4 md:flex justify-between ">
{% for option in question.data.options %} {% for option in question.data.options %}
{% if option.is_correct %} {% if option.is_correct %}
<p class="text-sm"> <p class="text-sm">
<span class="text-gray-500">Richtige Antwort:</span> <span class="text-gray-500 dark:text-white">Richtige Antwort:</span>
<span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span> <span class="ml-1 font-medium {% if option.value == 'Wahr' %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
</p> </p>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% endif %} {% endif %}
@@ -166,7 +332,7 @@
<a href=" {% url 'library:delete_question' question.id %}" <a href=" {% url 'library:delete_question' question.id %}"
class=" text-red-700 px-4 py-1 rounded hover:scale-110 "> class=" text-red-700 px-4 py-1 rounded hover:scale-110 ">
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="dark:text-white size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /> <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg> </svg>
{% endif %} {% endif %}
@@ -175,7 +341,7 @@
</div> </div>
</div> </div>
</div> </div>
</a>
{% endfor %} {% endfor %}
</div> </div>
@@ -190,29 +356,354 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</details>
<div class="flex justify-end">
{% if quiz.base_quiz %} <!-- Kritik zum Quiz -->
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="font-bold"> {{ quiz.user_id}}</span></p> <div class="max-w-full">
{% endif %} {% if quiz.criticism_quiz.all %}
<summary class="list-none">
<div class="flex items-center gap-3 mt-12">
<p class="text-lg font-bold text-red-800 dark:text-white"> Kritik zum Quiz</p>
<div class="h-0.5 flex-grow bg-gradient-to-r from-red-600 to-transparent rounded-full"></div>
</div>
</summary>
<details>
{% for critique in quiz.criticism_quiz.all %}
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-[#2a2f3a] shadow-sm">
{% if critique.id_question != None %}
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_question' critique.id_question %}" class="text-blue-600 font-bold hover:underline">{{critique.question_criticism}}</a>:
{% else %}
<span class="font-bold">{{critique.question_criticism}}</span>:
{% endif %}
{% endif %}
<span class="dark:text-white dark:bg-transparent">
{{ critique.criticism }}
</span>
<p class="text-sm italic dark:text-white dark:bg-transparent ">
Kritik von User {{ critique.user }}
</p>
<div class="flex justify-end">
<p class="text-xs italic dark:text-white dark:bg-transparent ">
{{ critique.creation_date }}
</p>
</div>
{% if quiz.user_id == request.user %}
<form method="POST" class="space-y-6" enctype="multipart/form-data" action="{% url 'library:delete_critique' critique.id quiz.id %}">
{% csrf_token %}
<div class="flex border-t-2 border-gray-200 mt-2">
<button type="submit" class=" mt-2
px-4 py-2 border border-transparent shadow-sm text-sm
font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 cursor-pointer
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Fehler von mir behoben!
</button>
</div> </div>
</form>
{% endif %}
<div class="flex justify-end"> </div>
{% if quiz.base_quiz %} {% endfor %}
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p> </details>
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end">
<summary class="list-none">
<div class="flex items-center gap-3 mt-12">
<p class="text-lg font-bold text-blue-600 dark:text-white">Informationen über das Quiz</p>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</summary>
<details>
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Quiz-Infos
</p>
<div class="flex">
{% if quiz.description %}
<p class="text-gray-600 dark:text-white">Beschreibung: {{ quiz.description }}</p>
{% endif %}
</div>
<div class="flex">
{% if quiz.base_quiz %}
<p class="text-gray-600 dark:text-white">Bearbeitung:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% else %}
<p class="text-gray-600 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% endif %}
</div>
<div class="flex">
{% if quiz.base_quiz %}
<p class="text-gray-600 dark:text-white ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p>
{% endif %}
</div>
<div class="flex">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<p class="text-gray-600 ">Autor:<span class="font-bold"> {{ quiz.base_quiz.user_id}}</span></p> <p class="text-gray-600 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p>
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end">
<div class="flex">
<p class="text-gray-600 dark:text-white">Quiz ID:<span class="font-bold"> {{ quiz.pk }}</span></p>
</div>
{% if quiz.reference_quizzes.all %}
<div class="flex">
<p class="text-gray-600 dark:text-white">Verweis auf andere Quizze:
<span class="font-bold">
{% for reference in quiz.reference_quizzes.all %}
<a href="{% url 'library:detail_quiz' reference.id %}">{{ reference.name}}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
{% endfor %}
</span></p>
</div>
{% endif %}
</div>
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<div class="flex flex-col max-w-full">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Credits
</p>
{% if quiz.credits %} {% if quiz.credits %}
<p class="text-gray-600 mb-2">Credits:<span class="font-bold"> {{ quiz.credits }}</span></p> <p class="text-blue-500 mb-2 w-full break-words dark:text-gray-400 ">
<span class="break-words italic">{{ quiz.credits }}</span>
<span class="font-normal break-words text-gray-600 dark:text-white">(Credits für das Quiz)</span>
</p>
{% else %}
<p class="text-gray-500 dark:text-gray-400 ">Keine Credits für das Quizformular vorhanden.</p>
{% endif %} {% endif %}
</div> </div>
<div class="flex flex-col max-w-full">
<ul class="space-y-2">
{% regroup questions by credits as grouped_credits %}
{% if grouped_credits|length == 1 and not grouped_credits.0.grouper %}
<li class="text-gray-500 dark:text-gray-400 ">Keine zusätzlichen Credits für die einzelnen Fragen vorhanden.</li>
{% else %}
{% for question in questions %}
{% if question.credits %}
<li class="text-blue-500 mb-2 w-full break-words break-words dark:text-gray-400 ">
<span class="italic">{{ question.credits }}</span>
<span class="font-normal break-words text-gray-600 dark:text-white">(Frage: {{ forloop.counter }})</span>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
</div> </div>
</div>
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Quiz-Statistik
</p>
<div class="space-y-2 text-gray-700 dark:text-white ">
<p>
<span class="font-semibold">{{ quiz.quiz_played_Lernmodus }}</span>
mal im Lernmodus gespielt
</p>
<p>
<span class="font-semibold">{{ quiz.quiz_played_Moderiermodus }}</span>
mal im Moderiermodus gespielt
<span class="text-sm text-gray-500 dark:text-gray-400">
(insg. {{ quiz.quiz_played_Moderiermodus_players }} Spieler)
</span>
</p>
<p class="text-xs opacity-50">
* zählt nur, wenn das Quiz vollständig beendet wird
</p>
</div>
</div>
</details>
{% if quiz.user_id != request.user %}
<summary class="list-none">
<div class="flex items-center gap-3 mt-12">
<p class="text-lg font-bold text-red-800 dark:text-white">Das Quiz kritisieren</p>
<div class="h-0.5 flex-grow bg-gradient-to-r from-red-600 to-transparent rounded-full"></div>
</div>
</summary>
<details>
{% if user.is_authenticated %}
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-red-800 dark:text-white dark:bg-transparent ">
Quiz kritisieren
</p>
<p class="text-xs opacity-50 mb-3 dark:text-white dark:bg-transparent">
Sie haben einen Fehler in dem Quiz entdeckt oder halten eine Frage für nicht angemessen? <br>
Dann weisen Sie den Autor gern darauf hin.
</p>
<select name="question_id" required
class="w-full rounded-lg border-gray-300 p-2 dark:bg-gray-800 dark:text-white">
<option value="">Kritik an einer bestimmten Frage oder am Quiz insgesamt?</option>
<option value="whole_quiz">
allgemeine Kritik am Quiz {{ quiz.name|truncatechars:60 }}
</option>
{% for question in questions %}
<option value="{{ question.id }}">
Frage {{ forloop.counter }}: {{ question.data.question|truncatechars:60 }}
</option>
{% endfor %}
</select>
<select name="critic" required
class="w-full rounded-lg border-gray-300 p-2 dark:bg-gray-800 dark:text-white mb-4">
<option value="">Was ist Ihre Kritik?</option>
<option value="content_error">
inhaltlicher Fehler
</option>
<option value="spelling_error">
Rechtschreibfehler oder Grammatikfehler
</option>
<option value="logic_error">
umständliche oder unsinnige Formulierung
</option>
<option value="inappropriate_error">
unangemessen
</option>
<option value="not_completed_error">
unvollständig
</option>
<option value="source_error">
Quellen unvollständig
</option>
<option value="quiz_error">
Quiz als "öffentliches Quiz" ungeeignet
</option>
</select>
<button type="submit" class="inline-flex items-center
px-4 py-2 border border-transparent shadow-sm text-sm
font-medium rounded-md text-white bg-red-600 hover:bg-red-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Abschicken
</button>
{% elif not user.is_authenticated %}
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-red-800 dark:text-white dark:bg-transparent ">
Quiz kritisieren
</p>
<div class="text-xs opacity-50 mb-3 dark:text-white dark:bg-transparent">
Sie haben einen Fehler in dem Quiz entdeckt oder halten eine Frage für nicht angemessen? <br>
Dann weisen Sie den Autor gern darauf hin.
Sie können nur Quizze anderer User kritisieren.
</div>
<a class="inline-flex items-center
px-4 py-2 border border-transparent shadow-sm text-sm
font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" href="{% url 'accounts:login' %}">Anmelden</a>
</div>
{% endif %}
{% endif %}
</div>
</form>
</details>
{% if quiz.user_id == request.user %}
<style>.sortable-drag {
position: fixed !important;
z-index: 99999 !important;
pointer-events: none;
}
</style>
<script>
Sortable.create(document.getElementById('questions-list'), {
animation: 300,
swapThreshold: 0.5,
delay: 100,
delayOnTouchOnly: true,
touchStartThreshold: 6,
forceFallback: true,
fallbackTolerance: 6,
fallbackOnBody: true,
onStart: () => {
if (navigator.vibrate) {
navigator.vibrate(75);
}
},
onEnd: () => {
const items = Array.from(document.querySelectorAll('#questions-list .question-item'));
// Counter im Browser updaten
items.forEach((el, i) => {
const counterEl = el.querySelector('.question-counter');
if (counterEl) counterEl.textContent = `${i+1}.`;
});
// Reihenfolge sammeln
const order = Array.from(document.querySelectorAll('#questions-list .question-item'))
.map((el, i) => ({id: el.dataset.questionId, position: i+1}));
// an Django senden
fetch("{% url 'library:reorder_questions' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({order})
});
}
});
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,20 +1,97 @@
{% extends 'base.html' %} {% extends 'base.html' %} {% block content %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{{ form.media }}
<style>
{% block content %}
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12"> .select2-container {
<form method="post" enctype="multipart/form-data"> width: 100% !important;
{% csrf_token %} min-width: 0; /* verhindert Überlaufen bei kleinen Screens */
{{ form.as_p }} box-sizing: border-box;
<div class="flex justify-center space-x-3"> }
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> /* Das Auswahlfeld */
Abbrechen .select2-selection {
</a> width: 100% !important;
<button type="submit" box-sizing: border-box;
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> }
Speichern
</button> /* Angezeigter Text im Auswahlfeld */
</div> .select2-selection__rendered {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select2-selection__choice {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
}
</style>
<input type="hidden" name="pk" value="{{ form.instance.pk }}" />
<div class="container mx-auto px-4 py-4">
<div
class="max-w-lg field-group mx-auto bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white"
>
<form
method="post"
enctype="multipart/form-data"
class="dark-form space-y-4"
>
{% csrf_token %} {{ form.as_p }} {% if form.instance.image %}
<img
src="{{ form.instance.image.image.url }}"
style="max-width: 100px"
alt="Bild der Frage"
/>
<label for="reset_image">
<input type="checkbox" name="reset_image" id="reset_image" />
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="quiz_image">Bild:</label>
<input
class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent"
type="file"
name="quiz_image"
id="quiz_image"
/>
</div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
<label class=""
>Mit der Erstellung von diesem Quiz stimmen Sie der
<a class="text-blue-600" href="/privacy">Datenschutzerklärung </a>
zu.</label
>
</div>
<div class="flex justify-center space-x-3">
<a
href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Abbrechen
</a>
<button
type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Speichern
</button>
</div>
</form> </form>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,145 @@
{% load static %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block head %}
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<button id="filterButton" class="dark:text-white dark:hover:bg-[#2a2f3a] p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
<span>Filter</span>
</button>
<div class="flex gap-2">
<a href="{% url link %}" class="dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div>
</div>
<div id="filterPanel" class="container mx-auto px-4 {% if request.GET.filter == '1' %}block{% else %}hidden{% endif %}">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200 dark:bg-transparent dark:text-white ">
<form action="{% url link %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<input type="hidden" name="filter" id="filter-input">
<div class="space-y-2">
<div class="flex gap-2">
<input type="checkbox" name="use_min_amount_questions" id="use_min_amount_questions" {% if form.use_min_amount_questions.value %}checked{% endif %}>
<label class="block text-sm font-medium text-gray-700 dark:text-white">minimale Anzahl an Fragen</label>
</div>
<input
type="range"
name="min_amount_questions"
id="id_min_amount_questions"
min="0"
max="100"
step="1"
value="{{ form.min_amount_questions.value|default_if_none:0 }}"
class="w-full accent-blue-600 dark:text-white"
oninput="document.getElementById('min_amount_value').textContent = this.value + ' Frage(n)'"
{% if not form.use_min_amount_questions.value %}disabled{% endif %}
>
<span id="min_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
{{ form.min_amount_questions.value|default_if_none:0 }} Frage(n)
</span>
<br>
<span class="text-xs text-gray-600 dark:text-white"> Quizze anderer User werden erst ab min. 1 Frage angezeigt.</span>
</div>
<div class="space-y-2">
<div class="flex gap-2">
<input type="checkbox" name="use_max_amount_questions" id="use_max_amount_questions" {% if form.use_max_amount_questions.value %}checked{% endif %}>
<label class="block text-sm font-medium text-gray-700 dark:text-white">maximale Anzahl an Fragen</label>
</div>
<input
type="range"
name="max_amount_questions"
id="id_max_amount_questions"
min="1"
max="100"
step="1"
value="{{ form.max_amount_questions.value|default_if_none:100 }}"
class="w-full accent-blue-600"
oninput="document.getElementById('max_amount_value').textContent = this.value+' Frage(n)'"
{% if not form.use_max_amount_questions.value %}disabled{% endif %}
>
<span id="max_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
{{ form.max_amount_questions.value|default_if_none:100 }} Frage(n)
</span>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-white">veröffentlicht von User</label>
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full dark:text-white" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-white">Kategorie</label>
{{ form.categories }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-white">Suche</label>
{{ form.search }}
</div>
<div class="md:col-span-3 flex justify-end">
<button
type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
</svg>
Filtern
</button>
</div>
</form>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const checkbox = document.getElementById('use_min_amount_questions');
const slider = document.getElementById('id_min_amount_questions');
const checkbox_max = document.getElementById('use_max_amount_questions');
const slider_max = document.getElementById('id_max_amount_questions');
checkbox.addEventListener('change', () => {
slider.disabled = !checkbox.checked;
});
checkbox_max.addEventListener('change', () => {
slider_max.disabled = !checkbox_max.checked;
});
});
</script>
{% endblock %}

View File

@@ -1,22 +1,9 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% load humanize %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'swiper/swiper-bundle.min.css' %}">
<style> <style>
.swiper {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.custom-outline { .custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0); -webkit-text-stroke: 0.2px rgb(0, 0, 0);
@@ -26,98 +13,83 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-4 flex justify-between items-center"> {% block head %}
<button id="filterButton" class="p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700"> {% include 'library/head.html' %}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {% endblock %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
<span>Filter</span>
</button>
<div class="flex gap-2">
<a href="{% url 'library:overview_quiz' %}" class="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div>
</div>
<div id="filterPanel" class="container mx-auto px-4"> <!-- Alle -->
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
<form method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
{{ form.min_amout_questions }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
{{ form.max_amout_questions }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
{{ form.user }}
</div>
<div class="md:col-span-3 flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
Filtern
</button>
</div>
</form>
</div>
</div>
<div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
{% if user.is_authenticated %} <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6"> </svg>
<div class="flex items-center gap-3"> <div class="gap-2 mx-2">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Eigene Quiz</h2> {% if user.is_authenticated %}
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>)
{% else %}
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
{% endif %}
</div>
{% if user.is_authenticated %}
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<p class="font-black underline">Alle ({{ total_all }})</p>
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
</div>
{% endif %}
<div class="flex items-center gap-3">
<h2 id="all-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Alle</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div> <div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div> </div>
</div> </div>
{% if all_quizzes %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in my_quizzes %} {% for quiz in all_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col"> <article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden flex flex-col">
<!-- Image Section (Top) --> <!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0"> <div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %} {% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center"> <div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild --> <!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center" <div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});"> style="background-image: url({{ quiz.image.image.url }});">
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div> <div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if quiz.id not in favorite_quiz_ids %} {% if quiz.id not in favorite_quiz_ids %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg> </svg>
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg> </svg>
@@ -125,18 +97,28 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div> </div>
<!-- Content Section (Bottom) --> <!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white" >
<!-- Title --> <!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2"> <h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a> <a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2> </h2>
<!-- Description --> <!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p> <p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid --> <!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm"> <div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
@@ -145,7 +127,7 @@
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div> </div>
<!-- Questions Count --> <!-- Questions Count -->
@@ -153,7 +135,7 @@
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span> <span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div> </div>
<!-- Status --> <!-- Status -->
@@ -161,11 +143,17 @@
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.status }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div> </div>
<!-- Rating --> <!-- Rating -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %} {% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span> <span class="text-yellow-500 text-base"></span>
@@ -173,26 +161,40 @@
<span class="text-gray-300 text-base"></span> <span class="text-gray-300 text-base"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span> <span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %}
</div> </div>
</div> </div>
<!--
{% if quiz.user_id %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{{ quiz.user_id.username }}
</span>
</div>
{% endif %}
-->
<!-- Zeit-->
{% if quiz.published_at != None %}
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div>
{% endif %}
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3"> <div class="flex justify-between items-center gap-2 mt-3 border-t pt-3" >
{% if quiz.questions.count != 0 %} {% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}" <a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2"> class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
@@ -207,7 +209,7 @@
{% endif %} {% endif %}
<div class="flex gap-1"> <div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 ">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
@@ -215,13 +217,13 @@
</a> </a>
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" <a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
</a> </a>
<a href="{% url 'library:delete_quiz' quiz.id %}" <a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
@@ -229,383 +231,16 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für eigene Quiz -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Favoriten</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in favorite_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
{% endif %}
{% if user.is_authenticated %}
{% if quiz.favorite == False %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</a>
{% endif %}
</div>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für eigene Quiz -->
{% if favorite_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if favorite_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Quiz von anderen Nutzern -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="other-quizzes" class="text-2xl font-bold text-gray-900">Quiz von anderen Nutzern</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if other_quizzes %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in other_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
{% endif %}
{% if user.is_authenticated %}
{% if quiz.id not in favorite_quiz_ids %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
</div>
</div>
</div> </div>
</article> </article>
{% endfor %} {% endfor %}
</div> </div>
<!-- Pagination für andere Quiz --> <!-- Pagination für andere Quiz -->
{% if other_quizzes.paginator.num_pages > 1 %} {% if all_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8"> <div class="flex justify-center space-x-2 mt-6 mb-8">
{% if other_quizzes.has_previous %} {% if all_quizzes.has_previous %}
<a href="?other_page={{ other_quizzes.previous_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}" <a href="?all_page={{ all_quizzes.previous_page_number }}&{{ querystring_all }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"> class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
@@ -613,10 +248,10 @@
</a> </a>
{% endif %} {% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ other_quizzes.number }} von {{ other_quizzes.paginator.num_pages }}</span> <span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span>
{% if other_quizzes.has_next %} {% if all_quizzes.has_next %}
<a href="?other_page={{ other_quizzes.next_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}" <a href="?all_page={{ all_quizzes.next_page_number }}&{{ querystring_all }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"> class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@@ -628,29 +263,56 @@
</div> </div>
{% else %} {% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center">Keine öffentlichen Quiz gefunden.</p> <p class="text-gray-600 text-center dark:text-white">Keine Quiz gefunden.</p>
</div> </div>
{% endif %} {% endif %}<script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('filterButton').addEventListener('click', function() {
document.getElementById('filterPanel').classList.toggle('show');
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
}); });
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
}); });
if (window.location.pathname === "/library/") {
window.addEventListener("beforeunload", function () {
localStorage.setItem("meine_seite_scroll", window.scrollY);
});
window.addEventListener("load", function () {
const scrollPos = localStorage.getItem("meine_seite_scroll");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
}
});
}
</script> </script>
{% endblock content %} {% endblock content %}

View File

@@ -0,0 +1,309 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %}
{% block head %}
{% include 'library/head.html' %}
{% endblock %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
<div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
<div class="gap-2 mx-2 ">
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
<p class="font-black underline">Meine Favoriten ({{ total_favorite }})</p>
</div>
{% endif %}
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Favoriten</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if request.user.is_authenticated and favorite_quizzes %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in favorite_quizzes %}
<article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col ">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0 ">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% if quiz.favorite == False %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% endif %}
{% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
<!--
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}-->
<!-- Zeit-->
{% if quiz.published_at != None %}
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span class="text-gray-800 text-sm dark:text-white "><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</a>
{% endif %}
</div>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für favorite Quizze -->
{% if favorite_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8 ">
{% if favorite_quizzes.has_previous %}
<a href="?favorite_page={{ favorite.previous_page_number }}&{{ querystring_favorite }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="dark:text-white px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
{% if favorite_quizzes.has_next %}
<a href="?favorite_page={{ favorite_quizzes.next_page_number }}&{{ querystring_favorite }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center dark:text-white">Keine favorisierten Quiz gefunden.</p>
</div>
{% endif %}<script>
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,306 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %}
{% block head %}
{% include 'library/head.html' %}
{% endblock %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
<div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
<div class="gap-2 mx-2">
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
<p class="font-black underline">Meine Quizze ({{ total_my }})</p>
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
</div>
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Quizze</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% endif %}
{% if request.user.is_authenticated and my_quizzes %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in my_quizzes %}
<article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% if quiz.id not in favorite_quiz_ids %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg>
</a>
</div>
{% endif %}
{% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
<!--
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
-->
<!-- Zeit-->
{% if quiz.published_at != None %}
<div class="flex items-center gap-2 ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</a>
{% endif %}
</div>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für Meine Quizze -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}&{{ querystring_my }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}&{{ querystring_my }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center dark:text-white">Keine eigenen Quiz gefunden.</p>
</div>
{% endif %}<script>
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,150 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Schätzfrage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1">
<textarea name="question" id="question" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea>
</div>
</div>
{% if image %}
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image">
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="question_image">Bild:</label>
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div>
<!-- Eingabe -->
{% if question and question.data.options %}
{% for option in question.data.options %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<input name="correct_answer" type="number" value="{{ question.data.correct_answer|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="min" type="number" value="{{ question.data.min|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="max" type="number" value="{{ question.data.max|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
<div class="flex space-x-4">
<input name="tolerance" type="number" value="{{ question.data.tolerance|default:'' }}" max="100" min="1" value="75"
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
</div>
{% endfor %}
{% else %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<input name="correct_answer" type="number"
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="min" type="number" value="0"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="max" type="number"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
<div class="flex space-x-4">
<input name="tolerance" type="number" max="100" min="1" value="75"
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
</div>
{% endif %}
<div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select>
</div>
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1">
<textarea name="credits" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
>{{ credits }}</textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Speichern
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Eingabe Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1">
<textarea name="question" id="question" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea>
</div>
</div>
{% if image %}
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image">
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="question_image">Bild:</label>
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div>
<!-- Eingabe -->
{% if question and question.data.options %}
{% for option in question.data.options %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<textarea name="correct_answer" rows="2"
class="dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>{{ option.value }}</textarea>
</div>
</div>
{% endfor %}
{% else %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<textarea name="correct_answer" rows="2"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea>
</div>
</div>
{% endif %}
<div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select>
</div>
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1">
<textarea name="credits" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
>{{ credits }}</textarea>
</div>
<div class="flex justify-end space-x-3">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% if question %}Speichern{% else %}Erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
{% endblock %}

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