3v324v23 Claude Sonnet 5.5 commited on
Commit
d70626e
·
1 Parent(s): bdbe42d

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 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 two ways to finish it: all its skills mastered, shown as "N מתוך M מיומנויות" and opening the next skill, or its quiz passed with at least 13 of 15, PTH-10, opening the quiz with the best grade so far), 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.
 
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: how far the skills are and the next unmastered one, and the quiz."""
245
  lessons, _, _ = lesson_rows(request, user)
246
  rows = []
247
  for l in lessons:
248
  if not l["behind"]:
249
  continue
250
- nxt = next((s for s in l["skills"] if s["state"] != MASTERED), None)
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, with the two ways to finish it (PTH-10): all its skills, or its quiz with at least 13 of 15. */
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><br><span class="late">⚠ היה אמור להסתיים ב־${shortDate(l.due_date)}</span></p>
74
- <p class="actions">${l.next_skill ? `<a class="button" data-way="skills" href="#/skill/${encodeURIComponent(l.next_skill.id)}">לסיים את המיומנויות: ${l.mastered} מתוך ${l.total}</a>` : ""}
75
- <a class="button" data-way="quiz" href="#/quiz/${encodeURIComponent(l.id)}">${l.quiz.open ? "להמשיך את הבוחן" : "בוחן"}: לפחות ${l.quiz.pass_right} מתוך ${l.quiz.size}</a></p>
76
- <p class="muted" data-best>${l.quiz.best === null ? "עוד לא נעשה בוחן" : `הציון הטוב ביותר בבוחן: ${l.quiz.best}`}</p></li>`).join("")}</ul></section>`;
 
 
 
 
 
 
 
 
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 test_the_skills_option_says_how_far_and_opens_the_next_unmastered_skill(world):
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["mastered"], row["total"]) == (3, len(skills)) and row["next_skill"]["id"] == skills[3] and row["next_skill"]["name"]
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 test_the_card_lists_the_lessons_behind_in_date_order_with_both_ways_to_finish(browser, bank):
21
  app, store, klass, lessons = world(bank)
22
  seed(app, store, bank, lessons)
23
- skills = [s["id"] for s in bank.skills if s["unit"] == lessons[0]]
24
- store.save_progress(KID_ID, skills[0], started_at=STAMP, mastered_at=STAMP)
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 "היה אמור להסתיים ב־" in rows.nth(0).inner_text()
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 "עוד לא נעשה בוחן" in mine.locator("[data-best]").inner_text()
 
36
  no_sideways_scroll(page)
37
  assert not page.errors
38
 
39
 
40
- def test_the_best_quiz_grade_shows_and_the_links_open_the_skill_and_the_quiz(browser, bank):
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
- assert "הציון הטוב ביותר בבוחן: 80" in page.inner_text(f"#behind-card li[data-lesson='{lessons[0]}'] [data-best]")
50
- page.click(f"#behind-card li[data-lesson='{lessons[1]}'] [data-way=skills]")
51
- page.wait_for_selector(".mx-sheet")
52
- assert page.evaluate("location.hash").startswith("#/skill/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 and no lesson behind
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 and the date it should have been completed by. 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
 
 
 
 
 
 
 
 
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: Two ways to catch up
67
- Each lesson listed as behind SHALL offer two ways to finish it: all its skills mastered, shown as "N מתוך M מיומנויות" and opening its next unmastered skill; or its בוחן passed with at least 13 right out of 15, opening the בוחן and showing the best grade so far.
68
 
69
- #### Scenario: Skills
70
- - **WHEN** a student has mastered 3 of 5 skills of a lesson that is behind
71
- - **THEN** its row says "3 מתוך 5 מיומנויות" and the skills option opens the first skill not mastered
72
 
73
- #### Scenario: The בוחן
74
- - **WHEN** the student's best grade in that lesson's בוחן is 80
75
- - **THEN** the row shows 80 and says that 13 right out of 15 is needed
 
 
 
 
 
 
 
 
 
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