Commit a566a5d6395366df475d15df76969fcc14f0c0ba

Authored by Miguel Barão
1 parent 255d1724
Exists in master and in 1 other branch dev

refactoring. split serve.py into serve.py and main.py

commandline options: --version and --port
1 1
2 # BUGS 2 # BUGS
3 3
  4 +- quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20
  5 +- servidor ntpd para configurar a data/hora dos portateis dell
  6 +- link na pagina com a nota para voltar ao principio.
  7 +- CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?)
  8 +- sock.bind(sockaddr) OSError: [Errno 48] Address already in use
4 - na pagina grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao do realizado. 9 - na pagina grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao do realizado.
5 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>. 10 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>.
6 - teste nao esta a mostrar imagens de vez em quando. 11 - teste nao esta a mostrar imagens de vez em quando.
@@ -16,6 +21,7 @@ ou usar push (websockets?) @@ -16,6 +21,7 @@ ou usar push (websockets?)
16 - Test.reset_answers() unused. 21 - Test.reset_answers() unused.
17 - mudar ref do test para test_id (ref já é usado nas perguntas) 22 - mudar ref do test para test_id (ref já é usado nas perguntas)
18 - incluir test_id na tabela questions (futuro semestre, pode quebrar compatibilidade). 23 - incluir test_id na tabela questions (futuro semestre, pode quebrar compatibilidade).
  24 +- na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenha excedido o tempo
19 25
20 # TODO 26 # TODO
21 27
@@ -42,7 +42,7 @@ Download and install: @@ -42,7 +42,7 @@ Download and install:
42 git clone https://git.xdi.uevora.pt/perguntations.git 42 git clone https://git.xdi.uevora.pt/perguntations.git
43 cd perguntations 43 cd perguntations
44 npm install 44 npm install
45 -pip3 install . # this must come last 45 +pip3 install .
46 ``` 46 ```
47 47
48 The command `npm` installs the javascript libraries and then `pip3` installs the python webserver. This will also install any required dependencies. 48 The command `npm` installs the javascript libraries and then `pip3` installs the python webserver. This will also install any required dependencies.
demo/demo.yaml
@@ -7,7 +7,7 @@ ref: tutorial @@ -7,7 +7,7 @@ ref: tutorial
7 7
8 # Database with student credentials and grades of all questions and tests done 8 # Database with student credentials and grades of all questions and tests done
9 # The database is an sqlite3 file generate with the command initdb 9 # The database is an sqlite3 file generate with the command initdb
10 -database: ../demo/students.db 10 +database: students.db
11 11
12 # Directory where the tests including submitted answers and grades are stored. 12 # Directory where the tests including submitted answers and grades are stored.
13 # The submitted tests and their corrections can be reviewed later. 13 # The submitted tests and their corrections can be reviewed later.
@@ -64,3 +64,20 @@ questions: @@ -64,3 +64,20 @@ questions:
64 - tut-success 64 - tut-success
65 - tut-warning 65 - tut-warning
66 - tut-alert 66 - tut-alert
  67 +
  68 +
  69 +# test:
  70 +# - ref1
  71 +# - block: a
  72 +# - block: [b, c]
  73 +# - ref2
  74 +
  75 +# blocks:
  76 +# a:
  77 +# - ref1
  78 +# - ref2
  79 +# - ref3
  80 +# b:
  81 +# - rr4
  82 +# - rr5
  83 +# - rr6
demo/questions/questions-tutorial.yaml
@@ -180,8 +180,8 @@ @@ -180,8 +180,8 @@
180 A solução correcta é a **Opção 0** ou a **Opção 1**. 180 A solução correcta é a **Opção 0** ou a **Opção 1**.
181 181
182 # ---------------------------------------------------------------------------- 182 # ----------------------------------------------------------------------------
183 -- ref: tut-checkbox  
184 - type: checkbox 183 +- type: checkbox
  184 + ref: tut-checkbox
