Commit d173a54ce9b33e8ff1ddcae4e53f9f64961e37ca

Authored by Miguel Barão
1 parent f8d971df
Exists in master and in 1 other branch dev

several fixes

1 1
2 # BUGS 2 # BUGS
3 3
  4 +- duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks
4 - quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) 5 - quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo)
5 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) 6 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
6 - devia mostrar timeout para o aluno saber a razao. 7 - devia mostrar timeout para o aluno saber a razao.
@@ -40,6 +41,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in @@ -40,6 +41,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in
40 41
41 # FIXED 42 # FIXED
42 43
  44 +- forgetting factor is hardcoded in student.py
43 - add aprendizatons --version 45 - add aprendizatons --version
44 - se aluno abre dois tabs no browser, conseque navegar em simultaneo para perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um campo hidden que tenha um céodigo único que indique qual a pergunta. do lado do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz redirect para /. 46 - se aluno abre dois tabs no browser, conseque navegar em simultaneo para perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um campo hidden que tenha um céodigo único que indique qual a pergunta. do lado do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz redirect para /.
45 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. 47 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido.
1 # Getting Started 1 # Getting Started
2 2
  3 +
  4 +## Installation
  5 +
3 To complete the installation we will need to perform the following steps: 6 To complete the installation we will need to perform the following steps:
4 7
5 1. install python3.7, pip and npm 8 1. install python3.7, pip and npm
6 1. download aprendizations from the repository 9 1. download aprendizations from the repository
7 1. install javascript libraries (with npm) 10 1. install javascript libraries (with npm)
8 1. install aprendizations (with pip) 11 1. install aprendizations (with pip)
9 -1. initialize database  
10 1. generate SSL certificates 12 1. generate SSL certificates
11 1. configure the firewall (optional) 13 1. configure the firewall (optional)
12 -1. try running `aprendizations demo.yaml`  
13 14
14 -These steps are explained next in detail. 15 +To use the software we need to:
  16 +
  17 +1. initialize database
  18 +1. go to the demo directory (or an existing course)
  19 +1. run `aprendizations demo.yaml`
  20 +
  21 +Each of these steps is explained below.
15 22
16 ### Install python3.7 with sqlite3 support and npm 23 ### Install python3.7 with sqlite3 support and npm
17 24
@@ -47,16 +54,21 @@ This will install python locally under `~/.local/bin`. Make sure to add it to yo @@ -47,16 +54,21 @@ This will install python locally under `~/.local/bin`. Make sure to add it to yo
47 54
48 ### Install pip 55 ### Install pip
49 56
50 -Python usually includes pip which is accessible through `python -m pip install something`, but it's also convenient to have the `pip` command installed.  
51 -If the `pip` command is not yet installed, run one of these: 57 +Python usually includes pip which is accessible through `python -m pip install something`, but it's also convenient to have the `pip` command directly available in the terminal.
  58 +To install `pip` from the system package manager:
52 59
53 ```sh 60 ```sh
54 sudo apt install python3.7-pip # Ubuntu 19.04+ 61 sudo apt install python3.7-pip # Ubuntu 19.04+
55 -python3.7 -m pip install pip # Ubuntu 18.04  
56 sudo pkg py37-pip # FreeBSD 62 sudo pkg py37-pip # FreeBSD
57 sudo port install py37-pip # MacOS 63 sudo port install py37-pip # MacOS
58 ``` 64 ```
59 65
  66 +otherwise run:
  67 +
  68 +```sh
  69 +python3.7 -m pip install pip # install in user area
  70 +```
  71 +
60 The latter will install `pip` in your user account under `~/.local/bin`. 72 The latter will install `pip` in your user account under `~/.local/bin`.
61 In the end you should be able to run `pip --version` and 73 In the end you should be able to run `pip --version` and
62 `python3 -c "import sqlite3"` without errors. 74 `python3 -c "import sqlite3"` without errors.
@@ -73,7 +85,7 @@ user = yes @@ -73,7 +85,7 @@ user = yes
73 85
74 This will set pip to install modules in the user area (recommended). 86 This will set pip to install modules in the user area (recommended).
75 87
76 -### Install aprendizations and dependencies: 88 +### Download and install aprendizations:
77 89
78 ```sh 90 ```sh
79 git clone https://git.xdi.uevora.pt/mjsb/aprendizations.git 91 git clone https://git.xdi.uevora.pt/mjsb/aprendizations.git
@@ -82,8 +94,7 @@ npm install # install javascript libraries @@ -82,8 +94,7 @@ npm install # install javascript libraries
82 pip install . # install aprendizations and dependencies 94 pip install . # install aprendizations and dependencies
83 ``` 95 ```
84 96
85 -Javascript libraries are initially installed in `aprendizations/node_modules` directory.  
86 -These libraries already have symbolic links from `aprendizations/aprendizations/static`. 97 +Javascript libraries are installed in `aprendizations/node_modules` and are linked from `aprendizations/aprendizations/static`.
87 98
88 Python packages are usually installed in: 99 Python packages are usually installed in:
89 100
@@ -92,7 +103,7 @@ Python packages are usually installed in: @@ -92,7 +103,7 @@ Python packages are usually installed in:
92 103
93 When aprendizations is installed with pip, all the dependencies are also installed. The javascript libraries previously installed with npm are copied to the above directory and the cloned repository is no longer needed. 104 When aprendizations is installed with pip, all the dependencies are also installed. The javascript libraries previously installed with npm are copied to the above directory and the cloned repository is no longer needed.
94 105
95 -At this point, aprendizations is installed in 106 +At this point `aprendizations` is installed in
96 107
97 ```sh 108 ```sh
98 ~/.local/bin # Linux/FreeBSD 109 ~/.local/bin # Linux/FreeBSD
@@ -106,29 +117,6 @@ aprendizations --version @@ -106,29 +117,6 @@ aprendizations --version
106 aprendizations --help 117 aprendizations --help
107 ``` 118 ```
108 119
109 -## Configuration  
110 -  
111 -### Database  
112 -  
113 -User data is maintained in a sqlite3 database which has to be created manually using the command `initdb-aprendizations`.  
114 -The database file should be located in the same directory as the main  
115 -YAML configuration file.  
116 -  
117 -For example, to run the included demo do:  
118 -  
119 -```sh  
120 -cd demo # contains a small example  
121 -initdb-aprendizations # show or initialize database  
122 -initdb-aprendizations --admin # add admin user  
123 -initdb-aprendizations inscricoes.csv # add students from CSV  
124 -initdb-aprendizations --add 1184 "Aladino da Silva" # add new user  
125 -initdb-aprendizations --update 1184 --pw alibaba # update password  
126 -initdb-aprendizations --help # for available options  
127 -```  
128 -  
129 -The default password is equal to the user name, if left undefined.  
130 -  
131 -  
132 ### SSL Certificates 120 ### SSL Certificates
133 121
134 We need certificates for https. Certificates can be self-signed or validated by a trusted authority. 122 We need certificates for https. Certificates can be self-signed or validated by a trusted authority.
@@ -150,7 +138,7 @@ Install the certbot from LetsEncrypt: @@ -150,7 +138,7 @@ Install the certbot from LetsEncrypt:
150 138
151 ```sh 139 ```sh
152 sudo pkg install py36-certbot # FreeBSD 140 sudo pkg install py36-certbot # FreeBSD
153 -sudo apt install certbot # Linux Ubuntu 141 +sudo apt install certbot # Ubuntu
154 ``` 142 ```
155 143
156 To generate or renew the certificates, ports 80 and 443 have to be accessible. The firewall and webserver have to be stopped. 144 To generate or renew the certificates, ports 80 and 443 have to be accessible. The firewall and webserver have to be stopped.
@@ -168,6 +156,30 @@ sudo cp /usr/local/etc/letsencrypt/live/www.example.com/privkey.pem . @@ -168,6 +156,30 @@ sudo cp /usr/local/etc/letsencrypt/live/www.example.com/privkey.pem .
168 chmod 400 cert.pem privkey.pem 156 chmod 400 cert.pem privkey.pem
169 ``` 157 ```
170 158
  159 +
  160 +## Configuration
  161 +
  162 +### Database
  163 +
  164 +User data is maintained in a sqlite3 database which has to be created manually using the `initdb-aprendizations` command.
  165 +The database file should be located in the same directory as the main
  166 +YAML configuration file.
  167 +
  168 +For example, to run the included demo do:
  169 +
  170 +```sh
  171 +cd demo # contains a small example
  172 +initdb-aprendizations # show or initialize database
  173 +initdb-aprendizations --admin # add admin user
  174 +initdb-aprendizations inscricoes.csv # add students from CSV
  175 +initdb-aprendizations --add 1184 "Aladino da Silva" # add new user
  176 +initdb-aprendizations --update 1184 --pw alibaba # update password
  177 +initdb-aprendizations --help # for available options
  178 +```
  179 +
  180 +The default password is equal to the user name, if left undefined.
  181 +
  182 +
