from copy import deepcopy from pathlib import Path import tempfile import unittest from unittest.mock import patch from PIL import Image, ImageDraw from copienator import EvaluationWorkspace, ExitCode, atomic_write_json from copienator.custom_remarks import apply_custom_remarks, set_remark, remarks_for_copy from copienator.commands import annotating, reading_annotations as standard from copienator.commands import reading_grouped_annotations as grouped class CustomRemarkTests(unittest.TestCase): def test_storage_preserves_other_students_and_feedback_is_idempotent(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) set_remark(root, "01", "Ex 1", "First") set_remark(root, "2", "Ex 1", "Other student") set_remark(root, "1", "Ex 2", "Other label") labels = {"Ex 1": {"result": {"feedback": [{"text": "Gemini"}]}}} apply_custom_remarks(root, "1", labels) apply_custom_remarks(root, "01", labels) self.assertEqual([f["text"] for f in labels["Ex 1"]["result"]["feedback"]], ["Gemini", "First"]) set_remark(root, "01", "Ex 1", " ") apply_custom_remarks(root, "1", labels) self.assertEqual(labels["Ex 1"]["result"]["feedback"], [{"text": "Gemini"}]) self.assertEqual(remarks_for_copy(root, "1"), {"Ex 2": "Other label"}) self.assertEqual(remarks_for_copy(root, "02"), {"Ex 1": "Other student"}) def test_readers_render_remark_after_checkbox_deletion_and_preserve_ink(self): for mode in ("Bnot", "BGnot"): with self.subTest(mode=mode), tempfile.TemporaryDirectory() as temporary: root = Path(temporary) workspace = EvaluationWorkspace(root) output = root / mode / "Copie01" output.mkdir(parents=True) answer = root / "answer.pdf" answer.touch() atomic_write_json(output / "bnote.json", {"images": [{"label": "Ex 1"}]}) atomic_write_json(output / "score.json", {"Ex 1": "4"}) original = {"01": {"Ex 1": {"pdf_path": answer, "coordinates": (0, 0), "result": {"score": 4, "feedback": [{"text": "Gemini"}]}}}} set_remark(root, "1", "Ex 1", "Teacher") ink = Image.new("RGBA", (1500, 110), (0, 0, 0, 0)) draw = ImageDraw.Draw(ink) draw.rectangle((100, 1, 120, 8), fill="red") draw.rectangle((100, 30, 120, 50), fill="blue") real_compose = annotating.compose_label_image def compose(*args, **kwargs): return real_compose(*args, **kwargs, render_fn=lambda text, *a, **k: Image.new("RGB", (1500, 20), "green")) with patch.object(annotating, "make_base_image", return_value=(Image.new("RGB", (1500, 100), "white"), 0, 0)), \ patch.object(annotating, "render_score_text", return_value=Image.new("RGB", (1500, 10), "white")), \ patch.object(annotating, "compose_label_image", side_effect=compose), \ patch.object(grouped, "get_extra_pdfs_as_images", return_value=[]): def run(with_ink=True): data = deepcopy(original) actions = [{"label": "Ex 1", "type": "clear_all"}] if mode == "Bnot": # The tablet header was ten pixels tall. atomic_write_json(output / "bnote.json", {"images": [ {"label": "Ex 1", "header_height": 10}]}) return standard.apply_actions_and_regenerate(workspace, data, "01", actions, None, ["Ex 1"], update_score=True, label_notes={"Ex 1": ink} if with_ink else {}) status, _ = grouped.apply_actions_and_regenerate_grouped(workspace, data, "01", actions, {"Ex 1": {"img": ink, "old_header_h": 10}} if with_ink else {}, ["Ex 1"], update_score=True) return status self.assertEqual(run(), ExitCode.SUCCESS) self.assertTrue((output / "Concat_F.pdf").is_file()) with Image.open(output / "Ex 1.jpg") as image: self.assertEqual(image.height, 130) self.assertGreater(image.getpixel((110, 4))[0], 180) self.assertGreater(image.getpixel((110, 65))[2], 180) # Even with no handwritten ink, a personal remark includes a perfect answer. original["01"]["Ex 1"]["result"]["feedback"] = [] self.assertEqual(run(with_ink=False), ExitCode.SUCCESS) self.assertTrue((output / "Concat_F.pdf").is_file()) set_remark(root, "1", "Ex 1", "") self.assertEqual(run(), ExitCode.SUCCESS) with Image.open(output / "Ex 1.jpg") as image: self.assertEqual(image.height, 110) self.assertEqual(run(with_ink=False), ExitCode.SUCCESS) self.assertFalse((output / "Concat_F.pdf").exists())