Initial commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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
|
||||
Reference in New Issue
Block a user