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