Manual conflict : utilities
This commit is contained in:
1 parent
f43b713590
commit
c155e3eebc
3 files changed
+100
-3
No files matched your search
@@ -11,6 +11,10 @@ def _path(root):
|
|||||||
|
|
||||||
def origins_for(root, copy_id, label, pdf):
|
def origins_for(root, copy_id, label, pdf):
|
||||||
entry = read_json(_path(root), default={}).get(str(copy_id), {}).get(label, {})
|
entry = read_json(_path(root), default={}).get(str(copy_id), {}).get(label, {})
|
||||||
|
return _valid_origins(entry, pdf)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_origins(entry, pdf):
|
||||||
if not entry or not Path(pdf).is_file():
|
if not entry or not Path(pdf).is_file():
|
||||||
return []
|
return []
|
||||||
if entry.get("sha256") != sha256(Path(pdf).read_bytes()).hexdigest():
|
if entry.get("sha256") != sha256(Path(pdf).read_bytes()).hexdigest():
|
||||||
@@ -18,6 +22,22 @@ def origins_for(root, copy_id, label, pdf):
|
|||||||
return entry.get("labels", [])
|
return entry.get("labels", [])
|
||||||
|
|
||||||
|
|
||||||
|
def populated_labels(root, copy_id, source_label):
|
||||||
|
"""Find current answer PDFs populated from this source's recorded content."""
|
||||||
|
entries = read_json(_path(root), default={}).get(str(copy_id), {})
|
||||||
|
copies = Path(root) / "Copies" / f"Copie{copy_id}"
|
||||||
|
populated = []
|
||||||
|
for label, entry in entries.items():
|
||||||
|
if label == source_label or source_label not in entry.get("labels", []):
|
||||||
|
continue
|
||||||
|
pdf = copies / f"{label}.pdf"
|
||||||
|
if not pdf.is_file():
|
||||||
|
pdf = copies / f"{label}_new.pdf"
|
||||||
|
if source_label in _valid_origins(entry, pdf):
|
||||||
|
populated.append(label)
|
||||||
|
return sorted(populated)
|
||||||
|
|
||||||
|
|
||||||
def save_origins(root, copy_id, label, pdf, origins):
|
def save_origins(root, copy_id, label, pdf, origins):
|
||||||
entry = {"labels": list(dict.fromkeys(origins)),
|
entry = {"labels": list(dict.fromkeys(origins)),
|
||||||
"sha256": sha256(Path(pdf).read_bytes()).hexdigest()}
|
"sha256": sha256(Path(pdf).read_bytes()).hexdigest()}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from tkinter import messagebox, ttk
|
from tkinter import messagebox, ttk
|
||||||
|
|
||||||
from copienator import CliError
|
from copienator import CliError
|
||||||
|
from copienator.answer_origins import populated_labels
|
||||||
from copienator.commands.resolve_manual import get_actual_pdf, parse_instruction_text
|
from copienator.commands.resolve_manual import get_actual_pdf, parse_instruction_text
|
||||||
from copienator.platform import open_path
|
from copienator.platform import open_path
|
||||||
from .cut_helper import CutHelper
|
from .cut_helper import CutHelper
|
||||||
@@ -41,6 +42,7 @@ class ManualResolutionPanel(ttk.Frame):
|
|||||||
self.get_evaluation = get_evaluation
|
self.get_evaluation = get_evaluation
|
||||||
self.pdf_buttons = []
|
self.pdf_buttons = []
|
||||||
self.cut_buttons = []
|
self.cut_buttons = []
|
||||||
|
self.copy_buttons = []
|
||||||
actions = ttk.Frame(self)
|
actions = ttk.Frame(self)
|
||||||
actions.pack(fill="x")
|
actions.pack(fill="x")
|
||||||
self.editor_button = ttk.Button(
|
self.editor_button = ttk.Button(
|
||||||
@@ -92,11 +94,25 @@ class ManualResolutionPanel(ttk.Frame):
|
|||||||
except (OSError, RuntimeError) as exc:
|
except (OSError, RuntimeError) as exc:
|
||||||
messagebox.showerror("Ouverture du PDF", str(exc))
|
messagebox.showerror("Ouverture du PDF", str(exc))
|
||||||
|
|
||||||
|
def open_copy(self, evaluation, copy_id, *, directory=False):
|
||||||
|
path = evaluation / "Copies" / f"Copie{copy_id}"
|
||||||
|
if not directory:
|
||||||
|
path = path.with_suffix(".pdf")
|
||||||
|
try:
|
||||||
|
if directory and not path.is_dir():
|
||||||
|
raise FileNotFoundError(f"Dossier introuvable : {path}")
|
||||||
|
if not directory and not path.is_file():
|
||||||
|
raise FileNotFoundError(f"PDF introuvable : {path}")
|
||||||
|
open_path(path)
|
||||||
|
except (OSError, RuntimeError) as exc:
|
||||||
|
messagebox.showerror("Ouverture du dossier" if directory else "Ouverture du PDF", str(exc))
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
for button in self.pdf_buttons + self.cut_buttons:
|
for button in self.pdf_buttons + self.cut_buttons + self.copy_buttons:
|
||||||
button.destroy()
|
button.destroy()
|
||||||
self.pdf_buttons.clear()
|
self.pdf_buttons.clear()
|
||||||
self.cut_buttons.clear()
|
self.cut_buttons.clear()
|
||||||
|
self.copy_buttons.clear()
|
||||||
self.cut_result.set("")
|
self.cut_result.set("")
|
||||||
self.text.configure(state="normal")
|
self.text.configure(state="normal")
|
||||||
self.text.delete("1.0", "end")
|
self.text.delete("1.0", "end")
|
||||||
@@ -111,7 +127,10 @@ class ManualResolutionPanel(ttk.Frame):
|
|||||||
else:
|
else:
|
||||||
self.editor_button.configure(state="normal")
|
self.editor_button.configure(state="normal")
|
||||||
malformed = []
|
malformed = []
|
||||||
|
provenance = {}
|
||||||
|
provenance_error = ""
|
||||||
for number, raw in enumerate(content.splitlines(keepends=True), 1):
|
for number, raw in enumerate(content.splitlines(keepends=True), 1):
|
||||||
|
populated = []
|
||||||
self.text.insert("end", raw.rstrip("\r\n"))
|
self.text.insert("end", raw.rstrip("\r\n"))
|
||||||
try:
|
try:
|
||||||
instructions = parse_instruction_text(raw)
|
instructions = parse_instruction_text(raw)
|
||||||
@@ -120,6 +139,21 @@ class ManualResolutionPanel(ttk.Frame):
|
|||||||
else:
|
else:
|
||||||
if instructions:
|
if instructions:
|
||||||
instruction = instructions[0]
|
instruction = instructions[0]
|
||||||
|
key = (instruction.copy_id, instruction.old_label)
|
||||||
|
if key not in provenance:
|
||||||
|
try:
|
||||||
|
provenance[key] = populated_labels(path.parent, *key)
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
provenance[key] = []
|
||||||
|
provenance_error = f" — Provenance indisponible : {exc}"
|
||||||
|
populated = provenance[key]
|
||||||
|
for title, directory in (("pdf", False), ("dir", True)):
|
||||||
|
button = ttk.Button(
|
||||||
|
self.text, text=title, width=3,
|
||||||
|
command=lambda directory=directory, copy_id=instruction.copy_id, root=path.parent: self.open_copy(root, copy_id, directory=directory),
|
||||||
|
)
|
||||||
|
self.copy_buttons.append(button)
|
||||||
|
self.text.window_create("end", window=button, padx=2)
|
||||||
for title, label in (("Source", instruction.old_label),
|
for title, label in (("Source", instruction.old_label),
|
||||||
("Cible", instruction.new_label)):
|
("Cible", instruction.new_label)):
|
||||||
button = ttk.Button(
|
button = ttk.Button(
|
||||||
@@ -132,12 +166,14 @@ class ManualResolutionPanel(ttk.Frame):
|
|||||||
command=lambda item=instruction, root=path.parent: self.open_cut(root, item))
|
command=lambda item=instruction, root=path.parent: self.open_cut(root, item))
|
||||||
self.cut_buttons.append(button)
|
self.cut_buttons.append(button)
|
||||||
self.text.window_create("end", window=button, padx=8)
|
self.text.window_create("end", window=button, padx=8)
|
||||||
|
if populated:
|
||||||
|
self.text.insert("end", "\n# -> " + ", ".join(populated))
|
||||||
if raw.endswith("\n"):
|
if raw.endswith("\n"):
|
||||||
self.text.insert("end", "\n")
|
self.text.insert("end", "\n")
|
||||||
detail = (" — lignes invalides : " + ", ".join(malformed)) if malformed else (
|
detail = (" — lignes invalides : " + ", ".join(malformed)) if malformed else (
|
||||||
f" — {len(self.pdf_buttons) // 2} instruction(s)"
|
f" — {len(self.pdf_buttons) // 2} instruction(s)"
|
||||||
)
|
)
|
||||||
self.status.configure(text=str(path) + detail)
|
self.status.configure(text=str(path) + detail + provenance_error)
|
||||||
finally:
|
finally:
|
||||||
self.text.configure(state="disabled")
|
self.text.configure(state="disabled")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
|
from copienator.answer_origins import save_origins
|
||||||
from copienator_gui.manual_resolution import ManualResolutionPanel
|
from copienator_gui.manual_resolution import ManualResolutionPanel
|
||||||
|
|
||||||
|
|
||||||
@@ -34,17 +35,27 @@ class ManualResolutionPanelTests(unittest.TestCase):
|
|||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.touch()
|
path.touch()
|
||||||
paths.append(path)
|
paths.append(path)
|
||||||
|
copy_paths = []
|
||||||
|
for copy in ("01", "02"):
|
||||||
|
pdf = self.evaluation / "Copies" / f"Copie{copy}.pdf"
|
||||||
|
pdf.touch()
|
||||||
|
copy_paths.extend((pdf, pdf.with_suffix("")))
|
||||||
panel = self.panel()
|
panel = self.panel()
|
||||||
self.assertEqual(panel.text.get("1.0", "end-1c"), content)
|
self.assertEqual(panel.text.get("1.0", "end-1c"), content)
|
||||||
self.assertEqual(len(panel.pdf_buttons), 4)
|
self.assertEqual(len(panel.pdf_buttons), 4)
|
||||||
self.assertEqual([button.cget("text") for button in panel.pdf_buttons],
|
self.assertEqual([button.cget("text") for button in panel.pdf_buttons],
|
||||||
["Source : Ex 1", "Cible : Ex 2",
|
["Source : Ex 1", "Cible : Ex 2",
|
||||||
"Source : Ex 3", "Cible : Ex 4"])
|
"Source : Ex 3", "Cible : Ex 4"])
|
||||||
|
self.assertEqual([button.cget("text") for button in panel.copy_buttons],
|
||||||
|
["pdf", "dir", "pdf", "dir"])
|
||||||
with patch("copienator_gui.manual_resolution.open_path") as opened:
|
with patch("copienator_gui.manual_resolution.open_path") as opened:
|
||||||
panel.editor_button.invoke()
|
panel.editor_button.invoke()
|
||||||
for button in panel.pdf_buttons:
|
for button in panel.pdf_buttons:
|
||||||
button.invoke()
|
button.invoke()
|
||||||
self.assertEqual(opened.call_args_list, [call(self.path), *map(call, paths)])
|
for button in panel.copy_buttons:
|
||||||
|
button.invoke()
|
||||||
|
self.assertEqual(opened.call_args_list,
|
||||||
|
[call(self.path), *map(call, paths), *map(call, copy_paths)])
|
||||||
|
|
||||||
def test_reload_updates_the_displayed_target_and_its_pdf_action(self):
|
def test_reload_updates_the_displayed_target_and_its_pdf_action(self):
|
||||||
self.path.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
self.path.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
||||||
@@ -63,6 +74,34 @@ class ManualResolutionPanelTests(unittest.TestCase):
|
|||||||
panel.pdf_buttons[1].invoke()
|
panel.pdf_buttons[1].invoke()
|
||||||
opened.assert_called_once_with(copies / "C.pdf")
|
opened.assert_called_once_with(copies / "C.pdf")
|
||||||
|
|
||||||
|
def test_populated_labels_appear_under_each_relevant_conflict(self):
|
||||||
|
content = "Copie01 A -> B|\nCopie02 A -> B|\nCopie01 A -> C|"
|
||||||
|
self.path.write_text(content, encoding="utf-8")
|
||||||
|
copies = self.evaluation / "Copies" / "Copie01"
|
||||||
|
copies.mkdir(parents=True)
|
||||||
|
for label, filename in (("label2", "label2_new.pdf"),
|
||||||
|
("label1", "label1.pdf"),
|
||||||
|
("stale", "stale.pdf"),
|
||||||
|
("archived", "archived_old.pdf")):
|
||||||
|
pdf = copies / filename
|
||||||
|
pdf.write_bytes(b"answer")
|
||||||
|
save_origins(self.evaluation, "01", label, pdf, ["A"])
|
||||||
|
(copies / "stale.pdf").write_bytes(b"changed answer")
|
||||||
|
panel = self.panel()
|
||||||
|
expected = (
|
||||||
|
"Copie01 A -> B|\n# -> label1, label2\n"
|
||||||
|
"Copie02 A -> B|\n"
|
||||||
|
"Copie01 A -> C|\n# -> label1, label2"
|
||||||
|
)
|
||||||
|
self.assertEqual(panel.text.get("1.0", "end-1c"), expected)
|
||||||
|
self.assertEqual(self.path.read_text(encoding="utf-8"), content)
|
||||||
|
self.assertEqual(len(panel.pdf_buttons), 6)
|
||||||
|
self.assertIn("3 instruction(s)", panel.status.cget("text"))
|
||||||
|
(copies / "label1.pdf").unlink()
|
||||||
|
panel.reload()
|
||||||
|
self.assertEqual(panel.text.get("1.0", "end-1c"),
|
||||||
|
expected.replace("label1, label2", "label2"))
|
||||||
|
|
||||||
def test_reload_keeps_invalid_lines_visible_and_removes_stale_actions(self):
|
def test_reload_keeps_invalid_lines_visible_and_removes_stale_actions(self):
|
||||||
self.path.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
self.path.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
||||||
panel = self.panel()
|
panel = self.panel()
|
||||||
@@ -71,9 +110,11 @@ class ManualResolutionPanelTests(unittest.TestCase):
|
|||||||
self.assertIn("bad instruction", panel.text.get("1.0", "end"))
|
self.assertIn("bad instruction", panel.text.get("1.0", "end"))
|
||||||
self.assertIn("lignes invalides : 1", panel.status.cget("text"))
|
self.assertIn("lignes invalides : 1", panel.status.cget("text"))
|
||||||
self.assertEqual(len(panel.pdf_buttons), 2)
|
self.assertEqual(len(panel.pdf_buttons), 2)
|
||||||
|
self.assertEqual(len(panel.copy_buttons), 2)
|
||||||
self.path.unlink()
|
self.path.unlink()
|
||||||
panel.reload()
|
panel.reload()
|
||||||
self.assertFalse(panel.pdf_buttons)
|
self.assertFalse(panel.pdf_buttons)
|
||||||
|
self.assertFalse(panel.copy_buttons)
|
||||||
self.assertEqual(str(panel.editor_button.cget("state")), "disabled")
|
self.assertEqual(str(panel.editor_button.cget("state")), "disabled")
|
||||||
|
|
||||||
def test_missing_source_does_not_prevent_opening_destination(self):
|
def test_missing_source_does_not_prevent_opening_destination(self):
|
||||||
|
|||||||
Reference in new issue
Block a user