├── logo.png ├── Pipfile.lock.license ├── logo.png.license ├── frontend ├── static │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico.license │ ├── apple-touch-icon.png │ ├── site.webmanifest.license │ ├── apple-touch-icon.png.license │ ├── favicon-16x16.png.license │ ├── favicon-32x32.png.license │ ├── android-chrome-192x192.png.license │ ├── android-chrome-512x512.png.license │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── .prettierrc.license ├── package.json.license ├── tsconfig.json.license ├── pnpm-lock.yaml.license ├── src │ ├── lib │ │ ├── assets │ │ │ ├── music │ │ │ │ ├── 1-128.mp3 │ │ │ │ ├── 1-128.mp3.license │ │ │ │ ├── 1_original.mp3 │ │ │ │ └── 1_original.mp3.license │ │ │ ├── landing │ │ │ │ ├── results.webp │ │ │ │ ├── results.webp.license │ │ │ │ ├── select.webp │ │ │ │ ├── select.webp.license │ │ │ │ ├── solution.webp.license │ │ │ │ ├── admin_overview.webp.license │ │ │ │ ├── opengraph-home.jpg.license │ │ │ │ ├── opengraph-home.webp.license │ │ │ │ ├── overview-mockup.webp.license │ │ │ │ ├── results-mockup.webp.license │ │ │ │ ├── select-mockup.webp.license │ │ │ │ ├── solution-mockup.webp.license │ │ │ │ ├── solution.webp │ │ │ │ ├── import-select-mockup.webp.license │ │ │ │ ├── admin_overview.webp │ │ │ │ ├── opengraph-home.jpg │ │ │ │ ├── opengraph-home.webp │ │ │ │ ├── results-mockup.webp │ │ │ │ ├── select-mockup.webp │ │ │ │ ├── overview-mockup.webp │ │ │ │ ├── solution-mockup.webp │ │ │ │ ├── import-select-mockup.webp │ │ │ │ └── .gitignore │ │ │ └── landing_new │ │ │ │ ├── edit.webp.license │ │ │ │ ├── find.webp.license │ │ │ │ ├── import.webp.license │ │ │ │ ├── result.webp.license │ │ │ │ ├── select.webp.license │ │ │ │ ├── edit.webp │ │ │ │ ├── edit_org.webp.license │ │ │ │ ├── find.webp │ │ │ │ ├── find_org.webp.license │ │ │ │ ├── import_org.webp.license │ │ │ │ ├── result_org.webp.license │ │ │ │ ├── select_org.webp.license │ │ │ │ ├── solution.webp.license │ │ │ │ ├── winners.webp.license │ │ │ │ ├── winners_org.webp.license │ │ │ │ ├── edit_org.webp │ │ │ │ ├── find_org.webp │ │ │ │ ├── import.webp │ │ │ │ ├── result-admin.webp.license │ │ │ │ ├── result-admin_org.webp.license │ │ │ │ ├── result.webp │ │ │ │ ├── select.webp │ │ │ │ ├── solution.webp │ │ │ │ ├── solution_org.webp.license │ │ │ │ ├── winners.webp │ │ │ │ ├── import_org.webp │ │ │ │ ├── result_org.webp │ │ │ │ ├── select_org.webp │ │ │ │ ├── result-admin.webp │ │ │ │ ├── solution_org.webp │ │ │ │ ├── winners_org.webp │ │ │ │ └── result-admin_org.webp │ │ ├── i18n │ │ │ ├── locales │ │ │ │ ├── ar.json.license │ │ │ │ ├── ca.json.license │ │ │ │ ├── cs.json.license │ │ │ │ ├── de.json.license │ │ │ │ ├── en.json.license │ │ │ │ ├── es.json.license │ │ │ │ ├── et.json.license │ │ │ │ ├── eu.json.license │ │ │ │ ├── fr.json.license │ │ │ │ ├── he.json.license │ │ │ │ ├── hu.json.license │ │ │ │ ├── id.json.license │ │ │ │ ├── it.json.license │ │ │ │ ├── ja.json.license │ │ │ │ ├── je.json.license │ │ │ │ ├── lb.json.license │ │ │ │ ├── nl.json.license │ │ │ │ ├── nn.json.license │ │ │ │ ├── pl.json.license │ │ │ │ ├── pt.json.license │ │ │ │ ├── ro.json.license │ │ │ │ ├── ru.json.license │ │ │ │ ├── ta.json.license │ │ │ │ ├── tr.json.license │ │ │ │ ├── uk.json.license │ │ │ │ ├── uz.json.license │ │ │ │ ├── vi.json.license │ │ │ │ ├── nb_NO.json.license │ │ │ │ ├── pt_BR.json.license │ │ │ │ ├── zh_Hans.json.license │ │ │ │ ├── zh_Hant.json.license │ │ │ │ ├── et.json │ │ │ │ └── ro.json │ │ │ ├── translations.ts │ │ │ └── index.ts │ │ ├── quiztivity │ │ │ ├── components │ │ │ │ ├── pdf │ │ │ │ │ └── edit.svelte │ │ │ │ ├── markdown │ │ │ │ │ ├── play.svelte │ │ │ │ │ └── edit.svelte │ │ │ │ └── abcd │ │ │ │ │ └── play.svelte │ │ │ └── types.ts │ │ ├── stores.svelte.ts │ │ ├── socket.ts │ │ ├── components │ │ │ ├── popover │ │ │ │ ├── smalltop.ts │ │ │ │ └── commandpalettenotice.svelte │ │ │ ├── controller │ │ │ │ └── code.svelte │ │ │ ├── buttons │ │ │ │ ├── gray.svelte │ │ │ │ └── brown.svelte │ │ │ └── DownloadQuiz.svelte │ │ ├── editor │ │ │ ├── checker-bg.svg │ │ │ └── slides │ │ │ │ ├── icons │ │ │ │ └── x-circle.svg │ │ │ │ ├── types │ │ │ │ ├── text.svelte │ │ │ │ ├── headline.svelte │ │ │ │ ├── rectangle.svelte │ │ │ │ └── circle.svelte │ │ │ │ └── settings_menu.svelte │ │ ├── stores.ts │ │ ├── config.ts │ │ ├── play │ │ │ ├── kahoot_mode_assets │ │ │ │ ├── kahoot_icons.ts │ │ │ │ └── 0.svg │ │ │ ├── title.svelte │ │ │ ├── admin │ │ │ │ └── slide.svelte │ │ │ ├── circular_progress.svelte │ │ │ └── results_kahoot.svelte │ │ ├── view_quiz │ │ │ └── Hoverable.svelte │ │ ├── clickOutside.js │ │ ├── dashboard │ │ │ └── useViewportAction.js │ │ ├── files │ │ │ └── dashboard.svelte │ │ ├── Spinner.svelte │ │ ├── search-card.svelte │ │ └── admin.ts │ ├── routes │ │ ├── docs │ │ │ ├── +page.js │ │ │ ├── pow │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── tos │ │ │ │ └── +page.js │ │ │ ├── develop │ │ │ │ └── +page.js │ │ │ ├── roadmap │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── attribution │ │ │ │ └── +page.js │ │ │ ├── self-host │ │ │ │ └── +page.js │ │ │ ├── import-from-kahoot │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── privacy-policy │ │ │ │ └── +page.js │ │ │ ├── (markdown) │ │ │ │ ├── features │ │ │ │ │ ├── +page.svx │ │ │ │ │ ├── remote-control │ │ │ │ │ │ └── +page.svx │ │ │ │ │ └── custom-field │ │ │ │ │ │ └── +page.svx │ │ │ │ ├── quiz │ │ │ │ │ └── question-types │ │ │ │ │ │ ├── text │ │ │ │ │ │ └── +page.svx │ │ │ │ │ │ ├── multiple-choice │ │ │ │ │ │ └── +page.svx │ │ │ │ │ │ ├── voting │ │ │ │ │ │ └── +page.svx │ │ │ │ │ │ ├── range │ │ │ │ │ │ └── +page.svx │ │ │ │ │ │ ├── order │ │ │ │ │ │ └── +page.svx │ │ │ │ │ │ ├── +page.svx │ │ │ │ │ │ └── check-choice │ │ │ │ │ │ └── +page.svx │ │ │ │ ├── +layout.svelte │ │ │ │ └── classquizcontroller │ │ │ │ │ └── +page.svx │ │ │ ├── +layout.svelte │ │ │ └── +page.svelte │ │ ├── account │ │ │ ├── oauth-error │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ │ ├── password-reset │ │ │ │ └── +page.server.ts │ │ │ ├── controllers │ │ │ │ ├── [controller_id] │ │ │ │ │ ├── commons.ts │ │ │ │ │ ├── +page.ts │ │ │ │ │ └── SaveIndicator.svelte │ │ │ │ ├── add │ │ │ │ │ ├── +page.ts │ │ │ │ │ └── wait │ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ │ └── +page.svelte │ │ │ │ └── +page.ts │ │ │ ├── register │ │ │ │ └── +page.server.ts │ │ │ ├── reset-password │ │ │ │ └── +page.server.ts │ │ │ ├── settings │ │ │ │ └── +page.server.ts │ │ │ ├── +layout.svelte │ │ │ └── login │ │ │ │ ├── +page.server.ts │ │ │ │ └── verified_badge.svelte │ │ ├── dashboard │ │ │ ├── files │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ │ ├── +page.server.ts │ │ │ └── +page.ts │ │ ├── overview │ │ │ └── +server.ts │ │ ├── create │ │ │ └── +page.server.ts │ │ ├── edit │ │ │ ├── videos │ │ │ │ └── +page.server.ts │ │ │ ├── +page.server.ts │ │ │ └── files │ │ │ │ ├── +page.ts │ │ │ │ └── uploader.svelte │ │ ├── +page.server.ts │ │ ├── controller │ │ │ └── +page.svelte │ │ ├── play │ │ │ └── +page.server.ts │ │ ├── quiztivity │ │ │ ├── share │ │ │ │ └── [share_id] │ │ │ │ │ └── +page.server.ts │ │ │ ├── edit │ │ │ │ ├── +page.ts │ │ │ │ └── +page.svelte │ │ │ ├── play │ │ │ │ ├── +page.ts │ │ │ │ ├── +error.svelte │ │ │ │ ├── navigation_bar.svelte │ │ │ │ └── +page.svelte │ │ │ └── create │ │ │ │ └── +page.svelte │ │ ├── +layout.server.ts │ │ ├── +layout.ts │ │ ├── results │ │ │ ├── +page.ts │ │ │ └── [result_id] │ │ │ │ ├── +page.ts │ │ │ │ └── general_overview.svelte │ │ ├── import │ │ │ └── +page.server.ts │ │ ├── explore │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ │ ├── moderation │ │ │ └── +page.ts │ │ ├── admin │ │ │ └── +page.server.ts │ │ ├── view │ │ │ ├── [quiz_id] │ │ │ │ └── +page.server.ts │ │ │ └── +error.svelte │ │ ├── user │ │ │ └── [user_id] │ │ │ │ └── +page.ts │ │ └── +error.svelte │ ├── app.d.ts │ ├── env.d.ts │ ├── app.html │ ├── hooks.server.ts │ └── app.css ├── .prettierrc ├── .npmrc ├── pnpm-workspace.yaml ├── .gitignore ├── postcss.config.cjs ├── svelte.config.js ├── .eslintrc.cjs ├── vite.config.js ├── i18next-scanner.config.engine.cjs ├── README.md ├── tsconfig.json └── tailwind.config.cjs ├── .dockerignore ├── classquiz ├── routers │ ├── __init__.py │ ├── box_controller │ │ ├── __init__.py │ │ └── embedded_ws.md │ ├── testing_routes.py │ ├── remote.py │ ├── admin.py │ └── cqa-file-format.md ├── storage │ ├── errors.py │ └── local_storage.py ├── db │ ├── __init__.py │ └── quiztivity.py ├── helpers │ ├── box_controller.py │ └── avatar.py ├── tests │ ├── test_kahoot_import.py │ ├── test_kahoot_search.py │ └── test_auth.py ├── kahoot_importer │ ├── get.py │ ├── README.md │ └── search.py ├── worker │ └── __init__.py ├── socket_server │ ├── session.py │ └── export_helpers.py └── emails │ └── templates │ └── footer.jinja2 ├── .tokeignore ├── prestart.sh ├── pytest.ini ├── migrations ├── README ├── script.py.mako └── versions │ ├── 7afe98d04169_added_custom_openid.py │ ├── b2acaede5c2f_made_title_and_description_be_text_not_.py │ ├── 694cb11c6886_added_totp.py │ ├── 9d7fa2e6b24c_added_mod_rating.py │ ├── 97144a8cf6b6_added_github_user_id.py │ ├── 400f8ed06c48_added_background_color.py │ ├── 7ad8502af419_added_kahoot_id.py │ ├── 820e06ef2c2a_added_background_image.py │ ├── da778d551bf4_added_github_oauth.py │ ├── 6dc09ad6f6ef_imported_from_kahoot_field.py │ ├── cda6903dfc0c_added_cover_image.py │ ├── 3f63c0130bce_added_instance_data.py │ ├── ec6cf07ff68a_added_apikey.py │ ├── 4bbe1850b61a_added_1.py │ ├── 32649a1ffcf2_added_controllers_table.py │ ├── 901dfcdf8d38_added_backup_code.py │ ├── 25f2c34a69c8_added_webauthn.py │ ├── 8ac2bed1718e_added_quiztivityshares.py │ └── ff573859eb32_user_avatar.py ├── .github ├── FUNDING.yml ├── codecov.yml ├── ISSUE_TEMPLATE │ ├── Repo_architecture_changes.md │ ├── Feature_request.md │ └── bug_report.yml ├── workflows │ ├── frontend_lint.yml │ ├── backend_lint.yml │ └── pytest.yml └── PULL_REQUEST_TEMPLATE.md ├── pyproject.toml ├── migration_to_rust.md ├── .flake8 ├── .gitignore ├── .reuse └── dep5 ├── Caddyfile-docker ├── CONTACT.md ├── Caddyfile ├── .vscode └── settings.json ├── .deepsource.toml ├── Dockerfile ├── run_tests.sh ├── CONTRIBUTING.md ├── image_cleanup.py ├── LICENSES └── MIT.txt ├── import_to_meili.py ├── start.sh ├── .pre-commit-config.yaml ├── docker-compose.dev.yml └── Pipfile /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/logo.png -------------------------------------------------------------------------------- /Pipfile.lock.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /logo.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | .env* 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/tsconfig.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /classquiz/routers/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/pnpm-lock.yaml.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/static/site.webmanifest.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /.tokeignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | *.y*ml 6 | *.json 7 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/music/1-128.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/music/1-128.mp3 -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ar.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ca.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/cs.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/de.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/en.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/es.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/et.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/eu.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/fr.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/he.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/hu.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/id.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/it.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ja.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/je.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/lb.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/nl.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/nn.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/pl.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/pt.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ro.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ru.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ta.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/tr.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/uk.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/uz.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/vi.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/translations.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/apple-touch-icon.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/favicon-16x16.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/favicon-32x32.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/music/1-128.mp3.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/nb_NO.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/pt_BR.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/zh_Hans.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/zh_Hant.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/android-chrome-192x192.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/android-chrome-512x512.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /prestart.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | alembic upgrade head 6 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/results.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/results.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/results.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/select.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/select.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/select.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/solution.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/edit.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/find.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/import.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/select.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/music/1_original.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/music/1_original.mp3 -------------------------------------------------------------------------------- /frontend/src/lib/assets/music/1_original.mp3.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/admin_overview.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/opengraph-home.jpg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/opengraph-home.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/overview-mockup.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/results-mockup.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/select-mockup.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/solution-mockup.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/solution.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/solution.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/edit.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/edit.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/edit_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/find.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/find.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/find_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/import_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/select_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/solution.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/winners.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/winners_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | [pytest] 6 | asyncio_mode = auto 7 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/import-select-mockup.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/edit_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/edit_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/find_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/find_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/import.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/import.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result-admin.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result-admin_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/result.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/select.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/select.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/solution.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/solution.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/solution_org.webp.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | 3 | SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/winners.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/winners.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/admin_overview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/admin_overview.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/opengraph-home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/opengraph-home.jpg -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/opengraph-home.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/opengraph-home.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/results-mockup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/results-mockup.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/select-mockup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/select-mockup.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/import_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/import_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/result_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/select_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/select_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/overview-mockup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/overview-mockup.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/solution-mockup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/solution-mockup.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result-admin.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/result-admin.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/solution_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/solution_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/winners_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/winners_org.webp -------------------------------------------------------------------------------- /frontend/src/lib/quiztivity/components/pdf/edit.svelte: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | engine-strict=true 6 | strict-peer-dependencies=false 7 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/import-select-mockup.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing/import-select-mockup.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing_new/result-admin_org.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mawoka-myblock/ClassQuiz/HEAD/frontend/src/lib/assets/landing_new/result-admin_org.webp -------------------------------------------------------------------------------- /frontend/src/routes/docs/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/pow/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/tos/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | 6 | 7 | Generic single-database configuration. 8 | -------------------------------------------------------------------------------- /frontend/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | onlyBuiltDependencies: 5 | - svelte-preprocess 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/develop/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/roadmap/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | ko_fi: mawoka 5 | liberapay: Mawoka 6 | github: mawoka-myblock 7 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/attribution/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/self-host/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | [tool.black] 6 | line-length = 120 7 | target-version = ["py310"] 8 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/import-from-kahoot/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/privacy-policy/+page.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const prerender = true; 6 | -------------------------------------------------------------------------------- /migration_to_rust.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Migration of the Backend to Rust (long term goal) 9 | -------------------------------------------------------------------------------- /frontend/src/lib/stores.svelte.ts: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 3 | 4 | SPDX-License-Identifier: MPL-2.0 5 | */ 6 | export const navbarVisible = $state({ visible: true }); 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | [flake8] 5 | max-line-length = 120 6 | extend-ignore = E203 7 | exclude = ./migrations/versions/, 8 | -------------------------------------------------------------------------------- /frontend/src/lib/socket.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { io } from 'socket.io-client'; 6 | 7 | export const socket = io(); 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | .DS_Store 6 | node_modules 7 | /build 8 | /.svelte-kit 9 | /package 10 | .env 11 | .env.* 12 | !.env.example 13 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/landing/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | *.png 6 | *.xcf 7 | admin_overview.webp 8 | results.webp 9 | select.webp 10 | solution.webp 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | __pycache__/ 6 | .idea/ 7 | node_modules/ 8 | .svelte-kit/ 9 | .env* 10 | *.db 11 | *.rdb 12 | survey.json 13 | .coverage 14 | export_deta.py 15 | target/ 16 | -------------------------------------------------------------------------------- /frontend/src/lib/components/popover/smalltop.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /* eslint-disable no-unused-vars */ 6 | export enum PopoverTypes { 7 | Copy, 8 | GameInLobby, 9 | Generic 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/routes/account/oauth-error/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export function load({ url }) { 6 | const error = url.searchParams.get('error'); 7 | return { 8 | error 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/routes/account/password-reset/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export function load({ url }) { 6 | const token = url.searchParams.get('token'); 7 | 8 | return { 9 | token 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/features/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Features 8 | 9 | 10 | - [Custom-Field](/docs/features/custom-field) 11 | - [Remote-Control](/docs/features/remote-control) 12 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/checker-bg.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /classquiz/storage/errors.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | class DeletionFailedError(Exception): 7 | pass 8 | 9 | 10 | class SavingFailedError(Exception): 11 | pass 12 | 13 | 14 | class DownloadingFailedError(Exception): 15 | pass 16 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: ClassQuiz 3 | Upstream-Contact: Mawoka 4 | Source: https://classquiz.de 5 | 6 | # Sample paragraph, commented out: 7 | # 8 | # Files: src/* 9 | # Copyright: $YEAR $NAME <$CONTACT> 10 | # License: ... 11 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/[controller_id]/commons.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /* eslint-disable no-unused-vars */ 6 | export enum SaveStatus { 7 | Unchanged, 8 | Saved, 9 | Saving, 10 | Error 11 | } 12 | 13 | /* eslint-enable */ 14 | -------------------------------------------------------------------------------- /classquiz/db/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import databases 7 | import sqlalchemy 8 | 9 | from classquiz.config import settings 10 | 11 | settings = settings() 12 | 13 | database = databases.Database(settings.db_url) 14 | metadata = sqlalchemy.MetaData() 15 | -------------------------------------------------------------------------------- /frontend/src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { writable } from 'svelte/store'; 6 | 7 | export const signedIn = writable(false); 8 | export const pathname = writable('/'); 9 | 10 | export const alertModal = writable({ open: false, title: '', body: '' }); 11 | -------------------------------------------------------------------------------- /Caddyfile-docker: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | :8080 { 6 | reverse_proxy * http://frontend:3000 7 | reverse_proxy /api/* http://api:80 8 | reverse_proxy /openapi.json http://api:80 # Only use if you need to serve the OpenAPI spec 9 | reverse_proxy /socket.io/* api:80 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/routes/dashboard/files/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load = (async ({ fetch }) => { 8 | const res = await fetch('/api/v1/storage/list'); 9 | return { files: await res.json() }; 10 | }) satisfies PageLoad; 11 | -------------------------------------------------------------------------------- /CONTACT.md: -------------------------------------------------------------------------------- 1 | 6 | Welcome to the ClassQuiz community 7 | 8 | Grant and indulge critique constructively, within desired privacy. 9 | Settle disputes within these confines. 10 | Finding yourselves unable, e-mail hi@mawoka.eu answered by Marlon (Mawoka), the project maintainer. 11 | -------------------------------------------------------------------------------- /frontend/src/routes/account/register/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent }) { 8 | const { email } = await parent(); 9 | if (email) { 10 | redirect(302, '/dashboard'); 11 | } 12 | return {}; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, 6 | { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } 7 | ], 8 | "theme_color": "#ffffff", 9 | "background_color": "#ffffff", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/routes/account/reset-password/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent }) { 8 | const { email } = await parent(); 9 | if (email) { 10 | redirect(302, '/dashboard'); 11 | } 12 | return {}; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/routes/overview/+server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { RequestHandler } from '@sveltejs/kit'; 6 | 7 | export const GET: RequestHandler = () => { 8 | return new Response(undefined, { 9 | status: 301, 10 | headers: { 11 | Location: '/dashboard' 12 | } 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/routes/create/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent }) { 8 | const { email } = await parent(); 9 | if (!email) { 10 | redirect(302, '/account/login?returnTo=/create'); 11 | } 12 | return {}; 13 | } 14 | -------------------------------------------------------------------------------- /classquiz/routers/box_controller/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from fastapi import APIRouter 7 | from classquiz.routers.box_controller import web, embedded 8 | 9 | router = APIRouter() 10 | 11 | router.include_router(web.router, prefix="/web") 12 | router.include_router(embedded.router, prefix="/embedded") 13 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/text/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Text 8 | 9 | ## Summary 10 | - Players can enter text 11 | - 4 different correct solutions can be given 12 | - case sensitivity can be set per answer 13 | 14 | ## Use case 15 | - Test if a name is remembered correctly 16 | -------------------------------------------------------------------------------- /frontend/src/routes/dashboard/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export const load = async ({ parent }) => { 8 | const { email } = await parent(); 9 | if (!email) { 10 | redirect(302, '/account/login?returnTo=/dashboard'); 11 | } 12 | return { 13 | email 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | #* { 6 | :8080 { 7 | # tls /home/mawoka/certs/cert.pem /home/mawoka/certs/key.pem 8 | reverse_proxy /* localhost:3000 9 | reverse_proxy /api* localhost:8000 10 | reverse_proxy /rapidoc* localhost:8000 11 | reverse_proxy /openapi.json localhost:8000 12 | reverse_proxy /socket.io* localhost:8000 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/add/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load = (async ({ fetch }) => { 8 | const resp = await fetch('/api/v1/users/me'); 9 | const json = await resp.json(); 10 | return { 11 | username: json.username 12 | }; 13 | }) satisfies PageLoad; 14 | -------------------------------------------------------------------------------- /frontend/src/routes/edit/videos/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageServerLoad } from './$types'; 6 | 7 | export const load = (({ setHeaders }) => { 8 | setHeaders({ 9 | 'Cross-Origin-Embedder-Policy': 'require-corp', 10 | 'Cross-Origin-Opener-Policy': 'same-origin' 11 | }); 12 | }) satisfies PageServerLoad; 13 | -------------------------------------------------------------------------------- /frontend/src/routes/account/settings/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent }) { 8 | const { email } = await parent(); 9 | if (!email) { 10 | redirect(302, '/account/login?returnTo=/account/settings'); 11 | } 12 | return { 13 | email 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/multiple-choice/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Multiple Choice 8 | 9 | ## Summary 10 | - The standard question type everyone knows 11 | - 1-4 answers can be correct 12 | - User can only choose one answer 13 | 14 | ## Use case 15 | Nothing specific; Main component of a quiz 16 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/icons/x-circle.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/voting/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Voting 8 | 9 | ## Summary 10 | - Like [Multiple Choice](/docs/quiz/question-types/multiple-choice) 11 | - No points 12 | - No correct answers 13 | - Bar graph shows how many voted for what 14 | 15 | ## Use case 16 | - Get the opinion of the audience 17 | -------------------------------------------------------------------------------- /frontend/src/routes/account/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | {@render children?.()} 19 | -------------------------------------------------------------------------------- /frontend/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | import type { PageServerLoad } from './$types'; 7 | 8 | export const load: PageServerLoad = async ({ parent }) => { 9 | const { email } = await parent(); 10 | if (email) { 11 | redirect(302, '/dashboard'); 12 | } 13 | return { 14 | email 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/routes/controller/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

ClassQuizController

9 |
10 |

Play a quiz with a physical controller, not on a touchscreen!

11 |
12 |

More infos will follow soon

13 |
14 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/range/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Range Answers 8 | 9 | ## Summary 10 | - The player has a slider where a number can be set 11 | - The creator can choose the range and the correct range 12 | 13 | ## Use case 14 | - Guessing a year 15 | - Guessing the population of a country 16 | - Guessing in general 17 | -------------------------------------------------------------------------------- /frontend/src/routes/play/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { signedIn } from '$lib/stores'; 6 | 7 | export async function load({ url, parent }) { 8 | const { email } = await parent(); 9 | if (email) { 10 | signedIn.set(true); 11 | } 12 | const token = url.searchParams.get('pin'); 13 | return { 14 | game_pin: token === null ? '' : token 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/share/[share_id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | import type { PageServerLoad } from './$types'; 8 | 9 | export const load = (({ params }) => { 10 | const quiz_id = params.share_id; 11 | redirect(301, `/quiztivity/play?id=${quiz_id}&share=true`); 12 | }) satisfies PageServerLoad; 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | { 6 | "sqltools.connections": [ 7 | { 8 | "previewLimit": 50, 9 | "server": "localhost", 10 | "driver": "PostgreSQL", 11 | "connectString": "postgresql://postgres:mysecretpassword@localhost:5432/classquiz", 12 | "name": "ClassQuiz" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export const google_auth_enabled = import.meta.env.VITE_GOOGLE_AUTH_ENABLED === 'true'; 6 | export const github_auth_enabled = import.meta.env.VITE_GITHUB_AUTH_ENABLED === 'true'; 7 | export const captcha_enabled = import.meta.env.VITE_CAPTCHA_ENABLED === 'true'; 8 | export const custom_oauth_name = import.meta.env.VITE_CUSTOM_OAUTH_NAME; 9 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { signedIn } from '$lib/stores'; 6 | import type { LayoutServerLoad } from './$types'; 7 | 8 | export const load: LayoutServerLoad = ({ locals }) => { 9 | if (locals.email) { 10 | signedIn.set(true); 11 | } else { 12 | signedIn.set(false); 13 | } 14 | return { 15 | email: locals.email 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /// 6 | 7 | // See https://kit.svelte.dev/docs/types#app 8 | // for information about these interfaces 9 | // and what to do when importing types 10 | declare namespace App { 11 | interface Locals { 12 | email: string | null; 13 | } 14 | 15 | // interface Platform {} 16 | 17 | // interface Stuff {} 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { signedIn } from '$lib/stores'; 6 | import type { LayoutLoad } from './$types'; 7 | 8 | export const load: LayoutLoad = ({ data }) => { 9 | const { email } = data; 10 | if (email) { 11 | signedIn.set(true); 12 | // throw redirect(302, '/dashboard'); 13 | } else { 14 | signedIn.set(false); 15 | } 16 | return {}; 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/order/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Order 8 | 9 | ## Summary 10 | - Bring the answers in a correct order 11 | - The order gets randomized before the quiz so that every player has the same random order 12 | - Remember to give some time, since it's kinda slow to move the answers up and down 13 | 14 | ## Use case 15 | - Order historic events 16 | -------------------------------------------------------------------------------- /frontend/src/routes/results/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load: PageLoad = async ({ fetch }) => { 8 | const res = await fetch('/api/v1/results/list?include_quiz=true'); 9 | let json; 10 | if (res.ok) { 11 | json = await res.json(); 12 | } else { 13 | json = []; 14 | } 15 | return { 16 | results: json 17 | }; 18 | }; // satisfies PageLoad; 19 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/types/text.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | {#if editor} 17 | 18 | {:else} 19 |

{data}

20 | {/if} 21 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | codecov: 5 | require_ci_to_pass: false 6 | 7 | coverage: 8 | precision: 2 9 | round: down 10 | range: "50...100" 11 | 12 | parsers: 13 | gcov: 14 | branch_detection: 15 | conditional: yes 16 | loop: yes 17 | method: no 18 | macro: no 19 | 20 | comment: 21 | layout: "reach,diff,flags,files,footer" 22 | behavior: default 23 | require_changes: false 24 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load = (async ({ fetch }) => { 8 | const resp = await fetch('/api/v1/box-controller/web/list'); 9 | // const resp = await fetch("https://localhost/api/v1/box-controller/web/list") 10 | const controllers = await resp.json(); 11 | return { 12 | controllers 13 | }; 14 | }) satisfies PageLoad; 15 | -------------------------------------------------------------------------------- /frontend/src/routes/import/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | import { signedIn } from '$lib/stores'; 7 | export async function load({ parent }) { 8 | const { email } = await parent(); 9 | if (!email) { 10 | redirect(302, '/account/login?returnTo=/import'); 11 | } else { 12 | if (email) { 13 | signedIn.set(true); 14 | } 15 | } 16 | return { 17 | email 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/routes/results/[result_id]/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | import type { PageLoad } from './$types'; 5 | 6 | export const load: PageLoad = async ({ params, fetch }) => { 7 | const res = await fetch(`/api/v1/results/${params.result_id}?include_quiz=true`); 8 | let json; 9 | if (res.ok) { 10 | json = await res.json(); 11 | } else { 12 | json = undefined; 13 | } 14 | return { 15 | results: json 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Question Types 8 | 9 | - [Range](/docs/quiz/question-types/range) 10 | - [Multiple Choice](/docs/quiz/question-types/multiple-choice) 11 | - [Voting](/docs/quiz/question-types/voting) 12 | - [Text](/docs/quiz/question-types/text) 13 | - [Order](/docs/quiz/question-types/order) 14 | - [Check Choice](/docs/quiz/question-types/check-choice) 15 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | version = 1 5 | test_patterns = ["classquiz/tests/**", "test_*.py"] 6 | 7 | 8 | [[analyzers]] 9 | name = "python" 10 | enabled = true 11 | 12 | [analyzers.meta] 13 | runtime_version = "3.x.x" 14 | 15 | [[analyzers]] 16 | name = "javascript" 17 | enabled = true 18 | 19 | #[[analyzers]] 20 | #name = "test-coverage" 21 | #enabled = false 22 | 23 | [[transformers]] 24 | name = "black" 25 | enabled = true 26 | -------------------------------------------------------------------------------- /classquiz/routers/testing_routes.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from fastapi import APIRouter 7 | from classquiz.db.models import User 8 | from classquiz.config import settings 9 | 10 | settings = settings() 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/user/{email}") 16 | async def get_user_by_email(email: str, secret_key: str) -> User: 17 | if secret_key == settings.secret_key: 18 | return await User.objects.filter(email=email).get() 19 | -------------------------------------------------------------------------------- /frontend/src/lib/play/kahoot_mode_assets/kahoot_icons.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import KahootModeIcon0 from '$lib/play/kahoot_mode_assets/0.svg'; 6 | import KahootModeIcon1 from '$lib/play/kahoot_mode_assets/1.svg'; 7 | import KahootModeIcon2 from '$lib/play/kahoot_mode_assets/2.svg'; 8 | import KahootModeIcon3 from '$lib/play/kahoot_mode_assets/3.svg'; 9 | 10 | export const kahoot_icons = [KahootModeIcon0, KahootModeIcon1, KahootModeIcon2, KahootModeIcon3]; 11 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/quiz/question-types/check-choice/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Check Choice 8 | 9 | ## Summary 10 | - Interface similar to [Multiple Choice](/docs/quiz/question-types/multiple-choice) 11 | - Up to 4 answers 12 | - All answers marked as correct have to be selected 13 | - No points if only one correct one is selected 14 | 15 | ## Use case 16 | - Select all correct statements 17 | - Select all correct nicknames 18 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/features/remote-control/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | # Remote Control 9 | 10 | Control the admin-screen from a different device! 11 | 12 | ## Use Case 13 | 14 | Imagine mirroring the admin-screen to a projector and you want to show the quiz full-screen without the disruption of 15 | buttons, etc. That's where this feature comes in handy, since you can control the quiz from another device, e.g. your 16 | phone. 17 | -------------------------------------------------------------------------------- /frontend/src/routes/dashboard/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load = (async ({ fetch }) => { 8 | const quiz_res = await fetch('/api/v1/quiz/list?page_size=100'); 9 | const quizzes = await quiz_res.json(); 10 | const quiztivity_res = await fetch('/api/v1/quiztivity/'); 11 | const quiztivities = await quiztivity_res.json(); 12 | return { 13 | quizzes, 14 | quiztivities 15 | }; 16 | }) satisfies PageLoad; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Repo_architecture_changes.md: -------------------------------------------------------------------------------- 1 | 6 | --- 7 | name: Project refactoring 8 | about: All issues concerning CI/CD, improvements of documentation, development setup etc. 🔧 9 | 10 | --- 11 | 12 | **Describe the change and why it is needed** 13 | A clear and concise description of the improvement you're asking for. 14 | 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | 19 | Thanks for contributing to ClassQuiz! 20 | -------------------------------------------------------------------------------- /frontend/src/routes/explore/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load = (async ({ fetch }) => { 8 | const response = await fetch('/api/v1/search/', { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | }, 13 | body: JSON.stringify({ 14 | q: '*', 15 | sort: ['created_at:desc'] 16 | }) 17 | }); 18 | return { 19 | results: await response.json() 20 | }; 21 | }) satisfies PageLoad; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | 6 | --- 7 | name: Feature request 8 | about: Make a clear demand for improving ClassQuiz 💪 9 | 10 | --- 11 | 12 | **Describe the bug** 13 | A clear and concise description of the new feature you propose. 14 | 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your feature. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | 22 | Thanks for contributing to ClassQuiz! 23 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/add/wait/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { error } from '@sveltejs/kit'; 6 | import type { PageServerLoad } from './$types'; 7 | 8 | export const load = (({ url }) => { 9 | const code = url.searchParams.get('code'); 10 | const id = url.searchParams.get('id'); 11 | if (!id || !code) { 12 | error(404, JSON.stringify({ detail: 'id and/or code are/is missing' })); 13 | } 14 | return { 15 | id, 16 | code 17 | }; 18 | }) satisfies PageServerLoad; 19 | -------------------------------------------------------------------------------- /frontend/src/routes/account/login/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent, url }) { 8 | const verified = url.searchParams.get('verified'); 9 | const returnTo = 10 | url.searchParams.get('returnTo') !== null ? url.searchParams.get('returnTo') : '/dashboard'; 11 | 12 | const { email } = await parent(); 13 | if (email) { 14 | redirect(302, returnTo); 15 | } 16 | return { 17 | verified: verified !== null 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 |
20 | {@render children?.()} 21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /frontend/src/lib/view_quiz/Hoverable.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 |
25 | {@render children?.({ hovering, })} 26 |
27 | -------------------------------------------------------------------------------- /frontend/src/routes/moderation/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | import type { PageLoad } from './$types'; 5 | 6 | export const load = (async ({ fetch, url }) => { 7 | const page = url.searchParams.get('page') ?? '1'; 8 | const all = Boolean(url.searchParams.get('all')) ?? false; 9 | const resp = await fetch( 10 | `/api/v1/moderation/quizzes?page=${page}&all=${all ? 'true' : 'false'}` 11 | ); 12 | const quizzes = await resp.json(); 13 | return { 14 | page, 15 | all, 16 | quizzes 17 | }; 18 | }) satisfies PageLoad; 19 | -------------------------------------------------------------------------------- /classquiz/helpers/box_controller.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import random 7 | 8 | 9 | def generate_code(specified_length: int) -> str: 10 | buttons = [ 11 | "B", 12 | "b", 13 | "G", 14 | "g", 15 | "Y", 16 | "y", 17 | "R", 18 | "r", 19 | ] # Capital stands for long press, lowercase letter for short press 20 | resulting_code = "" 21 | for _ in range(specified_length): 22 | resulting_code += random.choice(buttons) 23 | return resulting_code 24 | -------------------------------------------------------------------------------- /classquiz/tests/test_kahoot_import.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import pytest 7 | 8 | from classquiz.kahoot_importer.import_quiz import _download_image 9 | 10 | ddg_robots_txt = b"""a""" 11 | test_url = ( 12 | "https://gist.githubusercontent.com/mawoka-myblock/b43f0d888a9e6a25806b3c73e63b658f/raw" 13 | "/134f135f99f8f385695304f739667c70b636386a/test-gist" 14 | ) 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_download_image(): 19 | image = await _download_image(test_url) 20 | assert image == ddg_robots_txt 21 | -------------------------------------------------------------------------------- /frontend/src/lib/clickOutside.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /** Dispatch event on click outside of node */ 6 | export function clickOutside(node) { 7 | const handleClick = (event) => { 8 | if (node && !node.contains(event.target) && !event.defaultPrevented) { 9 | node.dispatchEvent(new CustomEvent('click_outside', node)); 10 | } 11 | }; 12 | 13 | document.addEventListener('click', handleClick, true); 14 | 15 | return { 16 | destroy() { 17 | document.removeEventListener('click', handleClick, true); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/types/headline.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | {#if editor} 17 | 22 | {:else} 23 |

24 | {data} 25 |

26 | {/if} 27 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_page": { 3 | "slogan": "Avatud tarkvaral põhinev mälumängu- ja küsitlusplatvorm!", 4 | "meta": { 5 | "title": "Avaleht", 6 | "description": "ClassQuiz on avatud lähtekoodiga ja vaba tarkvara, mis pakub võimaluse korraldada õpilatele ning muudele huvilistele interaktiivseid mälumänge, viktoriine ja küsitlusi" 7 | }, 8 | "stats": "ClassQuizis on juba {{user_count}} kasutajat ning {{quiz_count}} mängu.", 9 | "teachers_site": "Õpetaja vaade", 10 | "students_site": "Õpilase vaade" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/types/rectangle.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 |
21 | {#if editor} 22 | 23 | {/if} 24 |
25 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/types/circle.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 |
21 | {#if editor} 22 | 23 | {/if} 24 |
25 | -------------------------------------------------------------------------------- /frontend/src/routes/edit/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect, error } from '@sveltejs/kit'; 6 | import { signedIn } from '$lib/stores'; 7 | 8 | export async function load({ url, parent }) { 9 | const quiz_id = url.searchParams.get('quiz_id'); 10 | const { email } = await parent(); 11 | if (!email) { 12 | redirect(302, `/account/login?returnTo=/edit?quiz_id=${quiz_id}`); 13 | } 14 | 15 | if (email) { 16 | signedIn.set(true); 17 | } 18 | 19 | if (quiz_id === null) { 20 | error(404); 21 | } 22 | return { 23 | quiz_id 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 3 | 4 | SPDX-License-Identifier: MPL-2.0 5 | */ 6 | 7 | const tailwindcss = require('@tailwindcss/postcss'); 8 | const autoprefixer = require('autoprefixer'); 9 | const cssnano = require('cssnano'); 10 | const postcss_import = require('postcss-import'); 11 | 12 | const config = { 13 | plugins: [ 14 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 15 | tailwindcss(), 16 | postcss_import(), 17 | //But others, like autoprefixer, need to run after, 18 | autoprefixer, 19 | cssnano({ preset: 'default' }) 20 | ] 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | /// 6 | 7 | interface ImportMetaEnv { 8 | readonly VITE_GOOGLE_AUTH_ENABLED?: string; 9 | readonly VITE_GITHUB_AUTH_ENABLED?: string; 10 | readonly VITE_CAPTCHA_ENABLED?: string; 11 | readonly VITE_CUSTOM_OAUTH_NAME?: string; 12 | readonly VITE_REGISTRATION_DISABLED?: string; 13 | readonly VITE_HCAPTCHA?: string; 14 | readonly VITE_RECAPTCHA?: string; 15 | readonly VITE_SENTRY?: string; 16 | readonly VITE_PLAUSIBLE_DATA_URL?: string; 17 | } 18 | 19 | interface ImportMeta { 20 | readonly env: ImportMetaEnv; 21 | } 22 | -------------------------------------------------------------------------------- /classquiz/routers/box_controller/embedded_ws.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Basic Request 8 | 9 | Every request is a json-object: `{"type": "SOME_TYPE", "data": "ANY_DATA"}` 10 | 11 | ### Types 12 | 13 | #### e (Error) 14 | 15 | - [ ] Client 16 | - [x] Server 17 | 18 | Pretty self-explanatory. 19 | 20 | Value is a CamelCase errorcode. 21 | Codes: 22 | 23 | - `ValidationError` 24 | - `BadId` 25 | 26 | #### bp (ButtonPress) 27 | 28 | - [x] Client 29 | - [ ] Server 30 | 31 | Sends a button-press to the server, where `data` is either `b`, `g`, `y` or `r`. Capital letters indicate a long-press. 32 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/edit/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | import { error } from '@sveltejs/kit'; 7 | import type { Data } from '$lib/quiztivity/types'; 8 | 9 | export const load = (async ({ url, fetch }) => { 10 | const id = url.searchParams.get('id'); 11 | if (!id) { 12 | error(400, 'id missing'); 13 | } 14 | const resp = await fetch(`/api/v1/quiztivity/${id}`); 15 | if (!resp.ok) { 16 | error(404, 'quiztivity not found'); 17 | } 18 | const data: Data = await resp.json(); 19 | return { 20 | quiztivity: data 21 | }; 22 | }) satisfies PageLoad; 23 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 |
25 | {@render children?.()} 26 |
27 | -------------------------------------------------------------------------------- /frontend/src/lib/quiztivity/components/markdown/play.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 |
25 | {@html rendered_html} 26 |
27 | -------------------------------------------------------------------------------- /frontend/src/routes/admin/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { redirect } from '@sveltejs/kit'; 6 | 7 | export async function load({ parent, url }) { 8 | const { email } = await parent(); 9 | if (!email) { 10 | redirect(302, '/account/login'); 11 | } 12 | const token = url.searchParams.get('token'); 13 | const pin = url.searchParams.get('pin'); 14 | let auto_connect = url.searchParams.get('connect') !== null; 15 | if (token === null || pin === null) { 16 | auto_connect = false; 17 | } 18 | return { 19 | game_pin: pin === null ? '' : pin, 20 | game_token: token === null ? '' : token, 21 | auto_connect 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/routes/edit/files/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | import type { PrivateImageData } from '$lib/quiz_types'; 7 | 8 | export const load = (async ({ fetch }) => { 9 | const res = await fetch('/api/v1/storage/list'); 10 | const res2 = await fetch('/api/v1/storage/limit'); 11 | let json: PrivateImageData[]; 12 | if (res.ok) { 13 | json = await res.json(); 14 | } else { 15 | json = []; 16 | } 17 | const storage_usage: { limit: number; limit_reached: boolean; used: number } = 18 | await res2.json(); 19 | return { 20 | images: json, 21 | storage_usage 22 | }; 23 | }) satisfies PageLoad; 24 | -------------------------------------------------------------------------------- /frontend/src/routes/view/[quiz_id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { error } from '@sveltejs/kit'; 6 | import type { PageServerLoad } from './$types'; 7 | 8 | export const load: PageServerLoad = async ({ params, parent }) => { 9 | const { quiz_id } = params; 10 | const res = await fetch(`${process.env.API_URL}/api/v1/quiz/get/public/${quiz_id}`); 11 | const { email } = await parent(); 12 | if (res.status === 404 || res.status === 400) { 13 | error(404); 14 | } else if (res.status === 200) { 15 | const quiz = await res.json(); 16 | return { 17 | quiz, 18 | logged_in: Boolean(email) 19 | }; 20 | } else { 21 | error(500); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/lib/components/controller/code.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 |
22 | {#each code as c} 23 |
24 |

{c}

25 | 30 |
31 | {/each} 32 |
33 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """${message} 6 | 7 | Revision ID: ${up_revision} 8 | Revises: ${down_revision | comma,n} 9 | Create Date: ${create_date} 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | ${imports if imports else ""} 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = ${repr(up_revision)} 19 | down_revision = ${repr(down_revision)} 20 | branch_labels = ${repr(branch_labels)} 21 | depends_on = ${repr(depends_on)} 22 | 23 | 24 | def upgrade() -> None: 25 | ${upgrades if upgrades else "pass"} 26 | 27 | 28 | def downgrade() -> None: 29 | ${downgrades if downgrades else "pass"} 30 | -------------------------------------------------------------------------------- /migrations/versions/7afe98d04169_added_custom_openid.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added custom_openid 6 | 7 | Revision ID: 7afe98d04169 8 | Revises: 438516c09cf3 9 | Create Date: 2023-02-17 15:10:00.158320 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "7afe98d04169" 19 | down_revision = "438516c09cf3" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | op.execute("ALTER TYPE userauthtypes ADD VALUE 'CUSTOM';") 26 | 27 | 28 | def downgrade() -> None: 29 | op.execute("ALTER TYPE userauthtypes DROP VALUE 'CUSTOM';") 30 | -------------------------------------------------------------------------------- /classquiz/kahoot_importer/get.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from aiohttp import ClientSession 7 | from pydantic import BaseModel 8 | 9 | from classquiz.kahoot_importer import Card, Kahoot 10 | 11 | 12 | class _Response(BaseModel): 13 | card: Card 14 | kahoot: Kahoot 15 | 16 | 17 | async def get(game_id: str) -> _Response | int: 18 | async with ( 19 | ClientSession() as session, 20 | session.get(f"https://create.kahoot.it/rest/kahoots/{game_id}/card/?includeKahoot=true") as response, 21 | ): 22 | if response.status == 200: 23 | return _Response(**await response.json()) 24 | else: 25 | return response.status 26 | -------------------------------------------------------------------------------- /frontend/src/routes/explore/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 24 | ClassQuiz - Explore 25 | 26 | 27 |
28 | {#each quizzes.hits as quiz} 29 | 30 | {/each} 31 |
32 | -------------------------------------------------------------------------------- /frontend/src/routes/user/[user_id]/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | 7 | export const load: PageLoad = async ({ params, fetch }) => { 8 | const user_req = await fetch(`/api/v1/community/user/${params.user_id}`); 9 | const user = await user_req.json(); 10 | if (!user) { 11 | return { 12 | user: undefined, 13 | quizzes: undefined 14 | }; 15 | } 16 | const quiz_req = await fetch(`/api/v1/community/quizzes/${params.user_id}?imported=false`); 17 | let quizzes; 18 | if (quiz_req.status === 404) { 19 | quizzes = []; 20 | } else { 21 | quizzes = await quiz_req.json(); 22 | } 23 | return { 24 | user, 25 | quizzes 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /classquiz/routers/remote.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from fastapi import APIRouter, Depends, HTTPException 7 | 8 | from classquiz.auth import get_current_user 9 | from classquiz.db.models import User, GameInLobby 10 | from classquiz.config import redis 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/game_waiting") 16 | async def get_game_in_lobby(user: User = Depends(get_current_user)): 17 | game_in_lobby_raw = await redis.get(f"game_in_lobby:{user.id.hex}") 18 | if game_in_lobby_raw is None: 19 | raise HTTPException(status_code=404, detail="No game waiting") 20 | game_in_lobby = GameInLobby.model_validate_json(game_in_lobby_raw) 21 | return game_in_lobby 22 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/[controller_id]/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | import { error } from '@sveltejs/kit'; 7 | 8 | export const load = (async ({ fetch, params }) => { 9 | const res = await fetch(`/api/v1/box-controller/web/controller?id=${params.controller_id}`); 10 | if (res.status !== 200) { 11 | error(res.status, await res.text()); 12 | } 13 | const json: { 14 | id: string; 15 | player_name: string; 16 | last_seen?: string; 17 | first_seen?: string; 18 | name: string; 19 | os_version?: string; 20 | wanted_os_version?: string; 21 | } = await res.json(); 22 | return { 23 | controller: json 24 | }; 25 | }) satisfies PageLoad; 26 | -------------------------------------------------------------------------------- /frontend/src/routes/edit/files/uploader.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | {#await import('$lib/editor/uploader.svelte')} 23 | 24 | {:then c} 25 | 33 | {/await} 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | FROM python:3.13-slim 6 | 7 | COPY Pipfile* /app/ 8 | WORKDIR /app/ 9 | RUN apt update && \ 10 | apt install -y jq gcc libpq5 libpq-dev libmagic1 && \ 11 | jq -r '.default | to_entries[] | .key + .value.version' Pipfile.lock > requirements.txt && \ 12 | sed -i "s/psycopg2-binary/psycopg2/g" requirements.txt 13 | 14 | RUN pip install -r requirements.txt && \ 15 | apt remove -y jq gcc 16 | 17 | COPY classquiz/ /app/classquiz/ 18 | COPY image_cleanup.py /app/image_cleanup.py 19 | COPY alembic.ini /app/ 20 | COPY migrations/ /app/migrations/ 21 | COPY *start.sh /app/ 22 | COPY gunicorn_conf.py /app/ 23 | 24 | 25 | EXPOSE 80 26 | ENV PYTHONPATH=/app 27 | RUN chmod +x start.sh 28 | ENV APP_MODULE=classquiz:app 29 | CMD ["./start.sh"] 30 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/play/+page.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { PageLoad } from './$types'; 6 | import { error } from '@sveltejs/kit'; 7 | import type { Data } from '$lib/quiztivity/types'; 8 | 9 | export const load = (async ({ url, fetch }) => { 10 | const id = url.searchParams.get('id'); 11 | const share = url.searchParams.get('share') === 'true'; 12 | if (!id) { 13 | error(400, 'id missing'); 14 | } 15 | let resp: Response; 16 | if (share) { 17 | resp = await fetch(`/api/v1/quiztivity/shares/${id}`); 18 | } else { 19 | resp = await fetch(`/api/v1/quiztivity/${id}`); 20 | } 21 | if (!resp.ok) { 22 | error(resp.status); 23 | } 24 | const data: Data = await resp.json(); 25 | return { 26 | quiztivity: data 27 | }; 28 | }) satisfies PageLoad; 29 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | CONTAINER_BIN=podman 6 | 7 | run_tests() { 8 | pipenv run coverage run -m pytest -s -v --asyncio-mode=strict classquiz/tests 9 | } 10 | 11 | stop() { 12 | $CONTAINER_BIN compose -f docker-compose.dev.yml down --volumes 13 | } 14 | 15 | init() { 16 | if [ ! -d /tmp/storage ]; then 17 | mkdir /tmp/storage 18 | fi 19 | $CONTAINER_BIN compose -f docker-compose.dev.yml up -d 20 | sleep 2 21 | pipenv run alembic upgrade head 22 | } 23 | 24 | case $1 in 25 | +) init ;; 26 | -) stop ;; 27 | a) 28 | $CONTAINER_BIN volume rm classquiz_db 29 | init 30 | run_tests 31 | stop 32 | ;; 33 | prepare) 34 | stop 35 | $CONTAINER_BIN volume rm classquiz_db 36 | init 37 | ;; 38 | *) 39 | echo "Invalid option: -$OPTARG" >&2 40 | exit 1 41 | ;; 42 | esac 43 | -------------------------------------------------------------------------------- /frontend/src/lib/components/popover/commandpalettenotice.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /classquiz/tests/test_kahoot_search.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import pytest 7 | 8 | from classquiz.kahoot_importer.search import search 9 | 10 | 11 | @pytest.mark.asyncio 12 | @pytest.mark.order(-2) 13 | async def test_search(): 14 | res = await search(query="Python", limit=100) 15 | assert len(res.entities) == 100 16 | await search(query="Test Quiz", limit=100) 17 | await search(query="Biologie", limit=100) 18 | await search(query="Chemie", limit=100) 19 | await search(query="Deutsch", limit=100) 20 | await search(query="Mathe", limit=100) 21 | await search(query="Englisch", limit=100) 22 | await search(query="Barbie", limit=100) 23 | await search(query="Internet", limit=100) 24 | await search(query="Windows", limit=100) 25 | await search(query="Python", limit=100) 26 | -------------------------------------------------------------------------------- /frontend/src/lib/dashboard/useViewportAction.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | // Stolen from https://svelte.dev/repl/c6a402704224403f96a3db56c2f48dfc?version=3.55.0 6 | // skipcq: JS-0119 7 | let intersectionObserver; 8 | 9 | function ensureIntersectionObserver() { 10 | if (intersectionObserver) return; 11 | 12 | intersectionObserver = new IntersectionObserver((entries) => { 13 | entries.forEach((entry) => { 14 | const eventName = entry.isIntersecting ? 'enterViewport' : 'exitViewport'; 15 | entry.target.dispatchEvent(new CustomEvent(eventName)); 16 | }); 17 | }); 18 | } 19 | 20 | export default function viewport(element) { 21 | ensureIntersectionObserver(); 22 | 23 | intersectionObserver.observe(element); 24 | 25 | return { 26 | destroy() { 27 | intersectionObserver.unobserve(element); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/lib/play/title.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 |
18 |

{@html title}

19 |

{@html description}

20 | {#if cover_image} 21 |
22 |
23 | Not provided 28 |
29 |
30 | {/if} 31 |
32 | -------------------------------------------------------------------------------- /migrations/versions/b2acaede5c2f_made_title_and_description_be_text_not_.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Made title and description be text not string 6 | 7 | Revision ID: b2acaede5c2f 8 | Revises: 400f8ed06c48 9 | Create Date: 2022-12-17 11:31:42.036454 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "b2acaede5c2f" 19 | down_revision = "400f8ed06c48" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | op.alter_column("quiz", "title", type_=sa.Text) 26 | op.alter_column("quiz", "description", type_=sa.Text) 27 | 28 | 29 | def downgrade() -> None: 30 | op.alter_column("quiz", "title", type_=sa.String) 31 | op.alter_column("quiz", "description", type_=sa.String) 32 | -------------------------------------------------------------------------------- /frontend/src/lib/files/dashboard.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | {#if files}{:else} 39 | 40 | {/if} 41 | -------------------------------------------------------------------------------- /classquiz/routers/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from uuid import UUID 7 | 8 | from fastapi import APIRouter, Depends 9 | 10 | from classquiz.auth import get_admin_user 11 | from classquiz.db.models import User 12 | 13 | router = APIRouter() 14 | 15 | 16 | @router.delete("/user/id") 17 | async def delete_user_by_id(user_id: UUID, _: User = Depends(get_admin_user)): 18 | return {"deleted": await User.objects.delete(id=user_id)} 19 | 20 | 21 | @router.delete("/user/username") 22 | async def delete_user_by_username(username: str, _: User = Depends(get_admin_user)): 23 | return {"deleted": await User.objects.delete(username=username)} 24 | 25 | 26 | @router.delete("/user/email") 27 | async def delete_user_by_email(email: str, _: User = Depends(get_admin_user)): 28 | return {"deleted": await User.objects.delete(email=email)} 29 | -------------------------------------------------------------------------------- /classquiz/worker/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from arq import cron 7 | from arq.connections import RedisSettings 8 | 9 | from classquiz import settings 10 | from classquiz.db import database 11 | from classquiz.worker.storage import clean_editor_images_up, calculate_hash, quiz_update 12 | 13 | 14 | async def startup(ctx): 15 | ctx["db"] = database 16 | if not ctx["db"].is_connected: 17 | await ctx["db"].connect() 18 | 19 | 20 | async def shutdown(ctx): 21 | if ctx["db"].is_connected: 22 | await ctx["db"].disconnect() 23 | 24 | 25 | class WorkerSettings: 26 | functions = [calculate_hash, quiz_update] 27 | cron_jobs = [cron(clean_editor_images_up, hour={0, 6, 12, 18}, minute=0)] 28 | on_startup = startup 29 | on_shutdown = shutdown 30 | redis_settings = RedisSettings.from_dsn(str(settings.redis)) 31 | -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import adapter from '@sveltejs/adapter-node'; 6 | import preprocess from 'svelte-preprocess'; 7 | import { mdsvex } from 'mdsvex'; 8 | 9 | /** @type {import('@sveltejs/kit').Config} */ 10 | const config = { 11 | // Consult https://github.com/sveltejs/svelte-preprocess 12 | // for more information about preprocessors 13 | preprocess: [ 14 | preprocess({ 15 | postcss: true 16 | }), 17 | mdsvex() 18 | ], 19 | extensions: ['.svelte', '.svx'], 20 | 21 | kit: { 22 | adapter: adapter({ 23 | out: 'build', 24 | precompress: true 25 | }) 26 | // +++ SOON OBSOLETE +++ 27 | /* 28 | vite: { 29 | optimizeDeps: { 30 | include: ['swiper'] 31 | }, 32 | build: { 33 | sourcemap: true 34 | } 35 | } 36 | */ 37 | // --- SOON OBSOLETE --- 38 | } 39 | }; 40 | 41 | export default config; 42 | -------------------------------------------------------------------------------- /migrations/versions/694cb11c6886_added_totp.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added totp 6 | 7 | Revision ID: 694cb11c6886 8 | Revises: 901dfcdf8d38 9 | Create Date: 2022-12-18 13:20:48.091675 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "694cb11c6886" 19 | down_revision = "901dfcdf8d38" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("users", sa.Column("totp_secret", sa.String(length=32), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("users", "totp_secret") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 6 | # Contribute to ClassQuiz 7 | 8 | For the development-setup, please check out the 9 | docs: [classquiz.mawoka.eu/docs/develop](https://classquiz.mawoka.eu/docs/develop) 10 | 11 | ## Formatting git-commits 12 | 13 | Please use [Gitmoji](https://gitmoji.dev/) to format your commits. 14 | 15 | ## Opening PRs 16 | 17 | Just do so. 18 | 19 | ## Found a bug 20 | 21 | If it is a security-related bug, please contact me at [mawoka.eu/contact](https://mawoka.eu/contact). If not, just open 22 | an issue here on GitHub. 23 | 24 | **Please try to reproduce the bug with the adblocker disabled**, so I will be able to see the bug in Sentry. 25 | 26 | ## Want to translate? 27 | 28 | Go to [Weblate](https://translate.mawoka.eu/projects/classquiz/frontend/). 29 | If the language isn't available, please open 30 | an issue here, so I'll be able to add it. 31 | -------------------------------------------------------------------------------- /migrations/versions/9d7fa2e6b24c_added_mod_rating.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added Mod Rating 6 | 7 | Revision ID: 9d7fa2e6b24c 8 | Revises: 2ed6823c69b2 9 | Create Date: 2023-08-01 16:06:22.419662 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "9d7fa2e6b24c" 19 | down_revision = "2ed6823c69b2" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("quiz", sa.Column("mod_rating", sa.SmallInteger(), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("quiz", "mod_rating") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | ClassQuiz/docs - Index 8 | 12 | 13 | 36 | -------------------------------------------------------------------------------- /migrations/versions/97144a8cf6b6_added_github_user_id.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added github_user_id 6 | 7 | Revision ID: 97144a8cf6b6 8 | Revises: b2acaede5c2f 9 | Create Date: 2022-12-17 16:25:44.446361 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "97144a8cf6b6" 19 | down_revision = "b2acaede5c2f" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("users", sa.Column("github_user_id", sa.Integer(), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("users", "github_user_id") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/400f8ed06c48_added_background_color.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """added background-color 6 | 7 | Revision ID: 400f8ed06c48 8 | Revises: ec6cf07ff68a 9 | Create Date: 2022-09-26 17:56:00.426804 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "400f8ed06c48" 19 | down_revision = "ec6cf07ff68a" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("quiz", sa.Column("background_color", sa.Text(), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("quiz", "background_color") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/7ad8502af419_added_kahoot_id.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added kahoot_id 6 | 7 | Revision ID: 7ad8502af419 8 | Revises: 820e06ef2c2a 9 | Create Date: 2023-01-27 22:30:00.331395 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "7ad8502af419" 19 | down_revision = "820e06ef2c2a" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("quiz", sa.Column("kahoot_id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("quiz", "kahoot_id") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/820e06ef2c2a_added_background_image.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added background_image 6 | 7 | Revision ID: 820e06ef2c2a 8 | Revises: 694cb11c6886 9 | Create Date: 2022-12-28 18:27:57.035467 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "820e06ef2c2a" 19 | down_revision = "694cb11c6886" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.add_column("quiz", sa.Column("background_image", sa.Text(), nullable=True)) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_column("quiz", "background_image") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/da778d551bf4_added_github_oauth.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """added github_oauth 6 | 7 | Revision ID: da778d551bf4 8 | Revises: 0c081a52ab8a 9 | Create Date: 2022-06-11 16:41:35.761391 10 | 11 | """ 12 | 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | import ormar 17 | 18 | 19 | # revision identifiers, used by Alembic. 20 | revision = "da778d551bf4" 21 | down_revision = "0c081a52ab8a" 22 | branch_labels = None 23 | depends_on = None 24 | 25 | 26 | def upgrade() -> None: 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.execute("ALTER TYPE userauthtypes ADD VALUE 'GITHUB';") 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade() -> None: 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.execute("ALTER TYPE userauthtypes DROP VALUE 'GITHUB';") 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /frontend/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 18 | {$t('words.error')} - {status} 19 | 20 |

{status}

21 | 22 | {#if status === 404} 23 |

24 | {$t('error_page.404_text')} 25 |

26 | {:else} 27 |

28 | {$t('error_page.unknown_error_text')} 29 |

30 | {/if} 31 | 32 |
33 | Cat representing the {status}-http error code 38 |
39 | -------------------------------------------------------------------------------- /frontend/src/routes/view/+error.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 15 | Error - {status} 16 | 17 |

{status}

18 | 19 | {#if status === 404} 20 |

21 | The quiz you were looking for is gone or never even existed. Who knows? 22 |

23 | {:else} 24 |

25 | That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power 26 | to break stuff... 27 |

28 | {/if} 29 | 30 |
31 | Cat representing the {status}-http error code 36 |
37 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | module.exports = { 6 | root: true, 7 | parser: '@typescript-eslint/parser', 8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 9 | plugins: ['svelte3', '@typescript-eslint'], 10 | ignorePatterns: ['*.cjs'], 11 | overrides: [ 12 | { files: ['*.svelte'], processor: 'svelte3/svelte3' }, 13 | { 14 | files: ['*.*'], 15 | rules: { 16 | 'a11y-click-events-have-key-events': 'off' 17 | } 18 | } 19 | ], 20 | rules: { 21 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 22 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }] 23 | }, 24 | settings: { 25 | 'svelte3/typescript': () => require('typescript') 26 | }, 27 | parserOptions: { 28 | sourceType: 'module', 29 | ecmaVersion: 2020 30 | }, 31 | env: { 32 | browser: true, 33 | es2017: true, 34 | node: true 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /migrations/versions/6dc09ad6f6ef_imported_from_kahoot_field.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """imported_from_kahoot field 6 | 7 | Revision ID: 6dc09ad6f6ef 8 | Revises: 17ea75679da8 9 | Create Date: 2022-06-10 18:06:39.912634 10 | 11 | """ 12 | 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | 17 | 18 | # revision identifiers, used by Alembic. 19 | revision = "6dc09ad6f6ef" 20 | down_revision = "17ea75679da8" 21 | branch_labels = None 22 | depends_on = None 23 | 24 | 25 | def upgrade() -> None: 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.add_column("quiz", sa.Column("imported_from_kahoot", sa.Boolean(), nullable=True, default=False)) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade() -> None: 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_column("quiz", "imported_from_kahoot") 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /frontend/src/lib/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /image_cleanup.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import asyncio 6 | 7 | from classquiz.config import settings, redis, storage 8 | from classquiz.storage.errors import DeletionFailedError 9 | 10 | settings = settings() 11 | 12 | 13 | async def main(): 14 | edit_sessions = await redis.smembers("edit_sessions") 15 | for session_id in edit_sessions: 16 | session = await redis.get(f"edit_session:{session_id}") 17 | if session is None: 18 | images = await redis.lrange(f"edit_session:{session_id}:images", 0, 3000) 19 | if len(images) != 0: 20 | try: 21 | await storage.delete(images) 22 | except DeletionFailedError: 23 | print("Deletion Error", images) 24 | await redis.srem("edit_sessions", session_id) 25 | await redis.delete(f"edit_session:{session_id}:images") 26 | 27 | 28 | if __name__ == "__main__": 29 | asyncio.run(main()) 30 | -------------------------------------------------------------------------------- /migrations/versions/cda6903dfc0c_added_cover_image.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """added cover_image 6 | 7 | Revision ID: cda6903dfc0c 8 | Revises: 3f63c0130bce 9 | Create Date: 2022-09-08 16:40:01.675020 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "cda6903dfc0c" 19 | down_revision = "3f63c0130bce" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_unique_constraint(None, "instance_data", ["instance_id"]) 27 | op.add_column("quiz", sa.Column("cover_image", sa.Text(), nullable=True)) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade() -> None: 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_column("quiz", "cover_image") 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/classquizcontroller/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | # ClassQuizController 8 | 9 | ## Setup 10 | 11 | At first, you plug it into power, and after some seconds a WLAN-hotspot should be available, called 12 | **ClassQuizController** with no password, which you can connect to. You're getting asked to sign in, and on this page, 13 | you can enter your Wi-Fi SSID and your Wi-Fi password. 14 | 15 | After that, wait two minutes and [add the controller](/account/controllers/add). Give it a name and a player name. The 16 | player name is used in-game and the name is jus used for knowing which controller is which. 17 | 18 | You're getting presented with a code. The code consists of upper- and lowercase letters and colored bars under these. A 19 | long bar (or uppercase letter) stands for along press and a lowercase letter (short bar) stands, respectively, for a 20 | short press. If everything worked, the controller should be connected. 21 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { sveltekit } from '@sveltejs/kit/vite'; 6 | 7 | /** @type {import("vite").UserConfig} */ 8 | const config = { 9 | plugins: [ 10 | sveltekit(), 11 | { 12 | name: 'configure-response-headers', 13 | configureServer: (server) => { 14 | server.middlewares.use((_req, res, next) => { 15 | /* res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); 16 | res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); 17 | res.setHeader("Access-Control-Allow-Origin", "https://ncs3.classquiz.de");*/ 18 | next(); 19 | }); 20 | } 21 | } 22 | ], 23 | server: { 24 | port: 3000 25 | }, 26 | preview: { 27 | port: 3000 28 | }, 29 | optimizeDeps: { 30 | include: ['swiper', 'tippy.js'] 31 | }, 32 | build: { 33 | sourcemap: true 34 | } 35 | 36 | /* Trying 37 | 38 | ssr: { 39 | noExternal: ['@ckeditor/*'], 40 | } 41 | 42 | end trying*/ 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /frontend/i18next-scanner.config.engine.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 3 | 4 | SPDX-License-Identifier: MPL-2.0 5 | */ 6 | 7 | module.exports = { 8 | options: { 9 | debug: true, 10 | // read strings from functions: IllegalMoveError('KEY') or t('KEY') 11 | func: { 12 | list: ['IllegalMoveError', 't'], 13 | extensions: ['.js', '.svelte'] 14 | }, 15 | 16 | trans: false, 17 | 18 | // Create and update files `en.json`, `fr.json`, `es.json` 19 | lngs: ['en'], 20 | 21 | ns: [ 22 | // The namespace I use 23 | 'translation' 24 | ], 25 | 26 | defaultLng: 'en', 27 | defaultNs: 'translation', 28 | 29 | // Put a blank string as initial translation 30 | // (useful for Weblate be marked as 'not yet translated', see later) 31 | defaultValue: () => '', 32 | 33 | // Location of translation files 34 | resource: { 35 | loadPath: 'src/lib/i18n/locales/{{lng}}.json', 36 | savePath: 'src/lib/i18n/locales/{{lng}}.json', 37 | jsonIndent: 4 38 | }, 39 | 40 | nsSeparator: ':', 41 | keySeparator: '.' 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/routes/account/login/verified_badge.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | -------------------------------------------------------------------------------- /classquiz/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import pytest 7 | from classquiz.auth import get_password_hash, verify_password, settings, ALGORITHM, create_access_token 8 | from jose import JWTError, jwt 9 | 10 | test_passwords = ["password", "password123", "12345678", "saddsaasdsad", "dsadasasdasddasasdsdasad"] 11 | 12 | 13 | @pytest.mark.asyncio 14 | @pytest.mark.parametrize("password", test_passwords) 15 | async def test_password_hashes(password): 16 | passwd_hash = get_password_hash(password) 17 | assert verify_password(password, passwd_hash) 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_jwt_engine(): 22 | access_token = create_access_token({"sub": "test@test.com"}) 23 | assert access_token is not None 24 | payload = jwt.decode(access_token, settings.secret_key, algorithms=[ALGORITHM]) 25 | email: str = payload.get("sub") 26 | assert email == "test@test.com" 27 | with pytest.raises(JWTError): 28 | jwt.decode(access_token, "wrong_secret", algorithms=[ALGORITHM]) 29 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /frontend/src/routes/dashboard/files/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | 33 |
34 |
35 | {#each files as file, i} 36 |
37 | {/each} 38 |
39 |
40 | -------------------------------------------------------------------------------- /import_to_meili.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | import meilisearch 6 | from classquiz.db import database 7 | from classquiz.db.models import Quiz 8 | from classquiz.helpers import get_meili_data 9 | from classquiz.config import settings 10 | from asyncio import run 11 | 12 | settings = settings() 13 | 14 | 15 | async def __main__(): 16 | if not database.is_connected: 17 | await database.connect() 18 | meili_data = [] 19 | quizzes = await Quiz.objects.filter(public=True).all() 20 | for quiz in quizzes: 21 | meili_data.append(await get_meili_data(quiz)) 22 | print(len(meili_data)) 23 | client = meilisearch.Client(settings.meilisearch_url) 24 | client.delete_index(settings.meilisearch_index) 25 | client.create_index(settings.meilisearch_index) 26 | client.index(settings.meilisearch_index).add_documents(meili_data) 27 | client.index(settings.meilisearch_index).update_settings({"sortableAttributes": ["created_at"]}) 28 | 29 | 30 | if __name__ == "__main__": 31 | run(__main__()) 32 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/roadmap/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ClassQuiz/docs - Roadmap 9 | 13 | 14 |
17 |

Roadmap

18 | 19 | Tell me! 20 |
    21 |
  • 22 | 23 | User-management 24 |
      25 |
    • Password-reset
    • 26 |
    • Session-management
    • 27 |
    28 |
  • 29 |
  • (Better) Admin-screen styling
  • 30 |
  • (Better) Edit/Create-screen styling
  • 31 |
  • Public quiz-discovery
  • 32 |
33 |
34 | -------------------------------------------------------------------------------- /frontend/src/lib/quiztivity/components/abcd/play.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 |
25 |

{data.question}

26 | 27 |
28 | {#each data.answers as answer, i} 29 | 39 | {/each} 40 |
41 |
42 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/play/+error.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 18 | {$t('words.error')} - {status} 19 | 20 |

{status}

21 | 22 | {#if status === 404} 23 |

24 | {$t('error_page.404_text')} 25 |

26 | {:else if status === 410} 27 |

28 | {$t('quiztivity.share_expired')} 29 |

30 | {:else} 31 |

32 | {$t('error_page.unknown_error_text')} 33 |

34 | {/if} 35 | 36 |
37 | Cat representing the {status}-http error code 42 |
43 | -------------------------------------------------------------------------------- /migrations/versions/3f63c0130bce_added_instance_data.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """added instance_data 6 | 7 | Revision ID: 3f63c0130bce 8 | Revises: ff573859eb32 9 | Create Date: 2022-08-28 20:20:28.932854 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "3f63c0130bce" 19 | down_revision = "ff573859eb32" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "instance_data", 28 | sa.Column("instance_id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False), 29 | sa.PrimaryKeyConstraint("instance_id"), 30 | sa.UniqueConstraint("instance_id"), 31 | ) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade() -> None: 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_table("instance_data") 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/(markdown)/features/custom-field/+page.svx: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | # Custom Field 9 | 10 | ## What is the Custom-Field? 11 | 12 | The Custom-Field is an input-field which can be enabled in the start-quiz modal. The custom-field has to be named, and 13 | then it will be displayed if a player wants to join. 14 | 15 | ## Use Case 16 | 17 | Imagine you're a teacher, and you want to know the real names of your students, but you don't want everyone to see 18 | who is behind which username. That's where the custom-field comes into play. Players can enter their username and their 19 | real name and only the admin (you) can see which username belongs to which real name and which real names exist. The 20 | data can be obtained by downloading the quiz-results at the end. In the spreadsheet, you can then see all the data. 21 | 22 | ## Notes 23 | 24 | - There is no validation at all. Users can also leave it empty, enter numbers and more. 25 | - The value gets saved for future use, so make sure to clear it before starting a new quiz. 26 | -------------------------------------------------------------------------------- /.github/workflows/frontend_lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 6 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 7 | 8 | name: "[CI] Frontend / Lint" 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | paths: 14 | - "frontend/**" 15 | workflow_dispatch: 16 | pull_request: 17 | 18 | jobs: 19 | frontend_lint: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3.3.0 24 | with: 25 | fetch-depth: 1 26 | 27 | - uses: pnpm/action-setup@v2.2.4 28 | with: 29 | version: 8.14.0 30 | working-directory: ./frontend 31 | 32 | - name: Install dependencies 33 | working-directory: ./frontend 34 | run: | 35 | pnpm install 36 | 37 | - name: Lint 38 | working-directory: ./frontend 39 | run: | 40 | pnpm run lint-without-format-checking 41 | -------------------------------------------------------------------------------- /classquiz/socket_server/session.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | from classquiz.config import redis 6 | import json 7 | from typing import Any 8 | from socketio import AsyncServer 9 | from socketio.exceptions import ConnectionRefusedError 10 | 11 | 12 | async def get_session(sid: str, sio: AsyncServer, disconnect_on_error: bool = True) -> dict: 13 | session_id = (await sio.get_session(sid)).get("session_id") 14 | if session_id is None: 15 | raise ConnectionRefusedError("Session not configured") 16 | val = await redis.get(f"socket_io_session:{session_id}") 17 | if disconnect_on_error and val is None: 18 | raise ConnectionRefusedError("session not available") 19 | return json.loads(val) 20 | 21 | 22 | async def save_session(sid: str, sio: AsyncServer, data: Any, disconnect_on_error: bool = True) -> None: 23 | session_id = (await sio.get_session(sid)).get("session_id") 24 | if session_id is None: 25 | raise ConnectionRefusedError("Session not configured") 26 | await redis.set(f"socket_io_session:{session_id}", json.dumps(data), ex=3600) 27 | -------------------------------------------------------------------------------- /frontend/src/lib/quiztivity/components/markdown/edit.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 |
28 | 33 |
34 |
37 | {@html rendered_html} 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /frontend/src/lib/editor/slides/settings_menu.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 |
25 | 33 | 41 |
42 | -------------------------------------------------------------------------------- /frontend/src/lib/play/admin/slide.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 |
39 | 42 |
43 | Slide image 44 |
45 |
46 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/play/navigation_bar.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 |
22 |
23 | { 26 | current_slide_index -= 1; 27 | }}>{$t('words.back')} 29 |
30 |
31 | {current_slide_index + 1}/{question_count} 32 |
33 |
34 | { 37 | current_slide_index += 1; 38 | }}>{$t('words.next')} 40 |
41 |
42 | -------------------------------------------------------------------------------- /frontend/src/lib/play/circular_progress.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 |
26 | {text} 27 |
28 | 29 | 42 | -------------------------------------------------------------------------------- /classquiz/routers/cqa-file-format.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # .cqa - ClassQuizArchive 8 | 9 | A file-format for ClassQuiz-quizzes which stores images and the quiz in one compact file 10 | 11 | ## How the file format works 12 | 13 | ``` 14 | [QUIZ_DATA] {C7 C7 C7 00} 15 | {C6 C6 C6 00} [UTF-8 encoded image-index] {C5 C5 00} [IMAGE as it is] 16 | {C6 C6 C6 00} [UTF-8 encoded image-index] {C5 C5 00} [IMAGE as it is] 17 | ``` 18 | ## Understanding the schema 19 | 20 | ### in {} 21 | - `{C7 C7 C7 00}`: separates the quiz-data from the images 22 | - `{C6 C6 C6 00}`: Indicates that a new image-block starts 23 | - `{C5 C5 00}`: separates image-index and image 24 | 25 | ### in [] 26 | - `[QUIZ_DATA]`: The **gzipped JSON** of the quiz with some modifications: 27 | - Removed `id`-field and `user_id`-field 28 | - Dates (`created_at` and `updated_at`) formatted as ISO-Dates 29 | - Images replaced with their question-index 30 | - Cover-image with index `-1` 31 | - `[UTF-8 encoded image-index]`: The index of the question which the image belongs to. 32 | - `[IMAGE as it is]`: The unmodified uploaded image (jpg, png, webp, etc.) 33 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/locales/ro.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_page": { 3 | "slogan": "Platforma de întrebări open-source!", 4 | "meta": { 5 | "title": "Acasă", 6 | "description": "ClassQuiz este o aplicație open-source, gratuită, care le permite elevilor să învețe interactiv prin teste și quiz-uri" 7 | }, 8 | "stats": "Pe ClassQuiz sunt deja {{user_count}} utilizatori și {{quiz_count}} teste disponibile.", 9 | "see_what_true_and_false": "Verifică răspunsurile", 10 | "see_how_many_true_and_false": "Vezi câte răspunsuri au fost corecte sau greșite", 11 | "create_or_import": "Creează sau importă", 12 | "see_all_quizzes": "Vezi toate testele tale", 13 | "teachers_site": "Pagina profesorului", 14 | "students_site": "Pagina cursantului", 15 | "no_tracking": "Fără urmărire", 16 | "german_server": "Server din Germania", 17 | "user_friendly": "Ușor de folosit", 18 | "completely_free": "Complet gratuit", 19 | "quiz_results_downloadable": "Rezultatele testelor pot fi descărcate", 20 | "multilingual": "Multilingv", 21 | "self_hostable": "Găzduire proprie" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/versions/ec6cf07ff68a_added_apikey.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """added apikey 6 | 7 | Revision ID: ec6cf07ff68a 8 | Revises: cda6903dfc0c 9 | Create Date: 2022-09-19 21:04:37.967466 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "ec6cf07ff68a" 19 | down_revision = "cda6903dfc0c" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "api_keys", 28 | sa.Column("key", sa.String(length=48), nullable=False), 29 | sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 30 | sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_api_keys_users_id_user"), 31 | sa.PrimaryKeyConstraint("key"), 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade() -> None: 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table("api_keys") 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 40 | 41 |
42 | 43 |
44 | -------------------------------------------------------------------------------- /classquiz/kahoot_importer/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Kahoot-Importer 8 | 9 | With this package you can search kahoot-quizzes and get their data. 10 | 11 | This library is also tested. 12 | 13 | ## Get 14 | 15 | ```python 16 | from classquiz.kahoot_importer.get import get, _Response 17 | from asyncio import run 18 | # _Response is a pydantic-object, so you have access to 19 | # .dict() or .model_dump_json(exclude={"kahoot"}) 20 | 21 | async def main(): 22 | kahoot_quiz: _Response = await get("GAME_ID") 23 | print(kahoot_quiz.model_dump_json(exclude={"kahoot"})) 24 | run(main()) 25 | ``` 26 | 27 | 28 | ## Search 29 | 30 | ```python 31 | from classquiz.kahoot_importer.search import search, _Response 32 | from asyncio import run 33 | # _Response ia a pydantic-object, so you have access to 34 | # .dict() or .model_dump_json(exclude={"kahoot"}) 35 | 36 | async def main(): 37 | kahoot_quizzes: _Response = await search("QUERY") 38 | print(kahoot_quizzes.model_dump_json()) 39 | run(main()) 40 | ``` 41 | 42 | 43 | ## Import-Quiz 44 | This script is meant just to be used with classquiz, not alone. 45 | --- 46 | *Kahoot! and the K! logo are trademarks of Kahoot! AS* 47 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # create-svelte 8 | 9 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 10 | 11 | ## Creating a project 12 | 13 | If you're seeing this, you've probably already done this step. Congrats! 14 | 15 | ```bash 16 | # create a new project in the current directory 17 | npm init svelte@next 18 | 19 | # create a new project in my-app 20 | npm init svelte@next my-app 21 | ``` 22 | 23 | > Note: the `@next` is temporary 24 | 25 | ## Developing 26 | 27 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 28 | 29 | ```bash 30 | npm run dev 31 | 32 | # or start the server and open the app in a new browser tab 33 | npm run dev -- --open 34 | ``` 35 | 36 | ## Building 37 | 38 | To create a production version of your app: 39 | 40 | ```bash 41 | npm run build 42 | ``` 43 | 44 | You can preview the production build with `npm run preview`. 45 | 46 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 47 | -------------------------------------------------------------------------------- /classquiz/db/quiztivity.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from pydantic import BaseModel 7 | import enum 8 | 9 | 10 | class Pdf(BaseModel): 11 | url: str 12 | 13 | 14 | class _MemoryCard(BaseModel): 15 | image: str | None = None 16 | text: str | None = None 17 | id: str 18 | 19 | 20 | class Memory(BaseModel): 21 | cards: list[list[_MemoryCard]] 22 | 23 | 24 | class Markdown(BaseModel): 25 | # skipcq: PTC-W0052 26 | markdown: str 27 | 28 | 29 | class _AbcdAnswer(BaseModel): 30 | answer: str 31 | correct: bool 32 | 33 | 34 | class Abcd(BaseModel): 35 | question: str 36 | answers: list[_AbcdAnswer] 37 | 38 | 39 | class QuizTivityTypes(str, enum.Enum): 40 | SLIDE = "SLIDE" 41 | PDF = "PDF" 42 | MEMORY = "MEMORY" 43 | MARKDOWN = "MARKDOWN" 44 | ABCD = "ABCD" 45 | 46 | 47 | TYPE_CLASS_LIST = { 48 | QuizTivityTypes.PDF: type(Pdf), 49 | QuizTivityTypes.MEMORY: type(Memory), 50 | QuizTivityTypes.MARKDOWN: type(Markdown), 51 | QuizTivityTypes.ABCD: type(Abcd), 52 | } 53 | 54 | 55 | class QuizTivityPage(BaseModel): 56 | title: str | None = None 57 | type: QuizTivityTypes 58 | data: Pdf | Memory | Markdown | Abcd 59 | -------------------------------------------------------------------------------- /frontend/src/lib/quiztivity/types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | export enum QuizTivityTypes { 6 | // eslint-disable-next-line no-unused-vars 7 | SLIDE = 'SLIDE', 8 | // eslint-disable-next-line no-unused-vars 9 | PDF = 'PDF', 10 | // eslint-disable-next-line no-unused-vars 11 | MEMORY = 'MEMORY', 12 | // eslint-disable-next-line no-unused-vars 13 | MARKDOWN = 'MARKDOWN', 14 | // eslint-disable-next-line no-unused-vars 15 | ABCD = 'ABCD' 16 | } 17 | 18 | export interface Pdf { 19 | url: string; 20 | } 21 | 22 | export interface MemoryCard { 23 | image?: string; 24 | text?: string; 25 | id: string; 26 | } 27 | 28 | export interface Memory { 29 | cards: MemoryCard[][]; 30 | } 31 | 32 | export interface Markdown { 33 | markdown: string; 34 | } 35 | 36 | export interface AbcdAnswer { 37 | answer: string; 38 | correct: boolean; 39 | } 40 | 41 | export interface Abcd { 42 | question: string; 43 | answers: AbcdAnswer[]; 44 | } 45 | 46 | export interface QuizTivityPage { 47 | title?: string; 48 | type: QuizTivityTypes; 49 | data: Pdf | Memory | Markdown; 50 | id?: string; 51 | } 52 | 53 | export interface Data { 54 | id?: string; 55 | title: string; 56 | pages: QuizTivityPage[]; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/import-from-kahoot/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ClassQuiz/docs - Import from Kahoot 9 | 13 | 14 |
17 |

Import Quizzes from Kahoot into ClassQuiz

18 | 19 |

20 | All in all, this procedure is pretty simple, so just go to create.kahoot.it/discover, find the quiz you want to import, click on it and copy the URL into your clipboard. Then 25 | go to 26 | classquiz.de/import 27 | and paste the url into the text field. 28 |

29 | 30 |

Limitations

31 |
    32 |
  • Videos don't get imported
  • 33 |
  • Metadata doesn't get imported (Stuff like create-date, cover-image, etc.)
  • 34 |
  • Point-multipliers don't get imported
  • 35 |
36 |
37 | -------------------------------------------------------------------------------- /frontend/src/routes/results/[result_id]/general_overview.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 |
35 |
36 |

37 | {@html $t('results_page.general_overview.sentence', { 38 | title, 39 | date: new Date(timestamp).toLocaleString(), 40 | player_count: usernames.length, 41 | average_score: get_average_final_score() 42 | })} 43 |

44 |
45 |
46 | 47 | 52 | -------------------------------------------------------------------------------- /classquiz/emails/templates/footer.jinja2: -------------------------------------------------------------------------------- 1 | 6 | 14 |
15 | 18 | 19 | 20 | 26 | 27 | 28 |
22 |

23 | 💌 Send with love by Mawoka 25 |

29 |
30 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | set -e 8 | 9 | if [ -f /app/app/main.py ]; then 10 | DEFAULT_MODULE_NAME=app.main 11 | elif [ -f /app/main.py ]; then 12 | DEFAULT_MODULE_NAME=main 13 | fi 14 | MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} 15 | VARIABLE_NAME=${VARIABLE_NAME:-app} 16 | export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} 17 | 18 | if [ -f /app/gunicorn_conf.py ]; then 19 | DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py 20 | elif [ -f /app/app/gunicorn_conf.py ]; then 21 | DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py 22 | else 23 | DEFAULT_GUNICORN_CONF=/gunicorn_conf.py 24 | fi 25 | export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF} 26 | export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"} 27 | 28 | # If there's a prestart.sh script in the /app directory or other path specified, run it before starting 29 | PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh} 30 | echo "Checking for script in $PRE_START_PATH" 31 | if [ -f $PRE_START_PATH ] ; then 32 | echo "Running script $PRE_START_PATH" 33 | . "$PRE_START_PATH" 34 | else 35 | echo "There is no script $PRE_START_PATH" 36 | fi 37 | 38 | # Start Gunicorn 39 | exec gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE" 40 | -------------------------------------------------------------------------------- /classquiz/kahoot_importer/search.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | from aiohttp import ClientSession 7 | from pydantic import BaseModel 8 | 9 | from classquiz.kahoot_importer import _Entity 10 | 11 | 12 | # noqa : E501 13 | class _Response(BaseModel): 14 | entities: list[_Entity] 15 | totalHits: int 16 | cursor: int | None = None 17 | pageTimestamp: int 18 | 19 | 20 | async def search( 21 | query: str | None = None, 22 | limit: int | None = 9, 23 | cursor: int | None = 1, 24 | search_cluster: int | None = 1, 25 | inventory_item_id: str | None = "ANY", 26 | ) -> _Response: 27 | """ 28 | 29 | :param inventory_item_id: I don't know 30 | :param search_cluster: Doesn't seeem to matter 31 | :param cursor: The position in the result-list (page) 32 | :param query: The search query 33 | :param limit: Less or equals 100 34 | :return: 35 | """ 36 | async with ( 37 | ClientSession() as session, 38 | session.get( 39 | f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" # noqa : E501 40 | ) as response, 41 | ): 42 | return _Response(**await response.json()) 43 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import { I18nService } from './i18n-service'; 6 | import { I18NextTranslationService } from './translation-service'; 7 | import type { TType } from './translation-service'; 8 | import type { Readable, Writable } from 'svelte/store'; 9 | import { getContext, setContext } from 'svelte'; 10 | 11 | export type I18nContext = { 12 | t: Readable; 13 | currentLanguage: Writable; 14 | }; 15 | const CONTEXT_KEY = 't'; 16 | export const setLocalization = (context: I18nContext) => { 17 | return setContext(CONTEXT_KEY, context); 18 | }; 19 | 20 | // To make retrieving the t function easier. 21 | export const getLocalization = () => { 22 | return getContext(CONTEXT_KEY); 23 | }; 24 | 25 | export const initLocalizationContext = (start_lanugage: string): { i18n: I18nService } => { 26 | // Initialize our services 27 | const i18n = new I18nService(); 28 | const tranlator = new I18NextTranslationService(i18n); 29 | let locale: any; 30 | if (start_lanugage) { 31 | locale = start_lanugage; 32 | } 33 | tranlator.locale.set(locale); 34 | // skipcq: JS-0357 35 | setLocalization({ 36 | t: tranlator.translate, 37 | currentLanguage: locale 38 | }); 39 | 40 | return { 41 | i18n 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/edit/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 43 | 44 |
45 | 46 |
47 | 48 | {#if shares_menu_open} 49 | 50 | {/if} 51 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | #### :tophat: What? Why? 8 | *Please describe your pull request.* 9 | 10 | #### :pushpin: Related Issues 11 | *Link your PR to an issue* 12 | - Related to #? 13 | - Fixes #? 14 | 15 | #### Testing 16 | *Describe the best way to test or validate your PR.* 17 | 18 | #### :clipboard: Checklist 19 | ⚠️ No tests suites for now ⚠️ 20 | :rotating_light: Please review the [guidelines for contributing](https://github.com/mawoka-myblock/ClassQuiz/blob/master/CONTRIBUTING.md) to this repository. 21 | 22 | - [ ] :question: ~~**CONSIDER** adding a unit test if your PR resolves an issue.~~ 23 | - [ ] :heavy_check_mark: **DO** check open PR's to avoid duplicates. 24 | - [ ] :heavy_check_mark: **DO** keep pull requests small so they can be easily reviewed. 25 | - [ ] :heavy_check_mark: **DO** build locally before pushing. 26 | - [ ] :heavy_check_mark: ~~**DO** make sure tests pass.~~ 27 | - [ ] :heavy_check_mark: ~~**DO** add CHANGELOG upgrade notes if required.~~ 28 | - [ ] :x:~~**AVOID** breaking the continuous integration build.~~ 29 | - [ ] :x:**AVOID** making significant changes to the overall architecture. 30 | 31 | ### :camera: Screenshots 32 | *Please add screenshots of the changes you're proposing* 33 | 34 | :hearts: Thank you! 35 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 26 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | %sveltekit.head% 44 | 45 | 46 |
%sveltekit.body%
47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/backend_lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 6 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 7 | 8 | name: "[CI] Backend / Lint" 9 | 10 | on: 11 | workflow_dispatch: 12 | # push: 13 | # branches: [ master ] 14 | # pull_request: 15 | 16 | 17 | jobs: 18 | backend_lint: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3.3.0 23 | with: 24 | fetch-depth: 1 25 | - name: Set up Python 26 | uses: actions/setup-python@v2 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | python -m pip install flake8 32 | python -m pip install pipenv 33 | # if [ -f Pipfile ]; then pipenv install; fi 34 | - name: Lint with flake8 35 | run: | 36 | # stop the build if there are Python syntax errors or undefined names 37 | flake8 . --count --show-source --statistics 38 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 39 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 40 | -------------------------------------------------------------------------------- /frontend/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { Handle } from '@sveltejs/kit'; 6 | import * as jose from 'jose'; 7 | 8 | /** @type {import('@sveltejs/kit').Handle} */ 9 | export const handle: Handle = async ({ event, resolve }) => { 10 | const access_token = event.cookies.get('access_token'); 11 | if (!access_token) { 12 | event.locals.email = null; 13 | return resolve(event); 14 | } 15 | const jwt = jose.decodeJwt(access_token.replace('Bearer ', '')); 16 | if (!jwt) { 17 | event.locals.email = null; 18 | return resolve(event); 19 | } 20 | // if token expires, do a request to get a new one and set the response-cookies on the response 21 | if (Date.now() >= jwt.exp * 1000) { 22 | const res = await fetch(`${process.env.API_URL}/api/v1/users/check`, { 23 | method: 'GET', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | Cookie: event.request.headers.get('cookie') || '' 27 | } 28 | }); 29 | if (res.ok) { 30 | event.locals.email = await res.text(); 31 | const resp = await resolve(event); 32 | try { 33 | resp.headers.set('Set-Cookie', res.headers.get('set-cookie')); 34 | } catch { 35 | /* empty */ 36 | } 37 | return resp; 38 | } 39 | } 40 | event.locals.email = jwt.sub; 41 | return resolve(event); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | //{ 2 | // "compilerOptions": { 3 | // "moduleResolution": "bundler", 4 | // "module": "es2020", 5 | // "lib": ["es2020", "DOM"], 6 | // "target": "es2020", 7 | // /** 8 | // svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 9 | // to enforce using \`import type\` instead of \`import\` for Types. 10 | // */ 11 | // /** 12 | // TypeScript doesn't know about import usages in the template because it only sees the 13 | // script of a Svelte file. Therefore preserve all value imports. Requires TS 4.5 or higher. 14 | // */ 15 | // "isolatedModules": true, 16 | // "resolveJsonModule": true, 17 | // /** 18 | // To have warnings/errors of the Svelte compiler at the correct position, 19 | // enable source maps by default. 20 | // */ 21 | // "sourceMap": true, 22 | // "esModuleInterop": true, 23 | // "skipLibCheck": true, 24 | // "forceConsistentCasingInFileNames": true, 25 | // "baseUrl": ".", 26 | // "allowJs": true, 27 | // "checkJs": true, 28 | // "paths": { 29 | // "$lib": ["src/lib"], 30 | // "$lib/*": ["src/lib/*"] 31 | // } 32 | // }, 33 | // "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] 34 | //} 35 | { 36 | "extends": "./.svelte-kit/tsconfig.json", 37 | "compilerOptions": { 38 | "resolveJsonModule": true, 39 | "allowImportingTsExtensions": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # See https://pre-commit.com for more information 6 | # See https://pre-commit.com/hooks.html for more hooks 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v6.0.0 10 | hooks: 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: check-yaml 14 | args: 15 | - --allow-multiple-documents 16 | # - id: check-added-large-files 17 | 18 | - repo: https://github.com/psf/black-pre-commit-mirror 19 | rev: 25.12.0 20 | hooks: 21 | - id: black 22 | - repo: https://github.com/pre-commit/mirrors-prettier 23 | rev: 'v4.0.0-alpha.8' # Use the sha / tag you want to point at 24 | hooks: 25 | - id: prettier 26 | files: "^frontend/" 27 | exclude: "^frontend/src/lib/i18n/locales/" 28 | additional_dependencies: 29 | - "prettier-plugin-svelte@latest" 30 | - "prettier@latest" 31 | - repo: https://github.com/pycqa/flake8 32 | rev: 7.3.0 33 | hooks: 34 | - id: flake8 35 | # flake8 is passed in all tracked python files 36 | # so --exclude in .flake8 does not work 37 | exclude: ^migrations/versions/ 38 | - repo: https://github.com/fsfe/reuse-tool 39 | rev: v6.2.0 40 | hooks: 41 | - id: reuse 42 | -------------------------------------------------------------------------------- /classquiz/helpers/avatar.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import py_avataaars_no_png as pa 7 | from random import choice 8 | import gzip 9 | 10 | 11 | def _gen_avatar() -> str: 12 | mouth_type = pa.MouthType 13 | avatar = pa.PyAvataaar( 14 | style=pa.AvatarStyle.TRANSPARENT, 15 | skin_color=choice(list(pa.SkinColor)), 16 | hair_color=choice(list(pa.HairColor)), 17 | facial_hair_type=choice(list(pa.FacialHairType)), 18 | facial_hair_color=choice(list(pa.HairColor)), 19 | top_type=choice(list(pa.TopType)), 20 | hat_color=choice(list(pa.Color)), 21 | mouth_type=choice([mouth_type.DEFAULT, mouth_type.SMILE, mouth_type.TONGUE, mouth_type.TWINKLE]), 22 | eye_type=pa.EyesType.DEFAULT, # choice(list(pa.EyesType)) 23 | eyebrow_type=choice(list(pa.EyebrowType)), 24 | nose_type=choice(list(pa.NoseType)), 25 | accessories_type=choice(list(pa.AccessoriesType)), 26 | clothe_type=choice(list(pa.ClotheType)), 27 | clothe_color=choice(list(pa.Color)), 28 | clothe_graphic_type=choice(list(pa.ClotheGraphicType)), 29 | ) 30 | return avatar.render_svg() 31 | 32 | 33 | def gzipped_user_avatar() -> bytes: 34 | return gzip.compress(str.encode(_gen_avatar())) 35 | 36 | 37 | def str_user_avatar() -> str: 38 | return _gen_avatar() 39 | -------------------------------------------------------------------------------- /frontend/src/lib/search-card.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 48 | -------------------------------------------------------------------------------- /migrations/versions/4bbe1850b61a_added_1.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added #1 6 | 7 | Revision ID: 4bbe1850b61a 8 | Revises: 7afe98d04169 9 | Create Date: 2023-04-29 21:33:22.657554 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "4bbe1850b61a" 19 | down_revision = "7afe98d04169" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "quiztivitys", 28 | sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False), 29 | sa.Column("title", sa.Text(), nullable=False), 30 | sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), 31 | sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 32 | sa.Column("pages", sa.JSON(), nullable=False), 33 | sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_quiztivitys_users_id_user"), 34 | sa.PrimaryKeyConstraint("id"), 35 | ) 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade() -> None: 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table("quiztivitys") 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /frontend/src/lib/play/kahoot_mode_assets/0.svg: -------------------------------------------------------------------------------- 1 | 6 | Created by potrace 1.16, written by Peter Selinger 2001-2019 7 | -------------------------------------------------------------------------------- /migrations/versions/32649a1ffcf2_added_controllers_table.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added Controllers Table 6 | 7 | Revision ID: 32649a1ffcf2 8 | Revises: 89c4b5d547aa 9 | Create Date: 2023-06-30 01:24:43.124764 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | # revision identifiers, used by Alembic. 17 | revision = "32649a1ffcf2" 18 | down_revision = "89c4b5d547aa" 19 | branch_labels = None 20 | depends_on = None 21 | 22 | 23 | def upgrade() -> None: 24 | op.create_table( 25 | "controller", 26 | sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False), 27 | sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 28 | sa.Column("secret_key", sa.String(length=24), nullable=False), 29 | sa.Column("player_name", sa.Text(), nullable=False), 30 | sa.Column("last_seen", sa.DateTime(), nullable=True), 31 | sa.Column("first_seen", sa.DateTime(), nullable=True), 32 | sa.Column("name", sa.Text(), nullable=False), 33 | sa.Column("os_version", sa.Text(), nullable=True), 34 | sa.Column("wanted_os_version", sa.Text(), nullable=True), 35 | sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_controller_users_id_user"), 36 | sa.PrimaryKeyConstraint("id"), 37 | ) 38 | 39 | 40 | def downgrade() -> None: 41 | op.drop_table("controller") 42 | -------------------------------------------------------------------------------- /frontend/src/routes/account/oauth-error/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 |
13 |
14 |

Error authenticating

15 |
16 | 17 |
18 |

19 | {#if error === 'email'} 20 | GitHub didn't respond with an email-address. Are you sure your email-address is 21 | verified? 22 | {:else} 23 | There was an error authenticating you. Are you sure you've got an email? Is the 24 | Email verified? 25 | {/if} 26 |

27 |
28 | 36 |
37 |

38 | If the error persists, please open an issue on GitHub. 42 |

43 |
44 |
45 | -------------------------------------------------------------------------------- /frontend/src/lib/components/buttons/gray.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | {#if href} 28 | 37 | {@render children?.()} 38 | 39 | {:else} 40 | 49 | {/if} 50 | -------------------------------------------------------------------------------- /migrations/versions/901dfcdf8d38_added_backup_code.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added Backup-code 6 | 7 | Revision ID: 901dfcdf8d38 8 | Revises: 25f2c34a69c8 9 | Create Date: 2022-12-18 11:48:22.772981 10 | 11 | """ 12 | import os 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | from sqlalchemy.orm import Session 17 | import ormar 18 | 19 | # revision identifiers, used by Alembic. 20 | revision = "901dfcdf8d38" 21 | down_revision = "25f2c34a69c8" 22 | branch_labels = None 23 | depends_on = None 24 | 25 | 26 | def upgrade() -> None: 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.add_column("users", sa.Column("backup_code", sa.String(length=64), nullable=True)) 29 | conn = op.get_bind() 30 | session = Session(bind=conn) 31 | res = session.execute("SELECT id from users;") 32 | for row in res: 33 | user_id = str(row).strip(",.'()") 34 | session.execute( 35 | sa.sql.text("UPDATE users SET backup_code = :backup_code WHERE users.id=:user_id"), 36 | {"user_id": user_id, "backup_code": os.urandom(32).hex()}, 37 | ) 38 | op.alter_column("users", "backup_code", nullable=False) 39 | 40 | 41 | # ### end Alembic commands ### 42 | 43 | 44 | def downgrade() -> None: 45 | # ### commands auto generated by Alembic - please adjust! ### 46 | op.drop_column("users", "backup_code") 47 | 48 | 49 | # ### end Alembic commands ### 50 | -------------------------------------------------------------------------------- /frontend/src/lib/play/results_kahoot.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 45 | 46 |
47 |
48 |
49 |

50 | +{score_by_username[username] ?? '0'} 51 |

52 |

Total score: {sorted_scores[username] ?? '0'}

53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /frontend/src/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 3 | 4 | SPDX-License-Identifier: MPL-2.0 5 | */ 6 | 7 | @import 'tippy.js/animations/perspective-subtle.css'; 8 | @import 'tippy.js/dist/tippy.css'; 9 | /* Write your global styles here, in PostCSS syntax */ 10 | @import 'tailwindcss'; 11 | @plugin "@tailwindcss/typography"; 12 | @tailwind utilities; 13 | 14 | .marck-script { 15 | font-family: 'Marck Script'; 16 | } 17 | 18 | .link-hover { 19 | transition: all 0.3s; 20 | } 21 | 22 | .link-hover:hover { 23 | color: #4e6e58; 24 | } 25 | @layer base { 26 | button:not(:disabled), 27 | [role='button']:not(:disabled) { 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | @utility normal-background { 33 | @apply bg-gradient-to-r from-[#009444] via-[#39b54a] to-[#8dc63f] dark:bg-[#0f2702] dark:from-[#0f2702] dark:via-[#0f2702] dark:to-[#0f2702]; 34 | } 35 | 36 | @utility admin-button { 37 | @apply px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded-sm text-center hover:bg-gray-600 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50; 38 | } 39 | 40 | @utility action-button { 41 | @apply px-4 py-2 leading-5 text-black dark:text-white transition-colors duration-200 transform bg-gray-50 dark:bg-gray-700 rounded-sm text-center hover:bg-gray-300 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-gray-600; 42 | } 43 | @utility btn-nav { 44 | @apply text-lg font-medium px-3 text-gray-600 hover:text-green-600 py-1.5 transition-all duration-300; 45 | } 46 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | services: 6 | db: 7 | # build: 8 | # context: pg_uuidv7 9 | # args: 10 | # PG_MAJOR_VERSION: 16 11 | image: postgres:16 12 | environment: 13 | POSTGRES_PASSWORD: mysecretpassword 14 | POSTGRES_DB: classquiz 15 | volumes: 16 | - db:/var/lib/postgresql/data 17 | ports: 18 | - 5432:5432 19 | restart: unless-stopped 20 | meilisearch: 21 | image: getmeili/meilisearch:v1.7 22 | volumes: 23 | - search:/meili_data 24 | ports: 25 | - 7700:7700 26 | environment: 27 | MEILI_NO_ANALYTICS: "true" 28 | restart: unless-stopped 29 | caddy: 30 | image: caddy:alpine 31 | volumes: 32 | - ./Caddyfile:/etc/caddy/Caddyfile 33 | network_mode: "host" 34 | privileged: true 35 | restart: unless-stopped 36 | minio: 37 | image: docker.io/minio/minio 38 | environment: 39 | MINIO_ROOT_USER: classquiz 40 | MINIO_ROOT_PASSWORD: classquiz 41 | MINIO_ADDRESS: ":9000" 42 | MINIO_CONSOLE_ADDRESS: ":9001" 43 | volumes: 44 | - minio:/data 45 | ports: 46 | - 9000:9000 47 | - 9001:9001 48 | restart: unless-stopped 49 | command: minio server /data 50 | redis: 51 | image: redis:alpine 52 | restart: always 53 | healthcheck: 54 | test: ["CMD", "redis-cli", "ping"] 55 | ports: 56 | - 6379:6379 57 | volumes: 58 | db: 59 | search: 60 | minio: 61 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/[controller_id]/SaveIndicator.svelte: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 |
17 | {#if status === SaveStatus.Saved} 18 | 29 | {:else if status === SaveStatus.Saving} 30 | 31 | 35 | 39 | 40 | {/if} 41 |
42 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | [[source]] 6 | url = "https://pypi.org/simple" 7 | verify_ssl = true 8 | name = "pypi" 9 | 10 | [packages] 11 | uvicorn = "*" 12 | python-socketio = "*" 13 | passlib = "*" 14 | python-jose = "*" 15 | alembic = "*" 16 | email-validator = "*" 17 | python-multipart = "*" 18 | redis = "*" 19 | aiohttp = "*" 20 | gunicorn = "*" 21 | qrcode = "*" 22 | jinja2 = "*" 23 | argon2-cffi = "*" 24 | sentry-sdk = "*" 25 | aiofiles = "*" 26 | meilisearch = "*" 27 | bleach = "*" 28 | bidict = "*" 29 | xlsxwriter = "*" 30 | authlib = "*" 31 | httpx = "*" 32 | itsdangerous = "*" 33 | py-avataaars-no-png = "*" 34 | cryptography = "*" 35 | pyotp = "*" 36 | minio = "*" 37 | xxhash = "*" 38 | arq = "*" 39 | thumbhash-python = "*" 40 | python-magic = "*" 41 | openpyxl = "*" 42 | starlette = "*" 43 | pyopenssl = "*" 44 | python-dotenv = "*" 45 | webauthn = "==1.*" 46 | pypng = "*" 47 | ormar = "*" 48 | pydantic = "*" 49 | pydantic-settings = "*" 50 | asyncpg = "*" 51 | psycopg2 = "*" 52 | fastapi = "*" 53 | 54 | [dev-packages] 55 | coverage = "*" 56 | pytest = "*" 57 | pytest-asyncio = "*" 58 | flake8 = "*" 59 | black = "*" 60 | pytest-dependency = "*" 61 | pytest-order = "*" 62 | pre-commit = "*" 63 | python-socketio = {extras = ["client"], version = "*"} 64 | 65 | [requires] 66 | python_version = "3.13" 67 | 68 | [scripts] 69 | format = "black ." 70 | lint = "flake8 classquiz" 71 | test = "coverage run -m pytest --lf -v --asyncio-mode=strict classquiz/tests" 72 | worker = "arq classquiz.worker.WorkerSettings" 73 | -------------------------------------------------------------------------------- /frontend/src/lib/components/buttons/brown.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | {#if href} 30 | 42 | {@render children?.()} 43 | 44 | {:else} 45 | 55 | {/if} 56 | -------------------------------------------------------------------------------- /frontend/src/lib/admin.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | import type { QuizData } from '$lib/quiz_types'; 6 | 7 | export const get_question_title = (q_number: number, quiz_data: QuizData): string => { 8 | if (q_number - 1 === quiz_data.questions.length) { 9 | return; 10 | } 11 | try { 12 | return quiz_data.questions[q_number].question; 13 | } catch (e) { 14 | return ''; 15 | } 16 | }; 17 | 18 | export const getWinnersSorted = ( 19 | quiz_data: QuizData, 20 | final_results: Array | Array> 21 | ) => { 22 | const winners = {}; 23 | const q_count = quiz_data.questions.length; 24 | 25 | function sortObjectbyValue(obj) { 26 | const asc = false; 27 | const ret = {}; 28 | Object.keys(obj) 29 | .sort((a, b) => obj[asc ? a : b] - obj[asc ? b : a]) 30 | .forEach((s) => (ret[s] = obj[s])); 31 | return ret; 32 | } 33 | 34 | try { 35 | for (let i = 0; i < q_count; i++) { 36 | const q_res = final_results[i]; 37 | if (q_res === null) { 38 | continue; 39 | } 40 | for (const res of q_res) { 41 | if (res['right']) { 42 | if (winners[res['username']] === undefined) { 43 | winners[res['username']] = 0; 44 | } 45 | winners[res['username']] += 1; 46 | } 47 | } 48 | } 49 | 50 | return sortObjectbyValue(winners); 51 | } catch { 52 | return undefined; 53 | } 54 | }; 55 | 56 | export interface Player { 57 | username: string; 58 | } 59 | 60 | export interface PlayerAnswer { 61 | username: string; 62 | answer: string; 63 | right: string; 64 | } 65 | -------------------------------------------------------------------------------- /migrations/versions/25f2c34a69c8_added_webauthn.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added webauthn 6 | 7 | Revision ID: 25f2c34a69c8 8 | Revises: 97144a8cf6b6 9 | Create Date: 2022-12-17 16:53:28.909124 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "25f2c34a69c8" 19 | down_revision = "97144a8cf6b6" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "fido_credentials", 28 | sa.Column("pk", sa.Integer(), nullable=False), 29 | sa.Column("id", sa.LargeBinary(length=256), nullable=False), 30 | sa.Column("public_key", sa.LargeBinary(length=256), nullable=False), 31 | sa.Column("sign_count", sa.Integer(), nullable=False), 32 | sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 33 | sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_fido_credentials_users_id_user"), 34 | sa.PrimaryKeyConstraint("pk"), 35 | ) 36 | op.add_column("users", sa.Column("require_password", sa.Boolean(), nullable=False, server_default="f")) 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade() -> None: 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_column("users", "require_password") 43 | op.drop_table("fido_credentials") 44 | # ### end Alembic commands ### 45 | -------------------------------------------------------------------------------- /classquiz/socket_server/export_helpers.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import json 7 | from datetime import datetime 8 | 9 | from pydantic import ValidationError 10 | 11 | from classquiz.config import redis 12 | from classquiz.db.models import PlayGame, GameResults 13 | 14 | 15 | async def save_quiz_to_storage(game_pin: str): 16 | game = PlayGame.model_validate_json(await redis.get(f"game:{game_pin}")) 17 | player_count = await redis.scard(f"game_session:{game_pin}:players") 18 | answers = [] 19 | for i in range(len(game.questions)): 20 | redis_res = await redis.get(f"game_session:{game_pin}:{i}") 21 | try: 22 | answers.append(json.loads(redis_res)) 23 | except (ValidationError, TypeError): 24 | answers.append([]) 25 | player_scores = await redis.hgetall(f"game_session:{game_pin}:player_scores") 26 | custom_field_data = await redis.hgetall(f"game:{game_pin}:players:custom_fields") 27 | q_return = [] 28 | for q in game.questions: 29 | q_return.append(q.model_dump()) 30 | data = GameResults( 31 | id=game.game_id, 32 | quiz=game.quiz_id, 33 | user=game.user_id, 34 | timestamp=datetime.now(), 35 | player_count=player_count, 36 | answers=json.dumps(answers), 37 | player_scores=json.dumps(player_scores), 38 | custom_field_data=json.dumps(custom_field_data), 39 | title=game.title, 40 | description=game.description, 41 | questions=json.dumps(q_return), 42 | ) 43 | await data.save() 44 | -------------------------------------------------------------------------------- /migrations/versions/8ac2bed1718e_added_quiztivityshares.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """Added quiztivityshares 6 | 7 | Revision ID: 8ac2bed1718e 8 | Revises: 4bbe1850b61a 9 | Create Date: 2023-05-14 12:04:03.639173 10 | 11 | """ 12 | from alembic import op 13 | import sqlalchemy as sa 14 | import ormar 15 | 16 | 17 | # revision identifiers, used by Alembic. 18 | revision = "8ac2bed1718e" 19 | down_revision = "4bbe1850b61a" 20 | branch_labels = None 21 | depends_on = None 22 | 23 | 24 | def upgrade() -> None: 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table( 27 | "quiztivityshares", 28 | sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False), 29 | sa.Column("name", sa.Text(), nullable=True), 30 | sa.Column("expire_at", sa.DateTime(), nullable=True), 31 | sa.Column("quiztivity", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 32 | sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True), 33 | sa.ForeignKeyConstraint( 34 | ["quiztivity"], ["quiztivitys.id"], name="fk_quiztivityshares_quiztivitys_id_quiztivity" 35 | ), 36 | sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_quiztivityshares_users_id_user"), 37 | sa.PrimaryKeyConstraint("id"), 38 | ) 39 | # ### end Alembic commands ### 40 | 41 | 42 | def downgrade() -> None: 43 | # ### commands auto generated by Alembic - please adjust! ### 44 | op.drop_table("quiztivityshares") 45 | # ### end Alembic commands ### 46 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # .github/workflows/app.yaml 6 | name: PyTest 7 | on: 8 | push: 9 | paths: 10 | - "classquiz/**" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | 18 | steps: 19 | - name: Check out repository code 20 | uses: actions/checkout@v3 21 | - name: Install pipenv 22 | run: pipx install pipenv 23 | 24 | # Setup Python (faster than using Python container) 25 | - name: Setup Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: "3.10" 29 | cache: 'pipenv' 30 | - run: pipenv install --dev 31 | - name: Prepare tests 32 | run: | 33 | echo ${{ secrets.DOTENV }} | base64 --decode > .env 34 | set -o allexport; source .env; set +o allexport 35 | chmod +x run_tests.sh 36 | - name: Run tests 37 | run: | 38 | ./run_tests.sh a 39 | pipenv run coverage xml 40 | # - name: Report results to DeepSource 41 | # run: | 42 | # # Install deepsource CLI 43 | # curl https://deepsource.io/cli | sh 44 | # 45 | # # From the root directory, run the report coverage command 46 | # ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml 47 | # 48 | # env: 49 | # DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }} 50 | 51 | - name: Upload Coverage to Codecov 52 | uses: codecov/codecov-action@v2 53 | with: 54 | files: ./coverage.xml 55 | -------------------------------------------------------------------------------- /frontend/src/lib/components/DownloadQuiz.svelte: -------------------------------------------------------------------------------- 1 | 6 | 30 | 31 | {#if quiz_id} 32 |
37 |
38 |

{$t('downloader.select_download_type')}

39 |
40 |
41 | {$t('downloader.own_format')} 43 | 44 |
45 |
46 | {$t('downloader.excel_format')} 48 | 49 |
50 |
51 |

{$t('downloader.help')}

52 |
53 |
54 | {/if} 55 | -------------------------------------------------------------------------------- /migrations/versions/ff573859eb32_user_avatar.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | """user avatar 6 | 7 | Revision ID: ff573859eb32 8 | Revises: da778d551bf4 9 | Create Date: 2022-07-05 20:36:44.858963 10 | 11 | """ 12 | import asyncio 13 | 14 | from alembic import op 15 | import sqlalchemy as sa 16 | from sqlalchemy.orm import Session 17 | from sqlalchemy.sql.functions import user 18 | 19 | from classquiz.helpers.avatar import gzipped_user_avatar 20 | import ormar 21 | from classquiz.db.models import User 22 | from classquiz.db import metadata, database 23 | 24 | # revision identifiers, used by Alembic. 25 | revision = "ff573859eb32" 26 | down_revision = "da778d551bf4" 27 | branch_labels = None 28 | depends_on = None 29 | 30 | 31 | def upgrade() -> None: 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.add_column("users", sa.Column("avatar", sa.LargeBinary(length=25000), nullable=True)) 34 | conn = op.get_bind() 35 | session = Session(bind=conn) 36 | res = session.execute("SELECT id from users;") 37 | for row in res: 38 | user_id = str(row).strip(",.'()") 39 | avatar = gzipped_user_avatar().hex() 40 | session.execute( 41 | sa.sql.text("UPDATE users SET avatar = (decode(:avatar, 'hex')) WHERE users.id=:user_id"), 42 | {"user_id": user_id, "avatar": avatar}, 43 | ) 44 | op.alter_column("users", "avatar", nullable=False) 45 | 46 | # ### end Alembic commands ### 47 | 48 | 49 | def downgrade() -> None: 50 | # ### commands auto generated by Alembic - please adjust! ### 51 | op.drop_column("users", "avatar") 52 | # ### end Alembic commands ### 53 | -------------------------------------------------------------------------------- /frontend/src/routes/account/controllers/add/wait/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | 43 |
44 |
45 |
46 | 47 |
48 | {#if controller_seen} 49 |
50 |

Controller set up successfully!

51 |
52 | {:else} 53 |
54 |

Checking if controller has been connected in {5 - check_tick} seconds.

55 | 56 |
57 | {/if} 58 |
59 |
60 | -------------------------------------------------------------------------------- /frontend/src/routes/docs/pow/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ClassQuiz/docs - PoW 9 | 13 | 14 |
17 |

PoW

18 |

Why is it loading so long?

19 |

20 | Your browser is calculating something to prevent abuse. The time it takes depends on the 21 | power of your CPU and, believe it or not, luck. Sometimes it takes like 5 seconds, sometimes 22 | 30 secs, so please be patient. If you want to know what's going on, go to the technical part. 25 |

26 |

Technical Part

27 |

28 | The technique used, is called PoW, or Proof of Work. It is a way to "pay" for something by 29 | using your CPU. It's mainly used in crypto-stuff, but not in ClassQuiz. It's just used to 30 | prevent spam. Imagine it like a small fee you have to pay to upload an image, but you don't 31 | pay with money, instead by using your CPU intensively. The Proof of Work-algorythm used is 32 | called HashCash and is also used by Bitcoin. All in all, it's really simple (if you know 33 | what hashes are). Let me explain: 34 |
35 | You're hashing a string with a counter until the first x characters (in this case 4) are "0"/zeros. 36 | That's it. Then, you send the counter to the server, who hashes it once, checks if the first 37 | x characters are really "0" and that's it. 38 |

39 |
40 | -------------------------------------------------------------------------------- /frontend/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | // 3 | // SPDX-License-Identifier: MPL-2.0 4 | 5 | const config = { 6 | content: ['./src/**/*.{html,js,svelte,ts}'], 7 | darkMode: 'class', 8 | theme: { 9 | extend: { 10 | colors: { 11 | green: { 12 | 600: '#009444' 13 | } 14 | }, 15 | typography: (theme) => ({ 16 | DEFAULT: { 17 | css: { 18 | // color: theme('colors.yellow.50'), 19 | textDecoration: 'none', 20 | textColor: '#000', 21 | /*a: { 22 | color: theme('colors.blue.200') 23 | }, 24 | blockquote: { 25 | color: theme('colors.yellow.50') 26 | }, 27 | h1: { 28 | color: theme('colors.yellow.50') 29 | }, 30 | h2: { 31 | color: theme('colors.yellow.50') 32 | }, 33 | h3: { 34 | color: theme('colors.yellow.50') 35 | }, 36 | h4: { 37 | color: theme('colors.yellow.50') 38 | }, 39 | th: { 40 | color: theme('colors.yellow.50') 41 | }, 42 | strong: { 43 | color: theme('colors.yellow.50') 44 | },*/ 45 | 'code::before': { 46 | content: '""', 47 | 'padding-left': '0.25rem' 48 | }, 49 | 'code::after': { 50 | content: '""', 51 | 'padding-right': '0.25rem' 52 | }, 53 | code: { 54 | 'padding-top': '0.25rem', 55 | 'padding-bottom': '0.25rem', 56 | fontWeight: '400', 57 | color: theme('colors.gray.100'), 58 | 'border-radius': '0.25rem', 59 | backgroundColor: theme('colors.slate.800') 60 | } 61 | } 62 | } 63 | }) 64 | } 65 | }, 66 | 67 | plugins: [require('@tailwindcss/typography')] 68 | }; 69 | 70 | module.exports = config; 71 | -------------------------------------------------------------------------------- /classquiz/storage/local_storage.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | import os 7 | from shutil import copyfileobj 8 | from typing import BinaryIO, Generator 9 | 10 | import aiofiles 11 | import aiofiles.os 12 | 13 | _DEFAULT_CHUNK_SIZE = 32768 # bytes; arbitrary 14 | 15 | 16 | class LocalStorage: 17 | def __init__(self, base_path: str): 18 | self.base_path = base_path 19 | 20 | async def download(self, file_name: str) -> Generator | None: 21 | try: 22 | async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="rb") as f: 23 | while True: 24 | chunk = await f.read(8192) 25 | if not chunk: 26 | break 27 | yield chunk 28 | except FileNotFoundError: 29 | yield None 30 | 31 | # skipcq: PYL-W0613 32 | async def upload( 33 | self, 34 | file_name: str, 35 | file: BinaryIO, 36 | size: int | None, 37 | mime_type: str | None = None, 38 | ) -> None: 39 | with open(file=os.path.join(self.base_path, file_name), mode="wb") as f: 40 | copyfileobj(file, f) 41 | 42 | async def delete(self, file_names: [str]) -> None: 43 | for i in file_names: 44 | try: 45 | await aiofiles.os.remove(os.path.join(self.base_path, i)) 46 | except FileNotFoundError: 47 | pass 48 | return None 49 | 50 | def size(self, file_name: str) -> int | None: 51 | try: 52 | return os.stat(os.path.join(self.base_path, file_name)).st_size 53 | except FileNotFoundError: 54 | return None 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | name: Bug Report 5 | description: File a bug report 6 | labels: ["bug"] 7 | body: 8 | - type: input 9 | attributes: 10 | label: Which component is affected? 11 | description: Which component/part of ClassQuiz is affected by the bug? The editor? The admin-screen if a game is running? 12 | validations: 13 | required: true 14 | - type: dropdown 15 | attributes: 16 | label: Did the issue occur at ClassQuiz.de, or on a self-hosted instance? 17 | options: 18 | - On ClassQuiz.de 19 | - On a self-hosted instance 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: How can the issue be reproduced? 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Describe the bug (with screenshots if possible) 30 | validations: 31 | required: true 32 | - type: dropdown 33 | attributes: 34 | label: Device 35 | options: 36 | - Desktop 37 | - Laptop/Notebook 38 | - Smartphone 39 | - Tablet 40 | - Smartwatch 41 | - Fridge/Toaster 42 | validations: 43 | required: true 44 | - type: input 45 | attributes: 46 | label: Operating System 47 | placeholder: Windows, Linux, iOS, etc 48 | validations: 49 | required: true 50 | - type: input 51 | attributes: 52 | label: Browser 53 | placeholder: Chrome, Safari, Firefox, etc 54 | validations: 55 | required: true 56 | - type: markdown 57 | attributes: 58 | value: Additional 59 | validations: 60 | required: true 61 | -------------------------------------------------------------------------------- /frontend/src/routes/quiztivity/play/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 |
30 |
31 | {#if current_slide.type === QuizTivityTypes.MARKDOWN} 32 | {#await import('$lib/quiztivity/components/markdown/play.svelte') then c} 33 | 34 | {/await} 35 | {:else if current_slide.type === QuizTivityTypes.MEMORY} 36 | {#await import('$lib/quiztivity/components/memory/play.svelte') then c} 37 | 38 | {/await} 39 | {:else if current_slide.type === QuizTivityTypes.ABCD} 40 | {#await import('$lib/quiztivity/components/abcd/play.svelte') then c} 41 | 42 | {/await} 43 | {/if} 44 |
45 | 46 |
47 | --------------------------------------------------------------------------------