diff --git a/README.md b/README.md index 07d2681..24c8a0a 100644 --- a/README.md +++ b/README.md @@ -169,4 +169,13 @@ 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 | \ No newline at end of file +| 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 \ No newline at end of file diff --git a/django/THIRD_PARTY_LICENSES.md b/django/THIRD_PARTY_LICENSES.md new file mode 100644 index 0000000..ec0c51f Binary files /dev/null and b/django/THIRD_PARTY_LICENSES.md differ diff --git a/django/THIRD_PARTY_LICENSES_PYTHON.md b/django/THIRD_PARTY_LICENSES_PYTHON.md new file mode 100644 index 0000000..5cd52c0 Binary files /dev/null and b/django/THIRD_PARTY_LICENSES_PYTHON.md differ diff --git a/django/library/urls.py b/django/library/urls.py index f9cc6bf..1d2c621 100644 --- a/django/library/urls.py +++ b/django/library/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ path('detail/copy//', views.copy_quiz, name='copy_quiz'), path('favorite_quiz//', views.favorite_quiz, name='favorite_quiz'), path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"), - path('quiz//pdf/', views.pdf_solution, name='quiz_solution'), - path('quiz//pdf/task', views.pdf_task, name='quiz_task'), + #path('quiz//pdf/', views.pdf_solution, name='quiz_solution'), + #path('quiz//pdf/task', views.pdf_task, name='quiz_task'), ] diff --git a/django/library/views.py b/django/library/views.py index 5fb7109..11fbfd0 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -40,7 +40,7 @@ from PIL import Image as PILImage, ImageOps from django.conf import settings import os from django.utils.html import escape - +""" def pdf_solution(request, pk): quiz = get_object_or_404(QivipQuiz, pk=pk) @@ -337,7 +337,7 @@ def pdf_task(request, pk): story.append(Paragraph(link_html, styles['Normal'])) doc.build(story) return response - +""" @@ -860,7 +860,7 @@ def new_question(request): messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.') return redirect('library:overview_quiz') - if question_type not in ['multiple_choice', 'true_false','input']: + if question_type not in ['multiple_choice', 'true_false','input','order']: base_url = reverse('library:new_question') return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}') @@ -889,7 +889,7 @@ def new_question(request): ] } - elif question_type == 'multiple_choice': + elif question_type == 'multiple_choice' : options = [] i = 1 # Limit to maximum 6 options @@ -912,7 +912,35 @@ def new_question(request): 'question': question_text, 'options': options } + + elif question_type == 'order' : + options = [] + i = 1 + # Limit to maximum 6 options + while request.POST.get(f'option_{i}') and i <= 6: + is_correct = request.POST.get(f'correct_{i}') == '1' + options.append({ + 'order': i, + 'value': request.POST.get(f'option_{i}'), + + }) + i += 1 + + + # Validate minimum 2 options + if len(options) < 2: + messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.') + return redirect(request.get_full_path()) + + json_data = { + 'type': question_type, + 'question': question_text, + 'options': options + } + + + question = QivipQuestion() question.data = json.dumps(json_data) question.quiz_id = quiz @@ -966,6 +994,17 @@ def new_question(request): ] } standard_time_per_question = 30 + + elif question_type == 'order': + question_data = { + 'type': question_type, + 'question': '', + 'options': [ + {'value': ''}, + {'value': ''} + ] + } + standard_time_per_question = 30 template_name = f'library/question/question_{question_type}.html' context = { @@ -1092,6 +1131,32 @@ def edit_question(request, pk): 'options': options } + elif question_type == 'order': + options = [] + i = 1 + # Limit to maximum 6 options + while request.POST.get(f'option_{i}') and i <= 6: + options.append({ + 'order': i, + 'value': request.POST.get(f'option_{i}') + }) + i += 1 + + # Validate minimum 2 options + if len(options) < 2: + messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.') + return redirect(request.get_full_path()) + + # Validate maximum 6 options + if len(options) > 6: + messages.error(request, 'Maximal 6 Optionen sind erlaubt.') + return redirect(request.get_full_path()) + + json_data = { + 'type': question_type, + 'question': question_text, + 'options': options + } question.data = json.dumps(json_data) question.time_per_question = request.POST.get('time_per_question', '30') try: diff --git a/django/package-lock.json b/django/package-lock.json index e6f467b..49e6127 100644 --- a/django/package-lock.json +++ b/django/package-lock.json @@ -10,7 +10,11 @@ "license": "ISC", "dependencies": { "@tailwindcss/cli": "^4.1.5", + "sortablejs": "^1.15.6", "tailwindcss": "^4.1.5" + }, + "devDependencies": { + "license-checker": "^25.0.1" } }, "node_modules/@parcel/watcher": { @@ -537,6 +541,55 @@ "node": ">= 10" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -548,6 +601,60 @@ "node": ">=8" } }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -559,6 +666,16 @@ "node": ">=0.10" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -571,6 +688,15 @@ "node": ">=10.13.0" } }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -582,11 +708,106 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -622,6 +843,33 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -859,6 +1107,39 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -867,11 +1148,101 @@ "node": ">=4" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -888,6 +1259,177 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==" + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tailwindcss": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz", @@ -911,6 +1453,37 @@ "engines": { "node": ">=8.0" } + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true } } } diff --git a/django/package.json b/django/package.json index 384896d..2eb651c 100644 --- a/django/package.json +++ b/django/package.json @@ -11,6 +11,10 @@ "description": "", "dependencies": { "@tailwindcss/cli": "^4.1.5", + "sortablejs": "^1.15.6", "tailwindcss": "^4.1.5" + }, + "devDependencies": { + "license-checker": "^25.0.1" } } diff --git a/django/play/consumers/game.py b/django/play/consumers/game.py index b29baa2..01b3abc 100644 --- a/django/play/consumers/game.py +++ b/django/play/consumers/game.py @@ -112,7 +112,6 @@ class GameConsumer(AsyncWebsocketConsumer): await self.send(text_data=json.dumps({ 'type': 'answer_stats', 'stats': stats, - 'answer': answer, })) elif message_type == 'update_participants': @@ -175,93 +174,122 @@ class GameConsumer(AsyncWebsocketConsumer): return False @database_sync_to_async - def save_answer(self, participant_id, answer_index, time_remaining): - try: - participant = QuizGameParticipant.objects.get( - quiz_game__join_code=self.join_code, - participant_id=participant_id - ) - quiz_game = participant.quiz_game - current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index] - - # Check if answer is correct - is_correct = False - question_data = json.loads(current_question.data) - print(answer_index) - try: - if answer_index >= 0 : # -1 means timeout - is_correct = question_data['options'][answer_index]['is_correct'] - print(is_correct) - except: - user_input = str(answer_index).strip().lower().replace(" ", "") - correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "") - print("Richtig",correct_value) - print(user_input) - if user_input == correct_value: - is_correct=True - answer_index=int(0) - print(is_correct) - else: - answer_index=int(1) + 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) - # Calculate score based on correctness and time - score = 0 - if is_correct: - base_score = 1000 - time_factor = time_remaining / int(current_question.time_per_question)/int(1000) - score = int(base_score * (0.5 + 0.5 * time_factor)) - - # Update or create answer in QuizAnswer table - answer_obj, created = QuizAnswer.objects.get_or_create( - participant=participant, - question_index=quiz_game.current_question_index, - defaults={ - 'answer_index': answer_index, - 'is_correct': is_correct, - 'score': score, - 'time_remaining': time_remaining - } - ) - - if not created: - answer_obj.answer_index = answer_index - answer_obj.is_correct = is_correct - answer_obj.score = score - answer_obj.time_remaining = time_remaining - answer_obj.save() - - participant.last_score = score - participant.score += score - participant.last_answer_correct = is_correct - participant.save() - - return True - except (QuizGameParticipant.DoesNotExist, IndexError): - return False + + # Score berechnen + score = 0 + if is_correct: + base_score = 1000 + time_factor = time_remaining / int(current_question.time_per_question) / 1000 + score = int(base_score * (0.5 + 0.5 * time_factor)) + + # Antwort speichern oder aktualisieren + answer_obj, created = QuizAnswer.objects.get_or_create( + participant=participant, + question_index=quiz_game.current_question_index, + defaults={ + 'answer_index': answer_index, + 'answers_order': answers_order, + 'is_correct': is_correct, + 'score': score, + 'time_remaining': time_remaining + } + ) + if not created: + answer_obj.answer_index = answer_index + answer_obj.answers_order = answers_order + answer_obj.is_correct = is_correct + answer_obj.score = score + answer_obj.time_remaining = time_remaining + answer_obj.save() + + # Teilnehmer aktualisieren + participant.last_score = score + participant.score += score + participant.last_answer=answer_index + participant.last_answer_correct = is_correct + participant.save() + + return True + except (QuizGameParticipant.DoesNotExist, IndexError): + return False + @database_sync_to_async def get_answer_stats(self): - try: - quiz_game = QuizGame.objects.get(join_code=self.join_code) + try: + quiz_game = QuizGame.objects.get(join_code=self.join_code) + answers = QuizAnswer.objects.filter( + participant__quiz_game=quiz_game, + question_index=quiz_game.current_question_index + ) + + + + + current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index] + + question_data = json.loads(current_question.data) + question_type = question_data.get("type") + stats = {} + + for single_answer in answers: - # 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 answer.answer_index >= 0: # Ignore timeouts (-1) - stats[answer.answer_index] = stats.get(answer.answer_index, 0) + 1 - - return stats - except QuizGame.DoesNotExist: - return {} + if question_type == "order": + correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])] + + if single_answer.answers_order== correct_answer: + key = 0 # richtig + else: + key = 1 # falsch + stats[key] = stats.get(key, 0) + 1 + else: + key = str(single_answer.answer_index if single_answer.answer_index is not None else "unanswered") + stats[key] = stats.get(key, 0) + 1 + return stats + except: + pass + + + + @database_sync_to_async def save_rating(self, participant_id, rating): diff --git a/django/play/migrations/0013_quizanswer_answer_text_quizanswer_answers_order_and_more.py b/django/play/migrations/0013_quizanswer_answer_text_quizanswer_answers_order_and_more.py new file mode 100644 index 0000000..f25a490 --- /dev/null +++ b/django/play/migrations/0013_quizanswer_answer_text_quizanswer_answers_order_and_more.py @@ -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), + ), + ] diff --git a/django/play/models.py b/django/play/models.py index 98a381d..36d35e7 100644 --- a/django/play/models.py +++ b/django/play/models.py @@ -63,7 +63,9 @@ class QuizGameParticipant(models.Model): class QuizAnswer(models.Model): participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers') 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() score = models.IntegerField() time_remaining = models.IntegerField() diff --git a/django/play/views.py b/django/play/views.py index 025b6d6..9090044 100644 --- a/django/play/views.py +++ b/django/play/views.py @@ -339,13 +339,28 @@ def waiting_room(request, join_code): def selected_mode(request, join_code): - + + mode = request.GET.get('mode','default') request.session['mode'] = mode quiz_game = get_object_or_404(QuizGame, join_code=join_code) 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 = { 'quiz': quiz, 'quiz_game': quiz_game, diff --git a/django/static/js/sortablejs/Sortable.min.js b/django/static/js/sortablejs/Sortable.min.js new file mode 100644 index 0000000..95423a6 --- /dev/null +++ b/django/static/js/sortablejs/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY Eingabe - {% endif %} + - {% if quiz.user_id == request.user %} + Multiple Choice - {% endif %} - {% if quiz.user_id == request.user %} + Wahr/Falsch + + + Reihenfolge + {% endif %} @@ -131,8 +135,8 @@

{{ question.data.question }}

- - {% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% else %}Wahr/Falsch{% endif %} + + {% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% elif question.data.type == 'true_false' %}Wahr/Falsch{% else %}Reihenfolge{% endif %}
@@ -141,7 +145,7 @@ {% if question.data.type == 'multiple_choice' %}
    - {% for option in question.data.options %} + {% for option in question.data.options|dictsort:"order" %}
  • @@ -164,6 +168,21 @@ Bild der Frage {% endif %}
+ {% elif question.data.type == 'order' %} +
+
    + {% for option in question.data.options|dictsort:"order" %} +
  • + {{ forloop.counter }}.   + {{ option.value }} +
  • + {% endfor %} +
+ {% if question.image.image.url %} + Bild der Frage + {% endif %} +
+ {% elif question.data.type == 'input' %}
@@ -253,12 +272,13 @@ {% endif %}
+ + - - - {% endif %} + -->

Credits:

{% if quiz.credits %} diff --git a/django/templates/library/question/question_multiple_choice.html b/django/templates/library/question/question_multiple_choice.html index 08b5c4f..84e8838 100644 --- a/django/templates/library/question/question_multiple_choice.html +++ b/django/templates/library/question/question_multiple_choice.html @@ -41,7 +41,7 @@
{% if question and question.data.options %} - {% for option in question.data.options %} + {% for option in question.data.options|dictsort:"order" %}
+
+
+ + + {% if image %} + Bild der Frage + + {% endif %} +
+ + +
+ + +
+ +
+ {% if question and question.data.options %} + {% for option in question.data.options|dictsort:"order" %} +
+ {{ forloop.counter }}. + + + +
+ {% endfor %} + {% else %} + {% for i in "1234" %} +
+ {{ forloop.counter }}. + + + +
+ {% endfor %} + {% endif %} +
+ +
+ +
+ + +
+ +
+ +
+ +
+ + Abbrechen + + +
+ +
+
+ + +{% endblock %} diff --git a/django/templates/play/game/question_host.html b/django/templates/play/game/question_host.html index 7e42a98..02bd1d6 100644 --- a/django/templates/play/game/question_host.html +++ b/django/templates/play/game/question_host.html @@ -1,6 +1,8 @@ {% extends 'base.html' %} {% block content %} +{% load static %} +
@@ -48,7 +50,8 @@
- {% if question_data.type != "input" %} + {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %} +
{% for option in question_data.options %} - {% endfor %} + + {% endfor %}
+ + +{% elif question_data.type == "order" %} + +
+ {% for option in question_data.options %} + + + + + {% endfor %} +
+ +
+ {% if request.session.mode == "learn" %} + + {% endif %} + + + {% else %} {% if request.session.mode == "learn" %}
@@ -131,8 +196,12 @@ } else if (data.type === 'game_state_update' && data.action === 'show_scores' && data.redirect_url) { - window.location.href = data.redirect_url; - submitAnswer( -1 ); + + + setTimeout(() => { + submitAnswer( -1 ); + window.location.href = data.redirect_url; + }, 1000); } }; @@ -180,7 +249,7 @@ 'type': 'advance_to_scores', 'host_id': hostId })); - }, 2000); + }, 0); } // Timer @@ -199,6 +268,7 @@ if (remaining === 0) { advanceToScores(); + } else { requestAnimationFrame(updateTimer); } @@ -281,5 +351,74 @@ })); } } +let initialOrder = []; + +document.addEventListener('DOMContentLoaded', () => { + const container = document.getElementById('sortable-options'); + if (container) { + initialOrder = Array.from(container.children).map(div => div.dataset.index); + + Sortable.create(container, { + animation: 150, + onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren + }); + + updateOrderNumbers(); // initial setzen + } +}); + +function updateOrderNumbers() { + const container = document.getElementById('sortable-options'); + const children = container.children; + for (let i = 0; i < children.length; i++) { + const label = children[i].querySelector('.order-number'); + if (label) { + label.textContent = `${i + 1}.`; + } + } +} + + +function submitOrderAnswer() { + const order_button=document.getElementById("send_order") + order_button.disabled=true; + order_button.classList.add('opacity-50'); + const orderedDivs = document.querySelectorAll('#sortable-options button'); + +const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value); + + + const now = Date.now(); + let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime)); + {% if request.session.mode == "learn" %} + timeRemaining = questionDuration; + {% endif %} + + gameSocket.send(JSON.stringify({ + type: 'submit_answer', + participant_id: '{{ host_id }}', + answer: selectedOrder, + time_remaining: timeRemaining + })); + + hasAnswered = true; +} + +function resetOrderSelection() { + const container = document.getElementById('sortable-options'); + const divs = Array.from(container.children); + const indexMap = {}; + divs.forEach(div => { + indexMap[div.dataset.index] = div; + }); + + // Sort children back to initial order + container.innerHTML = ''; + initialOrder.forEach(i => { + container.appendChild(indexMap[i]); + }); +} + + {% endblock %} \ No newline at end of file diff --git a/django/templates/play/game/question_participant.html b/django/templates/play/game/question_participant.html index ee38c23..fdfe94b 100644 --- a/django/templates/play/game/question_participant.html +++ b/django/templates/play/game/question_participant.html @@ -1,6 +1,10 @@ {% extends 'base.html' %} {% block content %} + +{% load static %} + +
- {% if question_data.type != "input" %} + {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
{% for option in question_data.options %} - + + + {% endfor %} +
+ +
+ + + + + + + + + {% else %} 0) { requestAnimationFrame(updateTimer); } @@ -176,5 +256,74 @@ // Start timer when page loads updateTimer(); + +let initialOrder = []; + +document.addEventListener('DOMContentLoaded', () => { + const container = document.getElementById('sortable-options'); + if (container) { + initialOrder = Array.from(container.children).map(div => div.dataset.index); + + Sortable.create(container, { + animation: 150, + onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren + }); + + updateOrderNumbers(); // initial setzen + } +}); + +function updateOrderNumbers() { + const container = document.getElementById('sortable-options'); + const children = container.children; + for (let i = 0; i < children.length; i++) { + const label = children[i].querySelector('.order-number'); + if (label) { + label.textContent = `${i + 1}.`; + } + } +} + + +function submitOrderAnswer() { + const order_button=document.getElementById("send_order") + order_button.disabled=true; + order_button.classList.add('opacity-50'); + const orderedDivs = document.querySelectorAll('#sortable-options button'); + +const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value); + + + const now = Date.now(); + let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime)); + {% if request.session.mode == "learn" %} + timeRemaining = questionDuration; + {% endif %} + + gameSocket.send(JSON.stringify({ + type: 'submit_answer', + participant_id: participantId, + answer: selectedOrder, + time_remaining: timeRemaining + })); + + hasAnswered = true; +} + +function resetOrderSelection() { + const container = document.getElementById('sortable-options'); + const divs = Array.from(container.children); + const indexMap = {}; + divs.forEach(div => { + indexMap[div.dataset.index] = div; + }); + + // Sort children back to initial order + container.innerHTML = ''; + initialOrder.forEach(i => { + container.appendChild(indexMap[i]); + }); +} + {% endblock %} \ No newline at end of file diff --git a/django/templates/play/game/score_overview.html b/django/templates/play/game/score_overview.html index a85ad84..f93d05c 100644 --- a/django/templates/play/game/score_overview.html +++ b/django/templates/play/game/score_overview.html @@ -4,11 +4,14 @@

Ergebnisse

- +
+

Aktuelle Frage

{{ question_data.question }}

+ {% if question_data.type != "order" %}
+ {% for option in question_data.options %}
@@ -22,6 +25,28 @@
{% endfor %}
+ {% else %} +
+
+
+

Richtige Reihenfolge

+
    + {% for option in question_data.options|dictsort:"order" %} +
  • {{ forloop.counter }}. {{ option.value }}
  • + {% endfor %} +
+

0 Antworten

+
+
+
+
+
+

Falsche Reihenfolge

+

0 Antworten

+
+
+ + {% endif %}
diff --git a/requirements.txt b/requirements.txt index 7c74c1d..91ff057 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ redis channels_redis python-dotenv # Production server safety qrcode~=7.4.2 -reportlab~=4.2.0 \ No newline at end of file +reportlab~=4.2.0 +pip-licenses ~= 5.0.0 \ No newline at end of file