532 lines
18 KiB
Python
532 lines
18 KiB
Python
|
|
# retoor <retoor@molodetz.nl>
|
||
|
|
|
||
|
|
import json
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from devplacepy.database import get_table
|
||
|
|
from devplacepy.services.quiz import store
|
||
|
|
from devplacepy.services.quiz.store import QuizError
|
||
|
|
from devplacepy.utils import generate_uid, make_combined_slug
|
||
|
|
from tests.conftest import run_async
|
||
|
|
|
||
|
|
_counter = [0]
|
||
|
|
|
||
|
|
|
||
|
|
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, **overrides):
|
||
|
|
uid = generate_uid()
|
||
|
|
fields = {
|
||
|
|
"uid": uid,
|
||
|
|
"user_uid": owner["uid"],
|
||
|
|
"slug": make_combined_slug("Quiz", uid),
|
||
|
|
"title": "Quiz",
|
||
|
|
"description": "d",
|
||
|
|
"status": "draft",
|
||
|
|
"published_at": "",
|
||
|
|
"shuffle_questions": 0,
|
||
|
|
"shuffle_options": 0,
|
||
|
|
"reveal_answers": 1,
|
||
|
|
"allow_review": 1,
|
||
|
|
"time_limit_seconds": 0,
|
||
|
|
"pass_percent": 0,
|
||
|
|
"question_count": 0,
|
||
|
|
"total_points": 0,
|
||
|
|
"attempt_count": 0,
|
||
|
|
"stars": 0,
|
||
|
|
}
|
||
|
|
fields.update(overrides)
|
||
|
|
get_table("quizzes").insert(store.born_live(fields))
|
||
|
|
return store.get_quiz(uid)
|
||
|
|
|
||
|
|
|
||
|
|
def _numeric(quiz_uid, value=1.0, points=1):
|
||
|
|
return store.add_question(
|
||
|
|
quiz_uid,
|
||
|
|
{
|
||
|
|
"kind": "numeric",
|
||
|
|
"prompt": f"Value {value}",
|
||
|
|
"points": points,
|
||
|
|
"numeric_value": value,
|
||
|
|
"numeric_tolerance": 0.0,
|
||
|
|
"options": [],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _choice(quiz_uid, points=2):
|
||
|
|
return store.add_question(
|
||
|
|
quiz_uid,
|
||
|
|
{
|
||
|
|
"kind": "single_choice",
|
||
|
|
"prompt": "Pick one",
|
||
|
|
"points": points,
|
||
|
|
"options": [
|
||
|
|
{"label": "wrong", "match_value": "", "is_correct": False},
|
||
|
|
{"label": "right", "match_value": "", "is_correct": True},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_recompute_quiz_totals_reflects_the_live_questions(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_numeric(quiz["uid"], points=3)
|
||
|
|
_choice(quiz["uid"], points=2)
|
||
|
|
refreshed = store.get_quiz(quiz["uid"])
|
||
|
|
assert refreshed["question_count"] == 2
|
||
|
|
assert refreshed["total_points"] == 5
|
||
|
|
|
||
|
|
|
||
|
|
def test_deleting_a_question_recomputes_and_renumbers(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
first = _numeric(quiz["uid"], points=3)
|
||
|
|
_numeric(quiz["uid"], value=2.0, points=4)
|
||
|
|
store.delete_question(quiz["uid"], first["uid"], owner["uid"])
|
||
|
|
refreshed = store.get_quiz(quiz["uid"])
|
||
|
|
assert refreshed["question_count"] == 1
|
||
|
|
assert refreshed["total_points"] == 4
|
||
|
|
assert store.list_questions(quiz["uid"])[0]["position"] == 0
|
||
|
|
|
||
|
|
|
||
|
|
def test_reorder_requires_every_question_exactly_once(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
first = _numeric(quiz["uid"])
|
||
|
|
_numeric(quiz["uid"], value=2.0)
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.reorder_questions(quiz["uid"], [first["uid"]])
|
||
|
|
|
||
|
|
|
||
|
|
def test_reorder_applies_the_new_order(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
first = _numeric(quiz["uid"])
|
||
|
|
second = _numeric(quiz["uid"], value=2.0)
|
||
|
|
store.reorder_questions(quiz["uid"], [second["uid"], first["uid"]])
|
||
|
|
assert [q["uid"] for q in store.list_questions(quiz["uid"])] == [second["uid"], first["uid"]]
|
||
|
|
|
||
|
|
|
||
|
|
def test_validation_errors_flags_an_empty_quiz(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
assert store.validation_errors(quiz["uid"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_validation_errors_is_empty_for_a_complete_quiz(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
assert store.validation_errors(quiz["uid"]) == []
|
||
|
|
|
||
|
|
|
||
|
|
def test_validation_errors_flags_a_free_text_without_criteria(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
store.add_question(
|
||
|
|
quiz["uid"],
|
||
|
|
{"kind": "free_text", "prompt": "Explain", "points": 1, "options": []},
|
||
|
|
)
|
||
|
|
assert any("reference answer" in problem for problem in store.validation_errors(quiz["uid"]))
|
||
|
|
|
||
|
|
|
||
|
|
def test_validation_errors_flags_a_matching_with_one_pair(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
store.add_question(
|
||
|
|
quiz["uid"],
|
||
|
|
{
|
||
|
|
"kind": "matching",
|
||
|
|
"prompt": "Match",
|
||
|
|
"points": 1,
|
||
|
|
"options": [{"label": "a", "match_value": "b", "is_correct": False}],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assert any("two pairs" in problem for problem in store.validation_errors(quiz["uid"]))
|
||
|
|
|
||
|
|
|
||
|
|
def test_publish_refuses_an_incomplete_quiz(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
assert store.get_quiz(quiz["uid"])["status"] == "draft"
|
||
|
|
|
||
|
|
|
||
|
|
def test_publish_sets_the_status_once(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
published = store.publish_quiz(quiz["uid"])
|
||
|
|
assert published["status"] == "published"
|
||
|
|
assert published["published_at"]
|
||
|
|
assert published["publish_won"] is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_publishing_twice_does_not_win_twice(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
again = store.publish_quiz(quiz["uid"])
|
||
|
|
assert again.get("publish_won") is not True
|
||
|
|
|
||
|
|
|
||
|
|
def test_guard_editable_refuses_a_published_quiz(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.guard_editable(quiz["uid"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_every_mutating_entrypoint_is_frozen_after_publish(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
question = _choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.add_question(quiz["uid"], {"kind": "numeric", "prompt": "x", "options": []})
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.edit_question(quiz["uid"], question["uid"], {"kind": "numeric", "prompt": "x", "options": []})
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.delete_question(quiz["uid"], question["uid"], owner["uid"])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.reorder_questions(quiz["uid"], [question["uid"]])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.edit_quiz(quiz["uid"], {"title": "new"})
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_published_quiz_keeps_its_question_bytes(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
question = _choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
store.edit_question(
|
||
|
|
quiz["uid"], question["uid"], {"kind": "numeric", "prompt": "changed", "options": []}
|
||
|
|
)
|
||
|
|
assert store.list_questions(quiz["uid"])[0]["prompt"] == "Pick one"
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_view_quiz_hides_a_draft_from_everyone_else(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
other = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
assert store.can_view_quiz(quiz, owner) is True
|
||
|
|
assert store.can_view_quiz(quiz, other) is False
|
||
|
|
assert store.can_view_quiz(quiz, None) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_view_quiz_allows_everyone_once_published(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
other = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
assert store.can_view_quiz(store.get_quiz(quiz["uid"]), other) is True
|
||
|
|
assert store.can_view_quiz(store.get_quiz(quiz["uid"]), None) is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_starting_twice_returns_the_same_attempt(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
first = store.start_attempt(player, quiz)
|
||
|
|
second = store.start_attempt(player, quiz)
|
||
|
|
assert first["uid"] == second["uid"]
|
||
|
|
live = get_table("quiz_attempts").count(
|
||
|
|
user_uid=player["uid"], quiz_uid=quiz["uid"], status="in_progress", deleted_at=None
|
||
|
|
)
|
||
|
|
assert live == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_starting_materializes_a_blank_answer_per_question(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
_numeric(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, store.get_quiz(quiz["uid"]))
|
||
|
|
answers = store.answers_for(attempt["uid"])
|
||
|
|
assert len(answers) == 2
|
||
|
|
assert all(not answer["answered_at"] for answer in answers)
|
||
|
|
|
||
|
|
|
||
|
|
def test_starting_snapshots_the_max_points(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"], points=5)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, store.get_quiz(quiz["uid"]))
|
||
|
|
assert attempt["max_points"] == 5
|
||
|
|
|
||
|
|
|
||
|
|
def test_answering_credits_the_attempt(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
question = _numeric(quiz["uid"], value=7.0, points=4)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
answer, updated, result = run_async(
|
||
|
|
store.answer(
|
||
|
|
player, quiz, attempt, question["uid"], {"answer_text": "7", "option_uids": []}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
assert answer["is_correct"] == 1
|
||
|
|
assert float(answer["awarded_points"]) == 4.0
|
||
|
|
assert float(updated["score_points"]) == 4.0
|
||
|
|
assert int(updated["answered_count"]) == 1
|
||
|
|
assert result.graded_by == "auto"
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_second_answer_is_refused_and_credits_nothing(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
question = _numeric(quiz["uid"], value=7.0, points=4)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
run_async(
|
||
|
|
store.answer(
|
||
|
|
player, quiz, attempt, question["uid"], {"answer_text": "7", "option_uids": []}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
run_async(
|
||
|
|
store.answer(
|
||
|
|
player,
|
||
|
|
quiz,
|
||
|
|
store.get_attempt(attempt["uid"]),
|
||
|
|
question["uid"],
|
||
|
|
{"answer_text": "7", "option_uids": []},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
refreshed = store.get_attempt(attempt["uid"])
|
||
|
|
assert float(refreshed["score_points"]) == 4.0
|
||
|
|
assert int(refreshed["answered_count"]) == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_finishing_recomputes_the_score_from_the_answers(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, pass_percent=50)
|
||
|
|
first = _numeric(quiz["uid"], value=1.0, points=2)
|
||
|
|
_numeric(quiz["uid"], value=2.0, points=2)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
run_async(
|
||
|
|
store.answer(
|
||
|
|
player, quiz, attempt, first["uid"], {"answer_text": "1", "option_uids": []}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
finished, won = store.finish(quiz, store.get_attempt(attempt["uid"]))
|
||
|
|
assert won is True
|
||
|
|
assert finished["status"] == "completed"
|
||
|
|
assert float(finished["score_points"]) == 2.0
|
||
|
|
assert float(finished["score_percent"]) == 50.0
|
||
|
|
assert int(finished["passed"]) == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_finishing_twice_wins_only_once(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
store.finish(quiz, attempt)
|
||
|
|
_, won = store.finish(quiz, store.get_attempt(attempt["uid"]))
|
||
|
|
assert won is False
|
||
|
|
assert int(store.get_quiz(quiz["uid"])["attempt_count"]) == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_an_expired_attempt_refuses_further_answers(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, time_limit_seconds=60)
|
||
|
|
question = _numeric(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
past = (datetime.now(timezone.utc) - timedelta(seconds=10)).isoformat()
|
||
|
|
get_table("quiz_attempts").update({"uid": attempt["uid"], "expires_at": past}, ["uid"])
|
||
|
|
with pytest.raises(QuizError):
|
||
|
|
run_async(
|
||
|
|
store.answer(
|
||
|
|
player,
|
||
|
|
quiz,
|
||
|
|
store.get_attempt(attempt["uid"]),
|
||
|
|
question["uid"],
|
||
|
|
{"answer_text": "1", "option_uids": []},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
assert store.get_attempt(attempt["uid"])["status"] == "expired"
|
||
|
|
|
||
|
|
|
||
|
|
def test_expiry_happens_lazily_on_a_read(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, time_limit_seconds=60)
|
||
|
|
_numeric(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
past = (datetime.now(timezone.utc) - timedelta(seconds=10)).isoformat()
|
||
|
|
get_table("quiz_attempts").update({"uid": attempt["uid"], "expires_at": past}, ["uid"])
|
||
|
|
serialized = store.serialize_attempt(quiz, store.get_attempt(attempt["uid"]), player)
|
||
|
|
assert serialized["status"] == "expired"
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_serializer_withholds_the_answer_key_from_a_player(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, reveal_answers=0)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
payload = store.serialize_attempt(quiz, attempt, player)
|
||
|
|
question = payload["questions"][0]
|
||
|
|
assert "correct_boolean" not in question
|
||
|
|
assert "expected_answer" not in question
|
||
|
|
assert all("is_correct" not in option for option in question["options"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_serializer_gives_the_owner_the_answer_key(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
quiz = _quiz(owner, reveal_answers=0)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(owner, quiz)
|
||
|
|
payload = store.serialize_attempt(quiz, attempt, owner)
|
||
|
|
question = payload["questions"][0]
|
||
|
|
assert any("is_correct" in option for option in question["options"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_serializer_reveals_an_answered_question_when_configured(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, reveal_answers=1)
|
||
|
|
question = _numeric(quiz["uid"], value=3.0)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
run_async(
|
||
|
|
store.answer(
|
||
|
|
player, quiz, attempt, question["uid"], {"answer_text": "3", "option_uids": []}
|
||
|
|
)
|
||
|
|
)
|
||
|
|
payload = store.serialize_attempt(quiz, store.get_attempt(attempt["uid"]), player)
|
||
|
|
assert "numeric_value" in payload["questions"][0]
|
||
|
|
|
||
|
|
|
||
|
|
def test_matching_choices_are_exposed_without_the_pairing(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, reveal_answers=0)
|
||
|
|
store.add_question(
|
||
|
|
quiz["uid"],
|
||
|
|
{
|
||
|
|
"kind": "matching",
|
||
|
|
"prompt": "Match",
|
||
|
|
"points": 1,
|
||
|
|
"options": [
|
||
|
|
{"label": "a", "match_value": "A", "is_correct": False},
|
||
|
|
{"label": "b", "match_value": "B", "is_correct": False},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, quiz)
|
||
|
|
question = store.serialize_attempt(quiz, attempt, player)["questions"][0]
|
||
|
|
assert sorted(question["match_choices"]) == ["A", "B"]
|
||
|
|
assert all("match_value" not in option for option in question["options"])
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_cascade_removes_every_child_under_one_stamp(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
store.start_attempt(player, store.get_quiz(quiz["uid"]))
|
||
|
|
stamp = datetime.now(timezone.utc).isoformat()
|
||
|
|
store.cascade_questions(quiz["uid"], owner["uid"], stamp)
|
||
|
|
for table in ("quiz_questions", "quiz_options", "quiz_attempts", "quiz_answers"):
|
||
|
|
live = get_table(table).count(quiz_uid=quiz["uid"], deleted_at=None)
|
||
|
|
stamped = get_table(table).count(quiz_uid=quiz["uid"], deleted_at=stamp)
|
||
|
|
assert live == 0
|
||
|
|
assert stamped > 0
|
||
|
|
|
||
|
|
|
||
|
|
def test_prune_attempts_keeps_completed_records(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
quiz = store.get_quiz(quiz["uid"])
|
||
|
|
kept = store.start_attempt(player, quiz)
|
||
|
|
store.finish(quiz, kept)
|
||
|
|
future = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()
|
||
|
|
store.prune_attempts(future)
|
||
|
|
assert store.get_attempt(kept["uid"]) is not None
|
||
|
|
|
||
|
|
|
||
|
|
def test_prune_attempts_removes_an_abandoned_one(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner)
|
||
|
|
_choice(quiz["uid"])
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, store.get_quiz(quiz["uid"]))
|
||
|
|
future = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()
|
||
|
|
store.prune_attempts(future)
|
||
|
|
assert store.get_attempt(attempt["uid"]) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_attempt_order_is_persisted_as_json(local_db):
|
||
|
|
owner = _user("qs")
|
||
|
|
player = _user("qs")
|
||
|
|
quiz = _quiz(owner, shuffle_questions=1)
|
||
|
|
_numeric(quiz["uid"], value=1.0)
|
||
|
|
_numeric(quiz["uid"], value=2.0)
|
||
|
|
_numeric(quiz["uid"], value=3.0)
|
||
|
|
store.publish_quiz(quiz["uid"])
|
||
|
|
attempt = store.start_attempt(player, store.get_quiz(quiz["uid"]))
|
||
|
|
order = json.loads(attempt["question_order"])
|
||
|
|
assert sorted(order) == sorted(store.question_uids(quiz["uid"]))
|
||
|
|
assert store.attempt_order(store.get_attempt(attempt["uid"])) == order
|