171 ### Running the demo 183 ### Running the demo
172 184
173 The application includes a small example in `demo/demo.yaml` that can be used for initial testing. Run it with 185 The application includes a small example in `demo/demo.yaml` that can be used for initial testing. Run it with
@@ -178,7 +190,7 @@ aprendizations demo.yaml @@ -178,7 +190,7 @@ aprendizations demo.yaml
178 ``` 190 ```
179 191
180 Open the browser at [https://127.0.0.1:8443](https://127.0.0.1:8443). 192 Open the browser at [https://127.0.0.1:8443](https://127.0.0.1:8443).
181 -If it everything looks good, check at the correct address 193 +If everything looks good, check at the correct address
182 `https://www.example.com:8443`. 194 `https://www.example.com:8443`.
183 The option `--debug` provides more verbose logging and might 195 The option `--debug` provides more verbose logging and might
184 be useful during testing. 196 be useful during testing.
@@ -198,7 +210,7 @@ rdr on $ext_if proto tcp from any to any port 80 -> 127.0.0.1 port 8080 @@ -198,7 +210,7 @@ rdr on $ext_if proto tcp from any to any port 80 -> 127.0.0.1 port 8080
198 rdr on $ext_if proto tcp from any to any port 443 -> 127.0.0.1 port 8443 210 rdr on $ext_if proto tcp from any to any port 443 -> 127.0.0.1 port 8443
199 ``` 211 ```
200 212
201 -Under virtualbox with guest additions use `ext_if="vtnet0"`. 213 +Virtualbox with guest additions uses `ext_if="vtnet0"`.
202 214
203 Edit `/etc/rc.conf` 215 Edit `/etc/rc.conf`
204 216
@@ -228,6 +240,17 @@ Make sure the following steps have been done: @@ -228,6 +240,17 @@ Make sure the following steps have been done:
228 - (optional) configure the firewall to do port forwarding 240 - (optional) configure the firewall to do port forwarding
229 - run `aprendizations demo.yaml --check` 241 - run `aprendizations demo.yaml --check`
230 242
  243 +## Keeping aprendizations updated
  244 +
  245 +To update aprendizations to the latest version do:
  246 +
  247 +```sh
  248 +cd aprendizations
  249 +git pull # get latest version
  250 +npm update # update javascript libraries
  251 +pip install -U . # updates installed version to latest
  252 +```
  253 +
231 ## Troubleshooting 254 ## Troubleshooting
232 255
233 To help with troubleshooting, use the option `--debug` when running the server. 256 To help with troubleshooting, use the option `--debug` when running the server.
aprendizations/learnapp.py
@@ -79,7 +79,7 @@ class LearnApp(object): @@ -79,7 +79,7 @@ class LearnApp(object):
79 79
80 errors = 0 80 errors = 0
81 for qref in self.factory: 81 for qref in self.factory:
82 - logger.debug(f'[sanity_check_questions] Checking "{qref}"...') 82 + logger.debug(f'checking {qref}...')
83 try: 83 try:
84 q = self.factory[qref].generate() 84 q = self.factory[qref].generate()
85 except Exception: 85 except Exception:
@@ -195,9 +195,9 @@ class LearnApp(object): @@ -195,9 +195,9 @@ class LearnApp(object):
195 # ------------------------------------------------------------------------ 195 # ------------------------------------------------------------------------
196 async def check_answer(self, uid, answer): 196 async def check_answer(self, uid, answer):
197 knowledge = self.online[uid]['state'] 197 knowledge = self.online[uid]['state']
  198 + topic = knowledge.get_current_topic()
198 q, action = await knowledge.check_answer(answer) # may move questions 199 q, action = await knowledge.check_answer(answer) # may move questions
199 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') 200 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"')
200 - topic = knowledge.get_current_topic()  
201 201
202 # always save grade of answered question 202 # always save grade of answered question
203 with self.db_session() as s: 203 with self.db_session() as s:
@@ -208,7 +208,7 @@ class LearnApp(object): @@ -208,7 +208,7 @@ class LearnApp(object):
208 finishtime=str(q['finish_time']), 208 finishtime=str(q['finish_time']),
209 student_id=uid, 209 student_id=uid,
210 topic_id=topic)) 210 topic_id=topic))
211 - logger.debug(f'[check_answer] Saved "{q["ref"]}" into database') 211 + logger.debug(f'db insert answer of {q["ref"]}')
212 212
213 if knowledge.topic_has_finished(): 213 if knowledge.topic_has_finished():
214 # finished topic, save into database 214 # finished topic, save into database
@@ -222,7 +222,7 @@ class LearnApp(object): @@ -222,7 +222,7 @@ class LearnApp(object):
222 .one_or_none() 222 .one_or_none()
223 if a is None: 223 if a is None:
224 # insert new studenttopic into database 224 # insert new studenttopic into database
225 - logger.debug('[check_answer] Database insert studenttopic') 225 + logger.debug('db insert studenttopic')
226 t = s.query(Topic).get(topic) 226 t = s.query(Topic).get(topic)
227 u = s.query(Student).get(uid) 227 u = s.query(Student).get(uid)
228 # association object 228 # association object
@@ -231,14 +231,12 @@ class LearnApp(object): @@ -231,14 +231,12 @@ class LearnApp(object):
231 u.topics.append(a) 231 u.topics.append(a)
232 else: 232 else:
233 # update studenttopic in database 233 # update studenttopic in database
234 - logger.debug('[check_answer] Database update studenttopic') 234 + logger.debug(f'db update studenttopic to level {level}')
235 a.level = level 235 a.level = level
236 a.date = date 236 a.date = date
237 237
238 s.add(a) 238 s.add(a)
239 239
240 - logger.debug(f'[check_answer] Saved topic "{topic}" into database')  
241 -  
242 return q, action 240 return q, action
243 241
244 # ------------------------------------------------------------------------ 242 # ------------------------------------------------------------------------
@@ -347,7 +345,7 @@ class LearnApp(object): @@ -347,7 +345,7 @@ class LearnApp(object):
347 # Buils dictionary of question factories 345 # Buils dictionary of question factories
348 # ------------------------------------------------------------------------ 346 # ------------------------------------------------------------------------
349 def make_factory(self) -> Dict[str, QFactory]: 347 def make_factory(self) -> Dict[str, QFactory]:
350 - logger.info('Building questions factory...') 348 + logger.info('Building questions factory:')
351 factory = {} # {'qref': QFactory()} 349 factory = {} # {'qref': QFactory()}
352 g = self.deps 350 g = self.deps
353 for tref in g.nodes(): 351 for tref in g.nodes():
aprendizations/main.py
@@ -4,34 +4,17 @@ @@ -4,34 +4,17 @@
4 import argparse 4 import argparse
5 import logging 5 import logging
6 from os import environ, path 6 from os import environ, path
7 -import signal  
8 import ssl 7 import ssl
9 import sys 8 import sys
10 9
11 -# third party libraries  
12 -import tornado  
13 -  
14 # this project 10 # this project
15 from .learnapp import LearnApp, DatabaseUnusableError 11 from .learnapp import LearnApp, DatabaseUnusableError
16 -from .serve import WebApplication 12 +from .serve import run_webserver
17 from .tools import load_yaml 13 from .tools import load_yaml
18 from . import APP_NAME, APP_VERSION 14 from . import APP_NAME, APP_VERSION
19 15
20 16
21 # ---------------------------------------------------------------------------- 17 # ----------------------------------------------------------------------------
22 -# Signal handler to catch Ctrl-C and abort server  
23 -# ----------------------------------------------------------------------------  
24 -def signal_handler(signal, frame):  
25 - r = input(' --> Stop webserver? (yes/no) ').lower()  
26 - if r == 'yes':  
27 - tornado.ioloop.IOLoop.current().stop()  
28 - logging.critical('Webserver stopped.')  
29 - sys.exit(0)  
30 - else:  
31 - logging.info('Abort canceled...')  
32 -  
33 -  
34 -# ----------------------------------------------------------------------------  
35 def parse_cmdline_arguments(): 18 def parse_cmdline_arguments():
36 argparser = argparse.ArgumentParser( 19 argparser = argparse.ArgumentParser(
37 description='Server for online learning. Students and topics ' 20 description='Server for online learning. Students and topics '
@@ -91,7 +74,7 @@ def get_logger_config(debug=False): @@ -91,7 +74,7 @@ def get_logger_config(debug=False):
91 'version': 1, 74 'version': 1,
92 'formatters': { 75 'formatters': {
93 'standard': { 76 'standard': {
94 - 'format': '%(asctime)s | %(levelname)-10s | %(message)s', 77 + 'format': '%(asctime)s | %(levelname)-8s | %(message)s',
95 'datefmt': '%Y-%m-%d %H:%M:%S', 78 'datefmt': '%Y-%m-%d %H:%M:%S',
96 }, 79 },
97 }, 80 },
@@ -115,14 +98,14 @@ def get_logger_config(debug=False): @@ -115,14 +98,14 @@ def get_logger_config(debug=False):
115 'handlers': ['default'], 98 'handlers': ['default'],
116 'level': level, 99 'level': level,
117 'propagate': False, 100 'propagate': False,
118 - } for module in ['learnapp', 'models', 'factory', 'questions',  
119 - 'knowledge', 'tools']}) 101 + } for module in ['learnapp', 'models', 'factory', 'tools', 'serve',
  102 + 'questions', 'student']})
