Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
36b1561
Trying to add a Youtube URL
JasonDavidI8C Sep 23, 2024
f917e08
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 25, 2024
a35939b
Problem with PR
Titiftw Sep 25, 2024
071ae08
Merge remote-tracking branch 'origin/master'
Titiftw Sep 25, 2024
c76d9e8
PR remarks
Titiftw Sep 25, 2024
f85e95a
Display YouTube when presenting, remove line as suggested by PR
Titiftw Sep 25, 2024
c240b83
Analysis issues
Titiftw Sep 25, 2024
ce31cab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 25, 2024
24aa0e7
Imports went away
Titiftw Sep 25, 2024
95732b1
Merge branch 'master' of https://github.com/Titiftw/ClassQuiz
Titiftw Sep 25, 2024
acf6015
Issue analysis
Titiftw Sep 25, 2024
70811df
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 25, 2024
aec1dce
Tried to simplify function, removed unused arg
Titiftw Sep 25, 2024
c1223c0
Merge branch 'master' into feature/fixes
Titiftw Sep 25, 2024
990e328
Merge branch 'feature/fixes'
Titiftw Sep 25, 2024
f2297a3
Global fixes
Titiftw Sep 29, 2024
99d2d22
Incorrect height while playing
Titiftw Oct 4, 2024
478e082
Issue CSS question
Titiftw Oct 11, 2024
06dbd54
Issue CSS
Titiftw Oct 11, 2024
cc3f1a2
Trying this change
Titiftw Oct 11, 2024
307bb0c
Issue on mobile
Titiftw Oct 11, 2024
e588779
Center
Titiftw Oct 11, 2024
cf74ae1
Issue image size
Titiftw Oct 11, 2024
5c9f2e6
Max height image 50%
Titiftw Oct 11, 2024
da83b9f
Max height image
Titiftw Oct 11, 2024
be128e1
Still image height issue
Titiftw Oct 11, 2024
6a5ef05
Issue image gheight
Titiftw Oct 11, 2024
9d0350c
Again
Titiftw Oct 11, 2024
4eab014
Added style as input attribute
Titiftw Oct 11, 2024
409ce53
Revert "Added style as input attribute"
Titiftw Oct 17, 2024
efad75d
Revert "Again"
Titiftw Oct 17, 2024
2c6168b
Revert "Issue image gheight"
Titiftw Oct 17, 2024
1557c17
Revert "Still image height issue"
Titiftw Oct 17, 2024
330cc1f
Revert "Max height image"
Titiftw Oct 17, 2024
c584923
Revert "Max height image 50%"
Titiftw Oct 17, 2024
58157b6
Revert "Issue image size"
Titiftw Oct 17, 2024
f570487
Revert "Center"
Titiftw Oct 17, 2024
3897222
Revert "Issue on mobile"
Titiftw Oct 17, 2024
082382a
Revert "Trying this change"
Titiftw Oct 17, 2024
511bcb9
Revert "Issue CSS"
Titiftw Oct 17, 2024
0d85586
Revert "Issue CSS question"
Titiftw Oct 17, 2024
97d84a2
Revert "Incorrect height while playing"
Titiftw Oct 17, 2024
5e9eb6f
Removed style before PR
Titiftw Oct 17, 2024
522f782
Missing absolute class
Titiftw Oct 17, 2024
5a11c99
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 17, 2024
41eaff3
Python issues
Titiftw Oct 17, 2024
38dc9cc
Merge branch 'feature/fixes-pr' of https://github.com/Titiftw/ClassQu…
Titiftw Oct 17, 2024
85d267c
Refactored verify answer
Titiftw Oct 17, 2024
2e98735
Using any for abcd answer verification
Titiftw Oct 17, 2024
76897f7
Merge branch 'master' of https://github.com/Titiftw/ClassQuiz
Titiftw Sep 29, 2025
c4cffe1
Merge branch 'master' into feature/fixes-pr
Titiftw Sep 29, 2025
61cf261
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ node_modules/
survey.json
.coverage
export_deta.py
.DS_Store
target/
3 changes: 3 additions & 0 deletions classquiz/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ class QuizQuestion(BaseModel):
answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[TextQuizAnswer] | list[VotingQuizAnswer] | str
image: str | None = None
hide_results: bool | None = False
youtube_url: str | None = None
music: str | None = None

@field_validator("answers")
def answers_not_none_if_abcd_type(cls, v, info: ValidationInfo):
Expand Down Expand Up @@ -277,6 +279,7 @@ class AnswerData(BaseModel):
right: bool
time_taken: float # In milliseconds
score: int
total_score: int | None


class AnswerDataList(RootModel):
Expand Down
9 changes: 9 additions & 0 deletions classquiz/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,12 @@ def extract_image_ids_from_quiz(quiz: Quiz) -> list[str | uuid.UUID]:
continue
quiz_images.append(question["image"])
return quiz_images


