Files
devplacepy/devplacepy/services/devii/actions/catalog/quizzes.py
T
2026-07-26 16:46:41 +02:00

322 lines
13 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from ..spec import Action
from ._shared import body, confirm, path, query
KIND_LIST = (
"single_choice, multiple_choice, true_false, free_text, fill_blank, numeric, "
"ordering, matching"
)
DOCUMENT_HELP = (
"The complete quiz as a JSON string: "
'{"title": "...", "description": "...", '
'"settings": {"shuffle_questions": true, "reveal_answers": true, '
'"pass_percent": 70, "time_limit_seconds": 900}, '
'"questions": [{"kind": "single_choice", "prompt": "...", "points": 1, '
'"explanation": "...", "options": [{"label": "A"}, {"label": "B", "is_correct": true}]}]}. '
f"Question kinds: {KIND_LIST}. A free_text question needs expected_answer or "
"grading_criteria; numeric needs numeric_value; true_false needs correct_boolean; "
"fill_blank and matching need a match_value on every option."
)
QUIZ_ACTIONS: tuple[Action, ...] = (
Action(
name="list_quizzes",
method="GET",
path="/quizzes",
summary="List quizzes with the viewer's per-quiz progress",
description=(
"Returns published quizzes plus the signed-in viewer's state per quiz "
"(todo, in_progress, done) and the cross-quiz scoreboard."
),
handler="http",
requires_auth=False,
read_only=True,
params=(
query("search", "Match the title, description or author username."),
query("filter", "One of all, todo, done, mine, drafts. Defaults to all."),
query("page", "1-based page number."),
),
),
Action(
name="get_quiz",
method="GET",
path="/quizzes/{slug}",
summary="Get one quiz with its stats, leaderboard and comments",
handler="http",
requires_auth=False,
read_only=True,
params=(path("slug", "Quiz slug or uid."),),
),
Action(
name="export_quiz",
method="GET",
path="/quizzes/{slug}/export",
summary="Export a quiz as a full JSON document",
description=(
"The inverse of import_quiz. The owner's export includes every correct "
"answer; a public export of a published quiz omits them."
),
handler="http",
requires_auth=False,
read_only=True,
params=(path("slug", "Quiz slug or uid."),),
),
Action(
name="create_quiz",
method="POST",
path="/quizzes/create",
summary="Create an empty draft quiz",
handler="http",
requires_auth=True,
params=(
body("title", "Quiz title, 3 to 200 characters.", required=True),
body("description", "Markdown description."),
body("shuffle_questions", "Set to 1 to shuffle the question order."),
body("shuffle_options", "Set to 1 to shuffle the answer options."),
body("reveal_answers", "Set to 1 to reveal answers after each question."),
body("allow_review", "Set to 1 to allow reviewing answers on the results screen."),
body("time_limit_seconds", "Time limit in seconds, 0 for none."),
body("pass_percent", "Pass mark 0-100, 0 for no verdict."),
),
),
Action(
name="import_quiz",
method="POST",
path="/quizzes/import",
summary="Create a complete quiz from one JSON document",
description=(
"The full-automation entry point: creates the draft quiz, every question "
"and every option in one call. Publish it afterwards with publish_quiz."
),
handler="http",
requires_auth=True,
params=(body("document", DOCUMENT_HELP, required=True),),
),
Action(
name="edit_quiz",
method="POST",
path="/quizzes/edit/{slug}",
summary="Edit a draft quiz's title, description and settings",
description="Refused with a 400 once the quiz is published.",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
body("title", "Quiz title, 3 to 200 characters.", required=True),
body("description", "Markdown description."),
body("shuffle_questions", "Set to 1 to shuffle the question order."),
body("shuffle_options", "Set to 1 to shuffle the answer options."),
body("reveal_answers", "Set to 1 to reveal answers after each question."),
body("allow_review", "Set to 1 to allow reviewing answers on the results screen."),
body("time_limit_seconds", "Time limit in seconds, 0 for none."),
body("pass_percent", "Pass mark 0-100, 0 for no verdict."),
),
),
Action(
name="publish_quiz",
method="POST",
path="/quizzes/{slug}/publish",
summary="Publish a quiz, freezing it forever",
description=(
"IRREVERSIBLE. A published quiz, its questions and its options can never be "
"edited again; only deletion remains. Refused with the exact list of problems "
"when the quiz is incomplete."
),
handler="http",
requires_auth=True,
params=(path("slug", "Quiz slug or uid."), confirm()),
),
Action(
name="delete_quiz",
method="POST",
path="/quizzes/delete/{slug}",
summary="Delete a quiz and everything attached to it",
handler="http",
requires_auth=True,
params=(path("slug", "Quiz slug or uid."), confirm()),
),
Action(
name="add_quiz_question",
method="POST",
path="/quizzes/{slug}/questions",
summary="Add one question to a draft quiz",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
body("kind", f"Question kind, one of: {KIND_LIST}.", required=True),
body("prompt", "The question prompt, markdown.", required=True),
body("points", "Points for this question, 1 to 100."),
body("explanation", "Explanation shown after answering."),
body("options", "Option labels, one per line or comma separated."),
body("match_values", "Accepted answers aligned with the options, one per line."),
body("correct_indexes", "Comma separated 0-based indexes of the correct options."),
body("correct_boolean", "true_false only: 1 when the statement is true."),
body("expected_answer", "free_text only: the reference answer."),
body("grading_criteria", "free_text only: criteria for the AI reviewer."),
body("numeric_value", "numeric only: the correct value."),
body("numeric_tolerance", "numeric only: the accepted absolute tolerance."),
body("case_sensitive", "fill_blank only: 1 to compare case sensitively."),
),
),
Action(
name="edit_quiz_question",
method="POST",
path="/quizzes/{slug}/questions/{question_uid}",
summary="Replace one question of a draft quiz",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
path("question_uid", "The question uid."),
body("kind", f"Question kind, one of: {KIND_LIST}.", required=True),
body("prompt", "The question prompt, markdown.", required=True),
body("points", "Points for this question, 1 to 100."),
body("explanation", "Explanation shown after answering."),
body("options", "Option labels, one per line or comma separated."),
body("match_values", "Accepted answers aligned with the options, one per line."),
body("correct_indexes", "Comma separated 0-based indexes of the correct options."),
body("correct_boolean", "true_false only: 1 when the statement is true."),
body("expected_answer", "free_text only: the reference answer."),
body("grading_criteria", "free_text only: criteria for the AI reviewer."),
body("numeric_value", "numeric only: the correct value."),
body("numeric_tolerance", "numeric only: the accepted absolute tolerance."),
body("case_sensitive", "fill_blank only: 1 to compare case sensitively."),
),
),
Action(
name="delete_quiz_question",
method="POST",
path="/quizzes/{slug}/questions/{question_uid}/delete",
summary="Delete one question from a draft quiz",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
path("question_uid", "The question uid."),
confirm(),
),
),
Action(
name="reorder_quiz_questions",
method="POST",
path="/quizzes/{slug}/questions/reorder",
summary="Set the question order of a draft quiz",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
body(
"order",
"Every question uid in the wanted order, comma separated.",
required=True,
),
),
),
Action(
name="start_quiz_attempt",
method="POST",
path="/quizzes/{slug}/attempts",
summary="Start or resume an attempt on a published quiz",
description="Always returns the single in-progress attempt for this member.",
handler="http",
requires_auth=True,
params=(path("slug", "Quiz slug or uid."),),
),
Action(
name="get_quiz_attempt",
method="GET",
path="/quizzes/{slug}/attempts/{attempt_uid}",
summary="Read an attempt with its questions in play order",
description=(
"Correct answers are withheld until a question has been answered and the "
"quiz reveals answers."
),
handler="http",
requires_auth=True,
read_only=True,
params=(
path("slug", "Quiz slug or uid."),
path("attempt_uid", "The attempt uid."),
),
),
Action(
name="answer_quiz_question",
method="POST",
path="/quizzes/{slug}/attempts/{attempt_uid}/answer",
summary="Submit one answer and get it graded",
description=(
"Each question can be answered exactly once. free_text answers are graded "
"by the AI reviewer and fall back to keyword overlap when it is unavailable."
),
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
path("attempt_uid", "The attempt uid."),
body("question_uid", "The question being answered.", required=True),
body("answer_text", "Free text, the numeric value, or true/false."),
body("option_uids", "Chosen option uids, comma separated and in order for ordering."),
body("blanks", "fill_blank only: one answer per blank, comma separated."),
body("matches", "matching only: the chosen right-hand value per option_uid, in order."),
),
),
Action(
name="finish_quiz_attempt",
method="POST",
path="/quizzes/{slug}/attempts/{attempt_uid}/finish",
summary="Finish an attempt and get the final score",
handler="http",
requires_auth=True,
params=(
path("slug", "Quiz slug or uid."),
path("attempt_uid", "The attempt uid."),
),
),
Action(
name="get_quiz_result",
method="GET",
path="/quizzes/{slug}/attempts/{attempt_uid}/results",
summary="Read the result of a finished attempt",
handler="http",
requires_auth=True,
read_only=True,
params=(
path("slug", "Quiz slug or uid."),
path("attempt_uid", "The attempt uid."),
),
),
Action(
name="quiz_leaderboard",
method="GET",
path="/quizzes/{slug}/leaderboard",
summary="Top completed attempts on one quiz",
handler="http",
requires_auth=False,
read_only=True,
params=(
path("slug", "Quiz slug or uid."),
query("limit", "How many entries to return, up to 100."),
),
),
Action(
name="quiz_scoreboard",
method="GET",
path="/quizzes/scoreboard",
summary="The cross-quiz score per user",
description=(
"Each member contributes their best completed attempt per quiz, including "
"quizzes they wrote themselves."
),
handler="http",
requires_auth=False,
read_only=True,
params=(query("limit", "How many entries to return, up to 100."),),
),
)