# retoor <retoor@molodetz.nl>
from typing import Annotated
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from devplacepy.database import get_users_by_uids
from devplacepy.dependencies import json_or_form
from devplacepy.models import QuizQuestionForm, QuizReorderForm
from devplacepy.responses import action_result, respond
from devplacepy.schemas import QuizBuilderOut
from devplacepy.services.audit import record as audit
from devplacepy.services.quiz import QuizError, scoring, store
from devplacepy.utils import require_user
from ._shared import action_error, load_quiz, notify_quiz, quiz_seo, quiz_url, require_editor
router = APIRouter()
KIND_SPECS = [
{
"key": kind.key,
"label": kind.label,
"icon": kind.icon,
"has_options": kind.has_options,
"graded_by": kind.graded_by,
}
for kind in scoring.QUESTION_KINDS
]
def _question_payload(data: QuizQuestionForm) -> dict:
return {
"kind": data.kind,
"prompt": data.prompt.strip(),
"explanation": data.explanation.strip(),
"points": data.points,
"media_attachment_uid": data.media_attachment_uid.strip(),
"correct_boolean": int(bool(data.correct_boolean)),
"expected_answer": data.expected_answer.strip(),
"grading_criteria": data.grading_criteria.strip(),
"numeric_value": data.numeric_value,
"numeric_tolerance": data.numeric_tolerance,
"case_sensitive": int(bool(data.case_sensitive)),
"options": data.option_rows(),
}
@router.get("/{slug}/edit", response_class=HTMLResponse)
async def quiz_builder(request: Request, slug: str):
user = require_user(request)
quiz = require_editor(load_quiz(slug, user), user)
author = get_users_by_uids([quiz["user_uid"]]).get(quiz["user_uid"])
seo_ctx = quiz_seo(
request,
f"Edit {quiz.get('title') or 'quiz'}",
"Build and publish your quiz.",
robots="noindex,follow",
breadcrumbs=[{"name": quiz.get("title") or "Quiz", "url": quiz_url(quiz)}],
)
return respond(
request,
"quiz_edit.html",
{
**seo_ctx,
"request": request,
"user": user,
"quiz": store.serialize_quiz(quiz, user, author),
"questions": store.serialize_builder(quiz, user),
"kinds": KIND_SPECS,
"validation_errors": store.validation_errors(quiz["uid"]),
"quiz_error": request.query_params.get("error", ""),
},
model=QuizBuilderOut,
)
@router.post("/{slug}/questions")
async def add_question(
request: Request,
slug: str,
data: Annotated[QuizQuestionForm, Depends(json_or_form(QuizQuestionForm))],
):
user = require_user(request)
quiz = require_editor(load_quiz(slug, user), user)
try:
question = store.add_question(quiz["uid"], _question_payload(data))
except QuizError as exc:
return action_error(request, str(exc), f"/quizzes/{slug}/edit")
_audit_question(request, user, quiz, question, "create")
await notify_quiz(quiz["uid"])
return action_result(
request,
f"/quizzes/{slug}/edit",
data={"uid": question["uid"], "position": question["position"]},
)
@router.post("/{slug}/questions/reorder")
async def reorder_questions(
request: Request,
slug: str,
data: Annotated[QuizReorderForm, Depends(json_or_form(QuizReorderForm))],
):
user = require_user(request)
quiz = require_editor(load_quiz(slug, user), user)
try:
store.reorder_questions(quiz["uid"], data.order)
except QuizError as exc:
return action_error(request, str(exc), f"/quizzes/{slug}/edit")
audit.record(
request,
"quiz.question.reorder",
user=user,
target_type="quiz",
target_uid=quiz["uid"],
target_label=quiz.get("title") or quiz["uid"],
summary=f"{user['username']} reordered the questions of quiz {quiz.get('title')}",
links=[audit.target("quiz", quiz["uid"], quiz.get("title") or "")],
)
await notify_quiz(quiz["uid"])
return action_result(request, f"/quizzes/{slug}/edit", data={"order": data.order})
@router.post("/{slug}/questions/{question_uid}")
async def edit_question(
request: Request,
slug: str,
question_uid: str,
data: Annotated[QuizQuestionForm, Depends(json_or_form(QuizQuestionForm))],
):
user = require_user(request)
quiz = require_editor(load_quiz(slug, user), user)
try:
question = store.edit_question(quiz["uid"], question_uid, _question_payload(data))
except QuizError as exc:
return action_error(request, str(exc), f"/quizzes/{slug}/edit")
_audit_question(request, user, quiz, question, "edit")
await notify_quiz(quiz["uid"])
return action_result(request, f"/quizzes/{slug}/edit", data={"uid": question_uid})
@router.post("/{slug}/questions/{question_uid}/delete")
async def delete_question(request: Request, slug: str, question_uid: str):
user = require_user(request)
quiz = require_editor(load_quiz(slug, user), user)
try:
question = store.delete_question(quiz["uid"], question_uid, user["uid"])
except QuizError as exc:
return action_error(request, str(exc), f"/quizzes/{slug}/edit")
_audit_question(request, user, quiz, question, "delete")
await notify_quiz(quiz["uid"])
return action_result(request, f"/quizzes/{slug}/edit", data={"uid": question_uid})
def _audit_question(request, user: dict, quiz: dict, question: dict, action: str) -> None:
audit.record(
request,
f"quiz.question.{action}",
user=user,
target_type="quiz",
target_uid=quiz["uid"],
target_label=quiz.get("title") or quiz["uid"],
summary=(
f"{user['username']} {action}d a {question.get('kind')} question "
f"on quiz {quiz.get('title')}"
),
metadata={"question_uid": question.get("uid"), "kind": question.get("kind")},
links=[audit.target("quiz", quiz["uid"], quiz.get("title") or "")],
)