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 = "*"
|
||||
pytest = "*"
|
||||
pytest-asyncio = "*"
|
||||
flake8 = "*"
|
||||
black = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
|
||||
Generated
+104
-14
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b5be758b4e3beeea1b5552ecb2fcb275e660a7ba8f0c8f7f8afb1d0a877aa73b"
|
||||
"sha256": "075e07880b28878645b61e807c6091ae0ecdef50b3486e047aa853023fc2ae33"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -299,7 +299,7 @@
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"click": {
|
||||
@@ -331,7 +331,7 @@
|
||||
"sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e",
|
||||
"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"
|
||||
},
|
||||
"ecdsa": {
|
||||
@@ -544,7 +544,7 @@
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"jinja2": {
|
||||
@@ -922,18 +922,18 @@
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:69d05fac17bf3f43937afbb775c536eb516bd21355a4f17d59a966f4a531ce71",
|
||||
"sha256:fe45513881229dbee610620b9e0817b1f48c47ba635870320fd44a712204bbdd"
|
||||
"sha256:0107dc8e98a4f1d1d4aa00100e044287f77121a1e6d2085545c4b7fa94a7a27f",
|
||||
"sha256:4e95f4ec5f49e636efcf20061a5a9110c20852f607cfca6865c07aaa8a739ee2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.1"
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"rsa": {
|
||||
"hashes": [
|
||||
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
|
||||
"sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"
|
||||
],
|
||||
"markers": "python_version >= '3.6' and python_version < '4.0'",
|
||||
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||
"version": "==4.8"
|
||||
},
|
||||
"sentry-sdk": {
|
||||
@@ -946,11 +946,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:41aface2e85b517c3a466b4689b8055c02cd2e623461f09af7d93f3da65c4709",
|
||||
"sha256:88fafba4abc2f047e08a188fd4bbc10b0e464592c37b514c19f8f8f88d94450b"
|
||||
"sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a",
|
||||
"sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==61.3.1"
|
||||
"version": "==62.0.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -965,7 +965,7 @@
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.0'",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
@@ -1031,7 +1031,7 @@
|
||||
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
|
||||
"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"
|
||||
},
|
||||
"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'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
|
||||
@@ -1331,6 +1368,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==6.3.2"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
|
||||
"sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
@@ -1338,6 +1383,20 @@
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
@@ -1346,6 +1405,21 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"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'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
||||
@@ -1392,7 +1482,7 @@
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_version < '3.11'",
|
||||
"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/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">
|
||||
[](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'>
|
||||
<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
|
||||
from fastapi import FastAPI, Request
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from socketio import ASGIApp
|
||||
|
||||
from classquiz.db import database
|
||||
from classquiz.config import settings
|
||||
from classquiz.db import database
|
||||
from classquiz.routers import users, quiz, utils, stats, storage
|
||||
from classquiz.socket_server import sio
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
|
||||
settings = settings()
|
||||
if settings.sentry_dsn:
|
||||
@@ -41,8 +41,6 @@ async def shutdown() -> None:
|
||||
await database_.disconnect()
|
||||
|
||||
|
||||
|
||||
|
||||
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(utils.router, tags=["utils"], prefix="/api/v1/utils")
|
||||
|
||||
+7
-6
@@ -15,7 +15,8 @@ from passlib.hash import argon2
|
||||
|
||||
from classquiz.cache import get_cache
|
||||
from classquiz.config import settings
|
||||
from classquiz.db.models import *
|
||||
from datetime import datetime
|
||||
from classquiz.db.models import User, TokenData
|
||||
|
||||
settings = settings()
|
||||
|
||||
@@ -27,11 +28,11 @@ ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
|
||||
|
||||
class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
scopes: Optional[Dict[str, str]] = None,
|
||||
auto_error: bool = True,
|
||||
self,
|
||||
tokenUrl: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
scopes: Optional[Dict[str, str]] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import ormar
|
||||
|
||||
from classquiz.config import redis, settings
|
||||
from classquiz.db.models import User
|
||||
|
||||
settings = settings()
|
||||
|
||||
|
||||
|
||||
+11
-4
@@ -1,6 +1,8 @@
|
||||
import redis.asyncio as redis_lib
|
||||
from functools import lru_cache
|
||||
|
||||
import redis.asyncio as redis_lib
|
||||
from pydantic import BaseSettings, RedisDsn, PostgresDsn
|
||||
|
||||
from classquiz.storage import Storage
|
||||
|
||||
|
||||
@@ -8,6 +10,7 @@ class Settings(BaseSettings):
|
||||
"""
|
||||
Settings class for the shop app.
|
||||
"""
|
||||
|
||||
root_address: str = "http://127.0.0.1:8000"
|
||||
redis: RedisDsn = "redis://localhost:6379/0?decode_responses=True"
|
||||
skip_email_verification: bool = False
|
||||
@@ -34,7 +37,7 @@ class Settings(BaseSettings):
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = 'utf-8'
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
@lru_cache()
|
||||
@@ -43,5 +46,9 @@ def settings() -> Settings:
|
||||
|
||||
|
||||
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,
|
||||
deta_id=settings().deta_project_id, storage_path=settings().storage_path)
|
||||
storage: Storage = Storage(
|
||||
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
|
||||
"""
|
||||
|
||||
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
|
||||
email: 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())
|
||||
|
||||
class Meta:
|
||||
tablename = 'users'
|
||||
tablename = "users"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
@@ -29,6 +30,7 @@ class UserSession(ormar.Model):
|
||||
"""
|
||||
The user session model for user-sessions
|
||||
"""
|
||||
|
||||
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
|
||||
user: uuid.UUID = ormar.ForeignKey(User)
|
||||
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())
|
||||
|
||||
class Meta:
|
||||
tablename = 'user_sessions'
|
||||
tablename = "user_sessions"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
@@ -73,7 +75,7 @@ class Quiz(ormar.Model):
|
||||
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
|
||||
|
||||
class Meta:
|
||||
tablename = 'quiz'
|
||||
tablename = "quiz"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
@@ -82,6 +84,7 @@ class Token(BaseModel):
|
||||
"""
|
||||
For JWT
|
||||
"""
|
||||
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
@@ -90,6 +93,7 @@ class TokenData(BaseModel):
|
||||
"""
|
||||
For JWT
|
||||
"""
|
||||
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@@ -97,6 +101,7 @@ class PlayGame(BaseModel):
|
||||
"""
|
||||
For JWT
|
||||
"""
|
||||
|
||||
quiz_id: uuid.UUID | str
|
||||
description: str
|
||||
title: str
|
||||
|
||||
@@ -12,17 +12,18 @@ from classquiz.db.models import User
|
||||
settings = settings()
|
||||
|
||||
jinja = Environment(
|
||||
loader=PackageLoader('classquiz.emails', 'templates'),
|
||||
autoescape=select_autoescape(['html', 'xml']),
|
||||
enable_async=True)
|
||||
loader=PackageLoader("classquiz.emails", "templates"),
|
||||
autoescape=select_autoescape(["html", "xml"]),
|
||||
enable_async=True,
|
||||
)
|
||||
|
||||
|
||||
def _sendMail(template: str, to: str, subject: str):
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = settings.mail_address
|
||||
msg['To'] = to
|
||||
msg.attach(MIMEText(template, 'html'))
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = settings.mail_address
|
||||
msg["To"] = to
|
||||
msg.attach(MIMEText(template, "html"))
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||
server = smtplib.SMTP(host=settings.mail_server, port=settings.mail_port)
|
||||
server.ehlo()
|
||||
@@ -35,25 +36,19 @@ def _sendMail(template: str, to: str, subject: str):
|
||||
async def send_register_email(email: str):
|
||||
user = await User.objects.get_or_none(email=email, verified=False)
|
||||
if user is None:
|
||||
raise ValueError('User not found')
|
||||
template = jinja.get_template('register.jinja2')
|
||||
template = await template.render_async(
|
||||
base_url=settings.root_address,
|
||||
token=user.verify_key
|
||||
)
|
||||
_sendMail(template=template, to=email, subject='Verify your email')
|
||||
raise ValueError("User not found")
|
||||
template = jinja.get_template("register.jinja2")
|
||||
template = await template.render_async(base_url=settings.root_address, token=user.verify_key)
|
||||
_sendMail(template=template, to=email, subject="Verify your email")
|
||||
|
||||
|
||||
async def send_forgotten_password_email(email: str):
|
||||
user = await User.objects.get_or_none(email=email)
|
||||
if user is None:
|
||||
raise ValueError('User not found')
|
||||
template = jinja.get_template('forgotten_password.jinja2')
|
||||
raise ValueError("User not found")
|
||||
template = jinja.get_template("forgotten_password.jinja2")
|
||||
token = os.urandom(32).hex()
|
||||
template = await template.render_async(
|
||||
base_url=settings.root_address,
|
||||
token=token
|
||||
)
|
||||
template = await template.render_async(base_url=settings.root_address, token=token)
|
||||
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
from typing import List, Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class _CoverMetadata(BaseModel):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from classquiz.kahoot_importer import _Card, _Kahoot
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import html
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from aiohttp import ClientSession, FormData
|
||||
import html
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from classquiz.config import settings, storage
|
||||
from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User
|
||||
from classquiz.kahoot_importer.get import get as get_quiz
|
||||
from classquiz.config import settings, storage
|
||||
|
||||
settings = settings()
|
||||
|
||||
@@ -52,6 +53,7 @@ async def _download_image(url: str) -> bytes:
|
||||
# data = await resp.json()
|
||||
# return data["data"]["id"]
|
||||
|
||||
|
||||
async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
|
||||
"""
|
||||
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_id = uuid.uuid4()
|
||||
|
||||
|
||||
for q in quiz.kahoot.questions:
|
||||
answers: list[QuizAnswer] = []
|
||||
image = None
|
||||
if q.image is not None and 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 = f"{settings.root_address}/api/v1/storage/download/{image_name}"
|
||||
# image = q.image
|
||||
for a in q.choices:
|
||||
answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer))))
|
||||
quiz_questions.append(
|
||||
QuizQuestion(question=q.question, answers=answers, 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))
|
||||
QuizQuestion(
|
||||
question=q.question,
|
||||
answers=answers,
|
||||
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()
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
from uuid import UUID
|
||||
from typing import List, Any, Optional
|
||||
from aiohttp import ClientSession
|
||||
from classquiz.kahoot_importer import _Entity
|
||||
from typing import List
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
from classquiz.kahoot_importer import _Entity
|
||||
|
||||
|
||||
# noqa : E501
|
||||
class _Response(BaseModel):
|
||||
entities: List[_Entity]
|
||||
totalHits: int
|
||||
cursor: int
|
||||
cursor: int | None
|
||||
pageTimestamp: int
|
||||
|
||||
|
||||
async def search(query: str | None, limit: int | None = 9, cursor: int | None = 1,
|
||||
search_cluster: int | None = 1, inventory_item_id: str | None = "ANY") -> _Response:
|
||||
async def search(
|
||||
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
|
||||
@@ -26,8 +32,7 @@ async def search(query: str | None, limit: int | None = 9, cursor: int | None =
|
||||
"""
|
||||
async with ClientSession() as session:
|
||||
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:
|
||||
# print(
|
||||
# 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 re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
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.responses import JSONResponse
|
||||
import re
|
||||
from pydantic import ValidationError
|
||||
|
||||
from classquiz.auth import get_current_user, get_current_user_optional
|
||||
from classquiz.config import redis, settings, storage
|
||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame
|
||||
from classquiz.kahoot_importer.import_quiz import import_quiz
|
||||
|
||||
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"})
|
||||
else:
|
||||
game_pin = randint(10000000, 99999999)
|
||||
game = PlayGame(quiz_id=quiz_id, game_pin=str(game_pin), questions=quiz.questions, game_id=uuid.uuid4(),
|
||||
title=quiz.title, description=quiz.description)
|
||||
game = PlayGame(
|
||||
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)
|
||||
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.db.models import *
|
||||
from pydantic import BaseModel
|
||||
from classquiz.db.models import Quiz, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from classquiz.config import settings, storage
|
||||
import re
|
||||
|
||||
settings = settings()
|
||||
|
||||
@@ -10,7 +12,7 @@ router = APIRouter()
|
||||
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):
|
||||
if not re.match(file_regex, 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():
|
||||
yield from download
|
||||
|
||||
return StreamingResponse(iter_file(), media_type='image/*',
|
||||
headers={"Cache-Control": "public, immutable, max-age=31536000"})
|
||||
return StreamingResponse(
|
||||
iter_file(),
|
||||
media_type="image/*",
|
||||
headers={"Cache-Control": "public, immutable, max-age=31536000"},
|
||||
)
|
||||
|
||||
+63
-24
@@ -1,21 +1,38 @@
|
||||
import os
|
||||
|
||||
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.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from classquiz.auth import *
|
||||
from classquiz.config import redis
|
||||
from classquiz.auth import (
|
||||
get_password_hash,
|
||||
verify_password,
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
get_current_user,
|
||||
)
|
||||
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
|
||||
|
||||
settings = settings()
|
||||
router = APIRouter()
|
||||
|
||||
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:
|
||||
@@ -23,8 +40,11 @@ async def _sign_out_everywhere(user: User) -> None:
|
||||
await clear_cache_for_account(user)
|
||||
|
||||
|
||||
@router.post("/create", response_model=User,
|
||||
response_model_include={"id": ..., "verified": ..., "email": ...})
|
||||
@router.post(
|
||||
"/create",
|
||||
response_model=User,
|
||||
response_model_include={"id": ..., "verified": ..., "email": ...},
|
||||
)
|
||||
async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse:
|
||||
user = User(**user.dict(), id=uuid.uuid4())
|
||||
try:
|
||||
@@ -47,8 +67,11 @@ async def create_user(user: route_user, background_task: BackgroundTasks) -> Use
|
||||
|
||||
|
||||
@router.post("/token/cookie", response_model=Token)
|
||||
async def login_for_cookie_access_token(request: Request, response: Response,
|
||||
form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
async def login_for_cookie_access_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
):
|
||||
user = await authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
@@ -56,21 +79,29 @@ async def login_for_cookie_access_token(request: Request, response: Response,
|
||||
detail="Incorrect username or password",
|
||||
)
|
||||
session_key = os.urandom(32).hex()
|
||||
user_session = UserSession(user=user, session_key=session_key, ip_address=request.client.host,
|
||||
user_agent=request.headers.get("User-Agent"), id=uuid.uuid4())
|
||||
user_session = UserSession(
|
||||
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()
|
||||
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires)
|
||||
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}",
|
||||
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="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"}
|
||||
|
||||
|
||||
@@ -79,16 +110,20 @@ async def rememberme_token(request: Request, response: Response):
|
||||
rememberme_cookie = request.cookies.get("rememberme_token")
|
||||
if rememberme_cookie is None:
|
||||
raise HTTPException(status_code=400, detail="No rememberme cookie")
|
||||
user_session: UserSession | None = await UserSession.objects.filter(session_key=rememberme_cookie).select_related(
|
||||
UserSession.user).get_or_none()
|
||||
user_session: UserSession | 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):
|
||||
raise HTTPException(status_code=401, detail="No user session")
|
||||
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user_session.user.email}, expires_delta=access_token_expires
|
||||
access_token = create_access_token(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)
|
||||
await user_session.update(last_seen=datetime.now())
|
||||
|
||||
@@ -124,7 +159,11 @@ async def verify_user(verify_key: str):
|
||||
|
||||
|
||||
@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):
|
||||
raise HTTPException(status_code=400, detail="Incorrect password")
|
||||
user.password = get_password_hash(password_data.new_password)
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter
|
||||
from fastapi.responses import Response
|
||||
|
||||
from classquiz.config import settings
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
@@ -16,9 +17,7 @@ async def get_qr(quiz_pin: str):
|
||||
"""
|
||||
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}")
|
||||
buf = io.BytesIO()
|
||||
qr.make(fit=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ import aiohttp
|
||||
import socketio
|
||||
|
||||
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=[])
|
||||
settings = settings()
|
||||
@@ -14,35 +14,54 @@ settings = settings()
|
||||
async def join_game(sid, data):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
async with session.post("https://hcaptcha.com/siteverify",
|
||||
data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp:
|
||||
async with session.post(
|
||||
"https://hcaptcha.com/siteverify",
|
||||
data={"response": data["captcha"], "secret": settings.hcaptcha_key},
|
||||
) as resp:
|
||||
resp_data = await resp.json()
|
||||
if not resp_data["success"]:
|
||||
print("CAPTCHA FAILED")
|
||||
return
|
||||
except KeyError as e:
|
||||
except KeyError:
|
||||
print("CAPTCHA FAILED")
|
||||
return
|
||||
redis_res = await redis.get(f"game:{data['game_pin']}")
|
||||
if redis_res is None:
|
||||
await sio.emit("game_not_found", room=sid)
|
||||
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.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)
|
||||
# print(redis_res)
|
||||
redis_res["players"].append({"username": data["username"], "sid": sid})
|
||||
await redis.set(f"game_session:{data['game_pin']}",
|
||||
json.dumps({"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"])
|
||||
await redis.set(
|
||||
f"game_session:{data['game_pin']}",
|
||||
json.dumps(
|
||||
{
|
||||
"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.event
|
||||
async def start_game(sid, data):
|
||||
async def start_game(sid, _data):
|
||||
# print(sid, data, "START_GAME")
|
||||
session = await sio.get_session(sid)
|
||||
# print(session)
|
||||
@@ -55,14 +74,19 @@ async def register_as_admin(sid, data):
|
||||
game_pin = data["game_pin"]
|
||||
game_id = data["game_id"]
|
||||
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": []}),
|
||||
ex=18000)
|
||||
await redis.set(
|
||||
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(
|
||||
f"game:{data['game_pin']}")},
|
||||
room=sid)
|
||||
await sio.emit(
|
||||
"registered_as_admin",
|
||||
{"game_id": game_id, "game": await redis.get(f"game:{data['game_pin']}")},
|
||||
room=sid,
|
||||
)
|
||||
async with sio.session(sid) as session:
|
||||
session['game_pin'] = data["game_pin"]
|
||||
session["game_pin"] = data["game_pin"]
|
||||
session["admin"] = True
|
||||
sio.enter_room(sid, data["game_pin"])
|
||||
else:
|
||||
@@ -74,7 +98,7 @@ async def get_question_results(sid, data):
|
||||
session = await sio.get_session(sid)
|
||||
if session["admin"]:
|
||||
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)
|
||||
|
||||
|
||||
@@ -82,15 +106,15 @@ async def get_question_results(sid, data):
|
||||
async def set_question_number(sid, data):
|
||||
session = await sio.get_session(sid)
|
||||
if session["admin"]:
|
||||
game_pin = session['game_pin']
|
||||
game_pin = session["game_pin"]
|
||||
await sio.emit("set_question_number", data, room=game_pin)
|
||||
|
||||
|
||||
@sio.event
|
||||
async def submit_answer(sid, data):
|
||||
session = await sio.get_session(sid)
|
||||
redis_res = await redis.get(f"game_session:{session['game_pin']}")
|
||||
game_session = GameSession(**json.loads(redis_res))
|
||||
# redis_res = await redis.get(f"game_session:{session['game_pin']}")
|
||||
# game_session = GameSession(**json.loads(redis_res))
|
||||
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
|
||||
# print(game_session)
|
||||
answer_right = False
|
||||
@@ -100,21 +124,40 @@ async def submit_answer(sid, data):
|
||||
break
|
||||
answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}")
|
||||
if answers is None:
|
||||
await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}",
|
||||
json.dumps(
|
||||
[{"username": session["username"], "answer": data["answer"], "right": answer_right}]),
|
||||
ex=18000)
|
||||
await redis.set(
|
||||
f"game_session:{session['game_pin']}:{data['question_index']}",
|
||||
json.dumps(
|
||||
[
|
||||
{
|
||||
"username": session["username"],
|
||||
"answer": data["answer"],
|
||||
"right": answer_right,
|
||||
}
|
||||
]
|
||||
),
|
||||
ex=18000,
|
||||
)
|
||||
else:
|
||||
answers = json.loads(answers)
|
||||
answers.append({"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)
|
||||
answers.append(
|
||||
{
|
||||
"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))
|
||||
|
||||
|
||||
@sio.event
|
||||
async def get_game_data(sid, data):
|
||||
game_pin = (await sio.get_session(sid))['game_pin']
|
||||
async def get_game_data(sid, _data):
|
||||
game_pin = (await sio.get_session(sid))["game_pin"]
|
||||
game_data = await redis.get(f"game:{game_pin}")
|
||||
if game_data is not None:
|
||||
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 typing import Optional
|
||||
|
||||
from .deta_storage import DetaStorage
|
||||
from .local_storage import LocalStorage
|
||||
|
||||
|
||||
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.deta_key: str | None = deta_key
|
||||
self.deta_id: str | None = deta_id
|
||||
@@ -14,8 +21,11 @@ class Storage:
|
||||
if deta_key is None or deta_id is None:
|
||||
raise ValueError("deta_key and deta_id must be provided")
|
||||
else:
|
||||
self.deta_instance = DetaStorage(deta_base_url=self.deta_base_url, deta_key=self.deta_key,
|
||||
deta_id=self.deta_id)
|
||||
self.deta_instance = DetaStorage(
|
||||
deta_base_url=self.deta_base_url,
|
||||
deta_key=self.deta_key,
|
||||
deta_id=self.deta_id,
|
||||
)
|
||||
|
||||
elif backend == "local":
|
||||
if storage_path is None:
|
||||
@@ -28,8 +38,7 @@ class Storage:
|
||||
|
||||
async def download(self, file_name: str) -> BytesIO | None:
|
||||
if self.backend == "deta":
|
||||
return await self.deta_instance.download(
|
||||
file_name)
|
||||
return await self.deta_instance.download(file_name)
|
||||
elif self.backend == "local":
|
||||
return await self.local_instance.get_file(file_name)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from aiohttp import ClientSession
|
||||
from io import BytesIO
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
|
||||
class DetaStorage:
|
||||
def __init__(self, deta_base_url: str, deta_id: str, deta_key: str):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import io
|
||||
import os
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
|
||||
@@ -10,13 +11,13 @@ class LocalStorage:
|
||||
|
||||
async def get_file(self, file_name: str) -> io.BytesIO | None:
|
||||
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())
|
||||
except FileNotFoundError:
|
||||
return 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)
|
||||
|
||||
async def delete_file(self, file_names: [str]) -> None:
|
||||
|
||||
@@ -1,63 +1,116 @@
|
||||
from classquiz.kahoot_importer.get import get
|
||||
import pytest
|
||||
|
||||
from classquiz.kahoot_importer.get import get
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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("c5d31de6-4598-4574-8e67-ca9a2aba49f8")
|
||||
await get("3d545fc3-b830-44b6-94ac-c8e4a3c9fb88")
|
||||
quiz_list = ['c63d4d39-767e-4754-8c16-66c3c2d9633c', 'a73689d2-1108-4524-88e1-10c54282346c',
|
||||
'e8e2d869-7c75-4ad6-8872-031bc8e37fa9', '387b4139-9e77-4b47-93b7-dbb6d75c54d1',
|
||||
'a1fff657-b99d-4547-93cf-4e9629994844', '0880d792-a77e-4cba-8a8b-359de40ffe76',
|
||||
'e2d12f01-a3dd-4d0e-a6c4-29f38046117f', 'e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7',
|
||||
'e655c086-558d-4602-99fd-5fc429ffe0ce', '1642f29d-b5cb-4213-82e6-7c1fae401aca',
|
||||
'fabdd572-da3e-4621-b912-41b9bc5a18fb', 'ded3cbbf-5af3-4761-a6da-b3b6ca040e1a',
|
||||
'370b3e33-ed97-4d36-9362-7f76aa9d12b9', 'cfa32a28-1a01-47f1-9bca-c85640bceeb8',
|
||||
'993f615d-6890-4dd3-ac08-f462de9ac5a3', '7f08a377-e371-425e-95f9-7196401ae7ff',
|
||||
'19656fed-5891-48e7-ae7f-1358f623bfd0', '5e766d32-930a-4640-82a1-87c58612f579',
|
||||
'ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5', 'dd3c9175-addd-48df-8297-7598d4f12006',
|
||||
'b8de862c-fc4b-4df6-9fa5-81b91868fc3b', '3b082f4e-9c50-45ea-98c4-3845b14e400c',
|
||||
'10179f67-a344-4fd0-80a1-d1e1b6ca7953', '8288c1dd-9675-411c-9175-4180004da3c3',
|
||||
'af838c22-dcbb-497d-8c7b-f6499835b83d', '152c5fdb-3b0a-4cdf-a0d9-1984de0643ad',
|
||||
'846fd2bb-6b95-4333-b5d4-ef0a363e73a9', 'dd6b446f-817b-469b-9e6b-e1d34c466a7f',
|
||||
'2720d636-8918-4c6a-86a9-928c489382f4', 'a6985641-df97-4e56-97fc-e0901e32b464',
|
||||
'1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9', '677fa485-4231-4479-beb2-fe757053bf41',
|
||||
'2b6379f8-450e-46c0-a854-5e368a03d5a6', 'f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1',
|
||||
'51d2e839-a9d7-4f26-b1fd-fb0743c3a236', 'f2212243-4553-456d-87e7-7ddfd6ee7563',
|
||||
'2d6982d3-7b46-4022-bbb6-cb5451c581ba', '1cccae22-5140-46b5-ba6a-aa17cec14dc3',
|
||||
'b01dd2c2-ffcf-4c02-a274-e12296dbe4e1', '2669d193-a81f-4752-a003-dd2433beef92',
|
||||
'06203ee6-3615-4e26-bb08-4fab249698a4', '933c1426-c759-43a3-9770-c8e3948db998',
|
||||
'02046384-fb05-4f1d-855a-f0e52a0761e3', 'b5266e9b-7372-4df0-94a9-ec8a6c0aabc6',
|
||||
'fb8e3a3d-505a-4157-bd9a-b73b00591f10', '42092675-9171-400f-ab4d-c898ec9f70bd',
|
||||
'1d6307e2-98c2-42f2-b765-d02a68e0da80', '8f431ed0-31df-4435-bcba-aff46929fe82',
|
||||
'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']
|
||||
quiz_list = [
|
||||
"c63d4d39-767e-4754-8c16-66c3c2d9633c",
|
||||
"a73689d2-1108-4524-88e1-10c54282346c",
|
||||
"e8e2d869-7c75-4ad6-8872-031bc8e37fa9",
|
||||
"387b4139-9e77-4b47-93b7-dbb6d75c54d1",
|
||||
"a1fff657-b99d-4547-93cf-4e9629994844",
|
||||
"0880d792-a77e-4cba-8a8b-359de40ffe76",
|
||||
"e2d12f01-a3dd-4d0e-a6c4-29f38046117f",
|
||||
"e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7",
|
||||
"e655c086-558d-4602-99fd-5fc429ffe0ce",
|
||||
"1642f29d-b5cb-4213-82e6-7c1fae401aca",
|
||||
"fabdd572-da3e-4621-b912-41b9bc5a18fb",
|
||||
"ded3cbbf-5af3-4761-a6da-b3b6ca040e1a",
|
||||
"370b3e33-ed97-4d36-9362-7f76aa9d12b9",
|
||||
"cfa32a28-1a01-47f1-9bca-c85640bceeb8",
|
||||
"993f615d-6890-4dd3-ac08-f462de9ac5a3",
|
||||
"7f08a377-e371-425e-95f9-7196401ae7ff",
|
||||
"19656fed-5891-48e7-ae7f-1358f623bfd0",
|
||||
"5e766d32-930a-4640-82a1-87c58612f579",
|
||||
"ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5",
|
||||
"dd3c9175-addd-48df-8297-7598d4f12006",
|
||||
"b8de862c-fc4b-4df6-9fa5-81b91868fc3b",
|
||||
"3b082f4e-9c50-45ea-98c4-3845b14e400c",
|
||||
"10179f67-a344-4fd0-80a1-d1e1b6ca7953",
|
||||
"8288c1dd-9675-411c-9175-4180004da3c3",
|
||||
"af838c22-dcbb-497d-8c7b-f6499835b83d",
|
||||
"152c5fdb-3b0a-4cdf-a0d9-1984de0643ad",
|
||||
"846fd2bb-6b95-4333-b5d4-ef0a363e73a9",
|
||||
"dd6b446f-817b-469b-9e6b-e1d34c466a7f",
|
||||
"2720d636-8918-4c6a-86a9-928c489382f4",
|
||||
"a6985641-df97-4e56-97fc-e0901e32b464",
|
||||
"1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9",
|
||||
"677fa485-4231-4479-beb2-fe757053bf41",
|
||||
"2b6379f8-450e-46c0-a854-5e368a03d5a6",
|
||||
"f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1",
|
||||
"51d2e839-a9d7-4f26-b1fd-fb0743c3a236",
|
||||
"f2212243-4553-456d-87e7-7ddfd6ee7563",
|
||||
"2d6982d3-7b46-4022-bbb6-cb5451c581ba",
|
||||
"1cccae22-5140-46b5-ba6a-aa17cec14dc3",
|
||||
"b01dd2c2-ffcf-4c02-a274-e12296dbe4e1",
|
||||
"2669d193-a81f-4752-a003-dd2433beef92",
|
||||
"06203ee6-3615-4e26-bb08-4fab249698a4",
|
||||
"933c1426-c759-43a3-9770-c8e3948db998",
|
||||
"02046384-fb05-4f1d-855a-f0e52a0761e3",
|
||||
"b5266e9b-7372-4df0-94a9-ec8a6c0aabc6",
|
||||
"fb8e3a3d-505a-4157-bd9a-b73b00591f10",
|
||||
"42092675-9171-400f-ab4d-c898ec9f70bd",
|
||||
"1d6307e2-98c2-42f2-b765-d02a68e0da80",
|
||||
"8f431ed0-31df-4435-bcba-aff46929fe82",
|
||||
"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
|
||||
for i in quiz_list:
|
||||
rounds = rounds + 1
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from classquiz.kahoot_importer.search import search
|
||||
import pytest
|
||||
|
||||
from classquiz.kahoot_importer.search import search
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search():
|
||||
res = await search(query="Python", limit=100)
|
||||
print(res)
|
||||
assert len(res.entities) == 100
|
||||
await search(query="Test Quiz", limit=100)
|
||||
await search(query="Biologie", limit=100)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from classquiz.config import settings
|
||||
from classquiz.storage import Storage
|
||||
|
||||
@@ -9,11 +10,14 @@ file_contents = b"hello world"
|
||||
|
||||
def test_storage_init():
|
||||
with pytest.raises(NotImplementedError):
|
||||
Storage(backend="asdsad", deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id, storage_path=settings.storage_path)
|
||||
Storage(
|
||||
backend="asdsad",
|
||||
deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id,
|
||||
storage_path=settings.storage_path,
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
Storage(backend="deta", deta_key=None,
|
||||
deta_id=None, storage_path=None)
|
||||
Storage(backend="deta", deta_key=None, deta_id=None, storage_path=None)
|
||||
with pytest.raises(ValueError):
|
||||
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
|
||||
async def test_deta():
|
||||
storage: Storage = Storage(backend="deta", deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id, storage_path=settings.storage_path)
|
||||
storage: Storage = Storage(
|
||||
backend="deta",
|
||||
deta_key=settings.deta_project_key,
|
||||
deta_id=settings.deta_project_id,
|
||||
storage_path=settings.storage_path,
|
||||
)
|
||||
await storage_tester(storage)
|
||||
|
||||
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
### Build Step
|
||||
# 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=https://mawoka.eu
|
||||
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 package*.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# 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 . .
|
||||
@@ -23,14 +24,15 @@ RUN pnpm run build
|
||||
|
||||
### Serve Step
|
||||
# pull the Node.js Docker image
|
||||
FROM node:16
|
||||
FROM node:17.8-bullseye-slim
|
||||
|
||||
# change working directory
|
||||
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 --from=builder /usr/src/app/build .
|
||||
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
|
||||
|
||||
# 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/kit": "next",
|
||||
"@tailwindcss/typography": "^0.5.0-alpha.3",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/luxon": "^2.0.9",
|
||||
"@types/yup": "^0.29.13",
|
||||
|
||||
Generated
+3153
-4162
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,12 @@ export async function getSession(event) {
|
||||
if (redis_res === null) {
|
||||
const res = await fetch(`${process.env.API_URL}/api/v1/users/check`, {
|
||||
headers: {
|
||||
Cookie: `access_token=Bearer ${event.locals.token}`
|
||||
Cookie: `access_token=Bearer ${event.locals.token}` // skipcq: JS-0378
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
//@ts-ignore
|
||||
//@ts-ignore // skipcq: JS-0295
|
||||
user_email = json.email;
|
||||
} else {
|
||||
user_email = null;
|
||||
|
||||
@@ -24,7 +24,7 @@ export class I18nService {
|
||||
}
|
||||
|
||||
// Initializing i18n
|
||||
initialize() {
|
||||
initialize(): void {
|
||||
this.i18n.use(LanguageDetector).init({
|
||||
// lng: INITIAL_LANGUAGE,
|
||||
fallbackLng: 'en',
|
||||
@@ -50,7 +50,7 @@ export class I18nService {
|
||||
this.i18n.addResourceBundle('fr', 'translation', fr);
|
||||
}
|
||||
|
||||
changeLanguage(language: string) {
|
||||
changeLanguage(language: string): void {
|
||||
this.i18n.changeLanguage(language);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +1,117 @@
|
||||
{
|
||||
"index_page": {
|
||||
"meta": {
|
||||
"title": "Home",
|
||||
"description": "ClassQuiz ist eine Quiz-App wie KAHOOT! für Schüler*innen, welche open-source und kostenlos ist"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz ist eine Quiz-Platform, welche das erstellen und managen von Quizzes erlaubt.",
|
||||
"2": "Das Hauptfeature ist die KAHOOT!-Importfunktion, mit der man Quizzes von KAHOOT! importieren kann."
|
||||
},
|
||||
"slogan": "Die open-source Quiz-Platform!"
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Erstellt am",
|
||||
"question_count": "Fragenanzahl"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Quiz erfolgreich aktualisiert!",
|
||||
"success_update_body": "Das Quiz wurde erfolgreich aktualisiert!"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"title": "Quiz erfolgreich erstellt!",
|
||||
"body": "Das Quiz wurde erfolgreich erstellt!"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Schön, dich zu sehen!",
|
||||
"create_account": "Account erstellen",
|
||||
"forgot_password?": "Passwort vergessen?",
|
||||
"already_have_account?": "Hast du schon einen Account?"
|
||||
},
|
||||
"login_page": {
|
||||
"welcome_back": "Willkommen zurück!",
|
||||
"login_or_create_account": "Einloggen oder Account erstellen",
|
||||
"already_have_account": "Noch kein Account?",
|
||||
"modal": {
|
||||
"success": {
|
||||
"success_check_mail": "Login erfolgreich! Guck in deine Mailbox!",
|
||||
"success": "Login erfolgreich!",
|
||||
"description": {
|
||||
"success_check_mail": "Bitte überprüfe deine Mailbox, weil du eine Email mit einem Link, um dich einzuloggen, empfangen haben solltest.",
|
||||
"success": "Du hast dich erfolgreich angemeldet!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Falsche Email oder falsches Passwort!",
|
||||
"unexpected": "Unerwarteter Fehler!",
|
||||
"description": {
|
||||
"wrong_creds": "Bitte stelle sicher, dass dein Passwort und deine Email richtig sind!",
|
||||
"unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"words": {
|
||||
"question": "Frage",
|
||||
"answer": "Antwort",
|
||||
"stats": "Statistiken",
|
||||
"features": "Features",
|
||||
"import": "Importieren",
|
||||
"url": "URL",
|
||||
"submit": "Abschicken",
|
||||
"create": "Erstellen",
|
||||
"logout": "Abmelden",
|
||||
"title": "Titel",
|
||||
"play": "Spielen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"public": "Öffentlich",
|
||||
"start": "Start",
|
||||
"connect": "Verbinden",
|
||||
"pin": "PIN",
|
||||
"kick": "Kicken",
|
||||
"register": "Registrieren",
|
||||
"login": "Anmelden",
|
||||
"docs": "Dokumentation",
|
||||
"close": "Schließen",
|
||||
"save": "Speichern",
|
||||
"username": "Nutzername",
|
||||
"password": "Passwort",
|
||||
"email": "Email",
|
||||
"description": "Beschreibung",
|
||||
"image": "Bild",
|
||||
"settings": "Einstellungen",
|
||||
"repeat_password": "Passwort wiederholen"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Zeit in Sekunden",
|
||||
"right_or_true?": "Richtig?",
|
||||
"add_new_answer": "Neue Antwort hinzufügen",
|
||||
"add_new_question": "Neue Frage hinzufügen",
|
||||
"delete_question": "Frage löschen",
|
||||
"delete_answer": "Antwort löschen"
|
||||
},
|
||||
"import": {
|
||||
"need_help": "",
|
||||
"visit_docs": ""
|
||||
},
|
||||
"admin": {
|
||||
"already_registered_as_admin": ""
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "Ein Admin wurde schon für dieses Spiel registriert.",
|
||||
"start_game": "Spiel starten",
|
||||
"time_left": "Zeit übrig",
|
||||
"get_results": "Ergebnisse abfragen"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"visit_docs": "Besuche die Dokumentation"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": "Passwort zurücksetzen"
|
||||
}
|
||||
"index_page": {
|
||||
"meta": {
|
||||
"title": "Home",
|
||||
"description": "ClassQuiz ist eine Quiz-App wie KAHOOT! für Schüler*innen, welche open-source und kostenlos ist"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz ist eine Quiz-Platform, welche das erstellen und managen von Quizzes erlaubt.",
|
||||
"2": "Das Hauptfeature ist die KAHOOT!-Importfunktion, mit der man Quizzes von KAHOOT! importieren kann."
|
||||
},
|
||||
"slogan": "Die open-source Quiz-Platform!"
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Erstellt am",
|
||||
"question_count": "Fragenanzahl"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Quiz erfolgreich aktualisiert!",
|
||||
"success_update_body": "Das Quiz wurde erfolgreich aktualisiert!"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"title": "Quiz erfolgreich erstellt!",
|
||||
"body": "Das Quiz wurde erfolgreich erstellt!"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Schön, dich zu sehen!",
|
||||
"create_account": "Account erstellen",
|
||||
"forgot_password?": "Passwort vergessen?",
|
||||
"already_have_account?": "Hast du schon einen Account?"
|
||||
},
|
||||
"login_page": {
|
||||
"welcome_back": "Willkommen zurück!",
|
||||
"login_or_create_account": "Einloggen oder Account erstellen",
|
||||
"already_have_account": "Noch kein Account?",
|
||||
"modal": {
|
||||
"success": {
|
||||
"success_check_mail": "Login erfolgreich! Guck in deine Mailbox!",
|
||||
"success": "Login erfolgreich!",
|
||||
"description": {
|
||||
"success_check_mail": "Bitte überprüfe deine Mailbox, weil du eine Email mit einem Link, um dich einzuloggen, empfangen haben solltest.",
|
||||
"success": "Du hast dich erfolgreich angemeldet!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Falsche Email oder falsches Passwort!",
|
||||
"unexpected": "Unerwarteter Fehler!",
|
||||
"description": {
|
||||
"wrong_creds": "Bitte stelle sicher, dass dein Passwort und deine Email richtig sind!",
|
||||
"unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"words": {
|
||||
"question": "Frage",
|
||||
"answer": "Antwort",
|
||||
"stats": "Statistiken",
|
||||
"features": "Features",
|
||||
"import": "Importieren",
|
||||
"url": "URL",
|
||||
"submit": "Abschicken",
|
||||
"create": "Erstellen",
|
||||
"logout": "Abmelden",
|
||||
"title": "Titel",
|
||||
"play": "Spielen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"public": "Öffentlich",
|
||||
"start": "Start",
|
||||
"connect": "Verbinden",
|
||||
"pin": "PIN",
|
||||
"kick": "Kicken",
|
||||
"register": "Registrieren",
|
||||
"login": "Anmelden",
|
||||
"docs": "Dokumentation",
|
||||
"close": "Schließen",
|
||||
"save": "Speichern",
|
||||
"username": "Nutzername",
|
||||
"password": "Passwort",
|
||||
"email": "Email",
|
||||
"description": "Beschreibung",
|
||||
"image": "Bild",
|
||||
"settings": "Einstellungen",
|
||||
"repeat_password": "Passwort wiederholen",
|
||||
"overview": "Übersicht"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Zeit in Sekunden",
|
||||
"right_or_true?": "Richtig?",
|
||||
"add_new_answer": "Neue Antwort hinzufügen",
|
||||
"add_new_question": "Neue Frage hinzufügen",
|
||||
"delete_question": "Frage löschen",
|
||||
"delete_answer": "Antwort löschen"
|
||||
},
|
||||
"import": {
|
||||
"need_help": "",
|
||||
"visit_docs": ""
|
||||
},
|
||||
"admin": {
|
||||
"already_registered_as_admin": ""
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "Ein Admin wurde schon für dieses Spiel registriert.",
|
||||
"start_game": "Spiel starten",
|
||||
"time_left": "Zeit übrig",
|
||||
"get_results": "Ergebnisse abfragen"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"visit_docs": "Besuche die Dokumentation"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": "Passwort zurücksetzen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,110 @@
|
||||
{
|
||||
"index_page": {
|
||||
"slogan": "The open-source quiz-platform!",
|
||||
"meta": {
|
||||
"description": "ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use",
|
||||
"title": "Home"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.",
|
||||
"2": "The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes."
|
||||
}
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Created at",
|
||||
"question_count": "Question count"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Successfully updated quiz!",
|
||||
"success_update_body": "Updated the quiz successfully!"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"title": "Successfully created quiz!",
|
||||
"body": "Created the quiz successfully!"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Nice to meet you!",
|
||||
"create_account": "Create account",
|
||||
"forgot_password?": "Forgot password?",
|
||||
"already_have_account?": "Already have an account?"
|
||||
},
|
||||
"login_page": {
|
||||
"welcome_back": "Welcome back!",
|
||||
"login_or_create_account": "Login or create an account",
|
||||
"already_have_account": "Don't have an account?",
|
||||
"modal": {
|
||||
"success": {
|
||||
"success_check_mail": "Login Succeded! Check your mailbox!",
|
||||
"success": "Login Succeded!",
|
||||
"description": {
|
||||
"success_check_mail": "Please check your mailbox, since you should have received a mail, with a link you can click to login.",
|
||||
"success": "You've successfully logged in!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Wrong email or password!",
|
||||
"unexpected": "Unexpected error!",
|
||||
"description": {
|
||||
"wrong_creds": "Please make sure that your password and your email are correct!",
|
||||
"unexpected": "There was the good old unexpected error!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"words": {
|
||||
"question": "Question",
|
||||
"answer": "Answer",
|
||||
"stats": "Stats",
|
||||
"features": "Features",
|
||||
"login": "Login",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"play": "Play",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"public": "Public",
|
||||
"start": "Start",
|
||||
"create": "Create",
|
||||
"import": "Import",
|
||||
"logout": "Logout",
|
||||
"title": "Title",
|
||||
"url": "URL",
|
||||
"submit": "Submit",
|
||||
"connect": "Connect",
|
||||
"pin": "PIN",
|
||||
"kick": "Kick",
|
||||
"register": "Register",
|
||||
"docs": "Docs",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"description": "Description",
|
||||
"image": "Image",
|
||||
"settings": "Settings",
|
||||
"repeat_password": "Repeat password"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Time in seconds",
|
||||
"right_or_true?": "Right?",
|
||||
"add_new_answer": "Add new answer",
|
||||
"add_new_question": "Add new question",
|
||||
"delete_question": "Delete question",
|
||||
"delete_answer": "Delete answer"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Need help?",
|
||||
"visit_docs": "Visit the documentation"
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "There is already an admin registered for this game.",
|
||||
"start_game": "Start game",
|
||||
"time_left": "Time left",
|
||||
"get_results": "Get results"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": "Reset password"
|
||||
}
|
||||
"index_page": {
|
||||
"slogan": "The open-source quiz-platform!",
|
||||
"meta": {
|
||||
"description": "ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use",
|
||||
"title": "Home"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.",
|
||||
"2": "The main feature is a KAHOOT!-import function that allows you to import quizzes from KAHOOT!-quizzes."
|
||||
}
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Created at",
|
||||
"question_count": "Question count"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Successfully updated quiz!",
|
||||
"success_update_body": "Updated the quiz successfully!"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"title": "Successfully created quiz!",
|
||||
"body": "Created the quiz successfully!"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Nice to meet you!",
|
||||
"create_account": "Create account",
|
||||
"forgot_password?": "Forgot password?",
|
||||
"already_have_account?": "Already have an account?"
|
||||
},
|
||||
"login_page": {
|
||||
"welcome_back": "Welcome back!",
|
||||
"login_or_create_account": "Login or create an account",
|
||||
"already_have_account": "Don't have an account?",
|
||||
"modal": {
|
||||
"success": {
|
||||
"success_check_mail": "Login Succeded! Check your mailbox!",
|
||||
"success": "Login Succeded!",
|
||||
"description": {
|
||||
"success_check_mail": "Please check your mailbox, since you should have received a mail, with a link you can click to login.",
|
||||
"success": "You've successfully logged in!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Wrong email or password!",
|
||||
"unexpected": "Unexpected error!",
|
||||
"description": {
|
||||
"wrong_creds": "Please make sure that your password and your email are correct!",
|
||||
"unexpected": "There was the good old unexpected error!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"words": {
|
||||
"question": "Question",
|
||||
"answer": "Answer",
|
||||
"stats": "Stats",
|
||||
"features": "Features",
|
||||
"login": "Login",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"play": "Play",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"public": "Public",
|
||||
"start": "Start",
|
||||
"create": "Create",
|
||||
"import": "Import",
|
||||
"logout": "Logout",
|
||||
"title": "Title",
|
||||
"url": "URL",
|
||||
"submit": "Submit",
|
||||
"connect": "Connect",
|
||||
"pin": "PIN",
|
||||
"kick": "Kick",
|
||||
"register": "Register",
|
||||
"docs": "Docs",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"description": "Description",
|
||||
"image": "Image",
|
||||
"settings": "Settings",
|
||||
"repeat_password": "Repeat password",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Time in seconds",
|
||||
"right_or_true?": "Right?",
|
||||
"add_new_answer": "Add new answer",
|
||||
"add_new_question": "Add new question",
|
||||
"delete_question": "Delete question",
|
||||
"delete_answer": "Delete answer"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Need help?",
|
||||
"visit_docs": "Visit the documentation"
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "There is already an admin registered for this game.",
|
||||
"start_game": "Start game",
|
||||
"time_left": "Time left",
|
||||
"get_results": "Get results"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": "Reset password"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,110 @@
|
||||
{
|
||||
"login_page": {
|
||||
"modal": {
|
||||
"success": {
|
||||
"description": {
|
||||
"success": "Vous vous êtes connecté avec succès !",
|
||||
"success_check_mail": "Veuillez vérifier votre boîte mail, vous avez normalement reçu un mail avec un lien pour vous connecter."
|
||||
},
|
||||
"success": "Connexion réussie !",
|
||||
"success_check_mail": "Connexion réussie ! Vérifiez votre boîte mail !"
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Mot de passe ou email non-correct !",
|
||||
"description": {
|
||||
"wrong_creds": "Assurez-vous que votre mot de passe et votre email sont corrects !",
|
||||
"unexpected": "Une bonne vieille erreur inattendue s'est produite !"
|
||||
},
|
||||
"unexpected": "Erreur inattendue !"
|
||||
}
|
||||
},
|
||||
"welcome_back": "Bon retour parmi nous !",
|
||||
"login_or_create_account": "Se connecter ou créer un compte",
|
||||
"already_have_account": "Pas encore de compte ?"
|
||||
},
|
||||
"words": {
|
||||
"question": "Question",
|
||||
"answer": "Réponse",
|
||||
"stats": "Statistiques",
|
||||
"features": "Fonctionnalités",
|
||||
"login": "Connexion",
|
||||
"email": "Email",
|
||||
"username": "Pseudo",
|
||||
"password": "Mot de passe",
|
||||
"play": "Jouer",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"public": "Public",
|
||||
"start": "Commencer",
|
||||
"create": "Créer",
|
||||
"import": "Importer",
|
||||
"logout": "Se déconnecter",
|
||||
"url": "URL",
|
||||
"submit": "Valider",
|
||||
"connect": "Connexion",
|
||||
"pin": "PIN",
|
||||
"kick": "Éjecter",
|
||||
"register": "Enregistrer",
|
||||
"docs": "Documentation",
|
||||
"close": "Fermer",
|
||||
"save": "Enregistrer",
|
||||
"description": "Description",
|
||||
"image": "Image",
|
||||
"title": "titre",
|
||||
"settings": "Paramètres",
|
||||
"repeat_password": ""
|
||||
},
|
||||
"index_page": {
|
||||
"slogan": "La plateforme de quiz open-source !",
|
||||
"meta": {
|
||||
"description": "ClassQuiz est une application de quiz comme KAHOOT! pour les étudiants, libre et open-source",
|
||||
"title": "Accueil"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz est un outil de quiz vous permettant de créer et gérer des questionnaires.",
|
||||
"2": "La fonctionnalité principale est la fonction d'import de quiz KAHOOT, vou permettant de créer des quiz directement depuis KAHOOT!."
|
||||
}
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Crée le",
|
||||
"question_count": "Nombre de question"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Quiz mis à jour avec succès !",
|
||||
"success_update_body": "Quiz mis à jour avec succès !"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"body": "Quiz créé avec succès !",
|
||||
"title": "Quiz créé avec succès !"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Ravi de vous connaître !",
|
||||
"create_account": "Créer un compte",
|
||||
"forgot_password?": "Mot de passe oublié ?",
|
||||
"already_have_account?": "Vous avez déjà un compte ?"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Temps en seconde",
|
||||
"add_new_question": "Ajouter une autre question",
|
||||
"add_new_answer": "Ajouter une autre réponse",
|
||||
"delete_question": "Supprimer la question",
|
||||
"delete_answer": "Supprimer la réponse",
|
||||
"right_or_true?": "Vrai?"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"visit_docs": "Voir la documentation"
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "Il y a déjà un administrateur enregistré pour ce jeu.",
|
||||
"start_game": "Commencer le jeu",
|
||||
"time_left": "Temps restant",
|
||||
"get_results": "Obtenir les résultats"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": ""
|
||||
}
|
||||
"login_page": {
|
||||
"modal": {
|
||||
"success": {
|
||||
"description": {
|
||||
"success": "Vous vous êtes connecté avec succès !",
|
||||
"success_check_mail": "Veuillez vérifier votre boîte mail, vous avez normalement reçu un mail avec un lien pour vous connecter."
|
||||
},
|
||||
"success": "Connexion réussie !",
|
||||
"success_check_mail": "Connexion réussie ! Vérifiez votre boîte mail !"
|
||||
},
|
||||
"error": {
|
||||
"wrong_creds": "Mot de passe ou email non-correct !",
|
||||
"description": {
|
||||
"wrong_creds": "Assurez-vous que votre mot de passe et votre email sont corrects !",
|
||||
"unexpected": "Une bonne vieille erreur inattendue s'est produite !"
|
||||
},
|
||||
"unexpected": "Erreur inattendue !"
|
||||
}
|
||||
},
|
||||
"welcome_back": "Bon retour parmi nous !",
|
||||
"login_or_create_account": "Se connecter ou créer un compte",
|
||||
"already_have_account": "Pas encore de compte ?"
|
||||
},
|
||||
"words": {
|
||||
"question": "Question",
|
||||
"answer": "Réponse",
|
||||
"stats": "Statistiques",
|
||||
"features": "Fonctionnalités",
|
||||
"login": "Connexion",
|
||||
"email": "Email",
|
||||
"username": "Pseudo",
|
||||
"password": "Mot de passe",
|
||||
"play": "Jouer",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"public": "Public",
|
||||
"start": "Commencer",
|
||||
"create": "Créer",
|
||||
"import": "Importer",
|
||||
"logout": "Se déconnecter",
|
||||
"url": "URL",
|
||||
"submit": "Valider",
|
||||
"connect": "Connexion",
|
||||
"pin": "PIN",
|
||||
"kick": "Éjecter",
|
||||
"register": "Enregistrer",
|
||||
"docs": "Documentation",
|
||||
"close": "Fermer",
|
||||
"save": "Enregistrer",
|
||||
"description": "Description",
|
||||
"image": "Image",
|
||||
"title": "titre",
|
||||
"settings": "Paramètres",
|
||||
"repeat_password": "",
|
||||
"overview": ""
|
||||
},
|
||||
"index_page": {
|
||||
"slogan": "La plateforme de quiz open-source !",
|
||||
"meta": {
|
||||
"description": "ClassQuiz est une application de quiz comme KAHOOT! pour les étudiants, libre et open-source",
|
||||
"title": "Accueil"
|
||||
},
|
||||
"features_description": {
|
||||
"1": "ClassQuiz est un outil de quiz vous permettant de créer et gérer des questionnaires.",
|
||||
"2": "La fonctionnalité principale est la fonction d'import de quiz KAHOOT, vou permettant de créer des quiz directement depuis KAHOOT!."
|
||||
}
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Crée le",
|
||||
"question_count": "Nombre de question"
|
||||
},
|
||||
"edit_page": {
|
||||
"success_update_title": "Quiz mis à jour avec succès !",
|
||||
"success_update_body": "Quiz mis à jour avec succès !"
|
||||
},
|
||||
"create_page": {
|
||||
"success": {
|
||||
"body": "Quiz créé avec succès !",
|
||||
"title": "Quiz créé avec succès !"
|
||||
}
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "Ravi de vous connaître !",
|
||||
"create_account": "Créer un compte",
|
||||
"forgot_password?": "Mot de passe oublié ?",
|
||||
"already_have_account?": "Vous avez déjà un compte ?"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Temps en seconde",
|
||||
"add_new_question": "Ajouter une autre question",
|
||||
"add_new_answer": "Ajouter une autre réponse",
|
||||
"delete_question": "Supprimer la question",
|
||||
"delete_answer": "Supprimer la réponse",
|
||||
"right_or_true?": "Vrai?"
|
||||
},
|
||||
"import_page": {
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"visit_docs": "Voir la documentation"
|
||||
},
|
||||
"admin_page": {
|
||||
"already_registered_as_admin": "Il y a déjà un administrateur enregistré pour ce jeu.",
|
||||
"start_game": "Commencer le jeu",
|
||||
"time_left": "Temps restant",
|
||||
"get_results": "Obtenir les résultats"
|
||||
},
|
||||
"password_reset_page": {
|
||||
"reset_password": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import '@fontsource/marck-script/index.css';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { signedIn } from '$lib/stores';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
@@ -17,15 +18,11 @@
|
||||
<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"
|
||||
>
|
||||
<!-- Our logo and button -->
|
||||
<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"
|
||||
>ClassQuiz</a
|
||||
>
|
||||
|
||||
<!-- Our open/close buttons -->
|
||||
<!-- Open menu -->
|
||||
<button
|
||||
class="lg:hidden"
|
||||
id="open-menu"
|
||||
@@ -76,41 +73,73 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Our list of items -->
|
||||
<ul
|
||||
id="menu-items"
|
||||
class="lg:flex w-full flex-col lg:flex-row lg:pl-6"
|
||||
class:hidden={!menuItems}
|
||||
>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/play">{$t('words.play')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/account/register">{$t('words.register')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/account/login">{$t('words.login')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="https://github.com/mawoka-myblock/classquiz">GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/docs">{$t('words.docs')}</a
|
||||
>
|
||||
</li>
|
||||
{#if $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">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/play">{$t('words.play')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/account/register">{$t('words.register')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/account/login">{$t('words.login')}</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="https://github.com/mawoka-myblock/classquiz">GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a
|
||||
class="text-lg font-medium lg:px-4 text-gray-600 hover:text-green-600 link-hover"
|
||||
href="/docs">{$t('words.docs')}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
execute: async (_a, _b) => ({ response: '' }),
|
||||
render: (_a, _b) => {}
|
||||
};
|
||||
let hcaptchaWidgetID;
|
||||
let hcaptchaWidgetID: any;
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
@@ -41,7 +41,7 @@
|
||||
if (username.length <= 3) {
|
||||
return;
|
||||
}
|
||||
let captcha_resp;
|
||||
let captcha_resp: string;
|
||||
try {
|
||||
const { response } = await hcaptcha.execute(hcaptchaWidgetID, {
|
||||
async: true
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
export let results: Array<Answer>;
|
||||
export let game_data: QuizData;
|
||||
export let question_index: number;
|
||||
export let question_index: string;
|
||||
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++) {
|
||||
console.log(question[i].answer, 'HI!!!');
|
||||
data_store[question[i].answer] = 0;
|
||||
@@ -56,7 +57,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 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">
|
||||
<td
|
||||
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';
|
||||
|
||||
export const navbarVisible = writable(true);
|
||||
export const signedIn = writable(false);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
import Navbar from '$lib/navbar.svelte';
|
||||
import { navbarVisible } from '$lib/stores.ts';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import { initLocalizationContext } from '$lib/i18n';
|
||||
|
||||
initLocalizationContext();
|
||||
|
||||
if (import.meta.env.VITE_SENTRY !== null) {
|
||||
if (import.meta.env.VITE_SENTRY !== undefined) {
|
||||
console.log(import.meta.env.VITE_SENTRY)
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY,
|
||||
dsn: String(import.meta.env.VITE_SENTRY),
|
||||
integrations: [new BrowserTracing()],
|
||||
|
||||
// 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 lang="ts">
|
||||
<script lang='ts'>
|
||||
import { getLocalization } from '../../lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
export let token: string;
|
||||
let isSubmitting = false;
|
||||
let passwordData = {
|
||||
interface PasswordData {
|
||||
password1: string;
|
||||
password2: string;
|
||||
}
|
||||
let passwordData: PasswordData = {
|
||||
password1: '',
|
||||
password2: ''
|
||||
};
|
||||
let passwordsValid = false;
|
||||
const checkIfPasswordsValid = (pwdata) => {
|
||||
const checkIfPasswordsValid = (pwdata: PasswordData): void => {
|
||||
passwordsValid = pwdata.password1 === pwdata.password2 && pwdata.password1.length >= 8;
|
||||
};
|
||||
$: checkIfPasswordsValid(passwordData);
|
||||
@@ -51,7 +55,7 @@
|
||||
};
|
||||
</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
|
||||
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"
|
||||
>
|
||||
{#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
|
||||
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"
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { getLocalization } from '../../lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
let changePasswordData = {
|
||||
interface ChangePasswordData {
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
newPasswordConfirm: string
|
||||
}
|
||||
|
||||
let changePasswordData: ChangePasswordData = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
newPasswordConfirm: ''
|
||||
};
|
||||
|
||||
let passwordChangeDataValid = false;
|
||||
const checkPasswords = (data) => {
|
||||
console.log('Check password');
|
||||
const checkPasswords = (data: ChangePasswordData): void => {
|
||||
passwordChangeDataValid =
|
||||
data.newPassword === data.newPasswordConfirm &&
|
||||
data.newPassword.length >= 8 &&
|
||||
@@ -102,29 +107,32 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<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'>
|
||||
{$t('words.email')}: {user.email}
|
||||
</p>
|
||||
<!-- 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
|
||||
>Old Password:<input
|
||||
class="m-2 text-black"
|
||||
bind:value={changePasswordData.oldPassword}
|
||||
/></label
|
||||
>Old Password:<input
|
||||
type='password'
|
||||
class='m-2 text-black'
|
||||
bind:value={changePasswordData.oldPassword}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>New Password:<input
|
||||
class="m-2 text-black"
|
||||
bind:value={changePasswordData.newPassword}
|
||||
/></label
|
||||
>New Password:<input
|
||||
type='password'
|
||||
class='m-2 text-black'
|
||||
bind:value={changePasswordData.newPassword}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>New Password again:<input
|
||||
class="m-2 text-black"
|
||||
bind:value={changePasswordData.newPasswordConfirm}
|
||||
/></label
|
||||
>New Password again:<input
|
||||
type='password'
|
||||
class='m-2 text-black'
|
||||
bind:value={changePasswordData.newPasswordConfirm}
|
||||
/></label
|
||||
>
|
||||
<button
|
||||
class="border-2 px-2 py-1 rounded-md border-black text-slate-100 hover:bg-amber-700 hover:text-indigo-100 transition duration-75 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
|
||||
import { socket } from '$lib/socket';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(false);
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
@@ -38,7 +41,10 @@
|
||||
export let auto_connect: boolean;
|
||||
export let game_token: string;
|
||||
let success = false;
|
||||
let players: Array<string> = [];
|
||||
interface Player {
|
||||
username: string
|
||||
}
|
||||
let players: Array<Player> = [];
|
||||
let errorMessage = '';
|
||||
let game_started = false;
|
||||
let quiz_data: QuizData;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
import { signedIn } from '$lib/stores';
|
||||
|
||||
export async function load({ session, url }) {
|
||||
if (!session.authenticated) {
|
||||
return {
|
||||
@@ -6,6 +8,9 @@
|
||||
redirect: '/account/login'
|
||||
};
|
||||
}
|
||||
if (session.authenticated) {
|
||||
signedIn.set(true);
|
||||
}
|
||||
const token = url.searchParams.get('token');
|
||||
const pin = url.searchParams.get('pin');
|
||||
let auto_connect = url.searchParams.get('connect') !== null;
|
||||
@@ -26,6 +31,9 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Editor from '$lib/editor.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import Title from '$lib/play/title.svelte';
|
||||
import Question from '$lib/play/question.svelte';
|
||||
import { socket } from '$lib/socket';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
interface GameMeta {
|
||||
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">
|
||||
import Footer from '$lib/footer.svelte';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
import { signedIn } from '$lib/stores';
|
||||
export async function load({ url, session }) {
|
||||
if (!session.authenticated) {
|
||||
return {
|
||||
@@ -6,6 +7,10 @@
|
||||
redirect: '/account/login'
|
||||
};
|
||||
}
|
||||
|
||||
if (session.authenticated) {
|
||||
signedIn.set(true);
|
||||
}
|
||||
const quiz_id = url.searchParams.get('quiz_id');
|
||||
if (quiz_id === null) {
|
||||
return {
|
||||
@@ -23,6 +28,9 @@
|
||||
<script lang="ts">
|
||||
import Editor from '$lib/editor.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
{:then lol}
|
||||
{:then _}
|
||||
{#if data !== undefined}
|
||||
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
|
||||
<Editor bind:data submit_button_text={$t('words.save')} />
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script context="module" lang="ts">
|
||||
import { signedIn } from '$lib/stores';
|
||||
export async function load({ session }) {
|
||||
if (!session.authenticated) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/account/login'
|
||||
};
|
||||
} else {
|
||||
if (session.authenticated) {
|
||||
signedIn.set(true);
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
@@ -16,6 +21,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
|
||||
navbarVisible.set(true);
|
||||
|
||||
const { t } = getLocalization();
|
||||
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">
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
+141
-126
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script context='module' lang='ts'>
|
||||
export async function load({ session }) {
|
||||
if (!session.authenticated) {
|
||||
return {
|
||||
@@ -14,11 +14,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { QuizData } from '../app';
|
||||
<script lang='ts'>
|
||||
import type { Question } from '../app';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
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 getData = async (): Promise<Array<QuizData>> => {
|
||||
@@ -59,161 +74,161 @@
|
||||
<title>ClassQuiz - Overview</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class='min-h-screen flex flex-col'>
|
||||
{#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
|
||||
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"
|
||||
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'
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
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'
|
||||
/>
|
||||
</svg>
|
||||
{:then quizzes}
|
||||
<div class="flex flex-col w-fit mx-auto">
|
||||
<div class='flex flex-col w-fit mx-auto'>
|
||||
<!-- <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'>
|
||||
Primary
|
||||
</button>-->
|
||||
<div class="w-full grid grid-cols-4 gap-2">
|
||||
<div class='w-full grid grid-cols-4 gap-2'>
|
||||
<a
|
||||
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"
|
||||
>{$t('words.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'
|
||||
>{$t('words.create')}
|
||||
</a>
|
||||
<a
|
||||
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"
|
||||
>{$t('words.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'
|
||||
>{$t('words.import')}
|
||||
</a>
|
||||
<a
|
||||
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"
|
||||
>{$t('words.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'
|
||||
>{$t('words.logout')}
|
||||
</a>
|
||||
<a
|
||||
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"
|
||||
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'
|
||||
>
|
||||
{$t('words.settings')}
|
||||
</a>
|
||||
</div>
|
||||
<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="overflow-hidden shadow-md sm:rounded-lg">
|
||||
<table class="min-w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
{$t('words.title')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
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')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
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')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
{$t('words.play')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
{$t('words.edit')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
{$t('words.delete')}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
|
||||
>
|
||||
{$t('words.public')}
|
||||
</th>
|
||||
</tr>
|
||||
<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='overflow-hidden shadow-md sm:rounded-lg'>
|
||||
<table class='min-w-full'>
|
||||
<thead class='bg-gray-50 dark:bg-gray-700'>
|
||||
<tr>
|
||||
<th
|
||||
scope='col'
|
||||
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
|
||||
>
|
||||
{$t('words.title')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
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')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
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')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
|
||||
>
|
||||
{$t('words.play')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
|
||||
>
|
||||
{$t('words.edit')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
|
||||
>
|
||||
{$t('words.delete')}
|
||||
</th>
|
||||
<th
|
||||
scope='col'
|
||||
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
|
||||
>
|
||||
{$t('words.public')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each quizzes as quiz}
|
||||
<tr
|
||||
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
|
||||
{#each quizzes as quiz}
|
||||
<tr
|
||||
class='bg-white border-b dark:bg-gray-800 dark:border-gray-700'
|
||||
>
|
||||
<td
|
||||
class='py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white'
|
||||
>
|
||||
<td
|
||||
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
|
||||
>
|
||||
{quiz.title}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
{formatDate(quiz.created_at)}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
{quiz.questions.length}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
{quiz.title}
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
{formatDate(quiz.created_at)}
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
{quiz.questions.length}
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
startGame(quiz.id);
|
||||
}}
|
||||
class="border border-green-600"
|
||||
>
|
||||
{$t('words.start')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
class='border border-green-600'
|
||||
>
|
||||
<a
|
||||
href="/edit?quiz_id={quiz.id}"
|
||||
class="border border-yellow-600"
|
||||
>{$t('words.edit')}</a
|
||||
>
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
{$t('words.start')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
<a
|
||||
href='/edit?quiz_id={quiz.id}'
|
||||
class='border border-yellow-600'
|
||||
>{$t('words.edit')}</a
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
deleteQuiz(quiz.id);
|
||||
}}
|
||||
class="border border-red-600"
|
||||
>
|
||||
{$t('words.delete')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
class='border border-red-600'
|
||||
>
|
||||
{#if quiz.public}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{$t('words.delete')}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
|
||||
>
|
||||
{#if quiz.public}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script context="module" lang="ts">
|
||||
export async function load({ url }) {
|
||||
<script context='module' lang='ts'>
|
||||
import { signedIn } from '$lib/stores';
|
||||
|
||||
export async function load({ url, session }) {
|
||||
if (session.authenticated) {
|
||||
signedIn.set(true);
|
||||
}
|
||||
const token = url.searchParams.get('pin');
|
||||
return {
|
||||
props: {
|
||||
@@ -9,7 +14,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang='ts'>
|
||||
import { socket } from '$lib/socket';
|
||||
import JoinGame from '$lib/play/join.svelte';
|
||||
import type { Answer, QuizData } from '../app';
|
||||
@@ -27,7 +32,7 @@
|
||||
}
|
||||
|
||||
// Variables init
|
||||
let question_index = '';
|
||||
let question_index: string = '';
|
||||
let unique = {};
|
||||
navbarVisible.set(false);
|
||||
let game_pin_valid: boolean;
|
||||
@@ -75,7 +80,7 @@
|
||||
{#if gameData !== undefined}
|
||||
{#each gameData.questions as question}
|
||||
{#if question.image !== undefined}
|
||||
<link rel="preload" as="image" href={question.image} />
|
||||
<link rel='preload' as='image' href={question.image} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ["py310"]
|
||||
Reference in New Issue
Block a user