Merge branch 'master' into chores/add-minimal-ci

This commit is contained in:
Dynnammo
2022-04-09 16:37:48 +02:00
committed by GitHub
59 changed files with 4412 additions and 5052 deletions
+22
View File
@@ -0,0 +1,22 @@
version = 1
test_patterns = ["classquiz/tests/**", "test_*.py"]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
[[analyzers]]
name = "javascript"
enabled = true
[[analyzers]]
name = "test-coverage"
enabled = true
[[transformers]]
name = "black"
enabled = true
+3
View File
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
extend-ignore = E203
+47
View File
@@ -0,0 +1,47 @@
# .github/workflows/app.yaml
name: PyTest
on:
push:
paths:
- "classquiz/tests/**"
- "classquiz/kahoot_importer/**"
- "classquiz/storage/**"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Install pipenv
run: pipx install pipenv
# Setup Python (faster than using Python container)
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
cache: 'pipenv'
- run: pipenv install --dev
- name: Prepare tests
run: |
echo ${{ secrets.DOTENV }} | base64 --decode > .env
set -o allexport; source .env; set +o allexport
mkdir /tmp/storage/
- name: Run tests
run: |
pipenv run coverage run -m pytest --asyncio-mode=strict classquiz/tests
pipenv run coverage xml
- name: Report results to DeepSource
run: |
# Install deepsource CLI
curl https://deepsource.io/cli | sh
# From the root directory, run the report coverage command
./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml
env:
DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }}
+2
View File
@@ -27,6 +27,8 @@ aiofiles = "*"
coverage = "*" coverage = "*"
pytest = "*" pytest = "*"
pytest-asyncio = "*" pytest-asyncio = "*"
flake8 = "*"
black = "*"
[requires] [requires]
python_version = "3.10" python_version = "3.10"
Generated
+104 -14
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "b5be758b4e3beeea1b5552ecb2fcb275e660a7ba8f0c8f7f8afb1d0a877aa73b" "sha256": "075e07880b28878645b61e807c6091ae0ecdef50b3486e047aa853023fc2ae33"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -299,7 +299,7 @@
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_full_version >= '3.5.0'", "markers": "python_version >= '3.5'",
"version": "==2.0.12" "version": "==2.0.12"
}, },
"click": { "click": {
@@ -331,7 +331,7 @@
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
"sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"
], ],
"markers": "python_version >= '3.6' and python_version < '4.0'", "markers": "python_version >= '3.6' and python_version < '4'",
"version": "==2.2.1" "version": "==2.2.1"
}, },
"ecdsa": { "ecdsa": {
@@ -544,7 +544,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
], ],
"markers": "python_full_version >= '3.5.0'", "markers": "python_version >= '3.5'",
"version": "==3.3" "version": "==3.3"
}, },
"jinja2": { "jinja2": {
@@ -922,18 +922,18 @@
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:69d05fac17bf3f43937afbb775c536eb516bd21355a4f17d59a966f4a531ce71", "sha256:0107dc8e98a4f1d1d4aa00100e044287f77121a1e6d2085545c4b7fa94a7a27f",
"sha256:fe45513881229dbee610620b9e0817b1f48c47ba635870320fd44a712204bbdd" "sha256:4e95f4ec5f49e636efcf20061a5a9110c20852f607cfca6865c07aaa8a739ee2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.2.1" "version": "==4.2.2"
}, },
"rsa": { "rsa": {
"hashes": [ "hashes": [
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
"sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"
], ],
"markers": "python_version >= '3.6' and python_version < '4.0'", "markers": "python_version >= '3.6' and python_version < '4'",
"version": "==4.8" "version": "==4.8"
}, },
"sentry-sdk": { "sentry-sdk": {
@@ -946,11 +946,11 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:41aface2e85b517c3a466b4689b8055c02cd2e623461f09af7d93f3da65c4709", "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a",
"sha256:88fafba4abc2f047e08a188fd4bbc10b0e464592c37b514c19f8f8f88d94450b" "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==61.3.1" "version": "==62.0.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@@ -965,7 +965,7 @@
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
], ],
"markers": "python_full_version >= '3.5.0'", "markers": "python_version >= '3.5'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"sqlalchemy": { "sqlalchemy": {
@@ -1031,7 +1031,7 @@
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.9" "version": "==1.26.9"
}, },
"uvicorn": { "uvicorn": {
@@ -1284,6 +1284,43 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.4.0" "version": "==21.4.0"
}, },
"black": {
"hashes": [
"sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b",
"sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176",
"sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09",
"sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a",
"sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015",
"sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79",
"sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb",
"sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20",
"sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464",
"sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968",
"sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82",
"sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21",
"sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0",
"sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265",
"sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b",
"sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a",
"sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72",
"sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce",
"sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0",
"sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a",
"sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163",
"sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad",
"sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"
],
"index": "pypi",
"version": "==22.3.0"
},
"click": {
"hashes": [
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
},
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
@@ -1331,6 +1368,14 @@
"index": "pypi", "index": "pypi",
"version": "==6.3.2" "version": "==6.3.2"
}, },
"flake8": {
"hashes": [
"sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
"sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
],
"index": "pypi",
"version": "==4.0.1"
},
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
@@ -1338,6 +1383,20 @@
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
@@ -1346,6 +1405,21 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==21.3" "version": "==21.3"
}, },
"pathspec": {
"hashes": [
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
],
"version": "==0.9.0"
},
"platformdirs": {
"hashes": [
"sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d",
"sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"
],
"markers": "python_version >= '3.7'",
"version": "==2.5.1"
},
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
@@ -1362,6 +1436,22 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.11.0" "version": "==1.11.0"
}, },
"pycodestyle": {
"hashes": [
"sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
"sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.8.0"
},
"pyflakes": {
"hashes": [
"sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
"sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.0"
},
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
@@ -1392,7 +1482,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version < '3.11'",
"version": "==2.0.1" "version": "==2.0.1"
} }
} }
+2
View File
@@ -4,6 +4,8 @@
<a href="https://github.com/mawoka-myblock/ClassQuiz/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"><img alt="GitHub issues" src="https://img.shields.io/github/issues/mawoka-myblock/classquiz?style=for-the-badge"></a> <a href="https://github.com/mawoka-myblock/ClassQuiz/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"><img alt="GitHub issues" src="https://img.shields.io/github/issues/mawoka-myblock/classquiz?style=for-the-badge"></a>
<a href="https://github.com/mawoka-myblock/ClassQuiz/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/mawoka-myblock/classquiz?style=for-the-badge"></a> <a href="https://github.com/mawoka-myblock/ClassQuiz/blob/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/mawoka-myblock/classquiz?style=for-the-badge"></a>
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/mawoka-myblock/classquiz?style=for-the-badge"> <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/mawoka-myblock/classquiz?style=for-the-badge">
[![DeepSource](https://deepsource.io/gh/mawoka-myblock/ClassQuiz.svg/?label=active+issues&show_trend=true&token=5-2Na9HN-2CXcGkHjah_Rk09&style=for-the-badge)](https://deepsource.io/gh/mawoka-myblock/ClassQuiz/)
<img alt="Snky badge" src="https://img.shields.io/badge/Snyk-Check-success?style=for-the-badge">
<div align='center'> <div align='center'>
<h2 align='center'>ClassQuiz</h2> <h2 align='center'>ClassQuiz</h2>
-1
View File
@@ -1 +0,0 @@
Generic single-database configuration.
-84
View File
@@ -1,84 +0,0 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from classquiz.db import metadata
target_metadata = metadata
from classquiz.config import settings
url = settings.db_url
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
url=url,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
-24
View File
@@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
@@ -1,28 +0,0 @@
"""made some changes
Revision ID: 76507db71fde
Revises:
Create Date: 2022-02-25 18:38:08.687974
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '76507db71fde'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+3 -5
View File
@@ -1,12 +1,12 @@
from fastapi import FastAPI, Request
import sentry_sdk import sentry_sdk
from fastapi import FastAPI, Request
from sentry_sdk.integrations.redis import RedisIntegration
from socketio import ASGIApp from socketio import ASGIApp
from classquiz.db import database
from classquiz.config import settings from classquiz.config import settings
from classquiz.db import database
from classquiz.routers import users, quiz, utils, stats, storage from classquiz.routers import users, quiz, utils, stats, storage
from classquiz.socket_server import sio from classquiz.socket_server import sio
from sentry_sdk.integrations.redis import RedisIntegration
settings = settings() settings = settings()
if settings.sentry_dsn: if settings.sentry_dsn:
@@ -41,8 +41,6 @@ async def shutdown() -> None:
await database_.disconnect() await database_.disconnect()
app.include_router(users.router, tags=["users"], prefix="/api/v1/users") app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils") app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
+7 -6
View File
@@ -15,7 +15,8 @@ from passlib.hash import argon2
from classquiz.cache import get_cache from classquiz.cache import get_cache
from classquiz.config import settings from classquiz.config import settings
from classquiz.db.models import * from datetime import datetime
from classquiz.db.models import User, TokenData
settings = settings() settings = settings()
@@ -27,11 +28,11 @@ ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
class OAuth2PasswordBearerWithCookie(OAuth2): class OAuth2PasswordBearerWithCookie(OAuth2):
def __init__( def __init__(
self, self,
tokenUrl: str, tokenUrl: str,
scheme_name: Optional[str] = None, scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None, scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True, auto_error: bool = True,
): ):
if not scopes: if not scopes:
scopes = {} scopes = {}
+1
View File
@@ -6,6 +6,7 @@ import ormar
from classquiz.config import redis, settings from classquiz.config import redis, settings
from classquiz.db.models import User from classquiz.db.models import User
settings = settings() settings = settings()
+11 -4
View File
@@ -1,6 +1,8 @@
import redis.asyncio as redis_lib
from functools import lru_cache from functools import lru_cache
import redis.asyncio as redis_lib
from pydantic import BaseSettings, RedisDsn, PostgresDsn from pydantic import BaseSettings, RedisDsn, PostgresDsn
from classquiz.storage import Storage from classquiz.storage import Storage
@@ -8,6 +10,7 @@ class Settings(BaseSettings):
""" """
Settings class for the shop app. Settings class for the shop app.
""" """
root_address: str = "http://127.0.0.1:8000" root_address: str = "http://127.0.0.1:8000"
redis: RedisDsn = "redis://localhost:6379/0?decode_responses=True" redis: RedisDsn = "redis://localhost:6379/0?decode_responses=True"
skip_email_verification: bool = False skip_email_verification: bool = False
@@ -34,7 +37,7 @@ class Settings(BaseSettings):
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = 'utf-8' env_file_encoding = "utf-8"
@lru_cache() @lru_cache()
@@ -43,5 +46,9 @@ def settings() -> Settings:
redis: redis_lib.client.Redis = redis_lib.Redis().from_url(settings().redis) redis: redis_lib.client.Redis = redis_lib.Redis().from_url(settings().redis)
storage: Storage = Storage(backend=settings().storage_backend, deta_key=settings().deta_project_key, storage: Storage = Storage(
deta_id=settings().deta_project_id, storage_path=settings().storage_path) backend=settings().storage_backend,
deta_key=settings().deta_project_key,
deta_id=settings().deta_project_id,
storage_path=settings().storage_path,
)
+8 -3
View File
@@ -11,6 +11,7 @@ class User(ormar.Model):
""" """
The user model in the database The user model in the database
""" """
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
email: str = ormar.String(unique=True, max_length=100) email: str = ormar.String(unique=True, max_length=100)
username: str = ormar.String(unique=True, max_length=100) username: str = ormar.String(unique=True, max_length=100)
@@ -20,7 +21,7 @@ class User(ormar.Model):
created_at: datetime = ormar.DateTime(default=datetime.now()) created_at: datetime = ormar.DateTime(default=datetime.now())
class Meta: class Meta:
tablename = 'users' tablename = "users"
metadata = metadata metadata = metadata
database = database database = database
@@ -29,6 +30,7 @@ class UserSession(ormar.Model):
""" """
The user session model for user-sessions The user session model for user-sessions
""" """
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
user: uuid.UUID = ormar.ForeignKey(User) user: uuid.UUID = ormar.ForeignKey(User)
session_key: str = ormar.String(unique=True, max_length=64) session_key: str = ormar.String(unique=True, max_length=64)
@@ -38,7 +40,7 @@ class UserSession(ormar.Model):
last_seen: datetime = ormar.DateTime(default=datetime.now()) last_seen: datetime = ormar.DateTime(default=datetime.now())
class Meta: class Meta:
tablename = 'user_sessions' tablename = "user_sessions"
metadata = metadata metadata = metadata
database = database database = database
@@ -73,7 +75,7 @@ class Quiz(ormar.Model):
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
class Meta: class Meta:
tablename = 'quiz' tablename = "quiz"
metadata = metadata metadata = metadata
database = database database = database
@@ -82,6 +84,7 @@ class Token(BaseModel):
""" """
For JWT For JWT
""" """
access_token: str access_token: str
token_type: str token_type: str
@@ -90,6 +93,7 @@ class TokenData(BaseModel):
""" """
For JWT For JWT
""" """
email: str | None = None email: str | None = None
@@ -97,6 +101,7 @@ class PlayGame(BaseModel):
""" """
For JWT For JWT
""" """
quiz_id: uuid.UUID | str quiz_id: uuid.UUID | str
description: str description: str
title: str title: str
+17 -22
View File
@@ -12,17 +12,18 @@ from classquiz.db.models import User
settings = settings() settings = settings()
jinja = Environment( jinja = Environment(
loader=PackageLoader('classquiz.emails', 'templates'), loader=PackageLoader("classquiz.emails", "templates"),
autoescape=select_autoescape(['html', 'xml']), autoescape=select_autoescape(["html", "xml"]),
enable_async=True) enable_async=True,
)
def _sendMail(template: str, to: str, subject: str): def _sendMail(template: str, to: str, subject: str):
msg = MIMEMultipart('alternative') msg = MIMEMultipart("alternative")
msg['Subject'] = subject msg["Subject"] = subject
msg['From'] = settings.mail_address msg["From"] = settings.mail_address
msg['To'] = to msg["To"] = to
msg.attach(MIMEText(template, 'html')) msg.attach(MIMEText(template, "html"))
context = ssl.SSLContext(ssl.PROTOCOL_TLS) context = ssl.SSLContext(ssl.PROTOCOL_TLS)
server = smtplib.SMTP(host=settings.mail_server, port=settings.mail_port) server = smtplib.SMTP(host=settings.mail_server, port=settings.mail_port)
server.ehlo() server.ehlo()
@@ -35,25 +36,19 @@ def _sendMail(template: str, to: str, subject: str):
async def send_register_email(email: str): async def send_register_email(email: str):
user = await User.objects.get_or_none(email=email, verified=False) user = await User.objects.get_or_none(email=email, verified=False)
if user is None: if user is None:
raise ValueError('User not found') raise ValueError("User not found")
template = jinja.get_template('register.jinja2') template = jinja.get_template("register.jinja2")
template = await template.render_async( template = await template.render_async(base_url=settings.root_address, token=user.verify_key)
base_url=settings.root_address, _sendMail(template=template, to=email, subject="Verify your email")
token=user.verify_key
)
_sendMail(template=template, to=email, subject='Verify your email')
async def send_forgotten_password_email(email: str): async def send_forgotten_password_email(email: str):
user = await User.objects.get_or_none(email=email) user = await User.objects.get_or_none(email=email)
if user is None: if user is None:
raise ValueError('User not found') raise ValueError("User not found")
template = jinja.get_template('forgotten_password.jinja2') template = jinja.get_template("forgotten_password.jinja2")
token = os.urandom(32).hex() token = os.urandom(32).hex()
template = await template.render_async( template = await template.render_async(base_url=settings.root_address, token=token)
base_url=settings.root_address,
token=token
)
await redis.set(f"reset_passwd:{token}", str(user.id), ex=3600) await redis.set(f"reset_passwd:{token}", str(user.id), ex=3600)
_sendMail(template=template, to=email, subject='Reset your password') _sendMail(template=template, to=email, subject="Reset your password")
pass pass
+3 -2
View File
@@ -1,6 +1,7 @@
from pydantic import BaseModel
from uuid import UUID
from typing import List, Any, Optional from typing import List, Any, Optional
from uuid import UUID
from pydantic import BaseModel
class _CoverMetadata(BaseModel): class _CoverMetadata(BaseModel):
+1 -1
View File
@@ -1,6 +1,6 @@
from aiohttp import ClientSession from aiohttp import ClientSession
from pydantic import BaseModel from pydantic import BaseModel
from classquiz.kahoot_importer import _Card, _Kahoot from classquiz.kahoot_importer import _Card, _Kahoot
+23 -9
View File
@@ -1,12 +1,13 @@
import html
import json import json
import uuid import uuid
from datetime import datetime from datetime import datetime
from aiohttp import ClientSession, FormData from aiohttp import ClientSession
import html
from classquiz.config import settings, storage
from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User
from classquiz.kahoot_importer.get import get as get_quiz from classquiz.kahoot_importer.get import get as get_quiz
from classquiz.config import settings, storage
settings = settings() settings = settings()
@@ -52,6 +53,7 @@ async def _download_image(url: str) -> bytes:
# data = await resp.json() # data = await resp.json()
# return data["data"]["id"] # return data["data"]["id"]
async def import_quiz(quiz_id: str, user: User) -> Quiz | str: async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
""" """
Imports a quiz from Kahoot. Imports a quiz from Kahoot.
@@ -65,21 +67,33 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
quiz_questions: list[dict] = [] quiz_questions: list[dict] = []
quiz_id = uuid.uuid4() quiz_id = uuid.uuid4()
for q in quiz.kahoot.questions: for q in quiz.kahoot.questions:
answers: list[QuizAnswer] = [] answers: list[QuizAnswer] = []
image = None image = None
if q.image is not None and q.image != "": if q.image is not None and q.image != "":
image_bytes = await _download_image(q.image) image_bytes = await _download_image(q.image)
image_name= f"{quiz_id}--{uuid.uuid4()}" image_name = f"{quiz_id}--{uuid.uuid4()}"
image = await storage.upload(file_name=image_name, file_data=image_bytes) image = await storage.upload(file_name=image_name, file_data=image_bytes)
image = f"{settings.root_address}/api/v1/storage/download/{image_name}" image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
# image = q.image # image = q.image
for a in q.choices: for a in q.choices:
answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer)))) answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer))))
quiz_questions.append( quiz_questions.append(
QuizQuestion(question=q.question, answers=answers, time=str(q.time / 1000), image=image).dict()) QuizQuestion(
quiz_data = Quiz(id=quiz_id, public=False, title=quiz.kahoot.title, description=quiz.kahoot.description, question=q.question,
created_at=datetime.now(), updated_at=datetime.now(), user_id=user.id, answers=answers,
questions=json.dumps(quiz_questions)) time=str(q.time / 1000),
image=image,
).dict()
)
quiz_data = Quiz(
id=quiz_id,
public=False,
title=quiz.kahoot.title,
description=quiz.kahoot.description,
created_at=datetime.now(),
updated_at=datetime.now(),
user_id=user.id,
questions=json.dumps(quiz_questions),
)
return await quiz_data.save() return await quiz_data.save()
+14 -9
View File
@@ -1,20 +1,26 @@
from uuid import UUID from typing import List
from typing import List, Any, Optional
from aiohttp import ClientSession
from classquiz.kahoot_importer import _Entity
from aiohttp import ClientSession
from pydantic import BaseModel from pydantic import BaseModel
from classquiz.kahoot_importer import _Entity
# noqa : E501
class _Response(BaseModel): class _Response(BaseModel):
entities: List[_Entity] entities: List[_Entity]
totalHits: int totalHits: int
cursor: int cursor: int | None
pageTimestamp: int pageTimestamp: int
async def search(query: str | None, limit: int | None = 9, cursor: int | None = 1, async def search(
search_cluster: int | None = 1, inventory_item_id: str | None = "ANY") -> _Response: query: str | None,
limit: int | None = 9,
cursor: int | None = 1,
search_cluster: int | None = 1,
inventory_item_id: str | None = "ANY",
) -> _Response:
""" """
:param inventory_item_id: I dkon't know :param inventory_item_id: I dkon't know
@@ -26,8 +32,7 @@ async def search(query: str | None, limit: int | None = 9, cursor: int | None =
""" """
async with ClientSession() as session: async with ClientSession() as session:
async with session.get( async with session.get(
f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" # noqa : E501
) as response: ) as response:
# print( # print(
# f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}") # f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}")
+11 -6
View File
@@ -1,18 +1,17 @@
import json import json
import re
import uuid import uuid
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from pydantic import ValidationError
from classquiz.kahoot_importer.import_quiz import import_quiz
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import re from pydantic import ValidationError
from classquiz.auth import get_current_user, get_current_user_optional from classquiz.auth import get_current_user, get_current_user_optional
from classquiz.config import redis, settings, storage from classquiz.config import redis, settings, storage
from classquiz.db.models import Quiz, QuizInput, User, PlayGame from classquiz.db.models import Quiz, QuizInput, User, PlayGame
from classquiz.kahoot_importer.import_quiz import import_quiz
settings = settings() settings = settings()
@@ -63,8 +62,14 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)):
return JSONResponse(status_code=404, content={"detail": "quiz not found"}) return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else: else:
game_pin = randint(10000000, 99999999) game_pin = randint(10000000, 99999999)
game = PlayGame(quiz_id=quiz_id, game_pin=str(game_pin), questions=quiz.questions, game_id=uuid.uuid4(), game = PlayGame(
title=quiz.title, description=quiz.description) quiz_id=quiz_id,
game_pin=str(game_pin),
questions=quiz.questions,
game_id=uuid.uuid4(),
title=quiz.title,
description=quiz.description,
)
await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000) await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000)
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})} return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
+3 -8
View File
@@ -1,13 +1,8 @@
import os from fastapi import APIRouter
from fastapi import APIRouter, Response
from fastapi.background import BackgroundTasks
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from classquiz.auth import *
from classquiz.config import redis from classquiz.config import redis
from classquiz.db.models import * from pydantic import BaseModel
from classquiz.db.models import Quiz, User
router = APIRouter() router = APIRouter()
+9 -4
View File
@@ -1,7 +1,9 @@
import re
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from classquiz.config import settings, storage from classquiz.config import settings, storage
import re
settings = settings() settings = settings()
@@ -10,7 +12,7 @@ router = APIRouter()
file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$" file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$"
@router.get('/download/{file_name}') @router.get("/download/{file_name}")
async def download_file(file_name: str): async def download_file(file_name: str):
if not re.match(file_regex, file_name): if not re.match(file_regex, file_name):
raise HTTPException(status_code=400, detail="Invalid file name") raise HTTPException(status_code=400, detail="Invalid file name")
@@ -22,5 +24,8 @@ async def download_file(file_name: str):
def iter_file(): def iter_file():
yield from download yield from download
return StreamingResponse(iter_file(), media_type='image/*', return StreamingResponse(
headers={"Cache-Control": "public, immutable, max-age=31536000"}) iter_file(),
media_type="image/*",
headers={"Cache-Control": "public, immutable, max-age=31536000"},
)
+63 -24
View File
@@ -1,21 +1,38 @@
import os import os
from email_validator import validate_email, EmailNotValidError from email_validator import validate_email, EmailNotValidError
from fastapi import APIRouter, Response from fastapi import APIRouter, Response, HTTPException, Request, Depends, status
from datetime import timedelta, datetime
from fastapi.background import BackgroundTasks from fastapi.background import BackgroundTasks
from fastapi.responses import JSONResponse, RedirectResponse from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from classquiz.auth import * from classquiz.auth import (
from classquiz.config import redis get_password_hash,
verify_password,
authenticate_user,
create_access_token,
get_current_user,
)
from classquiz.cache import clear_cache_for_account from classquiz.cache import clear_cache_for_account
from classquiz.db.models import * from classquiz.config import redis, settings
import uuid
from pydantic import BaseModel
from classquiz.db.models import User, UserSession, UpdatePassword, Token
from classquiz.emails import send_register_email, send_forgotten_password_email from classquiz.emails import send_register_email, send_forgotten_password_email
settings = settings()
router = APIRouter() router = APIRouter()
route_user = User.get_pydantic( route_user = User.get_pydantic(
exclude={"id": ..., "verified": ..., "verify_key": ..., "created_at": ..., "usersessions": ...}) exclude={
"id": ...,
"verified": ...,
"verify_key": ...,
"created_at": ...,
"usersessions": ...,
}
)
async def _sign_out_everywhere(user: User) -> None: async def _sign_out_everywhere(user: User) -> None:
@@ -23,8 +40,11 @@ async def _sign_out_everywhere(user: User) -> None:
await clear_cache_for_account(user) await clear_cache_for_account(user)
@router.post("/create", response_model=User, @router.post(
response_model_include={"id": ..., "verified": ..., "email": ...}) "/create",
response_model=User,
response_model_include={"id": ..., "verified": ..., "email": ...},
)
async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse: async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse:
user = User(**user.dict(), id=uuid.uuid4()) user = User(**user.dict(), id=uuid.uuid4())
try: try:
@@ -47,8 +67,11 @@ async def create_user(user: route_user, background_task: BackgroundTasks) -> Use
@router.post("/token/cookie", response_model=Token) @router.post("/token/cookie", response_model=Token)
async def login_for_cookie_access_token(request: Request, response: Response, async def login_for_cookie_access_token(
form_data: OAuth2PasswordRequestForm = Depends()): request: Request,
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
):
user = await authenticate_user(form_data.username, form_data.password) user = await authenticate_user(form_data.username, form_data.password)
if not user: if not user:
raise HTTPException( raise HTTPException(
@@ -56,21 +79,29 @@ async def login_for_cookie_access_token(request: Request, response: Response,
detail="Incorrect username or password", detail="Incorrect username or password",
) )
session_key = os.urandom(32).hex() session_key = os.urandom(32).hex()
user_session = UserSession(user=user, session_key=session_key, ip_address=request.client.host, user_session = UserSession(
user_agent=request.headers.get("User-Agent"), id=uuid.uuid4()) user=user,
session_key=session_key,
ip_address=request.client.host,
user_agent=request.headers.get("User-Agent"),
id=uuid.uuid4(),
)
await user_session.save() await user_session.save()
# await user_session.save() # await user_session.save()
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token( access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires)
data={"sub": user.email}, expires_delta=access_token_expires
)
await redis.set(access_token, user.email, ex=settings.access_token_expire_minutes * 60) await redis.set(access_token, user.email, ex=settings.access_token_expire_minutes * 60)
response.set_cookie(key="access_token", value=f"Bearer {access_token}", response.set_cookie(
httponly=True, samesite='strict', max_age=settings.access_token_expire_minutes * 60) key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="strict",
max_age=settings.access_token_expire_minutes * 60,
)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60) response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.set_cookie(key="rememberme", value="") response.set_cookie(key="rememberme", value="")
response.set_cookie(key="rememberme_token", value=session_key, httponly=True, samesite='strict') response.set_cookie(key="rememberme_token", value=session_key, httponly=True, samesite="strict")
return {"access_token": access_token, "token_type": "bearer"} return {"access_token": access_token, "token_type": "bearer"}
@@ -79,16 +110,20 @@ async def rememberme_token(request: Request, response: Response):
rememberme_cookie = request.cookies.get("rememberme_token") rememberme_cookie = request.cookies.get("rememberme_token")
if rememberme_cookie is None: if rememberme_cookie is None:
raise HTTPException(status_code=400, detail="No rememberme cookie") raise HTTPException(status_code=400, detail="No rememberme cookie")
user_session: UserSession | None = await UserSession.objects.filter(session_key=rememberme_cookie).select_related( user_session: UserSession | None = (
UserSession.user).get_or_none() await UserSession.objects.filter(session_key=rememberme_cookie).select_related(UserSession.user).get_or_none()
)
if (user_session is None) or (user_session.user is None): if (user_session is None) or (user_session.user is None):
raise HTTPException(status_code=401, detail="No user session") raise HTTPException(status_code=401, detail="No user session")
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60) access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60)
access_token = create_access_token( access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires)
data={"sub": user_session.user.email}, expires_delta=access_token_expires response.set_cookie(
key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="strict",
max_age=settings.access_token_expire_minutes * 60,
) )
response.set_cookie(key="access_token", value=f"Bearer {access_token}",
httponly=True, samesite='strict', max_age=settings.access_token_expire_minutes * 60)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60) response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
await user_session.update(last_seen=datetime.now()) await user_session.update(last_seen=datetime.now())
@@ -124,7 +159,11 @@ async def verify_user(verify_key: str):
@router.put("/password/update") @router.put("/password/update")
async def change_password(password_data: UpdatePassword, response: Response, user: User = Depends(get_current_user)): async def change_password(
password_data: UpdatePassword,
response: Response,
user: User = Depends(get_current_user),
):
if not verify_password(password_data.old_password, user.password): if not verify_password(password_data.old_password, user.password):
raise HTTPException(status_code=400, detail="Incorrect password") raise HTTPException(status_code=400, detail="Incorrect password")
user.password = get_password_hash(password_data.new_password) user.password = get_password_hash(password_data.new_password)
+2 -3
View File
@@ -6,6 +6,7 @@ from fastapi import APIRouter
from fastapi.responses import Response from fastapi.responses import Response
from classquiz.config import settings from classquiz.config import settings
settings = settings() settings = settings()
router = APIRouter() router = APIRouter()
@@ -16,9 +17,7 @@ async def get_qr(quiz_pin: str):
""" """
Get QR code for quiz Get QR code for quiz
""" """
factory = qrcode.image.svg.SvgImage qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage, version=1, box_size=20, border=0)
qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage, version=1
, box_size=20, border=0)
qr.add_data(f"{settings.root_address}/play?pin={quiz_pin}") qr.add_data(f"{settings.root_address}/play?pin={quiz_pin}")
buf = io.BytesIO() buf = io.BytesIO()
qr.make(fit=True) qr.make(fit=True)
+72 -29
View File
@@ -4,7 +4,7 @@ import aiohttp
import socketio import socketio
from classquiz.config import redis, settings from classquiz.config import redis, settings
from classquiz.db.models import GameSession, PlayGame from classquiz.db.models import PlayGame
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[]) sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[])
settings = settings() settings = settings()
@@ -14,35 +14,54 @@ settings = settings()
async def join_game(sid, data): async def join_game(sid, data):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
try: try:
async with session.post("https://hcaptcha.com/siteverify", async with session.post(
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp: "https://hcaptcha.com/siteverify",
data={"response": data["captcha"], "secret": settings.hcaptcha_key},
) as resp:
resp_data = await resp.json() resp_data = await resp.json()
if not resp_data["success"]: if not resp_data["success"]:
print("CAPTCHA FAILED") print("CAPTCHA FAILED")
return return
except KeyError as e: except KeyError:
print("CAPTCHA FAILED") print("CAPTCHA FAILED")
return return
redis_res = await redis.get(f"game:{data['game_pin']}") redis_res = await redis.get(f"game:{data['game_pin']}")
if redis_res is None: if redis_res is None:
await sio.emit("game_not_found", room=sid) await sio.emit("game_not_found", room=sid)
else: else:
session = {'game_pin': data["game_pin"], "username": data["username"], "admin": False} session = {
"game_pin": data["game_pin"],
"username": data["username"],
"admin": False,
}
await sio.save_session(sid, session) await sio.save_session(sid, session)
await sio.emit("joined_game", redis_res, room=sid) await sio.emit("joined_game", redis_res, room=sid)
redis_res = (await redis.get(f"game_session:{data['game_pin']}")) redis_res = await redis.get(f"game_session:{data['game_pin']}")
redis_res = json.loads(redis_res) redis_res = json.loads(redis_res)
# print(redis_res) # print(redis_res)
redis_res["players"].append({"username": data["username"], "sid": sid}) redis_res["players"].append({"username": data["username"], "sid": sid})
await redis.set(f"game_session:{data['game_pin']}", await redis.set(
json.dumps({"admin": redis_res["admin"], "game_id": redis_res["game_id"], f"game_session:{data['game_pin']}",
"players": redis_res["players"], "answers": []}), ex=18000) json.dumps(
await sio.emit("player_joined", {"username": data["username"], "sid": sid}, room=redis_res["admin"]) {
"admin": redis_res["admin"],
"game_id": redis_res["game_id"],
"players": redis_res["players"],
"answers": [],
}
),
ex=18000,
)
await sio.emit(
"player_joined",
{"username": data["username"], "sid": sid},
room=redis_res["admin"],
)
sio.enter_room(sid, data["game_pin"]) # TODO: make more secure sio.enter_room(sid, data["game_pin"]) # TODO: make more secure
@sio.event @sio.event
async def start_game(sid, data): async def start_game(sid, _data):
# print(sid, data, "START_GAME") # print(sid, data, "START_GAME")
session = await sio.get_session(sid) session = await sio.get_session(sid)
# print(session) # print(session)
@@ -55,14 +74,19 @@ async def register_as_admin(sid, data):
game_pin = data["game_pin"] game_pin = data["game_pin"]
game_id = data["game_id"] game_id = data["game_id"]
if (await redis.get(f"game_session:{game_pin}")) is None: if (await redis.get(f"game_session:{game_pin}")) is None:
await redis.set(f"game_session:{game_pin}", json.dumps({"admin": sid, "game_id": game_id, "players": []}), await redis.set(
ex=18000) f"game_session:{game_pin}",
json.dumps({"admin": sid, "game_id": game_id, "players": []}),
ex=18000,
)
await sio.emit("registered_as_admin", {"game_id": game_id, "game": await redis.get( await sio.emit(
f"game:{data['game_pin']}")}, "registered_as_admin",
room=sid) {"game_id": game_id, "game": await redis.get(f"game:{data['game_pin']}")},
room=sid,
)
async with sio.session(sid) as session: async with sio.session(sid) as session:
session['game_pin'] = data["game_pin"] session["game_pin"] = data["game_pin"]
session["admin"] = True session["admin"] = True
sio.enter_room(sid, data["game_pin"]) sio.enter_room(sid, data["game_pin"])
else: else:
@@ -74,7 +98,7 @@ async def get_question_results(sid, data):
session = await sio.get_session(sid) session = await sio.get_session(sid)
if session["admin"]: if session["admin"]:
redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}")
game_pin = session['game_pin'] game_pin = session["game_pin"]
await sio.emit("question_results", redis_res, room=game_pin) await sio.emit("question_results", redis_res, room=game_pin)
@@ -82,15 +106,15 @@ async def get_question_results(sid, data):
async def set_question_number(sid, data): async def set_question_number(sid, data):
session = await sio.get_session(sid) session = await sio.get_session(sid)
if session["admin"]: if session["admin"]:
game_pin = session['game_pin'] game_pin = session["game_pin"]
await sio.emit("set_question_number", data, room=game_pin) await sio.emit("set_question_number", data, room=game_pin)
@sio.event @sio.event
async def submit_answer(sid, data): async def submit_answer(sid, data):
session = await sio.get_session(sid) session = await sio.get_session(sid)
redis_res = await redis.get(f"game_session:{session['game_pin']}") # redis_res = await redis.get(f"game_session:{session['game_pin']}")
game_session = GameSession(**json.loads(redis_res)) # game_session = GameSession(**json.loads(redis_res))
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
# print(game_session) # print(game_session)
answer_right = False answer_right = False
@@ -100,21 +124,40 @@ async def submit_answer(sid, data):
break break
answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}") answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}")
if answers is None: if answers is None:
await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", await redis.set(
json.dumps( f"game_session:{session['game_pin']}:{data['question_index']}",
[{"username": session["username"], "answer": data["answer"], "right": answer_right}]), json.dumps(
ex=18000) [
{
"username": session["username"],
"answer": data["answer"],
"right": answer_right,
}
]
),
ex=18000,
)
else: else:
answers = json.loads(answers) answers = json.loads(answers)
answers.append({"username": session["username"], "answer": data["answer"], "right": answer_right}) answers.append(
await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", json.dumps(answers), ex=18000) {
"username": session["username"],
"answer": data["answer"],
"right": answer_right,
}
)
await redis.set(
f"game_session:{session['game_pin']}:{data['question_index']}",
json.dumps(answers),
ex=18000,
)
# await redis.set(f"game_data:{session['game_pin']}", json.dumps(data)) # await redis.set(f"game_data:{session['game_pin']}", json.dumps(data))
@sio.event @sio.event
async def get_game_data(sid, data): async def get_game_data(sid, _data):
game_pin = (await sio.get_session(sid))['game_pin'] game_pin = (await sio.get_session(sid))["game_pin"]
game_data = await redis.get(f"game:{game_pin}") game_data = await redis.get(f"game:{game_pin}")
if game_data is not None: if game_data is not None:
await sio.emit("game_data", json.loads(game_data), room=game_pin) await sio.emit("game_data", json.loads(game_data), room=game_pin)
+16 -7
View File
@@ -1,11 +1,18 @@
from .deta_storage import DetaStorage
from .local_storage import LocalStorage
from io import BytesIO from io import BytesIO
from typing import Optional from typing import Optional
from .deta_storage import DetaStorage
from .local_storage import LocalStorage
class Storage: class Storage:
def __init__(self, backend: str, deta_key: Optional[str], deta_id: Optional[str], storage_path: Optional[str]): def __init__(
self,
backend: str,
deta_key: Optional[str],
deta_id: Optional[str],
storage_path: Optional[str],
):
self.backend = backend self.backend = backend
self.deta_key: str | None = deta_key self.deta_key: str | None = deta_key
self.deta_id: str | None = deta_id self.deta_id: str | None = deta_id
@@ -14,8 +21,11 @@ class Storage:
if deta_key is None or deta_id is None: if deta_key is None or deta_id is None:
raise ValueError("deta_key and deta_id must be provided") raise ValueError("deta_key and deta_id must be provided")
else: else:
self.deta_instance = DetaStorage(deta_base_url=self.deta_base_url, deta_key=self.deta_key, self.deta_instance = DetaStorage(
deta_id=self.deta_id) deta_base_url=self.deta_base_url,
deta_key=self.deta_key,
deta_id=self.deta_id,
)
elif backend == "local": elif backend == "local":
if storage_path is None: if storage_path is None:
@@ -28,8 +38,7 @@ class Storage:
async def download(self, file_name: str) -> BytesIO | None: async def download(self, file_name: str) -> BytesIO | None:
if self.backend == "deta": if self.backend == "deta":
return await self.deta_instance.download( return await self.deta_instance.download(file_name)
file_name)
elif self.backend == "local": elif self.backend == "local":
return await self.local_instance.get_file(file_name) return await self.local_instance.get_file(file_name)
+2 -1
View File
@@ -1,6 +1,7 @@
from aiohttp import ClientSession
from io import BytesIO from io import BytesIO
from aiohttp import ClientSession
class DetaStorage: class DetaStorage:
def __init__(self, deta_base_url: str, deta_id: str, deta_key: str): def __init__(self, deta_base_url: str, deta_id: str, deta_key: str):
+3 -2
View File
@@ -1,5 +1,6 @@
import io import io
import os import os
import aiofiles import aiofiles
import aiofiles.os import aiofiles.os
@@ -10,13 +11,13 @@ class LocalStorage:
async def get_file(self, file_name: str) -> io.BytesIO | None: async def get_file(self, file_name: str) -> io.BytesIO | None:
try: try:
async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='rb') as f: async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="rb") as f:
return io.BytesIO(await f.read()) return io.BytesIO(await f.read())
except FileNotFoundError: except FileNotFoundError:
return None return None
async def write_file(self, file_name: str, data: bytes) -> None: async def write_file(self, file_name: str, data: bytes) -> None:
async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='wb') as f: async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="wb") as f:
await f.write(data) await f.write(data)
async def delete_file(self, file_names: [str]) -> None: async def delete_file(self, file_names: [str]) -> None:
+105 -52
View File
@@ -1,63 +1,116 @@
from classquiz.kahoot_importer.get import get
import pytest import pytest
from classquiz.kahoot_importer.get import get
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get(): async def test_get():
await get('1f95eb0b-fcf4-4db2-879b-5418ef75116b') await get("1f95eb0b-fcf4-4db2-879b-5418ef75116b")
await get("c63d4d39-767e-4754-8c16-66c3c2d9633c") await get("c63d4d39-767e-4754-8c16-66c3c2d9633c")
await get("c5d31de6-4598-4574-8e67-ca9a2aba49f8") await get("c5d31de6-4598-4574-8e67-ca9a2aba49f8")
await get("3d545fc3-b830-44b6-94ac-c8e4a3c9fb88") await get("3d545fc3-b830-44b6-94ac-c8e4a3c9fb88")
quiz_list = ['c63d4d39-767e-4754-8c16-66c3c2d9633c', 'a73689d2-1108-4524-88e1-10c54282346c', quiz_list = [
'e8e2d869-7c75-4ad6-8872-031bc8e37fa9', '387b4139-9e77-4b47-93b7-dbb6d75c54d1', "c63d4d39-767e-4754-8c16-66c3c2d9633c",
'a1fff657-b99d-4547-93cf-4e9629994844', '0880d792-a77e-4cba-8a8b-359de40ffe76', "a73689d2-1108-4524-88e1-10c54282346c",
'e2d12f01-a3dd-4d0e-a6c4-29f38046117f', 'e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7', "e8e2d869-7c75-4ad6-8872-031bc8e37fa9",
'e655c086-558d-4602-99fd-5fc429ffe0ce', '1642f29d-b5cb-4213-82e6-7c1fae401aca', "387b4139-9e77-4b47-93b7-dbb6d75c54d1",
'fabdd572-da3e-4621-b912-41b9bc5a18fb', 'ded3cbbf-5af3-4761-a6da-b3b6ca040e1a', "a1fff657-b99d-4547-93cf-4e9629994844",
'370b3e33-ed97-4d36-9362-7f76aa9d12b9', 'cfa32a28-1a01-47f1-9bca-c85640bceeb8', "0880d792-a77e-4cba-8a8b-359de40ffe76",
'993f615d-6890-4dd3-ac08-f462de9ac5a3', '7f08a377-e371-425e-95f9-7196401ae7ff', "e2d12f01-a3dd-4d0e-a6c4-29f38046117f",
'19656fed-5891-48e7-ae7f-1358f623bfd0', '5e766d32-930a-4640-82a1-87c58612f579', "e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7",
'ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5', 'dd3c9175-addd-48df-8297-7598d4f12006', "e655c086-558d-4602-99fd-5fc429ffe0ce",
'b8de862c-fc4b-4df6-9fa5-81b91868fc3b', '3b082f4e-9c50-45ea-98c4-3845b14e400c', "1642f29d-b5cb-4213-82e6-7c1fae401aca",
'10179f67-a344-4fd0-80a1-d1e1b6ca7953', '8288c1dd-9675-411c-9175-4180004da3c3', "fabdd572-da3e-4621-b912-41b9bc5a18fb",
'af838c22-dcbb-497d-8c7b-f6499835b83d', '152c5fdb-3b0a-4cdf-a0d9-1984de0643ad', "ded3cbbf-5af3-4761-a6da-b3b6ca040e1a",
'846fd2bb-6b95-4333-b5d4-ef0a363e73a9', 'dd6b446f-817b-469b-9e6b-e1d34c466a7f', "370b3e33-ed97-4d36-9362-7f76aa9d12b9",
'2720d636-8918-4c6a-86a9-928c489382f4', 'a6985641-df97-4e56-97fc-e0901e32b464', "cfa32a28-1a01-47f1-9bca-c85640bceeb8",
'1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9', '677fa485-4231-4479-beb2-fe757053bf41', "993f615d-6890-4dd3-ac08-f462de9ac5a3",
'2b6379f8-450e-46c0-a854-5e368a03d5a6', 'f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1', "7f08a377-e371-425e-95f9-7196401ae7ff",
'51d2e839-a9d7-4f26-b1fd-fb0743c3a236', 'f2212243-4553-456d-87e7-7ddfd6ee7563', "19656fed-5891-48e7-ae7f-1358f623bfd0",
'2d6982d3-7b46-4022-bbb6-cb5451c581ba', '1cccae22-5140-46b5-ba6a-aa17cec14dc3', "5e766d32-930a-4640-82a1-87c58612f579",
'b01dd2c2-ffcf-4c02-a274-e12296dbe4e1', '2669d193-a81f-4752-a003-dd2433beef92', "ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5",
'06203ee6-3615-4e26-bb08-4fab249698a4', '933c1426-c759-43a3-9770-c8e3948db998', "dd3c9175-addd-48df-8297-7598d4f12006",
'02046384-fb05-4f1d-855a-f0e52a0761e3', 'b5266e9b-7372-4df0-94a9-ec8a6c0aabc6', "b8de862c-fc4b-4df6-9fa5-81b91868fc3b",
'fb8e3a3d-505a-4157-bd9a-b73b00591f10', '42092675-9171-400f-ab4d-c898ec9f70bd', "3b082f4e-9c50-45ea-98c4-3845b14e400c",
'1d6307e2-98c2-42f2-b765-d02a68e0da80', '8f431ed0-31df-4435-bcba-aff46929fe82', "10179f67-a344-4fd0-80a1-d1e1b6ca7953",
'd573d76f-d715-4a54-bdde-7ac7b6235fb2', 'feb8985b-84fb-4d41-bc41-70c322f508fa', "8288c1dd-9675-411c-9175-4180004da3c3",
'f9f945cc-c166-4f7d-a95f-f0be154c43cf', '009b130a-1709-41a6-943d-85eee4889792', "af838c22-dcbb-497d-8c7b-f6499835b83d",
'a3f765f7-2329-47e0-83eb-0837b4b54b43', 'fea9e558-206f-444a-8d7f-a3f0301fda8d', "152c5fdb-3b0a-4cdf-a0d9-1984de0643ad",
'9b419098-ccfc-467d-9de0-db18b60d6c28', 'e6c2174e-3a3f-4193-9782-1bf3b5cfae63', "846fd2bb-6b95-4333-b5d4-ef0a363e73a9",
'd42c962e-1691-4fa4-8926-c33157423bd2', '8d27aa83-a103-4f05-8230-69e857bc3ffd', "dd6b446f-817b-469b-9e6b-e1d34c466a7f",
'60cd19b2-6858-4704-b07b-5196e8e24325', '31624ea4-1e4f-4f90-ae4f-759a0eb93b61', "2720d636-8918-4c6a-86a9-928c489382f4",
'fcd038f4-6b05-485b-afe3-ce67e05b1695', '1207bbe5-2df5-4c86-8f54-1b2675a86859', "a6985641-df97-4e56-97fc-e0901e32b464",
'eea3b0b0-deae-41de-b010-d6d50855d016', 'ca8b0278-744c-4a3f-a2b8-6ab29d76fa7f', "1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9",
'68d37bc8-28b9-49fd-9e02-48a3c9195fdf', '8bf13f61-c5cc-4332-aab9-37405d1fdd14', "677fa485-4231-4479-beb2-fe757053bf41",
'8ceef7de-9408-48d1-8bb9-b8ac3ef73596', '7e053a85-b04c-484b-8815-d26ce646c748', "2b6379f8-450e-46c0-a854-5e368a03d5a6",
'dfc09237-85b7-4596-b4e8-0bfbfd1aaeb7', 'd8d291ab-9dfa-45c0-bfc2-0a81167e3c63', "f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1",
'd0834157-d464-4632-b620-147e211e0614', '30758f35-fabc-48bc-b410-47450de64c82', "51d2e839-a9d7-4f26-b1fd-fb0743c3a236",
'7232bc80-a9cb-4668-9449-16b9282a2331', '29a194bd-2ecc-44bc-a6a9-6b8a25c3d860', "f2212243-4553-456d-87e7-7ddfd6ee7563",
'52b43400-117a-4e16-81b8-b510fe987750', '52ef0c45-9ba1-4d5e-ba0c-82703196ec6c', "2d6982d3-7b46-4022-bbb6-cb5451c581ba",
'9f0fa778-e6ae-4f7b-b4fd-2ccb1bd37eb7', '54b57afd-c91c-4d5f-8177-c3fe940bc723', "1cccae22-5140-46b5-ba6a-aa17cec14dc3",
'23620661-6eb6-41df-9de4-18ff99e6f35f', 'c983fe89-6ac3-4395-882e-4b11aa11a99d', "b01dd2c2-ffcf-4c02-a274-e12296dbe4e1",
'd4d01494-e7d0-4a38-aa93-523a77855371', 'e5a77df6-fd29-48c7-8df6-f625de0179e7', "2669d193-a81f-4752-a003-dd2433beef92",
'bca45eef-fc64-465f-9106-403814f180a0', '3b5c305d-be5e-4209-9ba2-c2d1620a5a4f', "06203ee6-3615-4e26-bb08-4fab249698a4",
'14c533a5-c15c-493b-8c7d-7c20a0bc5ea1', '7b3b1e40-c75d-4c2c-843b-1599c10123c2', "933c1426-c759-43a3-9770-c8e3948db998",
'54604482-72aa-4d64-a3bb-9d8dc4a38b41', 'baf6f609-2dce-4978-840b-b74261c5ba6d', "02046384-fb05-4f1d-855a-f0e52a0761e3",
'250c536f-6cd9-4707-8706-b682961be032', 'a9b08140-ae44-409a-9941-22794c3fd097', "b5266e9b-7372-4df0-94a9-ec8a6c0aabc6",
'e516bd18-c46f-46b5-88dc-c7eac5a0f5a9', '6a220133-c71f-44df-8912-16f5b72568cb', "fb8e3a3d-505a-4157-bd9a-b73b00591f10",
'b63c2adf-b8f3-4e43-8651-44574475621f', '01d88287-387b-4a34-b6d3-0931aba8eb1a', "42092675-9171-400f-ab4d-c898ec9f70bd",
'eb0cef3d-4e2b-45b3-88df-fc547a763d01', 'a79dee0f-8579-4e0a-b77f-0a468a3ca71f', "1d6307e2-98c2-42f2-b765-d02a68e0da80",
'62e50edb-03e6-4f95-a6f4-8ff3ef28a32b', '337fffe8-cc68-44b7-9636-2433f86ed5a9', "8f431ed0-31df-4435-bcba-aff46929fe82",
'bd68f7b7-f8bb-489e-ac29-f8cbfe1978b3', '93d2a7d6-06a6-41bf-9632-c24e1f1c917a'] "d573d76f-d715-4a54-bdde-7ac7b6235fb2",
"feb8985b-84fb-4d41-bc41-70c322f508fa",
"f9f945cc-c166-4f7d-a95f-f0be154c43cf",
"009b130a-1709-41a6-943d-85eee4889792",
"a3f765f7-2329-47e0-83eb-0837b4b54b43",
"fea9e558-206f-444a-8d7f-a3f0301fda8d",
"9b419098-ccfc-467d-9de0-db18b60d6c28",
"e6c2174e-3a3f-4193-9782-1bf3b5cfae63",
"d42c962e-1691-4fa4-8926-c33157423bd2",
"8d27aa83-a103-4f05-8230-69e857bc3ffd",
"60cd19b2-6858-4704-b07b-5196e8e24325",
"31624ea4-1e4f-4f90-ae4f-759a0eb93b61",
"fcd038f4-6b05-485b-afe3-ce67e05b1695",
"1207bbe5-2df5-4c86-8f54-1b2675a86859",
"eea3b0b0-deae-41de-b010-d6d50855d016",
"ca8b0278-744c-4a3f-a2b8-6ab29d76fa7f",
"68d37bc8-28b9-49fd-9e02-48a3c9195fdf",
"8bf13f61-c5cc-4332-aab9-37405d1fdd14",
"8ceef7de-9408-48d1-8bb9-b8ac3ef73596",
"7e053a85-b04c-484b-8815-d26ce646c748",
"dfc09237-85b7-4596-b4e8-0bfbfd1aaeb7",
"d8d291ab-9dfa-45c0-bfc2-0a81167e3c63",
"d0834157-d464-4632-b620-147e211e0614",
"30758f35-fabc-48bc-b410-47450de64c82",
"7232bc80-a9cb-4668-9449-16b9282a2331",
"29a194bd-2ecc-44bc-a6a9-6b8a25c3d860",
"52b43400-117a-4e16-81b8-b510fe987750",
"52ef0c45-9ba1-4d5e-ba0c-82703196ec6c",
"9f0fa778-e6ae-4f7b-b4fd-2ccb1bd37eb7",
"54b57afd-c91c-4d5f-8177-c3fe940bc723",
"23620661-6eb6-41df-9de4-18ff99e6f35f",
"c983fe89-6ac3-4395-882e-4b11aa11a99d",
"d4d01494-e7d0-4a38-aa93-523a77855371",
"e5a77df6-fd29-48c7-8df6-f625de0179e7",
"bca45eef-fc64-465f-9106-403814f180a0",
"3b5c305d-be5e-4209-9ba2-c2d1620a5a4f",
"14c533a5-c15c-493b-8c7d-7c20a0bc5ea1",
"7b3b1e40-c75d-4c2c-843b-1599c10123c2",
"54604482-72aa-4d64-a3bb-9d8dc4a38b41",
"baf6f609-2dce-4978-840b-b74261c5ba6d",
"250c536f-6cd9-4707-8706-b682961be032",
"a9b08140-ae44-409a-9941-22794c3fd097",
"e516bd18-c46f-46b5-88dc-c7eac5a0f5a9",
"6a220133-c71f-44df-8912-16f5b72568cb",
"b63c2adf-b8f3-4e43-8651-44574475621f",
"01d88287-387b-4a34-b6d3-0931aba8eb1a",
"eb0cef3d-4e2b-45b3-88df-fc547a763d01",
"a79dee0f-8579-4e0a-b77f-0a468a3ca71f",
"62e50edb-03e6-4f95-a6f4-8ff3ef28a32b",
"337fffe8-cc68-44b7-9636-2433f86ed5a9",
"bd68f7b7-f8bb-489e-ac29-f8cbfe1978b3",
"93d2a7d6-06a6-41bf-9632-c24e1f1c917a",
]
rounds = 0 rounds = 0
for i in quiz_list: for i in quiz_list:
rounds = rounds + 1 rounds = rounds + 1
+3 -1
View File
@@ -1,10 +1,12 @@
from classquiz.kahoot_importer.search import search
import pytest import pytest
from classquiz.kahoot_importer.search import search
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_search(): async def test_search():
res = await search(query="Python", limit=100) res = await search(query="Python", limit=100)
print(res)
assert len(res.entities) == 100 assert len(res.entities) == 100
await search(query="Test Quiz", limit=100) await search(query="Test Quiz", limit=100)
await search(query="Biologie", limit=100) await search(query="Biologie", limit=100)
+14 -6
View File
@@ -1,4 +1,5 @@
import pytest import pytest
from classquiz.config import settings from classquiz.config import settings
from classquiz.storage import Storage from classquiz.storage import Storage
@@ -9,11 +10,14 @@ file_contents = b"hello world"
def test_storage_init(): def test_storage_init():
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
Storage(backend="asdsad", deta_key=settings.deta_project_key, Storage(
deta_id=settings.deta_project_id, storage_path=settings.storage_path) backend="asdsad",
deta_key=settings.deta_project_key,
deta_id=settings.deta_project_id,
storage_path=settings.storage_path,
)
with pytest.raises(ValueError): with pytest.raises(ValueError):
Storage(backend="deta", deta_key=None, Storage(backend="deta", deta_key=None, deta_id=None, storage_path=None)
deta_id=None, storage_path=None)
with pytest.raises(ValueError): with pytest.raises(ValueError):
Storage(backend="local", storage_path=None, deta_key=None, deta_id=None) Storage(backend="local", storage_path=None, deta_key=None, deta_id=None)
@@ -33,8 +37,12 @@ async def storage_tester(storage: Storage):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_deta(): async def test_deta():
storage: Storage = Storage(backend="deta", deta_key=settings.deta_project_key, storage: Storage = Storage(
deta_id=settings.deta_project_id, storage_path=settings.storage_path) backend="deta",
deta_key=settings.deta_project_key,
deta_id=settings.deta_project_id,
storage_path=settings.storage_path,
)
await storage_tester(storage) await storage_tester(storage)
+6 -4
View File
@@ -1,6 +1,6 @@
### Build Step ### Build Step
# pull the Node.js Docker image # pull the Node.js Docker image
FROM node:16 as builder FROM node:17.8-bullseye as builder
# ENV API_URL=http://api:80 # ENV API_URL=http://api:80
ENV API_URL=https://mawoka.eu ENV API_URL=https://mawoka.eu
ENV REDIS_URL=CHANGE_ME ENV REDIS_URL=CHANGE_ME
@@ -11,9 +11,10 @@ WORKDIR /usr/src/app
# copy the package.json files from local machine to the workdir in container # copy the package.json files from local machine to the workdir in container
COPY package*.json ./ COPY package*.json ./
COPY pnpm-lock.yaml ./
# run npm install in our local machine # run npm install in our local machine
RUN apt update && apt install -y curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && pnpm i RUN corepack enable && corepack prepare pnpm@7.0.0-rc.2 --activate && pnpm i
# copy the generated modules and all other files to the container # copy the generated modules and all other files to the container
COPY . . COPY . .
@@ -23,14 +24,15 @@ RUN pnpm run build
### Serve Step ### Serve Step
# pull the Node.js Docker image # pull the Node.js Docker image
FROM node:16 FROM node:17.8-bullseye-slim
# change working directory # change working directory
WORKDIR /app WORKDIR /app
RUN apt update && apt install -y curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm && apt purge curl -y && apt autoremove -y RUN corepack enable && corepack prepare pnpm@7.0.0-rc.2 --activate && pnpm i
# copy files from previous step # copy files from previous step
COPY --from=builder /usr/src/app/build . COPY --from=builder /usr/src/app/build .
COPY --from=builder /usr/src/app/package.json . COPY --from=builder /usr/src/app/package.json .
COPY --from=builder /usr/src/app/pnpm-lock.yaml .
COPY --from=builder /usr/src/app/node_modules ./node_modules COPY --from=builder /usr/src/app/node_modules ./node_modules
# our app is running on port 3000 within the container, so need to expose it # our app is running on port 3000 within the container, so need to expose it
+2
View File
@@ -22,6 +22,8 @@
"@sveltejs/adapter-node": "^1.0.0-next.71", "@sveltejs/adapter-node": "^1.0.0-next.71",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@tailwindcss/typography": "^0.5.0-alpha.3", "@tailwindcss/typography": "^0.5.0-alpha.3",
"@types/cookie": "^0.4.1",
"@types/ioredis": "^4.28.10",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.1",
"@types/luxon": "^2.0.9", "@types/luxon": "^2.0.9",
"@types/yup": "^0.29.13", "@types/yup": "^0.29.13",
+3153 -4162
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -25,12 +25,12 @@ export async function getSession(event) {
if (redis_res === null) { if (redis_res === null) {
const res = await fetch(`${process.env.API_URL}/api/v1/users/check`, { const res = await fetch(`${process.env.API_URL}/api/v1/users/check`, {
headers: { headers: {
Cookie: `access_token=Bearer ${event.locals.token}` Cookie: `access_token=Bearer ${event.locals.token}` // skipcq: JS-0378
} }
}); });
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
//@ts-ignore //@ts-ignore // skipcq: JS-0295
user_email = json.email; user_email = json.email;
} else { } else {
user_email = null; user_email = null;
+2 -2
View File
@@ -24,7 +24,7 @@ export class I18nService {
} }
// Initializing i18n // Initializing i18n
initialize() { initialize(): void {
this.i18n.use(LanguageDetector).init({ this.i18n.use(LanguageDetector).init({
// lng: INITIAL_LANGUAGE, // lng: INITIAL_LANGUAGE,
fallbackLng: 'en', fallbackLng: 'en',
@@ -50,7 +50,7 @@ export class I18nService {
this.i18n.addResourceBundle('fr', 'translation', fr); this.i18n.addResourceBundle('fr', 'translation', fr);
} }
changeLanguage(language: string) { changeLanguage(language: string): void {
this.i18n.changeLanguage(language); this.i18n.changeLanguage(language);
} }
} }
+115 -114
View File
@@ -1,116 +1,117 @@
{ {
"index_page": { "index_page": {
"meta": { "meta": {
"title": "Home", "title": "Home",
"description": "ClassQuiz ist eine Quiz-App wie KAHOOT! für Schüler*innen, welche open-source und kostenlos ist" "description": "ClassQuiz ist eine Quiz-App wie KAHOOT! für Schüler*innen, welche open-source und kostenlos ist"
}, },
"features_description": { "features_description": {
"1": "ClassQuiz ist eine Quiz-Platform, welche das erstellen und managen von Quizzes erlaubt.", "1": "ClassQuiz ist eine Quiz-Platform, welche das erstellen und managen von Quizzes erlaubt.",
"2": "Das Hauptfeature ist die KAHOOT!-Importfunktion, mit der man Quizzes von KAHOOT! importieren kann." "2": "Das Hauptfeature ist die KAHOOT!-Importfunktion, mit der man Quizzes von KAHOOT! importieren kann."
}, },
"slogan": "Die open-source Quiz-Platform!" "slogan": "Die open-source Quiz-Platform!"
}, },
"overview_page": { "overview_page": {
"created_at": "Erstellt am", "created_at": "Erstellt am",
"question_count": "Fragenanzahl" "question_count": "Fragenanzahl"
}, },
"edit_page": { "edit_page": {
"success_update_title": "Quiz erfolgreich aktualisiert!", "success_update_title": "Quiz erfolgreich aktualisiert!",
"success_update_body": "Das Quiz wurde erfolgreich aktualisiert!" "success_update_body": "Das Quiz wurde erfolgreich aktualisiert!"
}, },
"create_page": { "create_page": {
"success": { "success": {
"title": "Quiz erfolgreich erstellt!", "title": "Quiz erfolgreich erstellt!",
"body": "Das Quiz wurde erfolgreich erstellt!" "body": "Das Quiz wurde erfolgreich erstellt!"
} }
}, },
"register_page": { "register_page": {
"greeting": "Schön, dich zu sehen!", "greeting": "Schön, dich zu sehen!",
"create_account": "Account erstellen", "create_account": "Account erstellen",
"forgot_password?": "Passwort vergessen?", "forgot_password?": "Passwort vergessen?",
"already_have_account?": "Hast du schon einen Account?" "already_have_account?": "Hast du schon einen Account?"
}, },
"login_page": { "login_page": {
"welcome_back": "Willkommen zurück!", "welcome_back": "Willkommen zurück!",
"login_or_create_account": "Einloggen oder Account erstellen", "login_or_create_account": "Einloggen oder Account erstellen",
"already_have_account": "Noch kein Account?", "already_have_account": "Noch kein Account?",
"modal": { "modal": {
"success": { "success": {
"success_check_mail": "Login erfolgreich! Guck in deine Mailbox!", "success_check_mail": "Login erfolgreich! Guck in deine Mailbox!",
"success": "Login erfolgreich!", "success": "Login erfolgreich!",
"description": { "description": {
"success_check_mail": "Bitte überprüfe deine Mailbox, weil du eine Email mit einem Link, um dich einzuloggen, empfangen haben solltest.", "success_check_mail": "Bitte überprüfe deine Mailbox, weil du eine Email mit einem Link, um dich einzuloggen, empfangen haben solltest.",
"success": "Du hast dich erfolgreich angemeldet!" "success": "Du hast dich erfolgreich angemeldet!"
} }
}, },
"error": { "error": {
"wrong_creds": "Falsche Email oder falsches Passwort!", "wrong_creds": "Falsche Email oder falsches Passwort!",
"unexpected": "Unerwarteter Fehler!", "unexpected": "Unerwarteter Fehler!",
"description": { "description": {
"wrong_creds": "Bitte stelle sicher, dass dein Passwort und deine Email richtig sind!", "wrong_creds": "Bitte stelle sicher, dass dein Passwort und deine Email richtig sind!",
"unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!" "unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!"
} }
} }
} }
}, },
"words": { "words": {
"question": "Frage", "question": "Frage",
"answer": "Antwort", "answer": "Antwort",
"stats": "Statistiken", "stats": "Statistiken",
"features": "Features", "features": "Features",
"import": "Importieren", "import": "Importieren",
"url": "URL", "url": "URL",
"submit": "Abschicken", "submit": "Abschicken",
"create": "Erstellen", "create": "Erstellen",
"logout": "Abmelden", "logout": "Abmelden",
"title": "Titel", "title": "Titel",
"play": "Spielen", "play": "Spielen",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"delete": "Löschen", "delete": "Löschen",
"public": "Öffentlich", "public": "Öffentlich",
"start": "Start", "start": "Start",
"connect": "Verbinden", "connect": "Verbinden",
"pin": "PIN", "pin": "PIN",
"kick": "Kicken", "kick": "Kicken",
"register": "Registrieren", "register": "Registrieren",
"login": "Anmelden", "login": "Anmelden",
"docs": "Dokumentation", "docs": "Dokumentation",
"close": "Schließen", "close": "Schließen",
"save": "Speichern", "save": "Speichern",
"username": "Nutzername", "username": "Nutzername",
"password": "Passwort", "password": "Passwort",
"email": "Email", "email": "Email",
"description": "Beschreibung", "description": "Beschreibung",
"image": "Bild", "image": "Bild",
"settings": "Einstellungen", "settings": "Einstellungen",
"repeat_password": "Passwort wiederholen" "repeat_password": "Passwort wiederholen",
}, "overview": "Übersicht"
"editor": { },
"time_in_seconds": "Zeit in Sekunden", "editor": {
"right_or_true?": "Richtig?", "time_in_seconds": "Zeit in Sekunden",
"add_new_answer": "Neue Antwort hinzufügen", "right_or_true?": "Richtig?",
"add_new_question": "Neue Frage hinzufügen", "add_new_answer": "Neue Antwort hinzufügen",
"delete_question": "Frage löschen", "add_new_question": "Neue Frage hinzufügen",
"delete_answer": "Antwort löschen" "delete_question": "Frage löschen",
}, "delete_answer": "Antwort löschen"
"import": { },
"need_help": "", "import": {
"visit_docs": "" "need_help": "",
}, "visit_docs": ""
"admin": { },
"already_registered_as_admin": "" "admin": {
}, "already_registered_as_admin": ""
"admin_page": { },
"already_registered_as_admin": "Ein Admin wurde schon für dieses Spiel registriert.", "admin_page": {
"start_game": "Spiel starten", "already_registered_as_admin": "Ein Admin wurde schon für dieses Spiel registriert.",
"time_left": "Zeit übrig", "start_game": "Spiel starten",
"get_results": "Ergebnisse abfragen" "time_left": "Zeit übrig",
}, "get_results": "Ergebnisse abfragen"
"import_page": { },
"need_help": "Brauchst du Hilfe?", "import_page": {
"visit_docs": "Besuche die Dokumentation" "need_help": "Brauchst du Hilfe?",
}, "visit_docs": "Besuche die Dokumentation"
"password_reset_page": { },
"reset_password": "Passwort zurücksetzen" "password_reset_page": {
} "reset_password": "Passwort zurücksetzen"
}
} }
+108 -107
View File
@@ -1,109 +1,110 @@
{ {
"index_page": { "index_page": {
"slogan": "The open-source quiz-platform!", "slogan": "The open-source quiz-platform!",
"meta": { "meta": {
"description": "ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use", "description": "ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use",
"title": "Home" "title": "Home"
}, },
"features_description": { "features_description": {
"1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.", "1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.",
"2": "The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes." "2": "The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes."
} }
}, },
"overview_page": { "overview_page": {
"created_at": "Created at", "created_at": "Created at",
"question_count": "Question count" "question_count": "Question count"
}, },
"edit_page": { "edit_page": {
"success_update_title": "Successfully updated quiz!", "success_update_title": "Successfully updated quiz!",
"success_update_body": "Updated the quiz successfully!" "success_update_body": "Updated the quiz successfully!"
}, },
"create_page": { "create_page": {
"success": { "success": {
"title": "Successfully created quiz!", "title": "Successfully created quiz!",
"body": "Created the quiz successfully!" "body": "Created the quiz successfully!"
} }
}, },
"register_page": { "register_page": {
"greeting": "Nice to meet you!", "greeting": "Nice to meet you!",
"create_account": "Create account", "create_account": "Create account",
"forgot_password?": "Forgot password?", "forgot_password?": "Forgot password?",
"already_have_account?": "Already have an account?" "already_have_account?": "Already have an account?"
}, },
"login_page": { "login_page": {
"welcome_back": "Welcome back!", "welcome_back": "Welcome back!",
"login_or_create_account": "Login or create an account", "login_or_create_account": "Login or create an account",
"already_have_account": "Don't have an account?", "already_have_account": "Don't have an account?",
"modal": { "modal": {
"success": { "success": {
"success_check_mail": "Login Succeded! Check your mailbox!", "success_check_mail": "Login Succeded! Check your mailbox!",
"success": "Login Succeded!", "success": "Login Succeded!",
"description": { "description": {
"success_check_mail": "Please check your mailbox, since you should have received a mail, with a link you can click to login.", "success_check_mail": "Please check your mailbox, since you should have received a mail, with a link you can click to login.",
"success": "You've successfully logged in!" "success": "You've successfully logged in!"
} }
}, },
"error": { "error": {
"wrong_creds": "Wrong email or password!", "wrong_creds": "Wrong email or password!",
"unexpected": "Unexpected error!", "unexpected": "Unexpected error!",
"description": { "description": {
"wrong_creds": "Please make sure that your password and your email are correct!", "wrong_creds": "Please make sure that your password and your email are correct!",
"unexpected": "There was the good old unexpected error!" "unexpected": "There was the good old unexpected error!"
} }
} }
} }
}, },
"words": { "words": {
"question": "Question", "question": "Question",
"answer": "Answer", "answer": "Answer",
"stats": "Stats", "stats": "Stats",
"features": "Features", "features": "Features",
"login": "Login", "login": "Login",
"email": "Email", "email": "Email",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"play": "Play", "play": "Play",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"public": "Public", "public": "Public",
"start": "Start", "start": "Start",
"create": "Create", "create": "Create",
"import": "Import", "import": "Import",
"logout": "Logout", "logout": "Logout",
"title": "Title", "title": "Title",
"url": "URL", "url": "URL",
"submit": "Submit", "submit": "Submit",
"connect": "Connect", "connect": "Connect",
"pin": "PIN", "pin": "PIN",
"kick": "Kick", "kick": "Kick",
"register": "Register", "register": "Register",
"docs": "Docs", "docs": "Docs",
"close": "Close", "close": "Close",
"save": "Save", "save": "Save",
"description": "Description", "description": "Description",
"image": "Image", "image": "Image",
"settings": "Settings", "settings": "Settings",
"repeat_password": "Repeat password" "repeat_password": "Repeat password",
}, "overview": "Overview"
"editor": { },
"time_in_seconds": "Time in seconds", "editor": {
"right_or_true?": "Right?", "time_in_seconds": "Time in seconds",
"add_new_answer": "Add new answer", "right_or_true?": "Right?",
"add_new_question": "Add new question", "add_new_answer": "Add new answer",
"delete_question": "Delete question", "add_new_question": "Add new question",
"delete_answer": "Delete answer" "delete_question": "Delete question",
}, "delete_answer": "Delete answer"
"import_page": { },
"need_help": "Need help?", "import_page": {
"visit_docs": "Visit the documentation" "need_help": "Need help?",
}, "visit_docs": "Visit the documentation"
"admin_page": { },
"already_registered_as_admin": "There is already an admin registered for this game.", "admin_page": {
"start_game": "Start game", "already_registered_as_admin": "There is already an admin registered for this game.",
"time_left": "Time left", "start_game": "Start game",
"get_results": "Get results" "time_left": "Time left",
}, "get_results": "Get results"
"password_reset_page": { },
"reset_password": "Reset password" "password_reset_page": {
} "reset_password": "Reset password"
}
} }
+108 -107
View File
@@ -1,109 +1,110 @@
{ {
"login_page": { "login_page": {
"modal": { "modal": {
"success": { "success": {
"description": { "description": {
"success": "Vous vous êtes connecté avec succès !", "success": "Vous vous êtes connecté avec succès !",
"success_check_mail": "Veuillez vérifier votre boîte mail, vous avez normalement reçu un mail avec un lien pour vous connecter." "success_check_mail": "Veuillez vérifier votre boîte mail, vous avez normalement reçu un mail avec un lien pour vous connecter."
}, },
"success": "Connexion réussie !", "success": "Connexion réussie !",
"success_check_mail": "Connexion réussie! Vérifiez votre boîte mail!" "success_check_mail": "Connexion réussie! Vérifiez votre boîte mail!"
}, },
"error": { "error": {
"wrong_creds": "Mot de passe ou email non-correct !", "wrong_creds": "Mot de passe ou email non-correct !",
"description": { "description": {
"wrong_creds": "Assurez-vous que votre mot de passe et votre email sont corrects !", "wrong_creds": "Assurez-vous que votre mot de passe et votre email sont corrects !",
"unexpected": "Une bonne vieille erreur inattendue s'est produite !" "unexpected": "Une bonne vieille erreur inattendue s'est produite !"
}, },
"unexpected": "Erreur inattendue !" "unexpected": "Erreur inattendue !"
} }
}, },
"welcome_back": "Bon retour parmi nous!", "welcome_back": "Bon retour parmi nous!",
"login_or_create_account": "Se connecter ou créer un compte", "login_or_create_account": "Se connecter ou créer un compte",
"already_have_account": "Pas encore de compte ?" "already_have_account": "Pas encore de compte ?"
}, },
"words": { "words": {
"question": "Question", "question": "Question",
"answer": "Réponse", "answer": "Réponse",
"stats": "Statistiques", "stats": "Statistiques",
"features": "Fonctionnalités", "features": "Fonctionnalités",
"login": "Connexion", "login": "Connexion",
"email": "Email", "email": "Email",
"username": "Pseudo", "username": "Pseudo",
"password": "Mot de passe", "password": "Mot de passe",
"play": "Jouer", "play": "Jouer",
"edit": "Modifier", "edit": "Modifier",
"delete": "Supprimer", "delete": "Supprimer",
"public": "Public", "public": "Public",
"start": "Commencer", "start": "Commencer",
"create": "Créer", "create": "Créer",
"import": "Importer", "import": "Importer",
"logout": "Se déconnecter", "logout": "Se déconnecter",
"url": "URL", "url": "URL",
"submit": "Valider", "submit": "Valider",
"connect": "Connexion", "connect": "Connexion",
"pin": "PIN", "pin": "PIN",
"kick": "Éjecter", "kick": "Éjecter",
"register": "Enregistrer", "register": "Enregistrer",
"docs": "Documentation", "docs": "Documentation",
"close": "Fermer", "close": "Fermer",
"save": "Enregistrer", "save": "Enregistrer",
"description": "Description", "description": "Description",
"image": "Image", "image": "Image",
"title": "titre", "title": "titre",
"settings": "Paramètres", "settings": "Paramètres",
"repeat_password": "" "repeat_password": "",
}, "overview": ""
"index_page": { },
"slogan": "La plateforme de quiz open-source !", "index_page": {
"meta": { "slogan": "La plateforme de quiz open-source !",
"description": "ClassQuiz est une application de quiz comme KAHOOT! pour les étudiants, libre et open-source", "meta": {
"title": "Accueil" "description": "ClassQuiz est une application de quiz comme KAHOOT! pour les étudiants, libre et open-source",
}, "title": "Accueil"
"features_description": { },
"1": "ClassQuiz est un outil de quiz vous permettant de créer et gérer des questionnaires.", "features_description": {
"2": "La fonctionnalité principale est la fonction d'import de quiz KAHOOT, vou permettant de créer des quiz directement depuis KAHOOT!." "1": "ClassQuiz est un outil de quiz vous permettant de créer et gérer des questionnaires.",
} "2": "La fonctionnalité principale est la fonction d'import de quiz KAHOOT, vou permettant de créer des quiz directement depuis KAHOOT!."
}, }
"overview_page": { },
"created_at": "Crée le", "overview_page": {
"question_count": "Nombre de question" "created_at": "Crée le",
}, "question_count": "Nombre de question"
"edit_page": { },
"success_update_title": "Quiz mis à jour avec succès!", "edit_page": {
"success_update_body": "Quiz mis à jour avec succès !" "success_update_title": "Quiz mis à jour avec succès!",
}, "success_update_body": "Quiz mis à jour avec succès !"
"create_page": { },
"success": { "create_page": {
"body": "Quiz créé avec succès !", "success": {
"title": "Quiz créé avec succès !" "body": "Quiz créé avec succès !",
} "title": "Quiz créé avec succès !"
}, }
"register_page": { },
"greeting": "Ravi de vous connaître !", "register_page": {
"create_account": "Créer un compte", "greeting": "Ravi de vous connaître !",
"forgot_password?": "Mot de passe oublié ?", "create_account": "Créer un compte",
"already_have_account?": "Vous avez déjà un compte ?" "forgot_password?": "Mot de passe oublié ?",
}, "already_have_account?": "Vous avez déjà un compte ?"
"editor": { },
"time_in_seconds": "Temps en seconde", "editor": {
"add_new_question": "Ajouter une autre question", "time_in_seconds": "Temps en seconde",
"add_new_answer": "Ajouter une autre réponse", "add_new_question": "Ajouter une autre question",
"delete_question": "Supprimer la question", "add_new_answer": "Ajouter une autre réponse",
"delete_answer": "Supprimer la réponse", "delete_question": "Supprimer la question",
"right_or_true?": "Vrai?" "delete_answer": "Supprimer la réponse",
}, "right_or_true?": "Vrai?"
"import_page": { },
"need_help": "Besoin d'aide ?", "import_page": {
"visit_docs": "Voir la documentation" "need_help": "Besoin d'aide ?",
}, "visit_docs": "Voir la documentation"
"admin_page": { },
"already_registered_as_admin": "Il y a déjà un administrateur enregistré pour ce jeu.", "admin_page": {
"start_game": "Commencer le jeu", "already_registered_as_admin": "Il y a déjà un administrateur enregistré pour ce jeu.",
"time_left": "Temps restant", "start_game": "Commencer le jeu",
"get_results": "Obtenir les résultats" "time_left": "Temps restant",
}, "get_results": "Obtenir les résultats"
"password_reset_page": { },
"reset_password": "" "password_reset_page": {
} "reset_password": ""
}
} }
+64 -35
View File
@@ -1,6 +1,7 @@
<script> <script>
import '@fontsource/marck-script/index.css'; import '@fontsource/marck-script/index.css';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { signedIn } from '$lib/stores';
const { t } = getLocalization(); const { t } = getLocalization();
@@ -17,15 +18,11 @@
<nav <nav
class="w-screen px-4 lg:px-10 py-2 flex flex-col lg:flex-row lg:items-center fixed backdrop-blur-sm bg-white/60 shadow-md z-50 top-0" class="w-screen px-4 lg:px-10 py-2 flex flex-col lg:flex-row lg:items-center fixed backdrop-blur-sm bg-white/60 shadow-md z-50 top-0"
> >
<!-- Our logo and button -->
<section class="w-full lg:w-max flex justify-between"> <section class="w-full lg:w-max flex justify-between">
<!-- Logo -->
<a href="/" class="font-black tracking-tight text-xl text-black marck-script link-hover" <a href="/" class="font-black tracking-tight text-xl text-black marck-script link-hover"
>ClassQuiz</a >ClassQuiz</a
> >
<!-- Our open/close buttons -->
<!-- Open menu -->
<button <button
class="lg:hidden" class="lg:hidden"
id="open-menu" id="open-menu"
@@ -76,41 +73,73 @@
</button> </button>
</section> </section>
<!-- Our list of items -->
<ul <ul
id="menu-items" id="menu-items"
class="lg:flex w-full flex-col lg:flex-row lg:pl-6" class="lg:flex w-full flex-col lg:flex-row lg:pl-6"
class:hidden={!menuItems} class:hidden={!menuItems}
> >
<li class="py-2"> {#if $signedIn}
<a <li class="py-2">
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover" <a
href="/play">{$t('words.play')}</a class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
> href="/play">{$t('words.play')}</a
</li> >
<li class="py-2"> </li>
<a <li class="py-2">
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover" <a
href="/account/register">{$t('words.register')}</a class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
> href="/overview">{$t('words.overview')}</a
</li> >
<li class="py-2"> </li>
<a <li class="py-2">
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover" <a
href="/account/login">{$t('words.login')}</a class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
> href="https://github.com/mawoka-myblock/classquiz">GitHub</a
</li> >
<li class="py-2"> </li>
<a <li class="py-2">
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover" <a
href="https://github.com/mawoka-myblock/classquiz">GitHub</a class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
> href="/api/v1/users/logout">{$t('words.logout')}</a
</li> >
<li class="py-2"> </li>
<a <li class="py-2">
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover" <a
href="/docs">{$t('words.docs')}</a class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
> href="/docs">{$t('words.docs')}</a
</li> >
</li>
{:else}
<li class="py-2">
<a
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
href="/play">{$t('words.play')}</a
>
</li>
<li class="py-2">
<a
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
href="/account/register">{$t('words.register')}</a
>
</li>
<li class="py-2">
<a
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
href="/account/login">{$t('words.login')}</a
>
</li>
<li class="py-2">
<a
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
href="https://github.com/mawoka-myblock/classquiz">GitHub</a
>
</li>
<li class="py-2">
<a
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
href="/docs">{$t('words.docs')}</a
>
</li>
{/if}
</ul> </ul>
</nav> </nav>
+2 -2
View File
@@ -13,7 +13,7 @@
execute: async (_a, _b) => ({ response: '' }), execute: async (_a, _b) => ({ response: '' }),
render: (_a, _b) => {} render: (_a, _b) => {}
}; };
let hcaptchaWidgetID; let hcaptchaWidgetID: any;
onMount(() => { onMount(() => {
if (browser) { if (browser) {
@@ -41,7 +41,7 @@
if (username.length <= 3) { if (username.length <= 3) {
return; return;
} }
let captcha_resp; let captcha_resp: string;
try { try {
const { response } = await hcaptcha.execute(hcaptchaWidgetID, { const { response } = await hcaptcha.execute(hcaptchaWidgetID, {
async: true async: true
+4 -3
View File
@@ -3,10 +3,11 @@
export let results: Array<Answer>; export let results: Array<Answer>;
export let game_data: QuizData; export let game_data: QuizData;
export let question_index: number; export let question_index: string;
let data_store = {}; let data_store = {};
const question = game_data.questions[question_index].answers;
const question = game_data.questions[parseInt(question_index)].answers;
for (let i = 0; i < question.length; i++) { for (let i = 0; i < question.length; i++) {
console.log(question[i].answer, 'HI!!!'); console.log(question[i].answer, 'HI!!!');
data_store[question[i].answer] = 0; data_store[question[i].answer] = 0;
@@ -56,7 +57,7 @@
</thead> </thead>
<tbody> <tbody>
<!-- Product 1 --> <!-- Product 1 -->
{#each game_data.questions[question_index].answers as answer} {#each game_data.questions[parseInt(question_index)].answers as answer}
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"> <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td <td
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white" class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
+1
View File
@@ -1,3 +1,4 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export const navbarVisible = writable(true); export const navbarVisible = writable(true);
export const signedIn = writable(false);
+4 -3
View File
@@ -1,16 +1,17 @@
<script> <script>
import '../app.css'; import '../app.css';
import Navbar from '$lib/navbar.svelte'; import Navbar from '$lib/navbar.svelte';
import { navbarVisible } from '$lib/stores.ts'; import { navbarVisible } from '$lib/stores';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import { initLocalizationContext } from '$lib/i18n'; import { initLocalizationContext } from '$lib/i18n';
initLocalizationContext(); initLocalizationContext();
if (import.meta.env.VITE_SENTRY !== null) { if (import.meta.env.VITE_SENTRY !== undefined) {
console.log(import.meta.env.VITE_SENTRY)
Sentry.init({ Sentry.init({
dsn: import.meta.env.VITE_SENTRY, dsn: String(import.meta.env.VITE_SENTRY),
integrations: [new BrowserTracing()], integrations: [new BrowserTracing()],
// Set tracesSampleRate to 1.0 to capture 100% // Set tracesSampleRate to 1.0 to capture 100%
@@ -0,0 +1,18 @@
<script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ session }) {
if (session.authenticated) {
signedIn.set(true);
}
return {};
}
</script>
<script lang="ts">
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
</script>
<slot />
@@ -10,18 +10,22 @@
} }
</script> </script>
<script lang="ts"> <script lang='ts'>
import { getLocalization } from '../../lib/i18n'; import { getLocalization } from '../../lib/i18n';
const { t } = getLocalization(); const { t } = getLocalization();
export let token: string; export let token: string;
let isSubmitting = false; let isSubmitting = false;
let passwordData = { interface PasswordData {
password1: string;
password2: string;
}
let passwordData: PasswordData = {
password1: '', password1: '',
password2: '' password2: ''
}; };
let passwordsValid = false; let passwordsValid = false;
const checkIfPasswordsValid = (pwdata) => { const checkIfPasswordsValid = (pwdata: PasswordData): void => {
passwordsValid = pwdata.password1 === pwdata.password2 && pwdata.password1.length >= 8; passwordsValid = pwdata.password1 === pwdata.password2 && pwdata.password1.length >= 8;
}; };
$: checkIfPasswordsValid(passwordData); $: checkIfPasswordsValid(passwordData);
@@ -51,7 +55,7 @@
}; };
</script> </script>
<div class="flex items-center justify-center h-full px-4"> <div class='flex items-center justify-center h-full px-4'>
<div> <div>
<div <div
class="w-full max-w-sm mx-auto overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800" class="w-full max-w-sm mx-auto overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800"
@@ -122,7 +126,7 @@
type="submit" type="submit"
> >
{#if isSubmitting} {#if isSubmitting}
<svg class="h-4 w-4 animate-spin mx-auto" viewBox="3 3 18 18"> <svg class='h-4 w-4 animate-spin mx-auto' viewBox='3 3 18 18'>
<path <path
class="fill-black" class="fill-black"
d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z" d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"
@@ -12,6 +12,9 @@
<script lang="ts"> <script lang="ts">
import { getLocalization } from '../../lib/i18n'; import { getLocalization } from '../../lib/i18n';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
const { t } = getLocalization(); const { t } = getLocalization();
+26 -18
View File
@@ -27,15 +27,20 @@
created_at: string; created_at: string;
} }
let changePasswordData = { interface ChangePasswordData {
oldPassword: string,
newPassword: string,
newPasswordConfirm: string
}
let changePasswordData: ChangePasswordData = {
oldPassword: '', oldPassword: '',
newPassword: '', newPassword: '',
newPasswordConfirm: '' newPasswordConfirm: ''
}; };
let passwordChangeDataValid = false; let passwordChangeDataValid = false;
const checkPasswords = (data) => { const checkPasswords = (data: ChangePasswordData): void => {
console.log('Check password');
passwordChangeDataValid = passwordChangeDataValid =
data.newPassword === data.newPasswordConfirm && data.newPassword === data.newPasswordConfirm &&
data.newPassword.length >= 8 && data.newPassword.length >= 8 &&
@@ -102,29 +107,32 @@
/> />
</div> </div>
<div> <div>
<h1 class="text-slate-100 text-4xl font-bold my-2">{user.username}</h1> <h1 class='text-slate-100 text-4xl font-bold my-2'>{user.username}</h1>
<p class="text-slate-100 text-lg mb-6 md:max-w-lg"> <p class='text-slate-100 text-lg mb-6 md:max-w-lg'>
{$t('words.email')}: {user.email} {$t('words.email')}: {user.email}
</p> </p>
<!-- TODO: Add translation --> <!-- TODO: Add translation -->
<form class="flex flex-col md:flex-row" on:submit|preventDefault={changePassword}> <form class='flex flex-col md:flex-row' on:submit|preventDefault={changePassword}>
<label <label
>Old Password:<input >Old Password:<input
class="m-2 text-black" type='password'
bind:value={changePasswordData.oldPassword} class='m-2 text-black'
/></label bind:value={changePasswordData.oldPassword}
/></label
> >
<label <label
>New Password:<input >New Password:<input
class="m-2 text-black" type='password'
bind:value={changePasswordData.newPassword} class='m-2 text-black'
/></label bind:value={changePasswordData.newPassword}
/></label
> >
<label <label
>New Password again:<input >New Password again:<input
class="m-2 text-black" type='password'
bind:value={changePasswordData.newPasswordConfirm} class='m-2 text-black'
/></label bind:value={changePasswordData.newPasswordConfirm}
/></label
> >
<button <button
class="border-2 px-2 py-1 rounded-md border-black text-slate-100 hover:bg-amber-700 hover:text-indigo-100 transition duration-75 disabled:cursor-not-allowed disabled:opacity-50" class="border-2 px-2 py-1 rounded-md border-black text-slate-100 hover:bg-amber-700 hover:text-indigo-100 transition duration-75 disabled:cursor-not-allowed disabled:opacity-50"
+7 -1
View File
@@ -27,6 +27,9 @@
import { socket } from '$lib/socket'; import { socket } from '$lib/socket';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(false);
const { t } = getLocalization(); const { t } = getLocalization();
@@ -38,7 +41,10 @@
export let auto_connect: boolean; export let auto_connect: boolean;
export let game_token: string; export let game_token: string;
let success = false; let success = false;
let players: Array<string> = []; interface Player {
username: string
}
let players: Array<Player> = [];
let errorMessage = ''; let errorMessage = '';
let game_started = false; let game_started = false;
let quiz_data: QuizData; let quiz_data: QuizData;
+8
View File
@@ -1,4 +1,6 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { signedIn } from '$lib/stores';
export async function load({ session, url }) { export async function load({ session, url }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
@@ -6,6 +8,9 @@
redirect: '/account/login' redirect: '/account/login'
}; };
} }
if (session.authenticated) {
signedIn.set(true);
}
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
const pin = url.searchParams.get('pin'); const pin = url.searchParams.get('pin');
let auto_connect = url.searchParams.get('connect') !== null; let auto_connect = url.searchParams.get('connect') !== null;
@@ -26,6 +31,9 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Editor from '$lib/editor.svelte'; import Editor from '$lib/editor.svelte';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
const { t } = getLocalization(); const { t } = getLocalization();
+3
View File
@@ -3,6 +3,9 @@
import Title from '$lib/play/title.svelte'; import Title from '$lib/play/title.svelte';
import Question from '$lib/play/question.svelte'; import Question from '$lib/play/question.svelte';
import { socket } from '$lib/socket'; import { socket } from '$lib/socket';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
interface GameMeta { interface GameMeta {
started: boolean; started: boolean;
+14
View File
@@ -1,5 +1,19 @@
<script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ session }) {
if (session.authenticated) {
signedIn.set(true);
}
return {};
}
</script>
<script lang="ts"> <script lang="ts">
import Footer from '$lib/footer.svelte'; import Footer from '$lib/footer.svelte';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
</script> </script>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
+9 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ url, session }) { export async function load({ url, session }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
@@ -6,6 +7,10 @@
redirect: '/account/login' redirect: '/account/login'
}; };
} }
if (session.authenticated) {
signedIn.set(true);
}
const quiz_id = url.searchParams.get('quiz_id'); const quiz_id = url.searchParams.get('quiz_id');
if (quiz_id === null) { if (quiz_id === null) {
return { return {
@@ -23,6 +28,9 @@
<script lang="ts"> <script lang="ts">
import Editor from '$lib/editor.svelte'; import Editor from '$lib/editor.svelte';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
const { t } = getLocalization(); const { t } = getLocalization();
@@ -94,7 +102,7 @@
d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z" d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z"
/> />
</svg> </svg>
{:then lol} {:then _}
{#if data !== undefined} {#if data !== undefined}
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2"> <form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
<Editor bind:data submit_button_text={$t('words.save')} /> <Editor bind:data submit_button_text={$t('words.save')} />
+8
View File
@@ -1,10 +1,15 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { signedIn } from '$lib/stores';
export async function load({ session }) { export async function load({ session }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
status: 302, status: 302,
redirect: '/account/login' redirect: '/account/login'
}; };
} else {
if (session.authenticated) {
signedIn.set(true);
}
} }
return { return {
props: { props: {
@@ -16,6 +21,9 @@
<script lang="ts"> <script lang="ts">
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
const { t } = getLocalization(); const { t } = getLocalization();
let url_input = ''; let url_input = '';
+11
View File
@@ -1,3 +1,14 @@
<script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ session }) {
if (session.authenticated) {
signedIn.set(true);
}
return {};
}
</script>
<script lang="ts"> <script lang="ts">
import { navbarVisible } from '$lib/stores'; import { navbarVisible } from '$lib/stores';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
+141 -126
View File
@@ -1,4 +1,4 @@
<script context="module" lang="ts"> <script context='module' lang='ts'>
export async function load({ session }) { export async function load({ session }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
@@ -14,11 +14,26 @@
} }
</script> </script>
<script lang="ts"> <script lang='ts'>
import type { QuizData } from '../app'; import type { Question } from '../app';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getLocalization } from '$lib/i18n'; import { getLocalization } from '$lib/i18n';
import Footer from '$lib/footer.svelte'; import Footer from '$lib/footer.svelte';
import { navbarVisible, signedIn } from '$lib/stores';
interface QuizData {
id: string;
public: boolean;
title: string;
description: string;
created_at: string;
updated_at: string;
user_id: string;
questions: Question[];
}
signedIn.set(true);
navbarVisible.set(true);
const { t } = getLocalization(); const { t } = getLocalization();
const getData = async (): Promise<Array<QuizData>> => { const getData = async (): Promise<Array<QuizData>> => {
@@ -59,161 +74,161 @@
<title>ClassQuiz - Overview</title> <title>ClassQuiz - Overview</title>
</svelte:head> </svelte:head>
<div class="min-h-screen flex flex-col"> <div class='min-h-screen flex flex-col'>
{#await getData()} {#await getData()}
<svg class="h-8 w-8 animate-spin mx-auto my-20" viewBox="3 3 18 18"> <svg class='h-8 w-8 animate-spin mx-auto my-20' viewBox='3 3 18 18'>
<path <path
class="fill-black" class='fill-black'
d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z" d='M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z'
/> />
<path <path
class="fill-blue-100" class='fill-blue-100'
d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z" d='M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z'
/> />
</svg> </svg>
{:then quizzes} {:then quizzes}
<div class="flex flex-col w-fit mx-auto"> <div class='flex flex-col w-fit mx-auto'>
<!-- <button <!-- <button
class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-gray-50 rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'> class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-gray-50 rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'>
Primary Primary
</button>--> </button>-->
<div class="w-full grid grid-cols-4 gap-2"> <div class='w-full grid grid-cols-4 gap-2'>
<a <a
href="/create" href='/create'
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50'
>{$t('words.create')} >{$t('words.create')}
</a> </a>
<a <a
href="/import" href='/import'
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50'
>{$t('words.import')} >{$t('words.import')}
</a> </a>
<a <a
href="/api/v1/users/logout" href='/api/v1/users/logout'
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50'
>{$t('words.logout')} >{$t('words.logout')}
</a> </a>
<a <a
href="/account/settings" href='/account/settings'
class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50'
> >
{$t('words.settings')} {$t('words.settings')}
</a> </a>
</div> </div>
<div class="overflow-x-auto sm:-mx-8 lg:-mx-8"> <div class='overflow-x-auto sm:-mx-8 lg:-mx-8'>
<div class="inline-block py-2 min-w-full sm:px-6 lg:px-8"> <div class='inline-block py-2 min-w-full sm:px-6 lg:px-8'>
<div class="overflow-hidden shadow-md sm:rounded-lg"> <div class='overflow-hidden shadow-md sm:rounded-lg'>
<table class="min-w-full"> <table class='min-w-full'>
<thead class="bg-gray-50 dark:bg-gray-700"> <thead class='bg-gray-50 dark:bg-gray-700'>
<tr> <tr>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('words.title')} {$t('words.title')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('overview_page.created_at')} {$t('overview_page.created_at')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('overview_page.question_count')} {$t('overview_page.question_count')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('words.play')} {$t('words.play')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('words.edit')} {$t('words.edit')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('words.delete')} {$t('words.delete')}
</th> </th>
<th <th
scope="col" scope='col'
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400" class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
> >
{$t('words.public')} {$t('words.public')}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each quizzes as quiz} {#each quizzes as quiz}
<tr <tr
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700" class='bg-white border-b dark:bg-gray-800 dark:border-gray-700'
>
<td
class='py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white'
> >
<td {quiz.title}
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white" </td>
> <td
{quiz.title} class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
</td> >
<td {formatDate(quiz.created_at)}
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" </td>
> <td
{formatDate(quiz.created_at)} class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
</td> >
<td {quiz.questions.length}
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" </td>
> <td
{quiz.questions.length} class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
</td> >
<td <button
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" on:click={() => {
>
<button
on:click={() => {
startGame(quiz.id); startGame(quiz.id);
}} }}
class="border border-green-600" class='border border-green-600'
>
{$t('words.start')}
</button>
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
<a {$t('words.start')}
href="/edit?quiz_id={quiz.id}" </button>
class="border border-yellow-600" </td>
>{$t('words.edit')}</a <td
> class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
</td> >
<td <a
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" href='/edit?quiz_id={quiz.id}'
class='border border-yellow-600'
>{$t('words.edit')}</a
> >
<button </td>
on:click={() => { <td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
<button
on:click={() => {
deleteQuiz(quiz.id); deleteQuiz(quiz.id);
}} }}
class="border border-red-600" class='border border-red-600'
>
{$t('words.delete')}
</button>
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
{#if quiz.public} {$t('words.delete')}
</button>
{:else} </td>
<td
{/if} class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
</td> >
</tr> {#if quiz.public}
{/each}
{:else}
{/if}
</td>
</tr>
{/each}
</tbody> </tbody>
</table> </table>
</div> </div>
+10 -5
View File
@@ -1,5 +1,10 @@
<script context="module" lang="ts"> <script context='module' lang='ts'>
export async function load({ url }) { import { signedIn } from '$lib/stores';
export async function load({ url, session }) {
if (session.authenticated) {
signedIn.set(true);
}
const token = url.searchParams.get('pin'); const token = url.searchParams.get('pin');
return { return {
props: { props: {
@@ -9,7 +14,7 @@
} }
</script> </script>
<script lang="ts"> <script lang='ts'>
import { socket } from '$lib/socket'; import { socket } from '$lib/socket';
import JoinGame from '$lib/play/join.svelte'; import JoinGame from '$lib/play/join.svelte';
import type { Answer, QuizData } from '../app'; import type { Answer, QuizData } from '../app';
@@ -27,7 +32,7 @@
} }
// Variables init // Variables init
let question_index = ''; let question_index: string = '';
let unique = {}; let unique = {};
navbarVisible.set(false); navbarVisible.set(false);
let game_pin_valid: boolean; let game_pin_valid: boolean;
@@ -75,7 +80,7 @@
{#if gameData !== undefined} {#if gameData !== undefined}
{#each gameData.questions as question} {#each gameData.questions as question}
{#if question.image !== undefined} {#if question.image !== undefined}
<link rel="preload" as="image" href={question.image} /> <link rel='preload' as='image' href={question.image} />
{/if} {/if}
{/each} {/each}
{/if} {/if}
+3
View File
@@ -0,0 +1,3 @@
[tool.black]
line-length = 120
target-version = ["py310"]