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 | # 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) |
questions.py
@@ -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 | #============================================================================ |
serve.py
@@ -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 !!! --------') |
test.py
@@ -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() |