Commit fe0d075161039a8d68e1c68ed5ced9184378619a

Authored by Miguel Barão
1 parent df573134
Exists in master and in 1 other branch dev

- 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.
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
@@ -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:
@@ -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')
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()