Keep empty answers in the final individual outputs.

This commit is contained in:
sebastien committed 2026-10-13 10:15:57 +02:00
1 parent b1f1e0bc86
commit e7094d850e
8 files changed
+164 -37

No files matched your search

+23
View File
@@ -115,6 +115,29 @@ class ModificationTests(unittest.TestCase):
self.assertFalse((self.output / "Empty.jpg").exists())
self.assertFalse(read_json(self.output / "info.json")["Empty"]["not_empty"])
def test_retained_empty_scan_stays_empty_after_unrelated_edits_and_can_be_replaced(self):
self.images["Empty"] = Image.new("RGB", (100, 60), "white")
self.heights["Empty"] = 10
self.info["Empty"]["present"] = True
self.seed()
self.apply("Copie43 Ex 1 -> Ex 2|")
info = read_json(self.output / "info.json")
self.assertTrue((self.output / "Empty.jpg").is_file())
self.assertEqual(info["Empty"], self.info["Empty"])
with Image.open(self.output / "Concat.jpg") as image:
self.assertEqual(image.height, 120 + 180 + 40)
self.apply("Copie43 Ex 1 -> Empty")
self.assertTrue(read_json(self.output / "info.json")["Empty"]["not_empty"])
self.apply()
self.assertEqual(read_json(self.output / "info.json")["Empty"], self.info["Empty"])
def test_removed_body_is_absent_and_not_exported_as_an_empty_scan(self):
self.apply("Copie43 Ex 1 x> Ex 2")
info = read_json(self.output / "info.json")["Ex 1"]
self.assertFalse(info["present"])
self.assertFalse(info["not_empty"])
self.assertFalse((self.output / "Ex 1.jpg").exists())
def test_invalid_file_does_not_apply_valid_first_instruction(self):
before = {p.name: p.read_bytes() for p in self.output.iterdir() if p.is_file()}
with self.assertRaises(CliError):
+85 -8
View File
@@ -57,8 +57,8 @@ class AnswerReturnTests(unittest.TestCase):
), patch.object(annotating, "make_base_image", side_effect=self.supplement):
publish_answer_returns(self.root, self.source, self.destination)
files = sorted((self.destination / "answers").glob("*.jpg"))
self.assertEqual([p.name for p in files], ["Ex 1.jpg", "Ex 2.jpg"])
with Image.open(files[0]) as image:
self.assertEqual([p.name for p in files], ["Empty.jpg", "Ex 1.jpg", "Ex 2.jpg"])
with Image.open(self.destination / "answers" / "Ex 1.jpg") as image:
self.assertEqual(image.size, (100, 30 + 20 * sum((context, question, solution))))
colors = []
if context:
@@ -212,7 +212,7 @@ class AnswerReturnTests(unittest.TestCase):
)
self.assertEqual(
sorted(path.name for path in answers.iterdir()),
["Ex 1.jpg", "Ex 2.jpg"],
["Empty.jpg", "Ex 1.jpg", "Ex 2.jpg"],
)
@@ -304,7 +304,7 @@ class CompiledMembershipTests(unittest.TestCase):
self.assertEqual(info["Empty"], {"present": True, "not_empty": False, "touched": False, "score": "0"})
self.assertEqual(info["Absent"], {"present": False, "not_empty": False, "touched": False, "score": ""})
self.assertEqual(info["Perfect"], {"present": True, "not_empty": True, "touched": False, "score": "4"})
self.assertNotIn("Empty", rendered)
self.assertIn("Empty", rendered)
for label in rendered:
self.assertTrue((output / f"{label}.jpg").is_file())
# Selective redo keeps unselected saved answers in the PDF,
@@ -316,6 +316,76 @@ class CompiledMembershipTests(unittest.TestCase):
self.assertTrue(read_json(output / "info.json")["Perfect"]["touched"])
self.assertFalse(read_json(output / "info.json")["Empty"]["not_empty"])
def test_real_empty_pdf_is_published_in_all_annotation_modes(self):
import fitz
from copienator.commands import reading_annotations as standard
for mode in ("Anot", "Bnot", "BGnot"):
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
workspace = EvaluationWorkspace(root)
(root / "labels").write_text("Empty\nAbsent\n")
pdf = root / "answer.pdf"
with fitz.open() as document:
page = document.new_page(width=200, height=60)
page.insert_text((10, 30), "Copied question only")
document.save(pdf)
data = {"01": {"Empty": {"pdf_path": pdf, "coordinates": (0, 0),
"result": {"score": 0, "error": "empty-answer", "feedback": []}}}}
if mode == "Anot":
status = annotating.process_student("01", data["01"], root, ["Empty", "Absent"], True)
self.assertEqual(status, "success")
elif mode == "Bnot":
status = standard.apply_actions_and_regenerate(
workspace, data, "01", [], None, ["Empty", "Absent"])
self.assertEqual(status, 0)
else:
status, _ = reader.apply_actions_and_regenerate_grouped(
workspace, data, "01", [], {}, ["Empty", "Absent"])
self.assertEqual(status, 0)
output = root / mode / "Copie01"
destination = root / "return"
destination.mkdir()
with patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True,
RETURN_ANSWERS_CONTEXT=False, RETURN_ANSWERS_QUESTION=False,
RETURN_ANSWERS_SOLUTION=False):
publish_answer_returns(root, output, destination)
self.assertEqual([p.name for p in (destination / "answers").iterdir()], ["Empty.jpg"])
with Image.open(destination / "answers" / "Empty.jpg") as image:
self.assertGreater(image.height, 0)
self.assertLess(image.convert("L").getextrema()[0], 100)
self.assertFalse(read_json(destination / "info.json")["Empty"]["not_empty"])
self.assertFalse((output / "Concat.jpg").exists())
self.assertFalse((output / "Concat_F.pdf").exists())
def test_bnot_recovers_empty_scans_missing_from_review_layout(self):
from copienator.commands import reading_annotations as standard
for only_empty in (False, True):
with self.subTest(only_empty=only_empty), tempfile.TemporaryDirectory() as directory:
workspace = EvaluationWorkspace(Path(directory))
output = workspace.root / "Bnot" / "Copie01"
output.mkdir(parents=True)
answer = workspace.root / "answer.pdf"
answer.touch()
labels = {"Empty": {"pdf_path": answer, "coordinates": (0, 0),
"result": {"score": 0, "error": "empty-answer"}}}
if not only_empty:
labels["Full"] = {"pdf_path": answer, "coordinates": (0, 0),
"result": {"score": 4}}
atomic_write_json(output / "bnote.json", {"images": [
{"label": "Full", "hmin": 0, "hmax": 50, "header_height": 0}]})
with patch.object(annotating, "make_base_image", return_value=(None, 0, 0)), patch.object(
annotating, "compose_label_image", return_value=(Image.new("RGB", (100, 50)), 0)
) as render:
status = standard.apply_actions_and_regenerate(
workspace, {"01": labels}, "01", [], None, [*labels, "Absent"])
self.assertEqual(status, 0)
self.assertTrue(all(call.kwargs["with_empty"] for call in render.call_args_list))
self.assertTrue((output / "Empty.jpg").is_file())
self.assertFalse((output / "Absent.jpg").exists())
self.assertFalse(read_json(output / "info.json")["Empty"]["not_empty"])
self.assertEqual((output / "Concat.jpg").exists(), not only_empty)
self.assertFalse((output / "Concat_F.jpg").exists())
def test_all_empty_removes_stale_concat_and_records_false(self):
with tempfile.TemporaryDirectory() as directory:
workspace = EvaluationWorkspace(Path(directory))
@@ -325,10 +395,17 @@ class CompiledMembershipTests(unittest.TestCase):
(output / name).write_bytes(b"stale")
atomic_write_json(output / "touched.json", {"Empty": True})
atomic_write_json(output / "answer_labels.json", ["Empty"])
status, _ = reader.apply_actions_and_regenerate_grouped(
workspace, {"01": {"Empty": {"result": {"score": 0, "error": "empty-answer"}}}},
"01", [], {}, ["Empty"]
)
answer = workspace.root / "answer.pdf"
answer.touch()
with patch.object(annotating, "make_base_image", return_value=(Image.new("RGB", (100, 50)), 0, 0)), patch.object(
annotating, "compose_label_image", return_value=(Image.new("RGB", (100, 50)), 0)
):
status, _ = reader.apply_actions_and_regenerate_grouped(
workspace, {"01": {"Empty": {"result": {"score": 0, "error": "empty-answer"},
"pdf_path": answer, "coordinates": (0, 0)}}},
"01", [], {}, ["Empty"]
)
self.assertTrue((output / "Empty.jpg").is_file())
self.assertEqual(status, 0)
self.assertEqual(read_json(output / "info.json"), {
"Empty": {"present": True, "not_empty": False, "touched": False, "score": "0"}