|
# 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 "")],
|
|
)
|