Commit d187aad4125f77a4f052a1a33c81e4f922efc683

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

- adds courses

- updates mathjax to version 3 (not yet working correctly)
- updates other javascript libraries
1 1
2 # BUGS 2 # BUGS
3 3
  4 +- classificacoes so devia mostrar os que ja fizeram alguma coisa
  5 +- menu nao mostra as opcoes correctamente
  6 +- mathjax nao esta a correr sobre o titulo e sobre as solucoes.
  7 +- QFactory.generate() devia fazer run da gen_async, ou remover.
  8 +- finish topic vai para a lista de cursos. devia ficar no mesmo curso.
4 - marking all options right in a radio question breaks! 9 - marking all options right in a radio question breaks!
5 - impedir que quando students.db não é encontrado, crie um ficheiro vazio. 10 - impedir que quando students.db não é encontrado, crie um ficheiro vazio.
6 - opcao --prefix devia afectar a base de dados? 11 - opcao --prefix devia afectar a base de dados?
@@ -24,6 +29,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in @@ -24,6 +29,7 @@ sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in
24 29
25 # TODO 30 # TODO
26 31
  32 +- use run_script_async to run run_script using asyncio.run?
27 - ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos). 33 - ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos).
28 - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. 34 - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias.
29 - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. 35 - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos.
@@ -284,11 +284,11 @@ me:\ @@ -284,11 +284,11 @@ me:\
284 Common database manipulations: 284 Common database manipulations:
285 285
286 ```sh 286 ```sh
287 -initdb-aprendizations -u 12345 --pw alibaba # reset student password  
288 -initdb-aprendizations -a 12345 --pw alibaba # add new student 287 +initdb-aprendizations -u 12345 --pw alibaba # reset student password
  288 +initdb-aprendizations -a 007 "James Bond" --pw mi6 # add new student "007"
289 ``` 289 ```
290 290
291 -Common database queries: 291 +Some common database queries:
292 292
293 ```sh 293 ```sh
294 # Which students did at least one topic? 294 # Which students did at least one topic?
aprendizations/learnapp.py
1 1
2 # python standard library 2 # python standard library
3 -from os import path  
4 -import logging  
5 -from contextlib import contextmanager # `with` statement in db sessions  
6 import asyncio 3 import asyncio
  4 +from contextlib import contextmanager # `with` statement in db sessions
7 from datetime import datetime 5 from datetime import datetime
  6 +import logging
8 from random import random 7 from random import random
  8 +from os import path
9 from typing import Any, Dict, Iterable, List, Optional, Tuple 9 from typing import Any, Dict, Iterable, List, Optional, Tuple
10 10
11 # third party libraries 11 # third party libraries
12 import bcrypt 12 import bcrypt
13 -import sqlalchemy as sa  
14 import networkx as nx 13 import networkx as nx
  14 +import sqlalchemy as sa
15 15
16 # this project 16 # this project
17 from .models import Student, Answer, Topic, StudentTopic 17 from .models import Student, Answer, Topic, StudentTopic
18 -from .student import StudentState  
19 from .questions import Question, QFactory, QDict 18 from .questions import Question, QFactory, QDict
  19 +from .student import StudentState
20 from .tools import load_yaml 20 from .tools import load_yaml
21 21
22 # setup logger for this module 22 # setup logger for this module
@@ -57,7 +57,7 @@ class LearnApp(object): @@ -57,7 +57,7 @@ class LearnApp(object):
57 # init 57 # init
58 # ------------------------------------------------------------------------ 58 # ------------------------------------------------------------------------
59 def __init__(self, 59 def __init__(self,
60 - config_files: List[str], 60 + courses: str, # filename with course configurations
61 prefix: str, # path to topics 61 prefix: str, # path to topics
62 db: str, # database filename 62 db: str, # database filename
63 check: bool = False) -> None: 63 check: bool = False) -> None:
@@ -65,9 +65,18 @@ class LearnApp(object): @@ -65,9 +65,18 @@ class LearnApp(object):
65 self.db_setup(db) # setup database and check students 65 self.db_setup(db) # setup database and check students
66 self.online: Dict[str, Dict] = dict() # online students 66 self.online: Dict[str, Dict] = dict() # online students
67 67
  68 + config: Dict[str, Any] = load_yaml(courses)
  69 +
  70 + # courses dict
  71 + self.courses = config['courses']
  72 + logger.info(f'Courses: {", ".join(self.courses.keys())}')
  73 +
  74 + # topic dependencies are shared between all courses
68 self.deps = nx.DiGraph(prefix=prefix) 75 self.deps = nx.DiGraph(prefix=prefix)
69 - for c in config_files:  
70 - self.populate_graph(c) 76 + logger.info('Populating graph:')
  77 + for f in config['deps']:
  78 + self.populate_graph(f)
  79 + logger.info(f'Graph has {len(self.deps)} topics')
71 80
72 # factory is a dict with question generators for all topics 81 # factory is a dict with question generators for all topics
73 self.factory: Dict[str, QFactory] = self.make_factory() 82 self.factory: Dict[str, QFactory] = self.make_factory()
@@ -163,8 +172,9 @@ class LearnApp(object): @@ -163,8 +172,9 @@ class LearnApp(object):
163 self.online[uid] = { 172 self.online[uid] = {
164 'number': uid, 173 'number': uid,
165 'name': name, 174 'name': name,
166 - 'state': StudentState(deps=self.deps, factory=self.factory,  
167 - state=state), 175 + 'state': StudentState(uid=uid, state=state,
  176 + courses=self.courses, deps=self.deps,
  177 + factory=self.factory),
168 'counter': counter + 1, # counts simultaneous logins 178 'counter': counter + 1, # counts simultaneous logins
169 } 179 }
170 180
@@ -202,10 +212,10 @@ class LearnApp(object): @@ -202,10 +212,10 @@ class LearnApp(object):
202 # checks answer (updating student state) and returns grade. FIXME type of answer 212 # checks answer (updating student state) and returns grade. FIXME type of answer
203 # ------------------------------------------------------------------------ 213 # ------------------------------------------------------------------------
204 async def check_answer(self, uid: str, answer) -> Tuple[Question, str]: 214 async def check_answer(self, uid: str, answer) -> Tuple[Question, str]:
205 - knowledge = self.online[uid]['state']  
206 - topic = knowledge.get_current_topic() 215 + student = self.online[uid]['state']
  216 + topic = student.get_current_topic()
