Commit f8d971dfc5417b7585b938a647b959a33d9b06a6

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

maintenance release:

- code cleaning and bug fixing.
- updates javascript libraries.
- some refactoring.
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 3
4 - quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) 4 - 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) 5 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
6 -- devia mostrar timout para o aluno saber a razao. 6 +- devia mostrar timeout para o aluno saber a razao.
7 - permitir configuracao para escolher entre static files locais ou remotos 7 - permitir configuracao para escolher entre static files locais ou remotos
8 - sqlalchemy.pool.impl.NullPool: Exception during reset or similar 8 - sqlalchemy.pool.impl.NullPool: Exception during reset or similar
9 sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. 9 sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread.
@@ -40,6 +40,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in @@ -40,6 +40,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in
40 40
41 # FIXED 41 # FIXED
42 42
  43 +- add aprendizatons --version
43 - 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 /. 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 /.
44 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. 45 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido.
45 - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes 46 - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes
1 # Getting Started 1 # Getting Started
2 2
  3 +To complete the installation we will need to perform the following steps:
3 4
4 -## Requirements 5 +1. install python3.7, pip and npm
  6 +1. download aprendizations from the repository
  7 +1. install javascript libraries (with npm)
  8 +1. install aprendizations (with pip)
  9 +1. initialize database
  10 +1. generate SSL certificates
  11 +1. configure the firewall (optional)
  12 +1. try running `aprendizations demo.yaml`