185 title: Escolha múltipla, várias opções correctas 185 title: Escolha múltipla, várias opções correctas
186 text: | 186 text: |
187 As perguntas de escolha múltipla permitem apresentar um conjunto de opções 187 As perguntas de escolha múltipla permitem apresentar um conjunto de opções
@@ -359,7 +359,7 @@ @@ -359,7 +359,7 @@
359 ou em vírgula flutuante, como em `0.23`, `1e-3`. 359 ou em vírgula flutuante, como em `0.23`, `1e-3`.
360 correct: [3.14, 3.15] 360 correct: [3.14, 3.15]
361 solution: | 361 solution: |
362 - Sabems que $\pi\approx 3.14159265359$. 362 + Sabemos que $\pi\approx 3.14159265359$.
363 Portanto, um exemplo de uma resposta correcta é `3.1416`. 363 Portanto, um exemplo de uma resposta correcta é `3.1416`.
364 364
365 # --------------------------------------------------------------------------- 365 # ---------------------------------------------------------------------------
@@ -556,6 +556,7 @@ @@ -556,6 +556,7 @@
556 This question is not included in the test and will not shown up. 556 This question is not included in the test and will not shown up.
557 It also lacks a "ref" and is automatically named 557 It also lacks a "ref" and is automatically named
558 `questions/questions-tutorial.yaml:0012`. 558 `questions/questions-tutorial.yaml:0012`.
  559 + A warning is shown on the console about this.
559 The number at the end is the index position of this question. 560 The number at the end is the index position of this question.
560 Indices start at 0. 561 Indices start at 0.
561 562
perguntations/app.py
@@ -43,6 +43,14 @@ async def hash_password(pw): @@ -43,6 +43,14 @@ async def hash_password(pw):
43 43
44 # ============================================================================ 44 # ============================================================================
45 # Application 45 # Application
  46 +# state:
  47 +# self.Session
  48 +# self.online - {uid:
  49 +# {'student':{...}, 'test': {...}},
  50 +# ...
  51 +# }
  52 +# self.allowd - {'123', '124', ...}
  53 +# self.testfactory - TestFactory
46 # ============================================================================ 54 # ============================================================================
47 class App(object): 55 class App(object):
48 # ------------------------------------------------------------------------ 56 # ------------------------------------------------------------------------
@@ -61,12 +69,12 @@ class App(object): @@ -61,12 +69,12 @@ class App(object):
61 session.close() 69 session.close()
62 70
63 # ------------------------------------------------------------------------ 71 # ------------------------------------------------------------------------
64 - def __init__(self, conf={}): 72 + def __init__(self, conf):
65 self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...} 73 self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...}
66 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere 74 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere
67 75
68 - logger.info(f'Loading test configuration "{conf["filename"]}".')  
69 - testconf = load_yaml(conf['filename']) 76 + logger.info(f'Loading test configuration "{conf["testfile"]}".')
  77 + testconf = load_yaml(conf['testfile'])
70 testconf.update(conf) # command line options override configuration 78 testconf.update(conf) # command line options override configuration
71 79
72 # start test factory 80 # start test factory
@@ -89,13 +97,15 @@ class App(object): @@ -89,13 +97,15 @@ class App(object):
89 except Exception: 97 except Exception:
90 raise AppException(f'Database unusable {dbfile}.') 98 raise AppException(f'Database unusable {dbfile}.')
91 else: 99 else:
92 - logger.info(f'Database {dbfile} has {n} students.') 100 + logger.info(f'Database "{dbfile}" has {n} students.')
93 101
94 # command line option --allow-all 102 # command line option --allow-all
95 if conf['allow_all']: 103 if conf['allow_all']:
96 logger.info('Allowing all students:') 104 logger.info('Allowing all students:')
97 for student in self.get_all_students(): 105 for student in self.get_all_students():
98 self.allow_student(student[0]) 106 self.allow_student(student[0])
  107 + else:
  108 + logger.info('Students not yet allowed to login.')
