Auto-commit 2026-09-07 18:49: 1 file changed, 215 insertions(+), 192 deletions(-)

This commit is contained in:
herzogflorian 2026-09-07 18:49:29 +02:00
parent 816402334b
commit e06d698802

View File

@ -1,41 +1,38 @@
#!/usr/bin/env python3
"""Fill an AISE502 Moodle course backup (.mbz, moodle2 format) with the generated section content.
"""Add the AISE502 course-page content to a Moodle course backup (.mbz, moodle2 format, Moodle 4.4+/5.x).
python3 src/fill_mbz.py inspect <backup.mbz>
lists Moodle release, course format, every section (number, id, name) and the activities in it
python3 src/fill_mbz.py fill <backup.mbz> <out.mbz> [--summary]
week sections 1..14: section name -> "Week N: ...", the three template text areas (Lernziele /
Theorie / Selbststudium) -> blocks 1..3 of week_NN.html; two sections appended for the lecture
script and the project (cloned from the last week section, one text area each).
--summary: write each week's three blocks into the section description instead of text areas.
python3 moodle/src/fill_mbz.py inspect <backup.mbz>
python3 moodle/src/fill_mbz.py fill <backup.mbz> <out.mbz> [--text-only]
--text-only: text areas only, no file resources (small archive; upload the PDFs by hand)
Only text fields are changed; ids of new sections/labels are fresh unique numbers; everything else in
the archive is copied untouched. The result is validated structurally (all referenced directories exist,
XML parses) but a restore in Moodle is the real test.
fill appends 16 sections after the existing ones (weeks 1-14, lecture script, project exercise):
week N : text area block 1 | text area block 2 | file: slides of lecture N | text area block 3
script : text area (lecture_script.html) | file: script PDF
project : text area (project_exercise.html) | files: exercise sheet PDF, project slides PDF, UI sketches ZIP
Existing sections and activities stay untouched. New sections are cloned from the template week
("Woche n"), text areas from a plain label, file resources from an existing file resource.
Restore the result into the course with "Zusammenführen" (merge) and untick the existing sections
in the schema step, so that nothing is duplicated.
"""
import io, pathlib, re, shutil, sys, tarfile, tempfile, zipfile, html
import hashlib, html, itertools, json, pathlib, re, shutil, sys, tarfile, tempfile, time, zipfile
import xml.etree.ElementTree as ET
HERE = pathlib.Path(__file__).resolve().parent
OUT = HERE.parent
REPO = pathlib.Path(__file__).resolve().parents[2]
MOODLE = REPO / "moodle"
BASE_ID = 20_000_000
# ----------------------------------------------------------------------------- archive helpers
# ----------------------------------------------------------------------------- helpers
def unpack(mbz, dest):
data = open(mbz, "rb").read(4)
if data[:2] == b"PK":
with zipfile.ZipFile(mbz) as z: z.extractall(dest); return "zip"
with tarfile.open(mbz, "r:gz") as t: t.extractall(dest); return "tgz"
def pack(src, out, kind):
src = pathlib.Path(src)
if kind == "zip":
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for p in sorted(src.rglob("*")):
if p.is_file(): z.write(p, p.relative_to(src).as_posix())
if open(mbz, "rb").read(2) == b"PK":
with zipfile.ZipFile(mbz) as z: z.extractall(dest)
else:
with tarfile.open(out, "w:gz") as t:
for p in sorted(src.rglob("*")):
if p.is_file(): t.add(p, p.relative_to(src).as_posix())
with tarfile.open(mbz, "r:gz") as t: t.extractall(dest)
def pack_zip(src, out):
src = pathlib.Path(src)
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for p in sorted(src.rglob("*")):
if p.is_file(): z.write(p, p.relative_to(src).as_posix())
def read(p): return pathlib.Path(p).read_text(encoding="utf-8")
def write(p, s): pathlib.Path(p).write_text(s, encoding="utf-8")
@ -44,197 +41,223 @@ def tag(xml, name):
m = re.search(rf"<{name}>(.*?)</{name}>", xml, re.S)
return html.unescape(m.group(1)) if m else None
def set_tag(xml, name, value, count=1):
esc = html.escape(value, quote=False)
new, n = re.subn(rf"<{name}>.*?</{name}>", lambda m: f"<{name}>{esc}</{name}>", xml, count=count, flags=re.S)
if n == 0: # empty tag form <name/> or <name></name>
new, n = re.subn(rf"<{name}\s*/>", f"<{name}>{esc}</{name}>", xml, count=count)
assert n, f"tag {name} not found"
def set_tag(xml, name, value):
esc = html.escape(str(value), quote=False)
new, n = re.subn(rf"<{name}>.*?</{name}>", lambda m: f"<{name}>{esc}</{name}>", xml, count=1, flags=re.S)
if n == 0:
new, n = re.subn(rf"<{name}\s*/>", f"<{name}>{esc}</{name}>", xml, count=1)
assert n, f"tag <{name}> not found"
return new
def text_of(intro, n=60):
t = html.unescape(re.sub(r"<[^>]+>", " ", intro)); t = re.sub(r"\s+", " ", t).strip()
return t[:n]
# ----------------------------------------------------------------------------- model
class Backup:
def __init__(self, root):
self.root = pathlib.Path(root)
self.mb = read(self.root / "moodle_backup.xml")
self.release = tag(self.mb, "moodle_release")
self.format = tag(self.mb, "format") # course format e.g. topics
# sections: from moodle_backup.xml contents
self.course_format = tag(self.mb, "original_course_format")
contents = re.search(r"<contents>(.*?)</contents>", self.mb, re.S).group(1)
self.sections = []
for m in re.finditer(r"<section>\s*<sectionid>(\d+)</sectionid>\s*<title>(.*?)</title>\s*<directory>(.*?)</directory>\s*</section>", self.mb, re.S):
sid, title, d = int(m.group(1)), m.group(2), m.group(3)
sx = read(self.root / d / "section.xml")
self.sections.append(dict(id=sid, dir=d, number=int(tag(sx, "number")), name=tag(sx, "name") or "",
for s in re.findall(r"<section>.*?</section>", contents, re.S):
d = tag(s, "directory"); sx = read(self.root / d / "section.xml")
self.sections.append(dict(id=int(tag(s, "sectionid")), dir=d, number=int(tag(sx, "number")), name=tag(sx, "name") or "",
sequence=[int(x) for x in (tag(sx, "sequence") or "").split(",") if x.strip()],
visible=tag(sx, "visible")))
visible=tag(sx, "visible"), component=tag(sx, "component")))
self.sections.sort(key=lambda s: s["number"])
self.activities = {}
for m in re.finditer(r"<activity>\s*<moduleid>(\d+)</moduleid>\s*<sectionid>(\d+)</sectionid>\s*<modulename>(.*?)</modulename>\s*<title>(.*?)</title>\s*<directory>(.*?)</directory>\s*</activity>", self.mb, re.S):
mid, sid, mod, title, d = int(m.group(1)), int(m.group(2)), m.group(3), html.unescape(m.group(4)), m.group(5)
a = dict(moduleid=mid, sectionid=sid, modname=mod, title=title, dir=d)
mx = read(self.root / d / "module.xml")
a["visible"] = tag(mx, "visible")
if mod == "label":
lx = read(self.root / d / "label.xml")
a["intro"] = tag(lx, "intro") or ""
self.activities[mid] = a
def label_kind(self, a):
t = re.sub(r"<[^>]+>", " ", a.get("intro", ""))
t = html.unescape(t)
if a["visible"] == "0" or "Auftrag Dozierende" in t: return "hidden"
if "Lernziele" in t or "Learning objectives" in t: return "objectives"
if "Theorie" in t or "Theory" in t: return "theory"
if "Selbststudium" in t or "Self-study" in t: return "selfstudy"
return "other"
for a in re.findall(r"<activity>.*?</activity>", contents, re.S):
mid = int(tag(a, "moduleid")); d = tag(a, "directory"); mod = tag(a, "modulename")
rec = dict(moduleid=mid, sectionid=int(tag(a, "sectionid")), modname=mod, title=tag(a, "title"), dir=d)
rec["visible"] = tag(read(self.root / d / "module.xml"), "visible")
fr = re.search(r"<fileref>(.*?)</fileref>", read(self.root / d / "inforef.xml"), re.S)
rec["filerefs"] = [int(x) for x in re.findall(r"<id>(\d+)</id>", fr.group(1))] if fr else []
if mod == "label": rec["intro"] = tag(read(self.root / d / "label.xml"), "intro") or ""
if mod == "resource":
fx = self.files_xml()
rec["files"] = [tag(f, "filename") for f in re.findall(r"<file id=\"\d+\">.*?</file>", fx, re.S)
if int(re.search(r'<file id="(\d+)"', f).group(1)) in rec["filerefs"] and tag(f, "filename") != "."]
self.activities[mid] = rec
def files_xml(self):
if not hasattr(self, "_fx"): self._fx = read(self.root / "files.xml")
return self._fx
def inspect(mbz):
with tempfile.TemporaryDirectory() as tmp:
kind = unpack(mbz, tmp)
b = Backup(tmp)
print(f"archive: {kind} · Moodle {b.release} · course format: {b.format} · {len(b.sections)} sections · {len(b.activities)} activities")
unpack(mbz, tmp); b = Backup(tmp)
print(f"Moodle {b.release} · course format {b.course_format} · {len(b.sections)} sections · {len(b.activities)} activities · {pathlib.Path(mbz).stat().st_size/1e6:.1f} MB")
for s in b.sections:
print(f"\n[{s['number']}] id={s['id']} visible={s['visible']} name={s['name']!r}")
print(f"\n[{s['number']}] id={s['id']} visible={s['visible']} {s['name']!r}" + (f" (delegated: {s['component']})" if s['component'] not in (None, '$@NULL@$') else ""))
for mid in s["sequence"]:
a = b.activities.get(mid)
if not a: print(f" ? module {mid} not in backup contents"); continue
kind_ = b.label_kind(a) if a["modname"] == "label" else "-"
print(f" {a['modname']:<10} id={mid:<6} visible={a['visible']} kind={kind_:<10} {a['title'][:70]!r}")
if not a: print(f" ? module {mid} missing"); continue
extra = f" files={a['files']}" if a["modname"] == "resource" else ""
print(f" {a['modname']:<10} {mid:<9} visible={a['visible']} {a['title'][:70]!r}{extra}")
# ----------------------------------------------------------------------------- filling
def blocks_of(week):
t = read(OUT / f"week_{week:02d}.html")
# ----------------------------------------------------------------------------- content
def week_blocks(n, with_files=True):
t = read(MOODLE / f"week_{n:02d}.html")
parts = re.split(r"<!-- BLOCK \d · [^>]*-->\n", t)
assert len(parts) == 4, f"week {week}: expected 3 blocks, got {len(parts)-1}"
assert len(parts) == 4, f"week {n}: expected 3 blocks"
title = re.search(r"Section name\): (.*)", t).group(1).strip()
return title, [p.strip() + "\n" for p in parts[1:]]
blocks = [p.strip() + "\n" for p in parts[1:]]
if with_files:
blocks[1] = blocks[1].replace("<em>(added below by the lecturer)</em>", "<em>(file below)</em>")
return title, blocks
def label_name(intro):
t = html.unescape(re.sub(r"<[^>]+>", " ", intro)); t = re.sub(r"\s+", " ", t).strip()
return t[:60]
def fixed_section(name, with_files=True):
t = read(MOODLE / name)
t = re.sub(r"^<!--.*?-->\s*", "", t, count=1, flags=re.S)
if not with_files:
return t.strip() + "\n"
t = t.replace("AISE502_Vorlesung_Skript.pdf [link]", "AISE502_Vorlesung_Skript.pdf the file directly below")
t = t.replace(" [link]", " (file below)")
return t.strip() + "\n"
def set_label(b, a, intro_html):
p = b.root / a["dir"] / "label.xml"
lx = read(p)
lx = set_tag(lx, "intro", intro_html)
lx = set_tag(lx, "name", label_name(intro_html))
lx = set_tag(lx, "introformat", "1")
write(p, lx)
# title in moodle_backup.xml
b.mb = re.sub(rf"(<moduleid>{a['moduleid']}</moduleid>\s*<sectionid>\d+</sectionid>\s*<modulename>label</modulename>\s*<title>).*?(</title>)",
lambda m: m.group(1) + html.escape(label_name(intro_html), quote=False) + m.group(2), b.mb, count=1, flags=re.S)
def sketches_zip(dest):
src = REPO / "project_exercise" / "ui_sketches"
with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z:
for p in sorted(src.iterdir()):
if p.suffix in (".html", ".png", ".css"): z.write(p, f"ui_sketches/{p.name}")
return dest
def set_section_name(b, s, name):
p = b.root / s["dir"] / "section.xml"
write(p, set_tag(read(p), "name", name))
s["name"] = name
# ----------------------------------------------------------------------------- building
class Builder:
def __init__(self, b, scratch):
self.b = b; self.ids = itertools.count(BASE_ID); self.now = int(time.time()); self.scratch = scratch
self.fx = b.files_xml()
self.userid = tag(re.search(r"<file id=\"\d+\">.*?</file>", self.fx, re.S).group(0), "userid") or "0"
secs = [s for s in b.sections if s["component"] in (None, "$@NULL@$")]
self.tmpl_section = next((s for s in secs if s["name"].startswith("Woche n")), secs[-1])
labels = [a for a in b.activities.values() if a["modname"] == "label"]
self.tmpl_label = next((a for a in labels if not a["filerefs"]), labels[0])
self.tmpl_resource = next(a for a in b.activities.values() if a["modname"] == "resource")
self.next_number = max(s["number"] for s in b.sections) + 1
self.report = []
def set_section_summary(b, s, summary_html):
p = b.root / s["dir"] / "section.xml"
sx = set_tag(read(p), "summary", summary_html)
sx = set_tag(sx, "summaryformat", "1")
write(p, sx)
def _register_activity(self, mod, mid, sid, title, adir):
entry = (f" <activity>\n <moduleid>{mid}</moduleid>\n <sectionid>{sid}</sectionid>\n"
f" <modulename>{mod}</modulename>\n <title>{html.escape(title, quote=False)}</title>\n"
f" <directory>{adir}</directory>\n <insubsection></insubsection>\n </activity>\n")
self.b.mb = self.b.mb.replace(" </activities>", entry + " </activities>", 1)
for sname, val in ((f"{mod}_{mid}_included", "1"), (f"{mod}_{mid}_userinfo", "0")):
self.b.mb = self.b.mb.replace(" </settings>", f" <setting>\n <level>activity</level>\n <activity>{mod}_{mid}</activity>\n <name>{sname}</name>\n <value>{val}</value>\n </setting>\n </settings>", 1)
def next_ids(b):
"""fresh ids above everything used in the archive"""
nums = [int(x) for x in re.findall(r'id="(\d+)"|<id>(\d+)</id>|_(\d+)/|<moduleid>(\d+)|<sectionid>(\d+)|contextid="(\d+)"', b.mb) for x in x if x] if False else []
allx = "\n".join(read(p) for p in b.root.rglob("*.xml"))
nums = [int(n) for n in re.findall(r"\b(\d{1,9})\b", allx)]
base = (max(nums) // 1000 + 1) * 1000
return base
def clone_section(b, template, number, name, label_intros, base):
"""clone a week section directory as section `number` with len(label_intros) label activities (cloned from the template's first label)"""
tsec = template
sid = base; base += 1
sdir = f"sections/section_{sid}"
shutil.copytree(b.root / tsec["dir"], b.root / sdir)
sx = read(b.root / sdir / "section.xml")
sx = re.sub(r'<section id="\d+"', f'<section id="{sid}"', sx, count=1)
sx = set_tag(sx, "number", str(number)); sx = set_tag(sx, "name", name)
sx = set_tag(sx, "summary", "");
# template label activity
tlabel = next(b.activities[m] for m in tsec["sequence"] if b.activities[m]["modname"] == "label")
mids = []
for intro in label_intros:
mid = base; base += 1; aid = base; base += 1; ctx = base; base += 1
adir = f"activities/label_{mid}"
shutil.copytree(b.root / tlabel["dir"], b.root / adir)
mx = read(b.root / adir / "module.xml")
def _clone_module(self, tmpl, mod, mid, sid, number, showdescription):
adir = f"activities/{mod}_{mid}"
shutil.copytree(self.b.root / tmpl["dir"], self.b.root / adir)
mx = read(self.b.root / adir / "module.xml")
mx = re.sub(r'<module id="\d+"', f'<module id="{mid}"', mx, count=1)
mx = set_tag(mx, "sectionid", str(sid)); mx = set_tag(mx, "sectionnumber", str(number))
mx = set_tag(mx, "visible", "1"); mx = set_tag(mx, "visibleoncoursepage", "1") if "<visibleoncoursepage>" in mx else mx
write(b.root / adir / "module.xml", mx)
lx = read(b.root / adir / "label.xml")
mx = set_tag(mx, "sectionid", sid); mx = set_tag(mx, "sectionnumber", number)
mx = set_tag(mx, "added", self.now); mx = set_tag(mx, "visible", 1); mx = set_tag(mx, "visibleold", 1)
mx = set_tag(mx, "visibleoncoursepage", 1); mx = set_tag(mx, "showdescription", showdescription)
write(self.b.root / adir / "module.xml", mx)
return adir
def add_label(self, sid, number, intro):
mid, aid, ctx = next(self.ids), next(self.ids), next(self.ids)
adir = self._clone_module(self.tmpl_label, "label", mid, sid, number, 1)
lx = read(self.b.root / adir / "label.xml")
lx = re.sub(r'<activity id="\d+" moduleid="\d+" modulename="label" contextid="\d+"',
f'<activity id="{aid}" moduleid="{mid}" modulename="label" contextid="{ctx}"', lx, count=1)
lx = re.sub(r'<label id="\d+"', f'<label id="{aid}"', lx, count=1)
lx = set_tag(lx, "intro", intro); lx = set_tag(lx, "name", label_name(intro)); lx = set_tag(lx, "introformat", "1")
write(b.root / adir / "label.xml", lx)
# inforef: drop file references of the template label
write(b.root / adir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>')
# register in moodle_backup.xml: contents/activities + settings
entry = (f" <activity>\n <moduleid>{mid}</moduleid>\n <sectionid>{sid}</sectionid>\n"
f" <modulename>label</modulename>\n <title>{html.escape(label_name(intro), quote=False)}</title>\n"
f" <directory>{adir}</directory>\n </activity>\n")
b.mb = b.mb.replace("</activities>", entry + " </activities>", 1)
for sname, val in ((f"label_{mid}_included", "1"), (f"label_{mid}_userinfo", "0")):
b.mb = b.mb.replace("</settings>", f" <setting>\n <level>activity</level>\n <activity>label_{mid}</activity>\n <name>{sname}</name>\n <value>{val}</value>\n </setting>\n </settings>", 1)
mids.append(mid)
sx = set_tag(sx, "sequence", ",".join(str(m) for m in mids))
write(b.root / sdir / "section.xml", sx)
write(b.root / sdir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>')
entry = (f" <section>\n <sectionid>{sid}</sectionid>\n <title>{number}</title>\n <directory>{sdir}</directory>\n </section>\n")
b.mb = b.mb.replace("</sections>", entry + " </sections>", 1)
for sname, val in ((f"section_{sid}_included", "1"), (f"section_{sid}_userinfo", "0")):
b.mb = b.mb.replace("</settings>", f" <setting>\n <level>section</level>\n <section>section_{sid}</section>\n <name>{sname}</name>\n <value>{val}</value>\n </setting>\n </settings>", 1)
return base
lx = set_tag(lx, "name", text_of(intro)); lx = set_tag(lx, "intro", intro)
lx = set_tag(lx, "introformat", 1); lx = set_tag(lx, "timemodified", self.now)
write(self.b.root / adir / "label.xml", lx)
write(self.b.root / adir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>')
self._register_activity("label", mid, sid, text_of(intro), adir)
return mid
def fill(mbz, out, use_summary=False):
tmp = tempfile.mkdtemp()
kind = unpack(mbz, tmp)
b = Backup(tmp)
weeks = {s["number"]: s for s in b.sections if 1 <= s["number"] <= 14}
assert len(weeks) == 14, f"expected sections 1..14, found {sorted(weeks)}"
report = []
def _file_entry(self, fid, ctx, filename, data, mimetype):
h = hashlib.sha1(data).hexdigest()
if filename != ".":
d = self.b.root / "files" / h[:2]; d.mkdir(exist_ok=True); (d / h).write_bytes(data)
null = "$@NULL@$"
return (f' <file id="{fid}">\n <contenthash>{h}</contenthash>\n <contextid>{ctx}</contextid>\n'
f' <component>mod_resource</component>\n <filearea>content</filearea>\n <itemid>0</itemid>\n'
f' <filepath>/</filepath>\n <filename>{html.escape(filename, quote=False)}</filename>\n <userid>{self.userid}</userid>\n'
f' <filesize>{len(data)}</filesize>\n <mimetype>{mimetype if filename != "." else null}</mimetype>\n <status>0</status>\n'
f' <timecreated>{self.now}</timecreated>\n <timemodified>{self.now}</timemodified>\n'
f' <source>{html.escape(filename, quote=False) if filename != "." else null}</source>\n <author>{null}</author>\n'
f' <license>{"allrightsreserved" if filename != "." else null}</license>\n <sortorder>{1 if filename != "." else 0}</sortorder>\n'
f' <repositorytype>{null}</repositorytype>\n <repositoryid>{null}</repositoryid>\n <reference>{null}</reference>\n </file>\n')
def add_file(self, sid, number, name, path, mimetype):
mid, aid, ctx, fid1, fid2 = (next(self.ids) for _ in range(5))
adir = self._clone_module(self.tmpl_resource, "resource", mid, sid, number, 0)
rx = read(self.b.root / adir / "resource.xml")
rx = re.sub(r'<activity id="\d+" moduleid="\d+" modulename="resource" contextid="\d+"',
f'<activity id="{aid}" moduleid="{mid}" modulename="resource" contextid="{ctx}"', rx, count=1)
rx = re.sub(r'<resource id="\d+"', f'<resource id="{aid}"', rx, count=1)
rx = set_tag(rx, "name", name); rx = set_tag(rx, "intro", ""); rx = set_tag(rx, "timemodified", self.now)
write(self.b.root / adir / "resource.xml", rx)
data = pathlib.Path(path).read_bytes()
entries = self._file_entry(fid1, ctx, pathlib.Path(path).name, data, mimetype) + self._file_entry(fid2, ctx, ".", b"", mimetype)
self.fx = self.fx.replace("</files>", entries + "</files>", 1)
write(self.b.root / adir / "inforef.xml", f'<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n <fileref>\n <file>\n <id>{fid1}</id>\n </file>\n <file>\n <id>{fid2}</id>\n </file>\n </fileref>\n</inforef>')
self._register_activity("resource", mid, sid, name, adir)
return mid
def add_section(self, name, items):
sid = next(self.ids); number = self.next_number; self.next_number += 1
sdir = f"sections/section_{sid}"
shutil.copytree(self.b.root / self.tmpl_section["dir"], self.b.root / sdir)
mids = []
for kind, *args in items:
mids.append(self.add_label(sid, number, *args) if kind == "label" else self.add_file(sid, number, *args))
sx = read(self.b.root / sdir / "section.xml")
sx = re.sub(r'<section id="\d+"', f'<section id="{sid}"', sx, count=1)
sx = set_tag(sx, "number", number); sx = set_tag(sx, "name", name); sx = set_tag(sx, "summary", "")
sx = set_tag(sx, "sequence", ",".join(map(str, mids))); sx = set_tag(sx, "visible", 1); sx = set_tag(sx, "timemodified", self.now)
write(self.b.root / sdir / "section.xml", sx)
write(self.b.root / sdir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>')
entry = (f" <section>\n <sectionid>{sid}</sectionid>\n <title>{number}</title>\n"
f" <directory>{sdir}</directory>\n <parentcmid></parentcmid>\n <modname></modname>\n </section>\n")
self.b.mb = self.b.mb.replace(" </sections>", entry + " </sections>", 1)
for sname, val in ((f"section_{sid}_included", "1"), (f"section_{sid}_userinfo", "0")):
self.b.mb = self.b.mb.replace(" </settings>", f" <setting>\n <level>section</level>\n <section>section_{sid}</section>\n <name>{sname}</name>\n <value>{val}</value>\n </setting>\n </settings>", 1)
self.report.append(f"section {number}: {name} ({len(items)} items)")
def finish(self):
write(self.b.root / "moodle_backup.xml", self.b.mb); write(self.b.root / "files.xml", self.fx)
def fill(mbz, out, with_files=True):
tmp = pathlib.Path(tempfile.mkdtemp()); unpack(mbz, tmp)
b = Backup(tmp); bl = Builder(b, tmp)
files = (lambda *items: list(items)) if with_files else (lambda *items: [])
for n in range(1, 15):
s = weeks[n]; title, blocks = blocks_of(n)
set_section_name(b, s, title)
if use_summary:
set_section_summary(b, s, "\n".join(blocks)); report.append(f"week {n}: name + summary"); continue
kinds = {}
for mid in s["sequence"]:
a = b.activities[mid]
if a["modname"] == "label":
k = b.label_kind(a)
if k in ("objectives", "theory", "selfstudy") and k not in kinds: kinds[k] = a
missing = [k for k in ("objectives", "theory", "selfstudy") if k not in kinds]
assert not missing, f"week {n}: no template text area for {missing}; use --summary or adjust label_kind()"
for k, blk in zip(("objectives", "theory", "selfstudy"), blocks):
set_label(b, kinds[k], blk)
report.append(f"week {n}: name + 3 text areas")
# two extra sections
base = next_ids(b)
last = max(b.sections, key=lambda s: s["number"])
nxt = last["number"] + 1
script_html = read(OUT / "lecture_script.html"); project_html = read(OUT / "project_exercise.html")
def body(t): return re.sub(r"<!--.*?-->\s*", "", t, count=1, flags=re.S).strip() + "\n"
base = clone_section(b, weeks[14], nxt, "📘 Lecture script", [body(script_html)], base)
base = clone_section(b, weeks[14], nxt + 1, "🛠️ Project exercise", [body(project_html)], base)
report.append(f"sections {nxt} (script) and {nxt+1} (project) appended")
write(pathlib.Path(tmp) / "moodle_backup.xml", b.mb)
# structural validation
title, blocks = week_blocks(n, with_files)
sub = json.loads(read(MOODLE / "src" / "content" / f"week_{n:02d}.json"))["deck_subtitle"]
slides = REPO / "Folien" / f"AISE502_Vorlesung_{n}_Folien.pdf"
assert slides.exists(), slides
bl.add_section(title, [("label", blocks[0]), ("label", blocks[1]),
*files(("file", f"Slides {sub}", slides, "application/pdf")), ("label", blocks[2])])
bl.add_section("📘 Lecture script", [("label", fixed_section("lecture_script.html", with_files)),
*files(("file", "Lecture script AISE502_Vorlesung_Skript.pdf", REPO / "skript" / "AISE502_Vorlesung_Skript.pdf", "application/pdf"))])
zpath = sketches_zip(tmp.parent / "ui_sketches.zip") if with_files else None
bl.add_section("🛠️ Project exercise", [("label", fixed_section("project_exercise.html", with_files)),
*files(("file", "Exercise sheet project_exercise.pdf", REPO / "project_exercise" / "project_exercise.pdf", "application/pdf"),
("file", "Project slides AISE502_Projekt_Folien.pdf", REPO / "Folien" / "AISE502_Projekt_Folien.pdf", "application/pdf"),
("file", "UI sketches HTML and PNG (ZIP)", zpath, "application/zip"))])
bl.finish()
# ---- validation
b2 = Backup(tmp)
for s in b2.sections:
for mid in s["sequence"]:
assert mid in b2.activities, f"section {s['number']} references unknown module {mid}"
for p in pathlib.Path(tmp).rglob("*.xml"):
ET.parse(p)
pack(tmp, out, kind)
shutil.rmtree(tmp)
print("\n".join(report)); print(f"wrote {out} ({kind}); Moodle release in backup: {b2.release}; sections now: {[s['number'] for s in b2.sections]}")
for mid in s["sequence"]: assert mid in b2.activities, f"section {s['number']} references unknown module {mid}"
for p in tmp.rglob("*.xml"): ET.parse(p)
fx = b2.files_xml(); ids = set()
for f in re.findall(r"<file id=\"(\d+)\">(.*?)</file>", fx, re.S):
ids.add(int(f[0])); h = tag(f[1], "contenthash")
if tag(f[1], "filename") != ".": assert (tmp / "files" / h[:2] / h).exists(), f"content missing for file {f[0]}"
for a in b2.activities.values():
for r in a["filerefs"]: assert r in ids, f"{a['dir']} refers to unknown file {r}"
pack_zip(tmp, out); shutil.rmtree(tmp)
print("\n".join(bl.report)); print(f"wrote {out} ({pathlib.Path(out).stat().st_size/1e6:.1f} MB) · sections now {[s['number'] for s in b2.sections]}")
if __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "inspect": inspect(sys.argv[2])
elif len(sys.argv) >= 4 and sys.argv[1] == "fill": fill(sys.argv[2], sys.argv[3], "--summary" in sys.argv)
if len(sys.argv) == 3 and sys.argv[1] == "inspect": inspect(sys.argv[2])
elif len(sys.argv) >= 4 and sys.argv[1] == "fill": fill(sys.argv[2], sys.argv[3], "--text-only" not in sys.argv)
else: print(__doc__)