131 lines
4.7 KiB
Python
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
|