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
1 1
2 # BUGS 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 - mesmo aluno pode entrar várias vezes em simultaneo... 10 - mesmo aluno pode entrar várias vezes em simultaneo...
5 -- ordenar lista de alunos pelos online/offline, e depois pelo numero.  
6 - qd scripts não são executáveis rebenta. Testar isso e dar uma mensagem de erro. 11 - qd scripts não são executáveis rebenta. Testar isso e dar uma mensagem de erro.
7 - paths manipulation in strings is unix only ('/something'). use os.path to create paths. 12 - paths manipulation in strings is unix only ('/something'). use os.path to create paths.
8 - 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? 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,6 +20,10 @@
15 20
16 # TODO 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 - warning quando se executa novamente o mesmo teste na consola. ie se ja houver submissoes desse teste. 27 - warning quando se executa novamente o mesmo teste na consola. ie se ja houver submissoes desse teste.
19 - na cotacao da pergunta indicar o intervalo, e.g. [-0.2, 1], [0, 0.5] 28 - na cotacao da pergunta indicar o intervalo, e.g. [-0.2, 1], [0, 0.5]
20 - fazer uma calculadora javascript e por no menu. surge como modal 29 - fazer uma calculadora javascript e por no menu. surge como modal
@@ -32,6 +41,7 @@ @@ -32,6 +41,7 @@
32 41
33 # FIXED 42 # FIXED
34 43
  44 +- ordenar lista de alunos pelos online/offline, e depois pelo numero.
35 - hash das passwords concatenadas com salt gerado aleatoriamente. necessario acrescentar salt de cada aluno. gerar salt com os.urandom(256) 45 - hash das passwords concatenadas com salt gerado aleatoriamente. necessario acrescentar salt de cada aluno. gerar salt com os.urandom(256)
36 - fix ans directory. relative to what?? current dir? 46 - fix ans directory. relative to what?? current dir?
37 - textarea com opcao de numero de linhas (consoante o programa a desenvolver podem ser necessarias mais ou menos linhas) 47 - textarea com opcao de numero de linhas (consoante o programa a desenvolver podem ser necessarias mais ou menos linhas)
@@ -38,27 +38,29 @@ import os.path @@ -38,27 +38,29 @@ import os.path
38 import logging 38 import logging
39 import sys 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 qlogger.setLevel(logging.INFO) 44 qlogger.setLevel(logging.INFO)
50 45
51 fh = logging.FileHandler('question.log') 46 fh = logging.FileHandler('question.log')
52 ch = logging.StreamHandler() 47 ch = logging.StreamHandler()
53 ch.setLevel(logging.INFO) 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 fh.setFormatter(formatter) 51 fh.setFormatter(formatter)
57 ch.setFormatter(formatter) 52 ch.setFormatter(formatter)
58 53
59 qlogger.addHandler(fh) 54 qlogger.addHandler(fh)
60 qlogger.addHandler(ch) 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 # if an error occurs in a question, the question is replaced by this message 64 # if an error occurs in a question, the question is replaced by this message
63 qerror = { 65 qerror = {
64 'filename': 'questions.py', 66 'filename': 'questions.py',
@@ -112,7 +114,7 @@ class QuestionsPool(dict): @@ -112,7 +114,7 @@ class QuestionsPool(dict):
112 qlogger.error('Error loading questions from YAML file "{0}". Skipping this one.'.format(filename)) 114 qlogger.error('Error loading questions from YAML file "{0}". Skipping this one.'.format(filename))
113 continue 115 continue
114 self.add(questions, filename, path) 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 #============================================================================
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 from os import path 7 from os import path
8 import sys 8 import sys
9 import argparse 9 import argparse
  10 +import logging
10 # from threading import Lock 11 # from threading import Lock
11 12
12 try: 13 try:
@@ -31,6 +32,13 @@ import test @@ -31,6 +32,13 @@ import test
31 import database 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 # Classes that respond to HTTP 44 # Classes that respond to HTTP
@@ -198,25 +206,35 @@ def parse_arguments(): @@ -198,25 +206,35 @@ def parse_arguments():
198 206
199 # ============================================================================ 207 # ============================================================================
200 if __name__ == '__main__': 208 if __name__ == '__main__':
  209 +
  210 + logger.error('---------- Running perguntations ----------')
  211 +
201 # --- parse command line arguments and build base test 212 # --- parse command line arguments and build base test
202 arg = parse_arguments() 213 arg = parse_arguments()
203 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) 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 cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) 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 cherrypy.log('Terminated OK ------------------------', 'APPLICATION') 238 cherrypy.log('Terminated OK ------------------------', 'APPLICATION')
221 - print('\n- Server terminated OK')  
222 - print('=' * 79) 239 + print()
  240 + logger.critical('-------- !!! Server terminated !!! --------')
