test.py 7.06 KB

import os, sys, fnmatch
import random
import yaml, json
import sqlite3
from datetime import datetime

# my code
import questions
import database

# ===========================================================================
def read_configuration(filename, debug=False, show_points=False, show_hints=False, practice=False, save_answers=False, show_ref=False):
    # FIXME validar se ficheiros e directorios existem???

    try:
        f = open(filename, 'r', encoding='utf-8')
    except IOError:
        print('[ ERROR ]  Cannot open YAML file "%s"' % filename)
        sys.exit(1)
    else:
        with f:
            try:
                test = yaml.load(f)
            except yaml.YAMLError as exc:
                mark = exc.problem_mark
                print('[ ERROR ]  In YAML file "{0}" near line {1}, column {2}.'.format(filename,mark.line,mark.column+1))
                sys.exit(1)
    # -- test yaml was loaded ok

    errors = 0

    # defaults:
    test['ref'] = str(test.get('ref', filename))
    test['title'] = str(test.get('title', ''))
    test['show_hints'] = bool(test.get('show_hints', show_hints))
    test['show_points'] = bool(test.get('show_points', show_points))
    test['practice'] = bool(test.get('practice', practice))
    test['debug'] = bool(test.get('debug', debug))
    test['show_ref'] = bool(test.get('show_ref', show_ref))

    # this is the base directory where questions are stored
    test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir))))
    if not os.path.exists(test['questions_dir']):
        print('[ ERROR ]  Questions directory "{0}" does not exist.\n           Fix the "questions_dir" key in the configuration file "{1}".'.format(test['questions_dir'], filename))
        errors += 1

    # where to put the students answers (optional)
    if 'answers_dir' not in test:
        print('[ WARNG ]  Missing "answers_dir" in the test configuration file "{0}".\n           Tests are NOT being saved. Grades are still going into the database.'.format(filename))
        test['save_answers'] = False
    else:
        test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir'])))
        if not os.path.isdir(test['answers_dir']):
            print('[ ERROR ]  Directory "{0}" does not exist.'.format(test['answers_dir']))
            errors += 1
        test['save_answers'] = True

    # database with login credentials and grades
    if 'database' not in test:
        print('[ ERROR ]  Missing "database" key in the test configuration "{0}".'.format(filename))
        errors += 1
    else:
        test['database'] = os.path.normpath(os.path.expanduser(str(test['database'])))
        if not os.path.exists(test['database']):
            print('[ ERROR ]  Database "{0}" not found.'.format(test['database']))
            errors += 1

    if errors > 0:
        print('{0} error(s) found. Aborting!'.format(errors))
        sys.exit(1)

    # deal with questions files
    if 'files' not in test:
        # no files were defined = load all from questions_dir
        test['files'] = fnmatch.filter(os.listdir(test['questions_dir']), '*.yaml')
        print('[ WARNG ]  All YAML files from directory were loaded. Might not be such a good idea...')
    else:
        # only one file
        if isinstance(test['files'], str):
            test['files'] = [test['files']]

    # replace ref,points by actual questions from pool
    pool = questions.QuestionsPool()
    pool.add_from_files(files=test['files'], path=test['questions_dir'])

    for i, q in enumerate(test['questions']):
        # each question is a list of alternative versions, even if the list
        # contains only one element
        if isinstance(q, str):
            # normalize question to a dict
            #       - some_ref
            # becomes
            #       - ref: some_ref
            #         points: 1.0
            test['questions'][i] = [pool[q]]  # list with just one question
            test['questions'][i][0]['points'] = 1.0
            # Note: at this moment we do not know the questions types.
            # Some questions, like information, should have default points
            # set to 0. That must be done later when the question is
            # instantiated.

        elif isinstance(q, dict):
            if 'ref' not in q:
                print('  * Found a question without a "ref" key in the test "{}"'.format(filename))
                print('    Dictionary contents:', q)
                sys.exit(1)

            if isinstance(q['ref'], str):
                q['ref'] = [q['ref']]       # ref is always a list
            p = float(q.get('points', 1.0)) # default points is 1.0

            # create list of alternatives, normalized
            l = []
            for r in q['ref']:
                try:
                    qq = pool[r]
                except KeyError:
                    print('[ WARNG ]  Question reference "{0}" of test "{1}" not found. Skipping...'.format(r, test['ref']))
                    continue
                qq['points'] = p
                l.append(qq)

            # add question (i.e. list of alternatives) to the test
            test['questions'][i] = l

    return test

# ===========================================================================
class Test(dict):
    # -----------------------------------------------------------------------
    def __init__(self, d):
        super().__init__(d)

        qlist = []
        for i, qq in enumerate(self['questions']):
            try:
                q = random.choice(qq)  # select from alternative versions
            except IndexError:
                print(qq)  # FIXME
            qlist.append(questions.create_question(q))  # create instance
        self['questions'] = qlist
        self['start_time'] = datetime.now()

    # -----------------------------------------------------------------------
    def update_answers(self, ans):
        '''given a dictionary ans={'ref':'some answer'} updates the answers
        of the test.  FIXME: check if answer is to be corrected or not
        '''
        for q in self['questions']:
            q['answer'] = ans[q['ref']] if q['ref'] in ans else None

    # -----------------------------------------------------------------------
    def correct(self):
        '''Corrects all the answers and computes the final grade.'''

        self['finish_time'] = datetime.now()

        total_points = 0.0
        final_grade = 0.0
        for q in self['questions']:
            final_grade += q.correct() * q['points']
            total_points += q['points']

        final_grade = 20.0 * max(final_grade / total_points, 0.0)

        self['grade'] = final_grade
        return final_grade

    # -----------------------------------------------------------------------
    def save_json(self, path):
        filename = ' -- '.join((str(self['number']), self['ref'],
                               str(self['finish_time']))) + '.json'
        filepath = os.path.abspath(os.path.join(path, filename))
        with open(filepath, 'w') as f:
            json.dump(self, f, indent=2, default=str)
            # HACK default=str is required for datetime objects