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