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,8 +23,9 @@ server.socket_port = 8080 | ||
| 23 | log.screen = False | 23 | log.screen = False |
| 24 | 24 | ||
| 25 | # add path to the log files here. empty strings disable logging | 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 | # DO NOT DISABLE SESSIONS! | 30 | # DO NOT DISABLE SESSIONS! |
| 30 | tools.sessions.on = True | 31 | tools.sessions.on = True |
questions.py
| @@ -9,7 +9,6 @@ import os.path | @@ -9,7 +9,6 @@ import os.path | ||
| 9 | # Example usage: | 9 | # Example usage: |
| 10 | # | 10 | # |
| 11 | # pool = QuestionPool() | 11 | # pool = QuestionPool() |
| 12 | -# pool.add_from_file('filename.yaml') | ||
| 13 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) | 12 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) |
| 14 | # | 13 | # |
| 15 | # test = [] | 14 | # test = [] |
| @@ -22,29 +21,18 @@ import os.path | @@ -22,29 +21,18 @@ import os.path | ||
| 22 | # =========================================================================== | 21 | # =========================================================================== |
| 23 | class QuestionsPool(dict): | 22 | class QuestionsPool(dict): |
| 24 | '''This class contains base questions read from files, but which are | 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 | for i, q in enumerate(questions): | 29 | for i, q in enumerate(questions): |
| 42 | if not isinstance(q, dict): | 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 | continue | 32 | continue |
| 45 | 33 | ||
| 46 | if q['ref'] in self: | 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 | sys.exit(1) | 36 | sys.exit(1) |
| 49 | 37 | ||
| 50 | # filename and index (number in the file, 0 based) | 38 | # filename and index (number in the file, 0 based) |
| @@ -67,7 +55,16 @@ class QuestionsPool(dict): | @@ -67,7 +55,16 @@ class QuestionsPool(dict): | ||
| 67 | #------------------------------------------------------------------------ | 55 | #------------------------------------------------------------------------ |
| 68 | def add_from_files(self, files, path='.'): | 56 | def add_from_files(self, files, path='.'): |
| 69 | for filename in files: | 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,14 +100,14 @@ def create_question(q): | ||
| 103 | try: | 100 | try: |
| 104 | questiontype = types[q['type']] | 101 | questiontype = types[q['type']] |
| 105 | except KeyError: | 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 | questiontype = Question | 104 | questiontype = Question |
| 108 | 105 | ||
| 109 | # create question instance and return | 106 | # create question instance and return |
| 110 | try: | 107 | try: |
| 111 | qinstance = questiontype(q) | 108 | qinstance = questiontype(q) |
| 112 | except: | 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 | return qinstance | 112 | return qinstance |
| 116 | 113 | ||
| @@ -126,7 +123,7 @@ def question_generator(q): | @@ -126,7 +123,7 @@ def question_generator(q): | ||
| 126 | try: | 123 | try: |
| 127 | p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) | 124 | p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 128 | except FileNotFoundError: | 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 | sys.exit(1) | 127 | sys.exit(1) |
| 131 | 128 | ||
| 132 | try: | 129 | try: |
serve.py
| @@ -11,7 +11,7 @@ from os import path | @@ -11,7 +11,7 @@ from os import path | ||
| 11 | 11 | ||
| 12 | # path where this file is located | 12 | # path where this file is located |
| 13 | SERVER_PATH = path.dirname(path.realpath(__file__)) | 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 | # my code | 16 | # my code |
| 17 | from myauth import AuthController, require | 17 | from myauth import AuthController, require |
| @@ -187,7 +187,7 @@ if __name__ == '__main__': | @@ -187,7 +187,7 @@ if __name__ == '__main__': | ||
| 187 | # --- controller | 187 | # --- controller |
| 188 | root = Root(testconf) | 188 | root = Root(testconf) |
| 189 | 189 | ||
| 190 | - # --- Mount and run server. | 190 | + # --- Mount and run server |
| 191 | cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) | 191 | cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) |
| 192 | cherrypy.quickstart(root, '/', config=arg.server) | 192 | cherrypy.quickstart(root, '/', config=arg.server) |
| 193 | cherrypy.log('Terminated OK ------------------------', 'APPLICATION') | 193 | cherrypy.log('Terminated OK ------------------------', 'APPLICATION') |
test.py
| 1 | 1 | ||
| 2 | -import os, sys | 2 | +import os, sys, fnmatch |
| 3 | import random | 3 | import random |
| 4 | -import yaml | ||
| 5 | -import json | 4 | +import yaml, json |
| 6 | import sqlite3 | 5 | import sqlite3 |
| 7 | from datetime import datetime | 6 | from datetime import datetime |
| 8 | 7 | ||
| @@ -13,16 +12,23 @@ import database | @@ -13,16 +12,23 @@ import database | ||
| 13 | # =========================================================================== | 12 | # =========================================================================== |
| 14 | def read_configuration(filename, debug=False, show_points=False, show_hints=False, practice=False, save_answers=False, show_ref=False): | 13 | def read_configuration(filename, debug=False, show_points=False, show_hints=False, practice=False, save_answers=False, show_ref=False): |
| 15 | # FIXME validar se ficheiros e directorios existem??? | 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 | try: | 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 | # defaults: | 33 | # defaults: |
| 28 | test['ref'] = str(test.get('ref', filename)) | 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,34 +38,51 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals | ||
| 32 | test['practice'] = bool(test.get('practice', practice)) | 38 | test['practice'] = bool(test.get('practice', practice)) |
| 33 | test['debug'] = bool(test.get('debug', debug)) | 39 | test['debug'] = bool(test.get('debug', debug)) |
| 34 | test['show_ref'] = bool(test.get('show_ref', show_ref)) | 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 | if not os.path.isdir(test['answers_dir']): | 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 | if 'database' not in test: | 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 | sys.exit(1) | 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 | # replace ref,points by actual questions from pool | 83 | # replace ref,points by actual questions from pool |
| 60 | pool = questions.QuestionsPool() | 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 | for i, q in enumerate(test['questions']): | 87 | for i, q in enumerate(test['questions']): |
| 65 | # each question is a list of alternative versions, even if the list | 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,7 +116,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals | ||
| 93 | try: | 116 | try: |
| 94 | qq = pool[r] | 117 | qq = pool[r] |
| 95 | except KeyError: | 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 | continue | 120 | continue |
| 98 | qq['points'] = p | 121 | qq['points'] = p |
| 99 | l.append(qq) | 122 | l.append(qq) |
| @@ -111,7 +134,10 @@ class Test(dict): | @@ -111,7 +134,10 @@ class Test(dict): | ||
| 111 | 134 | ||
| 112 | qlist = [] | 135 | qlist = [] |
| 113 | for i, qq in enumerate(self['questions']): | 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 | qlist.append(questions.create_question(q)) # create instance | 141 | qlist.append(questions.create_question(q)) # create instance |
| 116 | self['questions'] = qlist | 142 | self['questions'] = qlist |
| 117 | self['start_time'] = datetime.now() | 143 | self['start_time'] = datetime.now() |