test.py 4.44 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()])