207 217
208 - q, action = await knowledge.check_answer(answer) # may move questions 218 + q, action = await student.check_answer(answer) # may move questions
209 219
210 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') 220 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"')
211 221
@@ -220,11 +230,11 @@ class LearnApp(object): @@ -220,11 +230,11 @@ class LearnApp(object):
220 topic_id=topic)) 230 topic_id=topic))
221 logger.debug(f'db insert answer of {q["ref"]}') 231 logger.debug(f'db insert answer of {q["ref"]}')
222 232
223 - if knowledge.topic_has_finished(): 233 + if student.topic_has_finished():
224 # finished topic, save into database 234 # finished topic, save into database
225 logger.info(f'User "{uid}" finished "{topic}"') 235 logger.info(f'User "{uid}" finished "{topic}"')
226 - level: float = knowledge.get_topic_level(topic)  
227 - date: str = str(knowledge.get_topic_date(topic)) 236 + level: float = student.get_topic_level(topic)
  237 + date: str = str(student.get_topic_date(topic))
228 238
229 with self.db_session() as s: 239 with self.db_session() as s:
230 a = s.query(StudentTopic) \ 240 a = s.query(StudentTopic) \
@@ -250,6 +260,18 @@ class LearnApp(object): @@ -250,6 +260,18 @@ class LearnApp(object):
250 return q, action 260 return q, action
251 261
252 # ------------------------------------------------------------------------ 262 # ------------------------------------------------------------------------
  263 + # Start course
  264 + # ------------------------------------------------------------------------
  265 + def start_course(self, uid: str, course: str) -> None:
  266 + student = self.online[uid]['state']
  267 + try:
  268 + student.start_course(course)
  269 + except Exception as e:
  270 + logger.warning(f'"{uid}" could not start course "{course}": {e}')
  271 + else:
  272 + logger.info(f'"{uid}" started course "{course}"')
  273 +
  274 + # ------------------------------------------------------------------------
253 # Start new topic 275 # Start new topic
254 # ------------------------------------------------------------------------ 276 # ------------------------------------------------------------------------
255 async def start_topic(self, uid: str, topic: str) -> None: 277 async def start_topic(self, uid: str, topic: str) -> None:
@@ -305,8 +327,6 @@ class LearnApp(object): @@ -305,8 +327,6 @@ class LearnApp(object):
305 # Edges are obtained from the deps defined in the YAML file for each topic. 327 # Edges are obtained from the deps defined in the YAML file for each topic.
306 # ------------------------------------------------------------------------ 328 # ------------------------------------------------------------------------
307 def populate_graph(self, conffile: str) -> None: 329 def populate_graph(self, conffile: str) -> None:
308 -  
309 - logger.info(f'Populating graph from: {conffile}...')  
310 config: Dict[str, Any] = load_yaml(conffile) # course configuration 330 config: Dict[str, Any] = load_yaml(conffile) # course configuration
311 331
312 # default attributes that apply to the topics 332 # default attributes that apply to the topics
@@ -347,7 +367,7 @@ class LearnApp(object): @@ -347,7 +367,7 @@ class LearnApp(object):
347 t['append_wrong'] = attr.get('append_wrong', default_append_wrong) 367 t['append_wrong'] = attr.get('append_wrong', default_append_wrong)
348 t['questions'] = attr.get('questions', []) 368 t['questions'] = attr.get('questions', [])
349 369
350 - logger.info(f'Loaded {g.number_of_nodes()} topics') 370 + logger.info(f'{len(topics):>6} topics in {conffile}')
351 371
352 # ======================================================================== 372 # ========================================================================
353 # methods that do not change state (pure functions) 373 # methods that do not change state (pure functions)
@@ -428,8 +448,13 @@ class LearnApp(object): @@ -428,8 +448,13 @@ class LearnApp(object):
428 return self.online[uid]['state'].get_current_topic() 448 return self.online[uid]['state'].get_current_topic()
429 449
430 # ------------------------------------------------------------------------ 450 # ------------------------------------------------------------------------
431 - def get_title(self) -> str:  
432 - return self.deps.graph.get('title', '') 451 + def get_student_course_title(self, uid: str) -> str:
  452 + return self.online[uid]['state'].get_current_course_title()
  453 +
  454 + # ------------------------------------------------------------------------
  455 + # def get_title(self) -> str:
  456 + # # return self.deps.graph.get('title', '')
  457 + # return self.
433 458
434 # ------------------------------------------------------------------------ 459 # ------------------------------------------------------------------------
435 def get_topic_name(self, ref: str) -> str: 460 def get_topic_name(self, ref: str) -> str:
@@ -442,6 +467,10 @@ class LearnApp(object): @@ -442,6 +467,10 @@ class LearnApp(object):
442 return path.join(prefix, topic, 'public') 467 return path.join(prefix, topic, 'public')
443 468
444 # ------------------------------------------------------------------------ 469 # ------------------------------------------------------------------------
  470 + def get_courses(self, uid: str) -> Dict:
  471 + return self.courses
  472 +
  473 + # ------------------------------------------------------------------------
