Home: behind lessons link only to the quiz; practice tests below 87 listed; archive home-quiz-link-and-weak-tests
Browse files- docs/tasks/prd-mathly.md +1 -1
- mathly2/realtests.py +3 -1
- mathly2/skills.py +2 -4
- mathly2/static/home.js +14 -6
- mathly2/tests/test_behind_home_api.py +33 -7
- mathly2/tests/test_behind_home_browser.py +32 -13
- openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/.openspec.yaml +2 -0
- openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/proposal.md +19 -0
- openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/specs/student-home/spec.md +62 -0
- openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/tasks.md +13 -0
- openspec/specs/student-home/spec.md +28 -12
docs/tasks/prd-mathly.md
CHANGED
|
@@ -387,7 +387,7 @@ The exercise screen is where a student practices one skill.
|
|
| 387 |
|
| 388 |
A tab bar at the bottom of the screen leads between them: home, path, submissions (STU-7), grades, collection and review (STU-8).
|
| 389 |
|
| 390 |
-
- **STU-1 Home** shows only what is critical: the nearest test's reminder with its readiness and today's task (TST-3), a required simulation, **every lesson behind the curriculum** (PTH-11; earliest date first, each with its
|
| 391 |
- **STU-2 Path:** see 4.4.
|
| 392 |
- **STU-3 Grades:** all quiz (best grade), exercise test, simulation and assignment grades.
|
| 393 |
- **STU-4 Collection:** see MST-7.
|
|
|
|
| 387 |
|
| 388 |
A tab bar at the bottom of the screen leads between them: home, path, submissions (STU-7), grades, collection and review (STU-8).
|
| 389 |
|
| 390 |
+
- **STU-1 Home** shows only what is critical: the nearest test's reminder with its readiness and today's task (TST-3), a required simulation, **every lesson behind the curriculum** (PTH-11; earliest date first, each with its name, its date and one link to its quiz, which shows the best grade so far and that at least 13 of 15 is needed to complete it, PTH-10), **the practice tests below the pass line** (a practice test of an upcoming test that was taken and whose best grade is below 87, i.e. 13 of 15: its test's date, which test, the best grade and a link to take it again; one not taken yet, or at 87 or more, is not listed), the **new** assignments (המשימות שלי: not handed in yet), and a button that continues the path at the next skill; my AI requests and their state (AI-1).
|
| 391 |
- **STU-2 Path:** see 4.4.
|
| 392 |
- **STU-3 Grades:** all quiz (best grade), exercise test, simulation and assignment grades.
|
| 393 |
- **STU-4 Collection:** see MST-7.
|
mathly2/realtests.py
CHANGED
|
@@ -265,7 +265,9 @@ def home(request: Request, user: dict = Depends(learner)) -> dict:
|
|
| 265 |
return {"tests": cards, "task": {"test_id": nearest["id"], **_with_skill_name(request, nearest["task"])} if nearest else None,
|
| 266 |
"required_simulations": [{"test_id": c["id"], "date": c["date"], "days_left": c["days_left"]} for c in cards if c["simulation"]["required"]],
|
| 267 |
"next_skill": {"id": next_id, "name": next_skill_row["name"]} if next_skill_row else None,
|
| 268 |
-
"behind": behind_lessons(request, user)
|
|
|
|
|
|
|
| 269 |
|
| 270 |
|
| 271 |
def _student_test(request: Request, user: dict, test_id: int) -> dict:
|
|
|
|
| 265 |
return {"tests": cards, "task": {"test_id": nearest["id"], **_with_skill_name(request, nearest["task"])} if nearest else None,
|
| 266 |
"required_simulations": [{"test_id": c["id"], "date": c["date"], "days_left": c["days_left"]} for c in cards if c["simulation"]["required"]],
|
| 267 |
"next_skill": {"id": next_id, "name": next_skill_row["name"]} if next_skill_row else None,
|
| 268 |
+
"behind": behind_lessons(request, user),
|
| 269 |
+
"weak_tests": [{"test_id": c["id"], "date": c["date"], "days_left": c["days_left"], "number": e["number"], "best": e["best"], "open": e["open"]}
|
| 270 |
+
for c in cards for e in c["exercise_tests"] if e["best"] is not None and e["best"] < attempts.PASS_GRADE]}
|
| 271 |
|
| 272 |
|
| 273 |
def _student_test(request: Request, user: dict, test_id: int) -> dict:
|
mathly2/skills.py
CHANGED
|
@@ -241,15 +241,13 @@ def lesson_rows(request: Request, user: dict) -> tuple[list[dict], list[str], di
|
|
| 241 |
|
| 242 |
|
| 243 |
def behind_lessons(request: Request, user: dict) -> list[dict]:
|
| 244 |
-
"""The lessons behind the curriculum for the home, the earliest date first:
|
| 245 |
lessons, _, _ = lesson_rows(request, user)
|
| 246 |
rows = []
|
| 247 |
for l in lessons:
|
| 248 |
if not l["behind"]:
|
| 249 |
continue
|
| 250 |
-
|
| 251 |
-
rows.append({"id": l["id"], "name": l["name"], "due_date": l["due_date"], "mastered": l["mastered"], "total": len(l["skills"]),
|
| 252 |
-
"next_skill": {"id": nxt["id"], "name": nxt["name"]} if nxt else None,
|
| 253 |
"quiz": {"best": l["quiz"]["best"], "open": l["quiz"]["open"], "pass_right": attempts.PASS_RIGHT, "size": attempts.QUIZ_SIZE}})
|
| 254 |
return sorted(rows, key=lambda r: (r["due_date"], r["id"]))
|
| 255 |
|
|
|
|
| 241 |
|
| 242 |
|
| 243 |
def behind_lessons(request: Request, user: dict) -> list[dict]:
|
| 244 |
+
"""The lessons behind the curriculum for the home, the earliest date first: each with its quiz (the best grade so far and what passes it)."""
|
| 245 |
lessons, _, _ = lesson_rows(request, user)
|
| 246 |
rows = []
|
| 247 |
for l in lessons:
|
| 248 |
if not l["behind"]:
|
| 249 |
continue
|
| 250 |
+
rows.append({"id": l["id"], "name": l["name"], "due_date": l["due_date"],
|
|
|
|
|
|
|
| 251 |
"quiz": {"best": l["quiz"]["best"], "open": l["quiz"]["open"], "pass_right": attempts.PASS_RIGHT, "size": attempts.QUIZ_SIZE}})
|
| 252 |
return sorted(rows, key=lambda r: (r["due_date"], r["id"]))
|
| 253 |
|
mathly2/static/home.js
CHANGED
|
@@ -66,14 +66,22 @@ function assignmentsCard(all) {
|
|
| 66 |
}).join("")}</ul></section>`;
|
| 67 |
}
|
| 68 |
|
| 69 |
-
/** Every lesson behind the curriculum,
|
| 70 |
function behindCard(lessons) {
|
| 71 |
if (!lessons?.length) return "";
|
| 72 |
return `<section class="card" id="behind-card"><h2>שיעורים בפיגור</h2>
|
| 73 |
-
<ul class="plain behind-list">${lessons.map(l => `<li data-lesson="${esc(l.id)}"><p><b>${fmt(l.name)}</b><
|
| 74 |
-
<p class="actions">
|
| 75 |
-
<
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
/** The photo help card (STU-6): only for a class with the AI on; it also lists the student's latest requests with their state. */
|
|
@@ -94,7 +102,7 @@ function draw(view, data, assignments, ai) {
|
|
| 94 |
view(`${tabs("home")}${backToSubjects()}
|
| 95 |
${first ? nearest(first, data.task, data.next_skill) : `<section class="card" id="no-test"><h2>הבית</h2>
|
| 96 |
<p class="muted">המורה עוד לא קבע מבחן. בינתיים אפשר להמשיך במסלול.</p></section>`}
|
| 97 |
-
${behindCard(data.behind)}${assignmentsCard(assignments)}${aiCard(ai)}
|
| 98 |
${required.map(t => `<a class="required" href="#/sim/${t.id}" data-required="${t.id}"><b>חובה</b> מבחן סימולציה למבחן ב־${shortDate(t.date)}<span aria-hidden="true">‹</span></a>`).join("")}
|
| 99 |
<p>${data.next_skill ? `<a class="primary-link" id="continue-path" href="#/skill/${encodeURIComponent(data.next_skill.id)}">להמשיך במסלול: ${fmt(data.next_skill.name)} <span aria-hidden="true">‹</span></a>`
|
| 100 |
: `<a class="primary-link" id="continue-path" href="${PATH}">למסלול: כל הכוכבים פתוחים לתרגול <span aria-hidden="true">‹</span></a>`}</p>`);
|
|
|
|
| 66 |
}).join("")}</ul></section>`;
|
| 67 |
}
|
| 68 |
|
| 69 |
+
/** Every lesson behind the curriculum, short: its name, its date and one link to its quiz (at least 13 of 15 completes the lesson, PTH-10). */
|
| 70 |
function behindCard(lessons) {
|
| 71 |
if (!lessons?.length) return "";
|
| 72 |
return `<section class="card" id="behind-card"><h2>שיעורים בפיגור</h2>
|
| 73 |
+
<ul class="plain behind-list">${lessons.map(l => `<li data-lesson="${esc(l.id)}"><p><b>${fmt(l.name)}</b> <span class="late">⚠ עד ${shortDate(l.due_date)}</span></p>
|
| 74 |
+
<p class="actions"><a class="button" data-way="quiz" href="#/quiz/${encodeURIComponent(l.id)}">${l.quiz.open ? "להמשיך את הבוחן" : "לבוחן"}: לפחות ${l.quiz.pass_right} מתוך ${l.quiz.size}</a>
|
| 75 |
+
<span class="muted" data-best>${l.quiz.best === null ? "עוד לא נעשה" : `הציון הטוב ביותר: ${l.quiz.best}`}</span></p></li>`).join("")}</ul></section>`;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/** The practice tests of upcoming tests that were taken and are below 87 (13 of 15): to take again. */
|
| 79 |
+
function weakTestsCard(tests) {
|
| 80 |
+
if (!tests?.length) return "";
|
| 81 |
+
return `<section class="card" id="weak-tests"><h2>מבחני תרגול להשלמה</h2>
|
| 82 |
+
<ul class="plain behind-list">${tests.map(t => `<li data-test="${t.test_id}" data-number="${t.number}"><p><b>מבחן תרגול ${t.number}</b> <span class="muted">למבחן ב־${shortDate(t.date)}</span></p>
|
| 83 |
+
<p class="actions"><a class="button" data-way="retake" href="#/exam/${t.test_id}/${t.number}">${t.open ? "להמשיך" : "לנסות שוב"}</a>
|
| 84 |
+
<span class="muted" data-best>הציון הטוב ביותר: ${t.best} · צריך 87 לפחות (13 מתוך 15)</span></p></li>`).join("")}</ul></section>`;
|
| 85 |
}
|
| 86 |
|
| 87 |
/** The photo help card (STU-6): only for a class with the AI on; it also lists the student's latest requests with their state. */
|
|
|
|
| 102 |
view(`${tabs("home")}${backToSubjects()}
|
| 103 |
${first ? nearest(first, data.task, data.next_skill) : `<section class="card" id="no-test"><h2>הבית</h2>
|
| 104 |
<p class="muted">המורה עוד לא קבע מבחן. בינתיים אפשר להמשיך במסלול.</p></section>`}
|
| 105 |
+
${behindCard(data.behind)}${weakTestsCard(data.weak_tests)}${assignmentsCard(assignments)}${aiCard(ai)}
|
| 106 |
${required.map(t => `<a class="required" href="#/sim/${t.id}" data-required="${t.id}"><b>חובה</b> מבחן סימולציה למבחן ב־${shortDate(t.date)}<span aria-hidden="true">‹</span></a>`).join("")}
|
| 107 |
<p>${data.next_skill ? `<a class="primary-link" id="continue-path" href="#/skill/${encodeURIComponent(data.next_skill.id)}">להמשיך במסלול: ${fmt(data.next_skill.name)} <span aria-hidden="true">‹</span></a>`
|
| 108 |
: `<a class="primary-link" id="continue-path" href="${PATH}">למסלול: כל הכוכבים פתוחים לתרגול <span aria-hidden="true">‹</span></a>`}</p>`);
|
mathly2/tests/test_behind_home_api.py
CHANGED
|
@@ -35,16 +35,11 @@ def test_a_lesson_without_a_date_is_never_listed_and_none_behind_means_an_empty_
|
|
| 35 |
assert behind(client) == []
|
| 36 |
|
| 37 |
|
| 38 |
-
def
|
| 39 |
client, store, bank = world
|
| 40 |
store.set_lesson_dates({"t": clock.school_day() - timedelta(days=1)})
|
| 41 |
-
skills = skills_of(bank, "t")
|
| 42 |
-
for s in skills[:3]:
|
| 43 |
-
store.save_progress(KID_ID, s, started_at=STAMP, mastered_at=STAMP)
|
| 44 |
row = behind(client)[0]
|
| 45 |
-
assert (row
|
| 46 |
-
store.save_progress(KID_ID, skills[1], mastered_at=None) # a skill not mastered in the middle comes first
|
| 47 |
-
assert behind(client)[0]["next_skill"]["id"] == skills[1]
|
| 48 |
|
| 49 |
|
| 50 |
def test_the_quiz_option_shows_the_best_grade_and_what_is_needed_and_a_pass_catches_the_lesson_up(world):
|
|
@@ -64,3 +59,34 @@ def test_the_path_and_the_home_agree_on_what_is_behind(world):
|
|
| 64 |
store.set_lesson_dates({"t": today - timedelta(days=4), "t2": today - timedelta(days=1)})
|
| 65 |
path = client.get("/api/path", headers=auth()).json()
|
| 66 |
assert {l["id"] for l in path["lessons"] if l["behind"]} == {r["id"] for r in behind(client)} == {"t", "t2"} and path["behind"] == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
assert behind(client) == []
|
| 36 |
|
| 37 |
|
| 38 |
+
def test_a_behind_lesson_has_only_the_quiz_and_no_skills_option(world):
|
| 39 |
client, store, bank = world
|
| 40 |
store.set_lesson_dates({"t": clock.school_day() - timedelta(days=1)})
|
|
|
|
|
|
|
|
|
|
| 41 |
row = behind(client)[0]
|
| 42 |
+
assert set(row) == {"id", "name", "due_date", "quiz"} and "next_skill" not in row and "mastered" not in row # nothing about finishing the skills
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
def test_the_quiz_option_shows_the_best_grade_and_what_is_needed_and_a_pass_catches_the_lesson_up(world):
|
|
|
|
| 59 |
store.set_lesson_dates({"t": today - timedelta(days=4), "t2": today - timedelta(days=1)})
|
| 60 |
path = client.get("/api/path", headers=auth()).json()
|
| 61 |
assert {l["id"] for l in path["lessons"] if l["behind"]} == {r["id"] for r in behind(client)} == {"t", "t2"} and path["behind"] == 2
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def weak(client):
|
| 65 |
+
return client.get("/api/home", headers=auth()).json()["weak_tests"]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def exercise(store, test, number, grade, when_offset=0):
|
| 69 |
+
row = store.create_attempt(KID_ID, "exercise_test", f"{test['id']}:{number}", MON, MON + timedelta(minutes=45), [])
|
| 70 |
+
store.save_attempt(row["id"], submitted_at=MON + timedelta(minutes=10 + when_offset), grade=grade)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_practice_tests_below_87_are_listed_and_not_taken_or_good_ones_are_not(world):
|
| 74 |
+
client, store, bank = world
|
| 75 |
+
test = store.create_test(1, clock.school_day() + timedelta(days=10), ["t"], "t@x.test", created_at=clock.now() - timedelta(days=20))
|
| 76 |
+
assert weak(client) == [] # none taken
|
| 77 |
+
exercise(store, test, 1, 25)
|
| 78 |
+
got = weak(client)
|
| 79 |
+
assert got == [{"test_id": test["id"], "date": test["date"].isoformat(), "days_left": 10, "number": 1, "best": 25, "open": False}]
|
| 80 |
+
exercise(store, test, 2, 87) # 13 of 15: good enough
|
| 81 |
+
assert [(w["number"], w["best"]) for w in weak(client)] == [(1, 25)]
|
| 82 |
+
exercise(store, test, 1, 86, 1) # a retake still below: the best of the two
|
| 83 |
+
assert weak(client)[0]["best"] == 86
|
| 84 |
+
exercise(store, test, 1, 90, 2)
|
| 85 |
+
assert weak(client) == [] # reached: no longer listed
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_a_past_test_is_not_listed(world):
|
| 89 |
+
client, store, bank = world
|
| 90 |
+
old = store.create_test(1, clock.school_day() - timedelta(days=3), ["t"], "t@x.test", created_at=clock.now() - timedelta(days=30))
|
| 91 |
+
exercise(store, old, 1, 10)
|
| 92 |
+
assert weak(client) == []
|
mathly2/tests/test_behind_home_browser.py
CHANGED
|
@@ -17,39 +17,58 @@ def seed(app, store, bank, lessons):
|
|
| 17 |
store.set_lesson_dates({lessons[0]: today - timedelta(days=2), lessons[1]: today - timedelta(days=9)})
|
| 18 |
|
| 19 |
|
| 20 |
-
def
|
| 21 |
app, store, klass, lessons = world(bank)
|
| 22 |
seed(app, store, bank, lessons)
|
| 23 |
-
|
| 24 |
-
store.
|
| 25 |
with running(app) as url:
|
| 26 |
page = sign_in(browser, url)
|
| 27 |
to_home(page)
|
| 28 |
page.wait_for_selector("#behind-card")
|
| 29 |
rows = page.locator("#behind-card li")
|
| 30 |
assert rows.count() == 2 and rows.nth(0).get_attribute("data-lesson") == lessons[1] and rows.nth(1).get_attribute("data-lesson") == lessons[0] # the earlier date first
|
| 31 |
-
assert "
|
| 32 |
mine = page.locator(f"#behind-card li[data-lesson='{lessons[0]}']")
|
| 33 |
-
assert f"1 מתוך {len(skills)}" in mine.locator("[data-way=skills]").inner_text() and mine.locator("[data-way=skills]").get_attribute("href") == f"#/skill/{skills[1]}"
|
| 34 |
assert "לפחות 13 מתוך 15" in mine.locator("[data-way=quiz]").inner_text() and mine.locator("[data-way=quiz]").get_attribute("href") == f"#/quiz/{lessons[0]}"
|
| 35 |
-
assert "עוד לא נעשה
|
|
|
|
| 36 |
no_sideways_scroll(page)
|
| 37 |
assert not page.errors
|
| 38 |
|
| 39 |
|
| 40 |
-
def
|
| 41 |
app, store, klass, lessons = world(bank)
|
| 42 |
seed(app, store, bank, lessons)
|
| 43 |
-
q = store.create_attempt(KID_ID, "quiz", lessons[0], clock.now() - timedelta(hours=2), clock.now() - timedelta(hours=1), [])
|
| 44 |
-
store.save_attempt(q["id"], submitted_at=clock.now() - timedelta(hours=1), grade=80)
|
| 45 |
with running(app) as url:
|
| 46 |
page = sign_in(browser, url)
|
| 47 |
to_home(page)
|
| 48 |
page.wait_for_selector("#behind-card")
|
| 49 |
-
|
| 50 |
-
page.
|
| 51 |
-
page.
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def test_no_card_when_no_lesson_is_behind_or_when_it_was_passed(browser, bank):
|
|
|
|
| 17 |
store.set_lesson_dates({lessons[0]: today - timedelta(days=2), lessons[1]: today - timedelta(days=9)})
|
| 18 |
|
| 19 |
|
| 20 |
+
def test_the_card_lists_the_lessons_behind_in_date_order_each_with_only_the_quiz_link(browser, bank):
|
| 21 |
app, store, klass, lessons = world(bank)
|
| 22 |
seed(app, store, bank, lessons)
|
| 23 |
+
q = store.create_attempt(KID_ID, "quiz", lessons[0], clock.now() - timedelta(hours=2), clock.now() - timedelta(hours=1), [])
|
| 24 |
+
store.save_attempt(q["id"], submitted_at=clock.now() - timedelta(hours=1), grade=80)
|
| 25 |
with running(app) as url:
|
| 26 |
page = sign_in(browser, url)
|
| 27 |
to_home(page)
|
| 28 |
page.wait_for_selector("#behind-card")
|
| 29 |
rows = page.locator("#behind-card li")
|
| 30 |
assert rows.count() == 2 and rows.nth(0).get_attribute("data-lesson") == lessons[1] and rows.nth(1).get_attribute("data-lesson") == lessons[0] # the earlier date first
|
| 31 |
+
assert page.locator("#behind-card [data-way=skills]").count() == 0 and page.locator("#behind-card a").count() == 2 # only the quiz links
|
| 32 |
mine = page.locator(f"#behind-card li[data-lesson='{lessons[0]}']")
|
|
|
|
| 33 |
assert "לפחות 13 מתוך 15" in mine.locator("[data-way=quiz]").inner_text() and mine.locator("[data-way=quiz]").get_attribute("href") == f"#/quiz/{lessons[0]}"
|
| 34 |
+
assert "80" in mine.locator("[data-best]").inner_text() and "עוד לא נעשה" in rows.nth(0).locator("[data-best]").inner_text()
|
| 35 |
+
assert "⚠" in mine.inner_text() and "מיומנויות" not in page.inner_text("#behind-card")
|
| 36 |
no_sideways_scroll(page)
|
| 37 |
assert not page.errors
|
| 38 |
|
| 39 |
|
| 40 |
+
def test_the_quiz_link_opens_the_quiz(browser, bank):
|
| 41 |
app, store, klass, lessons = world(bank)
|
| 42 |
seed(app, store, bank, lessons)
|
|
|
|
|
|
|
| 43 |
with running(app) as url:
|
| 44 |
page = sign_in(browser, url)
|
| 45 |
to_home(page)
|
| 46 |
page.wait_for_selector("#behind-card")
|
| 47 |
+
page.click(f"#behind-card li[data-lesson='{lessons[1]}'] [data-way=quiz]")
|
| 48 |
+
page.wait_for_selector("#qz-q")
|
| 49 |
+
assert page.evaluate("location.hash").startswith(f"#/quiz/{lessons[1]}/")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_practice_tests_below_87_are_listed_with_a_retake_link_and_others_are_not(browser, bank):
|
| 53 |
+
app, store, klass, lessons = world(bank)
|
| 54 |
+
test = store.create_test(klass["id"], clock.school_day() + timedelta(days=10), lessons[:1], "t@school.test", created_at=clock.now() - timedelta(days=20))
|
| 55 |
+
for skill in [x["id"] for x in bank.skills if x["unit"] == lessons[0]][:2]: # started skills, so a practice test can begin
|
| 56 |
+
store.save_progress(KID_ID, skill, started_at=STAMP)
|
| 57 |
+
for number, grade in ((1, 25), (2, 87)):
|
| 58 |
+
row = store.create_attempt(KID_ID, "exercise_test", f"{test['id']}:{number}", clock.now() - timedelta(hours=2), clock.now() - timedelta(hours=1), [])
|
| 59 |
+
store.save_attempt(row["id"], submitted_at=clock.now() - timedelta(hours=1), grade=grade)
|
| 60 |
+
with running(app) as url:
|
| 61 |
+
page = sign_in(browser, url)
|
| 62 |
+
to_home(page)
|
| 63 |
+
page.wait_for_selector("#weak-tests")
|
| 64 |
+
rows = page.locator("#weak-tests li")
|
| 65 |
+
assert rows.count() == 1 and rows.first.get_attribute("data-number") == "1" # 87 is good enough
|
| 66 |
+
assert "25" in rows.first.locator("[data-best]").inner_text() and "87" in rows.first.locator("[data-best]").inner_text()
|
| 67 |
+
assert rows.first.locator("[data-way=retake]").get_attribute("href") == f"#/exam/{test['id']}/1"
|
| 68 |
+
no_sideways_scroll(page)
|
| 69 |
+
page.click("#weak-tests [data-way=retake]")
|
| 70 |
+
page.wait_for_selector("#qz-q")
|
| 71 |
+
assert not page.errors
|
| 72 |
|
| 73 |
|
| 74 |
def test_no_card_when_no_lesson_is_behind_or_when_it_was_passed(browser, bank):
|
openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/.openspec.yaml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
schema: spec-driven
|
| 2 |
+
created: 2026-11-09
|
openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/proposal.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Proposal
|
| 2 |
+
|
| 3 |
+
## Why
|
| 4 |
+
|
| 5 |
+
The "שיעורים בפיגור" card on home is heavier than it needs to be, and the home does not show the practice tests the student did badly on.
|
| 6 |
+
|
| 7 |
+
## What Changes
|
| 8 |
+
|
| 9 |
+
- **The behind-lessons card is shorter**: each lesson shows its name, the date and one link, to its בוחן (with the best grade so far and the 13 of 15 needed). The "finish the skills" option is removed.
|
| 10 |
+
- **Home also lists the practice tests (מבחני תרגול) that did not reach 13 right out of 15**, a grade of 87: for each upcoming test, every practice test that was taken and whose best grade is below 87, with the grade and a link to try again. A practice test not taken yet, or already at 87 or more, is not listed (a test not taken is already today's task and is on the test's card).
|
| 11 |
+
|
| 12 |
+
## Capabilities
|
| 13 |
+
|
| 14 |
+
### Modified Capabilities
|
| 15 |
+
- `student-home`: "Home shows only what is critical", "Lessons behind on home"; "Two ways to catch up" is removed; "Practice tests below the pass line" is new.
|
| 16 |
+
|
| 17 |
+
## Impact
|
| 18 |
+
|
| 19 |
+
- `GET /api/home`: the behind lessons lose the skills fields, and a list of weak practice tests is added; `home.js`; PRD STU-1. No database change.
|
openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/specs/student-home/spec.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## MODIFIED Requirements
|
| 2 |
+
|
| 3 |
+
### Requirement: Home shows only what is critical
|
| 4 |
+
The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the lessons behind the curriculum, the practice tests below the pass line, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
|
| 5 |
+
|
| 6 |
+
#### Scenario: Nothing critical
|
| 7 |
+
- **WHEN** a student has no upcoming test, no new assignment, no lesson behind and no weak practice test
|
| 8 |
+
- **THEN** home shows only the button that continues the path
|
| 9 |
+
|
| 10 |
+
#### Scenario: Two tests
|
| 11 |
+
- **WHEN** a student has two upcoming tests
|
| 12 |
+
- **THEN** home shows the nearest one, and the other is listed on that test's card
|
| 13 |
+
|
| 14 |
+
### Requirement: Lessons behind on home
|
| 15 |
+
Home SHALL list every lesson behind the curriculum, the earliest date first, with its name, the date it should have been completed by and one link to its בוחן, which shows the best grade so far and that 13 right out of 15 is needed. A completed lesson, a lesson with no date and a lesson whose date is ahead SHALL NOT be listed, and with none behind there SHALL be no such card.
|
| 16 |
+
|
| 17 |
+
#### Scenario: Two lessons behind
|
| 18 |
+
- **WHEN** two lessons have passed their dates and are not completed
|
| 19 |
+
- **THEN** home lists both, the one with the earlier date first, each with a link to its בוחן
|
| 20 |
+
|
| 21 |
+
#### Scenario: Only the בוחן
|
| 22 |
+
- **WHEN** a lesson is listed as behind
|
| 23 |
+
- **THEN** its row has no option to finish its skills, only the link to the בוחן
|
| 24 |
+
|
| 25 |
+
#### Scenario: The grade
|
| 26 |
+
- **WHEN** the student's best grade in that lesson's בוחן is 80
|
| 27 |
+
- **THEN** the row shows 80 and says that 13 right out of 15 is needed
|
| 28 |
+
|
| 29 |
+
#### Scenario: Caught up
|
| 30 |
+
- **WHEN** a student passes the בוחן of a lesson that was behind
|
| 31 |
+
- **THEN** the lesson is no longer listed on home
|
| 32 |
+
|
| 33 |
+
#### Scenario: None behind
|
| 34 |
+
- **WHEN** no lesson is behind
|
| 35 |
+
- **THEN** home has no behind card
|
| 36 |
+
|
| 37 |
+
## ADDED Requirements
|
| 38 |
+
|
| 39 |
+
### Requirement: Practice tests below the pass line
|
| 40 |
+
Home SHALL list the practice tests of the student's upcoming tests that were taken and whose best grade is below 87 (13 right out of 15): the test's date, which practice test, the best grade and a link to take it again. A practice test not taken yet, or with a best grade of 87 or more, SHALL NOT be listed, and with none there SHALL be no such card.
|
| 41 |
+
|
| 42 |
+
#### Scenario: A weak one
|
| 43 |
+
- **WHEN** a student's best grade in practice test 1 for a test on the 16th is 25
|
| 44 |
+
- **THEN** home lists practice test 1 for that test with 25 and a link to take it again
|
| 45 |
+
|
| 46 |
+
#### Scenario: Good enough
|
| 47 |
+
- **WHEN** the best grade is 87 or more
|
| 48 |
+
- **THEN** it is not listed
|
| 49 |
+
|
| 50 |
+
#### Scenario: Not taken
|
| 51 |
+
- **WHEN** a practice test was not taken yet
|
| 52 |
+
- **THEN** it is not listed
|
| 53 |
+
|
| 54 |
+
#### Scenario: Retaken
|
| 55 |
+
- **WHEN** the student retakes it and the best grade reaches 87
|
| 56 |
+
- **THEN** it is no longer listed
|
| 57 |
+
|
| 58 |
+
## REMOVED Requirements
|
| 59 |
+
|
| 60 |
+
### Requirement: Two ways to catch up
|
| 61 |
+
**Reason**: The behind-lessons card only links to the בוחן now; finishing all the skills is no longer offered on home.
|
| 62 |
+
**Migration**: Remove the skills option and the next-skill data from the card; the lesson's skills are still on the path.
|
openspec/changes/archive/2026-11-09-home-quiz-link-and-weak-tests/tasks.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tasks
|
| 2 |
+
|
| 3 |
+
## 1. The data
|
| 4 |
+
|
| 5 |
+
- [x] 1.1 Drop the skills fields from the behind lessons in `GET /api/home` and add the weak practice tests (test, number, best grade, the test's date and days left, whether an attempt is open) for the upcoming tests, earliest test first; verify API tests for every scenario of "Lessons behind on home" and "Practice tests below the pass line", and that home still needs no more waits in a row than before
|
| 6 |
+
|
| 7 |
+
## 2. The screen
|
| 8 |
+
|
| 9 |
+
- [x] 2.1 Shorten the behind card to the name, the date and the בוחן link with the best grade, and add the card of practice tests below 87 with a link to retake each; verify Playwright tests: no skills link on the behind card, the weak practice test shows with its grade and its link opens the exam, a test not taken or at 87 is not listed, no card when none, no sideways scrolling at 360px
|
| 10 |
+
|
| 11 |
+
## 3. Wrap-up
|
| 12 |
+
|
| 13 |
+
- [x] 3.1 Update the PRD (STU-1); run the full suite and `openspec validate home-quiz-link-and-weak-tests --strict`; verify both pass
|
openspec/specs/student-home/spec.md
CHANGED
|
@@ -38,10 +38,10 @@ The student's screens (home, path, submissions "הגשות", grades, collection)
|
|
| 38 |
- **THEN** the submissions screen opens
|
| 39 |
|
| 40 |
### Requirement: Home shows only what is critical
|
| 41 |
-
The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the lessons behind the curriculum, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
|
| 42 |
|
| 43 |
#### Scenario: Nothing critical
|
| 44 |
-
- **WHEN** a student has no upcoming test, no new assignment
|
| 45 |
- **THEN** home shows only the button that continues the path
|
| 46 |
|
| 47 |
#### Scenario: Two tests
|
|
@@ -49,11 +49,19 @@ The home SHALL show only: the nearest test with its readiness and today's task,
|
|
| 49 |
- **THEN** home shows the nearest one, and the other is listed on that test's card
|
| 50 |
|
| 51 |
### Requirement: Lessons behind on home
|
| 52 |
-
Home SHALL list every lesson behind the curriculum, the earliest date first, with its name
|
| 53 |
|
| 54 |
#### Scenario: Two lessons behind
|
| 55 |
- **WHEN** two lessons have passed their dates and are not completed
|
| 56 |
-
- **THEN** home lists both, the one with the earlier date first
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
#### Scenario: Caught up
|
| 59 |
- **WHEN** a student passes the בוחן of a lesson that was behind
|
|
@@ -63,13 +71,21 @@ Home SHALL list every lesson behind the curriculum, the earliest date first, wit
|
|
| 63 |
- **WHEN** no lesson is behind
|
| 64 |
- **THEN** home has no behind card
|
| 65 |
|
| 66 |
-
### Requirement:
|
| 67 |
-
|
| 68 |
|
| 69 |
-
#### Scenario:
|
| 70 |
-
- **WHEN** a student
|
| 71 |
-
- **THEN**
|
| 72 |
|
| 73 |
-
#### Scenario:
|
| 74 |
-
- **WHEN** the
|
| 75 |
-
- **THEN**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
- **THEN** the submissions screen opens
|
| 39 |
|
| 40 |
### Requirement: Home shows only what is critical
|
| 41 |
+
The home SHALL show only: the nearest test with its readiness and today's task, a required simulation, the lessons behind the curriculum, the practice tests below the pass line, the new assignments (not handed in yet), and the button that continues the path. Other tests, assignments that were handed in or checked, and settings such as notifications SHALL NOT be on home; the other tests are reached from the test's card.
|
| 42 |
|
| 43 |
#### Scenario: Nothing critical
|
| 44 |
+
- **WHEN** a student has no upcoming test, no new assignment, no lesson behind and no weak practice test
|
| 45 |
- **THEN** home shows only the button that continues the path
|
| 46 |
|
| 47 |
#### Scenario: Two tests
|
|
|
|
| 49 |
- **THEN** home shows the nearest one, and the other is listed on that test's card
|
| 50 |
|
| 51 |
### Requirement: Lessons behind on home
|
| 52 |
+
Home SHALL list every lesson behind the curriculum, the earliest date first, with its name, the date it should have been completed by and one link to its בוחן, which shows the best grade so far and that 13 right out of 15 is needed. A completed lesson, a lesson with no date and a lesson whose date is ahead SHALL NOT be listed, and with none behind there SHALL be no such card.
|
| 53 |
|
| 54 |
#### Scenario: Two lessons behind
|
| 55 |
- **WHEN** two lessons have passed their dates and are not completed
|
| 56 |
+
- **THEN** home lists both, the one with the earlier date first, each with a link to its בוחן
|
| 57 |
+
|
| 58 |
+
#### Scenario: Only the בוחן
|
| 59 |
+
- **WHEN** a lesson is listed as behind
|
| 60 |
+
- **THEN** its row has no option to finish its skills, only the link to the בוחן
|
| 61 |
+
|
| 62 |
+
#### Scenario: The grade
|
| 63 |
+
- **WHEN** the student's best grade in that lesson's בוחן is 80
|
| 64 |
+
- **THEN** the row shows 80 and says that 13 right out of 15 is needed
|
| 65 |
|
| 66 |
#### Scenario: Caught up
|
| 67 |
- **WHEN** a student passes the בוחן of a lesson that was behind
|
|
|
|
| 71 |
- **WHEN** no lesson is behind
|
| 72 |
- **THEN** home has no behind card
|
| 73 |
|
| 74 |
+
### Requirement: Practice tests below the pass line
|
| 75 |
+
Home SHALL list the practice tests of the student's upcoming tests that were taken and whose best grade is below 87 (13 right out of 15): the test's date, which practice test, the best grade and a link to take it again. A practice test not taken yet, or with a best grade of 87 or more, SHALL NOT be listed, and with none there SHALL be no such card.
|
| 76 |
|
| 77 |
+
#### Scenario: A weak one
|
| 78 |
+
- **WHEN** a student's best grade in practice test 1 for a test on the 16th is 25
|
| 79 |
+
- **THEN** home lists practice test 1 for that test with 25 and a link to take it again
|
| 80 |
|
| 81 |
+
#### Scenario: Good enough
|
| 82 |
+
- **WHEN** the best grade is 87 or more
|
| 83 |
+
- **THEN** it is not listed
|
| 84 |
+
|
| 85 |
+
#### Scenario: Not taken
|
| 86 |
+
- **WHEN** a practice test was not taken yet
|
| 87 |
+
- **THEN** it is not listed
|
| 88 |
+
|
| 89 |
+
#### Scenario: Retaken
|
| 90 |
+
- **WHEN** the student retakes it and the best grade reaches 87
|
| 91 |
+
- **THEN** it is no longer listed
|