Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YouTube + Music #401

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,3 +12,4 @@ node_modules/
survey.json
.coverage
export_deta.py
.DS_Store
2 changes: 2 additions & 0 deletions classquiz/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ class QuizQuestion(BaseModel):
type: None | QuizQuestionType = QuizQuestionType.ABCD
answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[TextQuizAnswer] | list[VotingQuizAnswer] | str
image: str | None = None
youtube_url: str | None = None
music: str | None = None

@validator("answers")
def answers_not_none_if_abcd_type(cls, v, values):
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
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 @@ -65,17 +70,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.parse_raw(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.dict()["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.dict(),
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 @@ -88,7 +158,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.parse_raw(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 @@ -111,55 +196,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.dict()["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
Comment on lines -130 to -135
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see "images_to_delete" being populated. Am I wrong? It seems it's always set to []

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should find the images that were in the quiz before it was saved and deletes them, but I'll check that

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.dict(),
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
69 changes: 54 additions & 15 deletions classquiz/worker/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tempfile import SpooledTemporaryFile

from classquiz.db.models import StorageItem, Quiz, User
from classquiz.helpers import extract_image_ids_from_quiz
from classquiz.helpers import extract_image_ids_from_quiz, extract_music_ids_from_quiz
from classquiz.storage.errors import DeletionFailedError
from thumbhash import image_to_thumbhash

Expand Down Expand Up @@ -42,6 +42,8 @@ async def calculate_hash(ctx, file_id_as_str: str):
if file_data.storage_path is not None:
file_path = file_data.storage_path
file = SpooledTemporaryFile()
# The next line is giving error as the file is not found if
# the same volume is not mounted to the worker instance
file_data.size = await storage.get_file_size(file_name=file_path)
if file_data.size is None:
file_data.size = 0
Expand Down Expand Up @@ -73,19 +75,13 @@ async def calculate_hash(ctx, file_id_as_str: str):
await user.update()


# skipcq: PYL-W0613
async def quiz_update(ctx, old_quiz: Quiz, quiz_id: uuid.UUID):
new_quiz: Quiz = await Quiz.objects.get(id=quiz_id)
old_images = extract_image_ids_from_quiz(old_quiz)
new_images = extract_image_ids_from_quiz(new_quiz)

# If images are identical, then return
if sorted(old_images) == sorted(new_images):
print("Nothing's changed")
return
print("Change detected")
removed_images = list(set(old_images) - set(new_images))
added_images = list(set(new_images) - set(old_images))
async def manage_resources(
removed_images: list[str | uuid.UUID],
removed_musics: list[str | uuid.UUID],
added_images: list[str | uuid.UUID],
added_musics: list[str | uuid.UUID],
new_quiz: Quiz,
):
change_made = False
for image in removed_images:
if "--" in image:
Expand All @@ -96,15 +92,58 @@ async def quiz_update(ctx, old_quiz: Quiz, quiz_id: uuid.UUID):
continue
try:
await new_quiz.storageitems.remove(item)
except ormar.exceptions.NoMatch:
except ormar.exceptions.NoMatch as e:
print(e)
continue
change_made = True
for music in removed_musics:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(music))
if item is None:
continue
try:
await new_quiz.storageitems.remove(item)
except ormar.exceptions.NoMatch as e:
print(e)
continue
change_made = True
for image in added_images:
if "--" not in image:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(image))
if item is None:
continue
await new_quiz.storageitems.add(item)
change_made = True
for music in added_musics:
if "--" not in music:
item = await StorageItem.objects.get_or_none(id=uuid.UUID(music))
if item is None:
continue
await new_quiz.storageitems.add(item)
change_made = True

return change_made


# skipcq: PYL-W0613
async def quiz_update(ctx, old_quiz: Quiz, quiz_id: uuid.UUID):
new_quiz: Quiz = await Quiz.objects.get(id=quiz_id)
old_images = extract_image_ids_from_quiz(old_quiz)
new_images = extract_image_ids_from_quiz(new_quiz)
old_musics = extract_music_ids_from_quiz(old_quiz)
new_musics = extract_music_ids_from_quiz(new_quiz)

# If images are identical, then return
if sorted(old_images) == sorted(new_images) and sorted(old_musics) == sorted(new_musics):
print("Nothing's changed")
return
print("Change detected")

removed_images = list(set(old_images) - set(new_images))
removed_musics = list(set(old_musics) - set(new_musics))
added_images = list(set(new_images) - set(old_images))
added_musics = list(set(new_musics) - set(old_musics))

change_made = await manage_resources(removed_images, removed_musics, added_images, added_musics, new_quiz)

if change_made:
await new_quiz.update()
Loading