Commit a9131008a6eb2c630a1b6808c6fbec76a37e29d5

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

- allow chapters to not have any questions.yaml file

- fixed some mypy errors
- don't show topic dependencies that are not goals if already done
- allow admin to reload topic and regenerate QFactory
1 1
2 # BUGS 2 # BUGS
3 3
4 -- ir para inicio da pagina quando le nova pergunta. 4 +- goals se forem do tipo chapter deve importar todas as dependencias do chapter (e não mostrar chapters?).
5 - nao esta a seguir o max_tries definido no ficheiro de dependencias. 5 - nao esta a seguir o max_tries definido no ficheiro de dependencias.
6 - devia mostrar timeout 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 - templates question-*.html tem input hidden question_ref que não é usado. remover? 8 - templates question-*.html tem input hidden question_ref que não é usado. remover?
9 - shift-enter não está a funcionar 9 - shift-enter não está a funcionar
10 - default prefix should be obtained from each course (yaml conf)? 10 - default prefix should be obtained from each course (yaml conf)?
11 -- initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a)  
12 11
13 # TODO 12 # TODO
14 13
  14 +- alterar tabelas para incluir email de recuperacao de password (e outros avisos)
15 - registar last_seen e remover os antigos de cada vez que houver um login. 15 - registar last_seen e remover os antigos de cada vez que houver um login.
16 - indicar qtos topicos faltam (>=50%) para terminar o curso. 16 - indicar qtos topicos faltam (>=50%) para terminar o curso.
17 - ao fim de 3 tentativas com password errada, envia email com nova password. 17 - ao fim de 3 tentativas com password errada, envia email com nova password.
@@ -32,6 +32,9 @@ @@ -32,6 +32,9 @@
32 32
33 # FIXED 33 # FIXED
34 34
  35 +- initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a)
  36 +- dependencias que não são goals de um curso, só devem aparecer se ainda não tiverem sido feitas.
  37 +- ir para inicio da pagina quando le nova pergunta.
35 - CRITICAL nao esta a guardar o progresso na base de dados. 38 - CRITICAL nao esta a guardar o progresso na base de dados.
36 - mesma ref no mesmo ficheiro não é detectado. 39 - mesma ref no mesmo ficheiro não é detectado.
37 - enter nas respostas mostra json 40 - enter nas respostas mostra json
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 = '2020.01.dev1' 33 +APP_VERSION = '2020.01.dev2'
34 APP_DESCRIPTION = __doc__ 34 APP_DESCRIPTION = __doc__
35 35
36 __author__ = 'Miguel Barão' 36 __author__ = 'Miguel Barão'
aprendizations/learnapp.py
@@ -36,6 +36,16 @@ class DatabaseUnusableError(LearnException): @@ -36,6 +36,16 @@ class DatabaseUnusableError(LearnException):
36 36
37 # ============================================================================ 37 # ============================================================================
38 # LearnApp - application logic 38 # LearnApp - application logic
  39 +#
  40 +# self.deps - networkx topic dependencies
  41 +# self.courses - dict {course_id: {'title': ...,
  42 +# 'description': ...,
  43 +# 'goals': ...,}, ...}
  44 +# self.factory = dict {qref: QFactory()}
  45 +# self.online - dict {student_id: {'number': ...,
  46 +# 'name': ...,
  47 +# 'state': StudentState(),
  48 +# 'counter': ...}, ...}
39 # ============================================================================ 49 # ============================================================================
40 class LearnApp(object): 50 class LearnApp(object):
41 # ------------------------------------------------------------------------ 51 # ------------------------------------------------------------------------
@@ -67,7 +77,12 @@ class LearnApp(object): @@ -67,7 +77,12 @@ class LearnApp(object):
67 self.db_setup(db) # setup database and check students 77 self.db_setup(db) # setup database and check students
68 self.online: Dict[str, Dict] = dict() # online students 78 self.online: Dict[str, Dict] = dict() # online students
69 79
70 - config: Dict[str, Any] = load_yaml(courses) 80 + try:
  81 + config: Dict[str, Any] = load_yaml(courses)
  82 + except Exception:
  83 + msg = f'Failed to load yaml file "{courses}"'
  84 + logger.error(msg)
  85 + raise LearnException(msg)
