3v324v23 commited on
Commit
def530e
·
1 Parent(s): aeb78ec

choosing classes

Browse files
Files changed (3) hide show
  1. static/math.css +8 -1
  2. static/math.js +29 -3
  3. tests/test_phone_browser.py +14 -0
static/math.css CHANGED
@@ -212,6 +212,13 @@ html.math-on, html.math-on body{overflow:hidden}
212
  .mx-class-form input[type="text"],.mx-class-form input[name="name"],.mx-class-form textarea{box-sizing:border-box;width:100%;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);padding:11px 12px;font:inherit}
213
  .mx-class-form textarea{min-height:110px;resize:vertical;text-align:left}
214
  .mx-class-form fieldset{margin:0}
 
 
 
 
 
 
 
215
  .mx-unit-picks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
216
  .mx-unit-picks label{display:flex;align-items:center;gap:9px;border:1px solid var(--line);border-radius:11px;padding:10px;cursor:pointer}
217
  .mx-unit-picks label:has(input:checked){border-color:var(--mx-accent);background:var(--mx-accent-soft)}
@@ -263,7 +270,7 @@ html.math-on, html.math-on body{overflow:hidden}
263
  .mx-qdots i.now{outline:2px solid var(--mx-accent);outline-offset:1px}
264
  .mx-count{font:700 11px/1 var(--f-body);padding:4px 7px;border-radius:999px;background:var(--bg);color:var(--muted)}
265
  .mx-content-subject{display:flex;flex-direction:column;gap:8px;margin-top:14px}
266
- .mx-content-subject>h5{display:flex;align-items:baseline;gap:8px;margin:0;font-size:18px;color:var(--exam,var(--mx-accent))}
267
  .mx-content-subject>h5 small{color:var(--muted);font-size:13px;font-weight:500}
268
  .mx-content-unit{border:1px solid var(--line);border-radius:12px;background:var(--bg)}
269
  .mx-content-unit summary{display:flex;flex-direction:column;gap:3px;padding:12px;cursor:pointer}
 
212
  .mx-class-form input[type="text"],.mx-class-form input[name="name"],.mx-class-form textarea{box-sizing:border-box;width:100%;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);padding:11px 12px;font:inherit}
213
  .mx-class-form textarea{min-height:110px;resize:vertical;text-align:left}
214
  .mx-class-form fieldset{margin:0}
215
+ .mx-subject-picks{--exam:var(--mx-accent);display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:8px}
216
+ .mx-subject-picks label{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:13px;padding:9px 10px;cursor:pointer}
217
+ .mx-subject-picks label:has(input:checked),.mx-subject-picks label:has(input:indeterminate){border-color:var(--exam);background:color-mix(in srgb,var(--exam) 14%,var(--surface))}
218
+ .mx-subject-picks .mx-subject-icon{width:34px;height:34px;border-radius:10px;font-size:18px}
219
+ .mx-subject-picks label>span:last-child{display:flex;flex-direction:column;gap:2px;font-weight:700}
220
+ .mx-subject-picks label small{color:var(--muted);font-size:11px;font-weight:500}
221
+ .mx-subject-units[hidden],.mx-no-subject[hidden]{display:none}
222
  .mx-unit-picks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
223
  .mx-unit-picks label{display:flex;align-items:center;gap:9px;border:1px solid var(--line);border-radius:11px;padding:10px;cursor:pointer}
224
  .mx-unit-picks label:has(input:checked){border-color:var(--mx-accent);background:var(--mx-accent-soft)}
 
270
  .mx-qdots i.now{outline:2px solid var(--mx-accent);outline-offset:1px}
271
  .mx-count{font:700 11px/1 var(--f-body);padding:4px 7px;border-radius:999px;background:var(--bg);color:var(--muted)}
272
  .mx-content-subject{display:flex;flex-direction:column;gap:8px;margin-top:14px}
