Merge pull request #437 from mawoka-myblock/backend-dependency-update

This commit is contained in:
Marlon
2025-08-01 14:59:19 +02:00
committed by GitHub
29 changed files with 2172 additions and 2375 deletions
+5 -4
View File
@@ -8,16 +8,13 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
fastapi = "*"
uvicorn = "*" uvicorn = "*"
python-socketio = "*" python-socketio = "*"
ormar = { version = "*", extras = ["postgresql"] }
passlib = "*" passlib = "*"
python-jose = "*" python-jose = "*"
alembic = "*" alembic = "*"
email-validator = "*" email-validator = "*"
python-multipart = "*" python-multipart = "*"
pydantic = "*"
redis = "*" redis = "*"
aiohttp = "*" aiohttp = "*"
gunicorn = "*" gunicorn = "*"
@@ -46,7 +43,11 @@ starlette = "*"
pyopenssl = "*" pyopenssl = "*"
python-dotenv = "*" python-dotenv = "*"
webauthn = "==1.*" webauthn = "==1.*"
pydantic-settings = "==2.2.1" fastapi = "*"
pypng = "*"
ormar = "*"
pydantic = "*"
pydantic-settings = "*"
[dev-packages] [dev-packages]
coverage = "*" coverage = "*"
Generated
+1910 -2187
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -16,7 +16,7 @@ settings = settings()
async def cache_account(criteria: str, content: str) -> Union[User, None]: async def cache_account(criteria: str, content: str) -> Union[User, None]:
async def insert_into_redis(usermodel: User, key: str): async def insert_into_redis(usermodel: User, key: str):
await redis.set(key, usermodel.json(), ex=settings.cache_expiry) await redis.set(key, usermodel.model_dump_json(), ex=settings.cache_expiry)
if criteria == "email": if criteria == "email":
try: try:
@@ -54,7 +54,7 @@ async def get_from_redis(key: str) -> Union[None, User]:
if user is None: if user is None:
return None return None
else: else:
return User.parse_obj(loads(user)) return User.model_validate(loads(user))
async def get_cache(criteria: str, content: str) -> User: async def get_cache(criteria: str, content: str) -> User:
+3 -3
View File
@@ -7,8 +7,8 @@ from functools import lru_cache
from redis import asyncio as redis_lib from redis import asyncio as redis_lib
import redis as redis_base_lib import redis as redis_base_lib
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import RedisDsn, PostgresDsn, BaseModel from pydantic import RedisDsn, PostgresDsn, BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
import meilisearch as MeiliSearch import meilisearch as MeiliSearch
from arq import create_pool from arq import create_pool
from arq.connections import RedisSettings, ArqRedis from arq.connections import RedisSettings, ArqRedis
@@ -17,7 +17,7 @@ from classquiz.storage import Storage
class CustomOpenIDProvider(BaseModel): class CustomOpenIDProvider(BaseModel):
scopes: str = "openid email profile" scopes: str | None = "openid email profile"
server_metadata_url: str server_metadata_url: str
client_id: str client_id: str
client_secret: str client_secret: str
@@ -69,7 +69,7 @@ class Settings(BaseSettings):
s3_access_key: str | None = None s3_access_key: str | None = None
s3_secret_key: str | None = None s3_secret_key: str | None = None
s3_bucket_name: str = "classquiz" s3_bucket_name: str = "classquiz"
s3_base_url: str | None = None s3_base_url: str | None
async def initialize_arq(): async def initialize_arq():
+85 -24
View File
@@ -10,7 +10,7 @@ from typing import Optional
import ormar import ormar
from ormar import ReferentialAction from ormar import ReferentialAction
from pydantic import BaseModel, Json, validator, RootModel from pydantic import BaseModel, Json, field_validator, ConfigDict, RootModel, ValidationInfo
from enum import Enum from enum import Enum
from . import metadata, database from . import metadata, database
from .quiztivity import QuizTivityPage from .quiztivity import QuizTivityPage
@@ -45,7 +45,13 @@ class User(ormar.Model):
totp_secret: str = ormar.String(max_length=32, min_length=32, nullable=True, default=None) totp_secret: str = ormar.String(max_length=32, min_length=32, nullable=True, default=None)
storage_used: int = ormar.BigInteger(nullable=False, default=0, minimum=0) storage_used: int = ormar.BigInteger(nullable=False, default=0, minimum=0)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="users") ormar_config = ormar.OrmarConfig(
tablename="users",
metadata=metadata,
database=database,
)
model_config = ConfigDict(use_enum_values=True)
class FidoCredentials(ormar.Model): class FidoCredentials(ormar.Model):
@@ -55,14 +61,22 @@ class FidoCredentials(ormar.Model):
sign_count: int = ormar.Integer() sign_count: int = ormar.Integer()
user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE) user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="fido_credentials") ormar_config = ormar.OrmarConfig(
tablename="fido_credentials",
metadata=metadata,
database=database,
)
class ApiKey(ormar.Model): class ApiKey(ormar.Model):
key: str = ormar.String(max_length=48, min_length=48, primary_key=True) key: str = ormar.String(max_length=48, min_length=48, primary_key=True)
user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE) user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="api_keys") ormar_config = ormar.OrmarConfig(
tablename="api_keys",
metadata=metadata,
database=database,
)
class UserSession(ormar.Model): class UserSession(ormar.Model):
@@ -78,7 +92,11 @@ class UserSession(ormar.Model):
user_agent: str = ormar.String(max_length=255, nullable=True) user_agent: str = ormar.String(max_length=255, nullable=True)
last_seen: datetime = ormar.DateTime(default=datetime.now()) last_seen: datetime = ormar.DateTime(default=datetime.now())
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="user_sessions") ormar_config = ormar.OrmarConfig(
tablename="user_sessions",
metadata=metadata,
database=database,
)
class ABCDQuizAnswer(BaseModel): class ABCDQuizAnswer(BaseModel):
@@ -123,27 +141,27 @@ class QuizQuestion(BaseModel):
image: str | None = None image: str | None = None
hide_results: bool | None = False hide_results: bool | None = False
@validator("answers") @field_validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, info: ValidationInfo):
if values["type"] == QuizQuestionType.ABCD and not isinstance(v[0], ABCDQuizAnswer): if info.data["type"] == QuizQuestionType.ABCD and not isinstance(v[0], ABCDQuizAnswer):
raise ValueError("Answers can't be none if type is ABCD") raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and not isinstance(v, RangeQuizAnswer): if info.data["type"] == QuizQuestionType.RANGE and not isinstance(v, RangeQuizAnswer):
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
if values["type"] == QuizQuestionType.VOTING and not isinstance(v[0], VotingQuizAnswer): if info.data["type"] == QuizQuestionType.VOTING and not isinstance(v[0], VotingQuizAnswer):
raise ValueError("Answer must be from type VotingQuizAnswer if type is VOTING") raise ValueError("Answer must be from type VotingQuizAnswer if type is VOTING")
if values["type"] == QuizQuestionType.TEXT and not isinstance(v[0], TextQuizAnswer): if info.data["type"] == QuizQuestionType.TEXT and not isinstance(v[0], TextQuizAnswer):
raise ValueError("Answer must be from type TextQuizAnswer if type is TEXT") raise ValueError("Answer must be from type TextQuizAnswer if type is TEXT")
if values["type"] == QuizQuestionType.ORDER and not isinstance(v[0], VotingQuizAnswer): if info.data["type"] == QuizQuestionType.ORDER and not isinstance(v[0], VotingQuizAnswer):
raise ValueError("Answer must be from type VotingQuizAnswer if type is ORDER") raise ValueError("Answer must be from type VotingQuizAnswer if type is ORDER")
if values["type"] == QuizQuestionType.SLIDE and not isinstance(v, str): if info.data["type"] == QuizQuestionType.SLIDE and not isinstance(v, str):
raise ValueError("Answer must be from type SlideElement if type is SLIDE") raise ValueError("Answer must be from type SlideElement if type is SLIDE")
if values["type"] == QuizQuestionType.CHECK and not isinstance(v[0], ABCDQuizAnswer): if info.data["type"] == QuizQuestionType.CHECK and not isinstance(v[0], ABCDQuizAnswer):
raise ValueError("Answers can't be none if type is CHECK") raise ValueError("Answers can't be none if type is CHECK")
return v return v
class QuizInput(BaseModel): class QuizInput(BaseModel):
public: bool = False public: bool | None = False
title: str title: str
description: str description: str
cover_image: str | None = None cover_image: str | None = None
@@ -172,13 +190,21 @@ class Quiz(ormar.Model):
views: int = ormar.Integer(nullable=False, default=0, server_default="0") views: int = ormar.Integer(nullable=False, default=0, server_default="0")
mod_rating: int | None = ormar.SmallInteger(nullable=True) mod_rating: int | None = ormar.SmallInteger(nullable=True)
ormar_config = ormar.OrmarConfig(tablename="quiz", metadata=metadata, database=database) ormar_config = ormar.OrmarConfig(
tablename="quiz",
metadata=metadata,
database=database,
)
class InstanceData(ormar.Model): class InstanceData(ormar.Model):
instance_id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4(), nullable=False, unique=True) instance_id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4(), nullable=False, unique=True)
ormar_config = ormar.OrmarConfig(tablename="instance_data", metadata=metadata, database=database) ormar_config = ormar.OrmarConfig(
tablename="instance_data",
metadata=metadata,
database=database,
)
class Token(BaseModel): class Token(BaseModel):
@@ -254,9 +280,20 @@ class AnswerData(BaseModel):
class AnswerDataList(RootModel): class AnswerDataList(RootModel):
# Just a method to make a top-level list
root: list[AnswerData] root: list[AnswerData]
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root(item)
def append(self, item):
self.root.append(item)
def __len__(self) -> int:
return len(self.root)
class GameInLobby(BaseModel): class GameInLobby(BaseModel):
game_pin: str game_pin: str
@@ -287,7 +324,11 @@ class GameResults(ormar.Model):
description: str = ormar.Text(nullable=False) description: str = ormar.Text(nullable=False)
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="game_results") ormar_config = ormar.OrmarConfig(
tablename="game_results",
metadata=metadata,
database=database,
)
class QuizTivityInput(BaseModel): class QuizTivityInput(BaseModel):
@@ -302,7 +343,11 @@ class QuizTivity(ormar.Model):
user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE) user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
pages: list[QuizTivityPage] = ormar.JSON(nullable=False) pages: list[QuizTivityPage] = ormar.JSON(nullable=False)
ormar_config = ormar.OrmarConfig(tablename="quiztivitys", metadata=metadata, database=database) ormar_config = ormar.OrmarConfig(
tablename="quiztivitys",
metadata=metadata,
database=database,
)
class QuizTivityShare(ormar.Model): class QuizTivityShare(ormar.Model):
@@ -312,7 +357,11 @@ class QuizTivityShare(ormar.Model):
quiztivity: QuizTivity | None = ormar.ForeignKey(QuizTivity, ondelete=ReferentialAction.CASCADE) quiztivity: QuizTivity | None = ormar.ForeignKey(QuizTivity, ondelete=ReferentialAction.CASCADE)
user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE) user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="quiztivityshares") ormar_config = ormar.OrmarConfig(
tablename="quiztivityshares",
metadata=metadata,
database=database,
)
class OnlyId(BaseModel): class OnlyId(BaseModel):
@@ -357,7 +406,11 @@ class StorageItem(ormar.Model):
server: str | None = ormar.Text(default=None, nullable=True) server: str | None = ormar.Text(default=None, nullable=True)
imported: bool = ormar.Boolean(default=False, nullable=True) imported: bool = ormar.Boolean(default=False, nullable=True)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="storage_items") ormar_config = ormar.OrmarConfig(
tablename="storage_items",
metadata=metadata,
database=database,
)
class PublicStorageItem(BaseModel): class PublicStorageItem(BaseModel):
@@ -441,7 +494,11 @@ class Controller(ormar.Model):
os_version: str | None = ormar.Text(nullable=True) os_version: str | None = ormar.Text(nullable=True)
wanted_os_version: str = ormar.Text(nullable=True, default=None) wanted_os_version: str = ormar.Text(nullable=True, default=None)
ormar_config = ormar.OrmarConfig(tablename="controller", metadata=metadata, database=database) ormar_config = ormar.OrmarConfig(
tablename="controller",
metadata=metadata,
database=database,
)
class Rating(ormar.Model): class Rating(ormar.Model):
@@ -451,4 +508,8 @@ class Rating(ormar.Model):
created_at: datetime = ormar.DateTime(nullable=False, server_default=func.now()) created_at: datetime = ormar.DateTime(nullable=False, server_default=func.now())
quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz) quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz)
ormar_config = ormar.OrmarConfig(database=database, metadata=metadata, tablename="rating") ormar_config = ormar.OrmarConfig(
tablename="rating",
metadata=metadata,
database=database,
)
+2 -2
View File
@@ -166,7 +166,7 @@ async def handle_import_from_excel(data: BinaryIO, user: User) -> Quiz:
answers=answers_list, answers=answers_list,
time=str(time), time=str(time),
image=existing_question["image"] if existing_question is not None else None, image=existing_question["image"] if existing_question is not None else None,
).dict() ).model_dump()
) )
if existing_quiz is None: if existing_quiz is None:
quiz = Quiz( quiz = Quiz(
@@ -208,7 +208,7 @@ async def meilisearch_init():
quiz_count = await Quiz.objects.filter(public=True).count() quiz_count = await Quiz.objects.filter(public=True).count()
number_of_docs = 0 number_of_docs = 0
for i in meilisearch.index(settings.meilisearch_index).get_stats(): for i in meilisearch.index(settings.meilisearch_index).get_stats():
number_of_docs = i[1]["numberOfDocuments"] number_of_docs = i[1]
break break
if number_of_docs != quiz_count: if number_of_docs != quiz_count:
print("MeiliSearch and Database got out of sync, syncthing them") print("MeiliSearch and Database got out of sync, syncthing them")
+2 -2
View File
@@ -127,9 +127,9 @@ class NotFoundError(Exception):
async def get_images(api_key: str, params: GetImagesParams) -> GetImagesResponse: async def get_images(api_key: str, params: GetImagesParams) -> GetImagesResponse:
async with ( async with (
ClientSession() as session, ClientSession() as session,
session.get("https://pixabay.com/api/", params={"key": api_key, **params.dict()}) as resp, session.get("https://pixabay.com/api/", params={"key": api_key, **params.model_dump()}) as resp,
): ):
if resp.status == 200: if resp.status == 200:
return GetImagesResponse.parse_obj(await resp.json()) return GetImagesResponse.model_validate(await resp.json())
else: else:
raise NotFoundError raise NotFoundError
+2 -2
View File
@@ -20,7 +20,7 @@ from asyncio import run
async def main(): async def main():
kahoot_quiz: _Response = await get("GAME_ID") kahoot_quiz: _Response = await get("GAME_ID")
print(kahoot_quiz.json(exclude={"kahoot"})) print(kahoot_quiz.model_dump_json(exclude={"kahoot"}))
run(main()) run(main())
``` ```
@@ -35,7 +35,7 @@ from asyncio import run
async def main(): async def main():
kahoot_quizzes: _Response = await search("QUERY") kahoot_quizzes: _Response = await search("QUERY")
print(kahoot_quizzes.json()) print(kahoot_quizzes.model_dump_json())
run(main()) run(main())
``` ```
+1 -1
View File
@@ -87,7 +87,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | int:
answers=answers, answers=answers,
time=str(q.time / 1000), time=str(q.time / 1000),
image=image, image=image,
).dict() ).model_dump()
) )
cover = None cover = None
if quiz.kahoot.cover != "" and quiz.kahoot.cover is not None: if quiz.kahoot.cover != "" and quiz.kahoot.cover is not None:
+35 -35
View File
@@ -35,43 +35,43 @@ class Plan(BaseModel):
class GitHubOauthResponse(BaseModel): class GitHubOauthResponse(BaseModel):
login: str login: str
id: int id: int
email: Optional[str] email: Optional[str] = None
node_id: Optional[str] node_id: Optional[str] = None
avatar_url: Optional[str] avatar_url: Optional[str] = None
gravatar_id: Optional[str] gravatar_id: Optional[str] = None
url: Optional[str] url: Optional[str] = None
html_url: Optional[str] html_url: Optional[str] = None
followers_url: Optional[str] followers_url: Optional[str] = None
following_url: Optional[str] following_url: Optional[str] = None
gists_url: Optional[str] gists_url: Optional[str] = None
starred_url: Optional[str] starred_url: Optional[str] = None
subscriptions_url: Optional[str] subscriptions_url: Optional[str] = None
organizations_url: Optional[str] organizations_url: Optional[str] = None
repos_url: Optional[str] repos_url: Optional[str] = None
events_url: Optional[str] events_url: Optional[str] = None
received_events_url: Optional[str] received_events_url: Optional[str] = None
type: Optional[str] type: Optional[str] = None
site_admin: Optional[str] site_admin: Optional[str] = None
name: Optional[str] name: Optional[str] = None
company: Optional[str] company: Optional[str] = None
blog: Optional[str] blog: Optional[str] = None
location: Optional[str] location: Optional[str] = None
hireable: Optional[bool] hireable: Optional[bool] = None
bio: Optional[str] bio: Optional[str] = None
twitter_username: Optional[str] twitter_username: Optional[str] = None
public_repos: Optional[int] public_repos: Optional[int] = None
public_gists: Optional[int] public_gists: Optional[int] = None
followers: Optional[int] followers: Optional[int] = None
following: Optional[int] following: Optional[int] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
private_gists: Optional[int] private_gists: Optional[int] = None
total_private_repos: Optional[int] total_private_repos: Optional[int] = None
owned_private_repos: Optional[int] owned_private_repos: Optional[int] = None
disk_usage: Optional[int] disk_usage: Optional[int] = None
collaborators: Optional[int] collaborators: Optional[int] = None
two_factor_authentication: Optional[bool] two_factor_authentication: Optional[bool] = None
plan: Optional[Plan] plan: Optional[Plan] = None
@router.get("/login") @router.get("/login")
@@ -35,7 +35,7 @@ async def join_game(data: JoinGameInput) -> JoinGameResponse:
if game_pin is None: if game_pin is None:
raise HTTPException(status_code=404, detail="Game not found") raise HTTPException(status_code=404, detail="Game not found")
game = await redis.get(f"game:{game_pin}") game = await redis.get(f"game:{game_pin}")
game = PlayGame.parse_raw(game) game = PlayGame.model_validate_json(game)
# Check if game is already running # Check if game is already running
if game.started: if game.started:
raise HTTPException(status_code=400, detail="Game started already") raise HTTPException(status_code=400, detail="Game started already")
@@ -25,7 +25,7 @@ async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now:
username = await redis.get(f"game:cqc:player:{player_id}") username = await redis.get(f"game:cqc:player:{player_id}")
if redis_res_game is None or username is None: if redis_res_game is None or username is None:
raise HTTPException(status_code=404, detail="id not existent") raise HTTPException(status_code=404, detail="id not existent")
game = PlayGame.parse_raw(redis_res_game) game = PlayGame.model_validate_json(redis_res_game)
if not game.question_show: if not game.question_show:
return return
@@ -64,7 +64,7 @@ async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now:
answers = await set_answer(answers, game_pin=game_pin, data=answer_data, q_index=game.current_question) answers = await set_answer(answers, game_pin=game_pin, data=answer_data, q_index=game.current_question)
player_count = await redis.scard(f"game_session:{game_pin}:players") player_count = await redis.scard(f"game_session:{game_pin}:players")
await sio.emit("player_answer", {}) await sio.emit("player_answer", {})
if answers is not None and len(answers.__root__) == player_count: if answers is not None and len(answers) == player_count:
await sio.emit("everyone_answered", {}) await sio.emit("everyone_answered", {})
@@ -96,7 +96,7 @@ async def websocket_endpoint(ws: WebSocket, game_id: str):
player_id, game_pin = game_id.split(":") player_id, game_pin = game_id.split(":")
if player_id is None or game_pin is None: if player_id is None or game_pin is None:
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="BadId").json()) await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="BadId").model_dump_json())
await ws.close(code=status.WS_1003_UNSUPPORTED_DATA) await ws.close(code=status.WS_1003_UNSUPPORTED_DATA)
username = await redis.get(f"game:cqc:player:{player_id}") username = await redis.get(f"game:cqc:player:{player_id}")
await sio.emit( await sio.emit(
@@ -104,17 +104,19 @@ async def websocket_endpoint(ws: WebSocket, game_id: str):
{"username": username, "sid": None}, {"username": username, "sid": None},
room=f"admin:{game_pin}", room=f"admin:{game_pin}",
) )
await redis.sadd(f"game_session:{game_pin}:players", GamePlayer(username=username, sid=None).json()) await redis.sadd(f"game_session:{game_pin}:players", GamePlayer(username=username, sid=None).model_dump_json())
while True: while True:
raw_data = await ws.receive_text() raw_data = await ws.receive_text()
try: try:
data = WebSocketRequest.parse_raw(raw_data) data = WebSocketRequest.model_validate_json(raw_data)
except ValidationError as e: except ValidationError as e:
print("ValError") print("ValError")
print(e) print(e)
print(raw_data) print(raw_data)
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="ValidationError").json()) await ws.send_text(
WebSocketRequest(type=WebSocketTypes.Error, data="ValidationError").model_dump_json()
)
continue continue
if data.type == WebSocketTypes.ButtonPress: if data.type == WebSocketTypes.ButtonPress:
@@ -122,7 +124,9 @@ async def websocket_endpoint(ws: WebSocket, game_id: str):
try: try:
answer_index = button_to_index_map[data.data.lower()] answer_index = button_to_index_map[data.data.lower()]
except (KeyError, AttributeError): except (KeyError, AttributeError):
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="InvalidButton").json()) await ws.send_text(
WebSocketRequest(type=WebSocketTypes.Error, data="InvalidButton").model_dump_json()
)
continue continue
await submit_answer_fn(answer_index, game_pin, player_id, now) await submit_answer_fn(answer_index, game_pin, player_id, now)
+3 -3
View File
@@ -57,7 +57,7 @@ async def get_controller(id: uuid.UUID, user: User = Depends(get_current_user))
controller = await Controller.objects.get_or_none(id=id, user=user.id) controller = await Controller.objects.get_or_none(id=id, user=user.id)
if controller is None: if controller is None:
raise HTTPException(status_code=404, detail="Controller not found") raise HTTPException(status_code=404, detail="Controller not found")
return GetControllerResponse(**controller.dict()) return GetControllerResponse(**controller.model_dump())
class ModifyControllerInput(BaseModel): class ModifyControllerInput(BaseModel):
@@ -76,7 +76,7 @@ async def modify_controller(
controller.player_name = data.player_name controller.player_name = data.player_name
controller.name = data.name controller.name = data.name
await controller.update() await controller.update()
return GetControllerResponse(**controller.dict()) return GetControllerResponse(**controller.model_dump())
@router.get("/list") @router.get("/list")
@@ -86,7 +86,7 @@ async def get_all_controllers(user: User = Depends(get_current_user)) -> list[Ge
return [] return []
return_list = [] return_list = []
for controller in controllers: for controller in controllers:
return_list.append(GetControllerResponse(**controller.dict())) return_list.append(GetControllerResponse(**controller.model_dump()))
return return_list return return_list
+6 -4
View File
@@ -60,7 +60,9 @@ async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = D
edit_id = os.urandom(4).hex() edit_id = os.urandom(4).hex()
await redis.sadd("edit_sessions", edit_id) await redis.sadd("edit_sessions", edit_id)
await redis.set( await redis.set(
f"edit_session:{edit_id}", EditSessionData(quiz_id=quiz_id, edit=edit, user_id=user.id).json(), ex=3600 f"edit_session:{edit_id}",
EditSessionData(quiz_id=quiz_id, edit=edit, user_id=user.id).model_dump_json(),
ex=3600,
) )
return InitEditorResponse(token=edit_id) return InitEditorResponse(token=edit_id)
@@ -70,7 +72,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
session_data = await redis.get(f"edit_session:{edit_id}") session_data = await redis.get(f"edit_session:{edit_id}")
if session_data is None: if session_data is None:
raise HTTPException(status_code=401, detail="Edit ID not found!") raise HTTPException(status_code=401, detail="Edit ID not found!")
session_data = EditSessionData.parse_raw(session_data) session_data = EditSessionData.model_validate_json(session_data)
quiz_input.title = bleach.clean(quiz_input.title, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True) quiz_input.title = bleach.clean(quiz_input.title, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
quiz_input.description = bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True) quiz_input.description = bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
if quiz_input.background_color is not None: if quiz_input.background_color is not None:
@@ -122,7 +124,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
quiz.public = quiz_input.public quiz.public = quiz_input.public
quiz.description = quiz_input.description quiz.description = quiz_input.description
quiz.updated_at = datetime.now() quiz.updated_at = datetime.now()
quiz.questions = quiz_input.dict()["questions"] quiz.questions = quiz_input.model_dump()["questions"]
quiz.cover_image = quiz_input.cover_image quiz.cover_image = quiz_input.cover_image
quiz.background_color = quiz_input.background_color quiz.background_color = quiz_input.background_color
quiz.background_image = quiz_input.background_image quiz.background_image = quiz_input.background_image
@@ -140,7 +142,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
return quiz return quiz
else: else:
quiz = Quiz( quiz = Quiz(
**quiz_input.dict(), **quiz_input.model_dump(),
user_id=session_data.user_id, user_id=session_data.user_id,
id=session_data.quiz_id, id=session_data.quiz_id,
created_at=datetime.now(), created_at=datetime.now(),
+4 -4
View File
@@ -51,8 +51,8 @@ async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)
question["image"] = i question["image"] = i
if quiz.cover_image is not None: if quiz.cover_image is not None:
image_urls[-1] = quiz.cover_image image_urls[-1] = quiz.cover_image
quiz.cover_image = -1 quiz.cover_image = "-1"
quiz_dict = quiz.dict() quiz_dict = quiz.model_dump()
del quiz_dict["user_id"], quiz_dict["id"] del quiz_dict["user_id"], quiz_dict["id"]
quiz_dict["created_at"] = quiz_dict["created_at"].isoformat() quiz_dict["created_at"] = quiz_dict["created_at"].isoformat()
quiz_dict["updated_at"] = quiz_dict["updated_at"].isoformat() quiz_dict["updated_at"] = quiz_dict["updated_at"].isoformat()
@@ -129,7 +129,7 @@ async def import_quiz(file: UploadFile = File(), user: User = Depends(get_curren
question["image"] = image_urls[question["image"]] question["image"] = image_urls[question["image"]]
if quiz_dict["cover_image"] is not None: if quiz_dict["cover_image"] is not None:
quiz_dict["cover_image"] = image_urls[-1] quiz_dict["cover_image"] = image_urls[-1]
quiz = Quiz.parse_obj(quiz_dict) quiz = Quiz.model_validate(quiz_dict)
quiz.user_id = user.id quiz.user_id = user.id
quiz.imported_from_kahoot = None quiz.imported_from_kahoot = None
quiz.mod_rating = None quiz.mod_rating = None
@@ -165,7 +165,7 @@ async def export_quiz_as_excel(quiz_id: uuid.UUID, user: User = Depends(get_curr
], ],
) )
for i, question in enumerate(quiz.questions): for i, question in enumerate(quiz.questions):
question = QuizQuestion.parse_obj(question) question = QuizQuestion.model_validate(question)
data: list[Any] = [None] * 9 data: list[Any] = [None] * 9
data[0] = i + 1 data[0] = i + 1
data[1] = question.question data[1] = question.question
+20 -18
View File
@@ -7,7 +7,7 @@ import json
import uuid import uuid
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, validator from pydantic import BaseModel, field_validator
from classquiz.config import settings, redis from classquiz.config import settings, redis
from classquiz.db.models import ( from classquiz.db.models import (
PlayGame, PlayGame,
@@ -41,7 +41,7 @@ class _ABCDQuizAnswer(ABCDQuizAnswer):
class _QuizQuestion(QuizQuestion): class _QuizQuestion(QuizQuestion):
answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer] answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer]
@validator("answers") @field_validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, values):
# if values["type"] == QuizQuestionType.ABCD and type(v[0]) != _ABCDQuizAnswer: # if values["type"] == QuizQuestionType.ABCD and type(v[0]) != _ABCDQuizAnswer:
# print(type(v[0]), values) # print(type(v[0]), values)
@@ -83,7 +83,7 @@ async def get_live_game_data(
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None or user_id is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game = _PlayGame.parse_raw(redis_res) game = _PlayGame.model_validate_json(redis_res)
if game.user_id != user_id: if game.user_id != user_id:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
for i, question in enumerate(game.questions): for i, question in enumerate(game.questions):
@@ -105,18 +105,18 @@ async def get_live_game_data(
data_redis_res = await redis.get(f"game_session:{game_pin}") data_redis_res = await redis.get(f"game_session:{game_pin}")
if data_redis_res is None: if data_redis_res is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
data = GameSession.parse_raw(data_redis_res) data = GameSession.model_validate_json(data_redis_res)
for i in range(0, len(game.questions)): for i in range(0, len(game.questions)):
res = await redis.get(f"game_session:{game_pin}:{i}") res = await redis.get(f"game_session:{game_pin}:{i}")
if res is None: if res is None:
break break
else: else:
res = json.loads(res) res = json.loads(res)
ga_1 = GameAnswer1(id=i, answers=[GameAnswer2.parse_obj(i) for i in res]) ga_1 = GameAnswer1(id=i, answers=[GameAnswer2.model_validate(i) for i in res])
data.answers.append(ga_1) data.answers.append(ga_1)
player_count = await redis.scard(f"game_session:{game_pin}:players") player_count = await redis.scard(f"game_session:{game_pin}:players")
total_questions = len(game.questions) total_questions = len(game.questions)
game = _GetLivePlayGame(**{**game.dict(), "total_questions": total_questions}) game = _GetLivePlayGame(**{**game.model_dump(), "total_questions": total_questions})
if in_human_count: if in_human_count:
game.current_question += 1 game.current_question += 1
@@ -160,10 +160,10 @@ async def get_game_session(game_pin: str, api_key: str | None = None, game_id: u
redis_res = await redis.get(f"game_session:{game_pin}") redis_res = await redis.get(f"game_session:{game_pin}")
if redis_res is None: if redis_res is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
data = GameSession.parse_raw(redis_res) data = GameSession.model_validate_json(redis_res)
if user_id is None and data.game_id != str(game_id): if user_id is None and data.game_id != str(game_id):
raise HTTPException(status_code=401, detail="Game not found or API key not found") raise HTTPException(status_code=401, detail="Game not found or API key not found")
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}")) game = PlayGame.model_validate_json(await redis.get(f"game:{game_pin}"))
if game.user_id != user_id and data.game_id != str(game_id): if game.user_id != user_id and data.game_id != str(game_id):
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
for i in range(0, len(game.questions)): for i in range(0, len(game.questions)):
@@ -172,12 +172,12 @@ async def get_game_session(game_pin: str, api_key: str | None = None, game_id: u
break break
else: else:
res = json.loads(res) res = json.loads(res)
ga_1 = GameAnswer1(id=i, answers=[GameAnswer2.parse_obj(i) for i in res]) ga_1 = GameAnswer1(id=i, answers=[GameAnswer2.model_validate(i) for i in res])
data.answers.append(ga_1) data.answers.append(ga_1)
players = await redis.smembers(f"game_session:{game_pin}:players") players = await redis.smembers(f"game_session:{game_pin}:players")
player_list = [] player_list = []
for p in players: for p in players:
player_list.append(GamePlayer.parse_raw(p)) player_list.append(GamePlayer.model_validate_json(p))
return player_list return player_list
@@ -190,16 +190,18 @@ async def set_next_question(game_pin: str, question_number: int, api_key: str):
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None or user_id is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game_data = PlayGame.parse_raw(redis_res) game_data = PlayGame.model_validate_json(redis_res)
if game_data.user_id != user_id: if game_data.user_id != user_id:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game_data.current_question = question_number game_data.current_question = question_number
await redis.set(f"game:{game_pin}", game_data.json(), ex=18000) await redis.set(f"game:{game_pin}", game_data.model_dump_json(), ex=18000)
await sio.emit( await sio.emit(
"set_question_number", "set_question_number",
{ {
"question_index": question_number, "question_index": question_number,
"question": ReturnQuestion(**game_data.dict(include={"questions"})["questions"][question_number]).dict(), "question": ReturnQuestion(
**game_data.model_dump(include={"questions"})["questions"][question_number]
).model_dump(),
}, },
room=game_pin, room=game_pin,
) )
@@ -231,7 +233,7 @@ async def too_stupid_to_come_up_with_a_name(game_pin: str, api_key: str, in_huma
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None or user_id is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game = PlayGame.parse_raw(redis_res) game = PlayGame.model_validate_json(redis_res)
for i, question in enumerate(game.questions): for i, question in enumerate(game.questions):
if question.type == QuizQuestionType.ABCD: if question.type == QuizQuestionType.ABCD:
for o, answer in enumerate(question.answers): for o, answer in enumerate(question.answers):
@@ -244,7 +246,7 @@ async def too_stupid_to_come_up_with_a_name(game_pin: str, api_key: str, in_huma
if game.current_question >= 0: if game.current_question >= 0:
return [ return [
{ {
**game.questions[game.current_question].dict(), **game.questions[game.current_question].model_dump(),
"current_question": game.current_question + 1 if in_human_count else game.current_question, "current_question": game.current_question + 1 if in_human_count else game.current_question,
"total_questions": len(game.questions), "total_questions": len(game.questions),
} }
@@ -263,17 +265,17 @@ async def voting_results(game_pin: str, api_key: str, as_array: bool = False):
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None or user_id is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found or API key not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game = PlayGame.parse_raw(redis_res) game = PlayGame.model_validate_json(redis_res)
if game.questions[game.current_question].type != QuizQuestionType.VOTING: if game.questions[game.current_question].type != QuizQuestionType.VOTING:
return return
answer_data = await redis.get(f"game_session:{game_pin}:{game.current_question}") answer_data = await redis.get(f"game_session:{game_pin}:{game.current_question}")
if answer_data is None: if answer_data is None:
return return
answer_list = AnswerDataList.parse_raw(answer_data) answer_list = AnswerDataList.model_validate_json(answer_data)
answer_dict = {} answer_dict = {}
for answer in game.questions[game.current_question].answers: for answer in game.questions[game.current_question].answers:
answer_dict[answer.answer] = 0 answer_dict[answer.answer] = 0
for answer in answer_list.__root__: for answer in answer_list:
answer_dict[answer.answer] += 1 answer_dict[answer.answer] += 1
if as_array: if as_array:
return [answer_dict] return [answer_dict]
+5 -5
View File
@@ -63,7 +63,7 @@ class StartLoginResponse(BaseModel):
def verify_webauthn(data, fidocredentialss: list[FidoCredentials], login_session: LoginSession): def verify_webauthn(data, fidocredentialss: list[FidoCredentials], login_session: LoginSession):
try: try:
credential = AuthenticationCredential.parse_obj(data) credential = AuthenticationCredential.model_validate(data)
except ValidationError: except ValidationError:
print("ValidationError") print("ValidationError")
raise HTTPException(401) raise HTTPException(401)
@@ -141,7 +141,7 @@ async def start_login(data: StartLoginInput):
webauthn_challenge=webauthn_challenge, webauthn_challenge=webauthn_challenge,
) )
session_id = os.urandom(16).hex() session_id = os.urandom(16).hex()
await redis.set(f"login_session:{session_id}", login_session.json(), ex=600) await redis.set(f"login_session:{session_id}", login_session.model_dump_json(), ex=600)
return StartLoginResponse(step_1=step_1, step_2=step_2, session_id=session_id, webauthn_data=webauthn_data) return StartLoginResponse(step_1=step_1, step_2=step_2, session_id=session_id, webauthn_data=webauthn_data)
@@ -157,7 +157,7 @@ async def step_1_endpoint(session_id: str, data: StepInput, request: Request, re
redis_res = await redis.get(f"login_session:{session_id}") redis_res = await redis.get(f"login_session:{session_id}")
if redis_res is None: if redis_res is None:
raise HTTPException(401, detail="wrong credentials") raise HTTPException(401, detail="wrong credentials")
login_session = LoginSession.parse_raw(redis_res) login_session = LoginSession.model_validate_json(redis_res)
if step_id == 1: if step_id == 1:
if data.auth_type not in {*login_session.step_1, StartLoginResponseTypes.BACKUP}: if data.auth_type not in {*login_session.step_1, StartLoginResponseTypes.BACKUP}:
@@ -177,7 +177,7 @@ async def step_1_endpoint(session_id: str, data: StepInput, request: Request, re
return await log_user_in(user, request, response) return await log_user_in(user, request, response)
else: else:
login_session.step1_success = True login_session.step1_success = True
await redis.set(f"login_session:{session_id}", login_session.json(), ex=600) await redis.set(f"login_session:{session_id}", login_session.model_dump_json(), ex=600)
return Response(status_code=202) return Response(status_code=202)
else: else:
print("Wrong Password") print("Wrong Password")
@@ -189,7 +189,7 @@ async def step_1_endpoint(session_id: str, data: StepInput, request: Request, re
return await log_user_in(user, request, response) return await log_user_in(user, request, response)
else: else:
login_session.step1_success = True login_session.step1_success = True
await redis.set(f"login_session:{session_id}", login_session.json(), ex=600) await redis.set(f"login_session:{session_id}", login_session.model_dump_json(), ex=600)
return Response(status_code=202) return Response(status_code=202)
else: else:
raise HTTPException(401, detail="webauthn failed") raise HTTPException(401, detail="webauthn failed")
+8 -10
View File
@@ -22,7 +22,6 @@ from classquiz.config import redis, settings, storage, meilisearch
from classquiz.db.models import Quiz, User, PlayGame, GameInLobby, QuizQuestion, QuizQuestionType from classquiz.db.models import Quiz, User, PlayGame, GameInLobby, QuizQuestion, QuizQuestionType
from classquiz.helpers.box_controller import generate_code from classquiz.helpers.box_controller import generate_code
from classquiz.kahoot_importer.import_quiz import import_quiz from classquiz.kahoot_importer.import_quiz import import_quiz
from uuid import UUID
import urllib.parse import urllib.parse
settings = settings() settings = settings()
@@ -72,7 +71,7 @@ async def get_public_quiz(quiz_id: uuid.UUID):
else: else:
quiz.views += 1 quiz.views += 1
await quiz.update() await quiz.update()
return PublicQuizResponse(**quiz.dict()) return PublicQuizResponse(**quiz.model_dump())
@router.post("/start/{quiz_id}") @router.post("/start/{quiz_id}")
@@ -131,15 +130,15 @@ async def start_quiz(
if cqcs_enabled: if cqcs_enabled:
code = generate_code(6) code = generate_code(6)
await redis.set(f"game:cqc:code:{code}", game_pin, ex=3600) await redis.set(f"game:cqc:code:{code}", game_pin, ex=3600)
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000) await redis.set(f"game:{str(game.game_pin)}", game.model_dump_json(), ex=18000)
await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000) await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
await redis.set( await redis.set(
f"game_in_lobby:{user.id.hex}", f"game_in_lobby:{user.id.hex}",
GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).json(), GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).model_dump_json(),
ex=900, ex=900,
) )
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"}), "cqc_code": code} return {**quiz.model_dump(exclude={"id"}), **game.model_dump(exclude={"questions"}), "cqc_code": code}
class CheckIfCaptchaEnabledResponse(BaseModel): class CheckIfCaptchaEnabledResponse(BaseModel):
@@ -153,7 +152,7 @@ async def check_if_captcha_enabled(game_pin: str):
game = await redis.get(f"game:{game_pin}") game = await redis.get(f"game:{game_pin}")
if game is None: if game is None:
return JSONResponse(status_code=404, content={"detail": "game not found"}) return JSONResponse(status_code=404, content={"detail": "game not found"})
game = PlayGame.parse_raw(game) game = PlayGame.model_validate_json(game)
if game.captcha_enabled: if game.captcha_enabled:
return CheckIfCaptchaEnabledResponse(enabled=True, game_mode=game.game_mode, custom_field=game.custom_field) return CheckIfCaptchaEnabledResponse(enabled=True, game_mode=game.game_mode, custom_field=game.custom_field)
else: else:
@@ -229,9 +228,8 @@ async def export_quiz_answers(export_token: str, game_pin: str):
raise HTTPException(status_code=404, detail="export token not found") raise HTTPException(status_code=404, detail="export token not found")
data = json.loads(data) data = json.loads(data)
data2 = await redis.get(f"game:{game_pin}") data2 = await redis.get(f"game:{game_pin}")
game_data = PlayGame.parse_raw(data2) game_data = PlayGame.model_validate_json(data2)
print(type(game_data.quiz_id)) quiz = await Quiz.objects.get_or_none(id=game_data.quiz_id)
quiz = await Quiz.objects.get_or_none(id=UUID(game_data.quiz_id))
if quiz is None: if quiz is None:
raise HTTPException(status_code=404, detail="quiz not found") raise HTTPException(status_code=404, detail="quiz not found")
@@ -257,4 +255,4 @@ async def export_quiz_answers(export_token: str, game_pin: str):
@router.post("/excel-import") @router.post("/excel-import")
async def import_from_excel(file: UploadFile = File(), user: User = Depends(get_current_user)) -> Quiz: async def import_from_excel(file: UploadFile = File(), user: User = Depends(get_current_user)) -> Quiz:
quiz = await handle_import_from_excel(file.file, user) quiz = await handle_import_from_excel(file.file, user)
return Quiz.parse_obj(quiz.dict(exclude={"user_id": ...})) return Quiz.model_validate(quiz.model_dump(exclude={"user_id": ...}))
+4 -2
View File
@@ -19,7 +19,9 @@ router.include_router(shares_router, prefix="/shares")
@router.post("/create", response_model_exclude={"user": ...}) @router.post("/create", response_model_exclude={"user": ...})
async def create_quiztivity(data: QuizTivityInput, user: User = Depends(get_current_user)) -> QuizTivity: async def create_quiztivity(data: QuizTivityInput, user: User = Depends(get_current_user)) -> QuizTivity:
quiztivity = QuizTivity.parse_obj({**data.dict(), "user": user, "id": uuid4(), "created_at": datetime.now()}) quiztivity = QuizTivity.model_validate(
{**data.model_dump(), "user": user, "id": uuid4(), "created_at": datetime.now()}
)
return await quiztivity.save() return await quiztivity.save()
@@ -36,7 +38,7 @@ async def put_quiztivity(data: QuizTivityInput, uuid: UUID, user: User = Depends
quiztivity = await QuizTivity.objects.get_or_none(id=uuid, user=user) quiztivity = await QuizTivity.objects.get_or_none(id=uuid, user=user)
if quiztivity is None: if quiztivity is None:
raise HTTPException(status_code=404, detail="QuizTivity not found") raise HTTPException(status_code=404, detail="QuizTivity not found")
quiztivity.pages = data.dict()["pages"] quiztivity.pages = data.model_dump()["pages"]
quiztivity.title = data.title quiztivity.title = data.title
return await quiztivity.update() return await quiztivity.update()
+1 -1
View File
@@ -17,5 +17,5 @@ async def get_game_in_lobby(user: User = Depends(get_current_user)):
game_in_lobby_raw = await redis.get(f"game_in_lobby:{user.id.hex}") game_in_lobby_raw = await redis.get(f"game_in_lobby:{user.id.hex}")
if game_in_lobby_raw is None: if game_in_lobby_raw is None:
raise HTTPException(status_code=404, detail="No game waiting") raise HTTPException(status_code=404, detail="No game waiting")
game_in_lobby = GameInLobby.parse_raw(game_in_lobby_raw) game_in_lobby = GameInLobby.model_validate_json(game_in_lobby_raw)
return game_in_lobby return game_in_lobby
+3 -3
View File
@@ -66,7 +66,7 @@ router.include_router(oauth.router, tags=["users", "oauth"], prefix="/oauth")
async def create_user(user: RouteUser, background_task: BackgroundTasks) -> User | JSONResponse: async def create_user(user: RouteUser, background_task: BackgroundTasks) -> User | JSONResponse:
if settings.registration_disabled: if settings.registration_disabled:
raise HTTPException(status_code=423) raise HTTPException(status_code=423)
user = User(**user.dict(), id=uuid.uuid4(), avatar=gzipped_user_avatar(), created_at=datetime.now()) user = User(**user.model_dump(), id=uuid.uuid4(), avatar=gzipped_user_avatar(), created_at=datetime.now())
try: try:
validate_email(user.email) validate_email(user.email)
except EmailNotValidError as e: except EmailNotValidError as e:
@@ -232,7 +232,7 @@ async def reset_password_with_token(reset_password: ResetPassword, response: Res
@router.get("/sessions/list", response_model=list[UserSession], response_model_exclude={"user", "session_key", "quizs"}) @router.get("/sessions/list", response_model=list[UserSession], response_model_exclude={"user", "session_key", "quizs"})
async def list_sessions(user: User = Depends(get_current_user)): async def list_sessions(user: User = Depends(get_current_user)):
sessions = await UserSession.objects.filter(user=user).all() sessions = await UserSession.objects.filter(user=user).all()
return [session.dict() for session in sessions] return [session.model_dump() for session in sessions]
@router.delete("/sessions/{session_id}") @router.delete("/sessions/{session_id}")
@@ -341,7 +341,7 @@ async def get_email_from_jwt(data: GetEmailFromJWT):
async def generate_api_key(user: User = Depends(get_current_user)): async def generate_api_key(user: User = Depends(get_current_user)):
key = ApiKey(key=os.urandom(24).hex(), user=user) key = ApiKey(key=os.urandom(24).hex(), user=user)
await key.save() await key.save()
return key.dict(include={"key"}) return key.model_dump(include={"key"})
@router.get("/api_keys", response_model=list[ApiKey], response_model_include={"key"}) @router.get("/api_keys", response_model=list[ApiKey], response_model_include={"key"})
+1 -1
View File
@@ -59,7 +59,7 @@ class IpResponse(BaseModel):
@router.get("/ip-lookup/{ip}", response_model=IpResponse) @router.get("/ip-lookup/{ip}", response_model=IpResponse)
async def get_ip_data(ip: str, _: User = Depends(get_current_user)): async def get_ip_data(ip: str, _: User = Depends(get_current_user)):
async with ClientSession() as session, session.get(f"http://ip-api.com/json/{ip}") as response: async with ClientSession() as session, session.get(f"http://ip-api.com/json/{ip}") as response:
data = await response.json() data = await response.model_dump_json()
try: try:
return IpResponse(**data) return IpResponse(**data)
except ValidationError: except ValidationError:
+39 -50
View File
@@ -24,7 +24,7 @@ from classquiz.db.models import (
AnswerDataList, AnswerDataList,
AnswerData, AnswerData,
) )
from pydantic import BaseModel, ValidationError, validator from pydantic import BaseModel, ValidationError, field_validator, ValidationInfo
from datetime import datetime from datetime import datetime
from classquiz.socket_server.export_helpers import save_quiz_to_storage from classquiz.socket_server.export_helpers import save_quiz_to_storage
@@ -64,11 +64,11 @@ async def set_answer(answers, game_pin: str, q_index: int, data: AnswerData) ->
if answers is None: if answers is None:
answers = AnswerDataList([data]) answers = AnswerDataList([data])
else: else:
answers = AnswerDataList.parse_raw(answers) answers = AnswerDataList.model_validate_json(answers)
answers.root.append(data) answers.append(data)
await redis.set( await redis.set(
f"game_session:{game_pin}:{q_index}", f"game_session:{game_pin}:{q_index}",
answers.json(), answers.model_dump_json(),
ex=7200, ex=7200,
) )
return answers return answers
@@ -106,14 +106,12 @@ async def rejoin_game(sid: str, data: dict):
await sio.emit("time_sync", encrypted_datetime, room=sid) await sio.emit("time_sync", encrypted_datetime, room=sid)
await redis.set(redis_sid_key, sid) await redis.set(redis_sid_key, sid)
await redis.srem( await redis.srem(
f"game_session:{data.game_pin}:players", f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=data.old_sid).model_dump_json()
GamePlayer(username=data.username, sid=data.old_sid).json(),
) )
await redis.sadd( await redis.sadd(
f"game_session:{data.game_pin}:players", f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).model_dump_json()
GamePlayer(username=data.username, sid=sid).json(),
) )
game_data = PlayGame.parse_raw(redis_res) game_data = PlayGame.model_validate_json(redis_res)
session = { session = {
"game_pin": data.game_pin, "game_pin": data.game_pin,
"username": data.username, "username": data.username,
@@ -125,7 +123,7 @@ async def rejoin_game(sid: str, data: dict):
await sio.emit( await sio.emit(
"rejoined_game", "rejoined_game",
{ {
**json.loads(game_data.json(exclude={"quiz_id", "questions", "user_id"})), **json.loads(game_data.model_dump_json(exclude={"quiz_id", "questions", "user_id"})),
"question_count": len(game_data.questions), "question_count": len(game_data.questions),
}, },
room=sid, room=sid,
@@ -144,7 +142,7 @@ async def join_game(sid: str, data: dict):
await sio.emit("error", room=sid) await sio.emit("error", room=sid)
print(e) print(e)
return return
game_data = PlayGame.parse_raw(redis_res) game_data = PlayGame.model_validate_json(redis_res)
if game_data.started: if game_data.started:
await sio.emit("game_already_started", room=sid) await sio.emit("game_already_started", room=sid)
return return
@@ -161,7 +159,7 @@ async def join_game(sid: str, data: dict):
"secret": settings.hcaptcha_key, "secret": settings.hcaptcha_key,
}, },
) as resp: ) as resp:
resp_data = await resp.json() resp_data = await resp.model_dump_json()
if not resp_data["success"]: if not resp_data["success"]:
print("CAPTCHA FAILED") print("CAPTCHA FAILED")
return return
@@ -177,7 +175,7 @@ async def join_game(sid: str, data: dict):
}, },
) as resp: ) as resp:
try: try:
resp_data = await resp.json() resp_data = await resp.model_dump_json()
if not resp_data["success"]: if not resp_data["success"]:
print("CAPTCHA FAILED") print("CAPTCHA FAILED")
return return
@@ -201,17 +199,16 @@ async def join_game(sid: str, data: dict):
await sio.emit( await sio.emit(
"joined_game", "joined_game",
{ {
**json.loads(game_data.json(exclude={"quiz_id", "questions", "user_id"})), **json.loads(game_data.model_dump_json(exclude={"quiz_id", "questions", "user_id"})),
"question_count": len(game_data.questions), "question_count": len(game_data.questions),
}, },
room=sid, 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 = GameSession.parse_raw(redis_res) redis_res = GameSession.model_validate_json(redis_res)
await redis.set(f"game_session:{data.game_pin}:players:{data.username}", sid, ex=7200) await redis.set(f"game_session:{data.game_pin}:players:{data.username}", sid, ex=7200)
await redis.sadd( await redis.sadd(
f"game_session:{data.game_pin}:players", f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).model_dump_json()
GamePlayer(username=data.username, sid=sid).json(),
) )
if data.custom_field == "": if data.custom_field == "":
data.custom_field = None data.custom_field = None
@@ -222,11 +219,6 @@ async def join_game(sid: str, data: dict):
data.custom_field, data.custom_field,
) )
# await redis.set(
# f"game_session:{data.game_pin}",
# GameSession(admin=redis_res.admin, game_id=redis_res.game_id, answers=[]).json(),
# ex=18000,
# )
await sio.emit( await sio.emit(
"player_joined", "player_joined",
{"username": data.username, "sid": sid}, {"username": data.username, "sid": sid},
@@ -244,9 +236,9 @@ async def start_game(sid: str, _data: dict):
session = await get_session(sid, sio) session = await get_session(sid, sio)
if not session["admin"]: if not session["admin"]:
return return
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.model_validate_json(await redis.get(f"game:{session['game_pin']}"))
game_data.started = True game_data.started = True
await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) await redis.set(f"game:{session['game_pin']}", game_data.model_dump_json(), ex=7200)
await redis.delete(f"game_in_lobby:{game_data.user_id.hex}") await redis.delete(f"game_in_lobby:{game_data.user_id.hex}")
await sio.emit("start_game", room=session["game_pin"]) await sio.emit("start_game", room=session["game_pin"])
@@ -269,7 +261,7 @@ async def register_as_admin(sid: str, data: dict):
if (await redis.get(f"game_session:{game_pin}")) is None: if (await redis.get(f"game_session:{game_pin}")) is None:
await redis.set( await redis.set(
f"game_session:{game_pin}", f"game_session:{game_pin}",
GameSession(admin=sid, game_id=game_id, answers=[]).json(), GameSession(admin=sid, game_id=game_id, answers=[]).model_dump_json(),
ex=7200, ex=7200,
) )
@@ -299,10 +291,10 @@ async def get_question_results(sid: str, data: dict):
if redis_res is None: if redis_res is None:
redis_res = [] redis_res = []
else: else:
redis_res = AnswerDataList.parse_raw(redis_res).model_dump() redis_res = AnswerDataList.model_validate_json(redis_res).model_dump()
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.model_validate_json(await redis.get(f"game:{session['game_pin']}"))
game_data.question_show = False game_data.question_show = False
await redis.set(f"game:{session['game_pin']}", game_data.json()) await redis.set(f"game:{session['game_pin']}", game_data.model_dump_json())
game_pin = session["game_pin"] game_pin = session["game_pin"]
await sio.emit("question_results", redis_res, room=game_pin) await sio.emit("question_results", redis_res, room=game_pin)
@@ -322,14 +314,14 @@ class ReturnQuestion(QuizQuestion):
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution | list[VotingQuizAnswer] answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution | list[VotingQuizAnswer]
type: QuizQuestionType = QuizQuestionType.ABCD type: QuizQuestionType = QuizQuestionType.ABCD
@validator("answers") @field_validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, info: ValidationInfo):
if values["type"] == QuizQuestionType.ABCD and type(v[0]) is not ABCDQuizAnswerWithoutSolution: if info.data["type"] == QuizQuestionType.ABCD and type(v[0]) is not ABCDQuizAnswerWithoutSolution:
raise ValueError("Answers can't be none if type is ABCD") raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) is not RangeQuizAnswerWithoutSolution: if info.data["type"] == QuizQuestionType.RANGE and type(v) is not RangeQuizAnswerWithoutSolution:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE") raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
# skipcq: PTC-W0047 # skipcq: PTC-W0047
if values["type"] == QuizQuestionType.VOTING and type(v[0]) is not VotingQuizAnswer: if info.data["type"] == QuizQuestionType.VOTING and type(v[0]) is not VotingQuizAnswer:
pass pass
return v return v
@@ -341,12 +333,12 @@ async def set_question_number(sid, data: str):
if not session["admin"]: if not session["admin"]:
return return
game_pin = session["game_pin"] game_pin = session["game_pin"]
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.model_validate_json(await redis.get(f"game:{session['game_pin']}"))
game_data.current_question = int(float(data)) game_data.current_question = int(float(data))
game_data.question_show = True game_data.question_show = True
await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200) await redis.set(f"game:{session['game_pin']}", game_data.model_dump_json(), ex=7200)
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200) await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200)
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))] temp_return = game_data.model_dump(include={"questions"})["questions"][int(float(data))]
if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE: if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE:
await sio.emit( await sio.emit(
"set_question_number", "set_question_number",
@@ -366,7 +358,7 @@ async def set_question_number(sid, data: str):
"set_question_number", "set_question_number",
{ {
"question_index": int(float(data)), "question_index": int(float(data)),
"question": ReturnQuestion(**temp_return).dict(), "question": ReturnQuestion(**temp_return).model_dump(),
}, },
room=game_pin, room=game_pin,
) )
@@ -378,7 +370,7 @@ class _SubmitAnswerDataOrderType(BaseModel):
class _SubmitAnswerData(BaseModel): class _SubmitAnswerData(BaseModel):
question_index: int question_index: int
answer: str answer: str | int
complex_answer: list[_SubmitAnswerDataOrderType] | None = None complex_answer: list[_SubmitAnswerDataOrderType] | None = None
@@ -391,8 +383,9 @@ async def submit_answer(sid: str, data: dict):
await sio.emit("error", room=sid) await sio.emit("error", room=sid)
print(e) print(e)
return return
data.answer = str(data.answer)
session = await get_session(sid, sio) session = await get_session(sid, sio)
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.model_validate_json(await redis.get(f"game:{session['game_pin']}"))
answer_right = False answer_right = False
if game_data.questions[int(float(data.question_index))].type == QuizQuestionType.ABCD: if game_data.questions[int(float(data.question_index))].type == QuizQuestionType.ABCD:
for answer in game_data.questions[int(float(data.question_index))].answers: for answer in game_data.questions[int(float(data.question_index))].answers:
@@ -417,10 +410,10 @@ async def submit_answer(sid: str, data: dict):
for a in question.answers: for a in question.answers:
correct_answers.append({"answer": a.answer}) correct_answers.append({"answer": a.answer})
answer_order = [] answer_order = []
for a in data.dict()["complex_answer"]: for a in data.model_dump()["complex_answer"]:
answer_order.append(a["answer"]) answer_order.append(a["answer"])
data.answer = ", ".join(answer_order) data.answer = ", ".join(answer_order)
if correct_answers == data.dict()["complex_answer"]: if correct_answers == data.model_dump()["complex_answer"]:
answer_right = True answer_right = True
elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.TEXT: elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.TEXT:
@@ -471,15 +464,15 @@ async def submit_answer(sid: str, data: dict):
) )
player_count = await redis.scard(f"game_session:{session['game_pin']}:players") player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
await sio.emit("player_answer", {}) await sio.emit("player_answer", {})
if len(answers.root) == player_count: if len(answers) == player_count:
# await sio.emit( # await sio.emit(
# "question_results", # "question_results",
# await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"), # await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"),
# room=session["game_pin"], # room=session["game_pin"],
# ) # )
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}")) game_data = PlayGame.model_validate_json(await redis.get(f"game:{session['game_pin']}"))
game_data.question_show = False game_data.question_show = False
await redis.set(f"game:{session['game_pin']}", game_data.json()) await redis.set(f"game:{session['game_pin']}", game_data.model_dump_json())
await sio.emit("everyone_answered", {}) await sio.emit("everyone_answered", {})
@@ -514,11 +507,7 @@ async def show_solutions(sid: str, _data: dict):
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
if not session["admin"]: if not session["admin"]:
return return
await sio.emit( await sio.emit("solutions", game_data.questions[game_data.current_question].model_dump(), room=session["game_pin"])
"solutions",
game_data.questions[game_data.current_question].dict(),
room=session["game_pin"],
)
@sio.event @sio.event
@@ -552,7 +541,7 @@ async def kick_player(sid: str, data: dict):
player_sid = await redis.get(f"game_session:{session['game_pin']}:players:{data.username}") player_sid = await redis.get(f"game_session:{session['game_pin']}:players:{data.username}")
await redis.srem( await redis.srem(
f"game_session:{session['game_pin']}:players", f"game_session:{session['game_pin']}:players",
GamePlayer(username=data.username, sid=player_sid).json(), GamePlayer(username=data.username, sid=player_sid).model_dump_json(),
) )
await sio.leave_room(player_sid, session["game_pin"]) await sio.leave_room(player_sid, session["game_pin"])
await sio.emit("kick", room=player_sid) await sio.emit("kick", room=player_sid)
+2 -2
View File
@@ -13,7 +13,7 @@ from classquiz.db.models import PlayGame, GameResults
async def save_quiz_to_storage(game_pin: str): async def save_quiz_to_storage(game_pin: str):
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}")) game = PlayGame.model_validate_json(await redis.get(f"game:{game_pin}"))
player_count = await redis.scard(f"game_session:{game_pin}:players") player_count = await redis.scard(f"game_session:{game_pin}:players")
answers = [] answers = []
for i in range(len(game.questions)): for i in range(len(game.questions)):
@@ -26,7 +26,7 @@ async def save_quiz_to_storage(game_pin: str):
custom_field_data = await redis.hgetall(f"game:{game_pin}:players:custom_fields") custom_field_data = await redis.hgetall(f"game:{game_pin}:players:custom_fields")
q_return = [] q_return = []
for q in game.questions: for q in game.questions:
q_return.append(q.dict()) q_return.append(q.model_dump())
data = GameResults( data = GameResults(
id=game.game_id, id=game.game_id,
quiz=game.quiz_id, quiz=game.quiz_id,
+5 -2
View File
@@ -8,7 +8,7 @@ services:
# context: pg_uuidv7 # context: pg_uuidv7
# args: # args:
# PG_MAJOR_VERSION: 16 # PG_MAJOR_VERSION: 16
image: postgres image: postgres:16
environment: environment:
POSTGRES_PASSWORD: mysecretpassword POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: classquiz POSTGRES_DB: classquiz
@@ -45,7 +45,10 @@ services:
- 9001:9001 - 9001:9001
restart: unless-stopped restart: unless-stopped
redis: redis:
image: redis image: redis:alpine
restart: always
healthcheck:
test: [ "CMD", "redis-cli","ping" ]
ports: ports:
- 6379:6379 - 6379:6379
volumes: volumes:
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0