71 86
72 # --- topic dependencies are shared between all courses 87 # --- topic dependencies are shared between all courses
73 self.deps = nx.DiGraph(prefix=prefix) 88 self.deps = nx.DiGraph(prefix=prefix)
@@ -78,7 +93,6 @@ class LearnApp(object): @@ -78,7 +93,6 @@ class LearnApp(object):
78 logger.info(f'{len(t):>6} topics in {courses}') 93 logger.info(f'{len(t):>6} topics in {courses}')
79 for f in config.get('topics_from', []): 94 for f in config.get('topics_from', []):
80 c = load_yaml(f) # course configuration 95 c = load_yaml(f) # course configuration
81 -  
82 # FIXME set defaults?? 96 # FIXME set defaults??
83 logger.info(f'{len(c["topics"]):>6} topics imported from {f}') 97 logger.info(f'{len(c["topics"]):>6} topics imported from {f}')
84 self.populate_graph(c) 98 self.populate_graph(c)
@@ -91,8 +105,9 @@ class LearnApp(object): @@ -91,8 +105,9 @@ class LearnApp(object):
91 d.setdefault('title', '') # course title undefined 105 d.setdefault('title', '') # course title undefined
92 for goal in d['goals']: 106 for goal in d['goals']:
93 if goal not in self.deps.nodes(): 107 if goal not in self.deps.nodes():
94 - raise LearnException(f'Goal "{goal}" from course "{c}" '  
95 - ' does not exist') 108 + msg = f'Goal "{goal}" from course "{c}" does not exist'
  109 + logger.error(msg)
  110 + raise LearnException(msg)
96 111
97 # --- factory is a dict with question generators for all topics 112 # --- factory is a dict with question generators for all topics
98 self.factory: Dict[str, QFactory] = self.make_factory() 113 self.factory: Dict[str, QFactory] = self.make_factory()
@@ -230,7 +245,7 @@ class LearnApp(object): @@ -230,7 +245,7 @@ class LearnApp(object):
230 async def check_answer(self, uid: str, answer) -> Question: 245 async def check_answer(self, uid: str, answer) -> Question:
231 student = self.online[uid]['state'] 246 student = self.online[uid]['state']
232 await student.check_answer(answer) 247 await student.check_answer(answer)
233 - q = student.get_current_question() 248 + q: Question = student.get_current_question()
234 249
235 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') 250 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"')
236 251
@@ -287,21 +302,25 @@ class LearnApp(object): @@ -287,21 +302,25 @@ class LearnApp(object):
287 # ------------------------------------------------------------------------ 302 # ------------------------------------------------------------------------
288 # Start course 303 # Start course
289 # ------------------------------------------------------------------------ 304 # ------------------------------------------------------------------------
290 - def start_course(self, uid: str, course: str) -> None: 305 + def start_course(self, uid: str, course_id: str) -> None:
291 student = self.online[uid]['state'] 306 student = self.online[uid]['state']
292 try: 307 try:
293 - student.start_course(course)  
294 - except Exception as e:  
295 - logger.warning(f'"{uid}" could not start course "{course}": {e}') 308 + student.start_course(course_id)
  309 + except Exception:
  310 + logger.warning(f'"{uid}" could not start course "{course_id}"')
296 raise 311 raise
297 else: 312 else:
298 - logger.info(f'User "{uid}" started course "{course}"') 313 + logger.info(f'User "{uid}" started course "{course_id}"')
299 314
300 # ------------------------------------------------------------------------ 315 # ------------------------------------------------------------------------
301 # Start new topic 316 # Start new topic
302 # ------------------------------------------------------------------------ 317 # ------------------------------------------------------------------------
303 async def start_topic(self, uid: str, topic: str) -> None: 318 async def start_topic(self, uid: str, topic: str) -> None:
304 student = self.online[uid]['state'] 319 student = self.online[uid]['state']
  320 + if uid == '0':
  321 + logger.warning(f'Reloading "{topic}"')
  322 + self.factory.update(self.factory_for(topic))
  323 +
