3v324v23 commited on
Commit
9f3ca5d
·
1 Parent(s): 7bfd86c

Enhance status messages and lesson assertions in the application

Browse files

- Updated the status message to dynamically reflect whether the conversation or story has ended.
- Modified the lesson assertions in the scoring tests to include additional lesson IDs for consistency.
- Added new tests for workbook lessons 3, 4, 5, 6, 7, 8, 9, and 10 to ensure comprehensive coverage of lesson content and structure.
- Ensured that the story and task assertions in the tests accurately reflect the expected content and format.

Files changed (3) hide show
  1. app/workbook.py +0 -0
  2. static/index.html +2 -2
  3. tests/test_scoring.py +231 -3
app/workbook.py CHANGED
The diff for this file is too large to render. See raw diff
 
static/index.html CHANGED
@@ -1674,7 +1674,7 @@ $("conjBtn").onclick=()=>variation("conjugate");
1674
  function roundEnd(){ // a story stops at its end and waits for the microphone button; other topics go straight on
1675
  const story=isStory()&&!driving;
1676
  startCat(story);
1677
- if(story) $("status").textContent="הגעתם לסוף השיחה. לחצו על המיקרופון כדי להמשיך";
1678
  }
1679
  const nextPhrase=()=>{
1680
  if(quiz&&!quiz.over&&topic.indexOf(here)+1>=topic.length){ quizEnd(); return; } // past the quiz's last line
@@ -1938,7 +1938,7 @@ function drawContent(){
1938
  const lines=c?listed.filter(p=>p.cat===c.id).length:0;
1939
  if(n){
1940
  const row=mark("crow","","li");
1941
- row.append(mark("",c.story?"💬":c.write?"✎\uFE0E":n,"i"),mark("",c.name,"b"),mark("",c.write?`כתיבה · ${c.write.lines} משפטים`:`${c.story?"שיחה":"תרגול"} · ${lines} ${c.unit==="מילה"?"מילים":c.unit==="פועל"?"פעלים":"משפטים"}`,"small")); list.append(row); return;
1942
  }
1943
  const it=v||c, row=mark("crow added","","li"), text=mark("two",""), title=mark("","","b"), t=document.createElement("bdi"), acts=mark("acts","");
1944
  const up=mark("btn move","↑","button"), down=mark("btn move","↓","button"), see=mark("btn",v?"צפייה":"תרגול","button"), del=mark("btn danger","מחיקה","button");
 
1674
  function roundEnd(){ // a story stops at its end and waits for the microphone button; other topics go straight on
1675
  const story=isStory()&&!driving;
1676
  startCat(story);
1677
+ if(story) $("status").textContent=`הגעתם לסוף ${(CATS.find(c=>c.id===cat)||{}).voices?"השיחה":"הסיפור"}. לחצו על המיקרופון כדי להמשיך`;
1678
  }
1679
  const nextPhrase=()=>{
1680
  if(quiz&&!quiz.over&&topic.indexOf(here)+1>=topic.length){ quizEnd(); return; } // past the quiz's last line
 
1938
  const lines=c?listed.filter(p=>p.cat===c.id).length:0;
1939
  if(n){
1940
  const row=mark("crow","","li");
1941
+ row.append(mark("",c.story?"💬":c.write?"✎\uFE0E":n,"i"),mark("",c.name,"b"),mark("",c.write?`כתיבה · ${c.write.lines} משפטים`:`${c.story?c.voices?"שיחה":"סיפור":"תרגול"} · ${lines} ${c.unit==="מילה"?"מילים":c.unit==="פועל"?"פעלים":"משפטים"}`,"small")); list.append(row); return;
1942
  }
1943
  const it=v||c, row=mark("crow added","","li"), text=mark("two",""), title=mark("","","b"), t=document.createElement("bdi"), acts=mark("acts","");
1944
  const up=mark("btn move","↑","button"), down=mark("btn move","↓","button"), see=mark("btn",v?"צפייה":"תרגול","button"), del=mark("btn danger","מחיקה","button");
tests/test_scoring.py CHANGED
@@ -131,7 +131,7 @@ def test_api_end_to_end_with_stub_model():
131
  ph = c.get("/api/phrases").json()
132
  shown = {c["id"] for c in ph["categories"]} # the phrases of no lesson are in the bank, not on the page
133
  assert len(ph["phrases"]) == sum(p["cat"] in shown for p in PHRASES) and "ipa" not in ph["phrases"][0]
134
- assert TOILET["cat"] not in shown and [l["id"] for l in ph["lessons"] if not l.get("soon")] == ["1", "2"]
135
  assert len(ph["lessons"]) == 20 and [v["name"] for v in ph["levels"]] == ["רמה 1", "רמה 2"]
136
  pid = PHRASES.index(TOILET)
137
  r = c.post("/api/score", data={"phrase_id": pid}, files={"audio": ("a.wav", buf.getvalue(), "audio/wav")})
@@ -355,7 +355,7 @@ def test_references_use_one_generated_voice_and_any_cached_one():
355
  def test_workbook_lesson_1():
356
  from app.phrases import CATEGORIES, LESSONS, public_phrases
357
 
358
- assert [l["id"] for l in LESSONS if not l.get("soon")] == ["1", "2"]
359
  assert [c["id"] for c in CATEGORIES if c["lesson"] == "1"] == ["l1-pron", "l1-verbs", "l1-past", "l1-say", "l1-read", "l1-hw"]
360
  hw = CATEGORIES[5]["write"] # the workbook's homework: a task and five sentences to write, with nothing to say
361
  assert hw["lines"] == 5 and hw["task"].startswith("כתבו חמישה משפטים") and "מִין - מי" in hw["task"]
@@ -398,7 +398,7 @@ def test_workbook_lesson_2():
398
 
399
  cats = ["l2-dual", "l2-family", "l2-poss", "l2-say", "l2-say2", "l2-read", "l2-talk", "l2-meet"]
400
  assert [c["id"] for c in CATEGORIES if c["lesson"] == "2"] == cats + ["l2-hw"] # the lesson ends with its written homework
401
- hw = CATEGORIES[-1]["write"]
402
  assert hw["lines"] == 5 and "פתרו את כל השיעור השני" in hw["task"] and "קַעַד / יֻקְעֹד – ישב" in hw["task"]
403
  pub = public_phrases()
404
  assert [sum(p["cat"] == c for p in pub) for c in cats] == [17, 24, 48, 23, 25, 13, 27, 24]
@@ -426,6 +426,234 @@ def test_workbook_lesson_2():
426
  assert by["l2-dual", "שבוע, שבועיים"]["he_say"] == "שָׁבוּעַ, שבועיים" # not "shvua"
427
 
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  def test_sign_in_guards_the_practice_endpoints():
430
  from fastapi.testclient import TestClient
431
 
 
131
  ph = c.get("/api/phrases").json()
132
  shown = {c["id"] for c in ph["categories"]} # the phrases of no lesson are in the bank, not on the page
133
  assert len(ph["phrases"]) == sum(p["cat"] in shown for p in PHRASES) and "ipa" not in ph["phrases"][0]
134
+ assert TOILET["cat"] not in shown and [l["id"] for l in ph["lessons"] if not l.get("soon")] == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
135
  assert len(ph["lessons"]) == 20 and [v["name"] for v in ph["levels"]] == ["רמה 1", "רמה 2"]
136
  pid = PHRASES.index(TOILET)
137
  r = c.post("/api/score", data={"phrase_id": pid}, files={"audio": ("a.wav", buf.getvalue(), "audio/wav")})
 
355
  def test_workbook_lesson_1():
356
  from app.phrases import CATEGORIES, LESSONS, public_phrases
357
 
358
+ assert [l["id"] for l in LESSONS if not l.get("soon")] == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
359
  assert [c["id"] for c in CATEGORIES if c["lesson"] == "1"] == ["l1-pron", "l1-verbs", "l1-past", "l1-say", "l1-read", "l1-hw"]
360
  hw = CATEGORIES[5]["write"] # the workbook's homework: a task and five sentences to write, with nothing to say
361
  assert hw["lines"] == 5 and hw["task"].startswith("כתבו חמישה משפטים") and "מִין - מי" in hw["task"]
 
398
 
399
  cats = ["l2-dual", "l2-family", "l2-poss", "l2-say", "l2-say2", "l2-read", "l2-talk", "l2-meet"]
400
  assert [c["id"] for c in CATEGORIES if c["lesson"] == "2"] == cats + ["l2-hw"] # the lesson ends with its written homework
401
+ hw = next(c for c in CATEGORIES if c["id"] == "l2-hw")["write"]
402
  assert hw["lines"] == 5 and "פתרו את כל השיעור השני" in hw["task"] and "קַעַד / יֻקְעֹד – ישב" in hw["task"]
403
  pub = public_phrases()
404
  assert [sum(p["cat"] == c for p in pub) for c in cats] == [17, 24, 48, 23, 25, 13, 27, 24]
 
426
  assert by["l2-dual", "שבוע, שבועיים"]["he_say"] == "שָׁבוּעַ, שבועיים" # not "shvua"
427
 
428
 
429
+ def test_workbook_lesson_3():
430
+ from app.phrases import CATEGORIES, public_phrases
431
+
432
+ cats = ["l3-verbs", "l3-past", "l3-read", "l3-say", "l3-say2", "l3-nums", "l3-numread", "l3-numsay", "l3-story"]
433
+ mine = [c for c in CATEGORIES if c["lesson"] == "3"]
434
+ assert [c["id"] for c in mine] == cats + ["l3-hw"]
435
+ hw = mine[-1]["write"]
436
+ assert hw["lines"] == 5 and "פתרו את כל השיעור השלישי" in hw["task"] and "נַקַל / יֻנְקֹל – עבר (דירה, עבודה)" in hw["task"]
437
+ # the story is laid out like the conversation, but told in the student's own voice: it names none
438
+ assert mine[-2]["story"] and "voices" not in mine[-2] and CATEGORIES[[c["id"] for c in CATEGORIES].index("l2-talk")]["voices"] == ["אמיר", "ראאף"]
439
+ pub = public_phrases()
440
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [4, 32, 26, 12, 14, 37, 11, 11, 10]
441
+ assert all(p["read"] == (p["cat"] in ("l3-read", "l3-numread")) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l3-"))
442
+ assert all("seq" not in p for p in pub if p["cat"] == "l3-past")
443
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
444
+ # a verb is said in the past and the future together; עבר alone would read as "past", so it names what is moved
445
+ assert by["l3-verbs", "בישל (עבר, עתיד)"]["tr"] == "טַבַּח', יֻטְבֹּח'" and by["l3-verbs", "בישל (עבר, עתיד)"]["he_say"] == "בִּישֵּׁל"
446
+ assert by["l3-verbs", "עבר דירה (עבר, עתיד)"]["variants"][:2] == [("نقل، ينقل", "n a ʔ a l | j u n ʔ u l"), ("نقل، ينقل", "n a ʔ a l | j u n q u l")]
447
+ assert by["l3-past", "היא שחתה"]["tr"] == "הִיֵّ סַבְּחַת" and by["l3-past", "אנחנו בישלנו"]["tr"] == "אִחְנַא טַבַּחְ'נַא"
448
+ # a number shows its digits; the prompt voice says the Hebrew word only
449
+ assert by["l3-nums", "שבע עשרה (17)"]["tr"] == "סַבְּעְתַעַש" and by["l3-nums", "שבע עשרה (17)"]["he_say"] == "שבע עשרה"
450
+ assert by["l3-nums", "עשרת אלפים (10,000)"]["tr"] == "עַשְרַת אַלַאף" and by["l3-nums", "שלוש מאות (300)"]["tr"] == "תַלַאת מִיֵّה"
451
+ p = next(p for p in PHRASES if p["cat"] == "l3-read" and p["seq"] == 9)
452
+ assert "הִיֵّ טַבְּחַ'ת-לוֹ מַקְלוּבֵּה טַיְّבֵּה." in p["tr"]
453
+ p = next(p for p in PHRASES if p["cat"] == "l3-numsay" and p["seq"] == 6)
454
+ assert p["tr"] == "עִנְד-נַא חְדַעְשַר עַםّ וּסַבְּעְתַעְשַר עַמֵّה." # 11-19 gain a ר before a noun, which stays singular
455
+ p = next(p for p in PHRASES if p["cat"] == "l3-story" and p["seq"] == 1)
456
+ assert p["tr"] == "צַאחִבְּת-י רוּבַּה נַקְלַת עַלַא בֵּית גְ'דִיד פִי חַארֵת-נַא."
457
+
458
+
459
+ def test_workbook_lesson_4():
460
+ from app.phrases import CATEGORIES, public_phrases
461
+
462
+ cats = ["l4-verbs", "l4-past", "l4-read", "l4-read2", "l4-say", "l4-say2", "l4-story", "l4-meet", "l4-meetread"]
463
+ mine = [c for c in CATEGORIES if c["lesson"] == "4"]
464
+ assert [c["id"] for c in mine] == cats + ["l4-hw"]
465
+ hw = mine[-1]["write"]
466
+ assert hw["lines"] == 5 and "פתרו את כל השיעור הרביעי" in hw["task"] and "אִשְתַרַא / יִשְתְרִי – קנה" in hw["task"]
467
+ story = mine[cats.index("l4-story")]
468
+ assert story["story"] and "voices" not in story
469
+ pub = public_phrases()
470
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [4, 32, 12, 14, 12, 12, 20, 18, 6]
471
+ reads = ("l4-read", "l4-read2", "l4-meetread")
472
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l4-"))
473
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
474
+ # verbs that end in ا: the ا becomes ي before the endings of "I", "you" and "we"
475
+ assert by["l4-verbs", "בא (עבר, עתיד)"]["tr"] == "אַגַ'א, יִגִ'י" and by["l4-verbs", "נתן (עבר, עתיד)"]["tr"] == "אַעְטַא, יֵעַטִי"
476
+ assert by["l4-past", "אנחנו דיברנו"]["tr"] == "אִחְנַא חַכֵּינַא" and by["l4-past", "היא קנתה"]["tr"] == "הִיֵّ אִשְתַרַת"
477
+ assert by["l4-past", "הם באו"]["variants"][0] == ("همّ اجو", "h u m m e | ʔ a dʒ u")
478
+ p = next(p for p in PHRASES if p["cat"] == "l4-read" and p["seq"] == 4)
479
+ assert p["tr"].startswith("גַ'אר-י אַגַ'א וּאַעַטַא-נִי תִסְעַה וּתַמַאנִין שֵיכֵּל")
480
+ p = next(p for p in PHRASES if p["cat"] == "l4-story" and p["seq"] == 1)
481
+ assert p["tr"] == "קַבֵּל אֻסְבּוּע אַגַ'א עַלַא חַארֵת-נַא גַ'אר גְ'דִיד."
482
+ assert by["l4-story", "הוא אמר: \"ה' ישמור אותם!\""]["he_say"] == "הוא אמר: \"אלוהים ישמור אותם!\""
483
+ assert by["l4-meet", "אני רוצה קפה עם סוכר, תודה!"]["tr"] == "בִּדّ-י קַהְוֵה מַע סֻכַּّר, שֻכְּרַן!"
484
+
485
+
486
+ def test_workbook_lesson_5():
487
+ from app.phrases import CATEGORIES, public_phrases
488
+
489
+ # the workbook's order: more of lesson 4's story, the negation, then the new verbs and the market story
490
+ cats = ["l5-meet", "l5-meetread", "l5-neg", "l5-neg2", "l5-verbs", "l5-past", "l5-read", "l5-story", "l5-aread",
491
+ "l5-say", "l5-say2", "l5-say3"]
492
+ mine = [c for c in CATEGORIES if c["lesson"] == "5"]
493
+ assert [c["id"] for c in mine] == cats + ["l5-hw"]
494
+ hw = mine[-1]["write"]
495
+ assert hw["lines"] == 5 and "פתרו את כל השיעור החמישי" in hw["task"] and "זַאר / יִזוּר – ביקר" in hw["task"]
496
+ story = mine[cats.index("l5-story")]
497
+ assert story["story"] and "voices" not in story
498
+ pub = public_phrases()
499
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [13, 12, 13, 13, 4, 32, 23, 15, 12, 13, 11, 12]
500
+ reads = ("l5-meetread", "l5-neg", "l5-neg2", "l5-read", "l5-aread")
501
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l5-"))
502
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
503
+ assert by["l5-verbs", "היה (עבר, עתיד)"]["tr"] == "כַּאן, יִכּוּן" and by["l5-past", "אנחנו היינו"]["tr"] == "אִחְנַא כֻּנַّא"
504
+ assert ("انا قلت", "ʔ a n a | q u l t") in by["l5-past", "אני אמרתי"]["variants"] # rural ق accepted
505
+ # the negation: مش before a noun or an adjective, ما before a verb, which may also end in ش
506
+ p = next(p for p in PHRASES if p["cat"] == "l5-neg" and p["seq"] == 9)
507
+ assert p["tr"].startswith("אַנַא מַא קַעַדְתֵש עִנְד גַ'אר-י") and p["he"].startswith("אני לא ישבתי")
508
+ p = next(p for p in PHRASES if p["cat"] == "l5-neg2" and p["seq"] == 8)
509
+ assert p["tr"].endswith("קֻלְתִ-לّוֹ: וַאללַّה מַא אִשְתַרֵינַא אִשִי!") # والله is one word, not "and" + الله
510
+ p = next(p for p in PHRASES if p["cat"] == "l5-story" and p["seq"] == 5)
511
+ assert p["tr"].startswith("לַמַّא וְצִלְנַא עַלַא אֵ(ל)סّוּק") and p["variants"][0][1].split(" | ")[1] == "w sˤ i l n a"
512
+ assert by["l5-story", "ה' ייתן לך בריאות!"]["he_say"] == "אלוהים ייתן לך בריאות!"
513
+
514
+
515
+ def test_workbook_lesson_6():
516
+ from app.phrases import CATEGORIES, public_phrases
517
+
518
+ cats = ["l6-fut", "l6-futsay", "l6-verbs", "l6-past", "l6-read", "l6-read2", "l6-story", "l6-meet", "l6-meetread"]
519
+ mine = [c for c in CATEGORIES if c["lesson"] == "6"]
520
+ assert [c["id"] for c in mine] == cats + ["l6-hw"]
521
+ hw = mine[-1]["write"]
522
+ assert hw["lines"] == 5 and "פתרו את כל השיעור השישי" in hw["task"] and "פעם אחת בעבר ופעם אחת בעתיד" in hw["task"]
523
+ story = mine[cats.index("l6-story")]
524
+ assert story["story"] and "voices" not in story
525
+ pub = public_phrases()
526
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [24, 13, 5, 40, 11, 13, 12, 12, 12]
527
+ reads = ("l6-read", "l6-read2", "l6-meetread")
528
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l6-"))
529
+ assert all("seq" not in p for p in pub if p["cat"] in ("l6-fut", "l6-past")) # conjugations are drilled in random order
530
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
531
+ # the future: the Hebrew letters אית"ן on the future's base form, and אַ for "I" whatever the base form's vowel
532
+ assert by["l6-fut", "אני אשב"]["tr"] == "אַנַא אַקְעֹד" and by["l6-fut", "אנחנו נשב"]["tr"] == "אִחְנַא נֻקְעֹד"
533
+ assert by["l6-fut", "את תבקרי"]["tr"] == "אִנְתִי תְזוּרִי" and by["l6-fut", "הם ישמעו"]["he_say"] == "הֵם יִשְׁמְעוּ"
534
+ # فات is "entered" like lesson 3's دخل, so its card says more; اخد is the homework's fifth verb
535
+ assert by["l6-verbs", "נכנס ועבר (עבר, עתיד)"]["tr"] == "פַאת, יִפוּת" and by["l6-verbs", "לקח (עבר, עתיד)"]["tr"] == "אַחַ'ד, יֹאחֵ'ד"
536
+ assert by["l6-past", "אתם נכנסתם"]["tr"] == "אִנְתוּ פֻתّוּ" and by["l6-past", "אני ראיתי"]["tr"] == "אַנַא שֻפֵת"
537
+ p = next(p for p in PHRASES if p["cat"] == "l6-futsay" and p["seq"] == 3)
538
+ assert p["tr"] == "אִחְנַא סַבַּחְנַא פִי אֵלְבַּחַר, בֻּכְּרַא נִסְבַּח פִי אֵלְבִּרְכֵּה."
539
+ p = next(p for p in PHRASES if p["cat"] == "l6-story" and p["seq"] == 9)
540
+ assert p["tr"] == "לַוְ כַּאן עִנְד-י מַרַה זַיֵّ-כּ..." and p["variants"][0][0].endswith("زيّك...")
541
+
542
+
543
+ def test_workbook_lesson_7():
544
+ from app.phrases import CATEGORIES, public_phrases
545
+
546
+ # a review lesson: no new verbs and no story
547
+ cats = ["l7-fem", "l7-dual", "l7-nums", "l7-count", "l7-verbs", "l7-read", "l7-say", "l7-say2"]
548
+ mine = [c for c in CATEGORIES if c["lesson"] == "7"]
549
+ assert [c["id"] for c in mine] == cats + ["l7-hw"] and not any(c.get("story") for c in mine)
550
+ assert mine[0]["task"] == "אמרו בערבית מדוברת: בזכר ואז בנקבה" and mine[1]["task"].endswith("ביחיד ואז בזוגי")
551
+ hw = mine[-1]["write"]
552
+ assert hw["lines"] == 5 and "בשיעור השביעי" in hw["task"] and "פעם אחת בעבר ופעם אחת בעתיד" in hw["task"]
553
+ pub = public_phrases()
554
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [8, 13, 12, 15, 21, 23, 14, 13]
555
+ assert all(p["read"] == (p["cat"] == "l7-read") and "#" not in p["ar"] for p in pub if p["cat"].startswith("l7-"))
556
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
557
+ assert by["l7-fem", "רווק, רווקה"]["tr"] == "עַזַّאבִּי, עַזַّאבִּיֵّה" and by["l7-dual", "מיטה, שתי מיטות"]["tr"] == "תַחְ'ת, תַחְ'תֵין"
558
+ assert by["l7-nums", "מאה שבעים וחמש (175)"]["tr"] == "מִיֵّה חַ'מְסֵה וּסַבְּעִין" # units before tens
559
+ # 3-10: the noun in the plural and the number without its ة; from 11: the noun in the singular, and 11-19 with a ר
560
+ assert by["l7-count", "שבעה ילדים"]["tr"] == "סַבְּע אַוְלַאד" and by["l7-count", "שתים עשרה בנות"]["tr"] == "תְנַעְשַר בִּנְת"
561
+ # every verb of lessons 1-6, each still said in the past and the future together
562
+ verbs = [p for p in PHRASES if p["cat"] == "l7-verbs"]
563
+ assert [p["tr"] for p in verbs][:2] == ["פַתַח, יִפְתַח", "אַכַּל, יֹאכֹּל"] and verbs[-1]["tr"] == "פַאת, יִפוּת"
564
+ assert all(" | " in p["variants"][0][1] and p["he"].endswith("(עבר, עתיד)") for p in verbs)
565
+
566
+
567
+ def test_workbook_lesson_8():
568
+ from app.phrases import CATEGORIES, public_phrases
569
+
570
+ cats = ["l8-howmany", "l8-verbs", "l8-past", "l8-read", "l8-read2", "l8-read3", "l8-colors", "l8-colread", "l8-story",
571
+ "l8-colsay", "l8-colsay2"]
572
+ mine = [c for c in CATEGORIES if c["lesson"] == "8"]
573
+ assert [c["id"] for c in mine] == cats + ["l8-hw"]
574
+ hw = mine[-1]["write"]
575
+ assert hw["lines"] == 5 and "פתרו את כל השיעור השמיני" in hw["task"] and "פעלים וצבעים" in hw["task"]
576
+ song = mine[cats.index("l8-story")]
577
+ assert song["name"] == "שיר הצבעים" and song["story"] and "voices" not in song
578
+ assert mine[cats.index("l8-colors")]["task"].endswith("בזכר ואז בנקבה")
579
+ pub = public_phrases()
580
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [13, 5, 40, 12, 12, 13, 17, 13, 25, 10, 12]
581
+ reads = ("l8-read", "l8-read2", "l8-read3", "l8-colread")
582
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l8-"))
583
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
584
+ # גזרת פִעֵל: the first vowel is lost before the endings of "I", "you" and "we"
585
+ assert by["l8-verbs", "הבין (עבר, עתיד)"]["tr"] == "פִהֵם, יִפְהַם" and by["l8-verbs", "יצא ועלה (עבר, עתיד)"]["tr"] == "טִלֵע, יִטְלַע"
586
+ assert by["l8-past", "אני שתיתי"]["tr"] == "אַנַא שְרִבֵּת" and by["l8-past", "היא חזרה"]["tr"] == "הִיֵّ רִגְ'עַת"
587
+ # a colour is said in the masculine and the feminine together
588
+ assert by["l8-colors", "לבן, לבנה"]["tr"] == "אַבְּיַדֿ, בֵּידַֿא" and by["l8-colors", "לבן, לבנה"]["variants"][0][1] == "ʔ a b j a dˤ | b eː dˤ a"
589
+ assert by["l8-colors", "אפור, אפורה (מהמילה אפר)"]["tr"] == "רַמַאדִי, רַמַאדִיֵّה"
590
+ # how many: اكم and a singular noun for what is counted, قدّيش for what is not
591
+ p = next(p for p in PHRASES if p["cat"] == "l8-howmany" and p["seq"] == 9)
592
+ assert p["tr"].startswith("קַדֵّיש מַצַארִי בִּדّ-וֹ?")
593
+ p = next(p for p in PHRASES if p["cat"] == "l8-howmany" and p["seq"] == 2)
594
+ assert p["tr"].startswith("אַכַּם וַאחַד כַּאן פִי אֵ(ל)סַّיַّארַה?")
595
+ assert by["l8-story", "אני בשמיים נמצא"]["tr"] == "אַנַא פִי אֵ(ל)סַّמַאִי מַוְג'וּד"
596
+
597
+
598
+ def test_workbook_lesson_9():
599
+ from app.phrases import CATEGORIES, public_phrases
600
+
601
+ cats = ["l9-story", "l9-aread", "l9-asay", "l9-verbs", "l9-past", "l9-want", "l9-read", "l9-read2", "l9-say", "l9-mix",
602
+ "l9-mixread"]
603
+ mine = [c for c in CATEGORIES if c["lesson"] == "9"]
604
+ assert [c["id"] for c in mine] == cats + ["l9-hw"]
605
+ hw = mine[-1]["write"]
606
+ assert hw["lines"] == 5 and "השיעור התשיעי" in hw["task"] and "אחד בעבר ואחד בעתיד" in hw["task"]
607
+ story = mine[cats.index("l9-story")]
608
+ assert story["name"] == "סיפור הקפה" and story["story"] and "voices" not in story
609
+ pub = public_phrases()
610
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [15, 13, 12, 2, 16, 24, 13, 13, 12, 6, 6]
611
+ reads = ("l9-aread", "l9-read", "l9-read2", "l9-mixread")
612
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l9-"))
613
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
614
+ assert by["l9-verbs", "הודה (עבר, עתיד)"]["tr"] == "שַכַּר, יֻשְכֹּר"
615
+ # "to lunch" is one verb in Arabic: the Hebrew prompt names the meal, once
616
+ assert by["l9-verbs", "אכל צהריים (עבר, עתיד)"]["tr"] == "תְעַ'דַّא, יִתְעַ'דַّא"
617
+ assert by["l9-past", "אנחנו אכלנו צהריים"]["tr"] == "אִחְנַא תְעַ'דֵّינַא"
618
+ # wanting: بدّ with the person's suffix, then the verb in the future (Arabic has no infinitive)
619
+ assert by["l9-want", "אני רוצה לצאת"]["tr"] == "בִּדّ-י אַטְלַע" and by["l9-want", "את רוצה לצאת"]["tr"] == "בִּדֵّ-כּ תִטְלַעִי"
620
+ assert by["l9-want", "הם רוצים לאכול צהריים"]["tr"] == "בִּדّ-הֹם יִתְעַ'דּוּ"
621
+ assert by["l9-want", "אתה רוצה לשמוע"]["variants"][0][1] == "b i d d a k | t i s m a ʕ"
622
+ p = next(p for p in PHRASES if p["cat"] == "l9-story" and p["seq"] == 8)
623
+ assert p["tr"].startswith("שְרִבְּנַא קַהְוֵה סַאדַא")
624
+
625
+
626
+ def test_workbook_lesson_10():
627
+ from app.phrases import CATEGORIES, public_phrases
628
+
629
+ cats = ["l10-adj", "l10-compread", "l10-compread2", "l10-verbs", "l10-past", "l10-read", "l10-read2", "l10-say", "l10-say2",
630
+ "l10-dem", "l10-story", "l10-phrases"]
631
+ mine = [c for c in CATEGORIES if c["lesson"] == "10"]
632
+ assert [c["id"] for c in mine] == cats + ["l10-hw"]
633
+ hw = mine[-1]["write"]
634
+ assert hw["lines"] == 5 and "השיעור העשירי" in hw["task"] and "אחד בעבר ואחד בעתיד" in hw["task"]
635
+ story = mine[cats.index("l10-story")]
636
+ assert story["name"] == "סיפור דודי סעיד" and story["story"] and "voices" not in story
637
+ assert mine[0]["unit"] == "תואר" and mine[0]["task"].endswith("ערך היתרון שלו")
638
+ pub = public_phrases()
639
+ assert [sum(p["cat"] == c for p in pub) for c in cats] == [33, 8, 8, 5, 40, 12, 9, 12, 9, 12, 25, 19]
640
+ reads = ("l10-compread", "l10-compread2", "l10-read", "l10-read2")
641
+ assert all(p["read"] == (p["cat"] in reads) and "#" not in p["ar"] for p in pub if p["cat"].startswith("l10-"))
642
+ by = {(p["cat"], p["he"]): p for p in PHRASES}
643
+ # an adjective is said with its comparative (אַפְעַל)
644
+ assert by["l10-adj", "גדול, יותר גדול"]["tr"] == "כְּבִּיר, אַכְּבַּר"
645
+ assert by["l10-adj", "מת��ק, יותר מתוק"]["tr"] == "חִלוּ, אַחְלַא" and by["l10-adj", "חשוב, יותר חשוב"]["tr"] == "מֻהִםّ, אַהַםّ"
646
+ assert by["l10-adj", "טעים, יותר טעים (לאוכל בלבד)"]["tr"] == "זַאכִּי, אַזְכַּא"
647
+ assert by["l10-adj", "נקי, יותר נקי"]["variants"][0][1] == "n dˤ iː f | ʔ a n dˤ a f"
648
+ # גזרת פִעֵל again
649
+ assert by["l10-verbs", "שיחק (עבר, עתיד)"]["tr"] == "לִעֵבּ, יִלְעַבּ" and by["l10-verbs", "הגיע (עבר, עתיד)"]["tr"] == "וִצֵל, יוֹצַל"
650
+ assert by["l10-past", "אני שיחקתי"]["tr"] == "אַנַא לְעִבֵּת" and by["l10-past", "היא צחקה"]["tr"] == "הִיֵّ דִֿחְכַּת"
651
+ assert by["l10-past", "אנחנו הגענו"]["tr"] == "אִחְנַא וְצִלְנַא"
652
+ assert by["l10-dem", "אלה"]["tr"] == "הַדוֹל" and by["l10-dem", "הפעם הזאת"]["tr"] == "הַאדִי אֵלְמַרַّה"
653
+ p = next(p for p in PHRASES if p["cat"] == "l10-story" and p["seq"] == 8)
654
+ assert p["tr"].startswith("הוּّ קַאלִّ-י: וַלַא יְהִמֵّ-כּ")
655
+
656
+
657
  def test_sign_in_guards_the_practice_endpoints():
658
  from fastapi.testclient import TestClient
659