Commit f9a2254fba2acfcbb0c9396fc27b606f248c998e
1 parent
4da6fa98
Exists in
master
and in
1 other branch
- 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() | ... | ... |