Commit a9131008a6eb2c630a1b6808c6fbec76a37e29d5
1 parent
b2444bac
Exists in
master
and in
1 other branch
- 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
Showing
9 changed files
with
139 additions
and
90 deletions
Show diff stats
BUGS.md
| 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 | } |
package.json
| @@ -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 | } |