Commit 704d1d69b38b507dab68a6db6f9b28b02491a77c
1 parent
4feaaba5
Exists in
master
and in
1 other branch
- all logging configurations in a config/logger.yaml file.
- new 'alert' type of question. - commandline 'debug' argument partially working (still lacks loglevel setup). - information type questions use 'title' instead of hardcoded "Atenção".
Showing
7 changed files
with
45 additions
and
75 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | -- alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) | ||
| 5 | -- configuracao dos logs cherrypy para se darem bem com os outros | ||
| 6 | -- browser e ip usados gravado no test. | 4 | +- argumentos da linha de comando a funcionar. |
| 7 | - usar thread.Lock para aceder a variaveis de estado. | 5 | - usar thread.Lock para aceder a variaveis de estado. |
| 8 | - permitir adicionar imagens nas perguntas. | 6 | - permitir adicionar imagens nas perguntas. |
| 9 | -- argumentos da linha de comando a funcionar. | ||
| 10 | 7 | ||
| 11 | # TODO | 8 | # TODO |
| 12 | 9 | ||
| 13 | - implementar practice mode. | 10 | - implementar practice mode. |
| 14 | -- botões allow all/deny all. | ||
| 15 | - enviar logs para web? | 11 | - enviar logs para web? |
| 16 | - SQLAlchemy em vez da classe database. | 12 | - SQLAlchemy em vez da classe database. |
| 13 | +- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | ||
| 17 | - aviso na pagina principal para quem usa browser da treta | 14 | - aviso na pagina principal para quem usa browser da treta |
| 18 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. | 15 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. |
| 19 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput | 16 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput |
| 20 | - perguntas para professor corrigir mais tarde. | 17 | - perguntas para professor corrigir mais tarde. |
| 21 | -- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | ||
| 22 | - visualizar um teste ja realizado na página de administração | 18 | - visualizar um teste ja realizado na página de administração |
| 23 | -- Menu para professor com link para /results e /students | ||
| 24 | - fazer uma calculadora javascript e por no menu. surge como modal | 19 | - fazer uma calculadora javascript e por no menu. surge como modal |
| 25 | - GeoIP? | 20 | - GeoIP? |
| 26 | -- mostrar botão de reset apenas no final da pagina, com edit para escrever o número. | 21 | +- alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) |
| 27 | 22 | ||
| 28 | # FIXED | 23 | # FIXED |
| 29 | 24 | ||
| 25 | +- configuracao dos logs cherrypy para se darem bem com os outros | ||
| 26 | +- browser e ip usados gravado no test. | ||
| 27 | +- botões allow all/deny all. | ||
| 28 | +- mostrar botão de reset apenas no final da pagina, com edit para escrever o número. | ||
| 30 | - aluno faz login, mas fecha browser, ficando no estado (online,deny). Ao tentar login com outro browser está deny e o prof não consegue pô-lo em allow pois já não está na lista. => solucao é manter todos os alunos numa tabela. | 29 | - aluno faz login, mas fecha browser, ficando no estado (online,deny). Ao tentar login com outro browser está deny e o prof não consegue pô-lo em allow pois já não está na lista. => solucao é manter todos os alunos numa tabela. |
| 31 | - pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static... | 30 | - pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static... |
| 32 | - Não mostrar Professor nos activos em /admin | 31 | - Não mostrar Professor nos activos em /admin |
app.py
| @@ -3,12 +3,14 @@ | @@ -3,12 +3,14 @@ | ||
| 3 | import logging | 3 | import logging |
| 4 | from os import path | 4 | from os import path |
| 5 | import sqlite3 | 5 | import sqlite3 |
| 6 | - | ||
| 7 | import bcrypt | 6 | import bcrypt |
| 8 | 7 | ||
| 9 | import test | 8 | import test |
| 10 | import database | 9 | import database |
| 11 | 10 | ||
| 11 | +logger = logging.getLogger(__name__) | ||
| 12 | + | ||
| 13 | + | ||
| 12 | # ============================================================================ | 14 | # ============================================================================ |
| 13 | # Application | 15 | # Application |
| 14 | # ============================================================================ | 16 | # ============================================================================ |
| @@ -23,7 +25,7 @@ class App(object): | @@ -23,7 +25,7 @@ class App(object): | ||
| 23 | # } | 25 | # } |
| 24 | logger.info('============= Running perguntations =============') | 26 | logger.info('============= Running perguntations =============') |
| 25 | self.online = dict() # {uid: {'student':{}}} | 27 | self.online = dict() # {uid: {'student':{}}} |
| 26 | - self.allowed = set([]) # '0' is hardcoded to allowed elsewhere FIXME | 28 | + self.allowed = set([]) # '0' is hardcoded to allowed elsewhere |
| 27 | self.testfactory = test.TestFactory(filename, conf=conf) | 29 | self.testfactory = test.TestFactory(filename, conf=conf) |
| 28 | self.db = database.Database(self.testfactory['database']) # FIXME | 30 | self.db = database.Database(self.testfactory['database']) # FIXME |
| 29 | try: | 31 | try: |
| @@ -186,12 +188,3 @@ class App(object): | @@ -186,12 +188,3 @@ class App(object): | ||
| 186 | 188 | ||
| 187 | def set_user_ip(self, uid, ipaddress=''): | 189 | def set_user_ip(self, uid, ipaddress=''): |
| 188 | self.online[uid]['student']['ip_address'] = ipaddress | 190 | self.online[uid]['student']['ip_address'] = ipaddress |
| 189 | - | ||
| 190 | -# ============================================================================ | ||
| 191 | -ch = logging.StreamHandler() | ||
| 192 | -ch.setLevel(logging.INFO) | ||
| 193 | -ch.setFormatter(logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s')) | ||
| 194 | - | ||
| 195 | -logger = logging.getLogger(__name__) | ||
| 196 | -logger.setLevel(logging.INFO) | ||
| 197 | -logger.addHandler(ch) |
config/server.conf
| @@ -19,11 +19,3 @@ server.socket_port = 8080 | @@ -19,11 +19,3 @@ server.socket_port = 8080 | ||
| 19 | 19 | ||
| 20 | ; not required for snakeoil: | 20 | ; not required for snakeoil: |
| 21 | ; server.ssl_certificate_chain = 'ca_certs.crt' | 21 | ; server.ssl_certificate_chain = 'ca_certs.crt' |
| 22 | - | ||
| 23 | -# add path to the log files here. empty strings disable logging | ||
| 24 | -; log.error_file = 'logs/errors.log' | ||
| 25 | -; log.access_file = 'logs/access.log' | ||
| 26 | -log.error_file = '' | ||
| 27 | -log.access_file = '' | ||
| 28 | -log.screen = False | ||
| 29 | - |
questions.py
| @@ -35,18 +35,6 @@ import sys | @@ -35,18 +35,6 @@ import sys | ||
| 35 | 35 | ||
| 36 | # setup logger for this module | 36 | # setup logger for this module |
| 37 | logger = logging.getLogger(__name__) | 37 | logger = logging.getLogger(__name__) |
| 38 | -logger.setLevel(logging.INFO) | ||
| 39 | - | ||
| 40 | -# fh = logging.FileHandler('question.log') | ||
| 41 | -ch = logging.StreamHandler() | ||
| 42 | -ch.setLevel(logging.INFO) | ||
| 43 | - | ||
| 44 | -formatter = logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s') | ||
| 45 | -# fh.setFormatter(formatter) | ||
| 46 | -ch.setFormatter(formatter) | ||
| 47 | - | ||
| 48 | -# logger.addHandler(fh) | ||
| 49 | -logger.addHandler(ch) | ||
| 50 | 38 | ||
| 51 | try: | 39 | try: |
| 52 | import yaml | 40 | import yaml |
| @@ -168,8 +156,10 @@ class QuestionFactory(dict): | @@ -168,8 +156,10 @@ class QuestionFactory(dict): | ||
| 168 | 'text' : QuestionText, | 156 | 'text' : QuestionText, |
| 169 | 'text_regex': QuestionTextRegex, | 157 | 'text_regex': QuestionTextRegex, |
| 170 | 'textarea' : QuestionTextArea, | 158 | 'textarea' : QuestionTextArea, |
| 159 | + # informative panels | ||
| 171 | 'information': QuestionInformation, | 160 | 'information': QuestionInformation, |
| 172 | 'warning' : QuestionInformation, | 161 | 'warning' : QuestionInformation, |
| 162 | + 'alert' : QuestionInformation, | ||
| 173 | } | 163 | } |
| 174 | 164 | ||
| 175 | # Shallow copy so that script generated questions will not replace | 165 | # Shallow copy so that script generated questions will not replace |
serve.py
| @@ -5,9 +5,10 @@ | @@ -5,9 +5,10 @@ | ||
| 5 | from os import path | 5 | from os import path |
| 6 | import sys | 6 | import sys |
| 7 | import argparse | 7 | import argparse |
| 8 | -# import logging | 8 | +import logging.config |
| 9 | import html | 9 | import html |
| 10 | import json | 10 | import json |
| 11 | +import yaml | ||
| 11 | 12 | ||
| 12 | try: | 13 | try: |
| 13 | import cherrypy | 14 | import cherrypy |
| @@ -17,8 +18,6 @@ except ImportError: | @@ -17,8 +18,6 @@ except ImportError: | ||
| 17 | print('Some python packages are missing. See README.md for instructions.') | 18 | print('Some python packages are missing. See README.md for instructions.') |
| 18 | sys.exit(1) | 19 | sys.exit(1) |
| 19 | 20 | ||
| 20 | -# my code | ||
| 21 | -from app import App | ||
| 22 | 21 | ||
| 23 | # ============================================================================ | 22 | # ============================================================================ |
| 24 | # Authentication | 23 | # Authentication |
| @@ -219,33 +218,32 @@ def parse_arguments(): | @@ -219,33 +218,32 @@ def parse_arguments(): | ||
| 219 | argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.') | 218 | argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.') |
| 220 | serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) | 219 | serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) |
| 221 | argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file') | 220 | argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file') |
| 222 | - # argparser.add_argument('--debug', action='store_true', | ||
| 223 | - # help='Show datastructures when rendering questions') | ||
| 224 | - # argparser.add_argument('--show_ref', action='store_true', | ||
| 225 | - # help='Show filename and ref field for each question') | ||
| 226 | - # argparser.add_argument('--show_points', action='store_true', | ||
| 227 | - # help='Show normalized points for each question') | ||
| 228 | - # argparser.add_argument('--show_hints', action='store_true', | ||
| 229 | - # help='Show hints in questions, if available') | ||
| 230 | - # argparser.add_argument('--save_answers', action='store_true', | ||
| 231 | - # help='Saves answers in JSON format') | ||
| 232 | - # argparser.add_argument('--practice', action='store_true', | ||
| 233 | - # help='Show correction results and allow repetitive resubmission of the test') | 221 | + argparser.add_argument('--debug', action='store_true', |
| 222 | + help='Show datastructures when rendering questions') | ||
| 234 | argparser.add_argument('testfile', type=str, nargs='+', help='test/exam in YAML format.') # FIXME only one exam supported at the moment | 223 | argparser.add_argument('testfile', type=str, nargs='+', help='test/exam in YAML format.') # FIXME only one exam supported at the moment |
| 235 | return argparser.parse_args() | 224 | return argparser.parse_args() |
| 236 | 225 | ||
| 237 | # ============================================================================ | 226 | # ============================================================================ |
| 238 | if __name__ == '__main__': | 227 | if __name__ == '__main__': |
| 239 | 228 | ||
| 240 | - | ||
| 241 | - # --- path where this file is located | ||
| 242 | SERVER_PATH = path.dirname(path.realpath(__file__)) | 229 | SERVER_PATH = path.dirname(path.realpath(__file__)) |
| 243 | TEMPLATES_DIR = path.join(SERVER_PATH, 'templates') | 230 | TEMPLATES_DIR = path.join(SERVER_PATH, 'templates') |
| 231 | + LOGGER_CONF = path.join(SERVER_PATH, 'config/logger.yaml') | ||
| 244 | SESSION_KEY = 'userid' | 232 | SESSION_KEY = 'userid' |
| 245 | 233 | ||
| 246 | # --- parse command line arguments and build base test | 234 | # --- parse command line arguments and build base test |
| 247 | arg = parse_arguments() | 235 | arg = parse_arguments() |
| 248 | 236 | ||
| 237 | + if arg.debug: | ||
| 238 | + LOGGER_CONF = path.join(SERVER_PATH, 'config/logger-debug.yaml') | ||
| 239 | + | ||
| 240 | + # --- Setup logging | ||
| 241 | + with open(LOGGER_CONF,'r') as f: | ||
| 242 | + logging.config.dictConfig(yaml.load(f)) | ||
| 243 | + | ||
| 244 | + # --- start application | ||
| 245 | + from app import App | ||
| 246 | + | ||
| 249 | # FIXME do not send args that were not defined in the commandline | 247 | # FIXME do not send args that were not defined in the commandline |
| 250 | # this means options should be like --show-ref=true|false | 248 | # this means options should be like --show-ref=true|false |
| 251 | # and have no default value | 249 | # and have no default value |
| @@ -292,14 +290,11 @@ if __name__ == '__main__': | @@ -292,14 +290,11 @@ if __name__ == '__main__': | ||
| 292 | }, | 290 | }, |
| 293 | } | 291 | } |
| 294 | 292 | ||
| 295 | - # --- app specific configuration | 293 | + cherrypy.engine.unsubscribe('graceful', cherrypy.log.reopen_files) # FIXME what's this? |
| 296 | 294 | ||
| 295 | + # --- Start server | ||
| 297 | cherrypy.tree.mount(webapp, script_name='/', config=conf) | 296 | cherrypy.tree.mount(webapp, script_name='/', config=conf) |
| 298 | 297 | ||
| 299 | - # logger.info('Webserver listening at {}:{}'.format( | ||
| 300 | - # cherrypy.config['server.socket_host'], | ||
| 301 | - # cherrypy.config['server.socket_port'])) | ||
| 302 | - | ||
| 303 | if hasattr(cherrypy.engine, "signal_handler"): | 298 | if hasattr(cherrypy.engine, "signal_handler"): |
| 304 | cherrypy.engine.signal_handler.subscribe() | 299 | cherrypy.engine.signal_handler.subscribe() |
| 305 | if hasattr(cherrypy.engine, "console_control_handler"): | 300 | if hasattr(cherrypy.engine, "console_control_handler"): |
| @@ -309,4 +304,3 @@ if __name__ == '__main__': | @@ -309,4 +304,3 @@ if __name__ == '__main__': | ||
| 309 | cherrypy.engine.block() | 304 | cherrypy.engine.block() |
| 310 | # ...App running... | 305 | # ...App running... |
| 311 | app.exit() | 306 | app.exit() |
| 312 | - cherrypy.log('Terminated OK ------------------------', 'APPLICATION') |
templates/test.html
| @@ -124,19 +124,27 @@ | @@ -124,19 +124,27 @@ | ||
| 124 | % for i,q in enumerate(t['questions']): | 124 | % for i,q in enumerate(t['questions']): |
| 125 | <div class="ui-corner-all custom-corners"> | 125 | <div class="ui-corner-all custom-corners"> |
| 126 | % if q['type'] == 'information': | 126 | % if q['type'] == 'information': |
| 127 | - <div class="alert alert-warning drop-shadow" role="alert"> | 127 | + <div class="alert alert-info drop-shadow" role="alert"> |
| 128 | <h4> | 128 | <h4> |
| 129 | - <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> | ||
| 130 | - ${q['title']} | 129 | + <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> ${q['title']} |
| 131 | </h4> | 130 | </h4> |
| 132 | <p> | 131 | <p> |
| 133 | ${pretty(q['text'])} | 132 | ${pretty(q['text'])} |
| 134 | </p> | 133 | </p> |
| 135 | </div> | 134 | </div> |
| 136 | % elif q['type'] == 'warning': | 135 | % elif q['type'] == 'warning': |
| 136 | + <div class="alert alert-warning drop-shadow" role="alert"> | ||
| 137 | + <h4> | ||
| 138 | + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ${q['title']} | ||
| 139 | + </h4> | ||
| 140 | + <p> | ||
| 141 | + ${pretty(q['text'])} | ||
| 142 | + </p> | ||
| 143 | + </div> | ||
| 144 | + % elif q['type'] == 'alert': | ||
| 137 | <div class="alert alert-danger drop-shadow" role="alert"> | 145 | <div class="alert alert-danger drop-shadow" role="alert"> |
| 138 | <h4> | 146 | <h4> |
| 139 | - <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> Atenção | 147 | + <span class="glyphicon glyphicon-alert" aria-hidden="true"></span> ${q['title']} |
| 140 | </h4> | 148 | </h4> |
| 141 | <p> | 149 | <p> |
| 142 | ${pretty(q['text'])} | 150 | ${pretty(q['text'])} |
| @@ -255,9 +263,9 @@ | @@ -255,9 +263,9 @@ | ||
| 255 | <div class="panel-footer"> | 263 | <div class="panel-footer"> |
| 256 | 264 | ||
| 257 | % if t['debug']: | 265 | % if t['debug']: |
| 258 | - <pre> | 266 | + <pre><code> |
| 259 | ${yaml.dump(q)} | 267 | ${yaml.dump(q)} |
| 260 | - </pre> | 268 | + </code></pre> |
| 261 | % endif | 269 | % endif |
| 262 | 270 | ||
| 263 | % if t['show_ref']: | 271 | % if t['show_ref']: |
test.py
| @@ -8,12 +8,6 @@ import logging | @@ -8,12 +8,6 @@ import logging | ||
| 8 | 8 | ||
| 9 | # Logger configuration | 9 | # Logger configuration |
| 10 | logger = logging.getLogger(__name__) | 10 | logger = logging.getLogger(__name__) |
| 11 | -logger.setLevel(logging.INFO) | ||
| 12 | - | ||
| 13 | -ch = logging.StreamHandler() | ||
| 14 | -ch.setLevel(logging.INFO) | ||
| 15 | -ch.setFormatter(logging.Formatter('%(asctime)s | %(name)-10s | %(levelname)-8s | %(message)s')) | ||
| 16 | -logger.addHandler(ch) | ||
| 17 | 11 | ||
| 18 | try: | 12 | try: |
| 19 | import yaml | 13 | import yaml |
| @@ -188,7 +182,7 @@ class TestFactory(dict): | @@ -188,7 +182,7 @@ class TestFactory(dict): | ||
| 188 | q = self.question_factory.generate(random.choice(qq['ref'])) | 182 | q = self.question_factory.generate(random.choice(qq['ref'])) |
| 189 | 183 | ||
| 190 | # some defaults | 184 | # some defaults |
| 191 | - if q['type'] in ('information', 'warning'): | 185 | + if q['type'] in ('information', 'warning', 'alert'): |
| 192 | q['points'] = qq.get('points', 0.0) | 186 | q['points'] = qq.get('points', 0.0) |
| 193 | else: | 187 | else: |
| 194 | q['title'] = '{}. '.format(n) + q['title'] | 188 | q['title'] = '{}. '.format(n) + q['title'] |