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

This commit is contained in:
Dynnammo
2022-04-09 16:37:48 +02:00
committed by GitHub
59 changed files with 4412 additions and 5052 deletions
+22
View File
@@ -0,0 +1,22 @@
version = 1
test_patterns = ["classquiz/tests/**", "test_*.py"]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
[[analyzers]]
name = "javascript"
enabled = true
[[analyzers]]
name = "test-coverage"
enabled = true
[[transformers]]
name = "black"
enabled = true
+3
View File
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
extend-ignore = E203
+47
View File
@@ -0,0 +1,47 @@
# .github/workflows/app.yaml
name: PyTest
on:
push:
paths:
- "classquiz/tests/**"
- "classquiz/kahoot_importer/**"
- "classquiz/storage/**"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Install pipenv
run: pipx install pipenv
# Setup Python (faster than using Python container)
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
cache: 'pipenv'
- run: pipenv install --dev
- name: Prepare tests
run: |
echo ${{ secrets.DOTENV }} | base64 --decode > .env
set -o allexport; source .env; set +o allexport
mkdir /tmp/storage/
- name: Run tests
run: |
pipenv run coverage run -m pytest --asyncio-mode=strict classquiz/tests
pipenv run coverage xml
- name: Report results to DeepSource
run: |
# Install deepsource CLI
curl https://deepsource.io/cli | sh
# From the root directory, run the report coverage command
./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml
env:
DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }}
+2
View File
@@ -27,6 +27,8 @@ aiofiles = "*"
coverage = "*"
pytest = "*"
pytest-asyncio = "*"
flake8 = "*"
black = "*"
[requires]
python_version = "3.10"
Generated
+104 -14
View File
@@ -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"
}
}
+2
View File
@@ -4,6 +4,8 @@
<a href="https://github.com/mawoka-myblock/ClassQuiz/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc"><img alt="GitHub issues" src="https://img.shields.io/github/issues/mawoka-myblock/classquiz?style=for-the-badge"></a>
<a href="https://github.com/mawoka-myblock/ClassQuiz/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">
[![DeepSource](https://deepsource.io/gh/mawoka-myblock/ClassQuiz.svg/?label=active+issues&show_trend=true&token=5-2Na9HN-2CXcGkHjah_Rk09&style=for-the-badge)](https://deepsource.io/gh/mawoka-myblock/ClassQuiz/)
<img alt="Snky badge" src="https://img.shields.io/badge/Snyk-Check-success?style=for-the-badge">
<div align='center'>
<h2 align='center'>ClassQuiz</h2>
-1
View File
@@ -1 +0,0 @@
Generic single-database configuration.
-84
View File
@@ -1,84 +0,0 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from classquiz.db import metadata
target_metadata = metadata
from classquiz.config import settings
url = settings.db_url
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
url=url,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
-24
View File
@@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
@@ -1,28 +0,0 @@
"""made some changes
Revision ID: 76507db71fde
Revises:
Create Date: 2022-02-25 18:38:08.687974
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '76507db71fde'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+3 -5
View File
@@ -1,12 +1,12 @@
from fastapi import FastAPI, Request
import sentry_sdk
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")
+2 -1
View File
@@ -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()
+1
View File
@@ -6,6 +6,7 @@ import ormar
from classquiz.config import redis, settings
from classquiz.db.models import User
settings = settings()
+11 -4
View File
@@ -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,
)
+8 -3
View File
@@ -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
+17 -22
View File
@@ -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
+3 -2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
from aiohttp import ClientSession
from pydantic import BaseModel
from classquiz.kahoot_importer import _Card, _Kahoot
+23 -9
View File
@@ -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()
+14 -9
View File
@@ -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}")
+11 -6
View File
@@ -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"})}
+3 -8
View File
@@ -1,13 +1,8 @@
import os
from fastapi import APIRouter
from fastapi import APIRouter, Response
from fastapi.background import BackgroundTasks
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from classquiz.auth import *
from classquiz.config import redis
from classquiz.db.models import *
from pydantic import BaseModel
from classquiz.db.models import Quiz, User
router = APIRouter()
+9 -4
View File
@@ -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
View File
@@ -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)
+2 -3
View File
@@ -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)
+71 -28
View File
@@ -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']}",
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)
[
{
"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)
+16 -7
View File
@@ -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)
+2 -1
View File
@@ -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):
+3 -2
View File
@@ -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:
+105 -52
View File
@@ -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
+3 -1
View File
@@ -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)
+14 -6
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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",
+783 -1792
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -25,12 +25,12 @@ export async function getSession(event) {
if (redis_res === null) {
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;
+2 -2
View File
@@ -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);
}
}
+2 -1
View File
@@ -83,7 +83,8 @@
"description": "Beschreibung",
"image": "Bild",
"settings": "Einstellungen",
"repeat_password": "Passwort wiederholen"
"repeat_password": "Passwort wiederholen",
"overview": "Übersicht"
},
"editor": {
"time_in_seconds": "Zeit in Sekunden",
+2 -1
View File
@@ -83,7 +83,8 @@
"description": "Description",
"image": "Image",
"settings": "Settings",
"repeat_password": "Repeat password"
"repeat_password": "Repeat password",
"overview": "Overview"
},
"editor": {
"time_in_seconds": "Time in seconds",
+2 -1
View File
@@ -52,7 +52,8 @@
"image": "Image",
"title": "titre",
"settings": "Paramètres",
"repeat_password": ""
"repeat_password": "",
"overview": ""
},
"index_page": {
"slogan": "La plateforme de quiz open-source !",
+34 -5
View File
@@ -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,12 +73,43 @@
</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}
>
{#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"
@@ -112,5 +140,6 @@
href="/docs">{$t('words.docs')}</a
>
</li>
{/if}
</ul>
</nav>
+2 -2
View File
@@ -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
+4 -3
View File
@@ -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
View File
@@ -1,3 +1,4 @@
import { writable } from 'svelte/store';
export const navbarVisible = writable(true);
export const signedIn = writable(false);
+4 -3
View File
@@ -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();
+17 -9
View File
@@ -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,27 +107,30 @@
/>
</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"
type='password'
class='m-2 text-black'
bind:value={changePasswordData.oldPassword}
/></label
>
<label
>New Password:<input
class="m-2 text-black"
type='password'
class='m-2 text-black'
bind:value={changePasswordData.newPassword}
/></label
>
<label
>New Password again:<input
class="m-2 text-black"
type='password'
class='m-2 text-black'
bind:value={changePasswordData.newPasswordConfirm}
/></label
>
+7 -1
View File
@@ -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;
+8
View File
@@ -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
View File
@@ -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;
+14
View File
@@ -1,5 +1,19 @@
<script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ session }) {
if (session.authenticated) {
signedIn.set(true);
}
return {};
}
</script>
<script lang="ts">
import Footer from '$lib/footer.svelte';
import { navbarVisible } from '$lib/stores';
navbarVisible.set(true);
</script>
<div class="min-h-screen flex flex-col">
+9 -1
View File
@@ -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')} />
+8
View File
@@ -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 = '';
+11
View File
@@ -1,3 +1,14 @@
<script lang="ts" context="module">
import { signedIn } from '$lib/stores';
export async function load({ session }) {
if (session.authenticated) {
signedIn.set(true);
}
return {};
}
</script>
<script lang="ts">
import { navbarVisible } from '$lib/stores';
import { getLocalization } from '$lib/i18n';
+65 -50
View File
@@ -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,92 +74,92 @@
<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"
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"
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"
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">
<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"
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"
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"
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"
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"
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"
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"
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>
@@ -153,58 +168,58 @@
<tbody>
{#each quizzes as quiz}
<tr
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
class='bg-white border-b dark:bg-gray-800 dark:border-gray-700'
>
<td
class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
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"
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"
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"
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"
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='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"
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"
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"
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='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
{#if quiz.public}
+10 -5
View File
@@ -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}
+3
View File
@@ -0,0 +1,3 @@
[tool.black]
line-length = 120
target-version = ["py310"]