Source code for plom.finish.spreadsheet

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2019-2022 Colin B. Macdonald
# Copyright (C) 2020-2022 Andrew Rechnitzer
# Copyright (C) 2020 Dryden Wiebe

import arrow
import csv

from plom import get_question_label
from plom.finish import with_finish_messenger
from plom.finish import CSVFilename
from plom.misc_utils import utc_now_to_string, arrowtime_to_string


def write_spreadsheet(spreadSheetDict, labels, filename):
    """Writes all of the current marks to a local csv file.

    Arguments:
        spreadSheetDict (dict): Dictionary containing the tests to be
            written to a spreadsheet.
        labels (list): string labels for each question.
        filename (pathlib.Path/str): where to save the csv.

    Returns:
        tuple: Two booleans, the first is False if each test in
        spreadSheetDict is marked, True otherwise . The second
        is False if there is a test with no ID, True otherwise.
    """
    head = ["StudentID", "StudentName", "TestNumber"]
    # Note: csv library seems smart enough to escape the labels (comma, quotes, etc)
    for label in labels:
        head.append(f"{label} mark")
    head.append("Total")
    for label in labels:
        head.append(f"{label} version")
    head.append("LastUpdate")  # last time any part of the test was updated on server
    head.append("CSVWriteTime")  # when the test's row written in this csv file.
    head.append("Warnings")

    with open(filename, "w") as csvfile:
        testWriter = csv.DictWriter(
            csvfile,
            fieldnames=head,
            quotechar='"',
            quoting=csv.QUOTE_NONNUMERIC,
        )
        testWriter.writeheader()
        existsUnmarked = False
        existsMissingID = False
        for t, thisTest in spreadSheetDict.items():
            if thisTest["marked"] is False:
                existsUnmarked = True  # Check for unmarked tests as to return the appropriate warning
            row = {}
            row["StudentID"] = thisTest["sid"]
            row["StudentName"] = thisTest["sname"]
            row["TestNumber"] = int(t)
            tot = 0
            for i, label in enumerate(labels):
                q = i + 1
                if thisTest["marked"]:
                    tot += int(thisTest["q{}m".format(q)])
                row[f"{label} mark"] = thisTest["q{}m".format(q)]
                row[f"{label} version"] = thisTest["q{}v".format(q)]
            if thisTest["marked"]:
                row["Total"] = tot
            else:
                row["Total"] = ""

            lu = arrow.get(thisTest["last_update"])
            row["LastUpdate"] = arrowtime_to_string(lu)

            row["CSVWriteTime"] = utc_now_to_string()

            warnString = ""
            if not thisTest["identified"]:
                warnString += "[Unidentified]"
            if "Blank" in thisTest["sname"]:
                warnString += "[Blank ID]"
                existsMissingID = True
            if "No ID" in thisTest["sname"]:
                warnString += "[No ID]"
                existsMissingID = True
            if not thisTest["marked"]:
                warnString += "[Unmarked]"
            row["Warnings"] = warnString

            testWriter.writerow(row)

    return existsUnmarked, existsMissingID


[docs]@with_finish_messenger def pull_spreadsheet(*, msgr, filename=CSVFilename, verbose=True): """Download the "marks.csv" spreadsheet from the server, optionally printing status messages. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. filename (pathlib.Path/str): where to save the csv, defaults to "marks.csv". verbose (bool): echo diagnostics to stdout, default True. Returns: bool: True if all grading is complete and identified. False if grading is incomplete or some papers are not IDed. Note in either case we write the spreadsheet. """ spec = msgr.get_spec() numberOfQuestions = spec["numberOfQuestions"] spreadSheetDict = msgr.RgetSpreadsheet() qlabels = [get_question_label(spec, n + 1) for n in range(numberOfQuestions)] existsUnmarked, existsMissingID = write_spreadsheet( spreadSheetDict, qlabels, filename ) if verbose: if existsUnmarked: print(f'Partial marks written to "{filename}" (marking is not complete).') else: print(f'Marks written to "{filename}".') if existsMissingID: print("Warning: not every test is identified.") return not existsUnmarked and not existsMissingID