99 109
100 # ------------------------------------------------------------------------ 110 # ------------------------------------------------------------------------
101 # FIXME unused??? 111 # FIXME unused???
perguntations/main.py 0 → 100644
@@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +# python standard library
  4 +import argparse
  5 +import logging
  6 +import os
  7 +from os import environ, path
  8 +import ssl
  9 +import sys
  10 +# from typing import Any, Dict
  11 +
  12 +# this project
  13 +from .app import App
  14 +from .serve import run_webserver
  15 +from .tools import load_yaml
  16 +from . import APP_NAME, APP_VERSION
  17 +
  18 +
  19 +# ----------------------------------------------------------------------------
  20 +def parse_cmdline_arguments():
  21 + parser = argparse.ArgumentParser(
  22 + description='Server for online tests. Enrolled students and tests '
  23 + 'have to be previously configured. Please read the documentation '
  24 + 'included with this software before running the server.')
  25 + parser.add_argument('testfile',
  26 + type=str, nargs='?', # FIXME only one test supported
  27 + help='tests in YAML format')
  28 + parser.add_argument('--allow-all',
  29 + action='store_true',
  30 + help='Allow all students to login immediately')
  31 + parser.add_argument('--debug',
  32 + action='store_true',
  33 + help='Enable debug messages')
  34 + parser.add_argument('--show-ref',
  35 + action='store_true',
  36 + help='Show question references')
  37 + parser.add_argument('--review',
  38 + action='store_true',
  39 + help='Review mode: doesn\'t generate test')
  40 + parser.add_argument('--port',
  41 + type=int, default=8443,
  42 + help='port for the HTTPS server (default: 8443)')
  43 + parser.add_argument('-v', '--version', action='store_true',
  44 + help='Show version information and exit')
  45 + return parser.parse_args()
  46 +
  47 +
  48 +# ----------------------------------------------------------------------------
  49 +def get_logger_config(debug=False):
  50 + if debug:
  51 + filename = 'logger-debug.yaml'
  52 + level = 'DEBUG'
  53 + else:
  54 + filename = 'logger.yaml'
  55 + level = 'INFO'
  56 +
  57 + config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
  58 + config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)
  59 +
  60 + default_config = {
  61 + 'version': 1,
  62 + 'formatters': {
  63 + 'standard': {
  64 + 'format': '%(asctime)s %(levelname)-8s %(message)s',
  65 + 'datefmt': '%H:%M',
  66 + },
  67 + },
  68 + 'handlers': {
  69 + 'default': {
  70 + 'level': level,
  71 + 'class': 'logging.StreamHandler',
  72 + 'formatter': 'standard',
  73 + 'stream': 'ext://sys.stdout',
  74 + },
  75 + },
  76 + 'loggers': {
  77 + '': { # configuration for serve.py
  78 + 'handlers': ['default'],
  79 + 'level': level,
  80 + },
  81 + },
  82 + }
  83 + default_config['loggers'].update({
  84 + APP_NAME+'.'+module: {
  85 + 'handlers': ['default'],
  86 + 'level': level,
  87 + 'propagate': False,
  88 + } for module in ['app', 'models', 'factory', 'questions',
  89 + 'test', 'tools']})
  90 +
  91 + return load_yaml(config_file, default=default_config)
  92 +
  93 +
  94 +# ----------------------------------------------------------------------------
  95 +# Tornado web server
  96 +# ----------------------------------------------------------------------------
  97 +def main():
  98 + args = parse_cmdline_arguments()
  99 +
  100 + if args.version:
  101 + print(f'{APP_NAME} {APP_VERSION}\nPython {sys.version}')
  102 + sys.exit(0)
  103 +
  104 + # --- Setup logging ------------------------------------------------------
  105 + logging.config.dictConfig(get_logger_config(args.debug))
  106 + logging.info('====================== Start Logging ======================')
  107 +
  108 + # --- start application --------------------------------------------------
  109 + config = {
  110 + 'testfile': args.testfile,
  111 + 'debug': args.debug,
  112 + 'allow_all': args.allow_all,
  113 + 'show_ref': args.show_ref,
  114 + 'review': args.review,
  115 + }
  116 +
  117 + # testapp = App(config)
  118 + try:
  119 + testapp = App(config)
  120 + except Exception:
  121 + logging.critical('Failed to start application.')
  122 + sys.exit(-1)
  123 +
  124 + # --- get SSL certificates -----------------------------------------------
  125 + if 'XDG_DATA_HOME' in os.environ:
  126 + certs_dir = path.join(os.environ['XDG_DATA_HOME'], 'certs')
  127 + else:
  128 + certs_dir = path.expanduser('~/.local/share/certs')
  129 +
  130 + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  131 + try:
  132 + ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),
  133 + path.join(certs_dir, 'privkey.pem'))
  134 + except FileNotFoundError:
  135 + logging.critical(f'SSL certificates missing in {certs_dir}')
  136 + sys.exit(-1)
  137 +
  138 + # --- run webserver ----------------------------------------------------
  139 + run_webserver(app=testapp, ssl=ssl_ctx, port=args.port, debug=args.debug)
  140 +
  141 +
  142 +# ----------------------------------------------------------------------------
  143 +if __name__ == "__main__":
  144 + main()
