389 lines
12 KiB
Python
Raw Normal View History

2026-07-26 14:57:18 +02:00
# 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]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
2026-07-26 14:57:18 +02:00
"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