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() |