126 lines
5.0 KiB
Python
126 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
from copienator import atomic_write_json, configuration, read_json, utils
|
|
from copienator.filesystem import staged_directory
|
|
from copienator.platform import safe_filename
|
|
|
|
RETURN_ANSWER_OPTIONS_FILE = Path(".copienator") / "return_answers.json"
|
|
|
|
|
|
def configured_return_answer_options() -> dict[str, bool]:
|
|
return {
|
|
"context": bool(configuration.RETURN_ANSWERS_CONTEXT),
|
|
"question": bool(configuration.RETURN_ANSWERS_QUESTION),
|
|
"solution": bool(configuration.RETURN_ANSWERS_SOLUTION),
|
|
}
|
|
|
|
|
|
def save_return_answer_options(
|
|
root: Path,
|
|
*,
|
|
context: bool,
|
|
question: bool,
|
|
solution: bool,
|
|
) -> None:
|
|
path = Path(root) / RETURN_ANSWER_OPTIONS_FILE
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
atomic_write_json(
|
|
path,
|
|
{"context": context, "question": question, "solution": solution},
|
|
)
|
|
|
|
|
|
def load_return_answer_options(root: Path) -> dict[str, bool]:
|
|
options = configured_return_answer_options()
|
|
path = Path(root) / RETURN_ANSWER_OPTIONS_FILE
|
|
if not path.is_file():
|
|
return options
|
|
loaded = read_json(path)
|
|
if not isinstance(loaded, dict):
|
|
raise ValueError(f"Expected a return-answer options object in {path}")
|
|
for name in options:
|
|
if name in loaded:
|
|
if type(loaded[name]) is not bool:
|
|
raise ValueError(f"Expected a boolean for {name!r} in {path}")
|
|
options[name] = loaded[name]
|
|
return options
|
|
|
|
|
|
def publish_answer_returns(
|
|
root: Path,
|
|
source: Path,
|
|
destination: Path,
|
|
*,
|
|
answers_only: bool = False,
|
|
) -> None:
|
|
"""Publish reviewed answers, optionally without touching return metadata."""
|
|
scores = read_json(source / "score.json")
|
|
if not isinstance(scores, dict):
|
|
raise ValueError(f"Expected a score object in {source}")
|
|
info_path = source / "info.json"
|
|
if not info_path.is_file():
|
|
raise ValueError(f"Missing {info_path}; recompile annotations before giving-names")
|
|
info = read_json(info_path)
|
|
if not isinstance(info, dict) or set(info) != set(scores):
|
|
raise ValueError(f"Invalid question information in {info_path}; recompile annotations")
|
|
for label, entry in info.items():
|
|
if (
|
|
not isinstance(entry, dict)
|
|
or set(entry) != {"present", "not_empty", "touched", "score"}
|
|
or any(type(entry[key]) is not bool for key in ("present", "not_empty", "touched"))
|
|
or (entry["not_empty"] and not entry["present"])
|
|
or (entry["touched"] and not entry["not_empty"])
|
|
):
|
|
raise ValueError(f"Invalid question information in {info_path}; recompile annotations")
|
|
# Match score.json, including manual score edits awaiting recompilation.
|
|
entry["score"] = scores[label]
|
|
|
|
answers_dir = destination / "answers"
|
|
if answers_dir.is_symlink():
|
|
raise ValueError(f"Expected a real answer directory: {answers_dir}")
|
|
if configuration.RETURN_ANSWERS_ENABLED:
|
|
options = load_return_answer_options(root)
|
|
labels = [label for label, entry in info.items() if entry["present"] and entry["not_empty"]]
|
|
|
|
# Import the rendering backend only when individual images are requested.
|
|
from copienator.commands.annotating import make_base_image
|
|
from copienator.commands.reading_annotations import concatenate
|
|
|
|
all_labels = utils.read_all_labels(root)
|
|
with staged_directory(answers_dir) as staging:
|
|
for label in sorted(labels, key=utils.natural_key):
|
|
paths = []
|
|
if options["context"]:
|
|
paths.extend(utils.pdf_images_of_contexts(root, label, all_labels))
|
|
if options["question"]:
|
|
paths.append(utils.pdf_image_of_enonce(root, label))
|
|
if options["solution"]:
|
|
paths.append(utils.pdf_image_of_solution(root, label))
|
|
images = []
|
|
for path in paths:
|
|
if path:
|
|
supplement, _, _ = make_base_image(path)
|
|
if supplement is None:
|
|
raise ValueError(f"Could not render {path}")
|
|
images.append(supplement)
|
|
with Image.open(source / f"{label}.jpg") as answer:
|
|
images.append(answer.convert("RGB"))
|
|
image = concatenate(images)
|
|
output = staging / f"{safe_filename(label)}.jpg"
|
|
if output.exists():
|
|
raise ValueError(
|
|
f"Answer labels produce the same filename in {answers_dir}: {label}"
|
|
)
|
|
image.save(output)
|
|
elif answers_dir.exists():
|
|
# Replace the managed directory with an empty one to remove stale exports.
|
|
with staged_directory(answers_dir):
|
|
pass
|
|
if not answers_only:
|
|
atomic_write_json(destination / "info.json", info)
|
|
(destination / "touched.json").unlink(missing_ok=True)
|