@@ -2,21 +2,29 @@ @@ -2,21 +2,29 @@
2 import os, sys, fnmatch 2 import os, sys, fnmatch
3 import random 3 import random
4 import sqlite3 4 import sqlite3
  5 +import logging
5 from datetime import datetime 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 try: 16 try:
8 import yaml 17 import yaml
9 except ImportError: 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 sys.exit(1) 20 sys.exit(1)
12 21
13 try: 22 try:
14 import json 23 import json
15 except ImportError: 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 sys.exit(1) 26 sys.exit(1)
18 27
19 -  
20 # my code 28 # my code
21 import questions 29 import questions
22 import database 30 import database
@@ -28,7 +36,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -28,7 +36,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
28 try: 36 try:
29 f = open(filename, 'r', encoding='utf-8') 37 f = open(filename, 'r', encoding='utf-8')
30 except IOError: 38 except IOError:
31 - print('[ ERROR ] Cannot open YAML file "%s"' % filename) 39 + logger.critical('Cannot open YAML file "%s"' % filename)
32 sys.exit(1) 40 sys.exit(1)
33 else: 41 else:
34 with f: 42 with f:
@@ -36,7 +44,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -36,7 +44,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
36 test = yaml.load(f) 44 test = yaml.load(f)
37 except yaml.YAMLError as exc: 45 except yaml.YAMLError as exc:
38 mark = exc.problem_mark 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 sys.exit(1) 48 sys.exit(1)
41 # -- test yaml was loaded ok 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,39 +62,39 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
54 # this is the base directory where questions are stored 62 # this is the base directory where questions are stored
55 test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir)))) 63 test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir))))
56 if not os.path.exists(test['questions_dir']): 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 errors += 1 66 errors += 1
59 67
60 # where to put the students answers (optional) 68 # where to put the students answers (optional)
61 if 'answers_dir' not in test: 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 test['save_answers'] = False 71 test['save_answers'] = False
64 else: 72 else:
65 test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir']))) 73 test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir'])))
66 if not os.path.isdir(test['answers_dir']): 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 errors += 1 76 errors += 1
69 test['save_answers'] = True 77 test['save_answers'] = True
70 78
71 # database with login credentials and grades 79 # database with login credentials and grades
72 if 'database' not in test: 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 errors += 1 82 errors += 1
75 else: 83 else:
76 test['database'] = os.path.normpath(os.path.expanduser(str(test['database']))) 84 test['database'] = os.path.normpath(os.path.expanduser(str(test['database'])))
77 if not os.path.exists(test['database']): 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 errors += 1 87 errors += 1
80 88
81 if errors > 0: 89 if errors > 0:
82 - print('{0} error(s) found. Aborting!'.format(errors)) 90 + logger.critical('{0} error(s) found. Aborting!'.format(errors))
83 sys.exit(1) 91 sys.exit(1)
84 92
85 # deal with questions files 93 # deal with questions files
86 if 'files' not in test: 94 if 'files' not in test:
87 # no files were defined = load all from questions_dir 95 # no files were defined = load all from questions_dir
88 test['files'] = fnmatch.filter(os.listdir(test['questions_dir']), '*.yaml') 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 else: 98 else:
91 # only one file 99 # only one file
92 if isinstance(test['files'], str): 100 if isinstance(test['files'], str):
@@ -101,11 +109,14 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -101,11 +109,14 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
101 # contains only one element 109 # contains only one element
102 if isinstance(q, str): 110 if isinstance(q, str):
103 # normalize question to a dict 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 test['questions'][i][0]['points'] = 1.0 120 test['questions'][i][0]['points'] = 1.0
110 # Note: at this moment we do not know the questions types. 121 # Note: at this moment we do not know the questions types.
111 # Some questions, like information, should have default points 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,8 +125,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
114 125
115 elif isinstance(q, dict): 126 elif isinstance(q, dict):
116 if 'ref' not in q: 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 sys.exit(1) 129 sys.exit(1)
120 130
121 if isinstance(q['ref'], str): 131 if isinstance(q['ref'], str):
@@ -128,7 +138,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -128,7 +138,7 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
128 try: 138 try:
129 qq = pool[r] 139 qq = pool[r]
130 except KeyError: 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 continue 142 continue
133 qq['points'] = p 143 qq['points'] = p
134 l.append(qq) 144 l.append(qq)
@@ -148,8 +158,9 @@ class Test(dict): @@ -148,8 +158,9 @@ class Test(dict):
148 for i, qq in enumerate(self['questions']): 158 for i, qq in enumerate(self['questions']):
149 try: 159 try:
150 q = random.choice(qq) # select from alternative versions 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 qlist.append(questions.create_question(q)) # create instance 164 qlist.append(questions.create_question(q)) # create instance
154 self['questions'] = qlist 165 self['questions'] = qlist
155 self['start_time'] = datetime.now() 166 self['start_time'] = datetime.now()