|
# retoor <retoor@molodetz.nl>
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from devplacepy.database import get_table
|
|
from devplacepy.services.quiz import store
|
|
from devplacepy.utils import generate_uid, make_combined_slug
|
|
|
|
_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 _published(owner, points=10):
|
|
uid = generate_uid()
|
|
get_table("quizzes").insert(
|
|
store.born_live(
|
|
{
|
|
"uid": uid,
|
|
"user_uid": owner["uid"],
|
|
"slug": make_combined_slug("Board", uid),
|
|
"title": "Board",
|
|
"description": "",
|
|
"status": "published",
|
|
"published_at": datetime.now(timezone.utc).isoformat(),
|
|
"shuffle_questions": 0,
|
|
"shuffle_options": 0,
|
|
"reveal_answers": 0,
|
|
"allow_review": 1,
|
|
"time_limit_seconds": 0,
|
|
"pass_percent": 0,
|
|
"question_count": 1,
|
|
"total_points": points,
|
|
"attempt_count": 0,
|
|
"stars": 0,
|
|
}
|
|
)
|
|
)
|
|
return store.get_quiz(uid)
|
|
|
|
|
|
def _completed(player, quiz, points, percent):
|
|
uid = generate_uid()
|
|
get_table("quiz_attempts").insert(
|
|
store.born_live(
|
|
{
|
|
"uid": uid,
|
|
"quiz_uid": quiz["uid"],
|
|
"user_uid": player["uid"],
|
|
"status": "completed",
|
|
"question_order": "[]",
|
|
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
"expires_at": "",
|
|
"completed_at": datetime.now(timezone.utc).isoformat(),
|
|
"answered_count": 1,
|
|
"score_points": float(points),
|
|
"max_points": int(quiz["total_points"]),
|
|
"score_percent": float(percent),
|
|
"passed": 0,
|
|
}
|
|
)
|
|
)
|
|
store.clear_cache()
|
|
return uid
|
|
|
|
|
|
def _in_progress(player, quiz):
|
|
uid = generate_uid()
|
|
get_table("quiz_attempts").insert(
|
|
store.born_live(
|
|
{
|
|
"uid": uid,
|
|
"quiz_uid": quiz["uid"],
|
|
"user_uid": player["uid"],
|
|
"status": "in_progress",
|
|
"question_order": "[]",
|
|
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
"expires_at": "",
|
|
"completed_at": "",
|
|
"answered_count": 0,
|
|
"score_points": 0.0,
|
|
"max_points": int(quiz["total_points"]),
|
|
"score_percent": 0.0,
|
|
"passed": 0,
|
|
}
|
|
)
|
|
)
|
|
store.clear_cache()
|
|
return uid
|
|
|
|
|
|
def _entry_for(player):
|
|
for entry in store.scoreboard(100):
|
|
if entry["user"]["uid"] == player["uid"]:
|
|
return entry
|
|
return None
|
|
|
|
|
|
def test_a_completed_attempt_puts_a_member_on_the_board(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 8, 80.0)
|
|
entry = _entry_for(player)
|
|
assert entry["total_points"] == 8.0
|
|
assert entry["quizzes_completed"] == 1
|
|
|
|
|
|
def test_only_the_best_attempt_per_quiz_counts(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 4, 40.0)
|
|
_completed(player, quiz, 9, 90.0)
|
|
_completed(player, quiz, 2, 20.0)
|
|
entry = _entry_for(player)
|
|
assert entry["total_points"] == 9.0
|
|
assert entry["quizzes_completed"] == 1
|
|
|
|
|
|
def test_replaying_a_quiz_can_never_multiply_a_score(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 7, 70.0)
|
|
before = _entry_for(player)["total_points"]
|
|
for _ in range(5):
|
|
_completed(player, quiz, 7, 70.0)
|
|
assert _entry_for(player)["total_points"] == before
|
|
|
|
|
|
def test_a_worse_attempt_never_lowers_the_contribution(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 9, 90.0)
|
|
_completed(player, quiz, 1, 10.0)
|
|
assert _entry_for(player)["total_points"] == 9.0
|
|
|
|
|
|
def test_a_better_attempt_raises_by_exactly_the_difference(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 4, 40.0)
|
|
_completed(player, quiz, 10, 100.0)
|
|
assert _entry_for(player)["total_points"] == 10.0
|
|
|
|
|
|
def test_two_quizzes_sum_their_best_attempts(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
first = _published(owner)
|
|
second = _published(owner)
|
|
_completed(player, first, 6, 60.0)
|
|
_completed(player, second, 3, 30.0)
|
|
entry = _entry_for(player)
|
|
assert entry["total_points"] == 9.0
|
|
assert entry["quizzes_completed"] == 2
|
|
|
|
|
|
def test_an_author_scores_on_their_own_quiz(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(owner, quiz, 10, 100.0)
|
|
entry = _entry_for(owner)
|
|
assert entry["total_points"] == 10.0
|
|
assert entry["quizzes_completed"] == 1
|
|
assert entry["perfect_count"] == 1
|
|
|
|
|
|
def test_an_in_progress_attempt_is_not_counted(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_in_progress(player, quiz)
|
|
assert _entry_for(player) is None
|
|
|
|
|
|
def test_a_draft_quiz_is_not_counted(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
get_table("quizzes").update({"uid": quiz["uid"], "status": "draft"}, ["uid"])
|
|
_completed(player, quiz, 10, 100.0)
|
|
assert _entry_for(player) is None
|
|
|
|
|
|
def test_a_deleted_attempt_is_not_counted(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
attempt_uid = _completed(player, quiz, 10, 100.0)
|
|
get_table("quiz_attempts").update(
|
|
{"uid": attempt_uid, "deleted_at": datetime.now(timezone.utc).isoformat()}, ["uid"]
|
|
)
|
|
store.clear_cache()
|
|
assert _entry_for(player) is None
|
|
|
|
|
|
def test_ranks_are_a_gapless_total_order(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
for score in (3, 9, 6):
|
|
_completed(_user("sb"), quiz, score, score * 10.0)
|
|
ranks = [entry["rank"] for entry in store.scoreboard(100)]
|
|
assert ranks == list(range(1, len(ranks) + 1))
|
|
|
|
|
|
def test_equal_totals_resolve_deterministically(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
for _ in range(3):
|
|
_completed(_user("sb"), quiz, 5, 50.0)
|
|
first = [entry["user"]["uid"] for entry in store.scoreboard(100)]
|
|
store.clear_cache()
|
|
assert [entry["user"]["uid"] for entry in store.scoreboard(100)] == first
|
|
|
|
|
|
def test_the_limit_bounds_the_board(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
for score in range(1, 6):
|
|
_completed(_user("sb"), quiz, score, score * 10.0)
|
|
assert len(store.scoreboard(3)) == 3
|
|
|
|
|
|
def test_perfect_attempts_are_counted(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 10, 100.0)
|
|
assert _entry_for(player)["perfect_count"] == 1
|
|
|
|
|
|
def test_standing_for_finds_a_member_outside_the_top(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 2, 20.0)
|
|
standing = store.standing_for(player["uid"])
|
|
assert standing["user"]["uid"] == player["uid"]
|
|
assert standing["rank"] >= 1
|
|
|
|
|
|
def test_standing_for_is_none_without_attempts(local_db):
|
|
assert store.standing_for(_user("sb")["uid"]) is None
|
|
|
|
|
|
def test_attempt_states_reports_done_with_the_best_percentage(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 4, 40.0)
|
|
_completed(player, quiz, 9, 90.0)
|
|
states = store.attempt_states_for(player["uid"], [quiz["uid"]])
|
|
assert states[quiz["uid"]]["state"] == "done"
|
|
assert states[quiz["uid"]]["best_percent"] == 90.0
|
|
|
|
|
|
def test_attempt_states_reports_in_progress(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_in_progress(player, quiz)
|
|
states = store.attempt_states_for(player["uid"], [quiz["uid"]])
|
|
assert states[quiz["uid"]]["state"] == "in_progress"
|
|
assert states[quiz["uid"]]["attempt_uid"]
|
|
|
|
|
|
def test_attempt_states_is_empty_for_a_guest(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
assert store.attempt_states_for("", [quiz["uid"]]) == {}
|
|
|
|
|
|
def test_attempt_states_is_empty_without_quiz_uids(local_db):
|
|
assert store.attempt_states_for(_user("sb")["uid"], []) == {}
|
|
|
|
|
|
def test_completed_quiz_uids_lists_only_finished_quizzes(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
done = _published(owner)
|
|
open_quiz = _published(owner)
|
|
_completed(player, done, 5, 50.0)
|
|
_in_progress(player, open_quiz)
|
|
completed = store.completed_quiz_uids(player["uid"])
|
|
assert done["uid"] in completed
|
|
assert open_quiz["uid"] not in completed
|
|
|
|
|
|
def test_progress_for_summarises_the_viewer(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 10, 100.0)
|
|
progress = store.progress_for(player["uid"])
|
|
assert progress["completed"] == 1
|
|
assert progress["avg_percent"] == 100.0
|
|
assert progress["perfect_count"] == 1
|
|
|
|
|
|
def test_the_quiz_leaderboard_ranks_by_percentage(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(_user("sb"), quiz, 3, 30.0)
|
|
_completed(_user("sb"), quiz, 9, 90.0)
|
|
entries = store.quiz_leaderboard(quiz["uid"])
|
|
assert entries[0]["score_percent"] == 90.0
|
|
assert entries[0]["rank"] == 1
|
|
|
|
|
|
def test_the_quiz_leaderboard_ignores_unfinished_attempts(local_db):
|
|
owner = _user("sb")
|
|
quiz = _published(owner)
|
|
_in_progress(_user("sb"), quiz)
|
|
assert store.quiz_leaderboard(quiz["uid"]) == []
|
|
|
|
|
|
def test_progress_agrees_with_the_scoreboard_for_an_author_only_completion(local_db):
|
|
author = _user("sb")
|
|
quiz = _published(author)
|
|
_completed(author, quiz, 10, 100.0)
|
|
progress = store.progress_for(author["uid"])
|
|
entry = _entry_for(author)
|
|
assert progress["completed"] == entry["quizzes_completed"] == 1
|
|
assert progress["avg_percent"] == entry["avg_percent"] == 100.0
|
|
assert progress["total_points"] == entry["total_points"] == 10.0
|
|
assert progress["perfect_count"] == entry["perfect_count"] == 1
|
|
assert progress["rank"] == entry["rank"]
|
|
|
|
|
|
def test_progress_counts_a_ranked_completion(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
_completed(player, quiz, 8, 80.0)
|
|
progress = store.progress_for(player["uid"])
|
|
entry = _entry_for(player)
|
|
assert progress["completed"] == entry["quizzes_completed"] == 1
|
|
assert progress["avg_percent"] == entry["avg_percent"] == 80.0
|
|
assert progress["total_points"] == entry["total_points"] == 8.0
|
|
|
|
|
|
def test_completing_your_own_quiz_clears_it_from_todo(local_db):
|
|
author = _user("sb")
|
|
before = store.progress_for(author["uid"])["todo"]
|
|
quiz = _published(author)
|
|
assert store.progress_for(author["uid"])["todo"] == before + 1
|
|
_completed(author, quiz, 10, 100.0)
|
|
assert store.progress_for(author["uid"])["todo"] == before
|
|
|
|
|
|
def test_progress_todo_counts_every_published_quiz(local_db):
|
|
author = _user("sb")
|
|
other = _user("sb")
|
|
before = store.progress_for(author["uid"])["todo"]
|
|
_published(author)
|
|
_published(other)
|
|
_published(other)
|
|
assert store.progress_for(author["uid"])["todo"] == before + 3
|
|
|
|
|
|
def test_completing_a_quiz_moves_it_from_todo_to_completed(local_db):
|
|
owner = _user("sb")
|
|
player = _user("sb")
|
|
quiz = _published(owner)
|
|
before = store.progress_for(player["uid"])
|
|
_completed(player, quiz, 5, 50.0)
|
|
after = store.progress_for(player["uid"])
|
|
assert after["completed"] == before["completed"] + 1
|
|
assert after["todo"] == before["todo"] - 1
|