Commit f9a2254fba2acfcbb0c9396fc27b606f248c998e

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

- logging in serve.py replaced prints

- more widespread logging
- added issues in BUGS.md
Showing 4 changed files with 87 additions and 46 deletions   Show diff stats
BUGS.md
1 1  
2 2 # BUGS
3 3  
  4 +- cherrypy faz logs para consola...
  5 +- mensagens info nao aparecem no serve.py
  6 +- usar thread.Lock para aceder a variaveis de estado.
  7 +- ordenar activos por hora de login.
  8 +- se no teste uma das "ref" nao existir nos ficheiros de perguntas, rebenta.
  9 +- no final do teste (em modo avaliacao) o aluno é removido da lista allowed. para fazer novamente necessita de aprovação do professor.
4 10 - mesmo aluno pode entrar várias vezes em simultaneo...
5   -- ordenar lista de alunos pelos online/offline, e depois pelo numero.
6 11 - qd scripts não são executáveis rebenta. Testar isso e dar uma mensagem de erro.
7 12 - paths manipulation in strings is unix only ('/something'). use os.path to create paths.
8 13 - alunos vêm nota final arredondada às decimas, mas é apenas um arredondamento visual. Pode acontecer o aluno chumbar, mas ver uma nota positiva (e.g. 9.46 mostra 9.5 e presume que esta aprovado). Mostrar 3 casas?
... ... @@ -15,6 +20,10 @@
15 20  
16 21 # TODO
17 22  
  23 +- script de correcção pode enviar dicionario yaml com grade e comentarios. ex:
  24 + grade: 0.5
  25 + comments: Falhou na função xpto.
  26 + os comentários são guardados no teste (ficheiro) ou enviados para o browser no modo practice.
18 27 - warning quando se executa novamente o mesmo teste na consola. ie se ja houver submissoes desse teste.
19 28 - na cotacao da pergunta indicar o intervalo, e.g. [-0.2, 1], [0, 0.5]
20 29 - fazer uma calculadora javascript e por no menu. surge como modal
... ... @@ -32,6 +41,7 @@
32 41  
33 42 # FIXED
34 43  
  44 +- ordenar lista de alunos pelos online/offline, e depois pelo numero.
35 45 - hash das passwords concatenadas com salt gerado aleatoriamente. necessario acrescentar salt de cada aluno. gerar salt com os.urandom(256)
36 46 - fix ans directory. relative to what?? current dir?
37 47 - textarea com opcao de numero de linhas (consoante o programa a desenvolver podem ser necessarias mais ou menos linhas)
... ...
questions.py
... ... @@ -38,27 +38,29 @@ import os.path
38 38 import logging
39 39 import sys
40 40  
41   -try:
42   - import yaml
43   -except ImportError:
44   - print('The package "yaml" is missing. See README.md for instructions.')
45   - sys.exit(1)
46 41  
47 42  
48   -qlogger = logging.getLogger('Questions')
  43 +qlogger = logging.getLogger('questions')
49 44 qlogger.setLevel(logging.INFO)
50 45  
51 46 fh = logging.FileHandler('question.log')
52 47 ch = logging.StreamHandler()
53 48 ch.setLevel(logging.INFO)
54 49  
55   -formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
  50 +formatter = logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s')
56 51 fh.setFormatter(formatter)
57 52 ch.setFormatter(formatter)
58 53  
59 54 qlogger.addHandler(fh)
60 55 qlogger.addHandler(ch)
61 56  
  57 +try:
  58 + import yaml
  59 +except ImportError:
  60 + logger.critical('The package "yaml" is missing. See README.md for instructions.')
  61 + sys.exit(1)
  62 +
  63 +
