Cache for reading annotation

This commit is contained in:
sebastien committed 2026-10-24 10:22:41 +02:00
1 parent 55d5cf16c9
commit ff7496b113
12 files changed
+466 -13

No files matched your search

+113 -1
View File
@@ -8,8 +8,10 @@ from unittest.mock import patch
from PIL import Image
from copienator import EvaluationWorkspace, atomic_write_json, configuration, read_json
from copienator import EvaluationWorkspace, ExitCode, atomic_write_json, configuration, read_json
from copienator.annotation_data import AnnotationLoadResult
from copienator.commands import annotating, giving_names
from copienator.commands import reading_annotations as standard
from copienator.commands import reading_grouped_annotations as reader
from copienator.return_answers import (
publish_answer_returns,
@@ -216,6 +218,116 @@ class AnswerReturnTests(unittest.TestCase):
)
class RereadAnswerReturnTests(unittest.TestCase):
def check_reread(self, module, mode, *, existing_returns=True, fail_publication=False,
failed_copy=False):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name)
workspace = EvaluationWorkspace(root)
for folder in ("Copies", "Par label", mode, "Text2"):
(root / folder).mkdir()
(root / "labels").write_text("Ex 1\n")
atomic_write_json(root / "correction.json", {})
question = root / "Text2" / "Ex 1.pdf"
question.touch()
data = {}
original_score = b'{\n "Ex 1": "3.5"\n}\n'
destinations = {}
for copy_id in ("01", "02"):
output = root / mode / f"Copie{copy_id}"
output.mkdir()
(output / "score.json").write_bytes(original_score)
atomic_write_json(output / "bnote.json", {"images": [{"label": "Ex 1"}]})
pdf = root / "Copies" / f"{copy_id}.pdf"
if copy_id == "01":
pdf.touch()
data[copy_id] = {"Ex 1": {
"result": {"score": 1, "feedback": []},
"pdf_path": pdf,
"coordinates": (0, 0),
}}
destination = workspace.return_dir / f"Renamed student ({copy_id})"
destinations[copy_id] = destination
if existing_returns:
(destination / "answers").mkdir(parents=True)
(destination / "answers" / "old.jpg").write_bytes(b"previous answers")
(destination / "custom.pdf").write_bytes(b"keep PDF")
(destination / "info.json").write_bytes(b"keep info")
if not failed_copy:
del data["02"]
def compose(base, label, result, *args, **kwargs):
# Make the rendered score observable in the published answer pixels.
color = "red" if str(result["score"]) == "3.5" else "black"
return Image.new("RGB", (100, 50), color), 10
def render(path):
if Path(path) == question and fail_publication:
raise ValueError("cannot render supplement")
return Image.new("RGB", (100, 20), "blue"), 0, 0
with (
patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True),
patch.object(module, "load_annotation_data", return_value=AnnotationLoadResult(data, [])),
patch.object(standard, "detect_checks_and_notes", return_value=([], None)),
patch.object(annotating, "make_base_image", side_effect=render),
patch.object(annotating, "compose_label_image", side_effect=compose),
patch.object(reader, "get_extra_pdfs_as_images", return_value=[]),
patch.object(reader, "save_paginated_pdf"),
patch.object(reader, "save_checked_pdf"),
contextlib.redirect_stdout(io.StringIO()),
contextlib.redirect_stderr(io.StringIO()),
):
status = module.run(
workspace, update_score=True,
return_answers_context=False, return_answers_question=True,
return_answers_solution=False,
)
expected = ExitCode.PARTIAL if failed_copy or fail_publication else ExitCode.SUCCESS
self.assertEqual(status, expected)
self.assertEqual((root / mode / "Copie01" / "score.json").read_bytes(), original_score)
if not existing_returns:
self.assertFalse(workspace.return_dir.exists())
return
for destination in destinations.values():
self.assertEqual((destination / "custom.pdf").read_bytes(), b"keep PDF")
self.assertEqual((destination / "info.json").read_bytes(), b"keep info")
self.assertEqual((destinations["02"] / "answers" / "old.jpg").read_bytes(), b"previous answers")
answers = destinations["01"] / "answers"
if fail_publication:
self.assertEqual((answers / "old.jpg").read_bytes(), b"previous answers")
return
self.assertEqual([p.name for p in answers.iterdir()], ["Ex 1.jpg"])
with Image.open(answers / "Ex 1.jpg") as image:
self.assertEqual(image.size, (100, 70))
self.assertGreater(image.getpixel((50, 10))[2], 240)
self.assertGreater(image.getpixel((50, 45))[0], 240)
self.assertLess(image.getpixel((50, 45))[2], 10)
def test_reread_refreshes_answers_from_new_scores_and_supplements(self):
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
with self.subTest(mode=mode):
self.check_reread(module, mode)
def test_reread_does_not_create_named_returns(self):
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
with self.subTest(mode=mode):
self.check_reread(module, mode, existing_returns=False)
def test_failed_copy_does_not_republish_stale_annotations(self):
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
with self.subTest(mode=mode):
self.check_reread(module, mode, failed_copy=True)
def test_failed_publication_preserves_previous_answers(self):
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
with self.subTest(mode=mode):
self.check_reread(module, mode, fail_publication=True)
class CompiledMembershipTests(unittest.TestCase):
def test_update_score_preserves_file_and_manual_value_wins(self):
with tempfile.TemporaryDirectory() as directory: