Improve web finalisation navigation, caching and Anot support

This commit is contained in:
sebastien committed 2026-11-03 11:38:39 +01:00
1 parent 5de70dbc0d
commit b069805988
21 files changed
+339 -58

No files matched your search

+2
View File
@@ -103,6 +103,8 @@ class AddFinalScoreTests(unittest.TestCase):
self.assertFalse((student / "answers" / "001 - Ex 1.jpg").exists())
self.assertEqual((output / "histogramme.pdf").read_bytes(), b"histogram")
self.assertEqual(read_json(output / "stat.json"), {"Ex 1": "4.0"})
self.assertEqual(read_json(root / '.copienator/final_scores.json'),
{'version': 1, 'copies': {'1': {'name': 'Student', 'score': '12.3'}}})
def test_omits_answers_directory_when_it_was_not_generated(self):
with tempfile.TemporaryDirectory() as temporary:
+42
View File
@@ -0,0 +1,42 @@
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import pandas as pd
from copienator import atomic_write_json
from copienator.final_scores import exported_scores
class ExportedFinalScoresTests(unittest.TestCase):
def test_legacy_export_uses_only_sheet_predating_export(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)/'Interro'
(root/'A Rendre/Alice (01)').mkdir(parents=True)
ods = root/'marks.ods'
ods.write_bytes(b'sheet')
image = root/'output/Interro/Alice/Alice.jpg'
image.parent.mkdir(parents=True)
image.write_bytes(b'export')
os.utime(ods, ns=(1000000,1000000))
os.utime(image, ns=(2000000,2000000))
with patch('copienator.final_scores.configuration.FINAL_SCORE_OUTPUT_DIR',root/'output'), \
patch('copienator.final_scores.configuration.FINAL_SCORE_ODS_PATH',ods), \
patch('pandas.read_excel',return_value=pd.DataFrame({0:['Alice'],1:[12.39]})) as read:
self.assertEqual(exported_scores(root,root), {'1':'12.3'})
os.utime(ods, ns=(3000000,3000000))
read.reset_mock()
self.assertEqual(exported_scores(root,root), {})
read.assert_not_called()
def test_no_final_mark_before_export_and_saved_marks_do_not_read_live_sheet(self):
with tempfile.TemporaryDirectory() as temporary:
root=Path(temporary)
with patch('copienator.final_scores.configuration.FINAL_SCORE_OUTPUT_DIR',root/'output'), \
patch('copienator.final_scores.configuration.FINAL_SCORE_ODS_PATH',root/'missing.ods'), \
patch('pandas.read_excel') as read:
self.assertEqual(exported_scores(root,root), {})
atomic_write_json(root/'.copienator/final_scores.json',
{'version':1,'copies':{'01':{'score':'0.0','name':'Alice'}}})
self.assertEqual(exported_scores(root,root), {'1':'0.0'})
read.assert_not_called()
+93 -1
View File
@@ -212,6 +212,14 @@ class WebTests(unittest.TestCase):
self.service._regenerate(False)
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
def test_exported_final_score_is_bound_to_copy_id_after_rename(self):
self.assertIsNone(self.client.get('/api/state').json()['copies'][0]['finalScore'])
atomic_write_json(self.root/'.copienator/final_scores.json',
{'version':1,'copies':{'1':{'name':'Old name','score':'12.3'}}})
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
self.save('name', 'New name')
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
def test_os_lock_blocks_cli_and_releases(self):
with EvaluationLock(self.root):
with self.assertRaises(CliError):
@@ -277,7 +285,7 @@ class SnapshotTests(unittest.TestCase):
from copienator.commands import regenerate_reviewed
from copienator.commands import annotating
from copienator.custom_remarks import set_remark
for mode in ('Bnot', 'BGnot'):
for mode in ('Bnot', 'BGnot', 'Anot'):
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, mode)
@@ -331,6 +339,27 @@ class WebGuiTests(unittest.TestCase):
app.state_store.evaluation = None
self.assertEqual(state(), 'disabled')
def test_anot_generation_enables_button_and_selects_anot(self):
from copienator_gui.app import CopienatorApp
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'Anot')
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
app._update_controls()
self.assertEqual(str(app.finalisation_web_button.cget('state')), 'normal')
app._open_finalisation_web()
dialog = app.grab_current()
def descendants(widget):
for child in widget.winfo_children():
yield child
yield from descendants(child)
combo = next(w for w in descendants(dialog) if w.winfo_class() == 'TCombobox')
self.assertEqual(combo.get(), 'Anot')
self.assertEqual(tuple(combo.cget('values')), ('Anot',))
dialog.grab_release()
dialog.destroy()
def test_modal_lock_and_return_to_existing_workflow(self):
from copienator_gui.app import CopienatorApp
from copienator.finalisation_server import WebSession
@@ -457,3 +486,66 @@ class IncrementalReviewedTests(unittest.TestCase):
(root/'A Rendre/Bob (02)/info.json').unlink()
run(1)
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())
class CrossProcessCacheTests(unittest.TestCase):
def test_reviewed_cache_survives_worker_restart_and_selects_only_two_score_edits(self):
from copienator.review_snapshot import stage
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
output = root/'BGnot/Copie01'
data = {'Ex 1': {'pdf_path':root/'Copies/Copie01/Ex 1.pdf',
'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped')
for cid in ('02','03','04'):
shutil.copytree(output,root/'BGnot'/f'Copie{cid}')
script = '''
import json, sys
from pathlib import Path
from copienator import EvaluationWorkspace, configuration
from copienator.reviewed_cache import ReviewedReadCache
from string import Template
configuration.LATEX_ANOT_BEFORE = Template(sys.argv[3])
root=Path(sys.argv[1])
directories=sorted((root/'BGnot').glob('Copie*'))
cache=ReviewedReadCache(EvaluationWorkspace(root),'BGnot',directories)
if sys.argv[2]=='remember':
cache.remember([d.name[5:] for d in directories])
print(json.dumps([d.name for d in directories if not cache.unchanged(d.name[5:])]))
'''
def run(action='check', template='Stable ${width_in} template'):
return __import__('json').loads(subprocess.check_output(
[sys.executable,'-c',script,str(root),action,template],text=True))
self.assertEqual(run('remember'), [])
self.assertEqual(run(), [])
set_score(root/'BGnot/Copie01/score.json','Ex 1','1')
set_score(root/'BGnot/Copie03/score.json','Ex 1','2')
self.assertEqual(run(), ['Copie01','Copie03'])
self.assertEqual(run('remember'), [])
self.assertEqual(run(), [])
self.assertEqual(run(template='Changed template'), ['Copie01','Copie02','Copie03','Copie04'])
def test_legacy_reader_cache_template_is_stable_across_workers(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
atomic_write_json(root/'Bnot/Copie01/review_snapshot.json', {})
script = '''
import json, sys
from pathlib import Path
from string import Template
from copienator import EvaluationWorkspace, configuration
from copienator.annotation_read_cache import AnnotationReadCache
configuration.LATEX_ANOT_BEFORE=Template('Stable ${width_in} template')
root=Path(sys.argv[1])
data={'01':{'Ex 1':{'pdf_path':root/'Copies/Copie01/Ex 1.pdf'}}}
cache=AnnotationReadCache(EvaluationWorkspace(root),'Bnot',data)
if sys.argv[2]=='remember': cache.remember(['01'])
print(json.dumps(cache.unchanged('01')))
'''
for action in ('remember','check'):
result=subprocess.check_output([sys.executable,'-c',script,str(root),action],text=True)
self.assertEqual(result.strip(),'true')