5 13
6 -This application requires python3.7+ and a few additional python packages.  
7 -It also uses npm (Node package management) to install javascript libraries. 14 +These steps are explained next in detail.
8 15
9 ### Install python3.7 with sqlite3 support and npm 16 ### Install python3.7 with sqlite3 support and npm
10 17
11 -This can be done using the system package management, downloaded from [http://www.python.org](), or compiled from sources. 18 +Python can be installed either from the system package management or compiled from sources.
12 19
13 -- Installing from the system package manager: 20 +#### Installing from the system package manager
14 21
15 ```sh 22 ```sh
16 -sudo port install python37 npm5 # MacOS 23 +sudo apt install python3.7 npm # Linux (Ubuntu)
17 sudo pkg install python37 py37-sqlite3 npm # FreeBSD 24 sudo pkg install python37 py37-sqlite3 npm # FreeBSD
18 -sudo apt install python3.7 npm # Linux 25 +sudo port install python37 npm6 # MacOS
19 ``` 26 ```
20 27
21 -- Installing from source: 28 +#### Installing from source
22 29
23 -Download from [http://www.python.org]() and 30 +Make sure that the build tools and libraries are installed:
24 31
25 ```sh 32 ```sh
26 -unxz Python-3.7.tar.xz  
27 -tar xvf Python-3.7.tar 33 +# Ubuntu:
  34 +sudo apt install build-essential libssl-dev zlib1g-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev libdb5.3-dev libbz2-dev libexpat1-dev liblzma-dev tk-dev libffi-dev
  35 +```
  36 +
  37 +Download python from [http://www.python.org]() and
  38 +
  39 +```sh
  40 +tar xvfJ Python-3.7.tar.xz
28 cd Python-3.7 41 cd Python-3.7
29 -./configure --prefix=$HOME/.local/bin 42 +./configure --prefix=$HOME/.local --enable-optimizations
30 make && make install 43 make && make install
31 ``` 44 ```
32 45
33 -This will install python locally under `~/.local/bin`. Make sure to add it to your `PATH` (edit `~/.profile` in MacOS or FreeBSD).  
34 - 46 +This will install python locally under `~/.local/bin`. Make sure to add it to your `PATH` in `~/.profile`. If `~/bin` is already in the path, you may just make a symbolic link `ln -s ~/.local/bin ~/bin`.
35 47
36 ### Install pip 48 ### Install pip
37 49
  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.
38 If the `pip` command is not yet installed, run one of these: 51 If the `pip` command is not yet installed, run one of these:
39 52
40 ```sh 53 ```sh
41 -sudo apt install python3.7-pip # Ubuntu 54 +sudo apt install python3.7-pip # Ubuntu 19.04+
  55 +python3.7 -m pip install pip # Ubuntu 18.04
42 sudo pkg py37-pip # FreeBSD 56 sudo pkg py37-pip # FreeBSD
43 sudo port install py37-pip # MacOS 57 sudo port install py37-pip # MacOS
44 -python3.7 -m ensurepip --user # otherwise  
45 ``` 58 ```
46 59
47 The latter will install `pip` in your user account under `~/.local/bin`. 60 The latter will install `pip` in your user account under `~/.local/bin`.
48 -In the end you should be able to run `pip3 --version` and  
49 -`python3 -c "import sqlite3"` without errors (sometimes `pip3` is `pip`,  
50 -`pip3.7` or `pip-3.7`). 61 +In the end you should be able to run `pip --version` and
  62 +`python3 -c "import sqlite3"` without errors.
  63 +Sometimes the `pip` command is named `pip3`,
  64 +`pip3.7` or `pip-3.7`.
51 65
52 -If you want to always install python modules on the user account (recommended),  
53 -edit the pip configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or 66 +Edit the configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or
54 `Library/Application Support/pip/pip.conf` (MacOS) and add the lines 67 `Library/Application Support/pip/pip.conf` (MacOS) and add the lines
55 68
56 ```ini 69 ```ini
@@ -58,69 +71,72 @@ edit the pip configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or @@ -58,69 +71,72 @@ edit the pip configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or
58 user = yes 71 user = yes
59 ``` 72 ```
60 73
61 -### Install python packages and javascript libraries: 74 +This will set pip to install modules in the user area (recommended).
62 75
63 -Replace USER by your bitbucket username: 76 +### Install aprendizations and dependencies:
64 77
65 ```sh 78 ```sh
66 -cd somewhere  
67 git clone https://git.xdi.uevora.pt/mjsb/aprendizations.git 79 git clone https://git.xdi.uevora.pt/mjsb/aprendizations.git
68 cd aprendizations 80 cd aprendizations
69 npm install # install javascript libraries 81 npm install # install javascript libraries
70 pip install . # install aprendizations and dependencies 82 pip install . # install aprendizations and dependencies
71 ``` 83 ```
72 84
  85 +Javascript libraries are initially installed in `aprendizations/node_modules` directory.
  86 +These libraries already have symbolic links from `aprendizations/aprendizations/static`.
  87 +
73 Python packages are usually installed in: 88 Python packages are usually installed in:
74 89
75 - `~/.local/lib/python3.7/site-packages/` in Linux/FreeBSD. 90 - `~/.local/lib/python3.7/site-packages/` in Linux/FreeBSD.
76 - `~/Library/python/3.7/lib/python/site-packages/` in MacOS. 91 - `~/Library/python/3.7/lib/python/site-packages/` in MacOS.
77 92
78 -Javascript libraries are installed in `aprendizations/node_modules` directory.  
79 -This libraries have symbolic links from `aprendizations/aprendizations/static`. 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.
80 94
81 -At this point aprendizations is installed in 95 +At this point, aprendizations is installed in
82 96
83 ```sh 97 ```sh
  98 +~/.local/bin # Linux/FreeBSD
84 ~/Library/Python/3.7/bin # MacOS 99 ~/Library/Python/3.7/bin # MacOS
85 -~/.local/bin # FreeBSD/Linux  
86 ``` 100 ```
87 101
88 -Make sure this directory is in your `$PATH`. 102 +and can be run from the terminal:
89 103
90 -The server can be run with the command `aprendizations` from the terminal. 104 +```sh
  105 +aprendizations --version
  106 +aprendizations --help
  107 +```
91 108
92 ## Configuration 109 ## Configuration
93 110
94 ### Database 111 ### Database
95 112
96 -The user data is maintained in a sqlite3 database file. We first need to create  
97 -the database.  
98 -At the moment, the database should be located in the same directory as the main  
99 -configuration file (see below). As an example, do 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:
100 118
101 ```sh 119 ```sh
102 cd demo # contains a small example 120 cd demo # contains a small example
103 initdb-aprendizations # show or initialize database 121 initdb-aprendizations # show or initialize database
104 initdb-aprendizations --admin # add admin user 122 initdb-aprendizations --admin # add admin user
105 initdb-aprendizations inscricoes.csv # add students from CSV 123 initdb-aprendizations inscricoes.csv # add students from CSV
106 -initdb-aprendizations --add 1184 "Aladino da Silva" # add user 124 +initdb-aprendizations --add 1184 "Aladino da Silva" # add new user
107 initdb-aprendizations --update 1184 --pw alibaba # update password 125 initdb-aprendizations --update 1184 --pw alibaba # update password
108 -initdb-aprendizations --help # for the available options 126 +initdb-aprendizations --help # for available options
109 ``` 127 ```
110 128
111 -The default password is equal to the user name used to login. 129 +The default password is equal to the user name, if left undefined.
112 130
113 131
114 ### SSL Certificates 132 ### SSL Certificates
115 133
116 -We need certificates for https. Certificates can be self-signed or validated by  
117 -a trusted authority. 134 +We need certificates for https. Certificates can be self-signed or validated by a trusted authority.
118 135
119 Self-signed can be used locally for development and testing, but browsers will 136 Self-signed can be used locally for development and testing, but browsers will
120 -complain. LetsEncrypt issues trusted and free certificates, but the server must  
121 -have a registered publicly accessible domain name. 137 +complain. LetsEncrypt issues trusted and free certificates, but the server must have a registered publicly accessible domain name.
122 138
123 -#### Selfsigned 139 +#### Generating selfsigned certificates
124 140
125 Generate a selfsigned certificate and place it in `~/.local/share/certs`. 141 Generate a selfsigned certificate and place it in `~/.local/share/certs`.
126 142
@@ -128,18 +144,20 @@ Generate a selfsigned certificate and place it in `~/.local/share/certs`. @@ -128,18 +144,20 @@ Generate a selfsigned certificate and place it in `~/.local/share/certs`.
128 openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes 144 openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes
129 ``` 145 ```
130 146
131 -#### LetsEncrypt 147 +#### LetsEncrypt certificates
  148 +
  149 +Install the certbot from LetsEncrypt:
132 150
133 ```sh 151 ```sh
134 -sudo pkg install py27-certbot # FreeBSD 152 +sudo pkg install py36-certbot # FreeBSD
  153 +sudo apt install certbot # Linux Ubuntu
135 ``` 154 ```
136 155
137 -Shutdown the firewall and any web server that might be running. Then run the script to generate the certificate: 156 +To generate or renew the certificates, ports 80 and 443 have to be accessible. The firewall and webserver have to be stopped.
138 157
139 ```sh 158 ```sh
140 -sudo service pf stop # disable pf firewall (FreeBSD)  
141 -sudo certbot certonly --standalone -d www.example.com  
142 -sudo service pf start # enable pf firewall 159 +sudo certbot certonly --standalone -d www.example.com # first time
  160 +sudo certbot renew # renew
143 ``` 161 ```
144 162
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: 163 Certificates are saved under `/usr/local/etc/letsencrypt/live/www.example.com/`. Copy them to `~/.local/share/certs` and change permissions to be readable:
@@ -150,47 +168,32 @@ sudo cp /usr/local/etc/letsencrypt/live/www.example.com/privkey.pem . @@ -150,47 +168,32 @@ sudo cp /usr/local/etc/letsencrypt/live/www.example.com/privkey.pem .
150 chmod 400 cert.pem privkey.pem 168 chmod 400 cert.pem privkey.pem
151 ``` 169 ```
152 170
153 -Renews can be done as follows:  
154 -  
155 -```sh  
156 -sudo service pf stop # shutdown firewall  
157 -sudo certbot renew  
158 -sudo service pf start # start firewall  
159 -```  
160 -  
161 -and then copy the `cert.pem` and `privkey.pem` files to `~/.local/share/certs` directory. Change permissions and ownership as appropriate. 171 +### Running the demo
162 172
163 -  
164 -### Testing  
165 -  
166 -The application includes a small example in `demo/demo.yaml`. Run it with 173 +The application includes a small example in `demo/demo.yaml` that can be used for initial testing. Run it with
167 174
168 ```sh 175 ```sh
169 cd demo 176 cd demo
170 aprendizations demo.yaml 177 aprendizations demo.yaml
171 ``` 178 ```
172 179
173 -and open a browser at [https://127.0.0.1:8443](https://127.0.0.1:8443).  
174 -If it everything looks good,  
175 -check at the correct address `https://www.example.com` (requires port forward  
176 -in the firewall). The option `--debug` provides more verbose logging and might  
177 -be useful during testing. The option `--check` generates all the questions once  
178 -before running the server to check for any obvious syntax error.  
179 - 180 +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
  182 +`https://www.example.com:8443`.
  183 +The option `--debug` provides more verbose logging and might
  184 +be useful during testing.
180 185
181 ### Firewall configuration 186 ### Firewall configuration
182 187
183 -Ports 80 and 443 are only usable by root. For security reasons it is better to  
184 -run the server as an unprivileged user on higher ports like 8080 for http and  
185 -8443 for https. For this, we can configure port forwarding in the firewall to  
186 -redirect incoming tcp traffic from 80 to 8080 and 443 to 8443. 188 +Ports 80 and 443 are only usable by root. For security reasons the server runs as an unprivileged user on port 8443 for https.
  189 +To access the server in the default https port (443), port forwarding can be configured in the firewall.
187 190
188 #### FreeBSD and pf 191 #### FreeBSD and pf
189 192
190 Edit `/etc/pf.conf`: 193 Edit `/etc/pf.conf`:
191 194
192 ```sh 195 ```sh
193 -ext_if="em0" # this should be the correct network interface 196 +ext_if="em0" # change em0 to the correct network interface
194 rdr on $ext_if proto tcp from any to any port 80 -> 127.0.0.1 port 8080 197 rdr on $ext_if proto tcp from any to any port 80 -> 127.0.0.1 port 8080
195 rdr on $ext_if proto tcp from any to any port 443 -> 127.0.0.1 port 8443 198 rdr on $ext_if proto tcp from any to any port 443 -> 127.0.0.1 port 8443
196 ``` 199 ```
@@ -212,6 +215,18 @@ pflog_logfile="/var/log/pflog" @@ -212,6 +215,18 @@ pflog_logfile="/var/log/pflog"
212 215
213 Reboot or `sudo service pf start`. 216 Reboot or `sudo service pf start`.
214 217
  218 +### Testing the system
  219 +
  220 +Make sure the following steps have been done:
  221 +
  222 +- installed python3.7, pip and npm
  223 +- git-cloned the aprendizations from the main repository
  224 +- installed javascript libraries with npm
  225 +- installed aprendizations with pip
  226 +- initialized database with at least 1 user
  227 +- generate and copy certificates to the appropriate place
  228 +- (optional) configure the firewall to do port forwarding
  229 +- run `aprendizations demo.yaml --check`
215 230
216 ## Troubleshooting 231 ## Troubleshooting
217 232
@@ -243,20 +258,23 @@ me:\ @@ -243,20 +258,23 @@ me:\
243 258
244 ## FAQ 259 ## FAQ
245 260
246 -- Which students did at least one topic? 261 +Common database manipulations:
247 262
248 ```sh 263 ```sh
249 -sqlite3 students.db "select distinct student_id from studenttopic" 264 +initdb-aprendizations -u 12345 --pw alibaba # reset student password
  265 +initdb-aprendizations -a 12345 --pw alibaba # add new student
250 ``` 266 ```
251 267
252 -- How many topics has each student done? 268 +Common database queries:
253 269
254 ```sh 270 ```sh
255 -sqlite3 students.db "select student_id, count(topic_id) from studenttopic group by student_id order by count(topic_id) desc"  
256 -``` 271 +# Which students did at least one topic?
  272 +sqlite3 students.db "select distinct student_id from studenttopic"
257 273
258 -- Which questions have more wrong answers? 274 +# How many topics has each student done?
  275 +sqlite3 students.db "select student_id, count(topic_id) from studenttopic group by student_id order by count(topic_id) desc"
259 276
260 -```sh 277 +# Which questions have more wrong answers?
261 sqlite3 students.db "select count(ref), ref from answers where grade<1.0 group by ref order by count(ref) desc" 278 sqlite3 students.db "select count(ref), ref from answers where grade<1.0 group by ref order by count(ref) desc"
262 -```  
263 \ No newline at end of file 279 \ No newline at end of file
  280 +```
  281 +
aprendizations/__init__.py
@@ -30,7 +30,7 @@ are progressively uncovered as the students progress. @@ -30,7 +30,7 @@ are progressively uncovered as the students progress.
30 ''' 30 '''
31 31
32 APP_NAME = 'aprendizations' 32 APP_NAME = 'aprendizations'
33 -APP_VERSION = '2019.05.dev3' 33 +APP_VERSION = '2019.07.dev1'
34 APP_DESCRIPTION = __doc__ 34 APP_DESCRIPTION = __doc__
35 35
36 __author__ = 'Miguel Barão' 36 __author__ = 'Miguel Barão'
aprendizations/knowledge.py
@@ -1,237 +0,0 @@ @@ -1,237 +0,0 @@
1 -  
2 -# python standard library  
3 -import random  
4 -from datetime import datetime  
5 -import logging  
6 -import asyncio  
7 -  
8 -# third party libraries  
9 -import networkx as nx  
10 -  
11 -# setup logger for this module  
12 -logger = logging.getLogger(__name__)  
13 -  
14 -  
15 -# ----------------------------------------------------------------------------  
16 -# kowledge state of each student....??  
17 -# Contains:  
18 -# state - dict of topics with state of unlocked topics  
19 -# deps - access to dependency graph shared between students  
20 -# topic_sequence - list with the order of recommended topics  
21 -# ----------------------------------------------------------------------------  
22 -class StudentKnowledge(object):  
23 - # =======================================================================  
24 - # methods that update state  
25 - # =======================================================================  
26 - def __init__(self, deps, factory, state={}):  
27 - self.deps = deps # shared dependency graph  
28 - self.factory = factory # question factory  
29 - self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}  
30 -  
31 - self.update_topic_levels() # applies forgetting factor  
32 - self.unlock_topics() # whose dependencies have been completed  
33 - self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]  
34 - self.current_topic = None  
35 -  
36 - # ------------------------------------------------------------------------  
37 - # Updates the proficiency levels of the topics, with forgetting factor  
38 - # FIXME no dependencies are considered yet...  
39 - # ------------------------------------------------------------------------  
40 - def update_topic_levels(self):  
41 - now = datetime.now()  
42 - for tref, s in self.state.items():  
43 - dt = now - s['date']  
44 - s['level'] *= 0.98 ** dt.days # forgetting factor 0.95 FIXME  
45 -  
46 - # ------------------------------------------------------------------------  
47 - # Unlock topics whose dependencies are satisfied (> min_level)  
48 - # ------------------------------------------------------------------------  
49 - def unlock_topics(self):  
50 - for topic in self.deps.nodes():  
51 - if topic not in self.state: # if locked  
52 - pred = self.deps.predecessors(topic)  
53 - min_level = self.deps.node[topic]['min_level']  
54 - if all(d in self.state and self.state[d]['level'] > min_level  
55 - for d in pred): # all deps are greater than min_level  
56 -  
57 - self.state[topic] = {  
58 - 'level': 0.0, # unlocked  
59 - 'date': datetime.now()  
60 - }  
61 - logger.debug(f'[unlock_topics] Unlocked "{topic}".')  
62 - # else: # lock this topic if deps do not satisfy min_level  
63 - # del self.state[topic]  
64 -  
65 - # ------------------------------------------------------------------------  
66 - # Start a new topic.  
67 - # questions: list of generated questions to do in the topic  
68 - # current_question: the current question to be presented  
69 - # ------------------------------------------------------------------------  
70 - async def start_topic(self, topic):  
71 - logger.debug(f'[start_topic] topic "{topic}"')  
72 -  
73 - if self.current_topic == topic:  
74 - logger.info('Restarting current topic is not allowed.')  
75 - return False  
76 -  
77 - # do not allow locked topics  
78 - if self.is_locked(topic):  
79 - logger.debug(f'[start_topic] topic "{topic}" is locked')  
80 - return False  
81 -  
82 - # starting new topic  
83 - self.current_topic = topic  
84 - self.correct_answers = 0  
85 - self.wrong_answers = 0  
86 -  
87 - t = self.deps.node[topic]  
88 - k = t['choose']  
89 - if t['shuffle_questions']:  
90 - questions = random.sample(t['questions'], k=k)  
91 - else:  
92 - questions = t['questions'][:k]  
93 - logger.debug(f'[start_topic] questions: {", ".join(questions)}')  
94 -  
95 - # synchronous  
96 - # self.questions = [self.factory[ref].generate()  
97 - # for ref in questions]  
98 -  
99 - # asynchronous:  
100 - self.questions = [await self.factory[ref].generate_async()  
101 - for ref in questions]  
102 -  
103 - # get first question  
104 - self.next_question()  
105 -  
106 - logger.debug(f'[start_topic] generated {len(self.questions)} questions')  
107 - return True  
108 -  
109 - # ------------------------------------------------------------------------  
110 - # The topic has finished and there are no more questions.  
111 - # The topic level is updated in state and unlocks are performed.  
112 - # The current topic is unchanged.  
113 - # ------------------------------------------------------------------------  
114 - def finish_topic(self):  
115 - logger.debug(f'[finish_topic] current_topic {self.current_topic}')  
116 -  
117 - self.state[self.current_topic] = {  
118 - 'date': datetime.now(),  
119 - 'level': self.correct_answers / (self.correct_answers +  
120 - self.wrong_answers)  
121 - }  
122 - # self.current_topic = None  
123 - self.unlock_topics()  
124 -  
125 - # ------------------------------------------------------------------------  
126 - # corrects current question with provided answer.  
127 - # implements the logic:  
128 - # - if answer ok, goes to next question  
129 - # - if wrong, counts number of tries. If exceeded, moves on.  
130 - # ------------------------------------------------------------------------  
131 - async def check_answer(self, answer):  
132 - logger.debug('[check_answer]')  
133 -  
134 - q = self.current_question  
135 - q['answer'] = answer  
136 - q['finish_time'] = datetime.now()  
137 - await q.correct_async()  
138 - logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')  
139 -  
140 - if q['grade'] > 0.999:  
141 - self.correct_answers += 1  
142 - self.next_question()  
143 - action = 'right'  
144 -  
145 - else:  
146 - self.wrong_answers += 1  
147 - self.current_question['tries'] -= 1  
148 -  
149 - if self.current_question['tries'] > 0:  
150 - action = 'try_again'  
151 - else:  
152 - action = 'wrong'  
153 - if self.current_question['append_wrong']:  
154 - logger.debug('[check_answer] Wrong, append new instance')  
155 - self.questions.append(self.factory[q['ref']].generate())  
156 - self.next_question()  
157 -  
158 - # returns corrected question (not new one) which might include comments  
159 - return q, action  
160 -  
161 - # ------------------------------------------------------------------------  
162 - # Move to next question  
163 - # ------------------------------------------------------------------------  
164 - def next_question(self):  
165 - try:  
166 - self.current_question = self.questions.pop(0)  
167 - except IndexError:  
168 - self.current_question = None  
169 - self.finish_topic()  
170 - else:  
171 - self.current_question['start_time'] = datetime.now()  
172 - default_maxtries = self.deps.nodes[self.current_topic]['max_tries']  
173 - maxtries = self.current_question.get('max_tries', default_maxtries)  
174 - self.current_question['tries'] = maxtries  
175 - logger.debug(f'[next_question] "{self.current_question["ref"]}"')  
176 -  
177 - return self.current_question # question or None  
178 -  
179 - # ========================================================================  
180 - # pure functions of the state (no side effects)  
181 - # ========================================================================  
182 -  
183 - def topic_has_finished(self):  
184 - return self.current_question is None  
185 -  
186 - # ------------------------------------------------------------------------  
187 - # compute recommended sequence of topics ['a', 'b', ...]  
188 - # ------------------------------------------------------------------------  
189 - def recommend_topic_sequence(self, target=None):  
190 - tt = list(nx.topological_sort(self.deps))  
191 - unlocked = [t for t in tt if t in self.state]  
192 - locked = [t for t in tt if t not in unlocked]  
193 - return unlocked + locked  
194 -  
195 - # ------------------------------------------------------------------------  
196 - def get_current_question(self):  
197 - return self.current_question  
198 -  
199 - # ------------------------------------------------------------------------  
200 - def get_current_topic(self):  
201 - return self.current_topic  
202 -  
203 - # ------------------------------------------------------------------------  
204 - def is_locked(self, topic):  
205 - return topic not in self.state  
206 -  
207 - # ------------------------------------------------------------------------  
208 - # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}  
209 - # Levels are in the interval [0, 1] if unlocked or None if locked.  
210 - # Topics unlocked but not yet done have level 0.0.  
211 - # ------------------------------------------------------------------------  
212 - def get_knowledge_state(self):  
213 - return [{  
214 - 'ref': ref,  
215 - 'type': self.deps.nodes[ref]['type'],  
216 - 'name': self.deps.nodes[ref]['name'],  
217 - 'level': self.state[ref]['level'] if ref in self.state else None  
218 - } for ref in self.topic_sequence]  
219 -  
220 - # ------------------------------------------------------------------------  
221 - def get_topic_progress(self):  
222 - return self.correct_answers / (1 + self.correct_answers +  
223 - len(self.questions))  
224 -  
225 - # ------------------------------------------------------------------------  
226 - def get_topic_level(self, topic):  
227 - return self.state[topic]['level']  
228 -  
229 - # ------------------------------------------------------------------------  
230 - def get_topic_date(self, topic):  
231 - return self.state[topic]['date']  
232 -  
233 - # ------------------------------------------------------------------------  
234 - # Recommends a topic to practice/learn from the state.  
235 - # ------------------------------------------------------------------------  
236 - # def get_recommended_topic(self): # FIXME untested  
237 - # return min(self.state.items(), key=lambda x: x[1]['level'])[0]  
aprendizations/learnapp.py
@@ -10,13 +10,12 @@ from typing import Dict @@ -10,13 +10,12 @@ from typing import Dict
10 10
11 # third party libraries 11 # third party libraries
12 import bcrypt 12 import bcrypt
13 -from sqlalchemy import create_engine, func  
14 -from sqlalchemy.orm import sessionmaker 13 +import sqlalchemy as sa
15 import networkx as nx 14 import networkx as nx
16 15
17 # this project 16 # this project
18 from .models import Student, Answer, Topic, StudentTopic 17 from .models import Student, Answer, Topic, StudentTopic
19 -from .knowledge import StudentKnowledge 18 +from .student import StudentState
20 from .questions import QFactory 19 from .questions import QFactory
21 from .tools import load_yaml 20 from .tools import load_yaml
22 21
@@ -29,6 +28,10 @@ class LearnException(Exception): @@ -29,6 +28,10 @@ class LearnException(Exception):
29 pass 28 pass
30 29
31 30
  31 +class DatabaseUnusableError(LearnException):
  32 + pass
  33 +
  34 +
32 # ============================================================================ 35 # ============================================================================
33 # LearnApp - application logic 36 # LearnApp - application logic
34 # ============================================================================ 37 # ============================================================================
@@ -44,8 +47,9 @@ class LearnApp(object): @@ -44,8 +47,9 @@ class LearnApp(object):
44 yield session 47 yield session
45 session.commit() 48 session.commit()
46 except Exception: 49 except Exception:
47 - logger.error('DB rollback!!!') 50 + logger.error('!!! Database rollback !!!')
48 session.rollback() 51 session.rollback()
  52 + raise
49 finally: 53 finally:
50 session.close() 54 session.close()
51 55
@@ -105,7 +109,7 @@ class LearnApp(object): @@ -105,7 +109,7 @@ class LearnApp(object):
105 logger.error(f'{errors:>6} errors found.') 109 logger.error(f'{errors:>6} errors found.')
106 raise LearnException('Sanity checks') 110 raise LearnException('Sanity checks')
107 else: 111 else:
108 - logger.info('No errors found.') 112 + logger.info(' 0 errors found.')
109 113
110 # ------------------------------------------------------------------------ 114 # ------------------------------------------------------------------------
111 # login 115 # login
@@ -151,8 +155,8 @@ class LearnApp(object): @@ -151,8 +155,8 @@ class LearnApp(object):
151 self.online[uid] = { 155 self.online[uid] = {
152 'number': uid, 156 'number': uid,
153 'name': name, 157 'name': name,
154 - 'state': StudentKnowledge(deps=self.deps, factory=self.factory,  
155 - state=state), 158 + 'state': StudentState(deps=self.deps, factory=self.factory,
  159 + state=state),
156 'counter': counter + 1, # counts simultaneous logins 160 'counter': counter + 1, # counts simultaneous logins
157 } 161 }
158 162
@@ -263,20 +267,20 @@ class LearnApp(object): @@ -263,20 +267,20 @@ class LearnApp(object):
263 f'database') 267 f'database')
264 268
265 # ------------------------------------------------------------------------ 269 # ------------------------------------------------------------------------
266 - # setup and check database 270 + # setup and check database contents
267 # ------------------------------------------------------------------------ 271 # ------------------------------------------------------------------------
268 def db_setup(self, db): 272 def db_setup(self, db):
269 logger.info(f'Checking database "{db}":') 273 logger.info(f'Checking database "{db}":')
270 - engine = create_engine(f'sqlite:///{db}', echo=False)  
271 - self.Session = sessionmaker(bind=engine) 274 + engine = sa.create_engine(f'sqlite:///{db}', echo=False)
  275 + self.Session = sa.orm.sessionmaker(bind=engine)
