Commit 186cc76ce18f343cfcb4078ddcf2b9ec3c3871db
1 parent
1841cc32
Exists in
master
and in
1 other branch
adds config/logger.yaml files as a base for customization.
Showing
8 changed files
with
131 additions
and
36 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | +- permitir configuracao para escolher entre static files locais ou remotos | ||
| 4 | - sqlalchemy.pool.impl.NullPool: Exception during reset or similar | 5 | - sqlalchemy.pool.impl.NullPool: Exception during reset or similar |
| 5 | sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. | 6 | sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. |
| 6 | 7 |
README.md
| @@ -85,8 +85,9 @@ At this point aprendizations is installed in | @@ -85,8 +85,9 @@ At this point aprendizations is installed in | ||
| 85 | ~/.local/bin # FreeBSD/Linux | 85 | ~/.local/bin # FreeBSD/Linux |
| 86 | ``` | 86 | ``` |
| 87 | 87 | ||
| 88 | -and can be run with the command `aprendizations` (make sure this directory is | ||
| 89 | -in your `$PATH`). | 88 | +Make sure this directory is in your `$PATH`. |
| 89 | + | ||
| 90 | +The server can be run with the command `aprendizations` from the terminal. | ||
| 90 | 91 | ||
| 91 | ## Configuration | 92 | ## Configuration |
| 92 | 93 | ||
| @@ -121,7 +122,7 @@ have a registered publicly accessible domain name. | @@ -121,7 +122,7 @@ have a registered publicly accessible domain name. | ||
| 121 | 122 | ||
| 122 | #### Selfsigned | 123 | #### Selfsigned |
| 123 | 124 | ||
| 124 | -Generate a selfsigned certificate and place it in `aprendizations/certs`. | 125 | +Generate a selfsigned certificate and place it in `~/.local/share/certs`. |
| 125 | 126 | ||
| 126 | ```sh | 127 | ```sh |
| 127 | openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes | 128 | openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes |
| @@ -141,7 +142,7 @@ sudo certbot certonly --standalone -d www.example.com | @@ -141,7 +142,7 @@ sudo certbot certonly --standalone -d www.example.com | ||
| 141 | sudo service pf start # enable pf firewall | 142 | sudo service pf start # enable pf firewall |
| 142 | ``` | 143 | ``` |
| 143 | 144 | ||
| 144 | -Certificates are saved under `/usr/local/etc/letsencrypt/live/www.example.com/`. Copy them to `aprendizations/certs` and change permissions to be readable: | 145 | +Certificates are saved under `/usr/local/etc/letsencrypt/live/www.example.com/`. Copy them to `~/.local/share/certs` and change permissions to be readable: |
| 145 | 146 | ||
| 146 | ```sh | 147 | ```sh |
| 147 | sudo cp /usr/local/etc/letsencrypt/live/www.example.com/cert.pem . | 148 | sudo cp /usr/local/etc/letsencrypt/live/www.example.com/cert.pem . |
| @@ -157,7 +158,7 @@ sudo certbot renew | @@ -157,7 +158,7 @@ sudo certbot renew | ||
| 157 | sudo service pf start # start firewall | 158 | sudo service pf start # start firewall |
| 158 | ``` | 159 | ``` |
| 159 | 160 | ||
| 160 | -and then copy the `cert.pem` and `privkey.pem` files to `aprendizations/certs` directory. Change permissions and ownership as appropriate. | 161 | +and then copy the `cert.pem` and `privkey.pem` files to `~/.local/share/certs` directory. Change permissions and ownership as appropriate. |
| 161 | 162 | ||
| 162 | 163 | ||
| 163 | ### Testing | 164 | ### Testing |
| @@ -169,7 +170,8 @@ cd demo | @@ -169,7 +170,8 @@ cd demo | ||
| 169 | aprendizations demo.yaml | 170 | aprendizations demo.yaml |
| 170 | ``` | 171 | ``` |
| 171 | 172 | ||
| 172 | -and open a browser at [https://127.0.0.1:8443](). If it everything looks good, | 173 | +and open a browser at [https://127.0.0.1:8443](https://127.0.0.1:8443). |
| 174 | +If it everything looks good, | ||
| 173 | check at the correct address `https://www.example.com` (requires port forward | 175 | check at the correct address `https://www.example.com` (requires port forward |
| 174 | in the firewall). The option `--debug` provides more verbose logging and might | 176 | in the firewall). The option `--debug` provides more verbose logging and might |
| 175 | be useful during testing. The option `--check` generates all the questions once | 177 | be useful during testing. The option `--check` generates all the questions once |
| @@ -220,6 +222,7 @@ errors in the browser. | @@ -220,6 +222,7 @@ errors in the browser. | ||
| 220 | Logging levels can be adjusted in `~/.config/aprendizations/logger.yaml` and | 222 | Logging levels can be adjusted in `~/.config/aprendizations/logger.yaml` and |
| 221 | `~/.config/aprendizations/logger-debug.yaml`. | 223 | `~/.config/aprendizations/logger-debug.yaml`. |
| 222 | 224 | ||
| 225 | +If these files do not yet exist, there are examples in `aprendizations/config` that can be copied to `~/.config/aprendizations`. | ||
| 223 | 226 | ||
| 224 | #### UnicodeEncodeError | 227 | #### UnicodeEncodeError |
| 225 | 228 |
aprendizations/knowledge.py
| @@ -69,13 +69,14 @@ class StudentKnowledge(object): | @@ -69,13 +69,14 @@ class StudentKnowledge(object): | ||
| 69 | # ------------------------------------------------------------------------ | 69 | # ------------------------------------------------------------------------ |
| 70 | async def start_topic(self, topic): | 70 | async def start_topic(self, topic): |
| 71 | logger.debug('StudentKnowledge.start_topic()') | 71 | logger.debug('StudentKnowledge.start_topic()') |
| 72 | + | ||
| 72 | if self.current_topic == topic: | 73 | if self.current_topic == topic: |
| 73 | - logger.info(' Restarting current topic is not allowed.') | 74 | + logger.info('Restarting current topic is not allowed.') |
| 74 | return False | 75 | return False |
| 75 | 76 | ||
| 76 | # do not allow locked topics | 77 | # do not allow locked topics |
| 77 | if self.is_locked(topic): | 78 | if self.is_locked(topic): |
| 78 | - logger.debug(f' Topic {topic} is locked') | 79 | + logger.debug(f'Topic {topic} is locked') |
| 79 | return False | 80 | return False |
| 80 | 81 | ||
| 81 | # starting new topic | 82 | # starting new topic |
| @@ -96,23 +97,16 @@ class StudentKnowledge(object): | @@ -96,23 +97,16 @@ class StudentKnowledge(object): | ||
| 96 | # synchronous: | 97 | # synchronous: |
| 97 | # self.questions = [self.factory[ref].generate() for ref in questions] | 98 | # self.questions = [self.factory[ref].generate() for ref in questions] |
| 98 | 99 | ||
| 99 | - loop = asyncio.get_running_loop() | ||
| 100 | - | ||
| 101 | # async: | 100 | # async: |
| 102 | - # generators = [loop.run_in_executor(None, self.factory[qref].generate) | ||
| 103 | - # for qref in questions] | ||
| 104 | - # self.questions = await asyncio.gather(*generators) | ||
| 105 | - | ||
| 106 | - # another async: | ||
| 107 | - self.questions = [] | ||
| 108 | - for qref in questions: | ||
| 109 | - q = await loop.run_in_executor(None, self.factory[qref].generate) | ||
| 110 | - self.questions.append(q) | ||
| 111 | - | ||
| 112 | - logger.debug(f'Generated {len(self.questions)} questions') | 101 | + loop = asyncio.get_running_loop() |
| 102 | + generators = [loop.run_in_executor(None, self.factory[qref].generate) | ||
| 103 | + for qref in questions] | ||
| 104 | + self.questions = await asyncio.gather(*generators) | ||
| 113 | 105 | ||
| 114 | # get first question | 106 | # get first question |
| 115 | self.next_question() | 107 | self.next_question() |
| 108 | + | ||
| 109 | + logger.debug(f'Generated {len(self.questions)} questions') | ||
| 116 | return True | 110 | return True |
| 117 | 111 | ||
| 118 | # ------------------------------------------------------------------------ | 112 | # ------------------------------------------------------------------------ |
aprendizations/learnapp.py
| @@ -459,3 +459,5 @@ class LearnApp(object): | @@ -459,3 +459,5 @@ class LearnApp(object): | ||
| 459 | for uid, name in students if uid != '0'] | 459 | for uid, name in students if uid != '0'] |
| 460 | 460 | ||
| 461 | return sorted(rankings, key=lambda x: x[2], reverse=True) | 461 | return sorted(rankings, key=lambda x: x[2], reverse=True) |
| 462 | + | ||
| 463 | + # ------------------------------------------------------------------------ |
aprendizations/questions.py
| @@ -28,9 +28,9 @@ class QuestionException(Exception): | @@ -28,9 +28,9 @@ class QuestionException(Exception): | ||
| 28 | # =========================================================================== | 28 | # =========================================================================== |
| 29 | class Question(dict): | 29 | class Question(dict): |
| 30 | ''' | 30 | ''' |
| 31 | - Classes derived from this base class are meant to instantiate a question | ||
| 32 | - to a student. | ||
| 33 | - Instances can shuffle options, or automatically generate questions. | 31 | + Classes derived from this base class are meant to instantiate questions |
| 32 | + for each student. | ||
| 33 | + Instances can shuffle options or automatically generate questions. | ||
| 34 | ''' | 34 | ''' |
| 35 | def __init__(self, q: QDict) -> None: | 35 | def __init__(self, q: QDict) -> None: |
| 36 | super().__init__(q) | 36 | super().__init__(q) |
| @@ -125,7 +125,7 @@ class QuestionRadio(Question): | @@ -125,7 +125,7 @@ class QuestionRadio(Question): | ||
| 125 | self['correct'] = [float(correct[i]) for i in perm] | 125 | self['correct'] = [float(correct[i]) for i in perm] |
| 126 | 126 | ||
| 127 | # ------------------------------------------------------------------------ | 127 | # ------------------------------------------------------------------------ |
| 128 | - # can return negative values for wrong answers | 128 | + # can assign negative grades for wrong answers |
| 129 | def correct(self) -> None: | 129 | def correct(self) -> None: |
| 130 | super().correct() | 130 | super().correct() |
| 131 | 131 |
aprendizations/serve.py
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | # python standard library | 3 | # python standard library |
| 4 | -from os import path | ||
| 5 | -import os | ||
| 6 | import sys | 4 | import sys |
| 7 | import base64 | 5 | import base64 |
| 8 | import uuid | 6 | import uuid |
| @@ -13,12 +11,12 @@ import signal | @@ -13,12 +11,12 @@ import signal | ||
| 13 | import functools | 11 | import functools |
| 14 | import ssl | 12 | import ssl |
| 15 | import asyncio | 13 | import asyncio |
| 16 | -# from typing import NoReturn | 14 | +from os import path, environ |
| 17 | 15 | ||
| 18 | # third party libraries | 16 | # third party libraries |
| 19 | import tornado.ioloop | 17 | import tornado.ioloop |
| 20 | -import tornado.web | ||
| 21 | import tornado.httpserver | 18 | import tornado.httpserver |
| 19 | +import tornado.web | ||
| 22 | from tornado.escape import to_unicode | 20 | from tornado.escape import to_unicode |
| 23 | 21 | ||
| 24 | # this project | 22 | # this project |
| @@ -294,7 +292,7 @@ class QuestionHandler(BaseHandler): | @@ -294,7 +292,7 @@ class QuestionHandler(BaseHandler): | ||
| 294 | answer_qid = self.get_body_arguments('qid')[0] | 292 | answer_qid = self.get_body_arguments('qid')[0] |
| 295 | current_qid = self.learn.get_current_question_id(user) | 293 | current_qid = self.learn.get_current_question_id(user) |
| 296 | if answer_qid != current_qid: | 294 | if answer_qid != current_qid: |
| 297 | - logging.debug(f'User {user} desynchronized questions') | 295 | + logging.info(f'User {user} desynchronized questions') |
| 298 | self.write({ | 296 | self.write({ |
| 299 | 'method': 'invalid', | 297 | 'method': 'invalid', |
| 300 | 'params': { | 298 | 'params': { |
| @@ -428,16 +426,16 @@ def get_logger_config(debug=False): | @@ -428,16 +426,16 @@ def get_logger_config(debug=False): | ||
| 428 | filename = 'logger.yaml' | 426 | filename = 'logger.yaml' |
| 429 | level = 'INFO' | 427 | level = 'INFO' |
| 430 | 428 | ||
| 431 | - config_dir = os.environ.get('XDG_CONFIG_HOME', '~/.config/') | 429 | + config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/') |
| 432 | config_file = path.join(path.expanduser(config_dir), APP_NAME, filename) | 430 | config_file = path.join(path.expanduser(config_dir), APP_NAME, filename) |
| 433 | 431 | ||
| 434 | default_config = { | 432 | default_config = { |
| 435 | 'version': 1, | 433 | 'version': 1, |
| 436 | 'formatters': { | 434 | 'formatters': { |
| 437 | 'standard': { | 435 | 'standard': { |
| 438 | - 'format': '%(asctime)s %(name)-24s %(levelname)-8s ' | 436 | + 'format': '%(asctime)s %(name)-24s %(levelname)-10s - ' |
| 439 | '%(message)s', | 437 | '%(message)s', |
| 440 | - 'datefmt': '%H:%M:%S', | 438 | + 'datefmt': '%Y-%m-%d %H:%M:%S', |
| 441 | }, | 439 | }, |
| 442 | }, | 440 | }, |
| 443 | 'handlers': { | 441 | 'handlers': { |
| @@ -503,8 +501,8 @@ def main(): | @@ -503,8 +501,8 @@ def main(): | ||
| 503 | sys.exit(1) | 501 | sys.exit(1) |
| 504 | 502 | ||
| 505 | # --- get SSL certificates | 503 | # --- get SSL certificates |
| 506 | - if 'XDG_DATA_HOME' in os.environ: | ||
| 507 | - certs_dir = path.join(os.environ['XDG_DATA_HOME'], 'certs') | 504 | + if 'XDG_DATA_HOME' in environ: |
| 505 | + certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs') | ||
| 508 | else: | 506 | else: |
| 509 | certs_dir = path.expanduser('~/.local/share/certs') | 507 | certs_dir = path.expanduser('~/.local/share/certs') |
| 510 | 508 | ||
| @@ -523,7 +521,12 @@ def main(): | @@ -523,7 +521,12 @@ def main(): | ||
| 523 | logging.critical('Certificates cert.pem and privkey.pem not found') | 521 | logging.critical('Certificates cert.pem and privkey.pem not found') |
| 524 | sys.exit(1) | 522 | sys.exit(1) |
| 525 | 523 | ||
| 526 | - httpserver.listen(arg.port) | 524 | + try: |
| 525 | + httpserver.listen(arg.port) | ||
| 526 | + except OSError: | ||
| 527 | + logging.critical(f'Cannot bind port {arg.port}. Already in use?') | ||
| 528 | + sys.exit(1) | ||
| 529 | + | ||
| 527 | logging.info(f'Listening on port {arg.port}.') | 530 | logging.info(f'Listening on port {arg.port}.') |
| 528 | 531 | ||
| 529 | # --- run webserver | 532 | # --- run webserver |
| @@ -0,0 +1,46 @@ | @@ -0,0 +1,46 @@ | ||
| 1 | +--- | ||
| 2 | +version: 1 | ||
| 3 | + | ||
| 4 | +formatters: | ||
| 5 | + void: | ||
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(levelname)-9s | %(name)-24s | %(thread)-15d | %(message)s' | ||
| 9 | + datefmt: '%H:%M:%S' | ||
| 10 | + | ||
| 11 | +handlers: | ||
| 12 | + default: | ||
| 13 | + level: 'DEBUG' | ||
| 14 | + class: 'logging.StreamHandler' | ||
| 15 | + formatter: 'standard' | ||
| 16 | + stream: 'ext://sys.stdout' | ||
| 17 | + | ||
| 18 | +loggers: | ||
| 19 | + '': | ||
| 20 | + handlers: ['default'] | ||
| 21 | + level: 'DEBUG' | ||
| 22 | + | ||
| 23 | + 'aprendizations.factory': | ||
| 24 | + handlers: ['default'] | ||
| 25 | + level: 'DEBUG' | ||
| 26 | + propagate: false | ||
| 27 | + | ||
| 28 | + 'aprendizations.knowledge': | ||
| 29 | + handlers: ['default'] | ||
| 30 | + level: 'DEBUG' | ||
| 31 | + propagate: false | ||
| 32 | + | ||
| 33 | + 'aprendizations.learnapp': | ||
| 34 | + handlers: ['default'] | ||
| 35 | + level: 'DEBUG' | ||
| 36 | + propagate: false | ||
| 37 | + | ||
| 38 | + 'aprendizations.questions': | ||
| 39 | + handlers: ['default'] | ||
| 40 | + level: 'DEBUG' | ||
| 41 | + propagate: false | ||
| 42 | + | ||
| 43 | + 'aprendizations.tools': | ||
| 44 | + handlers: ['default'] | ||
| 45 | + level: 'DEBUG' | ||
| 46 | + propagate: false |
| @@ -0,0 +1,46 @@ | @@ -0,0 +1,46 @@ | ||
| 1 | +--- | ||
| 2 | +version: 1 | ||
| 3 | + | ||
| 4 | +formatters: | ||
| 5 | + void: | ||
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(thread)-15d | %(levelname)-9s | %(message)s' | ||
| 9 | + datefmt: '%Y-%m-%d %H:%M:%S' | ||
| 10 | + | ||
| 11 | +handlers: | ||
| 12 | + default: | ||
| 13 | + level: 'INFO' | ||
| 14 | + class: 'logging.StreamHandler' | ||
| 15 | + formatter: 'standard' | ||
| 16 | + stream: 'ext://sys.stdout' | ||
| 17 | + | ||
| 18 | +loggers: | ||
| 19 | + '': | ||
| 20 | + handlers: ['default'] | ||
| 21 | + level: 'INFO' | ||
| 22 | + | ||
| 23 | + 'aprendizations.factory': | ||
| 24 | + handlers: ['default'] | ||
| 25 | + level: 'INFO' | ||
| 26 | + propagate: false | ||
| 27 | + | ||
| 28 | + 'aprendizations.knowledge': | ||
| 29 | + handlers: ['default'] | ||
| 30 | + level: 'INFO' | ||
| 31 | + propagate: false | ||
| 32 | + | ||
| 33 | + 'aprendizations.learnapp': | ||
| 34 | + handlers: ['default'] | ||
| 35 | + level: 'INFO' | ||
| 36 | + propagate: false | ||
| 37 | + | ||
| 38 | + 'aprendizations.questions': | ||
| 39 | + handlers: ['default'] | ||
| 40 | + level: 'INFO' | ||
| 41 | + propagate: false | ||
| 42 | + | ||
| 43 | + 'aprendizations.tools': | ||
| 44 | + handlers: ['default'] | ||
| 45 | + level: 'INFO' | ||
| 46 | + propagate: false |