Commit fe0d075161039a8d68e1c68ed5ced9184378619a
1 parent
df573134
Exists in
master
and in
1 other branch
- add_from_files does not immediately abort on error
- pretty errors and warnings on startup - removed option 'save_answers' from test configuration. It is now automatically defined depending on the existence of a 'answers_dir' option. - several fixes in managing paths.
Showing
4 changed files
with
82 additions
and
58 deletions
Show diff stats
config/server.conf
| ... | ... | @@ -23,8 +23,9 @@ server.socket_port = 8080 |
| 23 | 23 | log.screen = False |
| 24 | 24 | |
| 25 | 25 | # add path to the log files here. empty strings disable logging |
| 26 | -log.error_file = '/Users/mjsb/Library/Logs/Perguntations/errors.log' | |
| 27 | -log.access_file = '/Users/mjsb/Library/Logs/Perguntations/access.log' | |
| 26 | +log.error_file = '/Users/mjsb/Work/Projects/perguntations/logs/errors.log' | |
| 27 | +log.access_file = '' | |
| 28 | +; log.access_file = '/Users/mjsb/Library/Logs/Perguntations/access.log' | |
| 28 | 29 | |
| 29 | 30 | # DO NOT DISABLE SESSIONS! |
| 30 | 31 | tools.sessions.on = True | ... | ... |
questions.py
| ... | ... | @@ -9,7 +9,6 @@ import os.path |
| 9 | 9 | # Example usage: |
| 10 | 10 | # |
| 11 | 11 | # pool = QuestionPool() |
| 12 | -# pool.add_from_file('filename.yaml') | |
| 13 | 12 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) |
| 14 | 13 | # |
| 15 | 14 | # test = [] |
| ... | ... | @@ -22,29 +21,18 @@ import os.path |
| 22 | 21 | # =========================================================================== |
| 23 | 22 | class QuestionsPool(dict): |
| 24 | 23 | '''This class contains base questions read from files, but which are |
| 25 | - not ready yet. They have to be instantiated separatly for each student.''' | |
| 24 | + not ready yet. They have to be instantiated for each student.''' | |
| 26 | 25 | |
| 27 | 26 | #------------------------------------------------------------------------ |
| 28 | - def add_from_file(self, filename, path='.'): | |
| 29 | - path_file = os.path.normpath(os.path.join(path, filename)) | |
| 30 | - try: | |
| 31 | - with open(path_file, 'r') as f: | |
| 32 | - questions = yaml.load(f) | |
| 33 | - except(FileNotFoundError): | |
| 34 | - print(' * Questions file "{0}" not found. Aborting...'.format(path_file)) | |
| 35 | - sys.exit(1) | |
| 36 | - except(yaml.parser.ParserError): | |
| 37 | - print(' * Error in questions file "{0}". Aborting...'.format(filename)) | |
| 38 | - sys.exit(1) | |
| 39 | - | |
| 40 | - # add defaults if missing from sources | |
| 27 | + def add(self, questions, filename, path): | |
| 28 | + # add some defaults if missing from sources | |
| 41 | 29 | for i, q in enumerate(questions): |
| 42 | 30 | if not isinstance(q, dict): |
| 43 | - print(' * question with index {0} in file {1} is not a dict. Ignoring...'.format(i, filename)) | |
| 31 | + print('[ WARNG ] Question with index {0} in file {1} is not a dict. Ignoring...'.format(i, filename)) | |
| 44 | 32 | continue |
| 45 | 33 | |
| 46 | 34 | if q['ref'] in self: |
| 47 | - print(' * question "{0}" in file "{1}" already exists in file "{2}".'.format(q['ref'], filename, self[q['ref']]['filename'])) | |
| 35 | + print('[ ERROR ] Duplicate question "{0}" in files "{1}" and "{2}".'.format(q['ref'], filename, self[q['ref']]['filename'])) | |
| 48 | 36 | sys.exit(1) |
| 49 | 37 | |
| 50 | 38 | # filename and index (number in the file, 0 based) |
| ... | ... | @@ -67,7 +55,16 @@ class QuestionsPool(dict): |
| 67 | 55 | #------------------------------------------------------------------------ |
| 68 | 56 | def add_from_files(self, files, path='.'): |
| 69 | 57 | for filename in files: |
| 70 | - self.add_from_file(filename, path) | |
| 58 | + try: | |
| 59 | + with open(os.path.normpath(os.path.join(path, filename)), 'r') as f: | |
| 60 | + questions = yaml.load(f) | |
| 61 | + except(FileNotFoundError): | |
| 62 | + print('[ ERROR ] Questions file "{0}" not found.'.format(filename)) | |
| 63 | + continue | |
| 64 | + except(yaml.parser.ParserError): | |
| 65 | + print('[ ERROR ] Error loading questions in YAML file "{0}".'.format(filename)) | |
| 66 | + continue | |
| 67 | + self.add(questions, filename, path) | |
| 71 | 68 | |
| 72 | 69 | |
| 73 | 70 | #============================================================================ |
| ... | ... | @@ -103,14 +100,14 @@ def create_question(q): |
| 103 | 100 | try: |
| 104 | 101 | questiontype = types[q['type']] |
| 105 | 102 | except KeyError: |
| 106 | - print(' * unsupported question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | |
| 103 | + print('[ ERROR ] Unsupported question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | |
| 107 | 104 | questiontype = Question |
| 108 | 105 | |
| 109 | 106 | # create question instance and return |
| 110 | 107 | try: |
| 111 | 108 | qinstance = questiontype(q) |
| 112 | 109 | except: |
| 113 | - print(' * Error creating question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | |
| 110 | + print('[ ERROR ] Creating question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | |
| 114 | 111 | |
| 115 | 112 | return qinstance |
| 116 | 113 | |
| ... | ... | @@ -126,7 +123,7 @@ def question_generator(q): |
| 126 | 123 | try: |
| 127 | 124 | p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 128 | 125 | except FileNotFoundError: |
| 129 | - print(' * Script "{0}" of question "{1}" in file "{2}" could not be found'.format(script, q['ref'], q['filename'])) | |
| 126 | + print('[ ERROR ] Script "{0}" of question "{2}:{1}" not found'.format(script, q['ref'], q['filename'])) | |
| 130 | 127 | sys.exit(1) |
| 131 | 128 | |
| 132 | 129 | try: | ... | ... |
serve.py
| ... | ... | @@ -11,7 +11,7 @@ from os import path |
| 11 | 11 | |
| 12 | 12 | # path where this file is located |
| 13 | 13 | SERVER_PATH = path.dirname(path.realpath(__file__)) |
| 14 | -TEMPLATES_DIR = SERVER_PATH + '/templates' | |
| 14 | +TEMPLATES_DIR = path.join(SERVER_PATH, 'templates') | |
| 15 | 15 | |
| 16 | 16 | # my code |
| 17 | 17 | from myauth import AuthController, require |
| ... | ... | @@ -187,7 +187,7 @@ if __name__ == '__main__': |
| 187 | 187 | # --- controller |
| 188 | 188 | root = Root(testconf) |
| 189 | 189 | |
| 190 | - # --- Mount and run server. | |
| 190 | + # --- Mount and run server | |
| 191 | 191 | cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) |
| 192 | 192 | cherrypy.quickstart(root, '/', config=arg.server) |
| 193 | 193 | cherrypy.log('Terminated OK ------------------------', 'APPLICATION') | ... | ... |
test.py
| 1 | 1 | |
| 2 | -import os, sys | |
| 2 | +import os, sys, fnmatch | |
| 3 | 3 | import random |
| 4 | -import yaml | |
| 5 | -import json | |
| 4 | +import yaml, json | |
| 6 | 5 | import sqlite3 |
| 7 | 6 | from datetime import datetime |
| 8 | 7 | |
| ... | ... | @@ -13,16 +12,23 @@ import database |
| 13 | 12 | # =========================================================================== |
| 14 | 13 | def read_configuration(filename, debug=False, show_points=False, show_hints=False, practice=False, save_answers=False, show_ref=False): |
| 15 | 14 | # FIXME validar se ficheiros e directorios existem??? |
| 16 | - if not os.path.isfile(filename): | |
| 17 | - print(' * Cannot find file "%s"' % filename) | |
| 18 | - sys.exit(1) | |
| 19 | 15 | |
| 20 | 16 | try: |
| 21 | - with open(filename, 'r') as f: | |
| 22 | - test = yaml.load(f) | |
| 23 | - except: | |
| 24 | - print(' * Error reading yaml file "{0}".'.format(filename)) | |
| 25 | - raise | |
| 17 | + f = open(filename, 'r') | |
| 18 | + except IOError: | |
| 19 | + print('[ ERROR ] Cannot open YAML file "%s"' % filename) | |
| 20 | + sys.exit(1) | |
| 21 | + else: | |
| 22 | + with f: | |
| 23 | + try: | |
| 24 | + test = yaml.load(f) | |
| 25 | + except yaml.YAMLError as exc: | |
| 26 | + mark = exc.problem_mark | |
| 27 | + print('[ ERROR ] In YAML file "{0}" near line {1}, column {2}.'.format(filename,mark.line,mark.column+1)) | |
| 28 | + sys.exit(1) | |
| 29 | + # -- test yaml was loaded ok | |
| 30 | + | |
| 31 | + errors = 0 | |
| 26 | 32 | |
| 27 | 33 | # defaults: |
| 28 | 34 | test['ref'] = str(test.get('ref', filename)) |
| ... | ... | @@ -32,34 +38,51 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals |
| 32 | 38 | test['practice'] = bool(test.get('practice', practice)) |
| 33 | 39 | test['debug'] = bool(test.get('debug', debug)) |
| 34 | 40 | test['show_ref'] = bool(test.get('show_ref', show_ref)) |
| 35 | - test['path'] = str(test.get('path', '.')) # questions dir FIXME use os.path | |
| 36 | - # FIXME check that the directory exists | |
| 37 | - if not os.path.exists(test['path']): | |
| 38 | - print(' * Questions path "{0}" does not exist. Fix it in "{1}".'.format(test['path'], filename)) | |
| 39 | - sys.exit(1) | |
| 40 | - test['save_answers'] = bool(test.get('save_answers', save_answers)) | |
| 41 | - if test['save_answers']: | |
| 42 | - if 'answers_dir' not in test: | |
| 43 | - raise exception('Missing "answers_dir" in the test configuration.') | |
| 41 | + | |
| 42 | + # this is the base directory where questions are stored | |
| 43 | + test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir)))) | |
| 44 | + if not os.path.exists(test['questions_dir']): | |
| 45 | + print('[ ERROR ] Questions directory "{0}" does not exist.\n Fix the "questions_dir" key in the configuration file "{1}".'.format(test['questions_dir'], filename)) | |
| 46 | + errors += 1 | |
| 47 | + | |
| 48 | + # where to put the students answers (optional) | |
| 49 | + if 'answers_dir' not in test: | |
| 50 | + print('[ WARNG ] Missing "answers_dir" in the test configuration file "{0}".\n Saving answers is disabled'.format(filename)) | |
| 51 | + test['save_answers'] = False | |
| 52 | + else: | |
| 53 | + test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir']))) | |
| 44 | 54 | if not os.path.isdir(test['answers_dir']): |
| 45 | - print(' * Directory "%s" does not exist. Creating...' % test['answers_dir']) | |
| 46 | - os.mkdir(test['answers_dir']) | |
| 55 | + print('[ ERROR ] Directory "{0}" does not exist.'.format(test['answers_dir'])) | |
| 56 | + errors += 1 | |
| 57 | + test['save_answers'] = True | |
| 47 | 58 | |
| 59 | + # database with login credentials and grades | |
| 48 | 60 | if 'database' not in test: |
| 49 | - print(' * Missing database in the test configuration.') | |
| 61 | + print('[ ERROR ] Missing "database" key in the test configuration "{0}".'.format(filename)) | |
| 62 | + errors += 1 | |
| 63 | + else: | |
| 64 | + test['database'] = os.path.normpath(os.path.expanduser(str(test['database']))) | |
| 65 | + if not os.path.exists(test['database']): | |
| 66 | + print('[ ERROR ] Database "{0}" not found.'.format(test['database'])) | |
| 67 | + errors += 1 | |
| 68 | + | |
| 69 | + if errors > 0: | |
| 70 | + print('{0} error(s) found. Aborting!'.format(errors)) | |
| 50 | 71 | sys.exit(1) |
| 51 | - if not os.path.exists(test['database']): | |
| 52 | - print(' * Database "{0}" not found.'.format(test['database'])) | |
| 53 | - sys.exit(1) | |
| 54 | - | |
| 55 | 72 | |
| 56 | - if isinstance(test['files'], str): | |
| 57 | - test['files'] = [test['files']] | |
| 73 | + # deal with questions files | |
| 74 | + if 'files' not in test: | |
| 75 | + # no files were defined = load all from questions_dir | |
| 76 | + test['files'] = fnmatch.filter(os.listdir(test['questions_dir']), '*.yaml') | |
| 77 | + print('[ WARNG ] All YAML files from directory were loaded. Might not be such a good idea...') | |
| 78 | + else: | |
| 79 | + # only one file | |
| 80 | + if isinstance(test['files'], str): | |
| 81 | + test['files'] = [test['files']] | |
| 58 | 82 | |
| 59 | 83 | # replace ref,points by actual questions from pool |
| 60 | 84 | pool = questions.QuestionsPool() |
| 61 | - pool.add_from_files(files= | |
| 62 | - test['files'], path=test['path']) | |
| 85 | + pool.add_from_files(files=test['files'], path=test['questions_dir']) | |
| 63 | 86 | |
| 64 | 87 | for i, q in enumerate(test['questions']): |
| 65 | 88 | # each question is a list of alternative versions, even if the list |
| ... | ... | @@ -93,7 +116,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals |
| 93 | 116 | try: |
| 94 | 117 | qq = pool[r] |
| 95 | 118 | except KeyError: |
| 96 | - print(' * question "{0}" in test "{1}" not found in the pool. Ignoring...'.format(r, test['ref'])) | |
| 119 | + print('[ WARNG ] Question reference "{0}" of test "{1}" not found. Skipping...'.format(r, test['ref'])) | |
| 97 | 120 | continue |
| 98 | 121 | qq['points'] = p |
| 99 | 122 | l.append(qq) |
| ... | ... | @@ -111,7 +134,10 @@ class Test(dict): |
| 111 | 134 | |
| 112 | 135 | qlist = [] |
| 113 | 136 | for i, qq in enumerate(self['questions']): |
| 114 | - q = random.choice(qq) # select from alternative versions | |
| 137 | + try: | |
| 138 | + q = random.choice(qq) # select from alternative versions | |
| 139 | + except IndexError: | |
| 140 | + print(qq) # FIXME | |
| 115 | 141 | qlist.append(questions.create_question(q)) # create instance |
| 116 | 142 | self['questions'] = qlist |
| 117 | 143 | self['start_time'] = datetime.now() | ... | ... |