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 |
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 |
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 |
16 | Welcome to the ClassQuiz docs!
17 | Index
18 |
35 |
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 |
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 |
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 |
11 |
19 |
23 |
29 |
30 |
31 |
You've successfully confirmed your email address.
32 |
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 |
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 | {
32 | select_answer(i);
33 | }}
34 | class:opacity-50={selected_answer !== undefined && !answer.correct}
35 | class:text-2xl={selected_answer === i}
36 | >
37 | {answer.answer}
38 |
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 |
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 |
26 | Time
27 |
32 |
33 |
34 | Title
35 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/frontend/src/lib/play/admin/slide.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
37 |
38 |
39 |
42 |
43 |
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 |
22 |
23 | 💌 Send with love by Mawoka
25 |
26 |
27 |
28 |
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 |
44 |
45 |
--------------------------------------------------------------------------------
/frontend/src/lib/components/buttons/gray.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
26 |
27 | {#if href}
28 |
37 | {@render children?.()}
38 |
39 | {:else}
40 |
47 | {@render children?.()}
48 |
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 |
27 |
28 |
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 |
53 | {@render children?.()}
54 |
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 |
47 |
--------------------------------------------------------------------------------