272 try: 276 try:
273 with self.db_session() as s: 277 with self.db_session() as s:
274 n = s.query(Student).count() 278 n = s.query(Student).count()
275 m = s.query(Topic).count() 279 m = s.query(Topic).count()
276 q = s.query(Answer).count() 280 q = s.query(Answer).count()
277 - except Exception as e:  
278 - logger.critical(f'Database "{db}" not usable!')  
279 - raise e 281 + except Exception:
  282 + logger.error(f'Database "{db}" not usable!')
  283 + raise DatabaseUnusableError()
280 else: 284 else:
281 logger.info(f'{n:6} students') 285 logger.info(f'{n:6} students')
282 logger.info(f'{m:6} topics') 286 logger.info(f'{m:6} topics')
@@ -306,7 +310,7 @@ class LearnApp(object): @@ -306,7 +310,7 @@ class LearnApp(object):
306 310
307 # iterate over topics and populate graph 311 # iterate over topics and populate graph
308 topics = config.get('topics', {}) 312 topics = config.get('topics', {})
309 - g = self.deps # the dependency graph 313 + g = self.deps # dependency graph
310 314
311 g.add_nodes_from(topics.keys()) 315 g.add_nodes_from(topics.keys())
312 for tref, attr in topics.items(): 316 for tref, attr in topics.items():
@@ -437,16 +441,16 @@ class LearnApp(object): @@ -437,16 +441,16 @@ class LearnApp(object):
437 total_topics = s.query(Topic).count() 441 total_topics = s.query(Topic).count()
438 442
439 # answer performance 443 # answer performance
440 - totalans = dict(s.query(Answer.student_id, func.count(Answer.ref)).  
441 - group_by(Answer.student_id).  
442 - all())  
443 - rightans = dict(s.query(Answer.student_id, func.count(Answer.ref)).  
444 - filter(Answer.grade == 1.0).  
445 - group_by(Answer.student_id).  
446 - all()) 444 + total = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
  445 + group_by(Answer.student_id).
  446 + all())
  447 + right = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)).
  448 + filter(Answer.grade == 1.0).
  449 + group_by(Answer.student_id).
  450 + all())