120 103
121 return load_yaml(config_file, default=default_config) 104 return load_yaml(config_file, default=default_config)
122 105
123 106
124 # ---------------------------------------------------------------------------- 107 # ----------------------------------------------------------------------------
125 -# Tornado web server 108 +# Start application and webserver
126 # ---------------------------------------------------------------------------- 109 # ----------------------------------------------------------------------------
127 def main(): 110 def main():
128 # --- Commandline argument parsing 111 # --- Commandline argument parsing
@@ -144,34 +127,6 @@ def main(): @@ -144,34 +127,6 @@ def main():
144 127
145 logging.info('====================== Start Logging ======================') 128 logging.info('====================== Start Logging ======================')
146 129
147 - # --- start application  
148 - logging.info('Starting App...')  
149 - try:  
150 - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,  
151 - check=arg.check)  
152 - except DatabaseUnusableError:  
153 - logging.critical('Failed to start application.')  
154 - print('--------------------------------------------------------------')  
155 - print('Could not find a usable database. Use one of the follwing ')  
156 - print('commands to initialize: ')  
157 - print(' ')  
158 - print(' initdb-aprendizations --admin # add admin ')  
159 - print(' initdb-aprendizations -a 86 "Max Smart" # add student ')  
160 - print(' initdb-aprendizations students.csv # add many students')  
161 - print('--------------------------------------------------------------')  
162 - sys.exit(1)  
163 - except Exception:  
164 - logging.critical('Failed to start application.')  
165 - sys.exit(1)  
166 -  
167 - # --- create web application  
168 - logging.info('Starting Web App (tornado)...')  
169 - try:  
170 - webapp = WebApplication(learnapp, debug=arg.debug)  
171 - except Exception:  
172 - logging.critical('Failed to start web application.')  
173 - sys.exit(1)  
174 -  
175 # --- get SSL certificates 130 # --- get SSL certificates
176 if 'XDG_DATA_HOME' in environ: 131 if 'XDG_DATA_HOME' in environ:
177 certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs') 132 certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')
@@ -186,44 +141,46 @@ def main(): @@ -186,44 +141,46 @@ def main():
186 logging.critical(f'SSL certificates missing in {certs_dir}') 141 logging.critical(f'SSL certificates missing in {certs_dir}')
187 print('--------------------------------------------------------------') 142 print('--------------------------------------------------------------')
188 print('Certificates should be issued by a certificate authority (CA),') 143 print('Certificates should be issued by a certificate authority (CA),')
189 - print('such as https://letsencrypt.org, and then copied to: ')  
190 - print(' ')  
191 - print(f' {certs_dir:<62}')  
192 - print(' ') 144 + print('such as https://letsencrypt.org. ')
193 print('For testing purposes a selfsigned certificate can be generated') 145 print('For testing purposes a selfsigned certificate can be generated')
194 print('locally by running: ') 146 print('locally by running: ')
195 print(' ') 147 print(' ')
196 print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ') 148 print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ')
197 print(' -out cert.pem -days 365 -nodes ') 149 print(' -out cert.pem -days 365 -nodes ')
198 print(' ') 150 print(' ')
  151 + print('Copy the cert.pem and privkey.pem files to: ')
  152 + print(' ')
  153 + print(f' {certs_dir:<62}')
  154 + print(' ')
  155 + print('(See README.md for more information) ')