305 try: 324 try:
306 await student.start_topic(topic) 325 await student.start_topic(topic)
307 except Exception as e: 326 except Exception as e:
@@ -393,66 +412,81 @@ class LearnApp(object): @@ -393,66 +412,81 @@ class LearnApp(object):
393 412
394 # ------------------------------------------------------------------------ 413 # ------------------------------------------------------------------------
395 # Buils dictionary of question factories 414 # Buils dictionary of question factories
  415 + # - visits each topic in the graph,
  416 + # - adds factory for each topic.
396 # ------------------------------------------------------------------------ 417 # ------------------------------------------------------------------------
397 def make_factory(self) -> Dict[str, QFactory]: 418 def make_factory(self) -> Dict[str, QFactory]:
398 419
399 logger.info('Building questions factory:') 420 logger.info('Building questions factory:')
400 - factory = {} 421 + factory = dict()
401 g = self.deps 422 g = self.deps
402 for tref in g.nodes(): 423 for tref in g.nodes():
403 - t = g.nodes[tref] 424 + factory.update(self.factory_for(tref))
404 425
405 - # load questions as list of dicts  
406 - topicpath: str = path.join(g.graph['prefix'], tref)  
407 - try:  
408 - fullpath: str = path.join(topicpath, t['file'])  
409 - except Exception:  
410 - msg1 = f'Invalid topic "{tref}"'  
411 - msg2 = f'Check dependencies of {", ".join(g.successors(tref))}'  
412 - raise LearnException(f'{msg1}. {msg2}') 426 + logger.info(f'Factory has {len(factory)} questions')
  427 + return factory
413 428
414 - logger.debug(f' Loading {fullpath}')  
415 - try:  
416 - questions: List[QDict] = load_yaml(fullpath)  
417 - except Exception: 429 + # ------------------------------------------------------------------------
  430 + # makes factory for a single topic
  431 + # ------------------------------------------------------------------------
  432 + def factory_for(self, tref: str) -> Dict[str, QFactory]:
  433 + factory: Dict[str, QFactory] = {}
  434 + g = self.deps
  435 + t = g.nodes[tref] # get node
  436 +
  437 + # load questions as list of dicts
  438 + topicpath: str = path.join(g.graph['prefix'], tref)
  439 + try:
  440 + fullpath: str = path.join(topicpath, t['file'])
  441 + except Exception:
  442 + msg1 = f'Invalid topic "{tref}"'
  443 + msg2 = f'Check dependencies of {", ".join(g.successors(tref))}'
  444 + raise LearnException(f'{msg1}. {msg2}')
  445 +
  446 + logger.debug(f' Loading {fullpath}')
  447 + try:
  448 + questions: List[QDict] = load_yaml(fullpath)
  449 + except Exception:
  450 + if t['type'] == 'chapter':
  451 + return factory # chapters may have no "questions"
  452 + else:
418 msg = f'Failed to load "{fullpath}"' 453 msg = f'Failed to load "{fullpath}"'
419 logger.error(msg) 454 logger.error(msg)
420 raise LearnException(msg) 455 raise LearnException(msg)
421 456
422 - if not isinstance(questions, list):  
423 - msg = f'File "{fullpath}" must be a list of questions' 457 + if not isinstance(questions, list):
  458 + msg = f'File "{fullpath}" must be a list of questions'
  459 + raise LearnException(msg)
  460 +
  461 + # update refs to include topic as prefix.
  462 + # refs are required to be unique only within the file.
  463 + # undefined are set to topic:n, where n is the question number
  464 + # within the file
  465 + localrefs: Set[str] = set() # refs in current file
  466 + for i, q in enumerate(questions):
  467 + qref = q.get('ref', str(i)) # ref or number
  468 + if qref in localrefs:
  469 + msg = f'Duplicate ref "{qref}" in "{topicpath}"'
