Add per label remark, post correction
This commit is contained in:
1 parent
3abd7873e6
commit
8918d64452
13 files changed
+343
-23
No files matched your search
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from copienator import atomic_write_json, configuration, read_json
|
||||
from copienator.annotation_documents import copy_documents
|
||||
from copienator.return_answers import load_return_answer_options
|
||||
from copienator.custom_remarks import load_remarks, copy_key
|
||||
|
||||
STATE_FILE = ".annotation-read-state.json"
|
||||
DOCUMENT_INPUTS = ("bnote.json", "checkboxes.json", "Reference.jpg", "Concat_annotated.pdf")
|
||||
@@ -36,7 +37,9 @@ class AnnotationReadCache:
|
||||
shared.extend(directory / name for name in DOCUMENT_INPUTS)
|
||||
common = (_files_digest(shared), load_return_answer_options(root),
|
||||
configuration.RETURN_ANSWERS_ENABLED,
|
||||
configuration.RETURN_JPEG_ENABLED, configuration.RETURN_PDF_ENABLED,
|
||||
configuration.LATEX_ANOT_BEFORE, configuration.LATEX_ANOT_AFTER)
|
||||
remarks = load_remarks(root)
|
||||
for copy_id, labels in data.items():
|
||||
directory = root / mode / f"Copie{copy_id}"
|
||||
paths = [directory / "score.json", directory / "bnote.json"]
|
||||
@@ -45,7 +48,7 @@ class AnnotationReadCache:
|
||||
for content in labels.values():
|
||||
pdf = Path(content["pdf_path"])
|
||||
paths.extend((pdf, pdf.with_suffix(".layout.json")))
|
||||
payload = json.dumps((1, common, labels, _files_digest(paths)),
|
||||
payload = json.dumps((2, common, labels, remarks.get(copy_key(copy_id), {}), _files_digest(paths)),
|
||||
sort_keys=True, default=str).encode()
|
||||
self.signatures[copy_id] = sha256(payload).hexdigest()
|
||||
|
||||
@@ -71,6 +74,9 @@ class AnnotationReadCache:
|
||||
outputs.extend(directory.glob("annotation_baseline.json"))
|
||||
for destination in self.workspace.return_dir.glob(f"* ({copy_id})"):
|
||||
outputs.extend((destination / "answers").glob("*.jpg"))
|
||||
stem = destination.name.rsplit(" (", 1)[0]
|
||||
outputs.extend(path for suffix in (".jpg", ".pdf")
|
||||
if (path := destination / (stem + suffix)).is_file())
|
||||
atomic_write_json(directory / STATE_FILE, {
|
||||
"signature": self.signatures[copy_id],
|
||||
"outputs": [str(path) for path in outputs],
|
||||
|
||||
@@ -79,8 +79,9 @@ def update_named_return_answers(
|
||||
annotation_dir_name: str,
|
||||
*,
|
||||
copy_ids: Collection[str] | None = None,
|
||||
refresh_named: bool = False,
|
||||
) -> ExitCode:
|
||||
"""Refresh answers/ in existing returns, optionally only successful rereads."""
|
||||
"""Refresh answers/; readers also refresh named outputs after successful rereads."""
|
||||
if copy_ids is not None and (not copy_ids or not workspace.return_dir.is_dir()):
|
||||
return ExitCode.SUCCESS
|
||||
workspace.require_directories(annotation_dir_name, "A Rendre")
|
||||
@@ -118,6 +119,18 @@ def update_named_return_answers(
|
||||
destination,
|
||||
answers_only=True,
|
||||
)
|
||||
if refresh_named:
|
||||
stem = destination.name[:match.start()].rstrip()
|
||||
for source_name, suffix, enabled in (
|
||||
("Concat.jpg", ".jpg", configuration.RETURN_JPEG_ENABLED),
|
||||
("Concat_F.pdf", ".pdf", configuration.RETURN_PDF_ENABLED),
|
||||
):
|
||||
source = source_folder / source_name
|
||||
target = destination / (stem + suffix)
|
||||
if enabled and source.is_file():
|
||||
replace_with_link_or_copy(source, target, prefer="symlink")
|
||||
else:
|
||||
target.unlink(missing_ok=True)
|
||||
print(f"Updated answers for {destination.name} from Copie{copy_id}")
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
print(f"Error updating answers for {destination.name}: {exc}", file=sys.stderr)
|
||||
|
||||
@@ -25,6 +25,7 @@ from copienator.annotation_data import AnnotationData, load_annotation_data
|
||||
from copienator.annotation_documents import copy_documents
|
||||
from copienator.annotation_layout import legacy_layout
|
||||
from copienator.annotation_read_cache import AnnotationReadCache
|
||||
from copienator.custom_remarks import apply_custom_remarks
|
||||
from copienator.annotation_modifications import (
|
||||
BASELINE,
|
||||
output_removals,
|
||||
@@ -175,6 +176,7 @@ def apply_actions_and_regenerate(
|
||||
|
||||
labels_data = data[student_id]
|
||||
apply_checkbox_actions(labels_data, actions, print)
|
||||
apply_custom_remarks(workspace.root, student_id, labels_data)
|
||||
score_path = output_dir / "score.json"
|
||||
preserve_score_file = update_score and score_path.is_file()
|
||||
if update_score:
|
||||
@@ -381,7 +383,7 @@ def run(
|
||||
status = ExitCode.PARTIAL
|
||||
else:
|
||||
regenerated.add(student_id)
|
||||
if update_named_return_answers(workspace, "Bnot", copy_ids=regenerated) != ExitCode.SUCCESS:
|
||||
if update_named_return_answers(workspace, "Bnot", copy_ids=regenerated, refresh_named=True) != ExitCode.SUCCESS:
|
||||
status = ExitCode.PARTIAL
|
||||
elif cache is not None:
|
||||
cache.remember(regenerated)
|
||||
|
||||
@@ -28,6 +28,7 @@ from copienator.annotation_data import AnnotationData, RefaireList, load_annotat
|
||||
from copienator.annotation_documents import copy_documents, tablet_documents
|
||||
from copienator.annotation_layout import legacy_layout
|
||||
from copienator.annotation_read_cache import AnnotationReadCache
|
||||
from copienator.custom_remarks import apply_custom_remarks
|
||||
from copienator.annotation_modifications import (
|
||||
BASELINE,
|
||||
baseline,
|
||||
@@ -235,6 +236,7 @@ def apply_actions_and_regenerate_grouped(
|
||||
output_dir = workspace.root / annotation_dir / f"Copie{student_id}"
|
||||
labels_data = data.get(student_id, {})
|
||||
apply_checkbox_actions(labels_data, actions, logs.append)
|
||||
apply_custom_remarks(workspace.root, student_id, labels_data)
|
||||
score_path = output_dir / "score.json"
|
||||
preserve_score_file = update_score and score_path.is_file()
|
||||
if update_score:
|
||||
@@ -700,7 +702,7 @@ def run(
|
||||
status = ExitCode.PARTIAL
|
||||
else:
|
||||
regenerated.add(futures[future])
|
||||
if update_named_return_answers(workspace, annotation_dir, copy_ids=regenerated) != ExitCode.SUCCESS:
|
||||
if update_named_return_answers(workspace, annotation_dir, copy_ids=regenerated, refresh_named=True) != ExitCode.SUCCESS:
|
||||
status = ExitCode.PARTIAL
|
||||
elif cache is not None:
|
||||
cache.remember(regenerated)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Teacher-authored feedback, independent of the generated correction data."""
|
||||
from pathlib import Path
|
||||
|
||||
from copienator import atomic_update_json, read_json
|
||||
|
||||
FILENAME = "custom_remarks.json"
|
||||
|
||||
|
||||
def copy_key(copy_id: str) -> str:
|
||||
if not copy_id.strip().isascii() or not copy_id.strip().isdecimal():
|
||||
raise ValueError("Numéro de copie invalide.")
|
||||
return str(int(copy_id))
|
||||
|
||||
|
||||
def load_remarks(root: Path) -> dict[str, dict[str, str]]:
|
||||
data = read_json(root / FILENAME, default={})
|
||||
validate(data)
|
||||
return data
|
||||
|
||||
|
||||
def validate(data):
|
||||
if not isinstance(data, dict) or any(
|
||||
not isinstance(key, str) or copy_key(key) != key
|
||||
or not isinstance(labels, dict)
|
||||
or any(not isinstance(label, str) or not isinstance(text, str)
|
||||
for label, text in labels.items())
|
||||
for key, labels in data.items()
|
||||
):
|
||||
raise ValueError(f"Format invalide dans {FILENAME}.")
|
||||
|
||||
|
||||
def remarks_for_copy(root: Path, copy_id: str) -> dict[str, str]:
|
||||
return load_remarks(root).get(copy_key(copy_id), {})
|
||||
|
||||
|
||||
def set_remark(root: Path, copy_id: str, label: str, text: str) -> str:
|
||||
key = copy_key(copy_id)
|
||||
text = text.strip()
|
||||
previous = ""
|
||||
|
||||
def update(data):
|
||||
nonlocal previous
|
||||
validate(data)
|
||||
labels = data.setdefault(key, {})
|
||||
previous = labels.get(label, "")
|
||||
if text:
|
||||
labels[label] = text
|
||||
else:
|
||||
labels.pop(label, None)
|
||||
if not labels:
|
||||
data.pop(key, None)
|
||||
|
||||
atomic_update_json(root / FILENAME, update, default_factory=dict)
|
||||
return previous
|
||||
|
||||
|
||||
def apply_custom_remarks(root: Path, copy_id: str, labels_data: dict) -> None:
|
||||
"""Append after checkbox actions; replace our own entries on repeated calls."""
|
||||
remarks = remarks_for_copy(root, copy_id)
|
||||
for label, content in labels_data.items():
|
||||
result = content["result"]
|
||||
feedback = [item for item in result.get("feedback", [])
|
||||
if not item.get("custom_remark")]
|
||||
if remarks.get(label):
|
||||
feedback.append({"text": remarks[label], "box_2d": None, "custom_remark": True})
|
||||
result["feedback"] = feedback
|
||||
Reference in new issue
Block a user