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 #!/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> python3 moodle/src/fill_mbz.py inspect <backup.mbz>
lists Moodle release, course format, every section (number, id, name) and the activities in it python3 moodle/src/fill_mbz.py fill <backup.mbz> <out.mbz> [--text-only]
python3 src/fill_mbz.py fill <backup.mbz> <out.mbz> [--summary] --text-only: text areas only, no file resources (small archive; upload the PDFs by hand)
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.
Only text fields are changed; ids of new sections/labels are fresh unique numbers; everything else in fill appends 16 sections after the existing ones (weeks 1-14, lecture script, project exercise):
the archive is copied untouched. The result is validated structurally (all referenced directories exist, week N : text area block 1 | text area block 2 | file: slides of lecture N | text area block 3
XML parses) but a restore in Moodle is the real test. 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 import xml.etree.ElementTree as ET
HERE = pathlib.Path(__file__).resolve().parent REPO = pathlib.Path(__file__).resolve().parents[2]
OUT = HERE.parent MOODLE = REPO / "moodle"
BASE_ID = 20_000_000
# ----------------------------------------------------------------------------- archive helpers # ----------------------------------------------------------------------------- helpers
def unpack(mbz, dest): def unpack(mbz, dest):
data = open(mbz, "rb").read(4) if open(mbz, "rb").read(2) == b"PK":
if data[:2] == b"PK": with zipfile.ZipFile(mbz) as z: z.extractall(dest)
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())
else: else:
with tarfile.open(out, "w:gz") as t: with tarfile.open(mbz, "r:gz") as t: t.extractall(dest)
for p in sorted(src.rglob("*")):
if p.is_file(): t.add(p, p.relative_to(src).as_posix()) 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 read(p): return pathlib.Path(p).read_text(encoding="utf-8")
def write(p, s): pathlib.Path(p).write_text(s, 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) m = re.search(rf"<{name}>(.*?)</{name}>", xml, re.S)
return html.unescape(m.group(1)) if m else None return html.unescape(m.group(1)) if m else None
def set_tag(xml, name, value, count=1): def set_tag(xml, name, value):
esc = html.escape(value, quote=False) esc = html.escape(str(value), quote=False)
new, n = re.subn(rf"<{name}>.*?</{name}>", lambda m: f"<{name}>{esc}</{name}>", xml, count=count, flags=re.S) new, n = re.subn(rf"<{name}>.*?</{name}>", lambda m: f"<{name}>{esc}</{name}>", xml, count=1, flags=re.S)
if n == 0: # empty tag form <name/> or <name></name> if n == 0:
new, n = re.subn(rf"<{name}\s*/>", f"<{name}>{esc}</{name}>", xml, count=count) new, n = re.subn(rf"<{name}\s*/>", f"<{name}>{esc}</{name}>", xml, count=1)
assert n, f"tag {name} not found" assert n, f"tag <{name}> not found"
return new 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 # ----------------------------------------------------------------------------- model
class Backup: class Backup:
def __init__(self, root): def __init__(self, root):
self.root = pathlib.Path(root) self.root = pathlib.Path(root)
self.mb = read(self.root / "moodle_backup.xml") self.mb = read(self.root / "moodle_backup.xml")
self.release = tag(self.mb, "moodle_release") self.release = tag(self.mb, "moodle_release")
self.format = tag(self.mb, "format") # course format e.g. topics self.course_format = tag(self.mb, "original_course_format")
# sections: from moodle_backup.xml contents contents = re.search(r"<contents>(.*?)</contents>", self.mb, re.S).group(1)
self.sections = [] 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): for s in re.findall(r"<section>.*?</section>", contents, re.S):
sid, title, d = int(m.group(1)), m.group(2), m.group(3) d = tag(s, "directory"); sx = read(self.root / d / "section.xml")
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 "",
self.sections.append(dict(id=sid, 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()], 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.sections.sort(key=lambda s: s["number"])
self.activities = {} 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): for a in re.findall(r"<activity>.*?</activity>", contents, 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) mid = int(tag(a, "moduleid")); d = tag(a, "directory"); mod = tag(a, "modulename")
a = dict(moduleid=mid, sectionid=sid, modname=mod, title=title, dir=d) rec = dict(moduleid=mid, sectionid=int(tag(a, "sectionid")), modname=mod, title=tag(a, "title"), dir=d)
mx = read(self.root / d / "module.xml") rec["visible"] = tag(read(self.root / d / "module.xml"), "visible")
a["visible"] = tag(mx, "visible") fr = re.search(r"<fileref>(.*?)</fileref>", read(self.root / d / "inforef.xml"), re.S)
if mod == "label": rec["filerefs"] = [int(x) for x in re.findall(r"<id>(\d+)</id>", fr.group(1))] if fr else []
lx = read(self.root / d / "label.xml") if mod == "label": rec["intro"] = tag(read(self.root / d / "label.xml"), "intro") or ""
a["intro"] = tag(lx, "intro") or "" if mod == "resource":
self.activities[mid] = a fx = self.files_xml()
rec["files"] = [tag(f, "filename") for f in re.findall(r"<file id=\"\d+\">.*?</file>", fx, re.S)
def label_kind(self, a): if int(re.search(r'<file id="(\d+)"', f).group(1)) in rec["filerefs"] and tag(f, "filename") != "."]
t = re.sub(r"<[^>]+>", " ", a.get("intro", "")) self.activities[mid] = rec
t = html.unescape(t) def files_xml(self):
if a["visible"] == "0" or "Auftrag Dozierende" in t: return "hidden" if not hasattr(self, "_fx"): self._fx = read(self.root / "files.xml")
if "Lernziele" in t or "Learning objectives" in t: return "objectives" return self._fx
if "Theorie" in t or "Theory" in t: return "theory"
if "Selbststudium" in t or "Self-study" in t: return "selfstudy"
return "other"
def inspect(mbz): def inspect(mbz):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
kind = unpack(mbz, tmp) unpack(mbz, tmp); b = Backup(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")
print(f"archive: {kind} · Moodle {b.release} · course format: {b.format} · {len(b.sections)} sections · {len(b.activities)} activities")
for s in b.sections: 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"]: for mid in s["sequence"]:
a = b.activities.get(mid) a = b.activities.get(mid)
if not a: print(f" ? module {mid} not in backup contents"); continue if not a: print(f" ? module {mid} missing"); continue
kind_ = b.label_kind(a) if a["modname"] == "label" else "-" extra = f" files={a['files']}" if a["modname"] == "resource" else ""
print(f" {a['modname']:<10} id={mid:<6} visible={a['visible']} kind={kind_:<10} {a['title'][:70]!r}") print(f" {a['modname']:<10} {mid:<9} visible={a['visible']} {a['title'][:70]!r}{extra}")
# ----------------------------------------------------------------------------- filling # ----------------------------------------------------------------------------- content
def blocks_of(week): def week_blocks(n, with_files=True):
t = read(OUT / f"week_{week:02d}.html") t = read(MOODLE / f"week_{n:02d}.html")
parts = re.split(r"<!-- BLOCK \d · [^>]*-->\n", t) 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() 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): def fixed_section(name, with_files=True):
t = html.unescape(re.sub(r"<[^>]+>", " ", intro)); t = re.sub(r"\s+", " ", t).strip() t = read(MOODLE / name)
return t[:60] 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): def sketches_zip(dest):
p = b.root / a["dir"] / "label.xml" src = REPO / "project_exercise" / "ui_sketches"
lx = read(p) with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z:
lx = set_tag(lx, "intro", intro_html) for p in sorted(src.iterdir()):
lx = set_tag(lx, "name", label_name(intro_html)) if p.suffix in (".html", ".png", ".css"): z.write(p, f"ui_sketches/{p.name}")
lx = set_tag(lx, "introformat", "1") return dest
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 set_section_name(b, s, name): # ----------------------------------------------------------------------------- building
p = b.root / s["dir"] / "section.xml" class Builder:
write(p, set_tag(read(p), "name", name)) def __init__(self, b, scratch):
s["name"] = name 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): def _register_activity(self, mod, mid, sid, title, adir):
p = b.root / s["dir"] / "section.xml" entry = (f" <activity>\n <moduleid>{mid}</moduleid>\n <sectionid>{sid}</sectionid>\n"
sx = set_tag(read(p), "summary", summary_html) f" <modulename>{mod}</modulename>\n <title>{html.escape(title, quote=False)}</title>\n"
sx = set_tag(sx, "summaryformat", "1") f" <directory>{adir}</directory>\n <insubsection></insubsection>\n </activity>\n")
write(p, sx) 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): def _clone_module(self, tmpl, mod, mid, sid, number, showdescription):
"""fresh ids above everything used in the archive""" adir = f"activities/{mod}_{mid}"
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 [] shutil.copytree(self.b.root / tmpl["dir"], self.b.root / adir)
allx = "\n".join(read(p) for p in b.root.rglob("*.xml")) mx = read(self.b.root / adir / "module.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")
mx = re.sub(r'<module id="\d+"', f'<module id="{mid}"', mx, count=1) 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, "sectionid", sid); mx = set_tag(mx, "sectionnumber", number)
mx = set_tag(mx, "visible", "1"); mx = set_tag(mx, "visibleoncoursepage", "1") if "<visibleoncoursepage>" in mx else mx mx = set_tag(mx, "added", self.now); mx = set_tag(mx, "visible", 1); mx = set_tag(mx, "visibleold", 1)
write(b.root / adir / "module.xml", mx) mx = set_tag(mx, "visibleoncoursepage", 1); mx = set_tag(mx, "showdescription", showdescription)
lx = read(b.root / adir / "label.xml") 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+"', 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) 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 = 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") lx = set_tag(lx, "name", text_of(intro)); lx = set_tag(lx, "intro", intro)
write(b.root / adir / "label.xml", lx) lx = set_tag(lx, "introformat", 1); lx = set_tag(lx, "timemodified", self.now)
# inforef: drop file references of the template label write(self.b.root / adir / "label.xml", lx)
write(b.root / adir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>') write(self.b.root / adir / "inforef.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<inforef>\n</inforef>')
# register in moodle_backup.xml: contents/activities + settings self._register_activity("label", mid, sid, text_of(intro), adir)
entry = (f" <activity>\n <moduleid>{mid}</moduleid>\n <sectionid>{sid}</sectionid>\n" return mid
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
def fill(mbz, out, use_summary=False): def _file_entry(self, fid, ctx, filename, data, mimetype):
tmp = tempfile.mkdtemp() h = hashlib.sha1(data).hexdigest()
kind = unpack(mbz, tmp) if filename != ".":
b = Backup(tmp) d = self.b.root / "files" / h[:2]; d.mkdir(exist_ok=True); (d / h).write_bytes(data)
weeks = {s["number"]: s for s in b.sections if 1 <= s["number"] <= 14} null = "$@NULL@$"
assert len(weeks) == 14, f"expected sections 1..14, found {sorted(weeks)}" return (f' <file id="{fid}">\n <contenthash>{h}</contenthash>\n <contextid>{ctx}</contextid>\n'
report = [] 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): for n in range(1, 15):
s = weeks[n]; title, blocks = blocks_of(n) title, blocks = week_blocks(n, with_files)
set_section_name(b, s, title) sub = json.loads(read(MOODLE / "src" / "content" / f"week_{n:02d}.json"))["deck_subtitle"]
if use_summary: slides = REPO / "Folien" / f"AISE502_Vorlesung_{n}_Folien.pdf"
set_section_summary(b, s, "\n".join(blocks)); report.append(f"week {n}: name + summary"); continue assert slides.exists(), slides
kinds = {} bl.add_section(title, [("label", blocks[0]), ("label", blocks[1]),
for mid in s["sequence"]: *files(("file", f"Slides – {sub}", slides, "application/pdf")), ("label", blocks[2])])
a = b.activities[mid] bl.add_section("📘 Lecture script", [("label", fixed_section("lecture_script.html", with_files)),
if a["modname"] == "label": *files(("file", "Lecture script – AISE502_Vorlesung_Skript.pdf", REPO / "skript" / "AISE502_Vorlesung_Skript.pdf", "application/pdf"))])
k = b.label_kind(a) zpath = sketches_zip(tmp.parent / "ui_sketches.zip") if with_files else None
if k in ("objectives", "theory", "selfstudy") and k not in kinds: kinds[k] = a bl.add_section("🛠️ Project exercise", [("label", fixed_section("project_exercise.html", with_files)),
missing = [k for k in ("objectives", "theory", "selfstudy") if k not in kinds] *files(("file", "Exercise sheet – project_exercise.pdf", REPO / "project_exercise" / "project_exercise.pdf", "application/pdf"),
assert not missing, f"week {n}: no template text area for {missing}; use --summary or adjust label_kind()" ("file", "Project slides – AISE502_Projekt_Folien.pdf", REPO / "Folien" / "AISE502_Projekt_Folien.pdf", "application/pdf"),
for k, blk in zip(("objectives", "theory", "selfstudy"), blocks): ("file", "UI sketches – HTML and PNG (ZIP)", zpath, "application/zip"))])
set_label(b, kinds[k], blk) bl.finish()
report.append(f"week {n}: name + 3 text areas") # ---- validation
# 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
b2 = Backup(tmp) b2 = Backup(tmp)
for s in b2.sections: for s in b2.sections:
for mid in s["sequence"]: for mid in s["sequence"]: assert mid in b2.activities, f"section {s['number']} references unknown module {mid}"
assert mid in b2.activities, f"section {s['number']} references unknown module {mid}" for p in tmp.rglob("*.xml"): ET.parse(p)
for p in pathlib.Path(tmp).rglob("*.xml"): fx = b2.files_xml(); ids = set()
ET.parse(p) for f in re.findall(r"<file id=\"(\d+)\">(.*?)</file>", fx, re.S):
pack(tmp, out, kind) ids.add(int(f[0])); h = tag(f[1], "contenthash")
shutil.rmtree(tmp) if tag(f[1], "filename") != ".": assert (tmp / "files" / h[:2] / h).exists(), f"content missing for file {f[0]}"
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 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 __name__ == "__main__":
if len(sys.argv) >= 3 and sys.argv[1] == "inspect": inspect(sys.argv[2]) 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) 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__) else: print(__doc__)