🔀 Merged ClassQuizController
This commit is contained in:
@@ -80,6 +80,7 @@ async def get_customized_avatar(
|
||||
clothe_color=clothe_color,
|
||||
clothe_graphic_type=clothe_graphic_type,
|
||||
).render_svg()
|
||||
# skipcq: PY-W0069
|
||||
# print(f"skin_color: {len(AvatarItemsAsList.skin_color)},")
|
||||
# print(f"hair_color: {len(AvatarItemsAsList.hair_color)},")
|
||||
# print(f"facial_hair_type: {len(AvatarItemsAsList.facial_hair_type)},")
|
||||
|
||||
@@ -57,12 +57,12 @@ async def submit_answer_fn(data_answer: int, game_pin: str, player_id: str, now:
|
||||
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)
|
||||
player_count = await redis.scard(f"game_session:{game_pin}:players")
|
||||
print(player_count, answers)
|
||||
await sio.emit("player_answer", {})
|
||||
if answers is not None and len(answers.__root__) == player_count:
|
||||
await sio.emit("everyone_answered", {})
|
||||
|
||||
|
||||
button_to_index_map = {"y": 0, "r": 2, "g": 1, "b": 3}
|
||||
button_to_index_map = {"y": 0, "r": 3, "g": 1, "b": 2}
|
||||
|
||||
|
||||
class WebSocketTypes(enum.Enum):
|
||||
@@ -81,9 +81,9 @@ wss_clients = {}
|
||||
@router.websocket("/{game_id}")
|
||||
async def websocket_endpoint(ws: WebSocket, game_id: str):
|
||||
try:
|
||||
if game_id in wss_clients.keys():
|
||||
if game_id in wss_clients:
|
||||
await ws.close(code=status.WS_1001_GOING_AWAY)
|
||||
print("Client {} already exists.".format(game_id))
|
||||
print(f"Client {game_id} already exists.")
|
||||
return
|
||||
await ws.accept()
|
||||
wss_clients[game_id] = ws
|
||||
@@ -119,7 +119,8 @@ async def websocket_endpoint(ws: WebSocket, game_id: str):
|
||||
await ws.send_text(WebSocketRequest(type=WebSocketTypes.Error, data="InvalidButton").json())
|
||||
continue
|
||||
await submit_answer_fn(answer_index, game_pin, player_id, now)
|
||||
print(f"Data from client {game_id}: {data}")
|
||||
|
||||
except WebSocketDisconnect as ex:
|
||||
print("Client {} is disconnected: {}".format(game_id, ex))
|
||||
print(f"Client {game_id} is disconnected: {ex}")
|
||||
wss_clients.pop(game_id, None)
|
||||
|
||||
+20
-88
@@ -4,24 +4,22 @@
|
||||
|
||||
import asyncio
|
||||
import html
|
||||
import re
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import asyncpg.exceptions
|
||||
import bleach
|
||||
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ, server_regex
|
||||
from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType
|
||||
import puremagic
|
||||
from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ, arq
|
||||
from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType, StorageItem
|
||||
from classquiz.auth import get_current_user
|
||||
import os
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from classquiz.helpers import get_meili_data, check_hashcash
|
||||
from classquiz.helpers import get_meili_data, check_image_string, extract_image_ids_from_quiz
|
||||
from classquiz.storage.errors import DeletionFailedError
|
||||
|
||||
settings = settings()
|
||||
@@ -67,56 +65,6 @@ async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = D
|
||||
return InitEditorResponse(token=edit_id)
|
||||
|
||||
|
||||
class GetPowData(BaseModel):
|
||||
data: str
|
||||
|
||||
|
||||
@router.get("/pow", response_model=GetPowData)
|
||||
async def get_pow_data(edit_id: str):
|
||||
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!")
|
||||
random_str = os.urandom(8).hex()
|
||||
await redis.set(f"edit_session:{edit_id}:pow", random_str, ex=3800)
|
||||
return GetPowData(data=random_str)
|
||||
|
||||
|
||||
class UploadImageReturn(BaseModel):
|
||||
id: str
|
||||
pow_data: str
|
||||
|
||||
|
||||
@router.post("/image", response_model=UploadImageReturn)
|
||||
async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()):
|
||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||
pow_data_server = await redis.get(f"edit_session:{edit_id}:pow")
|
||||
uploaded_images = await redis.llen(f"edit_session:{edit_id}:images")
|
||||
if pow_data_server is None:
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
if session_data is None:
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
if uploaded_images == 0 and not check_hashcash(pow_data, pow_data_server, "8"):
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
if uploaded_images != 0 and not check_hashcash(pow_data, pow_data_server, "8"):
|
||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||
file_bytes = await file.read()
|
||||
if len(file_bytes) > 2000000:
|
||||
raise HTTPException(status_code=400, detail="File too large")
|
||||
try:
|
||||
pm_data = puremagic.magic_string(file_bytes)[0]
|
||||
except puremagic.PureError:
|
||||
raise HTTPException(status_code=400, detail="Image couldn't be identified!")
|
||||
if pm_data.extension not in allowed_image_extensions:
|
||||
raise HTTPException(status_code=400, detail="Image-type now allowed!")
|
||||
session_data = EditSessionData.parse_raw(session_data)
|
||||
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
||||
await storage.upload(file_name=file_name, file_data=file_bytes)
|
||||
await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
||||
random_str = os.urandom(8).hex()
|
||||
await redis.set(f"edit_session:{edit_id}:pow", random_str, ex=3800)
|
||||
return UploadImageReturn(id=file_name, pow_data=random_str)
|
||||
|
||||
|
||||
@router.post("/finish")
|
||||
async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||
@@ -141,25 +89,10 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
quiz_input.questions[i].answers[i2].answer = html.unescape(
|
||||
bleach.clean(answer.answer, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
|
||||
)
|
||||
image_id_regex = r"^.{36}--.{36}$"
|
||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||
|
||||
extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$"
|
||||
images_to_delete = []
|
||||
old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
|
||||
|
||||
def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None):
|
||||
if old_quiz is None:
|
||||
return
|
||||
try:
|
||||
# Why does this work or not throw an error (TODO)
|
||||
if new == old_quiz.questions[index]["image"]:
|
||||
return
|
||||
else:
|
||||
images_to_delete.append(old_quiz.questions[index]["image"])
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
for i, question in enumerate(quiz_input.questions):
|
||||
image = question.image
|
||||
quiz_input.questions[i].question = html.unescape(
|
||||
@@ -167,29 +100,20 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
)
|
||||
if image == "":
|
||||
question.image = None
|
||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||
elif image is None:
|
||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||
elif bool(re.match(image_id_regex, question.image)):
|
||||
question.image = f"{settings.root_address}/api/v1/storage/download/{image}"
|
||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||
elif bool(re.match(imgur_regex, image)):
|
||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||
elif bool(re.match(server_regex, image)):
|
||||
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||
else:
|
||||
if image is not None and not check_image_string(image)[0]:
|
||||
raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!")
|
||||
|
||||
if quiz_input.cover_image == "":
|
||||
quiz_input.cover_image = None
|
||||
|
||||
# if quiz_input.background_image is None and old_quiz_data.background_image is not None:
|
||||
# mark_image_for_deletion(quiz_input.background_image)
|
||||
if quiz_input.cover_image is not None and not check_image_string(quiz_input.cover_image)[0]:
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
|
||||
if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)):
|
||||
if quiz_input.background_image is not None and not check_image_string(quiz_input.background_image)[0]:
|
||||
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:
|
||||
@@ -207,13 +131,14 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
for image in images_to_delete:
|
||||
if image is not None:
|
||||
try:
|
||||
await storage.delete([re.search(extract_file_name_re, image).group(1)])
|
||||
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")
|
||||
return await quiz.update()
|
||||
await quiz.update()
|
||||
return quiz
|
||||
else:
|
||||
quiz = Quiz(
|
||||
**quiz_input.dict(),
|
||||
@@ -222,6 +147,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
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)])
|
||||
@@ -229,6 +155,12 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||
await redis.srem("edit_sessions", edit_id)
|
||||
await redis.delete(f"edit_session:{edit_id}")
|
||||
await redis.delete(f"edit_session:{edit_id}:images")
|
||||
return await quiz.save()
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
import io
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
@@ -10,10 +11,11 @@ from aiohttp import ClientSession
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.config import storage, settings
|
||||
from classquiz.db.models import Quiz, User
|
||||
from classquiz.config import storage, settings, arq
|
||||
from classquiz.db.models import Quiz, User, StorageItem
|
||||
import gzip
|
||||
import urllib.parse
|
||||
import magic
|
||||
|
||||
router = APIRouter()
|
||||
settings = settings()
|
||||
@@ -44,12 +46,13 @@ async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)
|
||||
quiz_dict["updated_at"] = quiz_dict["updated_at"].isoformat()
|
||||
quiz_json = json.dumps(quiz_dict)
|
||||
bin_data = gzip.compress(quiz_json.encode("utf-8"), compresslevel=9)
|
||||
# bin_data = quiz_json.encode("utf-8")
|
||||
bin_data = bin_data + quiz_delimiter
|
||||
for image_key in image_urls.keys():
|
||||
for image_key in image_urls:
|
||||
bin_data = bin_data + image_delimiter + str(image_key).encode("utf-8") + image_index_delimiter
|
||||
image_data = None
|
||||
async with ClientSession() as session, session.get(image_urls[image_key]) as resp:
|
||||
async with ClientSession() as session, session.get(
|
||||
f"{settings.root_address}/api/v1/storage/download/{image_urls[image_key]}"
|
||||
) as resp:
|
||||
image_data = await resp.read()
|
||||
bin_data = bin_data + image_data
|
||||
|
||||
@@ -68,6 +71,8 @@ async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)
|
||||
|
||||
@router.post("/")
|
||||
async def import_quiz(file: UploadFile = File(), user: User = Depends(get_current_user)):
|
||||
if user.storage_used > settings.free_storage_limit:
|
||||
raise HTTPException(status_code=409, detail="Storage limit reached")
|
||||
data = await file.read()
|
||||
[split_data, images] = data.split(quiz_delimiter)
|
||||
decompressed_quiz = gzip.decompress(split_data)
|
||||
@@ -75,15 +80,32 @@ async def import_quiz(file: UploadFile = File(), user: User = Depends(get_curren
|
||||
image_splits = images.split(image_delimiter)
|
||||
quiz_id = uuid.uuid4()
|
||||
image_urls = {}
|
||||
print(len(data))
|
||||
for image_split in image_splits:
|
||||
res = image_split.split(image_index_delimiter)
|
||||
if len(res) != 2:
|
||||
continue
|
||||
[index, image_data] = res
|
||||
print(len(image_data))
|
||||
img_data = io.BytesIO(image_data)
|
||||
mime_type = magic.from_buffer(img_data.read(2048), mime=True)
|
||||
print(mime_type)
|
||||
index = int(index.decode("utf-8"))
|
||||
image_name = f"{quiz_id}--{uuid.uuid4()}"
|
||||
await storage.upload(file_name=image_name, file_data=image_data)
|
||||
image = f"{settings.root_address}/api/v1/storage/download/{image_name}"
|
||||
file_id = uuid.uuid4()
|
||||
file_obj = StorageItem(
|
||||
id=file_id,
|
||||
uploaded_at=datetime.now(),
|
||||
mime_type=mime_type,
|
||||
hash=None,
|
||||
user=user,
|
||||
size=0,
|
||||
deleted_at=None,
|
||||
alt_text=None,
|
||||
)
|
||||
await storage.upload(file_name=file_id.hex, file_data=img_data, mime_type=mime_type)
|
||||
await file_obj.save()
|
||||
await arq.enqueue_job("calculate_hash", file_id.hex)
|
||||
image = file_id.hex
|
||||
image_urls[index] = image
|
||||
quiz_dict["created_at"] = datetime.fromisoformat(quiz_dict["created_at"])
|
||||
quiz_dict["updated_at"] = datetime.fromisoformat(quiz_dict["updated_at"])
|
||||
|
||||
@@ -134,8 +134,6 @@ async def get_live_game_data(
|
||||
|
||||
@router.get("/user_count")
|
||||
async def get_game_user_count(game_pin: str, api_key: str, as_string: bool = False, as_array: bool = False):
|
||||
# if redis_res is None:
|
||||
# raise HTTPException(status_code=404, detail="Game not found")
|
||||
user_id = await check_api_key(api_key)
|
||||
redis_res = await redis.get(f"game_session:{game_pin}")
|
||||
if redis_res is None:
|
||||
@@ -149,12 +147,6 @@ async def get_game_user_count(game_pin: str, api_key: str, as_string: bool = Fal
|
||||
return {"players": {"count": player_count}}
|
||||
|
||||
|
||||
# class _LivePlayersReturn(BaseModel):
|
||||
# # players: list[GamePlayer | None]
|
||||
# answers: list[GameAnswer1 | None]
|
||||
# players: list[GamePlayer | None]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/players",
|
||||
)
|
||||
|
||||
@@ -149,7 +149,7 @@ class StepInput(BaseModel):
|
||||
|
||||
|
||||
@router.post("/step/{step_id}")
|
||||
async def step_1(session_id: str, data: StepInput, request: Request, response: Response, step_id: int):
|
||||
async def step_1_endpoint(session_id: str, data: StepInput, request: Request, response: Response, step_id: int):
|
||||
if step_id < 0 or step_id > 2:
|
||||
raise HTTPException(status_code=401)
|
||||
redis_res = await redis.get(f"login_session:{session_id}")
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.db.models import User, StorageItem, PublicStorageItem
|
||||
from classquiz.helpers.pixabay import get_images, GetImagesParams, BoolInput, GetImagesResponse, NotFoundError
|
||||
from classquiz.config import settings, storage, arq
|
||||
|
||||
settings = settings()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/images")
|
||||
async def search_pixabay_images(query: str, page: int = 1, user: User = Depends(get_current_user)) -> GetImagesResponse:
|
||||
if settings.pixabay_api_key is None:
|
||||
raise HTTPException(status_code=423, detail="Pixabay not set up")
|
||||
return await get_images(settings.pixabay_api_key, GetImagesParams(q=query, safesearch=BoolInput.true, page=page))
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
async def save_pixabay_image(id: str, user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||
if settings.pixabay_api_key is None:
|
||||
raise HTTPException(status_code=423, detail="Pixabay not set up")
|
||||
if user.storage_used > settings.free_storage_limit:
|
||||
raise HTTPException(status_code=409, detail="Storage limit reached")
|
||||
try:
|
||||
images = await get_images(settings.pixabay_api_key, GetImagesParams(id=id, safesearch=BoolInput.true))
|
||||
except NotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Pixabay file not found")
|
||||
image = images.hits[0]
|
||||
file_id = uuid4()
|
||||
file_data = b""
|
||||
async with ClientSession() as session, session.get(image.largeImageURL) as resp:
|
||||
async for i in resp.content.iter_chunked(1024):
|
||||
file_data += i
|
||||
content_type = resp.headers.get("Content-Type")
|
||||
|
||||
if content_type is None:
|
||||
content_type = "image/*"
|
||||
file = BytesIO(file_data)
|
||||
await storage.upload(file_name=file_id.hex, file_data=file, mime_type=content_type)
|
||||
file_obj: StorageItem = StorageItem(
|
||||
id=file_id,
|
||||
uploaded_at=datetime.now(),
|
||||
mime_type=content_type,
|
||||
hash=None,
|
||||
user=user,
|
||||
size=0,
|
||||
deleted_at=None,
|
||||
alt_text=None,
|
||||
imported=True,
|
||||
)
|
||||
await file_obj.save()
|
||||
await arq.enqueue_job("calculate_hash", file_id.hex)
|
||||
return PublicStorageItem.from_db_model(file_obj)
|
||||
@@ -10,18 +10,16 @@ from random import randint
|
||||
|
||||
import ormar.exceptions
|
||||
|
||||
from classquiz.helpers import get_meili_data, generate_spreadsheet
|
||||
from classquiz.helpers import generate_spreadsheet
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import ValidationError, BaseModel
|
||||
import bleach
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.config import redis, settings, storage, meilisearch
|
||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby, QuizQuestion
|
||||
from classquiz.db.models import Quiz, User, PlayGame, GameInLobby, QuizQuestion
|
||||
from classquiz.helpers.box_controller import generate_code
|
||||
from classquiz.kahoot_importer.import_quiz import import_quiz
|
||||
import html
|
||||
import urllib.parse
|
||||
|
||||
settings = settings()
|
||||
@@ -29,33 +27,6 @@ settings = settings()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/create", deprecated=True)
|
||||
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{36}--.{36}$"
|
||||
quiz_input.title = html.unescape(bleach.clean(quiz_input.title, tags=[], strip=True))
|
||||
quiz_input.description = html.unescape(bleach.clean(quiz_input.description, tags=[], strip=True))
|
||||
for question in quiz_input.questions:
|
||||
if question.image == "":
|
||||
question.image = None
|
||||
if (
|
||||
question.image is not None
|
||||
and not re.match(imgur_regex, question.image)
|
||||
and not re.match(server_regex, question.image)
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
if quiz_input.cover_image == "":
|
||||
quiz_input.cover_image = None
|
||||
|
||||
if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)):
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
|
||||
await redis.delete("global_quiz_count")
|
||||
if quiz_input.public:
|
||||
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
|
||||
return await quiz.save()
|
||||
|
||||
|
||||
@router.get("/get/{quiz_id}")
|
||||
async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user)):
|
||||
try:
|
||||
@@ -195,57 +166,14 @@ async def get_quiz_list(user: User = Depends(get_current_user), page_size: int |
|
||||
raise HTTPException(status_code=400, detail="Invalid page(size). page(size) have to be greater than 0.")
|
||||
|
||||
|
||||
@router.put("/update/{quiz_id}", deprecated=True)
|
||||
async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
||||
for question in quiz_input.questions:
|
||||
if question.image == "":
|
||||
question.image = None
|
||||
if (
|
||||
question.image is not None
|
||||
and not bool(re.match(server_regex, question.image))
|
||||
and not bool(re.match(imgur_regex, question.image))
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
try:
|
||||
quiz_id = uuid.UUID(quiz_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="badly formed quiz id")
|
||||
# Check Cover-Image
|
||||
|
||||
if quiz_input.cover_image == "":
|
||||
quiz_input.cover_image = None
|
||||
|
||||
if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)):
|
||||
raise HTTPException(status_code=400, detail="image url is not valid")
|
||||
|
||||
quiz = await Quiz.objects.get_or_none(id=quiz_id, user_id=user.id)
|
||||
if quiz is None:
|
||||
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
|
||||
else:
|
||||
quiz_input.description = html.unescape(bleach.clean(quiz_input.description, tags=[], strip=True))
|
||||
quiz_input.title = html.unescape(bleach.clean(quiz_input.title, tags=[], strip=True))
|
||||
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
|
||||
if quiz.public and not quiz_input.public:
|
||||
meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
|
||||
if not quiz.public and quiz_input.public:
|
||||
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
|
||||
quiz.title = quiz_input.title
|
||||
quiz.cover_image = quiz_input.cover_image
|
||||
quiz.public = quiz_input.public
|
||||
quiz.description = quiz_input.description
|
||||
quiz.updated_at = datetime.now()
|
||||
quiz.questions = quiz_input.dict()["questions"]
|
||||
|
||||
return await quiz.update()
|
||||
|
||||
|
||||
@router.post("/import/{quiz_id}")
|
||||
async def import_quiz_route(quiz_id: str, user: User = Depends(get_current_user)):
|
||||
if user.storage_used > settings.free_storage_limit:
|
||||
raise HTTPException(status_code=409, detail="Storage limit reached")
|
||||
try:
|
||||
return await import_quiz(quiz_id, user)
|
||||
except ValidationError:
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=400, detail="This quiz isn't (yet) supported")
|
||||
|
||||
|
||||
@@ -264,7 +192,9 @@ async def delete_quiz(quiz_id: str, user: User = Depends(get_current_user)):
|
||||
for question in quiz.questions:
|
||||
try:
|
||||
if question["image"] is not None and not str(question["image"]).startswith("https://i.imgur.com/"):
|
||||
pics_to_delete.append(pic_name_regex.match(question["image"]).group(1))
|
||||
old_image_to_delete = pic_name_regex.match(question["image"])
|
||||
if old_image_to_delete is not None:
|
||||
pics_to_delete.append(old_image_to_delete.group(1))
|
||||
except KeyError:
|
||||
pass
|
||||
if len(pics_to_delete) != 0:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from datetime import datetime
|
||||
from classquiz.db.models import User, QuizTivityInput, QuizTivity, QuizTivityShare, PublicQuizTivityShare
|
||||
from classquiz.routers.quiztivity.shares import router as shares_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(shares_router, prefix="/shares")
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
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()})
|
||||
return await quiztivity.save()
|
||||
|
||||
|
||||
@router.get("/{uuid}")
|
||||
async def get_quiztivity(uuid: UUID) -> QuizTivity:
|
||||
quiztivity = await QuizTivity.objects.get_or_none(id=uuid)
|
||||
if quiztivity is None:
|
||||
raise HTTPException(status_code=404, detail="QuizTivity not found")
|
||||
return quiztivity
|
||||
|
||||
|
||||
@router.put("/{uuid}")
|
||||
async def put_quiztivity(data: QuizTivityInput, uuid: UUID, user: User = Depends(get_current_user)) -> QuizTivity:
|
||||
quiztivity = await QuizTivity.objects.get_or_none(id=uuid, user=user)
|
||||
if quiztivity is None:
|
||||
raise HTTPException(status_code=404, detail="QuizTivity not found")
|
||||
quiztivity.pages = data.dict()["pages"]
|
||||
quiztivity.title = data.title
|
||||
return await quiztivity.update()
|
||||
|
||||
|
||||
@router.delete("/{uuid}")
|
||||
async def delete_quiztivity(uuid: UUID):
|
||||
quiztivity = await QuizTivity.objects.get_or_none(id=uuid)
|
||||
if quiztivity is None:
|
||||
raise HTTPException(status_code=404, detail="QuizTivity not found")
|
||||
await quiztivity.delete()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_quiztivities(user: User = Depends(get_current_user)) -> list[QuizTivity]:
|
||||
quiztivities = await QuizTivity.objects.filter(user=user).order_by(QuizTivity.created_at.desc()).all()
|
||||
return quiztivities
|
||||
|
||||
|
||||
@router.get("/{uuid}/shares")
|
||||
async def get_shares(uuid: UUID, user: User = Depends(get_current_user)) -> list[PublicQuizTivityShare]:
|
||||
shares = (
|
||||
await QuizTivityShare.objects.filter(quiztivity=uuid, user=user).order_by(QuizTivityShare.expire_at.asc()).all()
|
||||
)
|
||||
resp_shares = []
|
||||
for share in shares:
|
||||
resp_shares.append(PublicQuizTivityShare.from_db_model(share))
|
||||
return resp_shares
|
||||
@@ -0,0 +1,84 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.db.models import User, QuizTivityShare, QuizTivity, PublicQuizTivityShare
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_shares(user: User = Depends(get_current_user)) -> list[PublicQuizTivityShare]:
|
||||
shares = await QuizTivityShare.objects.filter(user=user).all()
|
||||
resp_shares = []
|
||||
for share in shares:
|
||||
resp_shares.append(PublicQuizTivityShare.from_db_model(share))
|
||||
return resp_shares
|
||||
|
||||
|
||||
class CreateShareInput(BaseModel):
|
||||
name: str | None
|
||||
quiztivity: UUID
|
||||
expire_in: int | None
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_share(data: CreateShareInput, user: User = Depends(get_current_user)) -> PublicQuizTivityShare:
|
||||
quiztivity = await QuizTivity.objects.get_or_none(id=data.quiztivity, user=user)
|
||||
expire_at = None
|
||||
if data.expire_in is not None:
|
||||
expire_at = (datetime.now() + timedelta(minutes=data.expire_in)).replace(tzinfo=None)
|
||||
if quiztivity is None:
|
||||
raise HTTPException(status_code=400, detail="Quiztivity wasn't found")
|
||||
share = await QuizTivityShare.objects.create(
|
||||
id=uuid4(), name=data.name, expire_at=expire_at, quiztivity=quiztivity, user=user
|
||||
)
|
||||
share = PublicQuizTivityShare.from_db_model(share)
|
||||
return share
|
||||
|
||||
|
||||
@router.delete("/{uuid}")
|
||||
async def delete_share(uuid: UUID, user: User = Depends(get_current_user)):
|
||||
share = await QuizTivityShare.objects.get_or_none(id=uuid, user=user)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share wasn't found")
|
||||
await share.delete()
|
||||
return
|
||||
|
||||
|
||||
class UpdateShareInput(BaseModel):
|
||||
name: str | None
|
||||
expire_in: int | None
|
||||
|
||||
|
||||
@router.put("/{uuid}")
|
||||
async def update_share(
|
||||
data: UpdateShareInput, uuid: UUID, user: User = Depends(get_current_user)
|
||||
) -> PublicQuizTivityShare:
|
||||
share = await QuizTivityShare.objects.get_or_none(id=uuid, user=user)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share wasn't found")
|
||||
expire_at = None
|
||||
if data.expire_in is not None:
|
||||
expire_at = (datetime.now() + timedelta(minutes=data.expire_in)).replace(tzinfo=None)
|
||||
share.name = data.name
|
||||
share.expire_at = expire_at
|
||||
return PublicQuizTivityShare.from_db_model(await share.update())
|
||||
|
||||
|
||||
@router.get("/{uuid}")
|
||||
async def get_share(uuid: UUID) -> QuizTivity:
|
||||
share = await QuizTivityShare.objects.select_related("quiztivity").get_or_none(id=uuid)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share not found")
|
||||
if share.expire_at is None:
|
||||
return share.quiztivity
|
||||
if share.expire_at < datetime.now():
|
||||
raise HTTPException(status_code=410, detail="Already expired")
|
||||
return share.quiztivity
|
||||
@@ -12,14 +12,14 @@ from classquiz.db.models import User, GameResults
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[GameResults])
|
||||
async def list_game_results(user: User = Depends(get_current_user)):
|
||||
results = await GameResults.objects.all(user=user.id)
|
||||
@router.get("/list")
|
||||
async def list_game_results(user: User = Depends(get_current_user)) -> list[GameResults]:
|
||||
results = await GameResults.objects.select_related(GameResults.quiz).all(user=user.id)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/list/{quiz_id}", response_model=list[GameResults])
|
||||
async def get_results_by_quiz(quiz_id: UUID, user: User = Depends(get_current_user)):
|
||||
@router.get("/list/{quiz_id}")
|
||||
async def get_results_by_quiz(quiz_id: UUID, user: User = Depends(get_current_user)) -> list[GameResults]:
|
||||
res = await GameResults.objects.all(user=user.id, quiz=quiz_id)
|
||||
if res is None:
|
||||
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||
@@ -27,9 +27,9 @@ async def get_results_by_quiz(quiz_id: UUID, user: User = Depends(get_current_us
|
||||
return res
|
||||
|
||||
|
||||
@router.get("/{game_id}", response_model=GameResults)
|
||||
async def get_game_result(game_id: UUID, user: User = Depends(get_current_user)):
|
||||
res = await GameResults.objects.get_or_none(user=user.id, id=game_id)
|
||||
@router.get("/{game_id}")
|
||||
async def get_game_result(game_id: UUID, user: User = Depends(get_current_user)) -> GameResults:
|
||||
res = await GameResults.objects.select_related(GameResults.quiz).get_or_none(user=user.id, id=game_id)
|
||||
if res is None:
|
||||
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||
else:
|
||||
@@ -40,8 +40,8 @@ class _SetNoteInput(BaseModel):
|
||||
note: str
|
||||
|
||||
|
||||
@router.post("/set_note", response_model=GameResults)
|
||||
async def set_note(id: UUID, data: _SetNoteInput, user: User = Depends(get_current_user)):
|
||||
@router.post("/set_note")
|
||||
async def set_note(id: UUID, data: _SetNoteInput, user: User = Depends(get_current_user)) -> GameResults:
|
||||
res = await GameResults.objects.get_or_none(user=user.id, id=id)
|
||||
if res is None:
|
||||
raise HTTPException(status_code=404, detail="Game Result not found")
|
||||
@@ -49,6 +49,7 @@ async def set_note(id: UUID, data: _SetNoteInput, user: User = Depends(get_curre
|
||||
return await res.update()
|
||||
|
||||
|
||||
# skipcq: PYL-W0105
|
||||
"""
|
||||
@router.get("/export/{result_id}", response_class=StreamingResponse)
|
||||
async def export_result(result_id: UUID, user: User = Depends(get_current_user)):
|
||||
|
||||
+238
-14
@@ -1,38 +1,262 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import SpooledTemporaryFile
|
||||
|
||||
import re
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends, Request, Response
|
||||
from fastapi.responses import StreamingResponse, RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from classquiz.config import settings, storage
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.config import settings, storage, arq
|
||||
from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem, PrivateStorageItem
|
||||
from classquiz.helpers import check_image_string
|
||||
from classquiz.storage.errors import DownloadingFailedError
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$"
|
||||
|
||||
def headers_from_storage_item(item: StorageItem) -> dict[str, str]:
|
||||
base_headers = {"Content-Type": item.mime_type}
|
||||
if item.hash is not None:
|
||||
base_headers["X-Hash"] = item.hash.hex()
|
||||
if item.thumbhash is not None:
|
||||
base_headers["X-Thumbhash"] = item.thumbhash
|
||||
if item.alt_text is not None:
|
||||
base_headers["X-Alt-Text"] = item.alt_text
|
||||
if item.size != 0:
|
||||
base_headers["Content-Size"] = str(item.size)
|
||||
return base_headers
|
||||
|
||||
|
||||
@router.get("/download/{file_name}")
|
||||
async def download_file(file_name: str):
|
||||
if not re.match(file_regex, file_name):
|
||||
item = None
|
||||
checked_image_string = check_image_string(file_name)
|
||||
if not checked_image_string[0]:
|
||||
raise HTTPException(status_code=400, detail="Invalid file name")
|
||||
if checked_image_string[1] is not None:
|
||||
item = await StorageItem.objects.get_or_none(id=checked_image_string[1])
|
||||
if item is None:
|
||||
print("Item not found")
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
file_name = item.storage_path
|
||||
if file_name is None:
|
||||
file_name = item.id.hex
|
||||
if storage.backend == "s3":
|
||||
if item is None:
|
||||
return RedirectResponse(url=await storage.get_url(file_name, 300))
|
||||
else:
|
||||
return RedirectResponse(url=await storage.get_url(file_name, 300), headers=headers_from_storage_item(item))
|
||||
try:
|
||||
download = await storage.download(file_name)
|
||||
download = storage.download(file_name)
|
||||
except DownloadingFailedError:
|
||||
print("error")
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
if download is None:
|
||||
print("dload is none")
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
def iter_file():
|
||||
yield from download
|
||||
media_type = "image/*"
|
||||
if item is not None:
|
||||
media_type = item.mime_type
|
||||
headers = {"Cache-Control": "public, immutable, max-age=31536000"}
|
||||
if item is not None:
|
||||
headers = {**headers, **headers_from_storage_item(item)}
|
||||
|
||||
return StreamingResponse(
|
||||
iter_file(),
|
||||
media_type="image/*",
|
||||
headers={"Cache-Control": "public, immutable, max-age=31536000"},
|
||||
download,
|
||||
media_type=media_type,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/info/{file_name}")
|
||||
async def get_basic_file_info(file_name: str) -> Response:
|
||||
checked_image_string = check_image_string(file_name)
|
||||
if not checked_image_string[0]:
|
||||
raise HTTPException(status_code=404, detail="Invalid file name")
|
||||
if checked_image_string[1] is not None:
|
||||
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
|
||||
resp = Response(status_code=200, headers=headers_from_storage_item(item))
|
||||
else:
|
||||
resp = Response(status_code=200, headers={"Content-Type": "image/*"})
|
||||
return resp
|
||||
|
||||
|
||||
@router.head("/download/{file_name}")
|
||||
async def download_file_head(file_name: str) -> Response:
|
||||
checked_image_string = check_image_string(file_name)
|
||||
if not checked_image_string[0]:
|
||||
raise HTTPException(status_code=404, detail="Invalid file name")
|
||||
if checked_image_string[1] is not None:
|
||||
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
|
||||
resp = Response(status_code=200, headers=headers_from_storage_item(item))
|
||||
else:
|
||||
resp = Response(status_code=200, headers={"Content-Type": "image/*"})
|
||||
storage_file_name = file_name
|
||||
if storage.backend == "s3":
|
||||
resp.status_code = 307
|
||||
resp.headers.append("Location", await storage.get_url(storage_file_name, 300))
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def upload_file(file: UploadFile = File(), 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()
|
||||
|
||||
size = 0
|
||||
file_obj = StorageItem(
|
||||
id=file_id,
|
||||
uploaded_at=datetime.now(),
|
||||
mime_type=file.content_type,
|
||||
hash=None,
|
||||
user=user,
|
||||
size=size,
|
||||
deleted_at=None,
|
||||
alt_text=None,
|
||||
)
|
||||
await storage.upload(file_name=file_id.hex, file_data=file.file, mime_type=file.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")
|
||||
async def upload_raw_file(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()
|
||||
data_file = SpooledTemporaryFile(max_size=1000)
|
||||
async for chunk in request.stream():
|
||||
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=0,
|
||||
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.get("/meta/{file_id}")
|
||||
async def get_file_info(file_id: UUID, user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||
if file_data is None:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return PublicStorageItem.from_db_model(file_data)
|
||||
|
||||
|
||||
@router.delete("/meta/{file_id}")
|
||||
async def mark_file_as_deleted(file_id: UUID, user: User = Depends(get_current_user)):
|
||||
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||
if file_data is None:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
storage_path = file_data.storage_path
|
||||
if storage_path is None:
|
||||
storage_path = file_data.id.hex
|
||||
await storage.delete(storage_path)
|
||||
file_data.deleted_at = datetime.now()
|
||||
await file_data.update()
|
||||
return
|
||||
|
||||
|
||||
@router.put("/meta/{file_id}")
|
||||
async def update_image_data(
|
||||
file_id: UUID, data: UpdateStorageItem, user: User = Depends(get_current_user)
|
||||
) -> PublicStorageItem:
|
||||
file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None)
|
||||
if file_data is None:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
if data.alt_text == "":
|
||||
data.alt_text = None
|
||||
if data.filename == "":
|
||||
data.filename = None
|
||||
file_data.filename = data.filename
|
||||
file_data.alt_text = data.alt_text
|
||||
await file_data.update()
|
||||
return PublicStorageItem.from_db_model(file_data)
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_images(
|
||||
since: datetime | None = None, user: User = Depends(get_current_user)
|
||||
) -> list[PrivateStorageItem]:
|
||||
if since is None:
|
||||
since = datetime.now() - timedelta(weeks=9999)
|
||||
storage_items = (
|
||||
await StorageItem.objects.filter(user=user)
|
||||
.filter(StorageItem.uploaded_at > since)
|
||||
.filter(StorageItem.deleted_at == None) # noqa: E711
|
||||
.order_by(StorageItem.uploaded_at.desc())
|
||||
.select_related([StorageItem.quizzes, StorageItem.quiztivities])
|
||||
.all()
|
||||
)
|
||||
if len(storage_items) == 0:
|
||||
raise HTTPException(status_code=404, detail="No items found")
|
||||
return_items: list[PrivateStorageItem] = []
|
||||
for item in storage_items:
|
||||
return_items.append(PrivateStorageItem.from_db_model(item))
|
||||
return return_items
|
||||
|
||||
|
||||
@router.get("/list/last")
|
||||
async def get_latest_images(count: int = 50, user: User = Depends(get_current_user)) -> list[PrivateStorageItem]:
|
||||
count = min(count, 50)
|
||||
items = (
|
||||
await StorageItem.objects.filter(user=user)
|
||||
.limit(count)
|
||||
.select_related([StorageItem.quizzes, StorageItem.quiztivities])
|
||||
.order_by(StorageItem.uploaded_at.desc())
|
||||
.all()
|
||||
)
|
||||
return_items: list[PrivateStorageItem] = []
|
||||
for item in items:
|
||||
return_items.append(PrivateStorageItem.from_db_model(item))
|
||||
return return_items
|
||||
|
||||
|
||||
class ReturnGetStorageLimit(BaseModel):
|
||||
limit: int
|
||||
limit_reached: bool
|
||||
used: int
|
||||
|
||||
|
||||
@router.get("/limit")
|
||||
async def get_storage_limit(user: User = Depends(get_current_user)) -> ReturnGetStorageLimit:
|
||||
user = await User.objects.get_or_none(id=user.id)
|
||||
if user.storage_used > settings.free_storage_limit:
|
||||
return ReturnGetStorageLimit(limit=settings.free_storage_limit, limit_reached=True, used=user.storage_used)
|
||||
else:
|
||||
return ReturnGetStorageLimit(limit=settings.free_storage_limit, limit_reached=False, used=user.storage_used)
|
||||
|
||||
@@ -79,7 +79,6 @@ async def create_user(user: RouteUser, background_task: BackgroundTasks) -> User
|
||||
if len(user.username) == 32:
|
||||
return JSONResponse({"details": "Username mustn't be 32 characters long"}, 400)
|
||||
await user.save()
|
||||
# print(settings.skip_email_verification)
|
||||
if settings.skip_email_verification:
|
||||
user.verify_key = None
|
||||
user.verified = True
|
||||
|
||||
Reference in New Issue
Block a user