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 | }, |