from copy import deepcopy from pathlib import Path import tempfile import unittest from unittest.mock import patch from PIL import Image from pdf2image import convert_from_path from copienator import EvaluationWorkspace, ExitCode, atomic_write_json from copienator import general_remarks as remarks from copienator.commands import annotating, reading_annotations as standard from copienator.commands import reading_grouped_annotations as grouped from copienator.return_answers import publish_answer_returns class GeneralRemarkTests(unittest.TestCase): def test_order_normalization_and_deletion_preserve_other_copies(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) remarks.set_remarks(root, "01", [" First ", "Second"]) remarks.set_remarks(root, "2", ["Other"]) self.assertEqual(remarks.set_remarks(root, "1", ["Second", "First"]), ["First", "Second"]) self.assertEqual(remarks.remarks_for_copy(root, "01"), ["Second", "First"]) remarks.set_remarks(root, "1", []) self.assertEqual(remarks.load_remarks(root), {"2": ["Other"]}) def test_latex_has_one_paragraph_per_remark_without_truncation(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) remarks.set_remarks(root, "1", [r"Justifier $x^2$.", "Soigner la rédaction."]) with patch.object(annotating, "render_real_latex_text", return_value=Image.new("RGB", (600, 150), "white")) as render: image = remarks.render(root, "01", 800) self.assertEqual(image.size, (800, 190)) self.assertIn(r"Justifier $x^2$.\par\medskip Soigner", render.call_args.args[0]) self.assertNotIn("max_lines", render.call_args.kwargs) def test_readers_publish_header_even_for_perfect_answers_then_remove_it(self): for mode in ("Bnot", "BGnot"): with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp: root = Path(tmp) workspace = EvaluationWorkspace(root) output = root / mode / "Copie01" output.mkdir(parents=True) (root / "labels").write_text("Ex 1\n") 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"}) data = {"01": {"Ex 1": {"pdf_path": answer, "coordinates": (0, 0), "result": {"score": 4, "feedback": []}}}} remarks.set_remarks(root, "1", ["First", "Second"]) def run(): if mode == "Bnot": return standard.apply_actions_and_regenerate(workspace, deepcopy(data), "01", [], None, ["Ex 1"], update_score=True) return grouped.apply_actions_and_regenerate_grouped(workspace, deepcopy(data), "01", [], {}, ["Ex 1"], update_score=True)[0] with patch.object(annotating, "make_base_image", return_value=(Image.new("RGB", (800, 100), "blue"), 0, 0)), \ patch.object(annotating, "compose_label_image", return_value=(Image.new("RGB", (800, 100), "blue"), 10)), \ patch.object(annotating, "render_real_latex_text", return_value=Image.new("RGB", (760, 60), "red")), \ patch.object(grouped, "get_extra_pdfs_as_images", return_value=[]), \ patch("copienator.configuration.RETURN_ANSWERS_ENABLED", True), \ patch("copienator.return_answers.load_return_answer_options", return_value={"context": False, "question": False, "solution": False}): self.assertEqual(run(), ExitCode.SUCCESS) with Image.open(output / "Concat.jpg") as image: self.assertEqual(image.height, 200) self.assertGreater(image.getpixel((40, 40))[0], 230) self.assertGreater(image.getpixel((40, 150))[2], 230) pages = convert_from_path(output / "Concat_F.pdf", dpi=100) self.assertGreater(pages[0].getpixel((60, 60))[0], 230) destination = root / "A Rendre" / "Student (01)" publish_answer_returns(root, output, destination) self.assertTrue((destination / "answers" / remarks.IMAGE_FILENAME).is_file()) remarks.set_remarks(root, "1", []) self.assertEqual(run(), ExitCode.SUCCESS) publish_answer_returns(root, output, destination) self.assertFalse((output / remarks.IMAGE_FILENAME).exists()) self.assertFalse((output / "Concat_F.pdf").exists()) self.assertFalse((destination / "answers" / remarks.IMAGE_FILENAME).exists()) with Image.open(output / "Concat.jpg") as image: self.assertEqual(image.height, 100)