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