199 print('--------------------------------------------------------------') 156 print('--------------------------------------------------------------')
200 sys.exit(1) 157 sys.exit(1)
  158 + else:
  159 + logging.info('SSL certificates loaded')
201 160
202 - # --- create webserver 161 + # --- start application
203 try: 162 try:
204 - httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)  
205 - except ValueError:  
206 - logging.critical('Certificates cert.pem and privkey.pem not found') 163 + learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,
  164 + check=arg.check)
  165 + except DatabaseUnusableError:
  166 + logging.critical('Failed to start application.')
  167 + print('--------------------------------------------------------------')
  168 + print('Could not find a usable database. Use one of the follwing ')
  169 + print('commands to initialize: ')
  170 + print(' ')
  171 + print(' initdb-aprendizations --admin # add admin ')
  172 + print(' initdb-aprendizations -a 86 "Max Smart" # add student ')
  173 + print(' initdb-aprendizations students.csv # add many students')
  174 + print('--------------------------------------------------------------')
207 sys.exit(1) 175 sys.exit(1)
208 -  
209 - try:  
210 - httpserver.listen(arg.port)  
211 - except OSError:  
212 - logging.critical(f'Cannot bind port {arg.port}. Already in use?') 176 + except Exception:
  177 + logging.critical('Failed to start application.')
213 sys.exit(1) 178 sys.exit(1)
  179 + else:
  180 + logging.info('Backend application started')
214 181
215 - logging.info(f'Listening on port {arg.port}.')  
216 -  
217 - # --- run webserver  
218 - signal.signal(signal.SIGINT, signal_handler)  
219 - logging.info('Webserver running. (Ctrl-C to stop)')  
220 -  
221 - try:  
222 - tornado.ioloop.IOLoop.current().start() # running...  
223 - except Exception:  
224 - logging.critical('Webserver stopped.')  
225 - tornado.ioloop.IOLoop.current().stop()  
226 - raise 182 + # --- run webserver forever
  183 + run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug)
227 184
228 185
229 # ---------------------------------------------------------------------------- 186 # ----------------------------------------------------------------------------
aprendizations/questions.py
@@ -461,7 +461,7 @@ class QFactory(object): @@ -461,7 +461,7 @@ class QFactory(object):
461 # i.e. a question object (radio, checkbox, ...). 461 # i.e. a question object (radio, checkbox, ...).
462 # ----------------------------------------------------------------------- 462 # -----------------------------------------------------------------------
463 def generate(self) -> Question: 463 def generate(self) -> Question:
464 - logger.debug(f'[QFactory.generate] "{self.question["ref"]}"...') 464 + logger.debug(f'generating {self.question["ref"]}...')
465 # Shallow copy so that script generated questions will not replace 465 # Shallow copy so that script generated questions will not replace
466 # the original generators 466 # the original generators
467 q = self.question.copy() 467 q = self.question.copy()
@@ -492,7 +492,7 @@ class QFactory(object): @@ -492,7 +492,7 @@ class QFactory(object):
492 492
493 # ----------------------------------------------------------------------- 493 # -----------------------------------------------------------------------
494 async def generate_async(self) -> Question: 494 async def generate_async(self) -> Question:
495 - logger.debug(f'[QFactory.generate_async] "{self.question["ref"]}"...') 495 + logger.debug(f'generating {self.question["ref"]}...')
496 # Shallow copy so that script generated questions will not replace 496 # Shallow copy so that script generated questions will not replace
497 # the original generators 497 # the original generators
498 q = self.question.copy() 498 q = self.question.copy()
@@ -520,5 +520,5 @@ class QFactory(object): @@ -520,5 +520,5 @@ class QFactory(object):
520 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') 520 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
521 raise 521 raise
522 else: 522 else:
523 - logger.debug(f'[generate_async] Done instance of {q["ref"]}') 523 + logger.debug('ok')
524 return qinstance 524 return qinstance
aprendizations/serve.py
@@ -6,6 +6,8 @@ import functools @@ -6,6 +6,8 @@ import functools
6 import logging.config 6 import logging.config
7 import mimetypes 7 import mimetypes
8 from os import path 8 from os import path
  9 +import signal
  10 +import sys
