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