182 lines
9.2 KiB
Python
182 lines
9.2 KiB
Python
"""Large checked copies survive the tablet round trip as separate documents."""
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
from copienator import configuration
|
|
from copienator import CliError, EvaluationWorkspace, ExitCode, read_json
|
|
from copienator.annotation_data import AnnotationLoadResult
|
|
from copienator.annotation_documents import annotation_limits, copy_documents
|
|
from copienator.commands import annotating_with_checks as checks
|
|
from copienator.commands import export, import_annotations, reading_annotations as reader
|
|
from copienator.commands import reading_grouped_annotations as grouped_reader
|
|
|
|
|
|
class CheckedDocumentTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
self.workspace = EvaluationWorkspace(self.root / 'Exam')
|
|
for name in ('Copies', 'Par label'):
|
|
(self.workspace.root / name).mkdir(parents=True)
|
|
(self.workspace.root / 'labels').write_text('Ex 1\nEx 2\nEx 3\n')
|
|
(self.workspace.root / 'correction.json').write_text('{}')
|
|
self.heights = {'Ex 1': 60, 'Ex 2': 40, 'Ex 3': 60}
|
|
self.labels = {}
|
|
for label in self.heights:
|
|
pdf = self.workspace.root / 'Copies' / f'{label}.pdf'
|
|
pdf.touch()
|
|
self.labels[label] = {'pdf_path': pdf, 'result': {'score': 2, 'feedback': []},
|
|
'coordinates': (0, 0)}
|
|
self.output = self.workspace.root / 'Bnot' / 'Copie01'
|
|
|
|
def compose(self, base, label, result, coordinates, **kwargs):
|
|
image = Image.new('RGB', (100, self.heights[label]), 'white')
|
|
callback = kwargs.get('draw_callback')
|
|
if callback:
|
|
callback.__self__.checkboxes.append({
|
|
'type': 'score', 'label': label, 'value': 3,
|
|
'final_box': [10, 5, 30, 25],
|
|
})
|
|
ImageDraw.Draw(image).rectangle((10, 5, 30, 25), outline='black', width=2)
|
|
return image, 0
|
|
|
|
def generate(self, overwrite=False):
|
|
with (
|
|
patch.object(configuration, 'ANNOTATION_MAX_HEIGHT_PX', 100),
|
|
patch.object(checks.annotating, 'make_base_image', return_value=(None, 0, 0)),
|
|
patch.object(checks.annotating, 'compose_label_image', side_effect=self.compose),
|
|
):
|
|
return checks._render_student(self.workspace, '01', self.labels,
|
|
overwrite=overwrite, output_mode='checks')
|
|
|
|
def test_splits_on_labels_and_resets_geometry_at_each_part(self):
|
|
self.assertEqual(annotation_limits()[0], 33000)
|
|
self.assertEqual(self.generate(), 'success')
|
|
parts = copy_documents(self.output)
|
|
self.assertEqual([part.name for part in parts], ['Part1', 'Part2'])
|
|
self.assertEqual([read_json(p / 'bnote.json')['height'] for p in parts], [100, 60])
|
|
self.assertEqual([item['hmin'] for item in read_json(self.output / 'bnote.json')['images']],
|
|
[0, 60, 0])
|
|
self.assertEqual(read_json(parts[1] / 'checkboxes.json')[0]['global_box'], [10, 5, 30, 25])
|
|
self.assertTrue(checks._output_complete(self.output))
|
|
self.assertEqual(self.generate(), 'skipped')
|
|
(parts[1] / 'Reference.jpg').unlink()
|
|
self.assertFalse(checks._output_complete(self.output))
|
|
|
|
def test_overwrite_removes_stale_parts_and_keeps_small_copy_layout(self):
|
|
self.generate()
|
|
self.labels = {'Ex 1': self.labels['Ex 1']}
|
|
self.generate(overwrite=True)
|
|
self.assertEqual(copy_documents(self.output), [self.output])
|
|
self.assertTrue((self.output / 'Concat.pdf').is_file())
|
|
self.assertFalse((self.output / 'Part1').exists())
|
|
|
|
def test_pixel_limit_splits_documents_at_exact_boundary(self):
|
|
with patch.object(configuration, 'ANNOTATION_MAX_PIXELS', 6000):
|
|
self.assertEqual(self.generate(), 'success')
|
|
parts = copy_documents(self.output)
|
|
self.assertEqual([read_json(p / 'bnote.json')['height'] for p in parts], [60, 40, 60])
|
|
|
|
def test_pixel_limit_accounts_for_width_growth_and_preserves_previous_copy(self):
|
|
original = self.compose
|
|
def wider(base, label, *args, **kwargs):
|
|
image, header = original(base, label, *args, **kwargs)
|
|
if label == 'Ex 2':
|
|
image = image.resize((200, image.height))
|
|
return image, header
|
|
with patch.object(self, 'compose', side_effect=wider), patch.object(configuration, 'ANNOTATION_MAX_PIXELS', 12000):
|
|
self.assertEqual(self.generate(), 'success')
|
|
parts = copy_documents(self.output)
|
|
self.assertEqual([read_json(p / 'bnote.json')['width'] for p in parts], [100, 200, 100])
|
|
sentinel = self.output / 'sentinel'
|
|
sentinel.write_text('previous')
|
|
with patch.object(configuration, 'ANNOTATION_MAX_PIXELS', 5999):
|
|
with self.assertRaisesRegex(CliError, 'Copie01.*Ex 1'):
|
|
self.generate(overwrite=True)
|
|
self.assertEqual(sentinel.read_text(), 'previous')
|
|
|
|
def test_single_oversized_answer_and_save_failure_preserve_previous_copy(self):
|
|
self.generate()
|
|
sentinel = self.output / 'sentinel'
|
|
sentinel.write_text('previous')
|
|
self.heights['Ex 3'] = 101
|
|
with self.assertRaisesRegex(CliError, 'Copie01.*Ex 3'):
|
|
self.generate(overwrite=True)
|
|
self.assertEqual(sentinel.read_text(), 'previous')
|
|
self.heights['Ex 3'] = 60
|
|
original = checks._save_document
|
|
def fail_second(directory, *args):
|
|
if directory.name == 'Part2':
|
|
raise OSError('disk full')
|
|
original(directory, *args)
|
|
with patch.object(checks, '_save_document', side_effect=fail_second):
|
|
with self.assertRaisesRegex(OSError, 'disk full'):
|
|
self.generate(overwrite=True)
|
|
self.assertEqual(sentinel.read_text(), 'previous')
|
|
|
|
def returned_parts(self):
|
|
self.generate()
|
|
export_root = self.root / 'Export'
|
|
with patch.object(export.configuration, 'EXPORT_DIR', export_root):
|
|
self.assertEqual(export.run(self.workspace, annotation_dir='Bnot'), ExitCode.SUCCESS)
|
|
self.assertEqual(sorted(p.name for p in (export_root / 'Exam').iterdir()),
|
|
['Copie01 Part1.pdf', 'Copie01 Part2.pdf'])
|
|
import_root = self.root / 'Import'
|
|
import_root.mkdir()
|
|
for part in copy_documents(self.output):
|
|
with Image.open(part / 'Reference.jpg') as reference:
|
|
image = reference.convert('RGB')
|
|
draw = ImageDraw.Draw(image)
|
|
# Change Ex 3's score and add handwriting in its body.
|
|
if part.name == 'Part2':
|
|
draw.rectangle((14, 9, 26, 21), fill='black')
|
|
draw.rectangle((70, 40, 85, 50), fill='black')
|
|
image.save(import_root / f'Copie01 {part.name}.pdf', 'PDF', resolution=72)
|
|
self.assertEqual(import_annotations.sync_annotated(
|
|
self.workspace, annotation_dir_name='Bnot', import_dir=import_root), ExitCode.SUCCESS)
|
|
|
|
def test_export_import_and_reader_merge_parts_without_moving_notes(self):
|
|
self.returned_parts()
|
|
with (
|
|
patch.object(reader, 'load_annotation_data', return_value=AnnotationLoadResult({'01': self.labels}, [])),
|
|
patch.object(reader.annotating, 'make_base_image', return_value=(None, 0, 0)),
|
|
patch.object(reader.annotating, 'compose_label_image', side_effect=self.compose),
|
|
):
|
|
self.assertEqual(reader.run(self.workspace), ExitCode.SUCCESS)
|
|
self.assertEqual(read_json(self.output / 'score.json'), {'Ex 1': '2', 'Ex 2': '2', 'Ex 3': '3'})
|
|
with Image.open(self.output / 'Ex 3.jpg') as image:
|
|
self.assertLess(max(image.getpixel((77, 45))), 80)
|
|
with Image.open(self.output / 'Ex 1.jpg') as image:
|
|
self.assertGreater(min(image.getpixel((77, 45))), 230)
|
|
|
|
def test_missing_return_part_preserves_previous_regenerated_files(self):
|
|
self.returned_parts()
|
|
(self.output / 'Part2' / 'Concat_annotated.pdf').unlink()
|
|
(self.output / 'score.json').write_text('{"previous": true}')
|
|
with patch.object(reader, 'load_annotation_data', return_value=AnnotationLoadResult({'01': self.labels}, [])):
|
|
self.assertEqual(reader.run(self.workspace), ExitCode.PARTIAL)
|
|
self.assertEqual(read_json(self.output / 'score.json'), {'previous': True})
|
|
|
|
def test_redo_reader_accepts_split_copy_documents(self):
|
|
self.returned_parts()
|
|
# Redo uses the same document layout under BRnot.
|
|
self.output.parent.rename(self.workspace.root / 'BRnot')
|
|
actions, notes, incomplete = grouped_reader._scan_redo_annotations(
|
|
self.workspace.root / 'BRnot', {'01': set(self.labels)})
|
|
self.assertFalse(incomplete)
|
|
self.assertEqual(set(notes['01']), set(self.labels))
|
|
self.assertEqual(actions['01'][0]['label'], 'Ex 3')
|
|
(self.workspace.root / 'BRnot/Copie01/Part2/Concat_annotated.pdf').unlink()
|
|
_, _, incomplete = grouped_reader._scan_redo_annotations(
|
|
self.workspace.root / 'BRnot', {'01': set(self.labels)})
|
|
self.assertEqual(incomplete, {'01'})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|