fix: harden classroom presentations

This commit is contained in:
ekklesia
2026-09-23 00:02:25 -05:00
parent ec1e0d1cd7
commit 6f2d169355
16 changed files with 709 additions and 215 deletions
+6 -7
View File
@@ -191,8 +191,8 @@ else:
check("and Parent Call in the corner",
every and CALL in every["launchers"],
str((every or {}).get("launchers")))
check("and Help beside it",
every and HELP in every["launchers"],
check("and the unfinished Help launcher is hidden",
every and HELP not in every["launchers"],
str((every or {}).get("launchers")))
no_ink, why = bar({"WONDERINK_ENABLED": "false"})
@@ -210,11 +210,10 @@ else:
check("with no Parent Call button on it",
bible and CALL not in bible["launchers"],
str((bible or {}).get("launchers")))
# Help is not optional. A teacher who needs the manual is by
# definition one who does not know where to look, so every room has
# it — what differs is which pages it opens on.
check("but Help is still there, because every room has a manual",
bible and HELP in bible["launchers"],
# The source and launcher remain installed, but the unfinished digital
# Help entry point is deliberately absent from the teacher-facing bar.
check("and unfinished Help stays hidden",
bible and HELP not in bible["launchers"],
str((bible or {}).get("launchers")))
SLIDES = "applications:ekklesia-presentations.desktop"
+169 -2
View File
@@ -14,11 +14,14 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
import os
import errno
import importlib.util
import shutil
import sys
import tempfile
import time
from datetime import timedelta
from importlib.machinery import SourceFileLoader
from pathlib import Path
root = Path(sys.argv[1])
@@ -26,6 +29,14 @@ sys.path.insert(0, str(root / "lib"))
import classroom_presentations as library
loader = SourceFileLoader(
"classroom_presentation_import",
str(root / "scripts" / "classroom-presentation-import"),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
importer = importlib.util.module_from_spec(spec)
loader.exec_module(importer)
passed = failed = 0
@@ -242,19 +253,175 @@ check("it lives in the runtime directory",
or str(library.presenting_lock()).startswith("/tmp"),
str(library.presenting_lock()))
print("10. the ledger remembers a download after its presentation is gone")
print("10. the ledger identifies a download transaction")
mark = library.fingerprint(done)
library.remember(mark, "some-id", shelf)
check("a fingerprint is path, size and time",
mark is not None and str(done.resolve()) in mark, str(mark))
check("it is written down", library.ledger(shelf).get(mark) == "some-id")
check("so Remove means removed, not removed until the next scan",
check("so an interrupted move can be matched after a restart",
mark in library.ledger(shelf))
touched = download("Session 7 - Romans.pptx", contents="y" * 900)
check("but re-downloading the same session is a new presentation",
library.fingerprint(touched) != mark,
"a different size or time is a different download")
print("11. a completed download is moved into the managed library")
original_render_pages = library.render_pages
original_render_thumbnail = library.render_thumbnail
original_first_text = library.first_text
original_convert = library.convert_to_pdf
def fake_render_pages(_pdf, directory, width=library.SLIDE_WIDTH,
progress=None):
directory.mkdir(parents=True, exist_ok=True)
(directory / "page-001.png").write_bytes(b"png")
return 1, ""
def fake_render_thumbnail(_source, destination,
width=library.THUMBNAIL_WIDTH):
destination.write_bytes(b"thumbnail")
return True
library.render_pages = fake_render_pages
library.render_thumbnail = fake_render_thumbnail
library.first_text = lambda _pdf, pages=2: ""
moved = download("Move me.pdf", contents="%PDF-1.4 moved")
moved_contents = moved.read_bytes()
managed = importer.import_one(moved, shelf)
check("the source leaves Downloads after import", not moved.exists())
check("the one original is kept in the managed entry",
managed is not None and managed.managed_copy.read_bytes() == moved_contents,
str(managed.managed_copy if managed else None))
check("its PDF and page images are derived assets",
managed is not None and managed.ready and managed.slides.exists()
and managed.page_image(1).exists())
print("12. conversion failure does not lose the downloaded original")
library.convert_to_pdf = lambda _source, _destination: (False, "broken deck")
broken_source = download("Broken deck.pptx", contents="not really a deck")
failed_import = importer.import_one(broken_source, shelf)
check("the failed source was still moved out of Downloads",
not broken_source.exists())
check("a failed card retains the managed original",
failed_import is not None and failed_import.failed
and failed_import.managed_copy.read_text() == "not really a deck")
check("the failure did not create a second original",
failed_import is not None
and list((failed_import.directory / "original").iterdir())
== [failed_import.managed_copy])
print("13. a cross-filesystem move publishes before deleting")
cross_source = download("Across filesystems.pdf", contents="cross-device")
cross_destination = work / "other-device" / cross_source.name
real_replace = importer.os.replace
source_text = str(cross_source)
destination_text = str(cross_destination)
def cross_replace(source, destination):
if os.fspath(source) == source_text and os.fspath(destination) == destination_text:
raise OSError(errno.EXDEV, "different filesystems")
return real_replace(source, destination)
importer.os.replace = cross_replace
try:
importer.move_download(cross_source, cross_destination)
finally:
importer.os.replace = real_replace
check("the cross-filesystem destination is complete",
cross_destination.read_text() == "cross-device")
check("only the destination remains after it is durable",
not cross_source.exists()
and not cross_destination.with_name(cross_destination.name + ".moving").exists())
print("14. restart recovery never deletes the only moved original")
resume_dir = shelf / "resume"
resume_original = resume_dir / "original" / "Resume.pdf"
resume_original.parent.mkdir(parents=True)
resume_original.write_text("%PDF-1.4 resume")
resume_data = {
"id": "resume",
"state": "importing",
"title": "Resume",
"original_name": "Resume.pdf",
"source_path": str(downloads / "Resume.pdf"),
"managed_copy": "original/Resume.pdf",
"format": "pdf",
"imported_at": library.stamp(),
"last_presented_at": None,
"times_presented": 0,
"thumbnail_page": 1,
"pages": 0,
"error": None,
}
library.Presentation(resume_dir, resume_data).save()
importer.sweep(shelf)
resumed = library.load(resume_dir)
check("an import interrupted after the move resumes in place",
resumed is not None and resumed.ready)
check("and its original survives recovery", resume_original.exists())
print("15. removal is isolated and refuses an active presentation")
remove_one = entry(id="remove-one", title="Similar title")
keep_one = entry(id="keep-one", title="Similar title (2)")
(remove_one.directory / "original").mkdir()
(remove_one.directory / "original" / "one.pdf").write_text("one")
(keep_one.directory / "original").mkdir()
(keep_one.directory / "original" / "two.pdf").write_text("two")
check("removing one managed entry succeeds", library.remove(remove_one))
check("a similarly named presentation and its files are untouched",
keep_one.directory.exists()
and (keep_one.directory / "original" / "two.pdf").exists())
runtime = work / "runtime"
runtime.mkdir()
old_runtime = os.environ.get("XDG_RUNTIME_DIR")
os.environ["XDG_RUNTIME_DIR"] = str(runtime)
library.presenting_lock().write_text(keep_one.id)
check("an actively presented entry cannot be removed",
not library.remove(keep_one) and keep_one.directory.exists())
library.presenting_lock().unlink()
if old_runtime is None:
os.environ.pop("XDG_RUNTIME_DIR", None)
else:
os.environ["XDG_RUNTIME_DIR"] = old_runtime
library.render_pages = original_render_pages
library.render_thumbnail = original_render_thumbnail
library.first_text = original_first_text
library.convert_to_pdf = original_convert
print("16. teacher-facing wording and compositor policy")
ui = (root / "scripts" / "classroom-presentations").read_text()
layout = (root / "config" / "plasma" / "lookandfeel" / "contents"
/ "layouts" / "org.kde.plasma.desktop-layout.js").read_text()
kwin = (root / "config" / "plasma" / "xdg" / "kwinrc").read_text()
rules = (root / "config" / "plasma" / "xdg" / "kwinrulesrc").read_text()
check("the empty state has one concise browser-neutral instruction",
'label="No presentations yet\\n\\n"' in ui
and "Download a PowerPoint or PDF to get started." in ui
and "Download a presentation in Chrome" not in ui)
check("cover selection uses the requested plain wording",
'label="Use as cover"' in ui and 'label="Current cover"' in ui
and "this is the library picture" not in ui)
check("Remove no longer promises a copy in Chrome",
"This will delete it from this computer." in ui
and "Your download in Chrome" not in ui)
check("the presenter has a compositor-enforced fullscreen rule",
"title=Presenting" in rules and "fullscreen=true" in rules
and "fullscreenrule=2" in rules)
check("normal title bars contain only Close",
"ButtonsOnRight=X" in kwin and "ButtonsOnRight=FX" not in kwin)
check("the unfinished Help launcher is absent from the panel",
"ekklesia-manual.desktop" not in layout)
shutil.rmtree(work, ignore_errors=True)
print()
+2 -1
View File
@@ -202,7 +202,8 @@ check("the restrictions are immutable", "[KDE Action Restrictions][$i]" in restr
print()
print("4d. clicking a launcher never hides an application")
check("minimise on click is off", 'minimizeActiveTaskOnClick", false' in layout)
check("there is no minimise button", "ButtonsOnRight=FX" in
check("there is no minimise button or keep-above chevron",
"ButtonsOnRight=X" in
(root / "config/plasma/xdg/kwinrc").read_text())
print()