perguntations/serve.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 # python standard library 3 # python standard library
4 -import os  
5 from os import path 4 from os import path
6 import sys 5 import sys
7 import base64 6 import base64
@@ -21,9 +20,6 @@ import tornado.web @@ -21,9 +20,6 @@ import tornado.web
21 import tornado.httpserver 20 import tornado.httpserver
22 21
23 # this project 22 # this project
24 -from perguntations.app import App, AppException  
25 -from perguntations.tools import load_yaml  
26 -from perguntations import APP_NAME  
27 from perguntations.parser_markdown import md_to_html 23 from perguntations.parser_markdown import md_to_html
28 24
29 25
@@ -159,7 +155,7 @@ class AdminHandler(BaseHandler): @@ -159,7 +155,7 @@ class AdminHandler(BaseHandler):
159 'data': { 155 'data': {
160 'title': self.testapp.testfactory['title'], 156 'title': self.testapp.testfactory['title'],
161 'ref': self.testapp.testfactory['ref'], 157 'ref': self.testapp.testfactory['ref'],
162 - 'filename': self.testapp.testfactory['filename'], 158 + 'filename': self.testapp.testfactory['testfile'],
163 'database': self.testapp.testfactory['database'], 159 'database': self.testapp.testfactory['database'],
164 'answers_dir': self.testapp.testfactory['answers_dir'], 160 'answers_dir': self.testapp.testfactory['answers_dir'],
165 } 161 }
@@ -390,153 +386,44 @@ class ReviewHandler(BaseHandler): @@ -390,153 +386,44 @@ class ReviewHandler(BaseHandler):
390 templ=self._templates) 386 templ=self._templates)
391 387
392 388
  389 +