def extract_music_ids_from_quiz(quiz: Quiz) -> list[str | uuid.UUID]:
quiz_musics = []
for question in quiz.questions:
if question.get("music") is None:
continue
quiz_musics.append(question.get("music"))
return quiz_musics
3 changes: 2 additions & 1 deletion classquiz/routers/box_controller/embedded/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now:
if answer_right:
score = calculate_score(abs(diff), int(float(question.time)))
await redis.set(f"answer_given:{player_id}:{game.current_question}", "True", ex=600)
await redis.hincrby(f"game_session:{game_pin}:player_scores", username, score)
total_score = await redis.hincrby(f"game_session:{game_pin}:player_scores", username, score)
answer_data = AnswerData(
username=username,
answer=selected_answer,
right=answer_right,
time_taken=abs(diff),
score=score,
total_score=total_score,
)
answers = await redis.get(f"game_session:{game_pin}:{game.current_question}")
answers = await set_answer(answers, game_pin=game_pin, data=answer_data, q_index=game.current_question)
Expand Down
160 changes: 98 additions & 62 deletions classquiz/routers/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
from datetime import datetime
from uuid import UUID

from classquiz.helpers import get_meili_data, check_image_string, extract_image_ids_from_quiz
from classquiz.helpers import (
get_meili_data,
check_image_string,
extract_image_ids_from_quiz,
extract_music_ids_from_quiz,
)
from classquiz.storage.errors import DeletionFailedError

settings = settings()
Expand Down Expand Up @@ -67,17 +72,82 @@ async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = D
return InitEditorResponse(token=edit_id)


@router.post("/finish")
async def finish_edit(edit_id: str, quiz_input: QuizInput):
session_data = await redis.get(f"edit_session:{edit_id}")
if session_data is None:
raise HTTPException(status_code=401, detail="Edit ID not found!")
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.description = bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
if quiz_input.background_color is not None:
quiz_input.background_color = bleach.clean(quiz_input.background_color, tags=[], strip=True)
async def finish_edit_function(
old_quiz_data: Quiz,
edit_id: str,
quiz_input: QuizInput,
images_to_delete: list[str | uuid.UUID],
musics_to_delete: list[str | uuid.UUID],
):
await arq.enqueue_job("quiz_update", old_quiz_data, old_quiz_data.id, _defer_by=2)
quiz = old_quiz_data
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
if not quiz_input.public:
meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
else:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
quiz.title = quiz_input.title
quiz.public = quiz_input.public
quiz.description = quiz_input.description
quiz.updated_at = datetime.now()
quiz.questions = quiz_input.model_dump()["questions"]
quiz.cover_image = quiz_input.cover_image
quiz.background_color = quiz_input.background_color
quiz.background_image = quiz_input.background_image
quiz.mod_rating = None
for image in images_to_delete:
if image is not None:
try:
await storage.delete([image])
except DeletionFailedError:
pass
for music in musics_to_delete:
if music is not None:
try:
await storage.delete([music])
except DeletionFailedError:
pass
await redis.srem("edit_sessions", edit_id)
await redis.delete(f"edit_session:{edit_id}")
await redis.delete(f"edit_session:{edit_id}:images")
await quiz.update()
return quiz


async def finish_create_function(session_data: EditSessionData, edit_id: str, quiz_input: QuizInput):
quiz = Quiz(
**quiz_input.model_dump(),
user_id=session_data.user_id,
id=session_data.quiz_id,
created_at=datetime.now(),
updated_at=datetime.now(),
)

await redis.delete("global_quiz_count")
if quiz_input.public:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
try:
await redis.srem("edit_sessions", edit_id)
await redis.delete(f"edit_session:{edit_id}")
await redis.delete(f"edit_session:{edit_id}:images")
await quiz.save()
except asyncpg.exceptions.UniqueViolationError:
raise HTTPException(status_code=400, detail="The quiz already exists")
new_images = extract_image_ids_from_quiz(quiz)
new_musics = extract_music_ids_from_quiz(quiz)
for image in new_images:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(image))
if item is None:
continue
await quiz.storageitems.add(item)
for music in new_musics:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(music))
if item is None:
continue
await quiz.storageitems.add(item)


def cleanup_questions(quiz_input: QuizInput):
for i, question in enumerate(quiz_input.questions):
if question.type == QuizQuestionType.ABCD:
for i2, answer in enumerate(question.answers):
Expand All @@ -90,7 +160,22 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
answer.answer, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True
)


@router.post("/finish")
async def finish_edit(edit_id: str, quiz_input: QuizInput):
session_data = await redis.get(f"edit_session:{edit_id}")
if session_data is None:
raise HTTPException(status_code=401, detail="Edit ID not found!")
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.description = bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
if quiz_input.background_color is not None:
quiz_input.background_color = bleach.clean(quiz_input.background_color, tags=[], strip=True)

cleanup_questions(quiz_input)

images_to_delete = []
musics_to_delete = []
old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)

for i, question in enumerate(quiz_input.questions):
Expand All @@ -113,55 +198,6 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
raise HTTPException(status_code=400, detail="image url is not valid")