9 import uuid 11 import uuid
10 12
11 # third party libraries 13 # third party libraries
@@ -16,6 +18,9 @@ from tornado.escape import to_unicode @@ -16,6 +18,9 @@ from tornado.escape import to_unicode
16 from .tools import md_to_html 18 from .tools import md_to_html
17 from . import APP_NAME 19 from . import APP_NAME
18 20
  21 +# setup logger for this module
  22 +logger = logging.getLogger(__name__)
  23 +
19 24
20 # ---------------------------------------------------------------------------- 25 # ----------------------------------------------------------------------------
21 # Decorator used to restrict access to the administrator only 26 # Decorator used to restrict access to the administrator only
@@ -211,11 +216,11 @@ class FileHandler(BaseHandler): @@ -211,11 +216,11 @@ class FileHandler(BaseHandler):
211 with open(filepath, 'rb') as f: 216 with open(filepath, 'rb') as f:
212 data = f.read() 217 data = f.read()
213 except FileNotFoundError: 218 except FileNotFoundError:
214 - logging.error(f'File not found: {filepath}') 219 + logger.error(f'File not found: {filepath}')
215 except PermissionError: 220 except PermissionError:
216 - logging.error(f'No permission: {filepath}') 221 + logger.error(f'No permission: {filepath}')
217 except Exception: 222 except Exception:
218 - logging.error(f'Error reading: {filepath}') 223 + logger.error(f'Error reading: {filepath}')
219 raise 224 raise
220 else: 225 else:
221 self.set_header("Content-Type", content_type) 226 self.set_header("Content-Type", content_type)
@@ -244,7 +249,7 @@ class QuestionHandler(BaseHandler): @@ -244,7 +249,7 @@ class QuestionHandler(BaseHandler):
244 # --- get question to render 249 # --- get question to render
245 @tornado.web.authenticated 250 @tornado.web.authenticated
246 def get(self): 251 def get(self):
247 - logging.debug('[QuestionHandler.get]') 252 + logger.debug('[QuestionHandler.get]')
248 user = self.current_user 253 user = self.current_user
249 q = self.learn.get_current_question(user) 254 q = self.learn.get_current_question(user)
250 255
@@ -275,7 +280,7 @@ class QuestionHandler(BaseHandler): @@ -275,7 +280,7 @@ class QuestionHandler(BaseHandler):
275 # --- post answer, returns what to do next: shake, new_question, finished 280 # --- post answer, returns what to do next: shake, new_question, finished
276 @tornado.web.authenticated 281 @tornado.web.authenticated
277 async def post(self) -> None: 282 async def post(self) -> None:
278 - logging.debug('[QuestionHandler.post]') 283 + logger.debug('[QuestionHandler.post]')
279 user = self.current_user 284 user = self.current_user
280 answer = self.get_body_arguments('answer') # list 285 answer = self.get_body_arguments('answer') # list
281 286
@@ -283,7 +288,7 @@ class QuestionHandler(BaseHandler): @@ -283,7 +288,7 @@ class QuestionHandler(BaseHandler):
283 answer_qid = self.get_body_arguments('qid')[0] 288 answer_qid = self.get_body_arguments('qid')[0]
284 current_qid = self.learn.get_current_question_id(user) 289 current_qid = self.learn.get_current_question_id(user)
285 if answer_qid != current_qid: 290 if answer_qid != current_qid:
286 - logging.info(f'User {user} desynchronized questions') 291 + logger.info(f'User {user} desynchronized questions')
287 self.write({ 292 self.write({
288 'method': 'invalid', 293 'method': 'invalid',
289 'params': { 294 'params': {
@@ -299,14 +304,14 @@ class QuestionHandler(BaseHandler): @@ -299,14 +304,14 @@ class QuestionHandler(BaseHandler):
299 # --- answers are in a list. fix depending on question type 304 # --- answers are in a list. fix depending on question type
300 qtype = self.learn.get_student_question_type(user) 305 qtype = self.learn.get_student_question_type(user)
301 if qtype in ('success', 'information', 'info'): 306 if qtype in ('success', 'information', 'info'):
302 - answer = None 307 + ans = None
303 elif qtype == 'radio' and not answer: 308 elif qtype == 'radio' and not answer:
304 - answer = None 309 + ans = None
305 elif qtype != 'checkbox': # radio, text, textarea, ... 310 elif qtype != 'checkbox': # radio, text, textarea, ...
306 - answer = answer[0] 311 + ans = answer[0]
307 312
308 # --- check answer (nonblocking) and get corrected question and action 313 # --- check answer (nonblocking) and get corrected question and action
309 - q, action = await self.learn.check_answer(user, answer) 314 + q, action = await self.learn.check_answer(user, ans)
310 315
311 # --- built response to return 316 # --- built response to return
312 response = {'method': action, 'params': {}} 317 response = {'method': action, 'params': {}}
@@ -349,6 +354,59 @@ class QuestionHandler(BaseHandler): @@ -349,6 +354,59 @@ class QuestionHandler(BaseHandler):
349 'tries': q['tries'], 354 'tries': q['tries'],
350 } 355 }
351 else: 356 else:
352 - logging.error(f'Unknown action: {action}') 357 + logger.error(f'Unknown action: {action}')
353 358
354 self.write(response) 359 self.write(response)
  360 +
  361 +
  362 +# ----------------------------------------------------------------------------
  363 +# Signal handler to catch Ctrl-C and abort server
  364 +# ----------------------------------------------------------------------------
  365 +def signal_handler(signal, frame):
  366 + r = input(' --> Stop webserver? (yes/no) ').lower()
  367 + if r == 'yes':
  368 + tornado.ioloop.IOLoop.current().stop()
  369 + logger.critical('Webserver stopped.')
  370 + sys.exit(0)
  371 + else:
  372 + logger.info('Abort canceled...')
  373 +
  374 +
  375 +# ----------------------------------------------------------------------------
  376 +def run_webserver(app, ssl, port=8443, debug=False):
  377 + # --- create web application
  378 + try:
  379 + webapp = WebApplication(app, debug=debug)
  380 + except Exception:
  381 + logger.critical('Failed to start web application.')
  382 + sys.exit(1)
  383 + else:
  384 + logger.info('Web application started (tornado.web.Application)')
  385 +
  386 + # --- create tornado webserver
  387 + try:
  388 + httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl)
  389 + except ValueError:
  390 + logger.critical('Certificates cert.pem and privkey.pem not found')
  391 + sys.exit(1)
  392 + else:
  393 + logger.debug('HTTP server started')
  394 +
  395 + try:
  396 + httpserver.listen(port)
  397 + except OSError:
  398 + logger.critical(f'Cannot bind port {port}. Already in use?')
  399 + sys.exit(1)
  400 + else:
  401 + logger.info(f'HTTP server listening on port {port}')
  402 +
  403 + # --- run webserver
  404 + signal.signal(signal.SIGINT, signal_handler)
  405 + logger.info('Webserver running. (Ctrl-C to stop)')
  406 +
  407 + try:
  408 + tornado.ioloop.IOLoop.current().start() # running...
  409 + except Exception:
  410 + logger.critical('Webserver stopped.')
  411 + tornado.ioloop.IOLoop.current().stop()
  412 + raise
aprendizations/student.py
@@ -3,20 +3,26 @@ @@ -3,20 +3,26 @@
3 import random 3 import random
4 from datetime import datetime 4 from datetime import datetime
5 import logging 5 import logging
  6 +from typing import List, Optional
6 7
7 # third party libraries 8 # third party libraries
8 import networkx as nx 9 import networkx as nx
9 10
  11 +# this project
  12 +from .questions import Question
  13 +
  14 +
10 # setup logger for this module 15 # setup logger for this module
11 logger = logging.getLogger(__name__) 16 logger = logging.getLogger(__name__)
12 17
13 18
14 # ---------------------------------------------------------------------------- 19 # ----------------------------------------------------------------------------
15 -# kowledge state of each student....?? 20 +# kowledge state of a student
16 # Contains: 21 # Contains:
17 -# state - dict of topics with state of unlocked topics 22 +# state - dict of unlocked topics and their levels
18 # deps - access to dependency graph shared between students 23 # deps - access to dependency graph shared between students
19 -# topic_sequence - list with the order of recommended topics 24 +# topic_sequence - list with the recommended topic sequence
  25 +# current_topic - nameref of the current topic
20 # ---------------------------------------------------------------------------- 26 # ----------------------------------------------------------------------------
21 class StudentState(object): 27 class StudentState(object):
22 # ======================================================================= 28 # =======================================================================
@@ -34,13 +40,13 @@ class StudentState(object): @@ -34,13 +40,13 @@ class StudentState(object):
34 40
35 # ------------------------------------------------------------------------ 41 # ------------------------------------------------------------------------
36 # Updates the proficiency levels of the topics, with forgetting factor 42 # Updates the proficiency levels of the topics, with forgetting factor
37 - # FIXME no dependencies are considered yet...  
38 # ------------------------------------------------------------------------ 43 # ------------------------------------------------------------------------
39 def update_topic_levels(self): 44 def update_topic_levels(self):
40 now = datetime.now() 45 now = datetime.now()
41 for tref, s in self.state.items(): 46 for tref, s in self.state.items():
42 dt = now - s['date'] 47 dt = now - s['date']
43 - s['level'] *= 0.98 ** dt.days # forgetting factor 48 + forgetting_factor = self.deps.node[tref]['forgetting_factor']
  49 + s['level'] *= forgetting_factor ** dt.days # forgetting factor
44 50
45 # ------------------------------------------------------------------------ 51 # ------------------------------------------------------------------------
46 # Unlock topics whose dependencies are satisfied (> min_level) 52 # Unlock topics whose dependencies are satisfied (> min_level)
@@ -51,13 +57,13 @@ class StudentState(object): @@ -51,13 +57,13 @@ class StudentState(object):
51 pred = self.deps.predecessors(topic) 57 pred = self.deps.predecessors(topic)
52 min_level = self.deps.node[topic]['min_level'] 58 min_level = self.deps.node[topic]['min_level']
53 if all(d in self.state and self.state[d]['level'] > min_level 59 if all(d in self.state and self.state[d]['level'] > min_level
54 - for d in pred): # all deps are greater than min_level 60 + for d in pred): # all deps are greater than min_level
55 61
56 self.state[topic] = { 62 self.state[topic] = {
57 'level': 0.0, # unlocked 63 'level': 0.0, # unlocked
58 'date': datetime.now() 64 'date': datetime.now()
59 } 65 }
60 - logger.debug(f'[unlock_topics] Unlocked "{topic}".') 66 + logger.debug(f'unlocked "{topic}"')
61 # else: # lock this topic if deps do not satisfy min_level 67 # else: # lock this topic if deps do not satisfy min_level
62 # del self.state[topic] 68 # del self.state[topic]
63 69
@@ -66,16 +72,17 @@ class StudentState(object): @@ -66,16 +72,17 @@ class StudentState(object):
66 # questions: list of generated questions to do in the topic 72 # questions: list of generated questions to do in the topic
67 # current_question: the current question to be presented 73 # current_question: the current question to be presented
68 # ------------------------------------------------------------------------ 74 # ------------------------------------------------------------------------
69 - async def start_topic(self, topic):  
70 - logger.debug(f'[start_topic] topic "{topic}"') 75 + async def start_topic(self, topic: str):
  76 + logger.debug(f'starting "{topic}"')
71 77
  78 + # avoid regenerating questions in the middle of the current topic
72 if self.current_topic == topic: 79 if self.current_topic == topic:
73 logger.info('Restarting current topic is not allowed.') 80 logger.info('Restarting current topic is not allowed.')
74 return 81 return
75 82
76 # do not allow locked topics 83 # do not allow locked topics
77 if self.is_locked(topic): 84 if self.is_locked(topic):
78 - logger.debug(f'[start_topic] topic "{topic}" is locked') 85 + logger.debug(f'is locked "{topic}"')
79 return 86 return
80 87
81 # starting new topic 88 # starting new topic
@@ -89,18 +96,13 @@ class StudentState(object): @@ -89,18 +96,13 @@ class StudentState(object):
89 questions = random.sample(t['questions'], k=k) 96 questions = random.sample(t['questions'], k=k)
90 else: 97 else:
91 questions = t['questions'][:k] 98 questions = t['questions'][:k]
92 - logger.debug(f'[start_topic] questions: {", ".join(questions)}') 99 + logger.debug(f'selected questions: {", ".join(questions)}')
93 100
94 - # synchronous  
95 - # self.questions = [self.factory[ref].generate()  
96 - # for ref in questions]  
97 -  
98 - # asynchronous:  
99 self.questions = [await self.factory[ref].generate_async() 101 self.questions = [await self.factory[ref].generate_async()
100 for ref in questions] 102 for ref in questions]
101 103
102 n = len(self.questions) 104 n = len(self.questions)
103 - logger.debug(f'[start_topic] generated {n} questions') 105 + logger.debug(f'generated {n} questions')
104 106
105 # get first question 107 # get first question
106 self.next_question() 108 self.next_question()
@@ -111,14 +113,14 @@ class StudentState(object): @@ -111,14 +113,14 @@ class StudentState(object):
111 # The current topic is unchanged. 113 # The current topic is unchanged.
112 # ------------------------------------------------------------------------ 114 # ------------------------------------------------------------------------
113 def finish_topic(self): 115 def finish_topic(self):
114 - logger.debug(f'[finish_topic] current_topic {self.current_topic}') 116 + logger.debug(f'finished {self.current_topic}')
115 117
116 self.state[self.current_topic] = { 118 self.state[self.current_topic] = {
117 'date': datetime.now(), 119 'date': datetime.now(),
118 'level': self.correct_answers / (self.correct_answers + 120 'level': self.correct_answers / (self.correct_answers +
119 self.wrong_answers) 121 self.wrong_answers)
120 } 122 }
121 - # self.current_topic = None 123 + self.current_topic = None
122 self.unlock_topics() 124 self.unlock_topics()
123 125
124 # ------------------------------------------------------------------------ 126 # ------------------------------------------------------------------------
@@ -128,13 +130,12 @@ class StudentState(object): @@ -128,13 +130,12 @@ class StudentState(object):
128 # - if wrong, counts number of tries. If exceeded, moves on. 130 # - if wrong, counts number of tries. If exceeded, moves on.
129 # ------------------------------------------------------------------------ 131 # ------------------------------------------------------------------------
130 async def check_answer(self, answer): 132 async def check_answer(self, answer):
131 - logger.debug('[check_answer]')  
132 -  
133 q = self.current_question 133 q = self.current_question
134 q['answer'] = answer 134 q['answer'] = answer
135 q['finish_time'] = datetime.now() 135 q['finish_time'] = datetime.now()
  136 + logger.debug(f'checking answer of {q["ref"]}...')
136 await q.correct_async() 137 await q.correct_async()
137 - logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}') 138 + logger.debug(f'grade = {q["grade"]:.2}')
138 139
139 if q['grade'] > 0.999: 140 if q['grade'] > 0.999:
140 self.correct_answers += 1 141 self.correct_answers += 1
@@ -150,7 +151,7 @@ class StudentState(object): @@ -150,7 +151,7 @@ class StudentState(object):
150 else: 151 else:
151 action = 'wrong' 152 action = 'wrong'
152 if self.current_question['append_wrong']: 153 if self.current_question['append_wrong']:
153 - logger.debug('[check_answer] Wrong, append new instance') 154 + logger.debug('wrong answer, append new question')
154 self.questions.append(self.factory[q['ref']].generate()) 155 self.questions.append(self.factory[q['ref']].generate())
155 self.next_question() 156 self.next_question()
156 157
@@ -158,9 +159,9 @@ class StudentState(object): @@ -158,9 +159,9 @@ class StudentState(object):
158 return q, action 159 return q, action
159 160
160 # ------------------------------------------------------------------------ 161 # ------------------------------------------------------------------------
161 - # Move to next question 162 + # Move to next question, or None
162 # ------------------------------------------------------------------------ 163 # ------------------------------------------------------------------------
163 - def next_question(self): 164 + def next_question(self) -> Optional[Question]:
164 try: 165 try:
165 self.current_question = self.questions.pop(0) 166 self.current_question = self.questions.pop(0)
166 except IndexError: 167 except IndexError:
@@ -171,7 +172,7 @@ class StudentState(object): @@ -171,7 +172,7 @@ class StudentState(object):
171 default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] 172 default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
172 maxtries = self.current_question.get('max_tries', default_maxtries) 173 maxtries = self.current_question.get('max_tries', default_maxtries)
173 self.current_question['tries'] = maxtries 174 self.current_question['tries'] = maxtries
174 - logger.debug(f'[next_question] "{self.current_question["ref"]}"') 175 + logger.debug(f'current_question = {self.current_question["ref"]}')
175 176
176 return self.current_question # question or None 177 return self.current_question # question or None
177 178
@@ -179,28 +180,35 @@ class StudentState(object): @@ -179,28 +180,35 @@ class StudentState(object):
179 # pure functions of the state (no side effects) 180 # pure functions of the state (no side effects)
180 # ======================================================================== 181 # ========================================================================
181 182
182 - def topic_has_finished(self):  
183 - return self.current_question is None 183 + def topic_has_finished(self) -> bool:
  184 + return self.current_topic is None
184 185
185 # ------------------------------------------------------------------------ 186 # ------------------------------------------------------------------------
186 # compute recommended sequence of topics ['a', 'b', ...] 187 # compute recommended sequence of topics ['a', 'b', ...]
187 # ------------------------------------------------------------------------ 188 # ------------------------------------------------------------------------
188 - def recommend_topic_sequence(self, target=None): 189 + def recommend_topic_sequence(self, target: str = '') -> List[str]:
189 tt = list(nx.topological_sort(self.deps)) 190 tt = list(nx.topological_sort(self.deps))
  191 + try:
  192 + idx = tt.index(target)
  193 + except ValueError:
  194 + pass
  195 + else:
  196 + del tt[idx:]
  197 +
190 unlocked = [t for t in tt if t in self.state] 198 unlocked = [t for t in tt if t in self.state]
191 locked = [t for t in tt if t not in unlocked] 199 locked = [t for t in tt if t not in unlocked]
192 return unlocked + locked 200 return unlocked + locked
193 201
194 # ------------------------------------------------------------------------ 202 # ------------------------------------------------------------------------
195 - def get_current_question(self): 203 + def get_current_question(self) -> Optional[Question]:
196 return self.current_question 204 return self.current_question
197 205
198 # ------------------------------------------------------------------------ 206 # ------------------------------------------------------------------------
199 - def get_current_topic(self): 207 + def get_current_topic(self) -> Optional[str]:
200 return self.current_topic 208 return self.current_topic
201 209
202 # ------------------------------------------------------------------------ 210 # ------------------------------------------------------------------------
203 - def is_locked(self, topic): 211 + def is_locked(self, topic: str) -> bool:
204 return topic not in self.state 212 return topic not in self.state
205 213
206 # ------------------------------------------------------------------------ 214 # ------------------------------------------------------------------------
@@ -217,16 +225,16 @@ class StudentState(object): @@ -217,16 +225,16 @@ class StudentState(object):
217 } for ref in self.topic_sequence] 225 } for ref in self.topic_sequence]
218 226
219 # ------------------------------------------------------------------------ 227 # ------------------------------------------------------------------------
220 - def get_topic_progress(self): 228 + def get_topic_progress(self) -> float:
221 return self.correct_answers / (1 + self.correct_answers + 229 return self.correct_answers / (1 + self.correct_answers +
222 len(self.questions)) 230 len(self.questions))
223 231
224 # ------------------------------------------------------------------------ 232 # ------------------------------------------------------------------------
225 - def get_topic_level(self, topic): 233 + def get_topic_level(self, topic: str) -> float:
226 return self.state[topic]['level'] 234 return self.state[topic]['level']
227 235
228 # ------------------------------------------------------------------------ 236 # ------------------------------------------------------------------------
229 - def get_topic_date(self, topic): 237 + def get_topic_date(self, topic: str):
230 return self.state[topic]['date'] 238 return self.state[topic]['date']
231 239
232 # ------------------------------------------------------------------------ 240 # ------------------------------------------------------------------------
aprendizations/tools.py
@@ -154,7 +154,7 @@ def load_yaml(filename: str, default: Any = None) -&gt; Any: @@ -154,7 +154,7 @@ def load_yaml(filename: str, default: Any = None) -&gt; Any:
154 except yaml.YAMLError as e: 154 except yaml.YAMLError as e:
155 if hasattr(e, 'problem_mark'): 155 if hasattr(e, 'problem_mark'):
156 mark = e.problem_mark 156 mark = e.problem_mark
157 - logger.error(f'File "{filename}" near line {mark.line}, ' 157 + logger.error(f'File "{filename}" near line {mark.line+1}, '
158 f'column {mark.column+1}') 158 f'column {mark.column+1}')
159 else: 159 else:
160 logger.error(f'File "{filename}"') 160 logger.error(f'File "{filename}"')
config/logger-debug.yaml
@@ -5,8 +5,8 @@ formatters: @@ -5,8 +5,8 @@ formatters:
5 void: 5 void:
6 format: '' 6 format: ''
7 standard: 7 standard:
8 - format: '%(asctime)s | %(levelname)-9s | %(name)-24s | %(thread)-15d | %(message)s'  
9 - datefmt: '%H:%M:%S' 8 + format: '%(asctime)s | %(thread)-15d | %(levelname)-8s | %(module)-10s | %(funcName)-20s | %(message)s'
  9 + # datefmt: '%H:%M:%S'