424 raise LearnException(msg) 470 raise LearnException(msg)
  471 + localrefs.add(qref)
425 472
426 - # update refs to include topic as prefix.  
427 - # refs are required to be unique only within the file.  
428 - # undefined are set to topic:n, where n is the question number  
429 - # within the file  
430 - localrefs: Set[str] = set() # refs in current file  
431 - for i, q in enumerate(questions):  
432 - qref = q.get('ref', str(i)) # ref or number  
433 - if qref in localrefs:  
434 - msg = f'Duplicate ref "{qref}" in "{topicpath}"'  
435 - raise LearnException(msg)  
436 - localrefs.add(qref)  
437 -  
438 - q['ref'] = f'{tref}:{qref}'  
439 - q['path'] = topicpath  
440 - q.setdefault('append_wrong', t['append_wrong']) 473 + q['ref'] = f'{tref}:{qref}'
  474 + q['path'] = topicpath
  475 + q.setdefault('append_wrong', t['append_wrong'])
441 476
442 - # if questions are left undefined, include all.  
443 - if not t['questions']:  
444 - t['questions'] = [q['ref'] for q in questions] 477 + # if questions are left undefined, include all.
  478 + if not t['questions']:
  479 + t['questions'] = [q['ref'] for q in questions]
445 480
446 - t['choose'] = min(t['choose'], len(t['questions'])) 481 + t['choose'] = min(t['choose'], len(t['questions']))
447 482
448 - for q in questions:  
449 - if q['ref'] in t['questions']:  
450 - factory[q['ref']] = QFactory(q)  
451 - logger.debug(f' + {q["ref"]}') 483 + for q in questions:
  484 + if q['ref'] in t['questions']:
  485 + factory[q['ref']] = QFactory(q)
  486 + logger.debug(f' + {q["ref"]}')
452 487
453 - logger.info(f'{len(t["questions"]):6} questions in {tref}') 488 + logger.info(f'{len(t["questions"]):6} questions in {tref}')
454 489
455 - logger.info(f'Factory has {len(factory)} questions')  
456 return factory 490 return factory
457 491
458 # ------------------------------------------------------------------------ 492 # ------------------------------------------------------------------------
@@ -469,47 +503,53 @@ class LearnApp(object): @@ -469,47 +503,53 @@ class LearnApp(object):
469 503
470 # ------------------------------------------------------------------------ 504 # ------------------------------------------------------------------------
471 def get_student_progress(self, uid: str) -> float: 505 def get_student_progress(self, uid: str) -> float:
472 - return self.online[uid]['state'].get_topic_progress() 506 + return float(self.online[uid]['state'].get_topic_progress())
473 507
474 # ------------------------------------------------------------------------ 508 # ------------------------------------------------------------------------
475 def get_current_question(self, uid: str) -> Optional[Question]: 509 def get_current_question(self, uid: str) -> Optional[Question]:
476 - return self.online[uid]['state'].get_current_question() 510 + q: Optional[Question] = self.online[uid]['state'].get_current_question()
  511 + return q