447 451
448 # compute percentage of right answers 452 # compute percentage of right answers
449 - perf = {uid: rightans.get(uid, 0.0)/totalans[uid] for uid in totalans} 453 + perf = {uid: right.get(uid, 0.0)/total[uid] for uid in total}
450 454
451 # compute topic progress 455 # compute topic progress
452 prog = {s[0]: 0.0 for s in students} 456 prog = {s[0]: 0.0 for s in students}
aprendizations/main.py 0 → 100644
@@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +# python standard library
  4 +import argparse
  5 +import logging
  6 +from os import environ, path
  7 +import signal
  8 +import ssl
  9 +import sys
  10 +
  11 +# third party libraries
  12 +import tornado
  13 +
  14 +# this project
  15 +from .learnapp import LearnApp, DatabaseUnusableError
  16 +from .serve import WebApplication
  17 +from .tools import load_yaml
  18 +from . import APP_NAME, APP_VERSION
  19 +
  20 +
  21 +# ----------------------------------------------------------------------------
  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():
  36 + argparser = argparse.ArgumentParser(
  37 + description='Server for online learning. Students and topics '
  38 + 'have to be previously configured. Please read the documentation '
  39 + 'included with this software before running the server.'
  40 + )
  41 +
  42 + argparser.add_argument(
  43 + 'conffile', type=str, nargs='*',
  44 + help='Topics configuration file in YAML format.'
  45 + )
  46 +
  47 + argparser.add_argument(
  48 + '--prefix', type=str, default='.',
  49 + help='Path where the topic directories can be found (default: .)'
  50 + )
  51 +
  52 + argparser.add_argument(
  53 + '--port', type=int, default=8443,
  54 + help='Port to be used by the HTTPS server (default: 8443)'
  55 + )
  56 +
  57 + argparser.add_argument(
  58 + '--db', type=str, default='students.db',
  59 + help='SQLite3 database file (default: students.db)'
  60 + )
  61 +
  62 + argparser.add_argument(
  63 + '--check', action='store_true',
  64 + help='Sanity check questions (can take awhile)'
  65 + )
  66 +
  67 + argparser.add_argument(
  68 + '--debug', action='store_true',
  69 + help='Enable debug mode'
  70 + )
  71 +
  72 + argparser.add_argument(
  73 + '--version', action='store_true',
  74 + help='Print version information'
  75 + )
  76 +
  77 + return argparser.parse_args()
  78 +
  79 +
  80 +# ----------------------------------------------------------------------------
  81 +def get_logger_config(debug=False):
  82 + if debug:
  83 + filename, level = 'logger-debug.yaml', 'DEBUG'
  84 + else:
  85 + filename, level = 'logger.yaml', 'INFO'
  86 +
  87 + config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
  88 + config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)
  89 +
  90 + default_config = {
  91 + 'version': 1,
  92 + 'formatters': {
  93 + 'standard': {
  94 + 'format': '%(asctime)s | %(levelname)-10s | %(message)s',
  95 + 'datefmt': '%Y-%m-%d %H:%M:%S',
  96 + },
  97 + },
  98 + 'handlers': {
  99 + 'default': {
  100 + 'level': level,
  101 + 'class': 'logging.StreamHandler',
  102 + 'formatter': 'standard',
  103 + 'stream': 'ext://sys.stdout',
  104 + },
  105 + },
  106 + 'loggers': {
  107 + '': { # configuration for serve.py
  108 + 'handlers': ['default'],
  109 + 'level': level,
  110 + },
  111 + },
  112 + }
  113 + default_config['loggers'].update({
  114 + APP_NAME+'.'+module: {
  115 + 'handlers': ['default'],
  116 + 'level': level,
  117 + 'propagate': False,
  118 + } for module in ['learnapp', 'models', 'factory', 'questions',
  119 + 'knowledge', 'tools']})
  120 +
  121 + return load_yaml(config_file, default=default_config)
  122 +
  123 +
  124 +# ----------------------------------------------------------------------------
  125 +# Tornado web server
  126 +# ----------------------------------------------------------------------------
  127 +def main():
  128 + # --- Commandline argument parsing
  129 + arg = parse_cmdline_arguments()
  130 +
  131 + if arg.version:
  132 + print(f'{APP_NAME} - {APP_VERSION}\nPython {sys.version}')
  133 + sys.exit(0)
  134 +
  135 + # --- Setup logging
  136 + logger_config = get_logger_config(arg.debug)
  137 + logging.config.dictConfig(logger_config)
  138 +
  139 + try:
  140 + logging.config.dictConfig(logger_config)
  141 + except Exception:
  142 + print('An error ocurred while setting up the logging system.')
  143 + sys.exit(1)
  144 +
  145 + logging.info('====================== Start Logging ======================')
  146 +
  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
  176 + if 'XDG_DATA_HOME' in environ:
  177 + certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')
  178 + else:
  179 + certs_dir = path.expanduser('~/.local/share/certs')
  180 +
  181 + ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  182 + try:
  183 + ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),
  184 + path.join(certs_dir, 'privkey.pem'))
  185 + except FileNotFoundError:
  186 + logging.critical(f'SSL certificates missing in {certs_dir}')
  187 + print('--------------------------------------------------------------')
  188 + 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(' ')
  193 + print('For testing purposes a selfsigned certificate can be generated')
  194 + print('locally by running: ')
  195 + print(' ')
  196 + print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ')
  197 + print(' -out cert.pem -days 365 -nodes ')
  198 + print(' ')
  199 + print('--------------------------------------------------------------')
  200 + sys.exit(1)
  201 +
  202 + # --- create webserver
  203 + 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')
  207 + 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?')
  213 + sys.exit(1)
  214 +
  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
  227 +
  228 +
  229 +# ----------------------------------------------------------------------------
  230 +if __name__ == "__main__":
  231 + main()