62 64 # if an error occurs in a question, the question is replaced by this message
63 65 qerror = {
64 66 'filename': 'questions.py',
... ... @@ -112,7 +114,7 @@ class QuestionsPool(dict):
112 114 qlogger.error('Error loading questions from YAML file "{0}". Skipping this one.'.format(filename))
113 115 continue
114 116 self.add(questions, filename, path)
115   - qlogger.info('Added {0} questions from "{1}" to the pool.'.format(len(questions), filename))
  117 + qlogger.info('Loaded {0} questions from "{1}".'.format(len(questions), filename))
116 118  
117 119  
118 120 #============================================================================
... ...
serve.py
... ... @@ -7,6 +7,7 @@
7 7 from os import path
8 8 import sys
9 9 import argparse
  10 +import logging
10 11 # from threading import Lock
11 12  
12 13 try:
... ... @@ -31,6 +32,13 @@ import test
31 32 import database
32 33  
33 34  
  35 +ch = logging.StreamHandler()
  36 +ch.setLevel(logging.INFO)
  37 +ch.setFormatter(logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s'))
  38 +
  39 +logger = logging.getLogger('serve')
  40 +logger.addHandler(ch)
  41 +
34 42  
35 43 # ============================================================================
36 44 # Classes that respond to HTTP
... ... @@ -198,25 +206,35 @@ def parse_arguments():
198 206  
199 207 # ============================================================================
200 208 if __name__ == '__main__':
  209 +
  210 + logger.error('---------- Running perguntations ----------')
  211 +
201 212 # --- parse command line arguments and build base test
202 213 arg = parse_arguments()
203 214 testconf = test.read_configuration(arg.testfile[0], debug=arg.debug, show_points=arg.show_points, show_hints=arg.show_hints, save_answers=arg.save_answers, practice=arg.practice, show_ref=arg.show_ref)
204 215  
205   - print('=' * 79)
206   - # print('- Title: %s' % testconf['title']) # FIXME problems with UnicodeEncodeError
207   - print('- Database: %s' % testconf['database'])
208   - print('- Loaded %i questions from:' % len(testconf['questions']))
209   - print(' path: %s' % testconf['questions_dir'])
210   - print(' files: %s' % ', '.join(testconf['files']))
211   - print('-' * 79)
212   - print('- Starting server...')
213   -
214   - # --- controller
215   - root = Root(testconf)
  216 + # FIXME problems with UnicodeEncodeError
  217 + logger.error(' Title: %s' % testconf['title'])
  218 + logger.error(' Database: %s' % testconf['database']) # FIXME check if db is ok?
216 219  
217   - # --- Mount and run server
  220 + # --- site wide configuration (valid for all apps)
218 221 cherrypy.config.update({'tools.staticdir.root': SERVER_PATH})
219   - cherrypy.quickstart(root, '/', config=arg.server)
  222 + cherrypy.config.update(arg.server)
  223 + # --- app specific configuration
  224 + app = cherrypy.tree.mount(Root(testconf), '/', arg.server)
  225 +
  226 + logger.info('Starting server at {}:{}'.format(
  227 + cherrypy.config['server.socket_host'],
  228 + cherrypy.config['server.socket_port']))
  229 +
  230 + if hasattr(cherrypy.engine, "signal_handler"):
  231 + cherrypy.engine.signal_handler.subscribe()
  232 + if hasattr(cherrypy.engine, "console_control_handler"):
  233 + cherrypy.engine.console_control_handler.subscribe()
  234 +
  235 + cherrypy.engine.start()
  236 + cherrypy.engine.block()
  237 +
220 238 cherrypy.log('Terminated OK ------------------------', 'APPLICATION')
221   - print('\n- Server terminated OK')
222   - print('=' * 79)
  239 + print()
  240 + logger.critical('-------- !!! Server terminated !!! --------')
... ...
test.py
... ... @@ -2,21 +2,29 @@
2 2 import os, sys, fnmatch
3 3 import random
4 4 import sqlite3
  5 +import logging
5 6 from datetime import datetime
6 7  
  8 +
  9 +ch = logging.StreamHandler()
  10 +ch.setLevel(logging.INFO)
  11 +ch.setFormatter(logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s'))
  12 +
  13 +logger = logging.getLogger('test')
  14 +logger.addHandler(ch)
  15 +
7 16 try:
8 17 import yaml
9 18 except ImportError:
10   - print('The package "yaml" is missing. See README.md for instructions.')
  19 + logger.critical('The package "yaml" is missing. See README.md for instructions.')
11 20 sys.exit(1)
12 21  
13 22 try:
14 23 import json
15 24 except ImportError:
16   - print('The package "json" is missing. See README.md for instructions.')
  25 + logger.critical('The package "json" is missing. See README.md for instructions.')
17 26 sys.exit(1)
18 27  
19   -
20 28 # my code
21 29 import questions
22 30 import database
... ... @@ -28,7 +36,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
28 36 try:
29 37 f = open(filename, 'r', encoding='utf-8')
30 38 except IOError:
31   - print('[ ERROR ] Cannot open YAML file "%s"' % filename)
  39 + logger.critical('Cannot open YAML file "%s"' % filename)
32 40 sys.exit(1)
33 41 else:
34 42 with f:
... ... @@ -36,7 +44,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
36 44 test = yaml.load(f)
37 45 except yaml.YAMLError as exc:
38 46 mark = exc.problem_mark
39   - print('[ ERROR ] In YAML file "{0}" near line {1}, column {2}.'.format(filename,mark.line,mark.column+1))
  47 + logger.critical('In YAML file "{0}" near line {1}, column {2}.'.format(filename,mark.line,mark.column+1))
40 48 sys.exit(1)
41 49 # -- test yaml was loaded ok
42 50  
... ... @@ -54,39 +62,39 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
54 62 # this is the base directory where questions are stored
55 63 test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir))))
56 64 if not os.path.exists(test['questions_dir']):
57   - print('[ ERROR ] Questions directory "{0}" does not exist.\n Fix the "questions_dir" key in the configuration file "{1}".'.format(test['questions_dir'], filename))
  65 + logger.error('Questions directory "{0}" does not exist. Fix the "questions_dir" key in the configuration file "{1}".'.format(test['questions_dir'], filename))