477 512
478 # ------------------------------------------------------------------------ 513 # ------------------------------------------------------------------------
479 def get_current_question_id(self, uid: str) -> str: 514 def get_current_question_id(self, uid: str) -> str:
480 - return self.online[uid]['state'].get_current_question()['qid'] 515 + return str(self.online[uid]['state'].get_current_question()['qid'])
481 516
482 # ------------------------------------------------------------------------ 517 # ------------------------------------------------------------------------
483 def get_student_question_type(self, uid: str) -> str: 518 def get_student_question_type(self, uid: str) -> str:
484 - return self.online[uid]['state'].get_current_question()['type'] 519 + return str(self.online[uid]['state'].get_current_question()['type'])
485 520
486 # ------------------------------------------------------------------------ 521 # ------------------------------------------------------------------------
487 def get_student_topic(self, uid: str) -> str: 522 def get_student_topic(self, uid: str) -> str:
488 - return self.online[uid]['state'].get_current_topic() 523 + return str(self.online[uid]['state'].get_current_topic())
489 524
490 # ------------------------------------------------------------------------ 525 # ------------------------------------------------------------------------
491 def get_student_course_title(self, uid: str) -> str: 526 def get_student_course_title(self, uid: str) -> str:
492 - return self.online[uid]['state'].get_current_course_title() 527 + return str(self.online[uid]['state'].get_current_course_title())
493 528
494 # ------------------------------------------------------------------------ 529 # ------------------------------------------------------------------------
495 - def get_student_course_id(self, uid: str) -> Optional[str]:  
496 - return self.online[uid]['state'].get_current_course_id() 530 + def get_current_course_id(self, uid: str) -> Optional[str]:
  531 + cid: Optional[str] = self.online[uid]['state'].get_current_course_id()
  532 + return cid
497 533
498 # ------------------------------------------------------------------------ 534 # ------------------------------------------------------------------------
499 def get_topic_name(self, ref: str) -> str: 535 def get_topic_name(self, ref: str) -> str:
500 - return self.deps.nodes[ref]['name'] 536 + return str(self.deps.nodes[ref]['name'])
501 537
502 # ------------------------------------------------------------------------ 538 # ------------------------------------------------------------------------
503 def get_current_public_dir(self, uid: str) -> str: 539 def get_current_public_dir(self, uid: str) -> str:
504 - topic: str = self.online[uid]['state'].get_current_topic() # FIXME returns None if its the last question in the topic 540 + topic: str = self.online[uid]['state'].get_current_topic()
505 prefix: str = self.deps.graph['prefix'] 541 prefix: str = self.deps.graph['prefix']
506 return path.join(prefix, topic, 'public') 542 return path.join(prefix, topic, 'public')
507 543
508 # ------------------------------------------------------------------------ 544 # ------------------------------------------------------------------------
509 - def get_courses(self, uid: str) -> Dict: 545 + def get_courses(self) -> Dict[str, Dict[str, Any]]:
510 return self.courses 546 return self.courses
511 547
512 # ------------------------------------------------------------------------ 548 # ------------------------------------------------------------------------
  549 + def get_course(self, course_id: str) -> Dict[str, Any]:
  550 + return self.courses[course_id]
  551 +
  552 + # ------------------------------------------------------------------------