aprendizations/serve.py
1 -#!/usr/bin/env python3  
2 1
3 # python standard library 2 # python standard library
4 -import argparse  
5 import asyncio 3 import asyncio
6 import base64 4 import base64
7 import functools 5 import functools
8 import logging.config 6 import logging.config
9 import mimetypes 7 import mimetypes
10 -from os import path, environ  
11 -import signal  
12 -import ssl  
13 -import sys 8 +from os import path
14 import uuid 9 import uuid
15 10
16 -  
17 # third party libraries 11 # third party libraries
18 -import tornado.ioloop  
19 -import tornado.httpserver  
20 import tornado.web 12 import tornado.web
21 from tornado.escape import to_unicode 13 from tornado.escape import to_unicode
22 14
23 # this project 15 # this project
24 -from .learnapp import LearnApp  
25 -from .tools import load_yaml, md_to_html 16 +from .tools import md_to_html
26 from . import APP_NAME 17 from . import APP_NAME
27 18
28 19
@@ -187,7 +178,6 @@ class RootHandler(BaseHandler): @@ -187,7 +178,6 @@ class RootHandler(BaseHandler):
187 # ---------------------------------------------------------------------------- 178 # ----------------------------------------------------------------------------
188 # /topic/... 179 # /topic/...
189 # Start a given topic 180 # Start a given topic
190 -# FIXME should not change state...  
191 # ---------------------------------------------------------------------------- 181 # ----------------------------------------------------------------------------
192 class TopicHandler(BaseHandler): 182 class TopicHandler(BaseHandler):
193 @tornado.web.authenticated 183 @tornado.web.authenticated
@@ -198,12 +188,12 @@ class TopicHandler(BaseHandler): @@ -198,12 +188,12 @@ class TopicHandler(BaseHandler):
198 await self.learn.start_topic(uid, topic) 188 await self.learn.start_topic(uid, topic)
199 except KeyError: 189 except KeyError:
200 self.redirect('/') 190 self.redirect('/')
201 - else:  
202 - self.render('topic.html',  
203 - appname=APP_NAME,  
204 - uid=uid,  
205 - name=self.learn.get_student_name(uid),  
206 - ) 191 +
  192 + self.render('topic.html',
  193 + appname=APP_NAME,
  194 + uid=uid,
  195 + name=self.learn.get_student_name(uid),
  196 + )
207 197
208 198
209 # ---------------------------------------------------------------------------- 199 # ----------------------------------------------------------------------------
@@ -218,16 +208,16 @@ class FileHandler(BaseHandler): @@ -218,16 +208,16 @@ class FileHandler(BaseHandler):
218 content_type = mimetypes.guess_type(filename)[0] 208 content_type = mimetypes.guess_type(filename)[0]
219 209
220 try: 210 try:
221 - f = open(filepath, 'rb') 211 + with open(filepath, 'rb') as f:
  212 + data = f.read()
222 except FileNotFoundError: 213 except FileNotFoundError:
223 logging.error(f'File not found: {filepath}') 214 logging.error(f'File not found: {filepath}')
224 except PermissionError: 215 except PermissionError:
225 logging.error(f'No permission: {filepath}') 216 logging.error(f'No permission: {filepath}')
226 - except Exception as e:  
227 - raise e 217 + except Exception:
  218 + logging.error(f'Error reading: {filepath}')
  219 + raise