58 66 errors += 1
59 67  
60 68 # where to put the students answers (optional)
61 69 if 'answers_dir' not in test:
62   - print('[ WARNG ] Missing "answers_dir" in the test configuration file "{0}".\n Tests are NOT being saved. Grades are still going into the database.'.format(filename))
  70 + logger.warning('Missing "answers_dir" in "{0}". Tests will NOT be saved.'.format(filename))
63 71 test['save_answers'] = False
64 72 else:
65 73 test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir'])))
66 74 if not os.path.isdir(test['answers_dir']):
67   - print('[ ERROR ] Directory "{0}" does not exist.'.format(test['answers_dir']))
  75 + logger.error('Directory "{0}" does not exist.'.format(test['answers_dir']))
68 76 errors += 1
69 77 test['save_answers'] = True
70 78  
71 79 # database with login credentials and grades
72 80 if 'database' not in test:
73   - print('[ ERROR ] Missing "database" key in the test configuration "{0}".'.format(filename))
  81 + logger.error('Missing "database" key in the test configuration "{0}".'.format(filename))
74 82 errors += 1
75 83 else:
76 84 test['database'] = os.path.normpath(os.path.expanduser(str(test['database'])))
77 85 if not os.path.exists(test['database']):
78   - print('[ ERROR ] Database "{0}" not found.'.format(test['database']))
  86 + logger.error('Database "{0}" not found.'.format(test['database']))
79 87 errors += 1
80 88  
81 89 if errors > 0:
82   - print('{0} error(s) found. Aborting!'.format(errors))
  90 + logger.critical('{0} error(s) found. Aborting!'.format(errors))
83 91 sys.exit(1)
84 92  
85 93 # deal with questions files
86 94 if 'files' not in test:
87 95 # no files were defined = load all from questions_dir
88 96 test['files'] = fnmatch.filter(os.listdir(test['questions_dir']), '*.yaml')
89   - print('[ WARNG ] All YAML files from directory were loaded. Might not be such a good idea...')
  97 + logger.warning('All YAML files from directory were loaded. Might not be such a good idea...')
90 98 else:
91 99 # only one file
92 100 if isinstance(test['files'], str):
... ... @@ -101,11 +109,14 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
101 109 # contains only one element
102 110 if isinstance(q, str):
103 111 # normalize question to a dict
104   - # - some_ref
105   - # becomes
106   - # - ref: some_ref
107   - # points: 1.0
108   - test['questions'][i] = [pool[q]] # list with just one question
  112 + # some_ref --> ref: some_ref
  113 + # points: 1.0
  114 + try:
  115 + test['questions'][i] = [pool[q]] # list with just one question
  116 + except KeyError:
  117 + logger.critical('Could not find question "{}".'.format(q))
  118 + sys.exit(1)
  119 +
109 120 test['questions'][i][0]['points'] = 1.0
110 121 # Note: at this moment we do not know the questions types.
111 122 # Some questions, like information, should have default points
... ... @@ -114,8 +125,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
114 125  
115 126 elif isinstance(q, dict):
116 127 if 'ref' not in q:
117   - print(' * Found a question without a "ref" key in the test "{}"'.format(filename))
118   - print(' Dictionary contents:', q)
  128 + logger.critical('Found question missing the "ref" key in "{}"'.format(filename))
119 129 sys.exit(1)
120 130  
121 131 if isinstance(q['ref'], str):
... ... @@ -128,7 +138,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
128 138 try:
129 139 qq = pool[r]
130 140 except KeyError:
131   - print('[ WARNG ] Question reference "{0}" of test "{1}" not found. Skipping...'.format(r, test['ref']))
  141 + logger.warning('Question reference "{0}" of test "{1}" not found. Skipping...'.format(r, test['ref']))
132 142 continue
133 143 qq['points'] = p
134 144 l.append(qq)
... ... @@ -148,8 +158,9 @@ class Test(dict):
148 158 for i, qq in enumerate(self['questions']):
149 159 try:
150 160 q = random.choice(qq) # select from alternative versions
151   - except IndexError:
152   - print(qq) # FIXME
  161 + except TypeError:
  162 + logger.error('in question {} (0-based index).'.format(i))
  163 + continue
153 164 qlist.append(questions.create_question(q)) # create instance
154 165 self['questions'] = qlist
155 166 self['start_time'] = datetime.now()
... ...