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 | # 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 |
README.md
| @@ -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??? |
| @@ -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 |
setup.py
| @@ -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 | }, |