Files
testpoint/accounts/leaderboard.py
T
2026-07-29 10:47:40 +00:00

131 lines
4.7 KiB
Python

from datetime import timedelta
from decimal import Decimal, ROUND_HALF_UP
from django.contrib.auth.models import User
from django.utils import timezone
from .models import StudentProfile
DEMO_STUDENTS = (
{"name": "Alex", "band": 8.5, "tests": 12, "country": "United Kingdom", "trend": "up"},
{"name": "Sarah", "band": 8.0, "tests": 10, "country": "Australia", "trend": "up"},
{"name": "John", "band": 8.0, "tests": 15, "country": "Canada", "trend": "same"},
{"name": "Emma", "band": 7.5, "tests": 8, "country": "United Kingdom", "trend": "up"},
{"name": "Daniel", "band": 7.5, "tests": 11, "country": "Germany", "trend": "down"},
{"name": "Mina", "band": 7.0, "tests": 9, "country": "South Korea", "trend": "up"},
{"name": "Omar", "band": 7.0, "tests": 7, "country": "United Arab Emirates", "trend": "same"},
{"name": "Layla", "band": 6.5, "tests": 13, "country": "Turkey", "trend": "up"},
)
def _rounded_half(value):
return float(
(Decimal(str(value)) * 2).quantize(Decimal("1"), rounding=ROUND_HALF_UP) / 2
)
def _objective_band(correct, total):
if not total:
return None
percentage = correct / total
boundaries = (
(0.90, 9.0), (0.85, 8.5), (0.80, 8.0), (0.75, 7.5),
(0.68, 7.0), (0.60, 6.5), (0.52, 6.0), (0.45, 5.5),
(0.38, 5.0), (0.30, 4.5), (0.22, 4.0), (0, 3.5),
)
return next(band for minimum, band in boundaries if percentage >= minimum)
def _attempt_band(attempt, skill="overall"):
answers = list(attempt.answers.all())
if skill != "overall":
answers = [
answer
for answer in answers
if answer.question.section.section_type == skill
]
manual = [answer.manual_score for answer in answers if answer.manual_score is not None]
objective = [answer for answer in answers if answer.is_correct is not None]
components = []
if objective:
components.append(
_objective_band(sum(answer.is_correct for answer in objective), len(objective))
)
if manual:
components.append(sum(manual) / len(manual))
return _rounded_half(sum(components) / len(components)) if components else None
def _period_start(period):
today = timezone.localdate()
if period == "week":
return today - timedelta(days=today.weekday())
if period == "month":
return today.replace(day=1)
return None
def build_leaderboard(period="week", skill="overall", country="", current_user=None):
start = _period_start(period)
users = User.objects.filter(studentattempt__is_complete=True).distinct().select_related(
"studentprofile"
).prefetch_related(
"studentattempt_set__answers__question__section"
)
rows = []
for user in users:
attempts = [
attempt
for attempt in user.studentattempt_set.all()
if attempt.is_complete
and attempt.submitted_at
and (not start or timezone.localdate(attempt.submitted_at) >= start)
]
bands = [
band for attempt in attempts if (band := _attempt_band(attempt, skill)) is not None
]
if not bands:
continue
try:
profile = user.studentprofile
except StudentProfile.DoesNotExist:
profile, _ = StudentProfile.objects.get_or_create(user=user)
user_country = profile.country or "Not set"
if country and user_country != country:
continue
rows.append(
{
"user_id": user.id,
"name": profile.display_name
or user.get_full_name()
or user.username,
"band": _rounded_half(sum(bands) / len(bands)),
"tests": len(bands),
"country": user_country,
"trend": "up",
"demo": False,
}
)
if len(rows) < 8:
used_names = {row["name"].casefold() for row in rows}
for demo in DEMO_STUDENTS:
if demo["name"].casefold() not in used_names and (
not country or demo["country"] == country
):
rows.append({**demo, "user_id": None, "demo": True})
rows.sort(key=lambda row: (-row["band"], -row["tests"], row["name"].casefold()))
for index, row in enumerate(rows, start=1):
row["rank"] = index
row["initial"] = row["name"][:1].upper()
row["is_current"] = bool(
current_user and row["user_id"] == current_user.id
)
current_row = next((row for row in rows if row["is_current"]), None)
countries = sorted(
{row["country"] for row in rows if row["country"] != "Not set"}
)
return rows, current_row, countries