322 lines
13 KiB
Python
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."),),
|
|
),
|
|
)
|