start-gui.sh et améliorations diverses
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from copienator_gui.app import CopienatorApp
|
||||
from copienator_gui.workflow import command_display
|
||||
from copienator.commands.page_splitter import _selected_inputs
|
||||
from copienator import EvaluationWorkspace
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display")
|
||||
class GuiConvenienceTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.repository = Path(self.temp.name)
|
||||
self.evaluation = self.repository / "Évaluation avec espaces"
|
||||
self.evaluation.mkdir()
|
||||
self.app = CopienatorApp(self.repository, False, self.evaluation)
|
||||
self.app.update()
|
||||
|
||||
def tearDown(self):
|
||||
for callback in self.app.tk.splitlist(self.app.tk.call("after", "info")):
|
||||
self.app.after_cancel(callback)
|
||||
self.app.destroy()
|
||||
self.temp.cleanup()
|
||||
|
||||
def test_reload_detects_added_and_removed_files_without_advancing(self):
|
||||
for name in ("enonce.pdf", "enonce.tex", "correction.tex"):
|
||||
(self.evaluation / name).touch()
|
||||
self.app._reload_inputs()
|
||||
self.assertIn("names", self.app.info_var.get())
|
||||
(self.repository / "names").touch()
|
||||
self.app._reload_inputs()
|
||||
self.app.update()
|
||||
self.assertEqual(self.app.state_store.step("inputs")["status"], "success")
|
||||
self.assertEqual(self.app.current_step.id, "inputs")
|
||||
(self.evaluation / "enonce.pdf").unlink()
|
||||
self.app._reload_inputs()
|
||||
self.assertEqual(self.app.state_store.step("inputs")["status"], "ready")
|
||||
self.assertIn("enonce.pdf", self.app.description_label.cget("text"))
|
||||
|
||||
def test_redo_targets_selected_copy_and_copies_runnable_command(self):
|
||||
for folder in ("Copies", "Copies Originales"):
|
||||
(self.evaluation / folder).mkdir()
|
||||
for name in ("Copie01.pdf", "Copie02.pdf"):
|
||||
(self.evaluation / folder / name).touch()
|
||||
self.app.tree.selection_set("page_splitter")
|
||||
self.app.update()
|
||||
self.app.copy_var.set("Copie02.pdf")
|
||||
self.app._redo_selected_pages()
|
||||
command = self.app._make_command()
|
||||
target = Path(command[-1])
|
||||
self.assertEqual(target, self.evaluation / "Copies" / "Copie02.pdf")
|
||||
self.assertEqual(_selected_inputs(EvaluationWorkspace(self.evaluation), target),
|
||||
[self.evaluation / "Copies Originales" / "Copie02.pdf"])
|
||||
self.app._copy_command()
|
||||
self.assertEqual(self.app.clipboard_get(), command_display(command))
|
||||
self.app._target_all_pages()
|
||||
self.assertEqual(self.app.arg_vars["target"].get(), self.app._evaluation_arg())
|
||||
|
||||
def test_console_selection_survives_output_and_is_read_only(self):
|
||||
self.app._append_console("Première ligne\nDeuxième ligne\n")
|
||||
self.app.console.tag_add("sel", "1.0", "1.end")
|
||||
self.app._append_console("Suite\n")
|
||||
self.app._copy_console_selection()
|
||||
self.assertEqual(self.app.clipboard_get(), "Première ligne")
|
||||
self.app._copy_console_all()
|
||||
expected = "Première ligne\nDeuxième ligne\nSuite\n"
|
||||
self.assertEqual(self.app.clipboard_get(), expected)
|
||||
self.app.console.insert("end", "unwanted edit")
|
||||
self.assertEqual(self.app.console.get("1.0", "end-1c"), expected)
|
||||
self.app._select_console_all()
|
||||
self.app._copy_console_selection()
|
||||
self.assertEqual(self.app.clipboard_get(), expected)
|
||||
@@ -812,7 +812,7 @@ class StandardCliTests(unittest.TestCase):
|
||||
evaluation = Path(directory) / "Exam"
|
||||
copy_pdf = evaluation / "Copies" / "Copie01.pdf"
|
||||
copy_pdf.parent.mkdir(parents=True)
|
||||
document = module.fitz.open()
|
||||
document = module.pymupdf.open()
|
||||
page = document.new_page(width=600, height=800)
|
||||
page.insert_text((100, 200), "Student answer")
|
||||
document.save(copy_pdf)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pymupdf
|
||||
|
||||
from copienator.commands.page_splitter import PDFPreviewer, PAGE_SPLITTER_KB
|
||||
|
||||
|
||||
class ReversePagesTests(unittest.TestCase):
|
||||
def test_reverse_restarts_with_fresh_settings_and_exports_reversed_pages(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
source = Path(temporary) / "copy.pdf"
|
||||
with pymupdf.open() as document:
|
||||
for index in range(3):
|
||||
page = document.new_page(width=200 + 20 * index, height=300)
|
||||
page.insert_text((30, 30), f"Page {index + 1}")
|
||||
document.save(source)
|
||||
original = source.read_bytes()
|
||||
preview = PDFPreviewer.__new__(PDFPreviewer)
|
||||
preview.doc = pymupdf.open(source)
|
||||
self.addCleanup(lambda: None if preview.doc.is_closed else preview.doc.close())
|
||||
preview.current_page_index = 2
|
||||
preview.page_settings = [{"keep": "none"}, {"keep": "left"}]
|
||||
preview.current_rotation = 180
|
||||
preview.file_rotation = 180
|
||||
preview.global_rotation = 180
|
||||
preview.processing = False
|
||||
preview.load_page = Mock()
|
||||
|
||||
preview.reverse_pages()
|
||||
|
||||
self.assertEqual([page.get_text().strip() for page in preview.doc],
|
||||
["Page 3", "Page 2", "Page 1"])
|
||||
self.assertEqual(preview.current_page_index, 0)
|
||||
self.assertEqual(preview.page_settings, [])
|
||||
self.assertEqual(preview.current_line_x, 120)
|
||||
self.assertEqual(preview.current_rotation, 0)
|
||||
self.assertEqual((preview.file_rotation, preview.global_rotation), (180, 180))
|
||||
preview.load_page.assert_called_once_with()
|
||||
self.assertEqual(source.read_bytes(), original)
|
||||
|
||||
preview.reverse_pages()
|
||||
self.assertEqual([page.get_text().strip() for page in preview.doc],
|
||||
["Page 1", "Page 2", "Page 3"])
|
||||
preview.reverse_pages()
|
||||
|
||||
preview.base_name = "copy"
|
||||
preview.split_dir = Path(temporary) / "split"
|
||||
preview.reorder_dir = Path(temporary) / "reorder"
|
||||
preview.final_file = Path(temporary) / "result.pdf"
|
||||
preview.output_dir = None
|
||||
preview.page_settings = [
|
||||
{"keep": "as_is", "rotation": 0, "line_x": page.rect.width / 2}
|
||||
for page in preview.doc
|
||||
]
|
||||
preview.split_pdf()
|
||||
preview.reorder_pdfs()
|
||||
preview.concate_files()
|
||||
with pymupdf.open(preview.final_file) as result:
|
||||
self.assertEqual([page.get_text().strip() for page in result],
|
||||
["Page 3", "Page 2", "Page 1"])
|
||||
|
||||
def test_single_page_can_restart_and_processing_ignores_shortcut(self):
|
||||
preview = PDFPreviewer.__new__(PDFPreviewer)
|
||||
preview.doc = pymupdf.open()
|
||||
self.addCleanup(preview.doc.close)
|
||||
preview.doc.new_page()
|
||||
preview.processing = True
|
||||
preview.load_page = Mock()
|
||||
preview.reverse_pages()
|
||||
preview.load_page.assert_not_called()
|
||||
preview.processing = False
|
||||
preview.reverse_pages()
|
||||
self.assertEqual(preview.current_page_index, 0)
|
||||
self.assertEqual(preview.page_settings, [])
|
||||
preview.load_page.assert_called_once_with()
|
||||
|
||||
def test_shortcut_available_with_personal_configuration(self):
|
||||
self.assertEqual(PAGE_SPLITTER_KB["reverse_pages"], "i")
|
||||
@@ -0,0 +1,165 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from copienator import CliError, EvaluationWorkspace, ExitCode
|
||||
from copienator.commands import enonce_info as personal
|
||||
from copienator.commands import gemini_for_enonce as gemini
|
||||
from copienator_gui.app import CopienatorApp
|
||||
from copienator_gui.workflow import build_workflow
|
||||
|
||||
|
||||
class PersonalStatementTests(unittest.TestCase):
|
||||
def test_personal_generation_uses_statement_and_groups_by_exercise(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / "correction.tex").write_text("Not SHEETINFO")
|
||||
blocks = [{"id": 42, "indexes": [{"indices": [1]}, {"indices": [2]}]},
|
||||
{"id": 99}]
|
||||
(root / "enonce.tex").write_text("\n".join(
|
||||
"%%SHEETINFO : " + json.dumps(block) for block in blocks))
|
||||
(root / "label_groups").write_text("obsolete groups")
|
||||
urls = []
|
||||
|
||||
def fetch(url):
|
||||
urls.append(url)
|
||||
if "/emacs/" in url:
|
||||
return io.BytesIO(b"Statement\n 1) Question\n###Solution\n 1) Answer\n###Rubric\n 1) Points")
|
||||
return io.BytesIO(b"Selected exercise content")
|
||||
|
||||
def compile_pdf(content, path):
|
||||
Path(path).write_bytes(b"test PDF")
|
||||
|
||||
with patch.object(personal.urllib.request, "urlopen", side_effect=fetch), patch.object(
|
||||
personal, "compile_to_pdf", side_effect=compile_pdf
|
||||
):
|
||||
self.assertEqual(personal.process_directory(EvaluationWorkspace(root)), ExitCode.SUCCESS)
|
||||
self.assertEqual((root / "labels").read_text(), "Ex 1 : 1)\nEx 1 : 2)\nEx 2\n")
|
||||
self.assertEqual((root / "label_groups").read_text(), "Ex 1 : 1), Ex 1 : 2)\nEx 2\n")
|
||||
for folder in ("Text2", "Sol2"):
|
||||
for label in ("Ex 1 : 1)", "Ex 1 : 2)", "Ex 2"):
|
||||
self.assertTrue((root / folder / f"{label}.tex").is_file())
|
||||
self.assertTrue((root / folder / f"{label}.pdf").is_file())
|
||||
self.assertIn("Rubric", (root / "Persp" / "Ex 1").read_text())
|
||||
self.assertTrue(any("/exo_q_text/42/1" in url for url in urls))
|
||||
|
||||
def test_personal_choice_and_optional_steps_only_in_personal_profile(self):
|
||||
standard = {step.id: step for step in build_workflow(False)}
|
||||
enabled = {step.id: step for step in build_workflow(True)}
|
||||
self.assertEqual([variant.id for variant in standard["statement"].variants], ["gemini"])
|
||||
self.assertEqual([variant.program for variant in enabled["statement"].variants],
|
||||
["statement", "statement-personal"])
|
||||
for ident in ("statement_groups", "statement_persp"):
|
||||
self.assertNotIn(ident, standard)
|
||||
self.assertTrue(enabled[ident].optional)
|
||||
self.assertFalse(enabled[ident].auto_start_first_visit)
|
||||
|
||||
|
||||
class SelectiveGeminiTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.workspace = EvaluationWorkspace(self.root)
|
||||
self.labels = ["Ex 1 : 1)", "Ex 1 : 2)", "Ex 2"]
|
||||
(self.root / "labels").write_text("\n".join(self.labels) + "\n")
|
||||
(self.root / "label_groups").write_text(", ".join(self.labels[:2]) + "\nEx 2\n")
|
||||
for folder in ("Text", "Sol", "Text2", "Sol2", "Persp"):
|
||||
(self.root / folder).mkdir()
|
||||
for label in self.labels:
|
||||
suffix = ".tex" if folder.endswith("2") else ""
|
||||
(self.root / folder / (label + suffix)).write_text(f"Personal {folder}: {label}")
|
||||
(self.root / "Persp" / "Ex 1").write_text("Old aggregate rubric")
|
||||
self.client = SimpleNamespace(models=SimpleNamespace(generate_content=Mock()))
|
||||
|
||||
def snapshot(self):
|
||||
return {str(path.relative_to(self.root)): path.read_bytes()
|
||||
for path in self.root.rglob("*") if path.is_file()}
|
||||
|
||||
def response(self, value):
|
||||
return SimpleNamespace(text=json.dumps(value))
|
||||
|
||||
def test_grouping_changes_only_groups_and_keeps_exact_labels(self):
|
||||
before = self.snapshot()
|
||||
self.client.models.generate_content.return_value = self.response({"groups": [[label] for label in self.labels]})
|
||||
self.assertEqual(gemini.refine_existing(self.workspace, "groups", api_client=self.client), ExitCode.SUCCESS)
|
||||
after = self.snapshot()
|
||||
self.assertEqual(after.pop("label_groups"), ("\n".join(self.labels) + "\n").encode())
|
||||
before.pop("label_groups")
|
||||
self.assertEqual(after, before)
|
||||
|
||||
def test_invalid_grouping_preserves_existing_groups(self):
|
||||
before = self.snapshot()
|
||||
for groups in ([[self.labels[0]]], [[*self.labels, self.labels[0]]], [[*self.labels, "invented"]]):
|
||||
self.client.models.generate_content.return_value = self.response({"groups": groups})
|
||||
with self.assertRaises(CliError):
|
||||
gemini.refine_existing(self.workspace, "groups", api_client=self.client)
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
def rubric_response(self, labels):
|
||||
return self.response({"rubrics": [{"label": label, "rubric_content": "Barème Gemini sur 4 points"}
|
||||
for label in labels]})
|
||||
|
||||
def test_rubrics_replace_only_persp_using_normal_prompt(self):
|
||||
before = self.snapshot()
|
||||
self.client.models.generate_content.side_effect = [self.rubric_response(self.labels[:2]),
|
||||
self.rubric_response(self.labels[2:])]
|
||||
self.assertEqual(gemini.refine_existing(self.workspace, "persp", api_client=self.client), ExitCode.SUCCESS)
|
||||
after = self.snapshot()
|
||||
self.assertEqual({key: value for key, value in before.items() if not key.startswith("Persp/")},
|
||||
{key: value for key, value in after.items() if not key.startswith("Persp/")})
|
||||
self.assertFalse((self.root / "Persp" / "Ex 1").exists())
|
||||
for label in self.labels:
|
||||
self.assertIn("Barème Gemini", (self.root / "Persp" / label).read_text())
|
||||
for call in self.client.models.generate_content.call_args_list:
|
||||
self.assertEqual(call.kwargs["contents"][0].parts[0].text, gemini.PROMPT_4)
|
||||
|
||||
def test_incomplete_or_failed_rubrics_preserve_entire_persp(self):
|
||||
before = self.snapshot()
|
||||
for last in (self.response({"rubrics": []}), RuntimeError("API unavailable")):
|
||||
self.client.models.generate_content.side_effect = [self.rubric_response(self.labels[:2]), last]
|
||||
with self.assertRaises((CliError, RuntimeError)):
|
||||
gemini.refine_existing(self.workspace, "persp", api_client=self.client)
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
def test_cli_dispatches_selective_modes_without_full_extraction(self):
|
||||
with patch.object(gemini, "refine_existing", return_value=ExitCode.SUCCESS) as refine, patch.object(
|
||||
gemini, "process_exam"
|
||||
) as full:
|
||||
for flag, mode in (("--groups-only", "groups"), ("--persp-only", "persp")):
|
||||
self.assertEqual(gemini.main([str(self.root), flag]), ExitCode.SUCCESS)
|
||||
self.assertEqual(refine.call_args.args[1], mode)
|
||||
full.assert_not_called()
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display")
|
||||
class PersonalStatementGuiTests(unittest.TestCase):
|
||||
def test_personal_requirements_and_optional_button_command_previews(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
evaluation = Path(temporary)
|
||||
(evaluation / "enonce.tex").touch()
|
||||
app = CopienatorApp(Path.cwd(), True, evaluation)
|
||||
try:
|
||||
app.update()
|
||||
app.tree.selection_set("statement")
|
||||
app.update()
|
||||
app._select_variant(1)
|
||||
self.assertIn("statement-personal", app.command_var.get())
|
||||
self.assertEqual(app._missing_requirements(app.current_step), [])
|
||||
self.assertIn("SHEETINFO", app.description_label.cget("text"))
|
||||
for ident, flag in (("statement_groups", "--groups-only"), ("statement_persp", "--persp-only")):
|
||||
app._select_statement_action(ident)
|
||||
app.update()
|
||||
self.assertEqual(app.current_step.id, ident)
|
||||
self.assertIn(flag, app.command_var.get())
|
||||
self.assertTrue(app.current_step.optional)
|
||||
self.assertFalse(app.runner.running)
|
||||
finally:
|
||||
for callback in app.tk.splitlist(app.tk.call("after", "info")):
|
||||
app.after_cancel(callback)
|
||||
app.destroy()
|
||||
Reference in New Issue
Block a user