Commit a566a5d6395366df475d15df76969fcc14f0c0ba
1 parent
255d1724
Exists in
master
and in
1 other branch
refactoring. split serve.py into serve.py and main.py
commandline options: --version and --port
Showing
9 changed files
with
240 additions
and
158 deletions
Show diff stats
BUGS.md
| 1 | 1 | |
| 2 | 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 | 9 | - na pagina grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao do realizado. |
| 5 | 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 | 11 | - teste nao esta a mostrar imagens de vez em quando. |
| ... | ... | @@ -16,6 +21,7 @@ ou usar push (websockets?) |
| 16 | 21 | - Test.reset_answers() unused. |
| 17 | 22 | - mudar ref do test para test_id (ref já é usado nas perguntas) |
| 18 | 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 | 26 | # TODO |
| 21 | 27 | ... | ... |
README.md
| ... | ... | @@ -42,7 +42,7 @@ Download and install: |
| 42 | 42 | git clone https://git.xdi.uevora.pt/perguntations.git |
| 43 | 43 | cd perguntations |
| 44 | 44 | npm install |
| 45 | -pip3 install . # this must come last | |
| 45 | +pip3 install . | |
| 46 | 46 | ``` |
| 47 | 47 | |
| 48 | 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 | |
| 8 | 8 | # Database with student credentials and grades of all questions and tests done |
| 9 | 9 | # The database is an sqlite3 file generate with the command initdb |
| 10 | -database: ../demo/students.db | |
| 10 | +database: students.db | |
| 11 | 11 | |
| 12 | 12 | # Directory where the tests including submitted answers and grades are stored. |
| 13 | 13 | # The submitted tests and their corrections can be reviewed later. |
| ... | ... | @@ -64,3 +64,20 @@ questions: |
| 64 | 64 | - tut-success |
| 65 | 65 | - tut-warning |
| 66 | 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 | 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 | 185 | title: Escolha múltipla, várias opções correctas |
| 186 | 186 | text: | |
| 187 | 187 | As perguntas de escolha múltipla permitem apresentar um conjunto de opções |
| ... | ... | @@ -359,7 +359,7 @@ |
| 359 | 359 | ou em vírgula flutuante, como em `0.23`, `1e-3`. |
| 360 | 360 | correct: [3.14, 3.15] |
| 361 | 361 | solution: | |
| 362 | - Sabems que $\pi\approx 3.14159265359$. | |
| 362 | + Sabemos que $\pi\approx 3.14159265359$. | |
| 363 | 363 | Portanto, um exemplo de uma resposta correcta é `3.1416`. |
| 364 | 364 | |
| 365 | 365 | # --------------------------------------------------------------------------- |
| ... | ... | @@ -556,6 +556,7 @@ |
| 556 | 556 | This question is not included in the test and will not shown up. |
| 557 | 557 | It also lacks a "ref" and is automatically named |
| 558 | 558 | `questions/questions-tutorial.yaml:0012`. |
| 559 | + A warning is shown on the console about this. | |
| 559 | 560 | The number at the end is the index position of this question. |
| 560 | 561 | Indices start at 0. |
| 561 | 562 | ... | ... |
perguntations/app.py
| ... | ... | @@ -43,6 +43,14 @@ async def hash_password(pw): |
| 43 | 43 | |
| 44 | 44 | # ============================================================================ |
| 45 | 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 | 55 | class App(object): |
| 48 | 56 | # ------------------------------------------------------------------------ |
| ... | ... | @@ -61,12 +69,12 @@ class App(object): |
| 61 | 69 | session.close() |
| 62 | 70 | |
| 63 | 71 | # ------------------------------------------------------------------------ |
| 64 | - def __init__(self, conf={}): | |
| 72 | + def __init__(self, conf): | |
| 65 | 73 | self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...} |
| 66 | 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 | 78 | testconf.update(conf) # command line options override configuration |
| 71 | 79 | |
| 72 | 80 | # start test factory |
| ... | ... | @@ -89,13 +97,15 @@ class App(object): |
| 89 | 97 | except Exception: |
| 90 | 98 | raise AppException(f'Database unusable {dbfile}.') |
| 91 | 99 | else: |
| 92 | - logger.info(f'Database {dbfile} has {n} students.') | |
| 100 | + logger.info(f'Database "{dbfile}" has {n} students.') | |
| 93 | 101 | |
| 94 | 102 | # command line option --allow-all |
| 95 | 103 | if conf['allow_all']: |
| 96 | 104 | logger.info('Allowing all students:') |
| 97 | 105 | for student in self.get_all_students(): |
| 98 | 106 | self.allow_student(student[0]) |
| 107 | + else: | |
| 108 | + logger.info('Students not yet allowed to login.') | |
| 99 | 109 | |
| 100 | 110 | # ------------------------------------------------------------------------ |
| 101 | 111 | # FIXME unused??? | ... | ... |
| ... | ... | @@ -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 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | |
| 3 | 3 | # python standard library |
| 4 | -import os | |
| 5 | 4 | from os import path |
| 6 | 5 | import sys |
| 7 | 6 | import base64 |
| ... | ... | @@ -21,9 +20,6 @@ import tornado.web |
| 21 | 20 | import tornado.httpserver |
| 22 | 21 | |
| 23 | 22 | # this project |
| 24 | -from perguntations.app import App, AppException | |
| 25 | -from perguntations.tools import load_yaml | |
| 26 | -from perguntations import APP_NAME | |
| 27 | 23 | from perguntations.parser_markdown import md_to_html |
| 28 | 24 | |
| 29 | 25 | |
| ... | ... | @@ -159,7 +155,7 @@ class AdminHandler(BaseHandler): |
| 159 | 155 | 'data': { |
| 160 | 156 | 'title': self.testapp.testfactory['title'], |
| 161 | 157 | 'ref': self.testapp.testfactory['ref'], |
| 162 | - 'filename': self.testapp.testfactory['filename'], | |
| 158 | + 'filename': self.testapp.testfactory['testfile'], | |
| 163 | 159 | 'database': self.testapp.testfactory['database'], |
| 164 | 160 | 'answers_dir': self.testapp.testfactory['answers_dir'], |
| 165 | 161 | } |
| ... | ... | @@ -390,153 +386,44 @@ class ReviewHandler(BaseHandler): |
| 390 | 386 | templ=self._templates) |
| 391 | 387 | |
| 392 | 388 | |
| 389 | + | |
| 393 | 390 | # ---------------------------------------------------------------------------- |
| 394 | 391 | def signal_handler(signal, frame): |
| 395 | 392 | r = input(' --> Stop webserver? (yes/no) ') |
| 396 | - if r in ('yes', 'YES'): | |
| 393 | + if r.lower() == 'yes': | |
| 397 | 394 | tornado.ioloop.IOLoop.current().stop() |
| 398 | 395 | logging.critical('Webserver stopped.') |
| 399 | 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 | 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 | 404 | except Exception: |
| 502 | 405 | logging.critical('Failed to start web application.') |
| 503 | 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 | 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 | 410 | except ValueError: |
| 523 | 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 | 421 | signal.signal(signal.SIGINT, signal_handler) |
| 531 | 422 | |
| 423 | + # --- run webserver | |
| 532 | 424 | try: |
| 533 | 425 | tornado.ioloop.IOLoop.current().start() # running... |
| 534 | 426 | except Exception: |
| 535 | - logging.critical('Webserver stopped.') | |
| 427 | + logging.critical('Webserver stopped!') | |
| 536 | 428 | tornado.ioloop.IOLoop.current().stop() |
| 537 | 429 | raise |
| 538 | - | |
| 539 | - | |
| 540 | -# ---------------------------------------------------------------------------- | |
| 541 | -if __name__ == "__main__": | |
| 542 | - main() | ... | ... |
perguntations/test.py
| ... | ... | @@ -49,7 +49,8 @@ class TestFactory(dict): |
| 49 | 49 | |
| 50 | 50 | # --- find refs of all questions used in the test |
| 51 | 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 | 55 | # --- for review, we are done. no factories needed |
| 55 | 56 | if self['review']: |
| ... | ... | @@ -70,7 +71,8 @@ class TestFactory(dict): |
| 70 | 71 | for i, q in enumerate(questions): |
| 71 | 72 | # make sure every question in the file is a dictionary |
| 72 | 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 | 77 | # check if ref is missing, then set to '/path/file.yaml:3' |
| 76 | 78 | if 'ref' not in q: |
| ... | ... | @@ -80,9 +82,11 @@ class TestFactory(dict): |
| 80 | 82 | # check for duplicate refs |
| 81 | 83 | if q['ref'] in self.question_factory: |
| 82 | 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 | 91 | # make factory only for the questions used in the test |
| 88 | 92 | if q['ref'] in qrefs: |
| ... | ... | @@ -98,8 +102,9 @@ class TestFactory(dict): |
| 98 | 102 | # check if all the questions can be correctly generated |
| 99 | 103 | try: |
| 100 | 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 | 108 | else: |
| 104 | 109 | logger.info(f'{n:4}. "{q["ref"]}" Ok.') |
| 105 | 110 | n += 1 |
| ... | ... | @@ -108,7 +113,6 @@ class TestFactory(dict): |
| 108 | 113 | if qmissing: |
| 109 | 114 | raise TestFactoryException(f'Could not find questions {qmissing}.') |
| 110 | 115 | |
| 111 | - | |
| 112 | 116 | # ------------------------------------------------------------------------ |
| 113 | 117 | # Checks for valid keys and sets default values. |
| 114 | 118 | # Also checks if some files and directories exist |
| ... | ... | @@ -120,13 +124,15 @@ class TestFactory(dict): |
| 120 | 124 | |
| 121 | 125 | # --- check database |
| 122 | 126 | if 'database' not in self: |
| 123 | - raise TestFactoryException('Missing "database" in configuration!') | |
| 127 | + raise TestFactoryException('Missing "database" in configuration') | |
| 124 | 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 | 132 | # --- check answers_dir |
| 128 | 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 | 137 | # --- check if answers_dir is a writable directory |
| 132 | 138 | testfile = path.join(path.expanduser(self['answers_dir']), 'REMOVE-ME') |
| ... | ... | @@ -134,16 +140,19 @@ class TestFactory(dict): |
| 134 | 140 | with open(testfile, 'w') as f: |
| 135 | 141 | f.write('You can safely remove this file.') |
| 136 | 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 | 146 | # --- check title |
| 140 | 147 | if not self['title']: |
| 141 | 148 | logger.warning('Undefined title!') |
| 142 | 149 | |
| 143 | 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 | 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 | 157 | # --- questions_dir |
| 149 | 158 | if 'questions_dir' not in self: |
| ... | ... | @@ -156,7 +165,9 @@ class TestFactory(dict): |
| 156 | 165 | |
| 157 | 166 | # --- files |
| 158 | 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 | 171 | # FIXME allow no files and define the questions directly in the test |
| 161 | 172 | |
| 162 | 173 | if isinstance(self['files'], str): |
| ... | ... | @@ -164,7 +175,7 @@ class TestFactory(dict): |
| 164 | 175 | |
| 165 | 176 | # --- questions |
| 166 | 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 | 180 | for i, q in enumerate(self['questions']): |
| 170 | 181 | # normalize question to a dict and ref to a list of references | ... | ... |
setup.py
| ... | ... | @@ -15,15 +15,21 @@ setup( |
| 15 | 15 | description=APP_DESCRIPTION.split('\n')[0], |
| 16 | 16 | long_description=APP_DESCRIPTION, |
| 17 | 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 | 19 | packages=find_packages(), |
| 20 | 20 | include_package_data=True, # install files from MANIFEST.in |
| 21 | 21 | python_requires='>=3.7.*', |
| 22 | 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 | 30 | entry_points={ |
| 25 | 31 | 'console_scripts': [ |
| 26 | - 'perguntations = perguntations.serve:main', | |
| 32 | + 'perguntations = perguntations.main:main', | |
| 27 | 33 | 'initdb = perguntations.initdb:main', |
| 28 | 34 | ] |
| 29 | 35 | }, | ... | ... |