393 # ---------------------------------------------------------------------------- 390 # ----------------------------------------------------------------------------
394 def signal_handler(signal, frame): 391 def signal_handler(signal, frame):
395 r = input(' --> Stop webserver? (yes/no) ') 392 r = input(' --> Stop webserver? (yes/no) ')
396 - if r in ('yes', 'YES'): 393 + if r.lower() == 'yes':
397 tornado.ioloop.IOLoop.current().stop() 394 tornado.ioloop.IOLoop.current().stop()
398 logging.critical('Webserver stopped.') 395 logging.critical('Webserver stopped.')
399 sys.exit(0) 396 sys.exit(0)
400 397
401 -  
402 # ---------------------------------------------------------------------------- 398 # ----------------------------------------------------------------------------
403 -def parse_cmdline_arguments():  
404 - parser = argparse.ArgumentParser(  
405 - description='Server for online tests. Enrolled students and tests '  
406 - 'have to be previously configured. Please read the documentation '  
407 - 'included with this software before running the server.')  
408 - parser.add_argument('testfile',  
409 - type=str, nargs='+', # FIXME only one test supported  
410 - help='test configuration in YAML format')  
411 - parser.add_argument('--allow-all',  
412 - action='store_true',  
413 - help='Allow all students to login immediately')  
414 - parser.add_argument('--debug',  
415 - action='store_true',  
416 - help='Enable debug messages')  
417 - parser.add_argument('--show-ref',  
418 - action='store_true',  
419 - help='Show question references')  
420 - parser.add_argument('--review',  
421 - action='store_true',  
422 - help='Review mode: doesn\'t generate test')  
423 - return parser.parse_args()  
424 -  
425 -  
426 -# ----------------------------------------------------------------------------  
427 -def get_logger_config(debug=False):  
428 - if debug:  
429 - filename = 'logger-debug.yaml'  
430 - level = 'DEBUG'  
431 - else:  
432 - filename = 'logger.yaml'  
433 - level = 'INFO'  
434 -  
435 - config_dir = os.environ.get('XDG_CONFIG_HOME', '~/.config/')  
436 - config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)  
437 -  
438 - default_config = {  
439 - 'version': 1,  
440 - 'formatters': {  
441 - 'standard': {  
442 - 'format': '%(asctime)s %(levelname)-8s %(message)s',  
443 - 'datefmt': '%H:%M',  
444 - },  
445 - },  
446 - 'handlers': {  
447 - 'default': {  
448 - 'level': level,  
449 - 'class': 'logging.StreamHandler',  
450 - 'formatter': 'standard',  
451 - 'stream': 'ext://sys.stdout',  
452 - },  
453 - },  
454 - 'loggers': {  
455 - '': { # configuration for serve.py  
456 - 'handlers': ['default'],  
457 - 'level': level,  
458 - },  
459 - },  
460 - }  
461 - default_config['loggers'].update({  
462 - APP_NAME+'.'+module: {  
463 - 'handlers': ['default'],  
464 - 'level': level,  
465 - 'propagate': False,  
466 - } for module in ['app', 'models', 'factory', 'questions',  
467 - 'test', 'tools']})  
468 -  
469 - return load_yaml(config_file, default=default_config)  
470 -  
471 -  
472 -# ----------------------------------------------------------------------------  
473 -# Tornado web server  
474 -# ----------------------------------------------------------------------------  
475 -def main():  
476 - args = parse_cmdline_arguments()  
477 -  
478 - # --- Setup logging  
479 - logging.config.dictConfig(get_logger_config(args.debug))  
480 - logging.info('====================== Start Logging ======================')  
481 -  
482 - # --- start application  
483 - config = {  
484 - 'filename': args.testfile[0] or '',  
485 - 'debug': args.debug,  
486 - 'allow_all': args.allow_all,  
487 - 'show_ref': args.show_ref,  
488 - 'review': args.review,  
489 - }  
490 - 399 +def run_webserver(app, ssl, port, debug):
  400 + # --- create web application ---------------------------------------------
  401 + logging.info('Starting WebApplication (tornado)')