445 def get_rankings(self, uid: str) -> Iterable[Tuple[str, str, float, float]]: 474 def get_rankings(self, uid: str) -> Iterable[Tuple[str, str, float, float]]:
446 475
447 logger.info(f'User "{uid}" get rankings') 476 logger.info(f'User "{uid}" get rankings')
aprendizations/main.py
@@ -24,8 +24,8 @@ def parse_cmdline_arguments(): @@ -24,8 +24,8 @@ def parse_cmdline_arguments():
24 ) 24 )
25 25
26 argparser.add_argument( 26 argparser.add_argument(
27 - 'conffile', type=str, nargs='*',  
28 - help='Topics configuration file in YAML format.' 27 + 'courses', type=str, # nargs='*',
  28 + help='Courses configuration file in YAML format.'
29 ) 29 )
30 30
31 argparser.add_argument( 31 argparser.add_argument(
@@ -161,7 +161,9 @@ def main(): @@ -161,7 +161,9 @@ def main():
161 161
162 # --- start application 162 # --- start application
163 try: 163 try:
164 - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db, 164 + learnapp = LearnApp(courses=arg.courses,
  165 + prefix=arg.prefix,
  166 + db=arg.db,
165 check=arg.check) 167 check=arg.check)
166 except DatabaseUnusableError: 168 except DatabaseUnusableError:
167 logging.critical('Failed to start application.') 169 logging.critical('Failed to start application.')
aprendizations/questions.py
@@ -461,6 +461,9 @@ class QFactory(object): @@ -461,6 +461,9 @@ 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 + # FIXME this is almost the same as the one below
  465 + # return asyncio.run(self.gen_async())
  466 +
464 logger.debug(f'generating {self.question["ref"]}...') 467 logger.debug(f'generating {self.question["ref"]}...')
465 # Shallow copy so that script generated questions will not replace 468 # Shallow copy so that script generated questions will not replace
466 # the original generators 469 # the original generators
@@ -491,7 +494,7 @@ class QFactory(object): @@ -491,7 +494,7 @@ class QFactory(object):
491 return qinstance 494 return qinstance
492 495
493 # ----------------------------------------------------------------------- 496 # -----------------------------------------------------------------------
494 - async def generate_async(self) -> Question: 497 + async def gen_async(self) -> Question:
495 logger.debug(f'generating {self.question["ref"]}...') 498 logger.debug(f'generating {self.question["ref"]}...')
496 # Shallow copy so that script generated questions will not replace 499 # Shallow copy so that script generated questions will not replace
497 # the original generators 500 # the original generators
@@ -520,5 +523,4 @@ class QFactory(object): @@ -520,5 +523,4 @@ class QFactory(object):
520 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') 523 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
521 raise 524 raise
522 else: 525 else:
523 - logger.debug('ok')  
524 return qinstance 526 return qinstance
aprendizations/serve.py
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
24 24
25 25
26 # ---------------------------------------------------------------------------- 26 # ----------------------------------------------------------------------------
27 -# Decorator used to restrict access to the administrator only 27 +# Decorator used to restrict access to the administrator
28 # ---------------------------------------------------------------------------- 28 # ----------------------------------------------------------------------------
29 def admin_only(func): 29 def admin_only(func):
30 @functools.wraps(func) 30 @functools.wraps(func)
@@ -46,11 +46,14 @@ class WebApplication(tornado.web.Application): @@ -46,11 +46,14 @@ class WebApplication(tornado.web.Application):
46 (r'/login', LoginHandler), 46 (r'/login', LoginHandler),
47 (r'/logout', LogoutHandler), 47 (r'/logout', LogoutHandler),
48 (r'/change_password', ChangePasswordHandler), 48 (r'/change_password', ChangePasswordHandler),
49 - (r'/question', QuestionHandler), # renders each question  
50 - (r'/rankings', RankingsHandler), # student rankings  
51 - (r'/topic/(.+)', TopicHandler), # start a topic  
52 - (r'/file/(.+)', FileHandler), # serve files  
53 - (r'/', RootHandler), # show list of topics 49 + (r'/question', QuestionHandler), # render question
  50 + (r'/rankings', RankingsHandler), # rankings table
  51 + # (r'/topics', TopicsHandler), # show list of topics
  52 + (r'/topic/(.+)', TopicHandler), # start topic
  53 + (r'/file/(.+)', FileHandler), # serve file
  54 + (r'/courses', CoursesHandler), # show list of courses
  55 + (r'/course/(.+)', CourseHandler), # show course topics
  56 + (r'/', RootHandler), # redirects
54 ] 57 ]
55 settings = { 58 settings = {
56 'template_path': path.join(path.dirname(__file__), 'templates'), 59 'template_path': path.join(path.dirname(__file__), 'templates'),
@@ -166,23 +169,73 @@ class ChangePasswordHandler(BaseHandler): @@ -166,23 +169,73 @@ class ChangePasswordHandler(BaseHandler):
166 169
167 170
168 # ---------------------------------------------------------------------------- 171 # ----------------------------------------------------------------------------
169 -# / (main page)  
170 -# Shows a list of topics and proficiency (stars, locked). 172 +# /
  173 +# redirects to appropriate place
171 # ---------------------------------------------------------------------------- 174 # ----------------------------------------------------------------------------
172 class RootHandler(BaseHandler): 175 class RootHandler(BaseHandler):
173 @tornado.web.authenticated 176 @tornado.web.authenticated
174 def get(self): 177 def get(self):
  178 + self.redirect('/courses')
  179 +
  180 +
  181 +# ----------------------------------------------------------------------------
  182 +# /courses
  183 +# Shows a list of available courses
  184 +# ----------------------------------------------------------------------------
  185 +class CoursesHandler(BaseHandler):
  186 + @tornado.web.authenticated
  187 + def get(self):
175 uid = self.current_user 188 uid = self.current_user
  189 + self.render('courses.html',
  190 + appname=APP_NAME,
  191 + uid=uid,
  192 + name=self.learn.get_student_name(uid),
  193 + courses=self.learn.get_courses(uid),
  194 + )
  195 +
  196 +
  197 +# ----------------------------------------------------------------------------
  198 +# /course/...
  199 +# Start a given course
  200 +# ----------------------------------------------------------------------------
  201 +class CourseHandler(BaseHandler):
  202 + @tornado.web.authenticated
  203 + def get(self, course):
  204 + uid = self.current_user
  205 +
  206 + try:
  207 + self.learn.start_course(uid, course)
  208 + except KeyError:
  209 + self.redirect('/courses')
  210 +
  211 + # print('TITULO: ---------------->', self.learn.get_title())
176 self.render('maintopics-table.html', 212 self.render('maintopics-table.html',
177 appname=APP_NAME, 213 appname=APP_NAME,
178 uid=uid, 214 uid=uid,
179 name=self.learn.get_student_name(uid), 215 name=self.learn.get_student_name(uid),
180 state=self.learn.get_student_state(uid), 216 state=self.learn.get_student_state(uid),
181 - title=self.learn.get_title(), 217 + title=self.learn.get_student_course_title(uid),
182 ) 218 )
183 219
184 220
185 # ---------------------------------------------------------------------------- 221 # ----------------------------------------------------------------------------
  222 +# /topics
  223 +# Shows a list of topics and proficiency (stars, locked).
  224 +# ----------------------------------------------------------------------------
  225 +# class TopicsHandler(BaseHandler):
  226 +# @tornado.web.authenticated
  227 +# def get(self):
  228 +# uid = self.current_user
  229 +# self.render('maintopics-table.html',
  230 +# appname=APP_NAME,
  231 +# uid=uid,
  232 +# name=self.learn.get_student_name(uid),
  233 +# state=self.learn.get_student_state(uid),
  234 +# title=self.learn.get_title(),
  235 +# )
  236 +
  237 +
  238 +# ----------------------------------------------------------------------------
186 # /topic/... 239 # /topic/...
187 # Start a given topic 240 # Start a given topic
188 # ---------------------------------------------------------------------------- 241 # ----------------------------------------------------------------------------
@@ -194,7 +247,7 @@ class TopicHandler(BaseHandler): @@ -194,7 +247,7 @@ class TopicHandler(BaseHandler):
194 try: 247 try:
195 await self.learn.start_topic(uid, topic) 248 await self.learn.start_topic(uid, topic)
196 except KeyError: 249 except KeyError:
197 - self.redirect('/') 250 + self.redirect('/topics')
198 251
199 self.render('topic.html', 252 self.render('topic.html',
200 appname=APP_NAME, 253 appname=APP_NAME,
aprendizations/static/js/topic.js
@@ -69,7 +69,7 @@ function new_question(type, question, tries, progress) { @@ -69,7 +69,7 @@ function new_question(type, question, tries, progress) {
69 var btntext = (type == "information") ? "Continuar" : "Responder"; 69 var btntext = (type == "information") ? "Continuar" : "Responder";
70 $("#submit").html(btntext).off().click(postAnswer); 70 $("#submit").html(btntext).off().click(postAnswer);
71 $('#topic_progress').css('width', (100*progress)+'%').attr('aria-valuenow', 100*progress); 71 $('#topic_progress').css('width', (100*progress)+'%').attr('aria-valuenow', 100*progress);
72 - MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]); 72 + MathJax.typeset();
73 73
74 if (type == "radio") { 74 if (type == "radio") {
75 $(".list-group-item").click(function (e) { 75 $(".list-group-item").click(function (e) {
@@ -130,12 +130,15 @@ function getFeedback(response) { @@ -130,12 +130,15 @@ function getFeedback(response) {
130 $('#right').show(); 130 $('#right').show();
131 $('#wrong').hide(); 131 $('#wrong').hide();
132 $('#comments').html(params['comments']); 132 $('#comments').html(params['comments']);
133 - MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#comments"]); 133 + MathJax.typeset();
  134 +
134 $("#submit").html("Continuar").off().click(getQuestion); 135 $("#submit").html("Continuar").off().click(getQuestion);
135 $("#link_solution_on_right").click(function(){ 136 $("#link_solution_on_right").click(function(){
136 $("#right").hide(); 137 $("#right").hide();
137 $('#solution').html(params['solution']).animateCSS('flipInX'); 138 $('#solution').html(params['solution']).animateCSS('flipInX');
138 - MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#solution"]); 139 + // MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#solution"]);
  140 + MathJax.typeset();
  141 +
139 }); 142 });
140 break; 143 break;
141 144
@@ -144,7 +147,7 @@ function getFeedback(response) { @@ -144,7 +147,7 @@ function getFeedback(response) {
144 $('#topic_progress').css('width', (100*params["progress"])+'%').attr('aria-valuenow', 100*params["progress"]); 147 $('#topic_progress').css('width', (100*params["progress"])+'%').attr('aria-valuenow', 100*params["progress"]);
145 showTriesLeft(params["tries"]); 148 showTriesLeft(params["tries"]);
146 $('#comments').html(params['comments']); 149 $('#comments').html(params['comments']);
147 - MathJax.Hub.Queue(["Typeset",MathJax.Hub,"#comments"]); 150 + MathJax.typeset();
148 break; 151 break;
149 152
150 case "wrong": 153 case "wrong":
@@ -153,11 +156,13 @@ function getFeedback(response) { @@ -153,11 +156,13 @@ function getFeedback(response) {
153 $('#topic_progress').css('width', (100*params["progress"])+'%').attr('aria-valuenow', 100*params["progress"]); 156 $('#topic_progress').css('width', (100*params["progress"])+'%').attr('aria-valuenow', 100*params["progress"]);
154 showTriesLeft(params["tries"]); 157 showTriesLeft(params["tries"]);
155 $('#comments').html(params['comments']); 158 $('#comments').html(params['comments']);
156 - MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#comments"]); 159 + // MathJax.typeset();
  160 + MathJax.typeset();
157 $("#link_solution_on_wrong").click(function () { 161 $("#link_solution_on_wrong").click(function () {
158 $("#wrong").hide(); 162 $("#wrong").hide();
159 $('#solution').html(params['solution']).animateCSS('flipInX'); 163 $('#solution').html(params['solution']).animateCSS('flipInX');
160 - MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#solution"]); 164 + // MathJax.Hub.Queue(["Typeset", MathJax.Hub, "#solution"]);
  165 + MathJax.typeset();
161 }); 166 });
162 $("fieldset").attr("disabled", "disabled"); 167 $("fieldset").attr("disabled", "disabled");
163 $("#submit").html("Continuar").off().click(getQuestion); 168 $("#submit").html("Continuar").off().click(getQuestion);
aprendizations/student.py
1 1
2 # python standard library 2 # python standard library
3 -import random  
4 from datetime import datetime 3 from datetime import datetime
5 import logging 4 import logging
6 -from typing import List, Optional, Tuple 5 +import random
  6 +from typing import Dict, List, Optional, Tuple
7 7
8 # third party libraries 8 # third party libraries
9 import networkx as nx 9 import networkx as nx
@@ -28,45 +28,33 @@ class StudentState(object): @@ -28,45 +28,33 @@ class StudentState(object):
28 # ======================================================================= 28 # =======================================================================
29 # methods that update state 29 # methods that update state
30 # ======================================================================= 30 # =======================================================================
31 - def __init__(self, deps, factory, state={}) -> None:  
32 - self.deps = deps # shared dependency graph 31 + def __init__(self, uid, state, courses, deps, factory) -> None:
  32 + # shared application data between all students
  33 + self.deps = deps # dependency graph
33 self.factory = factory # question factory 34 self.factory = factory # question factory
  35 + self.courses = courses # {'course': ['topic_id1', 'topic_id2',...]}
  36 +
  37 + # data of this student
  38 + self.uid = uid # user id '12345'
34 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} 39 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
35 40
  41 + # prepare for running
36 self.update_topic_levels() # applies forgetting factor 42 self.update_topic_levels() # applies forgetting factor
37 self.unlock_topics() # whose dependencies have been completed 43 self.unlock_topics() # whose dependencies have been completed
  44 + self.start_course(None)
38 45
39 - self.topic_sequence: List[str] = self.recommend_topic_sequence()  
40 - self.current_topic: Optional[str] = None  
41 -  
42 - # ------------------------------------------------------------------------  
43 - # Updates the proficiency levels of the topics, with forgetting factor  
44 - # ------------------------------------------------------------------------  
45 - def update_topic_levels(self) -> None:  
46 - now = datetime.now()  
47 - for tref, s in self.state.items():  
48 - dt = now - s['date']  
49 - forgetting_factor = self.deps.node[tref]['forgetting_factor']  
50 - s['level'] *= forgetting_factor ** dt.days # forgetting factor  
51 -  
52 - # ------------------------------------------------------------------------  
53 - # Unlock topics whose dependencies are satisfied (> min_level)  
54 # ------------------------------------------------------------------------ 46 # ------------------------------------------------------------------------
55 - def unlock_topics(self) -> None:  
56 - for topic in self.deps.nodes():  
57 - if topic not in self.state: # if locked  
58 - pred = self.deps.predecessors(topic)  
59 - min_level = self.deps.node[topic]['min_level']  
60 - if all(d in self.state and self.state[d]['level'] > min_level  
61 - for d in pred): # all deps are greater than min_level  
62 -  
63 - self.state[topic] = {  
64 - 'level': 0.0, # unlocked  
65 - 'date': datetime.now()  
66 - }  
67 - logger.debug(f'unlocked "{topic}"')  
68 - # else: # lock this topic if deps do not satisfy min_level  
69 - # del self.state[topic] 47 + def start_course(self, course: Optional[str]) -> None:
  48 + if course is None:
  49 + logger.debug('no active course')
  50 + self.current_course: Optional[str] = None
  51 + self.topic_sequence: List[str] = []
  52 + self.current_topic: Optional[str] = None
  53 + else:
  54 + logger.debug(f'starting course {course}')
  55 + self.current_course = course
  56 + topics = self.courses[course]['topics']
  57 + self.topic_sequence = self.recommend_topic_sequence(topics)
70 58
71 # ------------------------------------------------------------------------ 59 # ------------------------------------------------------------------------
72 # Start a new topic. 60 # Start a new topic.
@@ -99,8 +87,8 @@ class StudentState(object): @@ -99,8 +87,8 @@ class StudentState(object):
99 questions = t['questions'][:k] 87 questions = t['questions'][:k]
100 logger.debug(f'selected questions: {", ".join(questions)}') 88 logger.debug(f'selected questions: {", ".join(questions)}')
101 89
102 - self.questions: List[Question] = [await self.factory[ref].generate_async()  
103 - for ref in questions] 90 + self.questions: List[Question] = [await self.factory[ref].gen_async()
  91 + for ref in questions]
104 92
105 n = len(self.questions) 93 n = len(self.questions)
106 logger.debug(f'generated {n} questions') 94 logger.debug(f'generated {n} questions')
@@ -153,7 +141,9 @@ class StudentState(object): @@ -153,7 +141,9 @@ class StudentState(object):
153 action = 'wrong' 141 action = 'wrong'
154 if self.current_question['append_wrong']: 142 if self.current_question['append_wrong']:
155 logger.debug('wrong answer, append new question') 143 logger.debug('wrong answer, append new question')
156 - self.questions.append(self.factory[q['ref']].generate()) 144 + # self.questions.append(self.factory[q['ref']].generate())
  145 + new_question = await self.factory[q['ref']].gen_async()
  146 + self.questions.append(new_question)
157 self.next_question() 147 self.next_question()
158 148
159 # returns corrected question (not new one) which might include comments 149 # returns corrected question (not new one) which might include comments
@@ -177,6 +167,35 @@ class StudentState(object): @@ -177,6 +167,35 @@ class StudentState(object):
177 167
178 return self.current_question # question or None 168 return self.current_question # question or None
179 169
  170 + # ------------------------------------------------------------------------
  171 + # Update proficiency level of the topics using a forgetting factor
  172 + # ------------------------------------------------------------------------
  173 + def update_topic_levels(self) -> None:
  174 + now = datetime.now()
  175 + for tref, s in self.state.items():
  176 + dt = now - s['date']
  177 + forgetting_factor = self.deps.node[tref]['forgetting_factor']
  178 + s['level'] *= forgetting_factor ** dt.days # forgetting factor
  179 +
  180 + # ------------------------------------------------------------------------
  181 + # Unlock topics whose dependencies are satisfied (> min_level)
  182 + # ------------------------------------------------------------------------
  183 + def unlock_topics(self) -> None:
  184 + for topic in self.deps.nodes():
  185 + if topic not in self.state: # if locked
  186 + pred = self.deps.predecessors(topic)
  187 + min_level = self.deps.node[topic]['min_level']
  188 + if all(d in self.state and self.state[d]['level'] > min_level
  189 + for d in pred): # all deps are greater than min_level
  190 +
  191 + self.state[topic] = {
  192 + 'level': 0.0, # unlock
  193 + 'date': datetime.now()
  194 + }
  195 + logger.debug(f'unlocked "{topic}"')
  196 + # else: # lock this topic if deps do not satisfy min_level
  197 + # del self.state[topic]
  198 +
180 # ======================================================================== 199 # ========================================================================
181 # pure functions of the state (no side effects) 200 # pure functions of the state (no side effects)
182 # ======================================================================== 201 # ========================================================================
@@ -187,17 +206,17 @@ class StudentState(object): @@ -187,17 +206,17 @@ class StudentState(object):
187 # ------------------------------------------------------------------------ 206 # ------------------------------------------------------------------------
188 # compute recommended sequence of topics ['a', 'b', ...] 207 # compute recommended sequence of topics ['a', 'b', ...]
189 # ------------------------------------------------------------------------ 208 # ------------------------------------------------------------------------
190 - def recommend_topic_sequence(self, target: str = '') -> List[str]:  
191 - tt = list(nx.topological_sort(self.deps))  
192 - try:  
193 - idx = tt.index(target)  
194 - except ValueError:  
195 - pass  
196 - else:  
197 - del tt[idx:] 209 + def recommend_topic_sequence(self, targets: List[str] = []) -> List[str]:
  210 + G = self.deps
  211 + ts = set(targets)
  212 + for t in targets:
  213 + ts.update(nx.ancestors(G, t))
198 214
199 - unlocked = [t for t in tt if t in self.state]  
200 - locked = [t for t in tt if t not in unlocked] 215 + tl = list(nx.topological_sort(G.subgraph(ts)))
  216 +
  217 + # sort with unlocked first
  218 + unlocked = [t for t in tl if t in self.state]
  219 + locked = [t for t in tl if t not in unlocked]
201 return unlocked + locked 220 return unlocked + locked
202 221
203 # ------------------------------------------------------------------------ 222 # ------------------------------------------------------------------------
@@ -209,6 +228,10 @@ class StudentState(object): @@ -209,6 +228,10 @@ class StudentState(object):
209 return self.current_topic 228 return self.current_topic
210 229
211 # ------------------------------------------------------------------------ 230 # ------------------------------------------------------------------------
  231 + def get_current_course_title(self) -> Optional[str]:
  232 + return self.courses[self.current_course]['title']
  233 +
  234 + # ------------------------------------------------------------------------
212 def is_locked(self, topic: str) -> bool: 235 def is_locked(self, topic: str) -> bool:
213 return topic not in self.state 236 return topic not in self.state
214 237
aprendizations/templates/courses.html 0 → 100644
@@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
  1 +{% autoescape %}
  2 +
  3 +<!doctype html>
  4 +<html lang="pt-PT">
  5 +<head>
  6 + <title>{{appname}}</title>
  7 + <link rel="icon" href="/static/favicon.ico">
  8 +
  9 + <meta charset="utf-8">
  10 + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  11 + <meta name="author" content="Miguel Barão">
  12 +
  13 +<!-- Styles -->
  14 + <link rel="stylesheet" href="/static/mdbootstrap/css/bootstrap.min.css">
  15 + <link rel="stylesheet" href="/static/mdbootstrap/css/mdb.min.css">
  16 + <link rel="stylesheet" href="/static/css/maintopics.css">
  17 +
  18 +<!-- Scripts -->
  19 + <script defer src="/static/mdbootstrap/js/jquery-3.4.1.min.js"></script>
  20 + <script defer src="/static/mdbootstrap/js/popper.min.js"></script>
  21 + <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script>
  22 + <script defer src="/static/mdbootstrap/js/mdb.min.js"></script>
  23 + <script defer src="/static/fontawesome-free/js/all.min.js"></script>
  24 + <script defer src="/static/js/maintopics.js"></script>
  25 +
  26 +</head>
  27 +<!-- ===================================================================== -->
  28 +<body>
  29 +<nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-primary">
  30 + <img src="/static/logo_horizontal.png" height="48" width="120" class="navbar-brand" alt="UEvora">
  31 + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
  32 + <span class="navbar-toggler-icon"></span>
  33 + </button>
  34 +
  35 + <div class="collapse navbar-collapse" id="navbarText">
  36 + <ul class="navbar-nav mr-auto">
  37 + <li class="nav-item active">
  38 + <a class="nav-link" href="#">Cursos<span class="sr-only">(actual)</span></a>
  39 + </li>
  40 + </ul>
  41 +
  42 + <ul class="navbar-nav">
  43 + <li class="nav-item dropdown">
  44 + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
  45 + <i class="fas fa-user" aria-hidden="true"></i>
  46 + <span id="name">{{ escape(name) }}</span>
  47 + <span class="caret"></span>
  48 + </a>
  49 + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
  50 + <a class="dropdown-item" data-toggle="modal" data-target="#password_modal">Mudar Password</a>
  51 + <div class="dropdown-divider"></div>
  52 + <a class="dropdown-item" href="/logout">Sair</a>
  53 + </div>
  54 + </li>
  55 + </ul>
  56 + </div>
  57 +</nav>
  58 +<!-- ===================================================================== -->
  59 +<div class="container">
  60 +
  61 + <div id="notifications"></div>
  62 +
  63 + {% for k,v in courses.items() %}
  64 +
  65 + <a href="/course/{{k}}" class="card text-dark mb-3">
  66 + <div class="card-body">
  67 + <h6 class="card-title">{{ v['title'] }}</h6>
  68 + </div>
  69 + </a>
  70 +
  71 + {% end %} <!-- for -->
  72 +
  73 +
  74 +<!-- === Change Password Modal =========================================== -->
  75 +<div id="password_modal" class="modal fade" tabindex="-1" role="dialog">
  76 + <div class="modal-dialog" role="document">
  77 + <div class="modal-content">
  78 +<!-- header -->
  79 + <div class="modal-header">
  80 + <h5 class="modal-title">Alterar Password</h5>
  81 + </div>
  82 +<!-- body -->
  83 + <div class="modal-body">
  84 + <div class="control-group">
  85 + <label for="new_password" class="control-label">Introduza a nova password:</label>
  86 + <div class="controls">
  87 + <input type="password" id="new_password" name="new_password" autocomplete="new-password">
  88 + </div>
  89 + </div>
  90 + </div>
  91 +<!-- footer -->
  92 + <div class="modal-footer">
  93 + <button type="button" class="btn btn-default" data-dismiss="modal">Cancelar</button>
  94 + <button id="change_password" type="button" class="btn btn-danger" data-dismiss="modal">Alterar</button>
  95 + </div>
  96 +
  97 + </div><!-- /.modal-content -->
  98 + </div><!-- /.modal-dialog -->
  99 +</div><!-- /.modal -->
  100 +
  101 +</body>
  102 +</html>
aprendizations/templates/maintopics-table.html
@@ -34,8 +34,11 @@ @@ -34,8 +34,11 @@
34 34
35 <div class="collapse navbar-collapse" id="navbarText"> 35 <div class="collapse navbar-collapse" id="navbarText">
36 <ul class="navbar-nav mr-auto"> 36 <ul class="navbar-nav mr-auto">
  37 + <li class="nav-item">
  38 + <a class="nav-link" href="/courses">Cursos</a>
  39 + </li>
37 <li class="nav-item active"> 40 <li class="nav-item active">
38 - <a class="nav-link" href="#">Tópicos<span class="sr-only">(current)</span></a> 41 + <a class="nav-link" href="#">Tópicos<span class="sr-only">(actual)</span></a>
39 </li> 42 </li>
40 <li class="nav-item"> 43 <li class="nav-item">
41 <a class="nav-link" href="/rankings">Classificação</a> 44 <a class="nav-link" href="/rankings">Classificação</a>
aprendizations/templates/topic.html
@@ -8,15 +8,18 @@ @@ -8,15 +8,18 @@
8 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 8 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
9 <meta name="author" content="Miguel Barão"> 9 <meta name="author" content="Miguel Barão">
10 10
11 -<!-- MathJax -->  
12 - <script type="text/x-mathjax-config">  
13 - MathJax.Hub.Config({  
14 - tex2jax: {  
15 - inlineMath: [["$$$","$$$"], ["$","$"], ["\\(","\\)"]]  
16 - }  
17 - });  
18 - </script>  
19 - <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script> 11 +<!-- MathJax3 -->
  12 + <script>
  13 + MathJax = {
  14 + tex: {
  15 + inlineMath: [['$$$', '$$$'], ['\\(', '\\)']]
  16 + },
  17 + svg: {
  18 + fontCache: 'global'
  19 + }
  20 + };
  21 + </script>
  22 + <script async type="text/javascript" id="MathJax-script" src="/static/mathjax/es5/tex-svg.js"></script>
20 23
21 <!-- Styles --> 24 <!-- Styles -->
22 <link rel="stylesheet" href="/static/mdbootstrap/css/bootstrap.min.css"> 25 <link rel="stylesheet" href="/static/mdbootstrap/css/bootstrap.min.css">
package-lock.json
@@ -3,42 +3,42 @@ @@ -3,42 +3,42 @@
3 "lockfileVersion": 1, 3 "lockfileVersion": 1,
4 "dependencies": { 4 "dependencies": {
5 "@babel/code-frame": { 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==", 6 + "version": "7.5.5",
  7 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
  8 + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
9 "requires": { 9 "requires": {
10 "@babel/highlight": "^7.0.0" 10 "@babel/highlight": "^7.0.0"
11 } 11 }
12 }, 12 },
13 "@babel/core": { 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==", 14 + "version": "7.6.0",
  15 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz",
  16 + "integrity": "sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw==",
17 "requires": { 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", 18 + "@babel/code-frame": "^7.5.5",
  19 + "@babel/generator": "^7.6.0",
  20 + "@babel/helpers": "^7.6.0",
  21 + "@babel/parser": "^7.6.0",
  22 + "@babel/template": "^7.6.0",
  23 + "@babel/traverse": "^7.6.0",
  24 + "@babel/types": "^7.6.0",
25 "convert-source-map": "^1.1.0", 25 "convert-source-map": "^1.1.0",
26 "debug": "^4.1.0", 26 "debug": "^4.1.0",
27 "json5": "^2.1.0", 27 "json5": "^2.1.0",
28 - "lodash": "^4.17.11", 28 + "lodash": "^4.17.13",
29 "resolve": "^1.3.2", 29 "resolve": "^1.3.2",
30 "semver": "^5.4.1", 30 "semver": "^5.4.1",
31 "source-map": "^0.5.0" 31 "source-map": "^0.5.0"
32 } 32 }
33 }, 33 },
34 "@babel/generator": { 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==", 35 + "version": "7.6.0",
  36 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz",
  37 + "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==",
38 "requires": { 38 "requires": {
39 - "@babel/types": "^7.5.0", 39 + "@babel/types": "^7.6.0",
40 "jsesc": "^2.5.1", 40 "jsesc": "^2.5.1",
41 - "lodash": "^4.17.11", 41 + "lodash": "^4.17.13",
42 "source-map": "^0.5.0", 42 "source-map": "^0.5.0",
43 "trim-right": "^1.0.1" 43 "trim-right": "^1.0.1"
44 } 44 }
@@ -70,13 +70,13 @@ @@ -70,13 +70,13 @@
70 } 70 }
71 }, 71 },
72 "@babel/helpers": { 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==", 73 + "version": "7.6.0",
  74 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz",
  75 + "integrity": "sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ==",
76 "requires": { 76 "requires": {
77 - "@babel/template": "^7.4.4",  
78 - "@babel/traverse": "^7.5.0",  
79 - "@babel/types": "^7.5.0" 77 + "@babel/template": "^7.6.0",
  78 + "@babel/traverse": "^7.6.0",
  79 + "@babel/types": "^7.6.0"
80 } 80 }
81 }, 81 },
82 "@babel/highlight": { 82 "@babel/highlight": {
@@ -90,50 +90,50 @@ @@ -90,50 +90,50 @@
90 } 90 }
91 }, 91 },
92 "@babel/parser": { 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==" 93 + "version": "7.6.0",
  94 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz",
  95 + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ=="
96 }, 96 },
97 "@babel/template": { 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==", 98 + "version": "7.6.0",
  99 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
  100 + "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==",
101 "requires": { 101 "requires": {
102 "@babel/code-frame": "^7.0.0", 102 "@babel/code-frame": "^7.0.0",
103 - "@babel/parser": "^7.4.4",  
104 - "@babel/types": "^7.4.4" 103 + "@babel/parser": "^7.6.0",
  104 + "@babel/types": "^7.6.0"
105 } 105 }
106 }, 106 },
107 "@babel/traverse": { 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==", 108 + "version": "7.6.0",
  109 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz",
  110 + "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==",
111 "requires": { 111 "requires": {
112 - "@babel/code-frame": "^7.0.0",  
113 - "@babel/generator": "^7.5.0", 112 + "@babel/code-frame": "^7.5.5",
  113 + "@babel/generator": "^7.6.0",
114 "@babel/helper-function-name": "^7.1.0", 114 "@babel/helper-function-name": "^7.1.0",
115 "@babel/helper-split-export-declaration": "^7.4.4", 115 "@babel/helper-split-export-declaration": "^7.4.4",
116 - "@babel/parser": "^7.5.0",  
117 - "@babel/types": "^7.5.0", 116 + "@babel/parser": "^7.6.0",
  117 + "@babel/types": "^7.6.0",
118 "debug": "^4.1.0", 118 "debug": "^4.1.0",
119 "globals": "^11.1.0", 119 "globals": "^11.1.0",
120 - "lodash": "^4.17.11" 120 + "lodash": "^4.17.13"
121 } 121 }
122 }, 122 },
123 "@babel/types": { 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==", 124 + "version": "7.6.1",
  125 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
  126 + "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
127 "requires": { 127 "requires": {
128 "esutils": "^2.0.2", 128 "esutils": "^2.0.2",
129 - "lodash": "^4.17.11", 129 + "lodash": "^4.17.13",
130 "to-fast-properties": "^2.0.0" 130 "to-fast-properties": "^2.0.0"
131 } 131 }
132 }, 132 },
133 "@fortawesome/fontawesome-free": { 133 "@fortawesome/fontawesome-free": {
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==" 134 + "version": "5.10.2",
  135 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.10.2.tgz",
  136 + "integrity": "sha512-9pw+Nsnunl9unstGEHQ+u41wBEQue6XPBsILXtJF/4fNN1L3avJcMF/gGF86rIjeTAgfLjTY9ndm68/X4f4idQ=="
137 }, 137 },
138 "ansi-styles": { 138 "ansi-styles": {
139 "version": "3.2.1", 139 "version": "3.2.1",
@@ -154,9 +154,9 @@ @@ -154,9 +154,9 @@
154 } 154 }
155 }, 155 },
156 "codemirror": { 156 "codemirror": {
157 - "version": "5.48.0",  
158 - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.0.tgz",  
159 - "integrity": "sha512-3Ter+tYtRlTNtxtYdYNPxGxBL/b3cMcvPdPm70gvmcOO2Rauv/fUEewWa0tT596Hosv6ea2mtpx28OXBy1mQCg==" 157 + "version": "5.48.4",
  158 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.48.4.tgz",
  159 + "integrity": "sha512-pUhZXDQ6qXSpWdwlgAwHEkd4imA0kf83hINmUEzJpmG80T/XLtDDEzZo8f6PQLuRCcUQhmzqqIo3ZPTRaWByRA=="
160 }, 160 },
161 "color-convert": { 161 "color-convert": {
162 "version": "1.9.3", 162 "version": "1.9.3",
@@ -171,6 +171,11 @@ @@ -171,6 +171,11 @@
171 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 171 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
172 "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 172 "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
173 }, 173 },
  174 + "commander": {
  175 + "version": "3.0.1",
  176 + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.1.tgz",
  177 + "integrity": "sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ=="
  178 + },
174 "convert-source-map": { 179 "convert-source-map": {
175 "version": "1.6.0", 180 "version": "1.6.0",
176 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", 181 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
@@ -192,10 +197,15 @@ @@ -192,10 +197,15 @@
192 "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 197 "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
193 "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 198 "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
194 }, 199 },
  200 + "esm": {
  201 + "version": "3.2.25",
  202 + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
  203 + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
  204 + },
195 "esutils": { 205 "esutils": {
196 - "version": "2.0.2",  
197 - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",  
198 - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" 206 + "version": "2.0.3",
  207 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
  208 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
199 }, 209 },
200 "globals": { 210 "globals": {
201 "version": "11.12.0", 211 "version": "11.12.0",
@@ -226,19 +236,31 @@ @@ -226,19 +236,31 @@
226 } 236 }
227 }, 237 },
228 "lodash": { 238 "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==" 239 + "version": "4.17.15",
  240 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
  241 + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
232 }, 242 },
233 "mathjax": { 243 "mathjax": {
234 - "version": "2.7.5",  
235 - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.5.tgz",  
236 - "integrity": "sha512-OzsJNitEHAJB3y4IIlPCAvS0yoXwYjlo2Y4kmm9KQzyIBZt2d8yKRalby3uTRNN4fZQiGL2iMXjpdP1u2Rq2DQ==" 244 + "version": "3.0.0",
  245 + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.0.0.tgz",
  246 + "integrity": "sha512-z4uLbDHNbs/aRuR6zCcnzwFQuMixkHCcWqgVaommfK/3cA1Ahq7OXemn+m8JwTYcBApSHgcrSbPr9sm3sZFL+A==",
  247 + "requires": {
  248 + "mathjax-full": "git://github.com/mathjax/MathJax-src.git"
  249 + }
  250 + },
  251 + "mathjax-full": {
  252 + "version": "git://github.com/mathjax/MathJax-src.git#0d74266e1820220d33cb6b29d4ca3575b352ac0d",
  253 + "from": "git://github.com/mathjax/MathJax-src.git",
  254 + "requires": {
  255 + "esm": "^3.2.25",
  256 + "mj-context-menu": "^0.2.0",
  257 + "speech-rule-engine": "^3.0.0-beta.6"
  258 + }
237 }, 259 },
238 "mdbootstrap": { 260 "mdbootstrap": {
239 - "version": "4.8.5",  
240 - "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.8.5.tgz",  
241 - "integrity": "sha512-e4YGrdyb5dUlkLu0OdH5UKi6ZZEAC1YrfE8T8oWY+GWCbLjr1C0sI5ZEYAZrN8VI6acKN5tboq6lbK/8nWXG6g==", 261 + "version": "4.8.10",
  262 + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.8.10.tgz",
  263 + "integrity": "sha512-pUjs7Vds4J+MwepOo4obUy7bQ5aMeB8j1c3IxIcEYXOXmn8GOWMSpiRcfSXpH9R4Fgdfie++e0fm5+SebRnTYA==",
242 "requires": { 264 "requires": {
243 "@babel/core": "^7.3.3" 265 "@babel/core": "^7.3.3"
244 } 266 }
@@ -248,6 +270,11 @@ @@ -248,6 +270,11 @@
248 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 270 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
249 "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 271 "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
250 }, 272 },
  273 + "mj-context-menu": {
  274 + "version": "0.2.0",
  275 + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.2.0.tgz",
  276 + "integrity": "sha512-yJxrWBHCjFZEHsZgfs7m5g9OSCNzsVYadW6f6lX3pgZL67vmodtSW/4zhsYmuDKweXfHs0M1kJge1uQIasWA+g=="
  277 + },
