mathly / mathly2 /lessons.py
wzender's picture
Claude
Subjects and worksheets: more subjects, and a worksheet becomes a lesson (#6)
b6f5bc4 unverified
Raw History Blame Contribute Delete
5.11 kB
"""Lesson documents kept in the database (add-subjects-and-worksheets): giving their parts ids, and the checks before publishing.
The document's shape is described in bank.py. The editor, the JSON load and the AI's draft may hand in skills and mistakes without
ids, or with temporary ones; `assign_ids` gives each a server-made id `w<lesson>_<k>` / `w<lesson>_m<k>` from counters kept in
the document that only grow, so an id is never reused after a delete and a reorder changes none.
"""
from __future__ import annotations
import copy
import re
from .bank import lesson_unit_id
def _counter(document: dict, key: str, used: list[int]) -> int:
return max([int(document.get(key) or 1)] + [n + 1 for n in used])
def assign_ids(document: dict, lesson_id: int) -> dict:
"""A copy of the document in which every skill and mistake has an id of this lesson, choices point at the renamed mistakes,
and the counters are past every id used."""
doc = copy.deepcopy(document)
prefix = lesson_unit_id(lesson_id)
skill_id, mistake_id = re.compile(rf"^{prefix}_(\d+)$"), re.compile(rf"^{prefix}_m(\d+)$")
skills = doc.get("skills") if isinstance(doc.get("skills"), list) else []
mistakes = doc.get("mistakes") if isinstance(doc.get("mistakes"), dict) else {}
next_skill = _counter(doc, "next_skill", [int(m.group(1)) for s in skills if isinstance(s, dict) for m in [skill_id.match(str(s.get("id", "")))] if m])
for skill in skills:
if isinstance(skill, dict) and not skill_id.match(str(skill.get("id", ""))):
skill["id"] = f"{prefix}_{next_skill}"
next_skill += 1
next_mistake = _counter(doc, "next_mistake", [int(m.group(1)) for k in mistakes for m in [mistake_id.match(k)] if m])
renamed: dict[str, str] = {}
for key in list(mistakes):
if not mistake_id.match(key):
renamed[key] = f"{prefix}_m{next_mistake}"
next_mistake += 1
doc["mistakes"] = {renamed.get(k, k): v for k, v in mistakes.items()}
for skill in skills:
for question in (skill.get("questions") or []) if isinstance(skill, dict) else []:
for choice in question.get("choices") or [] if isinstance(question, dict) else []:
if isinstance(choice, dict) and choice.get("mistake") in renamed:
choice["mistake"] = renamed[choice["mistake"]]
doc["skills"], doc["next_skill"], doc["next_mistake"] = skills, next_skill, next_mistake
return doc
MIN_QUESTIONS = 5 # questions a skill needs before its lesson can be published
def publish_problems(document: dict, lesson_id: int) -> list[str]:
"""Every reason the draft cannot be published yet, in words and with where it is: a name, at least one skill, at least
MIN_QUESTIONS questions per skill, exactly one right choice per question, and for every wrong choice a mistake with a name
and a tip. Then the bank's own checks (bank.lesson_problems), so a published lesson always loads."""
from .bank import lesson_problems
problems: list[str] = []
if not str(document.get("name") or "").strip():
problems.append("诇砖讬注讜专 讗讬谉 砖诐")
skills, mistakes = document.get("skills") or [], document.get("mistakes") or {}
if not skills:
problems.append("讗讬谉 讘砖讬注讜专 讗祝 诪讬讜诪谞讜转")
for si, skill in enumerate(skills, 1):
where = f"诪讬讜诪谞讜转 {si}"
if not str(skill.get("name") or "").strip():
problems.append(f"{where}: 讗讬谉 砖诐")
questions = skill.get("questions") or []
if len(questions) < MIN_QUESTIONS:
problems.append(f"{where}: 讬砖 {len(questions)} 砖讗诇讜转, 爪专讬讱 诇驻讞讜转 {MIN_QUESTIONS}")
for qi, question in enumerate(questions, 1):
at = f"{where}, 砖讗诇讛 {qi}"
if not str(question.get("question") or "").strip():
problems.append(f"{at}: 讗讬谉 谞讜住讞")
choices = question.get("choices") or []
right = sum(1 for c in choices if c.get("correct"))
if right != 1:
problems.append(f"{at}: 爪专讬讻讛 转砖讜讘讛 谞讻讜谞讛 讗讞转 讘讚讬讜拽 (讬砖 {right})")
if len(choices) < 2:
problems.append(f"{at}: 爪专讬讱 诇驻讞讜转 砖转讬 转砖讜讘讜转")
for ci, choice in enumerate(choices, 1):
if not str(choice.get("text") or "").strip():
problems.append(f"{at}, 转砖讜讘讛 {ci}: 讗讬谉 讟拽住讟")
if choice.get("correct"):
continue
mistake = mistakes.get(choice.get("mistake") or "")
if not mistake:
problems.append(f"{at}, 转砖讜讘讛 {ci}: 诇转砖讜讘讛 讛砖讙讜讬讛 讗讬谉 讟注讜转")
elif not str(mistake.get("name") or "").strip() or not str(mistake.get("tip") or "").strip():
problems.append(f"{at}, 转砖讜讘讛 {ci}: 诇讟注讜转 砖讛讬讗 诪专讗讛 讞住专 砖诐 讗讜 讟讬驻")
if not problems:
problems += lesson_problems(document, lesson_id)
return problems