test.py 4.45 KB
"""
Test - instances of this class are individual tests
"""

# python standard library
from datetime import datetime
import json
import logging
from math import nan


logger = logging.getLogger(__name__)


# ============================================================================
class Test(dict):
    """
    Each instance Test() is a concrete test of a single student.
    A test can be in one of the states: ACTIVE, SUBMITTED, CORRECTED, QUIT
    Methods:
        t.start(student) - marks start of test (register time and state)
        t.reset_answers() - remove all answers from the test
        t.update_answer(ref, ans) - update answer of a given question
        t.submit(answers_dict) - update answers, register time and state
        t.correct_async()
        t.correct() - corrects questions and compute grade, register state
        t.giveup() - register the test as given up, answers are not corrected
        t.save_json(filename) - save the current test to file in JSON format
    """

    # ------------------------------------------------------------------------
    def __init__(self, d: dict):
        super().__init__(d)
        self["grade"] = nan
        self["comment"] = ""

    # ------------------------------------------------------------------------
    def start(self, uid: str) -> None:
        """
        Register student id and start time in the test
        """
        self["student"] = uid
        self["start_time"] = datetime.now()
        self["finish_time"] = None
        self["state"] = "ACTIVE"

    # ------------------------------------------------------------------------
    def reset_answers(self) -> None:
        """Removes all answers from the test (clean)"""
        for question in self["questions"]:
            question["answer"] = None

    # ------------------------------------------------------------------------
    def update_answer(self, ref: str, ans) -> None:
        """updates one answer in the test"""
        self["questions"][ref].set_answer(ans)

    # ------------------------------------------------------------------------
    def submit(self, answers: dict) -> None:
        """
        Given a dictionary ans={'ref': 'some answer'} updates the answers of
        multiple questions in the test.
        Only affects the questions referred in the dictionary.
        """
        self["finish_time"] = datetime.now()
        for ref, ans in answers.items():
            self["questions"][ref].set_answer(ans)
        self["state"] = "SUBMITTED"

    # ------------------------------------------------------------------------
    async def correct_async(self) -> None:
        """Corrects all the answers of the test and computes the final grade"""
        grade = 0.0
        for question in self["questions"]:
            await question.correct_async()
            grade += question["grade"] * question["points"]
            logger.debug(
                "Correcting %30s: %3g%%", question["ref"], question["grade"] * 100
            )

        # truncate to avoid negative final grade and adjust scale
        self["grade"] = max(0.0, grade) + self["scale"][0]
        self["state"] = "CORRECTED"

    # ------------------------------------------------------------------------
    def correct(self) -> None:
        """Corrects all the answers of the test and computes the final grade"""
        grade = 0.0
        for question in self["questions"]:
            question.correct()
            grade += question["grade"] * question["points"]
            logger.debug(
                "Correcting %30s: %3g%%", question["ref"], question["grade"] * 100
            )

        # truncate to avoid negative final grade and adjust scale
        self["grade"] = max(0.0, grade) + self["scale"][0]
        self["state"] = "CORRECTED"

    # ------------------------------------------------------------------------
    def giveup(self) -> None:
        """Test is marqued as QUIT and is not corrected"""
        self["finish_time"] = datetime.now()
        self["state"] = "QUIT"
        self["grade"] = 0.0

    # ------------------------------------------------------------------------
    def save_json(self, filename: str) -> None:
        """save test in JSON format"""
        with open(filename, "w", encoding="utf-8") as file:
            json.dump(self, file, indent=2, default=str)  # str for datetime

    # ------------------------------------------------------------------------
    def __str__(self) -> str:
        return "\n".join([f"{k}: {v}" for k, v in self.items()])