273
+ .mx-content-subject>h5{display:flex;align-items:baseline;gap:8px;margin:0;font-size:18px;color:var(--exam)}
274
  .mx-content-subject>h5 small{color:var(--muted);font-size:13px;font-weight:500}
275
  .mx-content-unit{border:1px solid var(--line);border-radius:12px;background:var(--bg)}
276
  .mx-content-unit summary{display:flex;flex-direction:column;gap:3px;padding:12px;cursor:pointer}
static/math.js CHANGED
@@ -402,19 +402,45 @@ function manageClassAction(action,id){
402
 
403
  function openClassEditor(row){
404
  const box=sheet.querySelector(".mx-management"), creating=!row;
405
- const selected=new Set(row?.lessons||G.units.map(unit=>unit.id));
 
406
  box.innerHTML=`<div class="mx-manage-title"><h4>${creating?"כיתה חדשה":`עריכת ${esc(row.name)}`}</h4><button class="mx-btn" type="button" data-class-action="cancel">חזרה לכיתות</button></div>
407
  <form class="mx-class-form"><label>שם הכיתה<input name="name" required maxlength="80" autocomplete="off" placeholder="למשל: ט׳ 1" value="${esc(row?.name||"")}"></label>
408
- <fieldset><legend>יחידות הלימוד<small>התלמידים יראו במסלול רק את המיומנויות של היחידות שסומנו. מקצוע היחידה קובע אילו מבחנים יוצגו במסלול.</small></legend>${[...new Set(G.units.map(unit=>unit.subject))].map(subject=>`<h6 class="mx-pick-subject">${esc(subject)}</h6><div class="mx-unit-picks">${G.units.filter(unit=>unit.subject===subject).map(unit=>{const skills=unitSkills(unit.id);return `<label class="u-${unit.id}"><input type="checkbox" name="unit" value="${esc(unit.id)}"${selected.has(unit.id)?" checked":""}><span>${esc(unit.name)}<small>${skills.length} תת־נושאים · ${questionCount(skills)} שאלות</small></span></label>`}).join("")}</div>`).join("")}</fieldset>
 
 
 
 
 
409
  <label>התלמידים<small>כתובת דוא״ל אחת בכל שורה. תלמיד יכול להשתייך ליותר מכיתה אחת.</small><textarea name="emails" dir="ltr" spellcheck="false" placeholder="student@example.com">${esc((row?.emails||[]).join("\n"))}</textarea></label>
410
  <label>מורי הכיתה<small>כתובת דוא״ל אחת בכל שורה. למורים תהיה גישה לכיתה הזו.</small><textarea name="teachers" dir="ltr" spellcheck="false" placeholder="teacher@example.com">${esc((row?.teachers||[]).join("\n"))}</textarea></label>
411
  <div class="mx-class-form-actions"><button class="mx-btn solid" type="submit">${creating?"פתיחת הכיתה":"שמירת השינויים"}</button><button class="mx-btn" type="button" data-class-action="cancel">ביטול</button><span role="status" aria-live="polite"></span></div></form>`;
412
  const form=box.querySelector("form"), message=form.querySelector('[role="status"]');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  form.querySelector('input[name="name"]').focus();
414
  form.onsubmit=async event=>{
415
  event.preventDefault();
416
  const lessons=[...form.querySelectorAll('input[name="unit"]:checked')].map(input=>input.value);
417
- if(!lessons.length){message.textContent="בחרו לפחות יחידת לימוד אחת.";return;}
418
  const lines=name=>form.elements[name].value.split(/[\n,;]+/).map(value=>value.trim().toLowerCase()).filter(Boolean);
419
  const body={name:form.elements.name.value.trim(),emails:lines("emails"),teachers:lines("teachers"),lessons};
420
  const buttons=form.querySelectorAll("button"); buttons.forEach(button=>button.disabled=true); message.textContent=creating?"פותחים את הכיתה…":"שומרים…";
 
402
 
403
  function openClassEditor(row){
404
  const box=sheet.querySelector(".mx-management"), creating=!row;
405
+ const selected=new Set(row?.lessons||[]); // a new class starts with no subject chosen
406
+ const subjects=[...new Set(G.units.map(unit=>unit.subject))], subjectUnits=subject=>G.units.filter(unit=>unit.subject===subject);
407
  box.innerHTML=`<div class="mx-manage-title"><h4>${creating?"כיתה חדשה":`עריכת ${esc(row.name)}`}</h4><button class="mx-btn" type="button" data-class-action="cancel">חזרה לכיתות</button></div>
408
  <form class="mx-class-form"><label>שם הכיתה<input name="name" required maxlength="80" autocomplete="off" placeholder="למשל: ט׳ 1" value="${esc(row?.name||"")}"></label>
409
+ <fieldset><legend>מקצועות<small>בחרו את המקצועות שהכיתה לומדת. בחירת מקצוע מסמנת את כל יחידות הלימוד שלו, ואפשר לבטל יחידות שהכיתה לא לומדת.</small></legend>
410
+ <div class="mx-subject-picks">${subjects.map(subject=>{const [icon,tone]=examLooks[subject]||["●","other"], units=subjectUnits(subject);
411
+ return `<label class="exam-${tone}"><input type="checkbox" name="subject" value="${esc(subject)}"><span aria-hidden="true" class="mx-subject-icon">${icon}</span><span>${esc(subject)}<small>${units.length} יחידות</small></span></label>`}).join("")}</div></fieldset>
412
+ <fieldset class="mx-unit-fieldset"><legend>יחידות הלימוד<small>התלמידים יראו במסלול רק את המיומנויות של היחידות שסומנו. מקצוע היחידה קובע אילו מבחנים יוצגו במסלול.</small></legend>
413
+ <p class="mx-note mx-no-subject">בחרו מקצוע כדי לראות את יחידות הלימוד שלו.</p>
414
+ ${subjects.map(subject=>`<section class="mx-subject-units" data-subject-units="${esc(subject)}"><h6 class="mx-pick-subject">${esc(subject)}</h6><div class="mx-unit-picks">${subjectUnits(subject).map(unit=>{const skills=unitSkills(unit.id);return `<label class="u-${unit.id}"><input type="checkbox" name="unit" value="${esc(unit.id)}" data-subject="${esc(subject)}"${selected.has(unit.id)?" checked":""}><span>${esc(unit.name)}<small>${skills.length} תת־נושאים · ${questionCount(skills)} שאלות</small></span></label>`}).join("")}</div></section>`).join("")}</fieldset>
415
  <label>התלמידים<small>כתובת דוא״ל אחת בכל שורה. תלמיד יכול להשתייך ליותר מכיתה אחת.</small><textarea name="emails" dir="ltr" spellcheck="false" placeholder="student@example.com">${esc((row?.emails||[]).join("\n"))}</textarea></label>
416
  <label>מורי הכיתה<small>כתובת דוא״ל אחת בכל שורה. למורים תהיה גישה לכיתה הזו.</small><textarea name="teachers" dir="ltr" spellcheck="false" placeholder="teacher@example.com">${esc((row?.teachers||[]).join("\n"))}</textarea></label>
417
  <div class="mx-class-form-actions"><button class="mx-btn solid" type="submit">${creating?"פתיחת הכיתה":"שמירת השינויים"}</button><button class="mx-btn" type="button" data-class-action="cancel">ביטול</button><span role="status" aria-live="polite"></span></div></form>`;
418
  const form=box.querySelector("form"), message=form.querySelector('[role="status"]');
419
+ const unitBoxes=subject=>[...form.querySelectorAll('input[name="unit"]')].filter(input=>input.dataset.subject===subject);
420
+ // a subject is chosen while any of its units is; a subject's units stay in view from choosing it until it is cleared
421
+ const open=new Set(subjects.filter(subject=>subjectUnits(subject).some(unit=>selected.has(unit.id))));
422
+ const syncSubjects=()=>{
423
+ form.querySelectorAll('input[name="subject"]').forEach(input=>{
424
+ const boxes=unitBoxes(input.value), on=boxes.filter(box=>box.checked).length;
425
+ input.checked=on>0; input.indeterminate=on>0&&on<boxes.length;
426
+ form.querySelector(`[data-subject-units="${CSS.escape(input.value)}"]`).hidden=!open.has(input.value);
427
+ });
428
+ form.querySelector(".mx-no-subject").hidden=open.size>0;
429
+ };
430
+ form.addEventListener("change",event=>{
431
+ const input=event.target;
432
+ if(input.name==="subject"){
433
+ unitBoxes(input.value).forEach(box=>box.checked=input.checked);
434
+ if(input.checked) open.add(input.value); else open.delete(input.value);
435
+ }
436
+ if(input.name==="subject"||input.name==="unit") syncSubjects();
437
+ });
438
+ syncSubjects();
439
  form.querySelector('input[name="name"]').focus();
440
  form.onsubmit=async event=>{
441
  event.preventDefault();
442
  const lessons=[...form.querySelectorAll('input[name="unit"]:checked')].map(input=>input.value);
443
+ if(!lessons.length){message.textContent="בחרו לפחות מקצוע אחד.";return;}
444
  const lines=name=>form.elements[name].value.split(/[\n,;]+/).map(value=>value.trim().toLowerCase()).filter(Boolean);
445
  const body={name:form.elements.name.value.trim(),emails:lines("emails"),teachers:lines("teachers"),lessons};
446
  const buttons=form.querySelectorAll("button"); buttons.forEach(button=>button.disabled=true); message.textContent=creating?"פותחים את הכיתה…":"שומרים…";
tests/test_phone_browser.py CHANGED
@@ -152,7 +152,21 @@ def test_class_management_geometry_and_permissions(phone_server, manager):
152
  playwright.expect(page.locator(".mx-exam-countdown time")).to_contain_text("16 בנובמבר 2026")
153
  button.click()
154
  playwright.expect(page.locator(".mx-exam-board")).to_contain_text("מתמטיקה")
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  page.get_by_role("button", name="עריכה").click()
 
156
  geometry = page.locator('input[name="unit"][value="a"]')
157
  geometry.check()
158
  page.get_by_role("button", name="שמירת השינויים").click()
 
152
  playwright.expect(page.locator(".mx-exam-countdown time")).to_contain_text("16 בנובמבר 2026")
153
  button.click()
154
  playwright.expect(page.locator(".mx-exam-board")).to_contain_text("מתמטיקה")
155
+ page.get_by_role("button", name="+ כיתה חדשה").click()
156
+ subject = page.locator('input[name="subject"][value="מתמטיקה"]')
157
+ units = page.locator('input[name="unit"][data-subject="מתמטיקה"]')
158
+ playwright.expect(subject).not_to_be_checked()
159
+ playwright.expect(page.locator('[data-subject-units="מתמטיקה"]')).to_be_hidden()
160
+ subject.check()
161
+ playwright.expect(page.locator('[data-subject-units="מתמטיקה"]')).to_be_visible()
162
+ assert units.count() > 1 and all(units.nth(i).is_checked() for i in range(units.count()))
163
+ units.first.uncheck()
164
+ assert subject.evaluate("input => input.checked && input.indeterminate")
165
+ subject.uncheck()
166
+ assert not any(units.nth(i).is_checked() for i in range(units.count()))
167
+ page.get_by_role("button", name="ביטול").click()
168
  page.get_by_role("button", name="עריכה").click()
169
+ playwright.expect(page.locator('input[name="subject"][value="מתמטיקה"]')).to_be_checked()
170
  geometry = page.locator('input[name="unit"][value="a"]')
171
  geometry.check()
172
  page.get_by_role("button", name="שמירת השינויים").click()