File size: 6,885 Bytes
21b042a 82cae90 21b042a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """A class's progress for its teacher (PRD TCH-7, TCH-10): per lesson, never per skill, with the periods and the readiness per
student. Nothing is stored: it is all read from the students' progress, quizzes and tests, the lessons' dates and the bank."""
from __future__ import annotations
from datetime import date, datetime
from fastapi import APIRouter, Depends, HTTPException, Request
from . import attempts, clock, readiness, reads
from .access import need
from .learning import LEARNING, MASTERED, NOT_STARTED, skill_state
from .realtests import _my_class, facts_for, prime_students, readiness_for
from .store import _when
router = APIRouter(prefix="/api/classes")
MONTHS = {"ספטמבר": 9, "אוקטובר": 10, "נובמבר": 11, "דצמבר": 12, "ינואר": 1, "פברואר": 2, "מרץ": 3, "אפריל": 4, "מאי": 5, "יוני": 6}
PERIODS = ({"id": 1, "label": "ספט׳–דצמ׳"}, {"id": 2, "label": "ינו׳–יוני"})
def period_of_month(month: int) -> int:
return 1 if 9 <= month <= 12 else 2
def school_year(today: date) -> int:
"""The calendar year in which this school year began (it starts in September)."""
return today.year if today.month >= 9 else today.year - 1
def period_end(period: int, today: date) -> date:
year = school_year(today)
return date(year, 12, 31) if period == 1 else date(year + 1, 6, 30)
def lesson_period(lesson: dict, due: date | None) -> int:
"""By the lesson's date in the curriculum or, without one, by the last month of the bank's month for it."""
if due:
return period_of_month(due.month)
month = MONTHS.get(str(lesson.get("when", "")).split("–")[-1].strip())
return period_of_month(month) if month else 2
def _day(value) -> date | None:
if not value:
return None
try:
moment = value if isinstance(value, datetime) else _when(value)
except ValueError:
return None
return clock.school_day(moment) if moment.tzinfo else moment.date()
def state_on(progress: dict | None, end: date, today: date) -> str:
"""The skill's state at the end of a period (today's state while the period has not ended). Mastery never goes away, so the stored times say."""
if end >= today:
return skill_state(progress)
if progress:
mastered, started = _day(progress.get("mastered_at")), _day(progress.get("started_at"))
if mastered and mastered <= end:
return MASTERED
if (started and started <= end) or (mastered and mastered <= end):
return LEARNING
return NOT_STARTED
def best_quiz(rows: list[dict], end: date, today: date) -> int | None:
"""The best grade of the quizzes handed in by the end of the period."""
grades = [r["grade"] for r in rows if r.get("submitted_at") and r.get("grade") is not None and (end >= today or _day(r["submitted_at"]) <= end)]
return max(grades) if grades else None
@router.get("/{class_id}/progress")
def class_progress(class_id: int, request: Request, period: int | None = None, user: dict = Depends(need("class.teach"))) -> dict:
klass = _my_class(request, user, class_id)
store, bank, here = request.app.state.store, request.app.state.bank, reads.of(request)
today = clock.school_day()
current = period_of_month(today.month)
if period is None:
period = current
if period not in (1, 2):
raise HTTPException(422, "תקופה לא מוכרת")
end = period_end(period, today)
members = store.members(klass["id"])
ids = [m["user_id"] for m in members if m["user_id"]]
here.prefetch(("curriculum",), ("tests_of_classes", [klass["id"]]))
prime_students(request, ids, kinds=("exercise_test", "simulation", "quiz"))
dates = here.curriculum()
lessons = [l for l in bank.lessons_with_skills() if lesson_period(l, dates.get(l["id"])) == period]
now_view = end >= today
students = []
for m in members:
uid = m["user_id"]
states = {p["skill_id"]: p for p in here.progress_of(uid)} if uid else {}
quizzes: dict[str, list[dict]] = {}
for row in (here.attempts_of(uid, "quiz") if uid else []):
quizzes.setdefault(row["scope"], []).append(row)
coverage = {}
for lesson in lessons:
mine = {s["id"]: state_on(states.get(s["id"]), end, today) for s in lesson["skills"]}
best = best_quiz(quizzes.get(lesson["id"], []), end, today)
mastered = sum(1 for v in mine.values() if v == MASTERED)
passed = any(r.get("submitted_at") and r["grade"] is not None and r["grade"] >= attempts.PASS_GRADE and (now_view or _day(r["submitted_at"]) <= end)
for r in quizzes.get(lesson["id"], []))
completed = passed or (bool(mine) and mastered == len(mine))
due = dates.get(lesson["id"])
started = best is not None or any(v != NOT_STARTED for v in mine.values())
coverage[lesson["id"]] = {"state": "completed" if completed else "learning" if started else "not_started",
"percent": round(100 * mastered / len(mine)) if mine else 0, "best": best, "completed": completed,
"behind": bool(now_view and due and due < today and not completed)}
students.append({"email": m["email"], "signed_in": bool(uid), "lessons": coverage})
nearest = next((t for t in sorted(store.tests_of_classes([klass["id"]]), key=lambda t: t["date"]) if t["date"] >= today), None)
test = None
if nearest:
rows = []
for m in members:
facts = facts_for(request, m["user_id"], nearest, today)
ready = readiness_for(facts)
covered = len(facts["covered"])
rows.append({"email": m["email"], "signed_in": bool(m["user_id"]), "readiness": ready,
"mastery": round(100 * facts["mastered"] / covered) if covered else None, # shown beside it; not part of it
"practice": ready["practice_best"], "simulation": ready["simulation_best"],
"practices": ready["practices"], "simulations": ready["simulations"]})
names = {l["id"]: l["name"] for l in bank.lessons}
test = {"id": nearest["id"], "date": nearest["date"].isoformat(), "days_left": (nearest["date"] - today).days,
"lessons": [names.get(i, i) for i in nearest["lessons"]], "students": rows}
return {"class": {"id": klass["id"], "name": klass["name"]}, "period": period, "current_period": current, "as_of": min(end, today).isoformat(),
"periods": [{**p, "current": p["id"] == current} for p in PERIODS],
"lessons": [{"id": l["id"], "name": l["name"], "due_date": dates[l["id"]].isoformat() if l["id"] in dates else None} for l in lessons],
"students": students, "test": test}
|