491 try: 402 try:
492 - testapp = App(config)  
493 - except Exception:  
494 - logging.critical('Failed to start application.')  
495 - sys.exit(-1)  
496 -  
497 - # --- create web application  
498 - logging.info('Starting Web App (tornado)')  
499 - try:  
500 - webapp = WebApplication(testapp, debug=args.debug) 403 + webapp = WebApplication(app, debug=debug)
501 except Exception: 404 except Exception:
502 logging.critical('Failed to start web application.') 405 logging.critical('Failed to start web application.')
503 raise 406 raise
504 407
505 - # --- get SSL certificates  
506 - if 'XDG_DATA_HOME' in os.environ:  
507 - certs_dir = path.join(os.environ['XDG_DATA_HOME'], 'certs')  
508 - else:  
509 - certs_dir = path.expanduser('~/.local/share/certs')  
510 -  
511 - ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)  
512 try: 408 try:
513 - ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),  
514 - path.join(certs_dir, 'privkey.pem'))  
515 - except FileNotFoundError:  
516 - logging.critical(f'SSL certificates missing in {certs_dir}')  
517 - sys.exit(-1)  
518 -  
519 - # --- create webserver  
520 - try:  
521 - httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx) 409 + httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl)
522 except ValueError: 410 except ValueError:
523 logging.critical('Certificates cert.pem, privkey.pem not found') 411 logging.critical('Certificates cert.pem, privkey.pem not found')
524 - sys.exit(-1) 412 + sys.exit(1)
525 413
526 - httpserver.listen(8443) 414 + try:
  415 + httpserver.listen(port)
  416 + except OSError:
  417 + logger.critical(f'Cannot bind port {port}. Already in use?')
  418 + sys.exit(1)
527 419
528 - # --- run webserver  
529 - logging.info('Webserver running... (Ctrl-C to stop)') 420 + logging.info(f'Webserver listening on {port}... (Ctrl-C to stop)')
530 signal.signal(signal.SIGINT, signal_handler) 421 signal.signal(signal.SIGINT, signal_handler)
531 422
  423 + # --- run webserver
532 try: 424 try:
533 tornado.ioloop.IOLoop.current().start() # running... 425 tornado.ioloop.IOLoop.current().start() # running...
534 except Exception: 426 except Exception:
535 - logging.critical('Webserver stopped.') 427 + logging.critical('Webserver stopped!')
536 tornado.ioloop.IOLoop.current().stop() 428 tornado.ioloop.IOLoop.current().stop()
537 raise 429 raise
538 -  
539 -  
540 -# ----------------------------------------------------------------------------  
541 -if __name__ == "__main__":  
542 - main()  
perguntations/test.py
@@ -49,7 +49,8 @@ class TestFactory(dict): @@ -49,7 +49,8 @@ class TestFactory(dict):
49 49
50 # --- find refs of all questions used in the test 50 # --- find refs of all questions used in the test
51 qrefs = {r for qq in self['questions'] for r in qq['ref']} 51 qrefs = {r for qq in self['questions'] for r in qq['ref']}
52 - logger.info(f'Declared {len(qrefs)} questions (each test uses {len(self["questions"])}).') 52 + logger.info(f'Declared {len(qrefs)} questions '
  53 + f'(each test uses {len(self["questions"])}).')
53 54
54 # --- for review, we are done. no factories needed 55 # --- for review, we are done. no factories needed
55 if self['review']: 56 if self['review']:
@@ -70,7 +71,8 @@ class TestFactory(dict): @@ -70,7 +71,8 @@ class TestFactory(dict):
70 for i, q in enumerate(questions): 71 for i, q in enumerate(questions):
71 # make sure every question in the file is a dictionary 72 # make sure every question in the file is a dictionary
72 if not isinstance(q, dict): 73 if not isinstance(q, dict):
73 - raise TestFactoryException(f'Question {i} in {file} is not a dictionary!') 74 + msg = f'Question {i} in {file} is not a dictionary'
  75 + raise TestFactoryException(msg)