10 10
11 handlers: 11 handlers:
12 default: 12 default:
@@ -25,7 +25,7 @@ loggers: @@ -25,7 +25,7 @@ loggers:
25 level: 'DEBUG' 25 level: 'DEBUG'
26 propagate: false 26 propagate: false
27 27
28 - 'aprendizations.knowledge': 28 + 'aprendizations.student':
29 handlers: ['default'] 29 handlers: ['default']
30 level: 'DEBUG' 30 level: 'DEBUG'
31 propagate: false 31 propagate: false
@@ -44,3 +44,8 @@ loggers: @@ -44,3 +44,8 @@ loggers:
44 handlers: ['default'] 44 handlers: ['default']
45 level: 'DEBUG' 45 level: 'DEBUG'
46 propagate: false 46 propagate: false
  47 +
  48 + 'aprendizations.serve':
  49 + handlers: ['default']
  50 + level: 'DEBUG'
  51 + propagate: false
config/logger.yaml
@@ -5,7 +5,7 @@ formatters: @@ -5,7 +5,7 @@ formatters:
5 void: 5 void:
6 format: '' 6 format: ''
7 standard: 7 standard:
8 - format: '%(asctime)s | %(thread)-15d | %(levelname)-9s | %(message)s' 8 + format: '%(asctime)s | %(levelname)-8s | %(module)-10s | %(message)s'
9 datefmt: '%Y-%m-%d %H:%M:%S' 9 datefmt: '%Y-%m-%d %H:%M:%S'
10 10
11 handlers: 11 handlers:
@@ -25,7 +25,7 @@ loggers: @@ -25,7 +25,7 @@ loggers:
25 level: 'INFO' 25 level: 'INFO'
26 propagate: false 26 propagate: false
27 27
28 - 'aprendizations.knowledge': 28 + 'aprendizations.student':
29 handlers: ['default'] 29 handlers: ['default']
30 level: 'INFO' 30 level: 'INFO'
31 propagate: false 31 propagate: false
@@ -44,3 +44,8 @@ loggers: @@ -44,3 +44,8 @@ loggers:
44 handlers: ['default'] 44 handlers: ['default']
45 level: 'INFO' 45 level: 'INFO'
46 propagate: false 46 propagate: false
  47 +
  48 + 'aprendizations.serve':
  49 + handlers: ['default']
  50 + level: 'INFO'
  51 + propagate: false
demo/demo.yaml
@@ -3,8 +3,7 @@ @@ -3,8 +3,7 @@
3 title: Example 3 title: Example
4 database: students.db 4 database: students.db
5 5
6 -  
7 -# values applie to each topic, if undefined there 6 +# values applied to each topic, if undefined there
8 file: questions.yaml 7 file: questions.yaml
9 shuffle_questions: true 8 shuffle_questions: true
10 choose: 6 9 choose: 6
@@ -27,3 +26,4 @@ topics: @@ -27,3 +26,4 @@ topics:
27 name: Sistema solar 26 name: Sistema solar
28 deps: 27 deps:
29 - math 28 - math
  29 + forgetting_factor: 0.1