import os, sys, fnmatch import random import sqlite3 from datetime import datetime try: import yaml except ImportError: print('The package "yaml" is missing. See README.md for instructions.') sys.exit(1) try: import json except ImportError: print('The package "json" is missing. See README.md for instructions.') sys.exit(1) # 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