Auto-commit 2026-09-07 18:49: 1 file changed, 215 insertions(+), 192 deletions(-)
This commit is contained in:
parent
816402334b
commit
e06d698802
@ -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__)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user