74 76
75 # check if ref is missing, then set to '/path/file.yaml:3' 77 # check if ref is missing, then set to '/path/file.yaml:3'
76 if 'ref' not in q: 78 if 'ref' not in q:
@@ -80,9 +82,11 @@ class TestFactory(dict): @@ -80,9 +82,11 @@ class TestFactory(dict):
80 # check for duplicate refs 82 # check for duplicate refs
81 if q['ref'] in self.question_factory: 83 if q['ref'] in self.question_factory:
82 other = self.question_factory[q['ref']] 84 other = self.question_factory[q['ref']]
83 - otherfile = path.join(other.question['path'], other.question['filename'])  
84 - raise TestFactoryException(f'Duplicate reference "{q["ref"]}" in files '  
85 - f'"{otherfile}" and "{fullpath}".') 85 + otherfile = path.join(other.question['path'],
  86 + other.question['filename'])
  87 + msg = (f'Duplicate reference "{q["ref"]}" in files '
  88 + f'"{otherfile}" and "{fullpath}".')
  89 + raise TestFactoryException(msg)
86 90
87 # make factory only for the questions used in the test 91 # make factory only for the questions used in the test
88 if q['ref'] in qrefs: 92 if q['ref'] in qrefs:
@@ -98,8 +102,9 @@ class TestFactory(dict): @@ -98,8 +102,9 @@ class TestFactory(dict):
98 # check if all the questions can be correctly generated 102 # check if all the questions can be correctly generated
99 try: 103 try:
100 self.question_factory[q['ref']].generate() 104 self.question_factory[q['ref']].generate()
101 - except Exception as e:  
102 - raise TestFactoryException(f'Failed to generate "{q["ref"]}"') 105 + except Exception:
  106 + msg = f'Failed to generate "{q["ref"]}"'
  107 + raise TestFactoryException(msg)
103 else: 108 else:
104 logger.info(f'{n:4}. "{q["ref"]}" Ok.') 109 logger.info(f'{n:4}. "{q["ref"]}" Ok.')
105 n += 1 110 n += 1
@@ -108,7 +113,6 @@ class TestFactory(dict): @@ -108,7 +113,6 @@ class TestFactory(dict):
108 if qmissing: 113 if qmissing:
109 raise TestFactoryException(f'Could not find questions {qmissing}.') 114 raise TestFactoryException(f'Could not find questions {qmissing}.')
110 115
111 -  
112 # ------------------------------------------------------------------------ 116 # ------------------------------------------------------------------------
113 # Checks for valid keys and sets default values. 117 # Checks for valid keys and sets default values.
114 # Also checks if some files and directories exist 118 # Also checks if some files and directories exist
@@ -120,13 +124,15 @@ class TestFactory(dict): @@ -120,13 +124,15 @@ class TestFactory(dict):
120 124
121 # --- check database 125 # --- check database
122 if 'database' not in self: 126 if 'database' not in self:
123 - raise TestFactoryException('Missing "database" in configuration!') 127 + raise TestFactoryException('Missing "database" in configuration')
124 elif not path.isfile(path.expanduser(self['database'])): 128 elif not path.isfile(path.expanduser(self['database'])):
125 - raise TestFactoryException(f'Database {self["database"]} not found!') 129 + msg = f'Database "{self["database"]}" not found!'
  130 + raise TestFactoryException(msg)
126 131
127 # --- check answers_dir 132 # --- check answers_dir
128 if 'answers_dir' not in self: 133 if 'answers_dir' not in self:
129 - raise TestFactoryException('Missing "answers_dir" in configuration!') 134 + msg = 'Missing "answers_dir" in configuration'
  135 + raise TestFactoryException(msg)
130 136
131 # --- check if answers_dir is a writable directory 137 # --- check if answers_dir is a writable directory
132 testfile = path.join(path.expanduser(self['answers_dir']), 'REMOVE-ME') 138 testfile = path.join(path.expanduser(self['answers_dir']), 'REMOVE-ME')
@@ -134,16 +140,19 @@ class TestFactory(dict): @@ -134,16 +140,19 @@ class TestFactory(dict):
134 with open(testfile, 'w') as f: 140 with open(testfile, 'w') as f:
135 f.write('You can safely remove this file.') 141 f.write('You can safely remove this file.')
136 except OSError: 142 except OSError:
137 - raise TestFactoryException(f'Cannot write answers to directory "{self["answers_dir"]}"!') 143 + msg = f'Cannot write answers to directory "{self["answers_dir"]}"'
  144 + raise TestFactoryException(msg)
