elemes/services/evaluation.py

49 lines
2.0 KiB
Python

"""Composite score computation for lesson evaluation.
Logic:
- Exercise is binary (passed/failed). Quiz scored as evalCorrect/evalTotal * 100.
- Composite = exercise_pct * (EXERCISE_WEIGHT/100) + quiz_pct * (QUIZ_WEIGHT/100).
- "Done": composite >= LESSON_DONE_MIN_PERCENT (global env var).
- Implicit AND: if quiz not attempted → quiz term contributes 0 → composite < threshold.
If exercise not passed → exercise term contributes 0 → composite < threshold.
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class CompositeResult:
composite_percent: float
is_done: bool
def compute_composite(
*,
exercise_passed: bool | None,
quiz_earned: int | None,
quiz_total: int | None,
has_exercise: bool,
has_quiz: bool,
exercise_weight: float,
quiz_weight: float,
done_min_percent: float,
) -> CompositeResult:
# Reading-only lesson (no exercise, no quiz) — auto-done
if not has_exercise and not has_quiz:
return CompositeResult(composite_percent=100.0, is_done=True)
# Exercise-only lesson
if has_exercise and not has_quiz:
pct = 100.0 if exercise_passed is True else 0.0
return CompositeResult(composite_percent=pct, is_done=exercise_passed is True and pct >= done_min_percent)
# Quiz-only lesson
if not has_exercise and has_quiz:
if quiz_earned is None or not quiz_total:
return CompositeResult(composite_percent=0.0, is_done=False)
pct = (quiz_earned / quiz_total) * 100.0
return CompositeResult(composite_percent=round(pct, 2), is_done=round(pct, 2) >= done_min_percent)
# Lesson has BOTH exercise AND quiz
ex_pct = 100.0 if exercise_passed is True else 0.0
quiz_pct = (quiz_earned / quiz_total) * 100.0 if (quiz_earned is not None and quiz_total) else 0.0
composite = round(ex_pct * (exercise_weight / 100.0) + quiz_pct * (quiz_weight / 100.0), 2)
return CompositeResult(composite_percent=composite, is_done=composite >= done_min_percent)