diff --git a/classquiz/__init__.py b/classquiz/__init__.py index 8baaf10..b966a5a 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -33,6 +33,7 @@ from classquiz.routers import ( box_controller, quiztivity, pixabay, + moderation, ) from classquiz.socket_server import sio from classquiz.helpers import meilisearch_init, telemetry_ping @@ -77,6 +78,7 @@ async def auth_middleware_wrapper(request: Request, call_next): return await rememberme_middleware(request, call_next) +app.include_router(moderation.router, tags=["moderation"], prefix="/api/v1/moderation", include_in_schema=True) app.include_router(pixabay.router, tags=["pixabay"], prefix="/api/v1/pixabay", include_in_schema=True) app.include_router(quiztivity.router, tags=["quiztivity"], prefix="/api/v1/quiztivity", include_in_schema=True) diff --git a/classquiz/auth.py b/classquiz/auth.py index 93f4c23..3b683a2 100644 --- a/classquiz/auth.py +++ b/classquiz/auth.py @@ -128,6 +128,23 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): return user +async def get_current_moderator(token: str = Depends(oauth2_scheme)): + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM]) + email: str = payload.get("sub") + if email is None: + raise credentials_exception + token_data = TokenData(email=email) + except JWTError: + raise credentials_exception + user = await get_user_from_mail(email=token_data.email) + if user is None: + raise credentials_exception + if user.username not in settings.mods: + raise credentials_exception + return user + + async def get_admin_user(token: str = Depends(oauth2_scheme)) -> User: user = await get_current_user(token) admin_user = await User.objects.order_by(User.created_at.asc()).get() diff --git a/classquiz/config.py b/classquiz/config.py index 4bf50b8..2b44b65 100644 --- a/classquiz/config.py +++ b/classquiz/config.py @@ -53,6 +53,7 @@ class Settings(BaseSettings): telemetry_enabled: bool = True free_storage_limit: int = 1074000000 pixabay_api_key: str | None = None + mods: list[str] = [] # storage_backend storage_backend: str | None = "local" diff --git a/classquiz/db/models.py b/classquiz/db/models.py index bf79a87..95511f7 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -184,6 +184,7 @@ class Quiz(ormar.Model): dislikes: int = ormar.Integer(nullable=False, default=0, server_default="0") plays: 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) class Meta: tablename = "quiz" diff --git a/classquiz/helpers/__init__.py b/classquiz/helpers/__init__.py index bef4394..388b3c4 100644 --- a/classquiz/helpers/__init__.py +++ b/classquiz/helpers/__init__.py @@ -170,6 +170,7 @@ async def handle_import_from_excel(data: BinaryIO, user: User) -> Quiz: else: existing_quiz.questions = [*existing_quiz.questions, *questions] existing_quiz.updated_at = datetime.now() + existing_quiz.mod_rating = None await existing_quiz.update() quiz = existing_quiz return quiz diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py index 54159cb..43d7238 100644 --- a/classquiz/routers/editor.py +++ b/classquiz/routers/editor.py @@ -129,6 +129,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): 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: diff --git a/classquiz/routers/eximport.py b/classquiz/routers/eximport.py index 3c54899..4ded1bd 100644 --- a/classquiz/routers/eximport.py +++ b/classquiz/routers/eximport.py @@ -128,5 +128,6 @@ async def import_quiz(file: UploadFile = File(), user: User = Depends(get_curren quiz = Quiz.parse_obj(quiz_dict) quiz.user_id = user.id quiz.imported_from_kahoot = None + quiz.mod_rating = None await quiz.save() return quiz diff --git a/classquiz/routers/moderation.py b/classquiz/routers/moderation.py new file mode 100644 index 0000000..856946b --- /dev/null +++ b/classquiz/routers/moderation.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2023 Marlon W (Mawoka) +# +# SPDX-License-Identifier: MPL-2.0 +import uuid + +from fastapi import APIRouter, Depends, Response, HTTPException +from pydantic import BaseModel + +from classquiz.auth import get_current_moderator +from classquiz.db.models import User, Quiz + +router = APIRouter() + + +@router.get("/status") +async def get_mod_status(resp: Response, user: User = Depends(get_current_moderator)): + resp.status_code = 200 + resp.set_cookie("moderator", "yes", path="/") + resp.headers.update({"Content-Type": "application/json"}) + resp.body = '{"status": "ok"}' + return resp + + +@router.get("/quizzes") +async def get_newest_quizzes( + page: int = 1, all: bool = False, user: User = Depends(get_current_moderator) +) -> list[Quiz]: + if page < 1: + raise HTTPException(status_code=400, detail="page 1 is the first") + if all: + quizzes = ( + await Quiz.objects.paginate(page=page) + .order_by(Quiz.updated_at.desc()) + .filter(Quiz.public == True) # noqa: E712 + .all() + ) + else: + # noinspection PyComparisonWithNone + quizzes = ( + await Quiz.objects.paginate(page=page) + .order_by(Quiz.updated_at.desc()) + .filter(Quiz.public == True) # noqa: E712 + .filter(Quiz.mod_rating == None) # noqa: E711 + .all() + ) + return quizzes + + +class SetModRatingForQuizInput(BaseModel): + rating: int | None + + +@router.post("/rating/set/{quiz_id}") +async def set_mod_rating_for_quiz( + data: SetModRatingForQuizInput, quiz_id: uuid.UUID, user: User = Depends(get_current_moderator) +) -> Quiz: + quiz = await Quiz.objects.get_or_none(public=True, id=quiz_id) + if quiz is None: + raise HTTPException(status_code=404, detail="Quiz not found") + quiz.mod_rating = data.rating + return await quiz.update() diff --git a/frontend/src/lib/collapsible.svelte b/frontend/src/lib/collapsible.svelte index 36af910..ac8be0d 100644 --- a/frontend/src/lib/collapsible.svelte +++ b/frontend/src/lib/collapsible.svelte @@ -7,7 +7,7 @@ SPDX-License-Identifier: MPL-2.0
{@html quiz.title}
++ {@html quiz.description ?? ''} +
+Questions: {quiz.questions.length}
+Page {data.page}
+{@html quiz.description}
+
{$t('view_quiz_page.made_by')} - @{quiz.user_id.username} + @{quiz.user_id.username}
{#if quiz.cover_image} -- {question.time}s + {question.time}s
{#if question.type === QuizQuestionType.ABCD || question.type === undefined || question.type === QuizQuestionType.CHECK} -+
All numbers between {question.answers.min_correct} and {question.answers.max_correct} are correct, where numbers between {question - .answers.min} and {question.answers.max} can be selected. + .answers.min} and {question.answers.max} can be selected.
{:else if question.type === QuizQuestionType.ORDER} -