513 def get_rankings(self, uid: str, course_id: str) -> Iterable[Tuple[str, str, float, float]]: 553 def get_rankings(self, uid: str, course_id: str) -> Iterable[Tuple[str, str, float, float]]:
514 554
515 logger.info(f'User "{uid}" get rankings for {course_id}') 555 logger.info(f'User "{uid}" get rankings for {course_id}')
aprendizations/main.py
@@ -179,10 +179,10 @@ def main(): @@ -179,10 +179,10 @@ def main():
179 sep='\n') 179 sep='\n')
180 sys.exit(1) 180 sys.exit(1)
181 except LearnException as e: 181 except LearnException as e:
182 - logging.critical(e) 182 + logging.critical('Failed to start backend')
183 sys.exit(1) 183 sys.exit(1)
184 except Exception: 184 except Exception:
185 - logging.critical('Failed to start backend.') 185 + logging.critical('Unknown error')
186 sys.exit(1) 186 sys.exit(1)
187 else: 187 else:
188 logging.info('LearnApp started') 188 logging.info('LearnApp started')
aprendizations/serve.py
@@ -96,7 +96,7 @@ class RankingsHandler(BaseHandler): @@ -96,7 +96,7 @@ class RankingsHandler(BaseHandler):
96 @tornado.web.authenticated 96 @tornado.web.authenticated
97 def get(self): 97 def get(self):
98 uid = self.current_user 98 uid = self.current_user
99 - current_course = self.learn.get_student_course_id(uid) 99 + current_course = self.learn.get_current_course_id(uid)
100 course_id = self.get_query_argument('course', default=current_course) 100 course_id = self.get_query_argument('course', default=current_course)
101 rankings = self.learn.get_rankings(uid, course_id) 101 rankings = self.learn.get_rankings(uid, course_id)
102 self.render('rankings.html', 102 self.render('rankings.html',
@@ -195,7 +195,7 @@ class CoursesHandler(BaseHandler): @@ -195,7 +195,7 @@ class CoursesHandler(BaseHandler):
195 appname=APP_NAME, 195 appname=APP_NAME,
196 uid=uid, 196 uid=uid,
197 name=self.learn.get_student_name(uid), 197 name=self.learn.get_student_name(uid),
198 - courses=self.learn.get_courses(uid), 198 + courses=self.learn.get_courses(),
199 ) 199 )
200 200
201 201
@@ -205,13 +205,11 @@ class CoursesHandler(BaseHandler): @@ -205,13 +205,11 @@ class CoursesHandler(BaseHandler):
205 # ---------------------------------------------------------------------------- 205 # ----------------------------------------------------------------------------
206 class CourseHandler(BaseHandler): 206 class CourseHandler(BaseHandler):
207 @tornado.web.authenticated 207 @tornado.web.authenticated
208 - def get(self, course): 208 + def get(self, course_id):
209 uid = self.current_user 209 uid = self.current_user
210 - if course == '':  
211 - course = self.learn.get_student_course_id(uid)  
212 210
213 try: 211 try:
214 - self.learn.start_course(uid, course) 212 + self.learn.start_course(uid, course_id)
215 except KeyError: 213 except KeyError:
216 self.redirect('/courses') 214 self.redirect('/courses')
217 215
@@ -220,8 +218,8 @@ class CourseHandler(BaseHandler): @@ -220,8 +218,8 @@ class CourseHandler(BaseHandler):
220 uid=uid, 218 uid=uid,
221 name=self.learn.get_student_name(uid), 219 name=self.learn.get_student_name(uid),
222 state=self.learn.get_student_state(uid), 220 state=self.learn.get_student_state(uid),
223 - course_title=self.learn.get_student_course_title(uid),  
224 - course_id=self.learn.get_student_course_id(uid), 221 + course_id=course_id,
  222 + course=self.learn.get_course(course_id)
225 ) 223 )
226 224
227 225
@@ -244,7 +242,7 @@ class TopicHandler(BaseHandler): @@ -244,7 +242,7 @@ class TopicHandler(BaseHandler):
244 uid=uid, 242 uid=uid,
245 name=self.learn.get_student_name(uid), 243 name=self.learn.get_student_name(uid),
246 # course_title=self.learn.get_student_course_title(uid), 244 # course_title=self.learn.get_student_course_title(uid),
247 - course_id=self.learn.get_student_course_id(uid), 245 + course_id=self.learn.get_current_course_id(uid),
248 ) 246 )
249 247
250 248
aprendizations/student.py
@@ -75,12 +75,12 @@ class StudentState(object): @@ -75,12 +75,12 @@ class StudentState(object):
75 logger.debug(f'start topic "{topic}"') 75 logger.debug(f'start topic "{topic}"')
76 76
77 # avoid regenerating questions in the middle of the current topic 77 # avoid regenerating questions in the middle of the current topic
78 - if self.current_topic == topic: 78 + if self.current_topic == topic and self.uid != '0':
79 logger.info('Restarting current topic is not allowed.') 79 logger.info('Restarting current topic is not allowed.')
80 return 80 return
81 81
82 # do not allow locked topics 82 # do not allow locked topics
83 - if self.is_locked(topic): 83 + if self.is_locked(topic) and self.uid != '0':
84 logger.debug(f'is locked "{topic}"') 84 logger.debug(f'is locked "{topic}"')
85 return 85 return
86 86
@@ -236,9 +236,17 @@ class StudentState(object): @@ -236,9 +236,17 @@ class StudentState(object):
236 G = self.deps 236 G = self.deps
237 ts = set(goals) 237 ts = set(goals)
238 for t in goals: 238 for t in goals:
239 - ts.update(nx.ancestors(G, t))  
240 - # FIXME filter by level done, only level < 50% are included  
241 - tl = list(nx.topological_sort(G.subgraph(ts))) 239 + ts.update(nx.ancestors(G, t)) # include dependencies not in goals
  240 +
  241 + todo = []
  242 + for t in ts:
  243 + level = self.state[t]['level'] if t in self.state else 0.0
  244 + min_level = G.nodes[t]['min_level']
  245 + if t in goals or level < min_level:
  246 + todo.append(t)
  247 +
  248 + # FIXME topological sort is a poor way to sort topics
  249 + tl = list(nx.topological_sort(G.subgraph(todo)))