if session_data.edit:
await arq.enqueue_job("quiz_update", old_quiz_data, old_quiz_data.id, _defer_by=2)
quiz = old_quiz_data
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
if not quiz_input.public:
meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
else:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
quiz.title = quiz_input.title
quiz.public = quiz_input.public
quiz.description = quiz_input.description
quiz.updated_at = datetime.now()
quiz.questions = quiz_input.model_dump()["questions"]
quiz.cover_image = quiz_input.cover_image
quiz.background_color = quiz_input.background_color
quiz.background_image = quiz_input.background_image
quiz.mod_rating = None
for image in images_to_delete:
if image is not None:
try:
await storage.delete([image])
except DeletionFailedError:
pass
await redis.srem("edit_sessions", edit_id)
await redis.delete(f"edit_session:{edit_id}")
await redis.delete(f"edit_session:{edit_id}:images")
await quiz.update()
return quiz
return await finish_edit_function(old_quiz_data, edit_id, quiz_input, images_to_delete, musics_to_delete)
else:
quiz = Quiz(
**quiz_input.model_dump(),
user_id=session_data.user_id,
id=session_data.quiz_id,
created_at=datetime.now(),
updated_at=datetime.now(),
)

await redis.delete("global_quiz_count")
if quiz_input.public:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
try:
await redis.srem("edit_sessions", edit_id)
await redis.delete(f"edit_session:{edit_id}")
await redis.delete(f"edit_session:{edit_id}:images")
await quiz.save()
except asyncpg.exceptions.UniqueViolationError:
raise HTTPException(status_code=400, detail="The quiz already exists")
new_images = extract_image_ids_from_quiz(quiz)
for image in new_images:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(image))
if item is None:
continue
await quiz.storageitems.add(item)
await finish_create_function(session_data, edit_id, quiz_input)
43 changes: 40 additions & 3 deletions classquiz/routers/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ async def get_basic_file_info(file_name: str) -> Response:
item = await StorageItem.objects.get_or_none(id=checked_image_string[1])
if item is None:
raise HTTPException(status_code=404, detail="File not found")
# return PublicStorageItem.from_db_model(item)
storage_file_name = item.storage_path
if storage_file_name is None:
storage_file_name = item.id.hex
Expand All @@ -103,7 +102,6 @@ async def download_file_head(file_name: str) -> Response:
item = await StorageItem.objects.get_or_none(id=checked_image_string[1])
if item is None:
raise HTTPException(status_code=404, detail="File not found")
# return PublicStorageItem.from_db_model(item)
storage_file_name = item.storage_path
if storage_file_name is None:
storage_file_name = item.id.hex
Expand Down Expand Up @@ -151,8 +149,10 @@ async def upload_raw_file(request: Request, user: User = Depends(get_current_use
if user.storage_used > settings.free_storage_limit:
raise HTTPException(status_code=409, detail="Storage limit reached")
file_id = uuid4()
body_len = 0
data_file = SpooledTemporaryFile(max_size=1000)
async for chunk in request.stream():
body_len += len(chunk)
data_file.write(chunk)
data_file.seek(0)
file_obj = StorageItem(
Expand All @@ -161,7 +161,43 @@ async def upload_raw_file(request: Request, user: User = Depends(get_current_use
mime_type=request.headers.get("Content-Type"),
hash=None,
user=user,
size=0,
size=body_len,
deleted_at=None,
alt_text=None,
)
# https://github.com/VirusTotal/vt-py/issues/119#issuecomment-1261246867
await storage.upload(
file_name=file_id.hex,
# skipcq: PYL-W0212
file_data=data_file._file,
mime_type=request.headers.get("Content-Type"),
)
await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex)
return PublicStorageItem.from_db_model(file_obj)


@router.post("/raw/{filename}")
async def upload_raw_file_with_filename(
filename: str, request: Request, user: User = Depends(get_current_user)
) -> PublicStorageItem:
if user.storage_used > settings.free_storage_limit:
raise HTTPException(status_code=409, detail="Storage limit reached")
file_id = uuid4()
body_len = 0
data_file = SpooledTemporaryFile(max_size=1000)
async for chunk in request.stream():
body_len += len(chunk)
data_file.write(chunk)
data_file.seek(0)
file_obj = StorageItem(
id=file_id,
uploaded_at=datetime.now(),
mime_type=request.headers.get("Content-Type"),
hash=None,
user=user,
size=body_len,
filename=filename,
deleted_at=None,
alt_text=None,
)
Expand Down Expand Up @@ -243,6 +279,7 @@ async def get_latest_images(count: int = 50, user: User = Depends(get_current_us
count = min(count, 50)
items = (
await StorageItem.objects.filter(user=user)
.filter(StorageItem.deleted_at == None) # noqa: E711
.limit(count)
.select_related([StorageItem.quizzes, StorageItem.quiztivities])
.order_by(StorageItem.uploaded_at.desc())
Expand Down
Loading
Loading