251 "ms": { 278 "ms": {
252 "version": "2.1.2", 279 "version": "2.1.2",
253 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 280 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -259,9 +286,9 @@ @@ -259,9 +286,9 @@
259 "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" 286 "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
260 }, 287 },
261 "resolve": { 288 "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==", 289 + "version": "1.12.0",
  290 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
  291 + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
265 "requires": { 292 "requires": {
266 "path-parse": "^1.0.6" 293 "path-parse": "^1.0.6"
267 } 294 }
@@ -272,15 +299,25 @@ @@ -272,15 +299,25 @@
272 "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 299 "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
273 }, 300 },
274 "semver": { 301 "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==" 302 + "version": "5.7.1",
  303 + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
  304 + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
278 }, 305 },
279 "source-map": { 306 "source-map": {
280 "version": "0.5.7", 307 "version": "0.5.7",
281 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 308 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
282 "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 309 "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
283 }, 310 },
  311 + "speech-rule-engine": {
  312 + "version": "3.0.0-beta.6",
  313 + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.0.0-beta.6.tgz",
  314 + "integrity": "sha512-B7gcT53jAsKpx7WvFYQcyUlFmgS3Wa9KlDy0FY8SOTa+Wz5EqmI0MpCD5/fYm8/2qiCPp8HwZg+H3cBgM+sNVw==",
  315 + "requires": {
  316 + "commander": "*",
  317 + "wicked-good-xpath": "*",
  318 + "xmldom-sre": "^0.1.31"
  319 + }
  320 + },
284 "supports-color": { 321 "supports-color": {
285 "version": "5.5.0", 322 "version": "5.5.0",
286 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 323 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -298,6 +335,16 @@ @@ -298,6 +335,16 @@
298 "version": "1.0.1", 335 "version": "1.0.1",
299 "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", 336 "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
300 "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" 337 "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
  338 + },
  339 + "wicked-good-xpath": {
  340 + "version": "1.3.0",
  341 + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
  342 + "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
  343 + },
  344 + "xmldom-sre": {
  345 + "version": "0.1.31",
  346 + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
  347 + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw=="
301 } 348 }
302 } 349 }
303 } 350 }
@@ -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.9.0",  
6 - "codemirror": "^5.48.0",  
7 - "mathjax": "^2.7.5",  
8 - "mdbootstrap": "^4.8.5" 5 + "@fortawesome/fontawesome-free": "^5",
  6 + "codemirror": "^5.48",
  7 + "mathjax": "^3",
  8 + "mdbootstrap": "^4.8.10"
9 }, 9 },
10 "private": true 10 "private": true
11 } 11 }