207 lines
11 KiB
Python
207 lines
11 KiB
Python
import json, sys, re
|
|
|
|
p = "/Users/drjones/pest-nocturne/index.html"
|
|
s = open(p).read()
|
|
|
|
# build merged question bank
|
|
sys.path.insert(0, "/Users/drjones/pest-nocturne")
|
|
import wa_exam_data2 as d2
|
|
WA = []
|
|
for cat in ["Core", "Structural", "Ornamental & Turf", "Agricultural"]:
|
|
for q, a, c, x in d2.ALL[cat]:
|
|
WA.append({"cat": cat, "q": q, "a": a, "c": c, "x": x})
|
|
|
|
# add EM019 manual questions
|
|
EM = json.load(open("/tmp/em019_exam.json"))
|
|
ALLQ = WA + EM
|
|
|
|
print(f"WA original: {len(WA)}, EM019 manual: {len(EM)}, total: {len(ALLQ)}")
|
|
|
|
# remove the old WA Prep block (from its <script> to EOF)
|
|
marker = "<script>\n/* ==== WA PREP"
|
|
idx = s.find(marker)
|
|
if idx == -1:
|
|
marker2 = "/* ==== WA PREP"
|
|
idx = s.find(marker2)
|
|
# back up to the preceding <script>
|
|
idx = s.rfind("<script>", 0, idx)
|
|
assert idx != -1, "old WA Prep block not found"
|
|
s = s[:idx].rstrip() + "\n"
|
|
|
|
data_json = json.dumps(ALLQ)
|
|
|
|
RENDER = r'''
|
|
<script>
|
|
/* ==== WA PREP — original question bank + mock exam ==== */
|
|
const WAEXAM = __WADATA__;
|
|
|
|
(function(){
|
|
const CATS = ['All','Core','Structural','Ornamental & Turf','Agricultural','EM019 Manual'];
|
|
let mode = 'practice'; // 'practice' | 'mock'
|
|
let curCat = 'All', qi = 0, score = 0, answered = 0, qlist = [];
|
|
let mock = { qs: [], answers: [], secs: 0, timer: null };
|
|
|
|
function pick(n, arr){ return arr.map((x,i)=>({...x,_r:Math.random()})).sort((a,b)=>a._r-b._r).slice(0,n); }
|
|
function buildList(){
|
|
qlist = curCat === 'All' ? ALLQ.slice() : ALLQ.filter(q=>q.cat===curCat);
|
|
qlist = qlist.map((x,i)=>({...x,_r:Math.random()})).sort((a,b)=>a._r-b._r);
|
|
qi = 0; score = 0; answered = 0;
|
|
}
|
|
|
|
function fmt(secs){ const m=Math.floor(secs/60), ss=secs%60; return m+':'+String(ss).padStart(2,'0'); }
|
|
|
|
function render(){
|
|
const root = document.getElementById('v-waprep');
|
|
const cats = CATS.map(c=>`<button class="wac${c===curCat?' on':''}" data-wacat="${c}">${c}</button>`).join('');
|
|
let body = '';
|
|
|
|
if (mode === 'mock') {
|
|
const total = mock.qs.length;
|
|
if (mock.submitted) {
|
|
const pct = Math.round(mock.score/total*100);
|
|
body = `<div class="wa-res"><div class="wa-big">${mock.score} / ${total}</div>
|
|
<div class="wa-pct ${pct>=70?'pass':'fail'}">${pct}% — ${pct>=70?'PASS':'NOT PASSING (70% needed)'}</div>
|
|
<p class="lede">Time used: ${fmt(mock.secs)} (of 120:00). Review below.</p></div>
|
|
${mock.qs.map((q,i)=>{
|
|
const u = mock.answers[i];
|
|
const ok = u === q.c;
|
|
return `<div class="wa-rev ${ok?'ok':'bad'}">
|
|
<div class="wa-rq">${i+1}. ${q.q}</div>
|
|
<div class="wa-ro">Your answer: <b>${u==null?'(skipped)':String.fromCharCode(65+u)+'. '+q.a[u]}</b></div>
|
|
${ok?'':`<div class="wa-ro">Correct: <b>${String.fromCharCode(65+q.c)+'. '+q.a[q.c]}</b></div>`}
|
|
<div class="wa-expl" style="display:block">${q.x}</div>
|
|
</div>`;
|
|
}).join('')}
|
|
<div class="wa-nav" style="display:block;text-align:center"><button class="btn" id="wa-again">New Mock Exam</button></div>`;
|
|
} else {
|
|
const q = mock.qs[qi];
|
|
body = `<div class="wa-prog"><span>Question ${qi+1} / ${total}</span><span class="wa-timer" id="wa-timer">${fmt(mock.secs)}</span></div>
|
|
<div class="wa-q">${q.q}</div>
|
|
<div class="wa-opts">${q.a.map((opt,i)=>`<button class="wa-opt${mock.answers[qi]===i?' sel':''}" data-i="${i}">${String.fromCharCode(65+i)}. ${opt}</button>`).join('')}</div>
|
|
<div class="wa-nav" style="display:flex;justify-content:space-between;align-items:center">
|
|
<button class="btn ghost" id="wa-prev" ${qi===0?'disabled':''}>← Back</button>
|
|
${qi+1<total?'<button class="btn" id="wa-next2">Next →</button>':'<button class="btn" id="wa-submit">Submit Exam</button>'}
|
|
</div>`;
|
|
}
|
|
} else {
|
|
const total = qlist.length;
|
|
const q = qlist[qi];
|
|
body = `
|
|
<div class="wa-prog"><span>${Math.min(answered+1, total)} / ${total}</span><span class="wa-score">Score ${score}/${answered||1}</span></div>
|
|
<div class="wa-q">${q.q}</div>
|
|
<div class="wa-opts">${q.a.map((opt, i)=>`<button class="wa-opt" data-i="${i}">${String.fromCharCode(65+i)}. ${opt}</button>`).join('')}</div>
|
|
<div class="wa-expl" id="wa-expl" style="display:none"></div>
|
|
<div class="wa-nav" style="display:none"><button class="btn" id="wa-next">${qi+1 < total ? 'Next →' : 'Restart'}</button></div>`;
|
|
}
|
|
|
|
root.innerHTML = `
|
|
<section class="hero" style="padding-top:26px">
|
|
<div class="eyebrow">${ALLQ.length} questions · original + the manual's own exam · not scraped</div>
|
|
<h1><span class="lo">Pass the license</span>WA<br><em>Exam Prep</em></h1>
|
|
<p class="lede">Practice by category with instant feedback, or sit a timed 50-question mock exam (120 minutes, 70% to pass) built from the full bank.</p>
|
|
<div class="wa-cats">${cats}</div>
|
|
<div style="margin-top:14px">
|
|
<button class="btn" id="wa-mode">${mode==='practice'?'Start Mock Exam (50 Q · 120 min)':'Back to Practice'}</button>
|
|
</div>
|
|
</section>
|
|
<section class="section" style="padding-top:10px">
|
|
<div class="wa-card">${body}</div>
|
|
</section>`;
|
|
|
|
// wire category buttons
|
|
root.querySelectorAll('[data-wacat]').forEach(b=>b.addEventListener('click', ()=>{ curCat = b.dataset.wacat; buildList(); render(); }));
|
|
const modeBtn = root.querySelector('#wa-mode');
|
|
if (modeBtn) modeBtn.addEventListener('click', ()=>{
|
|
if (mode === 'practice') { startMock(); } else { mode = 'practice'; buildList(); render(); }
|
|
});
|
|
|
|
if (mode === 'practice') {
|
|
root.querySelectorAll('.wa-opt').forEach(b=>b.addEventListener('click', ()=>{
|
|
const i = +b.dataset.i, q = qlist[qi];
|
|
const correct = i === q.c;
|
|
if (correct) score++;
|
|
answered++;
|
|
root.querySelectorAll('.wa-opt').forEach(o=>{ o.disabled = true; if (+o.dataset.i === q.c) o.classList.add('right'); });
|
|
if (!correct) b.classList.add('wrong');
|
|
const expl = root.querySelector('#wa-expl');
|
|
expl.style.display = 'block';
|
|
expl.innerHTML = `<strong>${correct?'Correct':'Incorrect'}.</strong> ${q.x}`;
|
|
root.querySelector('.wa-nav').style.display = 'block';
|
|
root.querySelector('.wa-score').textContent = `Score ${score}/${answered}`;
|
|
}));
|
|
const nx = root.querySelector('#wa-next');
|
|
if (nx) nx.addEventListener('click', ()=>{ if (qi+1 < qlist.length) qi++; else buildList(); render(); });
|
|
} else if (!mock.submitted) {
|
|
root.querySelectorAll('.wa-opt').forEach(b=>b.addEventListener('click', ()=>{
|
|
mock.answers[qi] = +b.dataset.i;
|
|
render();
|
|
}));
|
|
const prev = root.querySelector('#wa-prev');
|
|
if (prev) prev.addEventListener('click', ()=>{ if (qi>0){ qi--; render(); } });
|
|
const n2 = root.querySelector('#wa-next2');
|
|
if (n2) n2.addEventListener('click', ()=>{ qi++; render(); });
|
|
const sub = root.querySelector('#wa-submit');
|
|
if (sub) sub.addEventListener('click', ()=>{ submitMock(); });
|
|
}
|
|
const again = root.querySelector('#wa-again');
|
|
if (again) again.addEventListener('click', ()=>{ startMock(); });
|
|
}
|
|
|
|
function startMock(){
|
|
mode = 'mock';
|
|
const qs = pick(50, ALLQ);
|
|
mock = { qs: qs, answers: new Array(qs.length).fill(null), secs: 0, submitted: false, timer: null, score: 0 };
|
|
clearInterval(mock.timer);
|
|
mock.timer = setInterval(()=>{ if (!mock.submitted) { mock.secs++; const t=document.getElementById('wa-timer'); if (t) t.textContent = fmt(mock.secs); if (mock.secs >= 7200) submitMock(); } }, 1000);
|
|
qi = 0;
|
|
render();
|
|
}
|
|
function submitMock(){
|
|
clearInterval(mock.timer);
|
|
mock.score = mock.qs.reduce((s,q,i)=> s + (mock.answers[i]===q.c ? 1 : 0), 0);
|
|
mock.submitted = true;
|
|
render();
|
|
}
|
|
|
|
const origGo = window.go;
|
|
window.go = function(v){
|
|
if (v === 'waprep') { if (mode==='mock' && !mock.submitted && mock.qs.length) { /* keep state */ } else { buildList(); } render(); }
|
|
origGo(v);
|
|
};
|
|
})();
|
|
</script>
|
|
<style>
|
|
.wa-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
|
|
.wa-cats .wac{padding:8px 16px;border-radius:999px;border:1px solid var(--line,#2C3455);background:var(--surface,#141A30);color:var(--text,#EDE7D6);cursor:pointer;font-size:.85rem}
|
|
.wa-cats .wac.on{background:var(--brass,#E3A53F);color:#1a1206;border-color:var(--brass,#E3A53F);font-weight:700}
|
|
.wa-card{background:var(--surface,#141A30);border:1px solid var(--line,#2C3455);border-radius:14px;padding:24px;max-width:760px;margin:0 auto}
|
|
.wa-prog{display:flex;justify-content:space-between;font-family:"JetBrains Mono",monospace;font-size:.78rem;color:var(--text-faint,#6E7688);margin-bottom:16px}
|
|
.wa-timer{color:var(--brass,#E3A53F);font-weight:700}
|
|
.wa-score{color:var(--verdigris,#54BFA9)}
|
|
.wa-q{font-family:Fraunces,serif;font-size:1.3rem;line-height:1.4;color:var(--text,#EDE7D6);margin-bottom:18px}
|
|
.wa-opts{display:flex;flex-direction:column;gap:10px}
|
|
.wa-opt{text-align:left;padding:12px 16px;border-radius:10px;border:1px solid var(--line,#2C3455);background:var(--surface-2,#1B2239);color:var(--text,#EDE7D6);cursor:pointer;font-size:.95rem;line-height:1.4}
|
|
.wa-opt:hover:not(:disabled){border-color:var(--brass,#E3A53F)}
|
|
.wa-opt.sel{border-color:var(--brass,#E3A53F);background:color-mix(in srgb,var(--brass,#E3A53F) 14%,transparent)}
|
|
.wa-opt.right{background:color-mix(in srgb,var(--verdigris,#54BFA9) 22%,transparent);border-color:var(--verdigris,#54BFA9)}
|
|
.wa-opt.wrong{background:color-mix(in srgb,var(--crimson,#DE5F45) 22%,transparent);border-color:var(--crimson,#DE5F45)}
|
|
.wa-opt:disabled{cursor:default;opacity:.9}
|
|
.wa-expl{margin-top:16px;padding:12px 16px;border-left:3px solid var(--verdigris,#54BFA9);background:color-mix(in srgb,var(--verdigris,#54BFA9) 9%,transparent);border-radius:8px;font-size:.9rem;line-height:1.5;color:var(--text,#EDE7D6)}
|
|
.wa-nav{margin-top:16px;text-align:right}
|
|
.wa-res{text-align:center;padding:20px 0}
|
|
.wa-big{font-family:Fraunces,serif;font-size:3rem;color:var(--text,#EDE7D6)}
|
|
.wa-pct{font-size:1.2rem;font-weight:700;margin:6px 0 14px}
|
|
.wa-pct.pass{color:var(--verdigris,#54BFA9)}
|
|
.wa-pct.fail{color:var(--crimson,#DE5F45)}
|
|
.wa-rev{border:1px solid var(--line,#2C3455);border-radius:10px;padding:14px;margin-bottom:12px;text-align:left}
|
|
.wa-rev.ok{border-left:3px solid var(--verdigris,#54BFA9)}
|
|
.wa-rev.bad{border-left:3px solid var(--crimson,#DE5F45)}
|
|
.wa-rq{font-weight:600;color:var(--text,#EDE7D6);margin-bottom:6px}
|
|
.wa-ro{font-size:.88rem;color:var(--text-dim,#A3A99C);margin-bottom:4px}
|
|
</style>
|
|
'''.replace('__WADATA__', data_json)
|
|
|
|
s = s.rstrip() + '\n' + RENDER + '\n'
|
|
open(p, "w").write(s)
|
|
print(f"rebuilt WA Prep with {len(ALLQ)} questions + mock exam; size {len(s)}")
|