Various HTML improvements

This commit is contained in:
sebastien committed 2026-11-03 10:02:10 +01:00
1 parent fd943696fb
commit 8c1fa0af57
16 files changed
+403 -45

No files matched your search

+162
View File
@@ -58,6 +58,25 @@ class WebTests(unittest.TestCase):
self.assertEqual(response.status_code, 200, response.text)
return response.json()
def test_names_follow_gui_repository_even_when_package_is_elsewhere(self):
repository = self.root / 'application-folder'
repository.mkdir()
(repository / 'names').write_text('Alice\nBob\n', encoding='utf-8')
service = Finalisation(self.root, 'Bnot', 'token', repository=repository)
self.assertEqual(service.state()['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('Claire\n', encoding='utf-8')
self.assertEqual(service.state()['expectedNames'], ['Claire'])
def test_expected_names_use_local_roster_and_refresh_after_name_save(self):
(self.root / 'names').write_text('Alice\nBob\nAlice\n\n', encoding='utf-8')
state = self.client.get('/api/state').json()
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
state = self.save('name', 'Alice')
self.assertEqual(state['copies'][0]['name'], 'Alice')
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('', encoding='utf-8')
self.assertEqual(self.service.state()['expectedNames'], [])
def test_session_origin_and_paths(self):
outsider = TestClient(create_app(self.service, 'browser-token'))
self.assertEqual(outsider.get('/api/state').status_code, 403)
@@ -164,6 +183,35 @@ class WebTests(unittest.TestCase):
self.assertEqual(len(calls), 1)
self.assertEqual(self.service.job['status'], 'failed')
def test_names_only_skip_rendering_before_first_cache_and_after_restart(self):
from copienator.review_snapshot import stage
output = self.root / 'Bnot/Copie01'
data = {'Ex 1': {'pdf_path': self.root/'Copies/Copie01/Ex 1.pdf',
'result': {'score': 3, 'feedback': []}, 'coordinates': [0, 0]}}
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='standard', bnote={'images': []})
self.save('name', 'Alice')
self.assertFalse((output / '.reviewed-render-state.json').exists())
reopened = Finalisation(self.root, 'Bnot', 'token')
calls = []
class Process:
def __init__(self, command, **kwargs):
calls.append(command[3])
self.stdout = StringIO('done\n')
def __enter__(self): return self
def __exit__(self, *args): pass
def wait(self): return 0
with patch('copienator.finalisation.subprocess.Popen', Process):
reopened._regenerate(False)
self.assertEqual(calls, ['giving-names'])
self.save('name', 'Bob')
# A score edited outside the session must invalidate the names-only shortcut.
set_score(output/'score.json', 'Ex 1', '1')
calls.clear()
with patch('copienator.finalisation.subprocess.Popen', Process):
self.service._regenerate(False)
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
def test_os_lock_blocks_cli_and_releases(self):
with EvaluationLock(self.root):
with self.assertRaises(CliError):
@@ -256,6 +304,33 @@ class SnapshotTests(unittest.TestCase):
@unittest.skipUnless(os.environ.get('DISPLAY'), 'Tk requires a display')
class WebGuiTests(unittest.TestCase):
def test_web_button_requires_read_outputs_and_remains_enabled_when_stale(self):
from copienator_gui.app import CopienatorApp
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / 'Copies').mkdir()
(root / 'labels').write_text('Ex 1\n')
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
def state():
app._update_controls()
return str(app.finalisation_web_button.cget('state'))
self.assertEqual(state(), 'disabled')
output = root / 'BGnot/Copie01'
output.mkdir(parents=True)
atomic_write_json(output/'bnote.json', {'images': []})
self.assertEqual(state(), 'disabled')
seed(root, 'BGnot')
self.assertEqual(state(), 'normal')
app.state_store.update_step('read_annotations', status='stale')
self.assertEqual(state(), 'normal')
app.active_step_id = 'read_annotations'
self.assertEqual(state(), 'disabled')
app.active_step_id = None
self.assertEqual(state(), 'normal')
app.state_store.evaluation = None
self.assertEqual(state(), 'disabled')
def test_modal_lock_and_return_to_existing_workflow(self):
from copienator_gui.app import CopienatorApp
from copienator.finalisation_server import WebSession
@@ -295,3 +370,90 @@ class WebGuiTests(unittest.TestCase):
self.assertIsNone(app.web_session)
self.assertIsNone(app.grab_current())
self.assertEqual(app.state_store.step('read_annotations')['status'], 'stale')
class IncrementalReviewedTests(unittest.TestCase):
def test_only_changed_copies_render_and_missing_outputs_are_retried(self):
from copienator.commands import regenerate_reviewed, annotating
from copienator.review_snapshot import stage
from copienator.custom_remarks import set_remark
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
pdf = root / 'Copies/Copie01/Ex 1.pdf'
pdf.write_bytes(b'answer')
data = {'Ex 1': {'pdf_path':pdf, 'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
output = root / 'BGnot/Copie01'
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped')
shutil.copytree(output, root / 'BGnot/Copie02')
workspace = EvaluationWorkspace(root)
with patch.object(annotating, 'make_base_image', return_value=(Image.new('RGB',(100,100),'white'),0,0)), \
patch.object(annotating, 'compose_label_image', return_value=(Image.new('RGB',(100,120),'white'),20)) as compose, \
patch('copienator.commands.reading_grouped_annotations.get_extra_pdfs_as_images', return_value=[]):
def run(count):
compose.reset_mock()
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot',incremental=True), 0)
self.assertEqual(compose.call_count,count)
run(2)
run(0)
set_score(output/'score.json','Ex 1','2')
run(1)
run(0)
set_remark(root,'01','Ex 1','Justifier.')
run(1)
set_remark(root,'01','Ex 1','')
run(1)
(output/'Ex 1.jpg').unlink()
run(1)
(root/'Text2').mkdir()
(root/'Text2/Ex 1.pdf').write_bytes(b'changed question')
run(2)
run(0)
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot'),0)
self.assertEqual(compose.call_count,2)
def test_incremental_transfers_apply_changes_and_undo_without_repeating(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
workspace = EvaluationWorkspace(root)
from copienator.annotation_modifications import rebuild
with patch.object(modify_annotations, 'rebuild', wraps=rebuild) as render:
def run(text, count):
(root / DRAFT).write_text('### Source: Bnot\n' + text)
render.reset_mock()
self.assertEqual(modify_annotations.run(workspace, annotation_dir='Bnot', incremental=True), 0)
self.assertEqual(render.call_count, count)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 1)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 0)
run('Copie01 Ex 1 c{50}1> Ex 2|\n', 1)
run('', 1)
run('', 0)
self.assertEqual(Image.open(root/'Bnot/Copie01/Ex 1.jpg').height,120)
@patch('copienator.commands.giving_names.configuration.RETURN_ANSWERS_ENABLED', False)
def test_return_publication_skips_unchanged_and_repairs_missing_files(self):
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
shutil.copytree(root/'Bnot/Copie01',root/'Bnot/Copie02')
atomic_write_json(root/'Copies/Copie02.json',{'name':'Bob'})
atomic_write_json(root/'Copies/Copie01.json',{'name':'Alice'})
workspace = EvaluationWorkspace(root)
from copienator.return_answers import publish_answer_returns
with patch.object(giving_names,'publish_answer_returns',wraps=publish_answer_returns) as publish:
def run(count):
publish.reset_mock()
self.assertEqual(giving_names.prepare_named_returns(workspace,'Bnot',incremental=True),0)
self.assertEqual(publish.call_count,count)
run(2)
run(0)
set_score(root/'Bnot/Copie01/score.json','Ex 1','1')
run(1)
run(0)
(root/'A Rendre/Bob (02)/info.json').unlink()
run(1)
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())