Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions app/extended_curriculum.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@

from __future__ import annotations

import random
from typing import Any

from app.lessons_13_25 import LESSONS_13_25

TERM_SYNONYMS = {
"sorted": ("сортировка",),
"арифметический оператор": ("арифметика", "оператор"),
"оператор %": ("%", "остаток"),
"оператор and": ("and", "логическое и"),
"срез": ("slice", "срез строки"),
"метод replace": ("replace", "замена"),
"метод join": ("join", "соединение строк"),
"f-строка": ("f string", "форматированная строка"),
"кортеж": ("tuple",),
"распаковка": ("unpacking",),
"append": ("метод append", "добавление в список"),
}


def _theory(title: str, text: str, example: str, tip: str = "") -> dict[str, str]:
return {"title": title, "text": text, "example": example, "tip": tip}
Expand Down Expand Up @@ -1389,21 +1406,24 @@ def _make_questions(
) -> list[dict[str, Any]]:
_, title, subtitle, keyword, example, concept, _ = lesson_spec
code_task = _code_task(task_kind)
choice_id = f"{lesson_id}-choice"
options = [example, "print('Сначала пишем код, потом думаем')", "value = input() + 1"]
random.Random(f"{lesson_id}:{choice_id}").shuffle(options)
return [
{
"id": f"{lesson_id}-choice",
"id": choice_id,
"kind": "choice",
"prompt": f"Какой пример относится к теме «{title}»?",
"options": [example, "print('Сначала пишем код, потом думаем')", "value = input() + 1"],
"options": options,
"answer": example,
"explanation": concept,
},
{
"id": f"{lesson_id}-term",
"kind": "input",
"prompt": f"Какой ключевой инструмент или термин связывает урок «{title}»?",
"answers": [keyword],
"placeholder": "Например: словарь",
"answers": [keyword, *TERM_SYNONYMS.get(keyword, ())],
"placeholder": "Введите термин",
"explanation": f"Ключевой ориентир урока — «{keyword}». {subtitle}.",
},
{"id": f"{lesson_id}-code", "kind": "code", **code_task},
Expand All @@ -1429,11 +1449,10 @@ def build_extended_course() -> tuple[
"icon": unit["icon"],
}
)
question_ids: list[str] = []
question_ids_by_kind: dict[str, list[str]] = {"choice": [], "input": [], "code": []}
for lesson_index, spec in enumerate(unit["lessons"]):
slug, title, subtitle, keyword, example, concept, advice = spec
lesson_id = f"{module_id}-{slug}"
question_ids.append(f"{lesson_id}-choice")
if order >= 26:
questions = _make_questions(lesson_id, spec, TASK_CYCLES[unit_index][lesson_index])
lessons.append(
Expand Down Expand Up @@ -1462,10 +1481,31 @@ def build_extended_course() -> tuple[
"questions": questions,
}
)
else:
questions = next(lesson for lesson in LESSONS_13_25 if lesson["id"] == lesson_id)[
"questions"
]
for question in questions:
question_ids_by_kind[question["kind"]].append(question["id"])
order += 1
question_ids = [
random.Random(f"{module_id}:{kind}").choice(ids)
for kind, ids in question_ids_by_kind.items()
if ids
]
remaining_ids = [
question_id
for ids in question_ids_by_kind.values()
for question_id in ids
if question_id not in question_ids
]
random.Random(f"{module_id}:exam").shuffle(remaining_ids)
if remaining_ids:
question_ids.append(remaining_ids[0])
random.Random(f"{module_id}:exam-order").shuffle(question_ids)
exams[module_id] = {
"title": f"Контрольная точка: {unit['title']}",
"description": f"Четыре коротких вопроса по разделу «{unit['title']}».",
"description": f"Четыре вопроса разных типов по разделу «{unit['title']}».",
"question_ids": question_ids,
}
return modules, lessons, exams
Expand Down
77 changes: 75 additions & 2 deletions tests/test_curriculum.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import re
from pathlib import Path

from app import extended_curriculum
from app.content import EXAMS, LESSONS, MODULES, QUESTION_BY_ID
from app.evaluator import evaluate
from app.extended_curriculum import EXTRA_LESSONS
from app.evaluator import evaluate, normalize
from app.extended_curriculum import EXTRA_LESSONS, build_extended_course
from app.lessons_13_25 import LESSONS_13_25

LESSONS_13_25_IDENTITY = [
Expand Down Expand Up @@ -110,3 +111,75 @@ def test_lessons_four_to_six_do_not_require_future_topics() -> None:
assert QUESTION_BY_ID["while-code"]["tests"] == [
{"kind": "stdout", "expected": "5\n4\n3\n2\n1\nПуск!"}
]


def test_extended_questions_are_fair_and_exams_are_mixed() -> None:
extended_lessons = [lesson for lesson in LESSONS if lesson["order"] >= 13]
choice_positions = [
question["options"].index(question["answer"])
for lesson in extended_lessons
for question in lesson["questions"]
if question["kind"] == "choice"
]
assert len(set(choice_positions)) > 1
assert all(0 <= position < 3 for position in choice_positions)
assert (
sum(
len(question["answers"]) > 1
for lesson in extended_lessons
for question in lesson["questions"]
if question["kind"] == "input"
)
>= 10
)
extended_modules = {lesson["module_id"] for lesson in extended_lessons}
for module_id in extended_modules:
question_ids = EXAMS[module_id]["question_ids"]
assert len(question_ids) == len(set(question_ids)) == 4
assert {QUESTION_BY_ID[question_id]["kind"] for question_id in question_ids} == {
"choice",
"input",
"code",
}

term_question = next(
question
for lesson in extended_lessons
for question in lesson["questions"]
if question["kind"] == "input" and len(question["answers"]) > 1
)
assert all(
normalize(answer) not in normalize(term_question["placeholder"])
for answer in term_question["answers"]
)

keyword, synonyms = "sorted", ("сортировка",)
answers = next(
question["answers"]
for lesson in extended_lessons
for question in lesson["questions"]
if question["kind"] == "input" and keyword in question["answers"]
)
assert normalize(f" {synonyms[0].upper()} ") in {normalize(answer) for answer in answers}


def test_extended_generation_is_deterministic_and_handles_short_modules(monkeypatch) -> None:
first = build_extended_course()
second = build_extended_course()
assert first == second

monkeypatch.setattr(
extended_curriculum,
"COURSE_UNITS",
[
{
**extended_curriculum.COURSE_UNITS[0],
"lessons": extended_curriculum.COURSE_UNITS[0]["lessons"][:1],
}
],
)
monkeypatch.setattr(
extended_curriculum, "TASK_CYCLES", [extended_curriculum.TASK_CYCLES[0][:1]]
)
_, _, exams = build_extended_course()
assert len(next(iter(exams.values()))["question_ids"]) == 3
Loading