Merge branch 'master' into chores/add-minimal-ci
This commit is contained in:
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
[](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 +0,0 @@
|
|||||||
Generic single-database configuration.
|
|
||||||
@@ -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()
|
|
||||||
@@ -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 ###
|
|
||||||
@@ -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")
|
||||||
|
|||||||
+2
-1
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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"})}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
f"game_session:{session['game_pin']}:{data['question_index']}",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
[{"username": session["username"], "answer": data["answer"], "right": answer_right}]),
|
[
|
||||||
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Generated
+783
-1792
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,8 @@
|
|||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"image": "Bild",
|
"image": "Bild",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"repeat_password": "Passwort wiederholen"
|
"repeat_password": "Passwort wiederholen",
|
||||||
|
"overview": "Übersicht"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"time_in_seconds": "Zeit in Sekunden",
|
"time_in_seconds": "Zeit in Sekunden",
|
||||||
|
|||||||
@@ -83,7 +83,8 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"repeat_password": "Repeat password"
|
"repeat_password": "Repeat password",
|
||||||
|
"overview": "Overview"
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"time_in_seconds": "Time in seconds",
|
"time_in_seconds": "Time in seconds",
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"image": "Image",
|
"image": "Image",
|
||||||
"title": "titre",
|
"title": "titre",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"repeat_password": ""
|
"repeat_password": "",
|
||||||
|
"overview": ""
|
||||||
},
|
},
|
||||||
"index_page": {
|
"index_page": {
|
||||||
"slogan": "La plateforme de quiz open-source !",
|
"slogan": "La plateforme de quiz open-source !",
|
||||||
|
|||||||
@@ -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,12 +73,43 @@
|
|||||||
</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}
|
||||||
>
|
>
|
||||||
|
{#if $signedIn}
|
||||||
|
<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="/overview">{$t('words.overview')}</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="/api/v1/users/logout">{$t('words.logout')}</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>
|
||||||
|
{:else}
|
||||||
<li class="py-2">
|
<li class="py-2">
|
||||||
<a
|
<a
|
||||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||||
@@ -112,5 +140,6 @@
|
|||||||
href="/docs">{$t('words.docs')}</a
|
href="/docs">{$t('words.docs')}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,27 +107,30 @@
|
|||||||
/>
|
/>
|
||||||
</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'
|
||||||
|
class='m-2 text-black'
|
||||||
bind:value={changePasswordData.oldPassword}
|
bind:value={changePasswordData.oldPassword}
|
||||||
/></label
|
/></label
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
>New Password:<input
|
>New Password:<input
|
||||||
class="m-2 text-black"
|
type='password'
|
||||||
|
class='m-2 text-black'
|
||||||
bind:value={changePasswordData.newPassword}
|
bind:value={changePasswordData.newPassword}
|
||||||
/></label
|
/></label
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
>New Password again:<input
|
>New Password again:<input
|
||||||
class="m-2 text-black"
|
type='password'
|
||||||
|
class='m-2 text-black'
|
||||||
bind:value={changePasswordData.newPasswordConfirm}
|
bind:value={changePasswordData.newPasswordConfirm}
|
||||||
/></label
|
/></label
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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')} />
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,92 +74,92 @@
|
|||||||
<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>
|
||||||
@@ -153,58 +168,58 @@
|
|||||||
<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
|
<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'
|
||||||
>
|
>
|
||||||
{quiz.title}
|
{quiz.title}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
{formatDate(quiz.created_at)}
|
{formatDate(quiz.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
{quiz.questions.length}
|
{quiz.questions.length}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
startGame(quiz.id);
|
startGame(quiz.id);
|
||||||
}}
|
}}
|
||||||
class="border border-green-600"
|
class='border border-green-600'
|
||||||
>
|
>
|
||||||
{$t('words.start')}
|
{$t('words.start')}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/edit?quiz_id={quiz.id}"
|
href='/edit?quiz_id={quiz.id}'
|
||||||
class="border border-yellow-600"
|
class='border border-yellow-600'
|
||||||
>{$t('words.edit')}</a
|
>{$t('words.edit')}</a
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
deleteQuiz(quiz.id);
|
deleteQuiz(quiz.id);
|
||||||
}}
|
}}
|
||||||
class="border border-red-600"
|
class='border border-red-600'
|
||||||
>
|
>
|
||||||
{$t('words.delete')}
|
{$t('words.delete')}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||||
>
|
>
|
||||||
{#if quiz.public}
|
{#if quiz.public}
|
||||||
✅
|
✅
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
target-version = ["py310"]
|
||||||
Reference in New Issue
Block a user