#!/usr/bin/env python3 """Import the image-bearing MCQs that scripts/import_examgoal.py skipped. Downloads each question figure from the examgoal CDN into static/qimg/ and rewrites to the local path, so the bank stays self-contained. Usage: python3 scripts/import_with_images.py DB_PATH [--pkl /tmp/db.pkl] Safe to re-run: dedupes on exact stem match, skips already-downloaded files. """ import argparse, hashlib, html, pickle, re, sqlite3, sys, urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "scripts")) sys.path.insert(0, str(ROOT / "scripts" / "examgoal_model")) from question import Question from chapter import Chapter from fix_math import fix_field ALLOWED = {"br", "p", "sup", "sub", "b", "strong", "i", "em", "u", "ol", "ul", "li", "table", "tbody", "thead", "tr", "th", "td", "span", "h3"} SUBJECT_FIX = {"mathematics": "Maths", "physics": "Physics", "chemistry": "Chemistry"} QIMG = ROOT / "static" / "qimg" class FixUnpickler(pickle.Unpickler): def find_class(self, module, name): if name == "Question": return Question if name == "Chapter": return Chapter return super().find_class(module, name) def title_case(slug: str) -> str: return " ".join(w.capitalize() for w in slug.replace("-", " ").split()) def shift_of(paper_title, exam_date) -> str: t = str(paper_title or "") low = t.lower() if "evening" in low: return "S2" if "morning" in low: return "S1" return "S1" def url_hash(u: str) -> str: return hashlib.sha1(u.encode()).hexdigest()[:16] def fetch(url: str, dest: Path, tries=3) -> bool: if dest.exists() and dest.stat().st_size > 0: return True for _ in range(tries): try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=20) as r: b = r.read() if b: dest.write_bytes(b) return True except Exception: pass return False def rewrite_images(qid: str, raw: str): """Download each , rewrite to /static/qimg/.png.""" ok_all = True def repl(m): nonlocal ok_all url = html.unescape(m.group(1)) name = f"{qid[:24]}-{url_hash(url)}.png" if fetch(url, QIMG / name): return f'' ok_all = False return "" out = re.sub(r']*src="([^"]+)"[^>]*>', repl, raw) return out, ok_all def sanitize_img(qid: str, raw: str): """sanitize() + surviving img tags with local src only.""" if not raw: return "", True text = html.unescape(str(raw)) # unwrap : keep the fallback inside, drop variants def unwrap_pic(m): inner = m.group(0) imgs = re.findall(r"]*>", inner) return imgs[0] if imgs else "" text = re.sub(r"", unwrap_pic, text, flags=re.S | re.I) text = re.sub(r"", "", text, flags=re.S | re.I) text = re.sub(r"", "", text, flags=re.S | re.I) text = re.sub(r"]*>", "", text, flags=re.I) text, ok = rewrite_images(qid, text) def tag(m): whole, closing, name = m.group(0), m.group(1), m.group(2).lower() if name == "img": # keep ONLY if src is already a local rewritten path if 'src="/static/qimg/' in whole: return whole return "" if name in ALLOWED: return f"<{closing}{name}>" return "" text = re.sub(r"<(/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s[^>]*)?)>", tag, text) text = re.sub(r"[ \t\xa0]+", " ", text) text = re.sub(r"\n\s*\n+", "\n", text) text = text.strip() return fix_field(text), ok def main(): ap = argparse.ArgumentParser() ap.add_argument("db") ap.add_argument("--pkl", default="/tmp/db.pkl") args = ap.parse_args() print(f"loading {args.pkl} ...", flush=True) with open(args.pkl, "rb") as f: data = FixUnpickler(f).load() QIMG.mkdir(parents=True, exist_ok=True) con = sqlite3.connect(args.db) cur = con.cursor() existing = {r[0] for r in cur.execute("SELECT stem FROM questions")} print(f"existing stems: {len(existing)}", flush=True) def ensure_ids(subject, chapter, subtopic): cur.execute("SELECT id FROM chapters WHERE subject=? AND name=?", (subject, chapter)) row = cur.fetchone() if row: ch_id = row[0] else: cur.execute("INSERT INTO chapters(subject,name) VALUES(?,?)", (subject, chapter)) ch_id = cur.lastrowid cur.execute("SELECT id FROM subtopics WHERE chapter_id=? AND name=?", (ch_id, subtopic)) row = cur.fetchone() if row: return ch_id, row[0] cur.execute("INSERT INTO subtopics(chapter_id,name) VALUES(?,?)", (ch_id, subtopic)) return ch_id, cur.lastrowid imported = skipped = dl_fail = opt_img = 0 diff_map = {"easy": "Easy", "medium": "Medium", "hard": "Hard"} for _key, chap in data.items(): for q in chap.question_dict.values(): d = q.__dict__ if d.get("type") != "mcq": continue co = d.get("correct_options") or [] if len(co) != 1 or co[0] not in ("A", "B", "C", "D"): continue stem_raw = d.get("question") or "" opts_raw = [o for o in (d.get("options") or []) if isinstance(o, dict)] opt_txt = " ".join((o.get("content") or "") for o in opts_raw) if "