228 else: 220 else:
229 - data = f.read()  
230 - f.close()  
231 self.set_header("Content-Type", content_type) 221 self.set_header("Content-Type", content_type)
232 self.write(data) 222 self.write(data)
233 await self.flush() 223 await self.flush()
@@ -362,186 +352,3 @@ class QuestionHandler(BaseHandler): @@ -362,186 +352,3 @@ class QuestionHandler(BaseHandler):
362 logging.error(f'Unknown action: {action}') 352 logging.error(f'Unknown action: {action}')
363 353
364 self.write(response) 354 self.write(response)
365 -  
366 -  
367 -# ----------------------------------------------------------------------------  
368 -# Signal handler to catch Ctrl-C and abort server  
369 -# ----------------------------------------------------------------------------  
370 -def signal_handler(signal, frame):  
371 - r = input(' --> Stop webserver? (yes/no) ').lower()  
372 - if r == 'yes':  
373 - tornado.ioloop.IOLoop.current().stop()  
374 - logging.critical('Webserver stopped.')  
375 - sys.exit(0)  
376 - else:  
377 - logging.info('Abort canceled...')  
378 -  
379 -  
380 -# ----------------------------------------------------------------------------  
381 -def parse_cmdline_arguments():  
382 - argparser = argparse.ArgumentParser(  
383 - description='Server for online learning. Enrolled students and topics '  
384 - 'have to be previously configured. Please read the documentation '  
385 - 'included with this software before running the server.'  
386 - )  
387 -  
388 - argparser.add_argument(  
389 - 'conffile', type=str, nargs='+',  
390 - help='Topics configuration file in YAML format.'  
391 - )  
392 -  
393 - argparser.add_argument(  
394 - '--prefix', type=str, default='.',  
395 - help='Path where the topic directories can be found, e.g. ~/topics'  
396 - )  
397 -  
398 - argparser.add_argument(  
399 - '--port', type=int, default=8443,  
400 - help='Port to be used by the HTTPS server, e.g. 8443'  
401 - )  
402 -  
403 - argparser.add_argument(  
404 - '--db', type=str, default='students.db',  
405 - help='SQLite3 database file, e.g. students.db'  
406 - )  
407 -  
408 - argparser.add_argument(  
409 - '--check', action='store_true',  
410 - help='Sanity check all questions'  
411 - )  
412 -  
413 - argparser.add_argument(  
414 - '--debug', action='store_true',  
415 - help='Enable debug messages'  
416 - )  
417 -  
418 - return argparser.parse_args()  
419 -  
420 -  
421 -# ----------------------------------------------------------------------------  
422 -def get_logger_config(debug=False):  
423 - if debug:  
424 - filename = 'logger-debug.yaml'  
425 - level = 'DEBUG'  
426 - else:  
427 - filename = 'logger.yaml'  
428 - level = 'INFO'  
429 -  
430 - config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')  
431 - config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)  
432 -  
433 - default_config = {  
434 - 'version': 1,  
435 - 'formatters': {  
436 - 'standard': {  
437 - 'format': '%(asctime)s %(name)-24s %(levelname)-10s - '  
438 - '%(message)s',  
439 - 'datefmt': '%Y-%m-%d %H:%M:%S',  
440 - },  
441 - },  
442 - 'handlers': {  
443 - 'default': {  
444 - 'level': level,  
445 - 'class': 'logging.StreamHandler',  
446 - 'formatter': 'standard',  
447 - 'stream': 'ext://sys.stdout',  
448 - },  
449 - },  
450 - 'loggers': {  
451 - '': { # configuration for serve.py  
452 - 'handlers': ['default'],  
453 - 'level': level,  
454 - },  
455 - },  
456 - }  
457 - default_config['loggers'].update({  
458 - APP_NAME+'.'+module: {  
459 - 'handlers': ['default'],  
460 - 'level': level,  
461 - 'propagate': False,  
462 - } for module in ['learnapp', 'models', 'factory', 'questions',  
463 - 'knowledge', 'tools']})  
464 -  
465 - return load_yaml(config_file, default=default_config)  
466 -  
467 -  
468 -# ----------------------------------------------------------------------------  
469 -# Tornado web server  
470 -# ----------------------------------------------------------------------------  
471 -def main():  
472 - # --- Commandline argument parsing  
473 - arg = parse_cmdline_arguments()  
474 -  
475 - # --- Setup logging  
476 - logger_config = get_logger_config(arg.debug)  
477 - logging.config.dictConfig(logger_config)  
478 -  
479 - try:  
480 - logging.config.dictConfig(logger_config)  
481 - except Exception:  
482 - print('An error ocurred while setting up the logging system.')  
483 - sys.exit(1)  
484 -  
485 - logging.info('====================== Start Logging ======================')  
486 -  
487 - # --- start application  
488 - logging.info('Starting App...')  
489 - try:  
490 - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,  
491 - check=arg.check)  
492 - except Exception:  
493 - logging.critical('Failed to start application.')  
494 - sys.exit(1)  
495 -  
496 - # --- create web application  
497 - logging.info('Starting Web App (tornado)...')  
498 - try:  
499 - webapp = WebApplication(learnapp, debug=arg.debug)  
500 - except Exception:  
501 - logging.critical('Failed to start web application.')  
502 - sys.exit(1)  
503 -  
504 - # --- get SSL certificates  
505 - if 'XDG_DATA_HOME' in environ:  
506 - certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')  
507 - else:  
508 - certs_dir = path.expanduser('~/.local/share/certs')  
509 -  
510 - ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)  
511 - try:  
512 - ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),  
513 - path.join(certs_dir, 'privkey.pem'))  
514 - except FileNotFoundError:  
515 - logging.critical(f'SSL certificates missing in {certs_dir}')  
516 - sys.exit(1)  
517 -  
518 - # --- create webserver  
519 - try:  
520 - httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)  
521 - except ValueError:  
522 - logging.critical('Certificates cert.pem and privkey.pem not found')  
523 - sys.exit(1)  
524 -  
525 - try:  
526 - httpserver.listen(arg.port)  
527 - except OSError:  
528 - logging.critical(f'Cannot bind port {arg.port}. Already in use?')  
529 - sys.exit(1)  
530 -  
531 - logging.info(f'Listening on port {arg.port}.')  
532 -  
533 - # --- run webserver  
534 - signal.signal(signal.SIGINT, signal_handler)  
535 - logging.info('Webserver running. (Ctrl-C to stop)')  
536 -  
537 - try:  
538 - tornado.ioloop.IOLoop.current().start() # running...  
539 - except Exception:  
540 - logging.critical('Webserver stopped.')  
541 - tornado.ioloop.IOLoop.current().stop()  
542 - raise  
543 -  
544 -  
545 -# ----------------------------------------------------------------------------  
546 -if __name__ == "__main__":  
547 - main()  
aprendizations/student.py 0 → 100644
@@ -0,0 +1,236 @@ @@ -0,0 +1,236 @@
  1 +
  2 +# python standard library
  3 +import random
  4 +from datetime import datetime
  5 +import logging
  6 +
  7 +# third party libraries
  8 +import networkx as nx
  9 +
  10 +# setup logger for this module
  11 +logger = logging.getLogger(__name__)
  12 +
  13 +
  14 +# ----------------------------------------------------------------------------
  15 +# kowledge state of each student....??
  16 +# Contains:
  17 +# state - dict of topics with state of unlocked topics
  18 +# deps - access to dependency graph shared between students
  19 +# topic_sequence - list with the order of recommended topics
  20 +# ----------------------------------------------------------------------------
  21 +class StudentState(object):
  22 + # =======================================================================
  23 + # methods that update state
  24 + # =======================================================================
  25 + def __init__(self, deps, factory, state={}):
  26 + self.deps = deps # shared dependency graph
  27 + self.factory = factory # question factory
  28 + self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
  29 +
  30 + self.update_topic_levels() # applies forgetting factor
  31 + self.unlock_topics() # whose dependencies have been completed
  32 + self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]
  33 + self.current_topic = None
  34 +
  35 + # ------------------------------------------------------------------------
  36 + # Updates the proficiency levels of the topics, with forgetting factor
  37 + # FIXME no dependencies are considered yet...
  38 + # ------------------------------------------------------------------------
  39 + def update_topic_levels(self):
  40 + now = datetime.now()
  41 + for tref, s in self.state.items():
  42 + dt = now - s['date']
  43 + s['level'] *= 0.98 ** dt.days # forgetting factor
  44 +
  45 + # ------------------------------------------------------------------------
  46 + # Unlock topics whose dependencies are satisfied (> min_level)
  47 + # ------------------------------------------------------------------------
  48 + def unlock_topics(self):
  49 + for topic in self.deps.nodes():
  50 + if topic not in self.state: # if locked
  51 + pred = self.deps.predecessors(topic)
  52 + min_level = self.deps.node[topic]['min_level']
  53 + 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
  55 +
  56 + self.state[topic] = {
  57 + 'level': 0.0, # unlocked
  58 + 'date': datetime.now()
  59 + }
  60 + logger.debug(f'[unlock_topics] Unlocked "{topic}".')
  61 + # else: # lock this topic if deps do not satisfy min_level
  62 + # del self.state[topic]
  63 +
  64 + # ------------------------------------------------------------------------
  65 + # Start a new topic.
  66 + # questions: list of generated questions to do in the topic
  67 + # current_question: the current question to be presented
  68 + # ------------------------------------------------------------------------
  69 + async def start_topic(self, topic):
  70 + logger.debug(f'[start_topic] topic "{topic}"')
  71 +
  72 + if self.current_topic == topic:
  73 + logger.info('Restarting current topic is not allowed.')
  74 + return
  75 +
  76 + # do not allow locked topics
  77 + if self.is_locked(topic):
  78 + logger.debug(f'[start_topic] topic "{topic}" is locked')
  79 + return
  80 +
  81 + # starting new topic
  82 + self.current_topic = topic
  83 + self.correct_answers = 0
  84 + self.wrong_answers = 0
  85 +
  86 + t = self.deps.node[topic]
  87 + k = t['choose']
  88 + if t['shuffle_questions']:
  89 + questions = random.sample(t['questions'], k=k)
  90 + else:
  91 + questions = t['questions'][:k]
  92 + logger.debug(f'[start_topic] questions: {", ".join(questions)}')
  93 +
  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()
  100 + for ref in questions]
  101 +
  102 + n = len(self.questions)
  103 + logger.debug(f'[start_topic] generated {n} questions')
  104 +
  105 + # get first question
  106 + self.next_question()
  107 +
  108 + # ------------------------------------------------------------------------
  109 + # The topic has finished and there are no more questions.
  110 + # The topic level is updated in state and unlocks are performed.
  111 + # The current topic is unchanged.
  112 + # ------------------------------------------------------------------------
  113 + def finish_topic(self):
  114 + logger.debug(f'[finish_topic] current_topic {self.current_topic}')
  115 +
  116 + self.state[self.current_topic] = {
  117 + 'date': datetime.now(),
  118 + 'level': self.correct_answers / (self.correct_answers +
  119 + self.wrong_answers)
  120 + }
  121 + # self.current_topic = None
  122 + self.unlock_topics()
  123 +
  124 + # ------------------------------------------------------------------------
  125 + # corrects current question with provided answer.
  126 + # implements the logic:
  127 + # - if answer ok, goes to next question
  128 + # - if wrong, counts number of tries. If exceeded, moves on.
  129 + # ------------------------------------------------------------------------
  130 + async def check_answer(self, answer):
  131 + logger.debug('[check_answer]')
  132 +
  133 + q = self.current_question
  134 + q['answer'] = answer
  135 + q['finish_time'] = datetime.now()
  136 + await q.correct_async()
  137 + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
  138 +
  139 + if q['grade'] > 0.999:
  140 + self.correct_answers += 1
  141 + self.next_question()
  142 + action = 'right'
  143 +
  144 + else:
  145 + self.wrong_answers += 1
  146 + self.current_question['tries'] -= 1
  147 +
  148 + if self.current_question['tries'] > 0:
  149 + action = 'try_again'
  150 + else:
  151 + action = 'wrong'
  152 + if self.current_question['append_wrong']:
  153 + logger.debug('[check_answer] Wrong, append new instance')
  154 + self.questions.append(self.factory[q['ref']].generate())
  155 + self.next_question()
  156 +
  157 + # returns corrected question (not new one) which might include comments
  158 + return q, action
  159 +
  160 + # ------------------------------------------------------------------------
  161 + # Move to next question
  162 + # ------------------------------------------------------------------------
  163 + def next_question(self):
  164 + try:
  165 + self.current_question = self.questions.pop(0)
  166 + except IndexError:
  167 + self.current_question = None
  168 + self.finish_topic()
  169 + else:
  170 + self.current_question['start_time'] = datetime.now()
  171 + default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
  172 + maxtries = self.current_question.get('max_tries', default_maxtries)
  173 + self.current_question['tries'] = maxtries
  174 + logger.debug(f'[next_question] "{self.current_question["ref"]}"')
  175 +
  176 + return self.current_question # question or None
  177 +
  178 + # ========================================================================
  179 + # pure functions of the state (no side effects)
  180 + # ========================================================================
  181 +
  182 + def topic_has_finished(self):
  183 + return self.current_question is None
  184 +
  185 + # ------------------------------------------------------------------------
  186 + # compute recommended sequence of topics ['a', 'b', ...]
  187 + # ------------------------------------------------------------------------
  188 + def recommend_topic_sequence(self, target=None):
  189 + tt = list(nx.topological_sort(self.deps))
  190 + unlocked = [t for t in tt if t in self.state]
  191 + locked = [t for t in tt if t not in unlocked]
  192 + return unlocked + locked
  193 +
  194 + # ------------------------------------------------------------------------
  195 + def get_current_question(self):
  196 + return self.current_question
  197 +
  198 + # ------------------------------------------------------------------------
  199 + def get_current_topic(self):
  200 + return self.current_topic
  201 +
  202 + # ------------------------------------------------------------------------
  203 + def is_locked(self, topic):
  204 + return topic not in self.state
  205 +
  206 + # ------------------------------------------------------------------------
  207 + # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}
  208 + # Levels are in the interval [0, 1] if unlocked or None if locked.
  209 + # Topics unlocked but not yet done have level 0.0.
  210 + # ------------------------------------------------------------------------
  211 + def get_knowledge_state(self):
  212 + return [{
  213 + 'ref': ref,
  214 + 'type': self.deps.nodes[ref]['type'],
  215 + 'name': self.deps.nodes[ref]['name'],
  216 + 'level': self.state[ref]['level'] if ref in self.state else None
  217 + } for ref in self.topic_sequence]
  218 +
  219 + # ------------------------------------------------------------------------
  220 + def get_topic_progress(self):
  221 + return self.correct_answers / (1 + self.correct_answers +
  222 + len(self.questions))
  223 +
  224 + # ------------------------------------------------------------------------
  225 + def get_topic_level(self, topic):
  226 + return self.state[topic]['level']
  227 +
  228 + # ------------------------------------------------------------------------
  229 + def get_topic_date(self, topic):
  230 + return self.state[topic]['date']
  231 +
  232 + # ------------------------------------------------------------------------
  233 + # Recommends a topic to practice/learn from the state.
  234 + # ------------------------------------------------------------------------
  235 + # def get_recommended_topic(self): # FIXME untested
  236 + # return min(self.state.items(), key=lambda x: x[1]['level'])[0]
