fix: harden classroom presentations
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user