Cropping of individual exos

This commit is contained in:
2026-09-09 15:24:39 +02:00
parent 3969580e01
commit 0a167072ed
7 changed files with 719 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
import contextlib
import io
import tempfile
import unittest
from pathlib import Path
import pymupdf
from copienator.cli import CliError
from copienator.commands import crop_exercise_bottoms
from copienator.commands.clean import apply_cleanup, build_cleanup_plan
from copienator.dispatcher import main
from copienator.workspace import EvaluationWorkspace
from copienator_gui.workflow import build_workflow
class CropExerciseBottomsCommandTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.workspace = EvaluationWorkspace(Path(self.temp.name) / "Évaluation")
self.workspace.copies_dir.mkdir(parents=True)
self.copy_pdf = self.workspace.copies_dir / "Copie01.pdf"
with pymupdf.open() as document:
document.new_page(width=600, height=800)
document.save(self.copy_pdf)
self.answers = self.workspace.copies_dir / "Copie01"
self.answers.mkdir()
self.large = self.answers / "Ex 1.pdf"
self.short = self.answers / "Ex 2.pdf"
self._make_answer(self.large, 400, 100, footer=True)
self._make_answer(self.short, 200, 100, footer=False)
self.large_original = self.large.read_bytes()
self.short_original = self.short.read_bytes()
@staticmethod
def _make_answer(path: Path, height: float, text_y: float, *, footer: bool) -> None:
with pymupdf.open() as document:
page = document.new_page(width=600, height=height)
page.insert_text((80, text_y), "student answer", fontsize=16)
if footer:
page.insert_text((20, height - 3), "Ex 2", fontsize=10)
document.save(path)
def test_dispatcher_replaces_only_changed_answers_and_backs_them_up(self):
with contextlib.redirect_stdout(io.StringIO()) as log:
self.assertEqual(
main(["crop-answer-bottoms", str(self.workspace.root), "--workers", "2"]),
0,
)
self.assertIn("1 PDF remplacé", log.getvalue())
with pymupdf.open(self.large) as cropped:
self.assertLess(cropped[0].rect.height, 250)
self.assertEqual(self.short.read_bytes(), self.short_original)
backups = list(
self.workspace.runs_dir.glob(
"crop-exercise-bottoms-*/Copies/Copie01/Ex 1.pdf"
)
)
self.assertEqual(len(backups), 1)
self.assertEqual(backups[0].read_bytes(), self.large_original)
self.assertFalse(
list(
self.workspace.runs_dir.glob(
"crop-exercise-bottoms-*/Copies/Copie01/Ex 2.pdf"
)
)
)
self.assertTrue((backups[0].parents[2] / "report.json").is_file())
def test_detection_failure_does_not_publish_an_earlier_result(self):
broken = self.answers / "Ex 3.pdf"
broken.write_bytes(b"not a PDF")
with contextlib.redirect_stdout(io.StringIO()), self.assertRaises(Exception):
crop_exercise_bottoms.run(
self.workspace, self.workspace.root, workers=1
)
self.assertEqual(self.large.read_bytes(), self.large_original)
self.assertEqual(self.short.read_bytes(), self.short_original)
self.assertEqual(broken.read_bytes(), b"not a PDF")
self.assertFalse(self.workspace.runs_dir.exists())
def test_invalid_target_and_worker_count_are_rejected(self):
with self.assertRaises(CliError):
crop_exercise_bottoms.run(self.workspace, self.large, workers=1)
with self.assertRaises(CliError):
crop_exercise_bottoms.run(self.workspace, self.workspace.root, workers=0)
def test_archiving_removes_the_retained_originals_and_report(self):
with contextlib.redirect_stdout(io.StringIO()):
crop_exercise_bottoms.run(
self.workspace, self.workspace.root, workers=1
)
run_dirs = list(
self.workspace.runs_dir.glob("crop-exercise-bottoms-*")
)
self.assertEqual(len(run_dirs), 1)
self.workspace.correction_file.write_text("{}")
student = self.workspace.return_dir / "Student"
student.mkdir(parents=True)
(student / "answer.jpg").write_bytes(b"return image")
(student / "score.json").write_text("{}")
plan = build_cleanup_plan(self.workspace)
self.assertTrue(
any(path.name == "report.json" for path in plan.deleted_files)
)
with contextlib.redirect_stdout(io.StringIO()):
apply_cleanup(self.workspace, plan)
self.assertFalse(self.workspace.runs_dir.exists())
def test_optional_step_follows_splitting_and_precedes_grouping(self):
steps = build_workflow(False)
index = next(
i for i, step in enumerate(steps) if step.id == "crop_exercise_bottoms"
)
self.assertEqual(steps[index - 1].id, "splitting")
self.assertEqual(steps[index + 1].id, "grouping")
self.assertTrue(steps[index].optional)
self.assertFalse(steps[index].auto_start_first_visit)
if __name__ == "__main__":
unittest.main()