|
# retoor <retoor@molodetz.nl>
|
|
|
|
from devplacepy.services.quiz import scoring
|
|
|
|
from .._shared import endpoint, field
|
|
|
|
KIND_KEYS = list(scoring.KIND_KEYS)
|
|
FILTER_KEYS = ["all", "todo", "done", "mine", "drafts"]
|
|
STATUS_KEYS = ["draft", "published"]
|
|
GRADED_BY_KEYS = ["auto", "ai", "fallback"]
|
|
VIEWER_STATES = ["todo", "in_progress", "done"]
|
|
|
|
SAMPLE_QUIZ = {
|
|
"uid": "0198f2c0-1111-7aaa-8bbb-000000000001",
|
|
"slug": "8bbb000000000001-sqlite-fundamentals",
|
|
"url": "/quizzes/8bbb000000000001-sqlite-fundamentals",
|
|
"title": "SQLite fundamentals",
|
|
"status": "published",
|
|
"question_count": 10,
|
|
"total_points": 14,
|
|
"attempt_count": 23,
|
|
"time_limit_seconds": 900,
|
|
"pass_percent": 70,
|
|
"viewer_owns": False,
|
|
"viewer_can_edit": False,
|
|
"viewer_can_play": True,
|
|
"viewer_state": "todo",
|
|
"validation_errors": [],
|
|
}
|
|
|
|
GROUP = {
|
|
"slug": "quizzes",
|
|
"title": "Quizzes",
|
|
"intro": """
|
|
# Quizzes
|
|
|
|
A quiz is user-generated content like a gist or a project: it has an owner, a slug, comments,
|
|
votes, bookmarks and reactions. Any signed-in member authors quizzes, every member plays them,
|
|
and guests read published ones.
|
|
|
|
**Publishing is terminal.** A draft is fully editable; the moment its owner publishes it, the
|
|
quiz, its questions and its options are frozen forever. There is no unpublish and no
|
|
post-publish edit, which is what makes two members' scores on the same quiz comparable. Every
|
|
write endpoint on a published quiz returns `400`; only delete still works. Publish validates
|
|
the whole quiz first and refuses with the exact list of problems.
|
|
|
|
Playing a quiz creates an **attempt**. There is at most one in-progress attempt per member per
|
|
quiz - starting again returns the existing one. Each question can be answered exactly once; a
|
|
second submit returns `400` and credits nothing. A time limit is stored on the attempt and
|
|
evaluated lazily on read, so an expired attempt reads as `expired` with no background process
|
|
involved.
|
|
|
|
Seven question kinds are graded deterministically. The eighth, `free_text`, is graded by the
|
|
internal AI gateway against the author's criteria and billed to the answering member's own API
|
|
key. When the gateway is unavailable the answer is still graded, by a deterministic
|
|
token-overlap fallback, and the answer carries `graded_by: "fallback"` so the degradation is
|
|
visible rather than silent. `graded_by` is one of `auto`, `ai`, `fallback`.
|
|
|
|
**Correct answers are never served to a player mid-attempt.** `is_correct` on the options and
|
|
`correct_boolean` / `expected_answer` / `numeric_value` / `match_value` on the question are
|
|
omitted unless the viewer owns the quiz, or the question has already been answered in this
|
|
attempt and the quiz has `reveal_answers` on. A public export of a published quiz omits them
|
|
too; the owner's export includes them.
|
|
|
|
The **scoreboard** at `/quizzes/scoreboard` sums each member's **best** completed attempt per
|
|
quiz, never the sum of all attempts, so replaying a quiz can raise a member's contribution to
|
|
their personal best and never beyond it. Quizzes a member wrote themselves count like any
|
|
other.
|
|
|
|
All endpoints negotiate HTML or JSON. POST bodies are form encoded
|
|
(`application/x-www-form-urlencoded`). Action POSTs answer `{"ok": true, "redirect": "...",
|
|
"data": {...}}`; an invalid domain operation answers `400` as
|
|
`{"error": {"status": 400, "message": "..."}}`.
|
|
""",
|
|
"endpoints": [
|
|
endpoint(
|
|
id="quizzes-list",
|
|
method="GET",
|
|
path="/quizzes",
|
|
title="Quiz hub",
|
|
summary=(
|
|
"Published quizzes with the viewer's per-quiz state, the filter counts and "
|
|
"the cross-quiz scoreboard."
|
|
),
|
|
auth="public",
|
|
negotiation=True,
|
|
params=[
|
|
field("search", "query", "string", False, "sqlite", "Match the title, description or author username."),
|
|
field("filter", "query", "enum", False, "all", "Which quizzes to list.", options=FILTER_KEYS),
|
|
field("page", "query", "integer", False, "1", "1-based page number."),
|
|
],
|
|
sample_response={
|
|
"quizzes": [
|
|
{
|
|
**SAMPLE_QUIZ,
|
|
"viewer_best_percent": 0.0,
|
|
"comment_count": 3,
|
|
"stars": 5,
|
|
}
|
|
],
|
|
"filter": "all",
|
|
"counts": {"all": 12, "todo": 9, "done": 3, "mine": 2, "drafts": 1},
|
|
"pagination": {"page": 1, "total": 12, "total_pages": 1},
|
|
"scoreboard": [
|
|
{"rank": 1, "user": {"username": "alice"}, "total_points": 84.0,
|
|
"quizzes_completed": 7, "avg_percent": 88.4, "perfect_count": 2}
|
|
],
|
|
"viewer_can_create": True,
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-scoreboard",
|
|
method="GET",
|
|
path="/quizzes/scoreboard",
|
|
title="Quiz scoreboard",
|
|
summary=(
|
|
"Score per user across every published quiz, counting each member's best "
|
|
"attempt per quiz. Cached about 15 seconds."
|
|
),
|
|
auth="public",
|
|
params=[
|
|
field("limit", "query", "integer", False, "20", "How many entries to return, up to 100."),
|
|
],
|
|
sample_response={
|
|
"scoreboard": [
|
|
{"rank": 1, "user": {"username": "alice"}, "total_points": 84.0,
|
|
"quizzes_completed": 7, "avg_percent": 88.4, "perfect_count": 2}
|
|
],
|
|
"viewer_standing": None,
|
|
"limit": 20,
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-new",
|
|
method="GET",
|
|
path="/quizzes/new",
|
|
title="New quiz form",
|
|
summary="The create form behind the New quiz button.",
|
|
auth="user",
|
|
negotiation=True,
|
|
sample_response={"viewer_can_create": True},
|
|
),
|
|
endpoint(
|
|
id="quizzes-create",
|
|
method="POST",
|
|
path="/quizzes/create",
|
|
title="Create a quiz",
|
|
summary="Create a draft quiz. Add its questions afterwards, then publish it.",
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("title", "form", "string", True, "SQLite fundamentals", "3 to 200 characters."),
|
|
field("description", "form", "string", False, "Ten questions on WAL.", "Markdown, up to 5000 characters."),
|
|
field("shuffle_questions", "form", "boolean", False, "1", "Shuffle the question order per attempt."),
|
|
field("shuffle_options", "form", "boolean", False, "1", "Shuffle the answer options."),
|
|
field("reveal_answers", "form", "boolean", False, "1", "Reveal the correct answer after each question."),
|
|
field("allow_review", "form", "boolean", False, "1", "Allow reviewing every answer on the results screen."),
|
|
field("time_limit_seconds", "form", "integer", False, "900", "0 for no limit, up to 86400."),
|
|
field("pass_percent", "form", "integer", False, "70", "0 to 100, 0 for no pass or fail verdict."),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit",
|
|
"data": {"uid": SAMPLE_QUIZ["uid"], "slug": SAMPLE_QUIZ["slug"]},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-import",
|
|
method="POST",
|
|
path="/quizzes/import",
|
|
title="Import a quiz document",
|
|
summary=(
|
|
"Create a complete quiz - metadata, settings, every question and every option - "
|
|
"from one JSON document. Capped at 100 questions and 12 options per question."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field(
|
|
"document",
|
|
"form",
|
|
"string",
|
|
True,
|
|
'{"title": "SQLite fundamentals", "questions": [{"kind": "single_choice", "prompt": "Which journal mode allows concurrent readers?", "options": [{"label": "DELETE"}, {"label": "WAL", "is_correct": true}]}]}',
|
|
"The complete quiz as a JSON string. See the export endpoint for the exact shape.",
|
|
),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit",
|
|
"data": {"uid": SAMPLE_QUIZ["uid"], "slug": SAMPLE_QUIZ["slug"], "question_count": 10},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-detail",
|
|
method="GET",
|
|
path="/quizzes/{slug}",
|
|
title="Quiz detail",
|
|
summary=(
|
|
"One quiz with its stats, its leaderboard, its comments and the viewer's own "
|
|
"state. A draft is visible only to its owner and to administrators."
|
|
),
|
|
auth="public",
|
|
negotiation=True,
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={
|
|
"quiz": SAMPLE_QUIZ,
|
|
"leaderboard": [],
|
|
"comments": [],
|
|
"viewer_state": "todo",
|
|
"star_count": 5,
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-export",
|
|
method="GET",
|
|
path="/quizzes/{slug}/export",
|
|
title="Export a quiz",
|
|
summary=(
|
|
"The full quiz document, the exact inverse of the import endpoint. The owner "
|
|
"gets every correct answer; everyone else gets the questions without the key."
|
|
),
|
|
auth="public",
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={
|
|
"title": "SQLite fundamentals",
|
|
"description": "Ten questions on WAL, indexing and transactions.",
|
|
"settings": {"shuffle_questions": True, "reveal_answers": True,
|
|
"pass_percent": 70, "time_limit_seconds": 900},
|
|
"questions": [
|
|
{
|
|
"kind": "single_choice",
|
|
"prompt": "Which journal mode allows concurrent readers and one writer?",
|
|
"points": 1,
|
|
"options": [{"label": "DELETE"}, {"label": "WAL", "is_correct": True}],
|
|
}
|
|
],
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-leaderboard",
|
|
method="GET",
|
|
path="/quizzes/{slug}/leaderboard",
|
|
title="Quiz leaderboard",
|
|
summary="Top completed attempts on one quiz, best percentage first.",
|
|
auth="public",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("limit", "query", "integer", False, "25", "How many entries to return, up to 100."),
|
|
],
|
|
sample_response={
|
|
"quiz_uid": SAMPLE_QUIZ["uid"],
|
|
"entries": [
|
|
{"rank": 1, "user": {"username": "bob"}, "score_points": 13.0,
|
|
"score_percent": 92.86, "passed": True, "completed_at": "2026-07-25T10:00:00+00:00"}
|
|
],
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-builder",
|
|
method="GET",
|
|
path="/quizzes/{slug}/edit",
|
|
title="Quiz builder",
|
|
summary=(
|
|
"The owner's builder page: the quiz, every question with its answer key, the "
|
|
"question-kind catalogue and the live pre-publish checklist."
|
|
),
|
|
auth="user",
|
|
negotiation=True,
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={
|
|
"quiz": SAMPLE_QUIZ,
|
|
"questions": [],
|
|
"kinds": [{"key": "single_choice", "label": "Single choice", "has_options": True}],
|
|
"validation_errors": ["Add at least one question before publishing."],
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-edit",
|
|
method="POST",
|
|
path="/quizzes/edit/{slug}",
|
|
title="Edit a quiz",
|
|
summary="Change the title, description and settings of a DRAFT quiz. 400 once published.",
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("title", "form", "string", True, "SQLite fundamentals", "3 to 200 characters."),
|
|
field("description", "form", "string", False, "Updated description.", "Markdown, up to 5000 characters."),
|
|
field("shuffle_questions", "form", "boolean", False, "1", "Shuffle the question order per attempt."),
|
|
field("shuffle_options", "form", "boolean", False, "1", "Shuffle the answer options."),
|
|
field("reveal_answers", "form", "boolean", False, "1", "Reveal the correct answer after each question."),
|
|
field("allow_review", "form", "boolean", False, "1", "Allow reviewing every answer on the results screen."),
|
|
field("time_limit_seconds", "form", "integer", False, "900", "0 for no limit, up to 86400."),
|
|
field("pass_percent", "form", "integer", False, "70", "0 to 100, 0 for no pass or fail verdict."),
|
|
],
|
|
sample_response={"ok": True, "redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals"},
|
|
),
|
|
endpoint(
|
|
id="quizzes-publish",
|
|
method="POST",
|
|
path="/quizzes/{slug}/publish",
|
|
title="Publish a quiz",
|
|
summary=(
|
|
"IRREVERSIBLE. Freezes the quiz, its questions and its options forever. "
|
|
"Refuses with the validation problems when the quiz is incomplete."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals",
|
|
"data": {"uid": SAMPLE_QUIZ["uid"], "status": "published"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-delete",
|
|
method="POST",
|
|
path="/quizzes/delete/{slug}",
|
|
title="Delete a quiz",
|
|
summary=(
|
|
"Owner or administrator. Removes the quiz with its questions, options, "
|
|
"attempts and answers. The only operation left on a published quiz."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={"ok": True, "redirect": "/quizzes"},
|
|
),
|
|
endpoint(
|
|
id="quizzes-question-add",
|
|
method="POST",
|
|
path="/quizzes/{slug}/questions",
|
|
title="Add a question",
|
|
summary="Append one question with its options to a DRAFT quiz. 400 once published.",
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("kind", "form", "enum", True, "single_choice", "The question kind.", options=KIND_KEYS),
|
|
field("prompt", "form", "string", True, "Which journal mode allows concurrent readers?", "Markdown, up to 2000 characters."),
|
|
field("points", "form", "integer", False, "1", "1 to 100."),
|
|
field("explanation", "form", "string", False, "WAL keeps readers off the writer's lock.", "Shown after answering."),
|
|
field("options", "form", "string", False, "DELETE\nWAL\nMEMORY", "Option labels, one per line or comma separated."),
|
|
field("match_values", "form", "string", False, "", "Accepted answers aligned with the options, for fill_blank and matching."),
|
|
field("correct_indexes", "form", "string", True, "1", "0-based indexes of the correct options, comma separated. Required for choice questions."),
|
|
field("correct_boolean", "form", "boolean", False, "1", "true_false only: the statement is true."),
|
|
field("expected_answer", "form", "string", False, "", "free_text only: the reference answer."),
|
|
field("grading_criteria", "form", "string", False, "", "free_text only: criteria for the AI reviewer."),
|
|
field("numeric_value", "form", "number", False, "0", "numeric only: the correct value."),
|
|
field("numeric_tolerance", "form", "number", False, "0", "numeric only: accepted absolute tolerance."),
|
|
field("case_sensitive", "form", "boolean", False, "0", "fill_blank only: compare case sensitively."),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit",
|
|
"data": {"uid": "0198f2c0-2222-7aaa-8bbb-000000000002", "position": 0},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-question-edit",
|
|
method="POST",
|
|
path="/quizzes/{slug}/questions/{question_uid}",
|
|
title="Edit a question",
|
|
summary="Replace one question and its options on a DRAFT quiz. 400 once published.",
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("question_uid", "path", "string", True, "0198f2c0-2222-7aaa-8bbb-000000000002", "The question uid."),
|
|
field("kind", "form", "enum", True, "single_choice", "The question kind.", options=KIND_KEYS),
|
|
field("prompt", "form", "string", True, "Which journal mode allows concurrent readers?", "Markdown, up to 2000 characters."),
|
|
field("points", "form", "integer", False, "1", "1 to 100."),
|
|
field("explanation", "form", "string", False, "", "Shown after answering."),
|
|
field("options", "form", "string", False, "DELETE\nWAL\nMEMORY", "Option labels, one per line or comma separated."),
|
|
field("match_values", "form", "string", False, "", "Accepted answers aligned with the options."),
|
|
field("correct_indexes", "form", "string", True, "1", "0-based indexes of the correct options."),
|
|
field("correct_boolean", "form", "boolean", False, "1", "true_false only."),
|
|
field("expected_answer", "form", "string", False, "", "free_text only."),
|
|
field("grading_criteria", "form", "string", False, "", "free_text only."),
|
|
field("numeric_value", "form", "number", False, "0", "numeric only."),
|
|
field("numeric_tolerance", "form", "number", False, "0", "numeric only."),
|
|
field("case_sensitive", "form", "boolean", False, "0", "fill_blank only."),
|
|
],
|
|
sample_response={"ok": True, "redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit"},
|
|
),
|
|
endpoint(
|
|
id="quizzes-question-delete",
|
|
method="POST",
|
|
path="/quizzes/{slug}/questions/{question_uid}/delete",
|
|
title="Delete a question",
|
|
summary="Remove one question and its options from a DRAFT quiz, then renumber.",
|
|
auth="user",
|
|
encoding="form",
|
|
destructive=True,
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("question_uid", "path", "string", True, "0198f2c0-2222-7aaa-8bbb-000000000002", "The question uid."),
|
|
],
|
|
sample_response={"ok": True, "redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit"},
|
|
),
|
|
endpoint(
|
|
id="quizzes-question-reorder",
|
|
method="POST",
|
|
path="/quizzes/{slug}/questions/reorder",
|
|
title="Reorder the questions",
|
|
summary="Set a new question order on a DRAFT quiz. Every uid must be listed exactly once.",
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("order", "form", "string", True, "uid-b,uid-a,uid-c", "Every question uid in the wanted order, comma separated."),
|
|
],
|
|
sample_response={"ok": True, "redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/edit"},
|
|
),
|
|
endpoint(
|
|
id="quizzes-attempt-start",
|
|
method="POST",
|
|
path="/quizzes/{slug}/attempts",
|
|
title="Start or resume an attempt",
|
|
summary=(
|
|
"Returns the member's single in-progress attempt, creating it when there is "
|
|
"none. The question order and one blank answer row per question are "
|
|
"materialized at start."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
params=[field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid.")],
|
|
sample_response={
|
|
"ok": True,
|
|
"redirect": "/quizzes/8bbb000000000001-sqlite-fundamentals/attempts/0198f2c0-3333-7aaa-8bbb-000000000003",
|
|
"data": {"uid": "0198f2c0-3333-7aaa-8bbb-000000000003", "status": "in_progress"},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-attempt-get",
|
|
method="GET",
|
|
path="/quizzes/{slug}/attempts/{attempt_uid}",
|
|
title="Read an attempt",
|
|
summary=(
|
|
"The attempt with its questions in play order. Correct answers are withheld "
|
|
"until a question is answered and the quiz reveals answers."
|
|
),
|
|
auth="user",
|
|
negotiation=True,
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("attempt_uid", "path", "string", True, "0198f2c0-3333-7aaa-8bbb-000000000003", "The attempt uid."),
|
|
],
|
|
sample_response={
|
|
"quiz": SAMPLE_QUIZ,
|
|
"attempt": {
|
|
"uid": "0198f2c0-3333-7aaa-8bbb-000000000003",
|
|
"status": "in_progress",
|
|
"remaining_seconds": 812,
|
|
"answered_count": 2,
|
|
"question_count": 10,
|
|
"score_points": 2.0,
|
|
"max_points": 14,
|
|
"score_percent": 14.29,
|
|
"questions": [
|
|
{
|
|
"uid": "0198f2c0-2222-7aaa-8bbb-000000000002",
|
|
"kind": "single_choice",
|
|
"prompt": "Which journal mode allows concurrent readers?",
|
|
"points": 1,
|
|
"options": [{"uid": "opt-a", "label": "DELETE"}, {"uid": "opt-b", "label": "WAL"}],
|
|
}
|
|
],
|
|
},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-attempt-answer",
|
|
method="POST",
|
|
path="/quizzes/{slug}/attempts/{attempt_uid}/answer",
|
|
title="Answer a question",
|
|
summary=(
|
|
"Grade and record one answer. Each question can be answered exactly once; a "
|
|
"second submit answers 400 and credits nothing."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("attempt_uid", "path", "string", True, "0198f2c0-3333-7aaa-8bbb-000000000003", "The attempt uid."),
|
|
field("question_uid", "form", "string", True, "0198f2c0-2222-7aaa-8bbb-000000000002", "The question being answered."),
|
|
field("answer_text", "form", "string", False, "true", "Free text, the numeric value, or true/false."),
|
|
field("option_uids", "form", "string", False, "opt-b", "Chosen option uids, comma separated and in order for ordering."),
|
|
field("blanks", "form", "string", False, "WAL,NORMAL", "fill_blank only: one answer per blank, comma separated."),
|
|
field("matches", "form", "string", False, "one,two", "matching only: the chosen right-hand value per option_uid, in order."),
|
|
],
|
|
sample_response={
|
|
"ok": True,
|
|
"answer": {
|
|
"question_uid": "0198f2c0-2222-7aaa-8bbb-000000000002",
|
|
"answered": True,
|
|
"is_correct": True,
|
|
"awarded_points": 1.0,
|
|
"feedback": "Correct.",
|
|
"graded_by": "auto",
|
|
"confidence": 1.0,
|
|
},
|
|
"attempt": {"answered_count": 3, "score_points": 3.0, "max_points": 14},
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-attempt-finish",
|
|
method="POST",
|
|
path="/quizzes/{slug}/attempts/{attempt_uid}/finish",
|
|
title="Finish an attempt",
|
|
summary=(
|
|
"Close the attempt and compute the final score from its answer rows. A second "
|
|
"finish returns the same result and awards nothing again."
|
|
),
|
|
auth="user",
|
|
encoding="form",
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("attempt_uid", "path", "string", True, "0198f2c0-3333-7aaa-8bbb-000000000003", "The attempt uid."),
|
|
],
|
|
sample_response={
|
|
"quiz": SAMPLE_QUIZ,
|
|
"attempt": {"status": "completed", "score_points": 13.0, "max_points": 14,
|
|
"score_percent": 92.86, "passed": True},
|
|
"review": [],
|
|
"fallback_count": 0,
|
|
},
|
|
),
|
|
endpoint(
|
|
id="quizzes-attempt-results",
|
|
method="GET",
|
|
path="/quizzes/{slug}/attempts/{attempt_uid}/results",
|
|
title="Attempt results",
|
|
summary=(
|
|
"The result of one attempt: score, percentage, pass verdict, and the "
|
|
"per-question review when the author allowed it. Attempt owner or admin."
|
|
),
|
|
auth="user",
|
|
negotiation=True,
|
|
params=[
|
|
field("slug", "path", "string", True, SAMPLE_QUIZ["slug"], "Quiz slug or uid."),
|
|
field("attempt_uid", "path", "string", True, "0198f2c0-3333-7aaa-8bbb-000000000003", "The attempt uid."),
|
|
],
|
|
sample_response={
|
|
"quiz": SAMPLE_QUIZ,
|
|
"attempt": {"status": "completed", "score_percent": 92.86, "passed": True},
|
|
"review": [],
|
|
"fallback_count": 0,
|
|
},
|
|
),
|
|
],
|
|
}
|