package-lock.json
@@ -2,15 +2,233 @@ @@ -2,15 +2,233 @@
2 "requires": true, 2 "requires": true,
3 "lockfileVersion": 1, 3 "lockfileVersion": 1,
4 "dependencies": { 4 "dependencies": {
  5 + "@babel/code-frame": {
  6 + "version": "7.0.0",
  7 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
  8 + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
  9 + "requires": {
  10 + "@babel/highlight": "^7.0.0"
  11 + }
  12 + },
  13 + "@babel/core": {
  14 + "version": "7.5.4",
  15 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.4.tgz",
  16 + "integrity": "sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==",
  17 + "requires": {
  18 + "@babel/code-frame": "^7.0.0",
  19 + "@babel/generator": "^7.5.0",
  20 + "@babel/helpers": "^7.5.4",
  21 + "@babel/parser": "^7.5.0",
  22 + "@babel/template": "^7.4.4",
  23 + "@babel/traverse": "^7.5.0",
  24 + "@babel/types": "^7.5.0",
  25 + "convert-source-map": "^1.1.0",
  26 + "debug": "^4.1.0",
  27 + "json5": "^2.1.0",
  28 + "lodash": "^4.17.11",
  29 + "resolve": "^1.3.2",
  30 + "semver": "^5.4.1",
  31 + "source-map": "^0.5.0"
  32 + }
  33 + },
  34 + "@babel/generator": {
  35 + "version": "7.5.0",
  36 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz",
  37 + "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==",
  38 + "requires": {
  39 + "@babel/types": "^7.5.0",
  40 + "jsesc": "^2.5.1",
  41 + "lodash": "^4.17.11",
  42 + "source-map": "^0.5.0",
  43 + "trim-right": "^1.0.1"
  44 + }
  45 + },
  46 + "@babel/helper-function-name": {
  47 + "version": "7.1.0",
  48 + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
  49 + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
  50 + "requires": {
  51 + "@babel/helper-get-function-arity": "^7.0.0",
  52 + "@babel/template": "^7.1.0",
  53 + "@babel/types": "^7.0.0"
  54 + }
  55 + },
  56 + "@babel/helper-get-function-arity": {
  57 + "version": "7.0.0",
  58 + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
  59 + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
  60 + "requires": {
  61 + "@babel/types": "^7.0.0"
  62 + }
  63 + },
  64 + "@babel/helper-split-export-declaration": {
  65 + "version": "7.4.4",
  66 + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
  67 + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
  68 + "requires": {
  69 + "@babel/types": "^7.4.4"
  70 + }
  71 + },
  72 + "@babel/helpers": {
  73 + "version": "7.5.4",
  74 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz",
  75 + "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==",
  76 + "requires": {
  77 + "@babel/template": "^7.4.4",
  78 + "@babel/traverse": "^7.5.0",
  79 + "@babel/types": "^7.5.0"
  80 + }
  81 + },
  82 + "@babel/highlight": {
  83 + "version": "7.5.0",
  84 + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
  85 + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
  86 + "requires": {
  87 + "chalk": "^2.0.0",
  88 + "esutils": "^2.0.2",
  89 + "js-tokens": "^4.0.0"
  90 + }
  91 + },
  92 + "@babel/parser": {
  93 + "version": "7.5.0",
  94 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz",
  95 + "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA=="
  96 + },
  97 + "@babel/template": {
  98 + "version": "7.4.4",
  99 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
  100 + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
  101 + "requires": {
  102 + "@babel/code-frame": "^7.0.0",
  103 + "@babel/parser": "^7.4.4",
  104 + "@babel/types": "^7.4.4"
  105 + }
  106 + },
  107 + "@babel/traverse": {
  108 + "version": "7.5.0",
  109 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz",
  110 + "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==",
  111 + "requires": {
  112 + "@babel/code-frame": "^7.0.0",
  113 + "@babel/generator": "^7.5.0",
  114 + "@babel/helper-function-name": "^7.1.0",
  115 + "@babel/helper-split-export-declaration": "^7.4.4",
  116 + "@babel/parser": "^7.5.0",
  117 + "@babel/types": "^7.5.0",
  118 + "debug": "^4.1.0",
  119 + "globals": "^11.1.0",
  120 + "lodash": "^4.17.11"
  121 + }
  122 + },
  123 + "@babel/types": {
  124 + "version": "7.5.0",
  125 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz",
  126 + "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==",
  127 + "requires": {
  128 + "esutils": "^2.0.2",
  129 + "lodash": "^4.17.11",
  130 + "to-fast-properties": "^2.0.0"
  131 + }
  132 + },
5 "@fortawesome/fontawesome-free": { 133 "@fortawesome/fontawesome-free": {
6 - "version": "5.8.1",  
7 - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.8.1.tgz",  
8 - "integrity": "sha512-GJtx6e55qLEOy2gPOsok2lohjpdWNGrYGtQx0FFT/++K4SYx+Z8LlPHdQBaFzKEwH5IbBB4fNgb//uyZjgYXoA==" 134 + "version": "5.9.0",
  135 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz",
  136 + "integrity": "sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg=="
  137 + },
  138 + "ansi-styles": {
  139 + "version": "3.2.1",
  140 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
  141 + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
  142 + "requires": {
  143 + "color-convert": "^1.9.0"
  144 + }
  145 + },
  146 + "chalk": {
  147 + "version": "2.4.2",
  148 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
  149 + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
  150 + "requires": {
  151 + "ansi-styles": "^3.2.1",
  152 + "escape-string-regexp": "^1.0.5",
  153 + "supports-color": "^5.3.0"
  154 + }
9 }, 155 },
10 "codemirror": { 156 "codemirror": {
11 - "version": "5.45.0",  
12 - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.45.0.tgz",  
13 - "integrity": "sha512-c19j644usCE8gQaXa0jqn2B/HN9MnB2u6qPIrrhrMkB+QAP42y8G4QnTwuwbVSoUS1jEl7JU9HZMGhCDL0nsAw==" 157 + "version": "5.48.0",
  158 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.0.tgz",
  159 + "integrity": "sha512-3Ter+tYtRlTNtxtYdYNPxGxBL/b3cMcvPdPm70gvmcOO2Rauv/fUEewWa0tT596Hosv6ea2mtpx28OXBy1mQCg=="
  160 + },
  161 + "color-convert": {
  162 + "version": "1.9.3",
  163 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
  164 + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
  165 + "requires": {
  166 + "color-name": "1.1.3"
  167 + }
  168 + },
  169 + "color-name": {
  170 + "version": "1.1.3",
  171 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
  172 + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
  173 + },
  174 + "convert-source-map": {
  175 + "version": "1.6.0",
  176 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
  177 + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
  178 + "requires": {
  179 + "safe-buffer": "~5.1.1"
  180 + }
  181 + },
  182 + "debug": {
  183 + "version": "4.1.1",
  184 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
  185 + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
  186 + "requires": {
  187 + "ms": "^2.1.1"
  188 + }
  189 + },
  190 + "escape-string-regexp": {
  191 + "version": "1.0.5",
  192 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
  193 + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
  194 + },
  195 + "esutils": {
  196 + "version": "2.0.2",
  197 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
  198 + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
  199 + },
  200 + "globals": {
  201 + "version": "11.12.0",
  202 + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
  203 + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
  204 + },
  205 + "has-flag": {
  206 + "version": "3.0.0",
  207 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
  208 + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
  209 + },
  210 + "js-tokens": {
  211 + "version": "4.0.0",
  212 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
  213 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
  214 + },
  215 + "jsesc": {
  216 + "version": "2.5.2",
  217 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
  218 + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
  219 + },
  220 + "json5": {
  221 + "version": "2.1.0",
  222 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
  223 + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
  224 + "requires": {
  225 + "minimist": "^1.2.0"
  226 + }
  227 + },
  228 + "lodash": {
  229 + "version": "4.17.14",
  230 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
  231 + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
14 }, 232 },
15 "mathjax": { 233 "mathjax": {
16 "version": "2.7.5", 234 "version": "2.7.5",
@@ -18,9 +236,68 @@ @@ -18,9 +236,68 @@
18 "integrity": "sha512-OzsJNitEHAJB3y4IIlPCAvS0yoXwYjlo2Y4kmm9KQzyIBZt2d8yKRalby3uTRNN4fZQiGL2iMXjpdP1u2Rq2DQ==" 236 "integrity": "sha512-OzsJNitEHAJB3y4IIlPCAvS0yoXwYjlo2Y4kmm9KQzyIBZt2d8yKRalby3uTRNN4fZQiGL2iMXjpdP1u2Rq2DQ=="
19 }, 237 },
20 "mdbootstrap": { 238 "mdbootstrap": {
21 - "version": "4.7.6",  
22 - "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.7.6.tgz",  
23 - "integrity": "sha512-b5Dgg/DQon8E3F/oIKJsCiFN1E5kgQBlndQ8vNzDnGcQXo2ruVKZA6Z3cvIJrv2IKS1kPBWOUwDedJiCLoAZxA==" 239 + "version": "4.8.5",
  240 + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.8.5.tgz",
  241 + "integrity": "sha512-e4YGrdyb5dUlkLu0OdH5UKi6ZZEAC1YrfE8T8oWY+GWCbLjr1C0sI5ZEYAZrN8VI6acKN5tboq6lbK/8nWXG6g==",
  242 + "requires": {
  243 + "@babel/core": "^7.3.3"
  244 + }
  245 + },
  246 + "minimist": {
  247 + "version": "1.2.0",
  248 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
  249 + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
  250 + },
  251 + "ms": {
  252 + "version": "2.1.2",
  253 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
  254 + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
  255 + },
  256 + "path-parse": {
  257 + "version": "1.0.6",
  258 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
  259 + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
  260 + },
  261 + "resolve": {
  262 + "version": "1.11.1",
  263 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
  264 + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
  265 + "requires": {
  266 + "path-parse": "^1.0.6"
  267 + }
  268 + },
  269 + "safe-buffer": {
  270 + "version": "5.1.2",
  271 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
  272 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
  273 + },
  274 + "semver": {
  275 + "version": "5.7.0",
  276 + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
  277 + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
  278 + },
  279 + "source-map": {
  280 + "version": "0.5.7",
  281 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
  282 + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
  283 + },
  284 + "supports-color": {
  285 + "version": "5.5.0",
  286 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
  287 + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
  288 + "requires": {
  289 + "has-flag": "^3.0.0"
  290 + }
  291 + },
  292 + "to-fast-properties": {
  293 + "version": "2.0.0",
  294 + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
  295 + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
  296 + },
  297 + "trim-right": {
  298 + "version": "1.0.1",
  299 + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
  300 + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
24 } 301 }
25 } 302 }
26 } 303 }
@@ -2,10 +2,10 @@ @@ -2,10 +2,10 @@
2 "description": "Javascript libraries required to run the server", 2 "description": "Javascript libraries required to run the server",
3 "email": "mjsb@uevora.pt", 3 "email": "mjsb@uevora.pt",
4 "dependencies": { 4 "dependencies": {
5 - "@fortawesome/fontawesome-free": "^5.8.1",  
6 - "codemirror": "^5.45.0", 5 + "@fortawesome/fontawesome-free": "^5.9.0",
  6 + "codemirror": "^5.48.0",
7 "mathjax": "^2.7.5", 7 "mathjax": "^2.7.5",
8 - "mdbootstrap": "^4.7.6" 8 + "mdbootstrap": "^4.8.5"
9 }, 9 },
10 "private": true 10 "private": true
11 } 11 }
@@ -25,9 +25,9 @@ setup( @@ -25,9 +25,9 @@ setup(
25 ], 25 ],
26 entry_points={ 26 entry_points={
27 'console_scripts': [ 27 'console_scripts': [
28 - 'aprendizations = aprendizations.serve:main', 28 + 'aprendizations = aprendizations.main:main',
29 'initdb-aprendizations = aprendizations.initdb:main', 29 'initdb-aprendizations = aprendizations.initdb:main',
30 - 'redirect = aprendizations.redirect:main', 30 + # 'redirect = aprendizations.redirect:main',
31 ] 31 ]
32 }, 32 },
33 classifiers=[ 33 classifiers=[