242 250
243 # sort with unlocked first 251 # sort with unlocked first
244 unlocked = [t for t in tl if t in self.state] 252 unlocked = [t for t in tl if t in self.state]
aprendizations/templates/maintopics-table.html
@@ -60,7 +60,7 @@ @@ -60,7 +60,7 @@
60 60
61 <div id="notifications"></div> 61 <div id="notifications"></div>
62 62
63 - <h4>{{ course_title }}</h4> 63 + <h4>{{ course['title'] }}</h4>
64 64
65 <table class="table table-hover"> 65 <table class="table table-hover">
66 <thead class=""> 66 <thead class="">
package-lock.json
@@ -8,14 +8,14 @@ @@ -8,14 +8,14 @@
8 "integrity": "sha512-vKDJUuE2GAdBERaQWmmtsciAMzjwNrROXA5KTGSZvayAsmuTGjam5z6QNqNPCwDfVljLWuov1nEC3mEQf/n6fQ==" 8 "integrity": "sha512-vKDJUuE2GAdBERaQWmmtsciAMzjwNrROXA5KTGSZvayAsmuTGjam5z6QNqNPCwDfVljLWuov1nEC3mEQf/n6fQ=="
9 }, 9 },
10 "codemirror": { 10 "codemirror": {
11 - "version": "5.50.0",  
12 - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.50.0.tgz",  
13 - "integrity": "sha512-32LAmGcBNhKtJP4WGgkcaCVQDyChAyaWA6jasg778ziZzo3PWBuhpAQIJMO8//Id45RoaLyXjuhcRUBoS8Vg+Q==" 11 + "version": "5.51.0",
  12 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.51.0.tgz",
  13 + "integrity": "sha512-vyuYYRv3eXL0SCuZA4spRFlKNzQAewHcipRQCOKgRy7VNAvZxTKzbItdbCl4S5AgPZ5g3WkHp+ibWQwv9TLG7Q=="
14 }, 14 },
15 "mdbootstrap": { 15 "mdbootstrap": {
16 - "version": "4.11.0",  
17 - "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.11.0.tgz",  
18 - "integrity": "sha512-3yhRRo8UQDqRgeEutSpx9jIECzkyPebOq/oYsG2TLAmXVmujDBb+OoTW6+yZ1MtaQZCu8AF8D1/pM9Y8sLj3uA==" 16 + "version": "4.12.0",
  17 + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.12.0.tgz",
  18 + "integrity": "sha512-+X4x63tE96zpVOcRlVUGdcR65M9Ud+/l1TvdmcwUjEGo3ktn9TO3e6S3DBLTvchO9U5eKuJh/MIWIGac7+569g=="
19 } 19 }
20 } 20 }
21 } 21 }
@@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
3 "email": "mjsb@uevora.pt", 3 "email": "mjsb@uevora.pt",
4 "dependencies": { 4 "dependencies": {
5 "@fortawesome/fontawesome-free": "^5.12.0", 5 "@fortawesome/fontawesome-free": "^5.12.0",
6 - "codemirror": "^5.50.0",  
7 - "mdbootstrap": "^4.11.0" 6 + "codemirror": "^5.51.0",
  7 + "mdbootstrap": "^4.12.0"
8 }, 8 },
9 "private": true 9 "private": true
10 } 10 }