# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from devplacepy.config import QUIZ_MAX_OPTIONS, QUIZ_MAX_QUESTIONS
from devplacepy.database import get_table
from devplacepy.models import QuizDocument, QuizImportForm
from devplacepy.services.quiz import store
from devplacepy.services.quiz.store import QuizError
from devplacepy.utils import generate_uid, make_combined_slug
_counter = [0]
DOCUMENT = {
"title": "SQLite fundamentals",
"description": "Questions on WAL and indexing.",
"settings": {"shuffle_questions": True, "reveal_answers": True, "pass_percent": 70},
"questions": [
{
"kind": "single_choice",
"prompt": "Which journal mode allows concurrent readers?",
"points": 2,
"explanation": "WAL keeps readers off the writer's lock.",
"options": [{"label": "DELETE"}, {"label": "WAL", "is_correct": True}],
},
{
"kind": "free_text",
"prompt": "Why does a partial index help?",
"points": 3,
"expected_answer": "It keeps the live query off a one bucket index.",
"grading_criteria": "Accept any answer mentioning the planner.",
},
],
}
def _user(prefix):
_counter[0] += 1
uid = generate_uid()
get_table("users").insert(
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return get_table("users").find_one(uid=uid)
def _quiz(owner, document):
uid = generate_uid()
get_table("quizzes").insert(
store.born_live(
{
"uid": uid,
"user_uid": owner["uid"],
"slug": make_combined_slug(document.title, uid),
"title": document.title,
"description": document.description,
"status": "draft",
"published_at": "",
"question_count": 0,
"total_points": 0,
"attempt_count": 0,
"stars": 0,
**store.document_settings(document),
}
)
)
store.import_questions(uid, document)
return store.get_quiz(uid)
def test_parse_document_accepts_the_reference_shape():
document = store.parse_document(DOCUMENT)
assert document.title == "SQLite fundamentals"
assert len(document.questions) == 2
def test_parse_document_rejects_junk():
with pytest.raises(QuizError):
store.parse_document({"title": "x"})
def test_the_import_form_parses_a_json_string():
import json
form = QuizImportForm(document=json.dumps(DOCUMENT))
assert form.document.title == "SQLite fundamentals"
def test_the_import_form_rejects_invalid_json():
with pytest.raises(ValidationError):
QuizImportForm(document="{not json")
def test_a_document_needs_at_least_one_question():
with pytest.raises(ValidationError):
QuizDocument(title="Empty quiz", questions=[])
def test_a_document_is_capped_at_the_question_limit():
question = {"kind": "numeric", "prompt": "q", "numeric_value": 1}
with pytest.raises(ValidationError):
QuizDocument(title="Too big", questions=[question] * (QUIZ_MAX_QUESTIONS + 1))
def test_a_question_is_capped_at_the_option_limit():
options = [{"label": f"L{index}"} for index in range(QUIZ_MAX_OPTIONS + 1)]
options[0]["is_correct"] = True
with pytest.raises(ValidationError):
QuizDocument(
title="Too many options",
questions=[{"kind": "single_choice", "prompt": "q", "options": options}],
)
def test_a_single_choice_needs_exactly_one_correct_option():
with pytest.raises(ValidationError):
QuizDocument(
title="Bad choice",
questions=[
{
"kind": "single_choice",
"prompt": "q",
"options": [{"label": "a"}, {"label": "b"}],
}
],
)
def test_a_free_text_needs_a_reference_or_criteria():
with pytest.raises(ValidationError):
QuizDocument(
title="Bad free text",
questions=[{"kind": "free_text", "prompt": "explain"}],
)
def test_a_matching_needs_a_right_hand_value_on_every_pair():
with pytest.raises(ValidationError):
QuizDocument(
title="Bad matching",
questions=[
{
"kind": "matching",
"prompt": "q",
"options": [{"label": "a", "match_value": "A"}, {"label": "b"}],
}
],
)
def test_document_settings_map_onto_the_quiz_columns():
settings = store.document_settings(store.parse_document(DOCUMENT))
assert settings["shuffle_questions"] == 1
assert settings["reveal_answers"] == 1
assert settings["pass_percent"] == 70
def test_importing_creates_every_question_in_order(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
questions = store.list_questions(quiz["uid"])
assert [q["kind"] for q in questions] == ["single_choice", "free_text"]
assert [q["position"] for q in questions] == [0, 1]
def test_importing_recomputes_the_totals(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
assert quiz["question_count"] == 2
assert quiz["total_points"] == 5
def test_importing_stores_the_kind_specific_fields(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
free_text = store.list_questions(quiz["uid"])[1]
assert free_text["expected_answer"].startswith("It keeps")
assert free_text["grading_criteria"].startswith("Accept")
def test_the_owner_export_round_trips_through_import(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
exported = store.export_document(quiz["uid"], True)
reimported = _quiz(owner, store.parse_document(exported))
assert reimported["question_count"] == quiz["question_count"]
assert reimported["total_points"] == quiz["total_points"]
assert store.export_document(reimported["uid"], True) == exported
def test_the_owner_export_carries_the_answer_key(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
exported = store.export_document(quiz["uid"], True)
assert any(option.get("is_correct") for option in exported["questions"][0]["options"])
assert exported["questions"][1]["expected_answer"]
def test_the_public_export_omits_the_answer_key(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
exported = store.export_document(quiz["uid"], False)
assert all("is_correct" not in option for option in exported["questions"][0]["options"])
assert "expected_answer" not in exported["questions"][1]
assert "correct_boolean" not in exported["questions"][0]
def test_the_public_export_keeps_the_prompts_and_labels(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
exported = store.export_document(quiz["uid"], False)
assert exported["questions"][0]["prompt"].startswith("Which journal mode")
assert [option["label"] for option in exported["questions"][0]["options"]] == ["DELETE", "WAL"]
def test_the_export_carries_the_settings(local_db):
owner = _user("qd")
quiz = _quiz(owner, store.parse_document(DOCUMENT))
exported = store.export_document(quiz["uid"], False)
assert exported["settings"]["pass_percent"] == 70
assert exported["settings"]["reveal_answers"] is True
def test_exporting_an_unknown_quiz_raises(local_db):
with pytest.raises(QuizError):
store.export_document("does-not-exist", True)