180 lines
8.7 KiB
Python
180 lines
8.7 KiB
Python
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from copienator import read_json
|
|
from copienator.custom_remarks import remarks_for_copy
|
|
from copienator_gui.app import CopienatorApp
|
|
from copienator_gui.scores import score_path_for_id, set_score
|
|
from copienator_gui.workflow import build_workflow
|
|
|
|
|
|
class ScoreEditorTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.evaluation = Path(temporary.name)
|
|
folder = self.evaluation / "A Rendre" / "Student (01)"
|
|
folder.mkdir(parents=True)
|
|
self.path = folder / "score.json"
|
|
self.path.write_text('{"Ex 1": "2", "Ex 2": "4"}')
|
|
|
|
def test_step_is_optional_and_follows_return_preparation_in_both_profiles(self):
|
|
for personal in (False, True):
|
|
steps = build_workflow(personal)
|
|
index = next(index for index, step in enumerate(steps) if step.id == "giving_names")
|
|
self.assertEqual(steps[index + 1].id, "edit_scores")
|
|
self.assertTrue(steps[index + 1].optional)
|
|
self.assertTrue(steps[index + 1].is_manual)
|
|
|
|
def test_update_preserves_other_labels_and_follows_symlinks(self):
|
|
source = self.evaluation / "source-score.json"
|
|
self.path.rename(source)
|
|
self.path.symlink_to(source)
|
|
path = score_path_for_id(self.evaluation, "1")
|
|
self.assertEqual(path, score_path_for_id(self.evaluation, "01"))
|
|
self.assertEqual(set_score(path, "Ex 1", "3,5"), "2")
|
|
self.assertTrue(path.is_symlink())
|
|
self.assertEqual(read_json(source), {"Ex 1": "3.5", "Ex 2": "4"})
|
|
self.assertEqual(read_json(path), read_json(source))
|
|
|
|
def test_invalid_score_or_missing_label_does_not_modify_file(self):
|
|
before = self.path.read_bytes()
|
|
for value in ("", "abc", "NaN", "Infinity", "-1"):
|
|
with self.subTest(value=value), self.assertRaises(ValueError):
|
|
set_score(self.path, "Ex 1", value)
|
|
self.assertEqual(self.path.read_bytes(), before)
|
|
with self.assertRaises(ValueError):
|
|
set_score(self.path, "Missing", "3")
|
|
self.assertEqual(self.path.read_bytes(), before)
|
|
|
|
def test_missing_and_ambiguous_ids_are_rejected(self):
|
|
with self.assertRaises(FileNotFoundError):
|
|
score_path_for_id(self.evaluation, "2")
|
|
with self.assertRaises(ValueError):
|
|
score_path_for_id(self.evaluation, "../1")
|
|
(self.path.parent.parent / "Other (1)").mkdir()
|
|
with self.assertRaises(ValueError):
|
|
score_path_for_id(self.evaluation, "1")
|
|
|
|
|
|
@unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display")
|
|
class ScoreEditorGuiTests(ScoreEditorTests):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.app = CopienatorApp(Path.cwd(), True, self.evaluation)
|
|
self.addCleanup(self.close_app)
|
|
self.app.update()
|
|
self.app.tree.selection_set("edit_scores")
|
|
self.app.update()
|
|
self.panel = self.app.score_editor_panel
|
|
|
|
def close_app(self):
|
|
for callback in self.app.tk.splitlist(self.app.tk.call("after", "info")):
|
|
self.app.after_cancel(callback)
|
|
self.app.destroy()
|
|
|
|
def test_selection_loads_score_and_set_keeps_editor_open(self):
|
|
self.assertEqual(str(self.panel.set_button.cget("state")), "disabled")
|
|
self.panel.id_var.set("1")
|
|
self.assertEqual(tuple(self.panel.label_entry.cget("values")), ("Ex 1", "Ex 2"))
|
|
self.panel.label_var.set("Ex 1")
|
|
self.assertEqual(self.panel.score_var.get(), "2")
|
|
self.app.state_store.update_step("final_score", status="success")
|
|
self.panel.score_var.set("3,5")
|
|
self.panel.set_button.invoke()
|
|
self.app.update()
|
|
self.assertIs(self.app.score_editor_panel, self.panel)
|
|
self.assertEqual(self.panel.id_var.get(), "1")
|
|
self.assertEqual(read_json(self.path), {"Ex 1": "3.5", "Ex 2": "4"})
|
|
self.assertEqual(self.app.state_store.step("final_score")["status"], "stale")
|
|
self.panel.label_var.set("Ex 2")
|
|
self.assertEqual(self.panel.score_var.get(), "4")
|
|
self.panel.id_var.set("2")
|
|
self.assertEqual(self.panel.score_var.get(), "")
|
|
self.assertEqual(str(self.panel.set_button.cget("state")), "disabled")
|
|
|
|
def test_invalid_score_shows_error_and_running_disables_edits(self):
|
|
self.panel.id_var.set("01")
|
|
self.panel.label_var.set("Ex 1")
|
|
self.panel.score_var.set("bad")
|
|
with patch("copienator_gui.scores.messagebox.showerror") as error:
|
|
self.panel.set_button.invoke()
|
|
error.assert_called_once()
|
|
self.assertEqual(read_json(self.path)["Ex 1"], "2")
|
|
self.panel.set_busy(True)
|
|
self.assertEqual(str(self.panel.set_button.cget("state")), "disabled")
|
|
|
|
def test_skipping_without_changes_preserves_downstream_status(self):
|
|
self.app.state_store.update_step("final_score", status="success")
|
|
self.app._skip_step()
|
|
self.assertEqual(self.app.state_store.step("edit_scores")["status"], "skipped")
|
|
self.assertEqual(self.app.state_store.step("final_score")["status"], "success")
|
|
|
|
def test_remark_saves_independently_reloads_and_clears(self):
|
|
self.panel.id_var.set("1")
|
|
self.panel.label_var.set("Ex 1")
|
|
self.panel.score_var.set("bad unsaved score")
|
|
self.panel.remark_entry.insert("1.0", "Justifier.\nDétailler le calcul.")
|
|
self.panel.remark_button.invoke()
|
|
self.assertEqual(read_json(self.path)["Ex 1"], "2")
|
|
self.assertEqual(remarks_for_copy(self.evaluation, "01")["Ex 1"], "Justifier.\nDétailler le calcul.")
|
|
self.assertEqual(self.app.state_store.step("read_annotations")["status"], "stale")
|
|
self.panel.label_var.set("Ex 2")
|
|
self.assertEqual(self.panel.remark_entry.get("1.0", "end-1c"), "")
|
|
self.panel.label_var.set("Ex 1")
|
|
self.assertIn("Justifier.", self.panel.remark_entry.get("1.0", "end-1c"))
|
|
self.panel.remark_entry.delete("1.0", "end")
|
|
self.panel.remark_button.invoke()
|
|
self.assertEqual(remarks_for_copy(self.evaluation, "1"), {})
|
|
self.panel.set_busy(True)
|
|
self.assertEqual(str(self.panel.remark_button.cget("state")), "disabled")
|
|
|
|
def test_general_remarks_edits_invalidate_generation_and_preserve_options(self):
|
|
from copienator.general_remarks import remarks_for_copy as general_for_copy
|
|
self.app.tree.selection_set("general_remarks")
|
|
self.app.update()
|
|
panel = self.app.general_remarks_panel
|
|
panel.id_var.set("01")
|
|
panel.editor.insert("1.0", "First")
|
|
self.app.state_store.update_step("read_annotations", status="success",
|
|
values={"update_score": False, "return_answers_solution": False})
|
|
panel.save()
|
|
entry = self.app.state_store.step("read_annotations")
|
|
self.assertEqual(entry["status"], "stale")
|
|
self.assertTrue(entry["values"]["update_score"])
|
|
self.assertFalse(entry["values"]["return_answers_solution"])
|
|
panel.new()
|
|
panel.editor.insert("1.0", "Second")
|
|
panel.save()
|
|
panel.move(-1)
|
|
self.assertEqual(general_for_copy(self.evaluation, "1"), ["Second", "First"])
|
|
panel.delete()
|
|
panel.listbox.selection_set(0)
|
|
panel.delete()
|
|
self.assertEqual(general_for_copy(self.evaluation, "1"), [])
|
|
panel.id_var.set("2")
|
|
self.assertEqual(str(panel.editor.cget("state")), "disabled")
|
|
|
|
def test_completing_editor_selects_regeneration_and_preserves_options(self):
|
|
self.app.state_store.update_step("read_annotations", status="success", variant="grouped",
|
|
values={"update_score": False, "return_answers_solution": False, "annotation_dir": "BGnot"})
|
|
with patch.object(self.app.runner, "start") as start:
|
|
self.app.run_button.invoke()
|
|
self.app.update()
|
|
start.assert_not_called()
|
|
self.assertEqual(self.app.state_store.step("edit_scores")["status"], "success")
|
|
self.assertEqual(self.app.current_step.id, "general_remarks")
|
|
self.app.run_button.invoke()
|
|
self.app.update()
|
|
self.assertEqual(self.app.current_step.id, "read_annotations")
|
|
self.assertEqual(self.app.tree.selection(), ("read_annotations",))
|
|
self.assertEqual(self.app.tree.item("read_annotations", "values"), ("À revalider",))
|
|
self.assertTrue(self.app.arg_vars["update_score"].get())
|
|
self.assertEqual(self.app.variant_var.get(), "grouped")
|
|
self.assertFalse(self.app.arg_vars["return_answers_solution"].get())
|
|
self.assertTrue(self.app.state_store.step("read_annotations")["values"]["update_score"])
|
|
self.assertIn("--update-score", self.app._make_command())
|