138 145
139 # --- check title 146 # --- check title
140 if not self['title']: 147 if not self['title']:
141 logger.warning('Undefined title!') 148 logger.warning('Undefined title!')
142 149
143 if self['scale_points']: 150 if self['scale_points']:
144 - logger.info(f'Grades will be scaled to [{self["scale_min"]}, {self["scale_max"]}]') 151 + smin, smax = self["scale_min"], self["scale_max"]
  152 + logger.info(f'Grades will be scaled to [{smin}, {smax}]')
145 else: 153 else:
146 - logger.info('Grades are just the sum of points defined for the questions, not being scaled.') 154 + logger.info('Grades are just the sum of points defined for the '
  155 + 'questions, not being scaled.')
147 156
148 # --- questions_dir 157 # --- questions_dir
149 if 'questions_dir' not in self: 158 if 'questions_dir' not in self:
@@ -156,7 +165,9 @@ class TestFactory(dict): @@ -156,7 +165,9 @@ class TestFactory(dict):
156 165
157 # --- files 166 # --- files
158 if 'files' not in self: 167 if 'files' not in self:
159 - raise TestFactoryException('Missing "files" in configuration with the list of question files to import!') 168 + msg = ('Missing "files" in configuration with the list of '
  169 + 'question files to import!')
  170 + raise TestFactoryException(msg)
160 # FIXME allow no files and define the questions directly in the test 171 # FIXME allow no files and define the questions directly in the test
161 172
162 if isinstance(self['files'], str): 173 if isinstance(self['files'], str):
@@ -164,7 +175,7 @@ class TestFactory(dict): @@ -164,7 +175,7 @@ class TestFactory(dict):
164 175
165 # --- questions 176 # --- questions
166 if 'questions' not in self: 177 if 'questions' not in self:
167 - raise TestFactoryException(f'Missing "questions" in Configuration!') 178 + raise TestFactoryException(f'Missing "questions" in Configuration')
168 179
169 for i, q in enumerate(self['questions']): 180 for i, q in enumerate(self['questions']):
170 # normalize question to a dict and ref to a list of references 181 # normalize question to a dict and ref to a list of references
@@ -15,15 +15,21 @@ setup( @@ -15,15 +15,21 @@ setup(
15 description=APP_DESCRIPTION.split('\n')[0], 15 description=APP_DESCRIPTION.split('\n')[0],
16 long_description=APP_DESCRIPTION, 16 long_description=APP_DESCRIPTION,
17 long_description_content_type="text/markdown", 17 long_description_content_type="text/markdown",
18 - url="https:USERNAME//bitbucket.org/USERNAME/perguntations.git", 18 + url="https://git.xdi.uevora.pt/mjsb/perguntations.git",
19 packages=find_packages(), 19 packages=find_packages(),
20 include_package_data=True, # install files from MANIFEST.in 20 include_package_data=True, # install files from MANIFEST.in
21 python_requires='>=3.7.*', 21 python_requires='>=3.7.*',
22 install_requires=[ 22 install_requires=[
23 - 'tornado', 'mistune', 'pyyaml', 'pygments', 'sqlalchemy', 'bcrypt'], 23 + 'tornado>=6.0',
  24 + 'mistune',
  25 + 'pyyaml>=5.1',
  26 + 'pygments',
  27 + 'sqlalchemy',
  28 + 'bcrypt>=3.1'
  29 + ],
24 entry_points={ 30 entry_points={
25 'console_scripts': [ 31 'console_scripts': [
26 - 'perguntations = perguntations.serve:main', 32 + 'perguntations = perguntations.main:main',
27 'initdb = perguntations.initdb:main', 33 'initdb = perguntations.initdb:main',
28 ] 34 ]
29 }, 35 },