Source code for plom.create.push_pull_rubrics

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2021-2024 Colin B. Macdonald

"""Tools for upload/downloading rubrics from Plom servers."""

import json
import sys

if sys.version_info >= (3, 9):
    from importlib import resources
else:
    import importlib_resources as resources

if sys.version_info < (3, 11):
    import tomli as tomllib
else:
    import tomllib
import tomlkit

# try to avoid importing Pandas unless we use specific functions: Issue #2154
# import pandas

from plom.create import with_manager_messenger


[docs] @with_manager_messenger def download_rubrics(*, msgr): """Download a list of rubrics from a server. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. Returns: list: list of dicts, possibly an empty list if server has no rubrics. """ return msgr.MgetRubrics()
[docs] @with_manager_messenger def download_rubrics_to_file(filename, *, msgr, verbose: bool = True) -> None: """Download the rubrics from a server and save them to a file. Args: filename (pathlib.Path): A filename to save to. The extension is used to determine what format, supporting: `.json`, `.toml`, and `.csv`. If no extension is included, default to `.toml`. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. verbose (bool): display diagnostic output on stdout. Returns: None: but saves a file as a side effect. """ if filename.suffix.casefold() not in (".json", ".toml", ".csv"): filename = filename.with_suffix(filename.suffix + ".toml") suffix = filename.suffix if verbose: print(f'Saving server\'s current rubrics to "{filename}"') rubrics = download_rubrics(msgr=msgr) with open(filename, "w") as f: if suffix == ".json": json.dump(rubrics, f, indent=" ") elif suffix == ".toml": tomlkit.dump({"rubric": rubrics}, f) elif suffix == ".csv": import pandas df = pandas.json_normalize(rubrics) df.to_csv(f, index=False, sep=",", encoding="utf-8") else: raise NotImplementedError(f'Don\'t know how to export to "{filename}"')
[docs] def upload_rubrics_from_file(filename, *, msgr, verbose: bool = True) -> None: """Load rubrics from a file and upload them to a server. Args: filename (pathlib.Path): A filename to load from. Types `.json`, `.toml`, and `.csv` are supported. If no suffix is included we'll try to append `.toml`. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. verbose (bool): display diagnostic output on stdout. Returns: None """ if filename.suffix.casefold() not in (".json", ".toml", ".csv"): filename = filename.with_suffix(filename.suffix + ".toml") suffix = filename.suffix if suffix == ".json": with open(filename, "r") as f: rubrics = json.load(f) elif suffix == ".toml": with open(filename, "rb") as f: rubrics = tomllib.load(f)["rubric"] elif suffix == ".csv": with open(filename, "r") as f: import pandas df = pandas.read_csv(f) df.fillna("", inplace=True) # TODO: flycheck is whining about this to_json rubrics = json.loads(df.to_json(orient="records")) else: raise NotImplementedError(f'Don\'t know how to import from "{filename}"') if verbose: print(f'Adding {len(rubrics)} rubrics from file "{filename}"') upload_rubrics(rubrics, msgr=msgr)
[docs] @with_manager_messenger def upload_rubrics(rubrics, *, msgr) -> None: """Upload a list of rubrics to a server.""" for rub in rubrics: # Generally we don't want to upload autogenerated rubrics: this HAL # test is not quite right as fact some autogenerated deltas and # "no marks", "full marks" are made by manager... Issue #1494. if rub.get("username", None) == "HAL": continue # But we get around the above by uploading only relative and neutral # rubrics. No absolute or delta rubrics (as those are currently # autogenerated). The exact logic may need tweaking in the future! if rub["kind"] not in ("neutral", "relative"): continue msgr.McreateRubric(rub)
[docs] @with_manager_messenger def upload_demo_rubrics(*, msgr, numquestions: int = 3) -> int: """Load some demo rubrics and upload to server. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. numquestions (int): how many questions should we build for. TODO: get number of questions from the server spec if omitted. Returns: How many rubrics were created. The demo data is a bit sparse: we fill in missing pieces and multiply over questions. """ with open(resources.files("plom") / "demo_rubrics.toml", "rb") as f: _rubrics_in = tomllib.load(f) rubrics_in = _rubrics_in["rubric"] rubrics = [] for rub in rubrics_in: if not rub.get("kind"): if rub["delta"] == ".": rub["kind"] = "neutral" rub["value"] = 0 rub["out_of"] = 0 elif rub["delta"].startswith("+") or rub["delta"].startswith("-"): rub["kind"] = "relative" rub["value"] = int(rub["delta"]) rub["out_of"] = 0 # unused for relative else: raise ValueError(f'not sure how to map "kind" for rubric:\n {rub}') rub["display_delta"] = rub["delta"] rub.pop("delta") # Multiply rubrics w/o question numbers, avoids repetition in demo file if rub.get("question") is None: for q in range(1, numquestions + 1): r = rub.copy() r["question"] = q rubrics.append(r) else: rubrics.append(rub) upload_rubrics(rubrics, msgr=msgr) return len(rubrics)