Commit a6b50da08def113bd299f00b1f8933ab59062153
1 parent
d8e3946a
Exists in
master
and in
1 other branch
fixes error where progress is not being saved in the database
Showing
6 changed files
with
151 additions
and
85 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 | - 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. |
| 5 | - devia mostrar timeout para o aluno saber a razao. | 6 | - devia mostrar timeout para o aluno saber a razao. |
| 6 | - permitir configuracao para escolher entre static files locais ou remotos | 7 | - permitir configuracao para escolher entre static files locais ou remotos |
| @@ -31,6 +32,8 @@ | @@ -31,6 +32,8 @@ | ||
| 31 | 32 | ||
| 32 | # FIXED | 33 | # FIXED |
| 33 | 34 | ||
| 35 | +- CRITICAL nao esta a guardar o progresso na base de dados. | ||
| 36 | +- mesma ref no mesmo ficheiro não é detectado. | ||
| 34 | - enter nas respostas mostra json | 37 | - enter nas respostas mostra json |
| 35 | - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | 38 | - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) |
| 36 | - double click submits twice. | 39 | - double click submits twice. |
aprendizations/learnapp.py
| @@ -7,7 +7,7 @@ from datetime import datetime | @@ -7,7 +7,7 @@ from datetime import datetime | ||
| 7 | import logging | 7 | import logging |
| 8 | from random import random | 8 | from random import random |
| 9 | from os import path | 9 | from os import path |
| 10 | -from typing import Any, Dict, Iterable, List, Optional, Tuple | 10 | +from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict |
| 11 | 11 | ||
| 12 | # third party libraries | 12 | # third party libraries |
| 13 | import bcrypt | 13 | import bcrypt |
| @@ -88,9 +88,9 @@ class LearnApp(object): | @@ -88,9 +88,9 @@ class LearnApp(object): | ||
| 88 | self.courses = config['courses'] | 88 | self.courses = config['courses'] |
| 89 | logger.info(f'Courses: {", ".join(self.courses.keys())}') | 89 | logger.info(f'Courses: {", ".join(self.courses.keys())}') |
| 90 | for c, d in self.courses.items(): | 90 | for c, d in self.courses.items(): |
| 91 | + d.setdefault('title', '') # course title undefined | ||
| 91 | for goal in d['goals']: | 92 | for goal in d['goals']: |
| 92 | if goal not in self.deps.nodes(): | 93 | if goal not in self.deps.nodes(): |
| 93 | - # logger.error(f'Goal "{goal}" of "{c}"" not in the graph') | ||
| 94 | raise LearnException(f'Goal "{goal}" from course "{c}" ' | 94 | raise LearnException(f'Goal "{goal}" from course "{c}" ' |
| 95 | ' does not exist') | 95 | ' does not exist') |
| 96 | 96 | ||
| @@ -229,9 +229,8 @@ class LearnApp(object): | @@ -229,9 +229,8 @@ class LearnApp(object): | ||
| 229 | # ------------------------------------------------------------------------ | 229 | # ------------------------------------------------------------------------ |
| 230 | async def check_answer(self, uid: str, answer) -> Question: | 230 | async def check_answer(self, uid: str, answer) -> Question: |
| 231 | student = self.online[uid]['state'] | 231 | student = self.online[uid]['state'] |
| 232 | - topic = student.get_current_topic() | ||
| 233 | - | ||
| 234 | - q = await student.check_answer(answer) | 232 | + await student.check_answer(answer) |
| 233 | + q = student.get_current_question() | ||
| 235 | 234 | ||
| 236 | logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') | 235 | logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') |
| 237 | 236 | ||
| @@ -243,14 +242,24 @@ class LearnApp(object): | @@ -243,14 +242,24 @@ class LearnApp(object): | ||
| 243 | starttime=str(q['start_time']), | 242 | starttime=str(q['start_time']), |
| 244 | finishtime=str(q['finish_time']), | 243 | finishtime=str(q['finish_time']), |
| 245 | student_id=uid, | 244 | student_id=uid, |
| 246 | - topic_id=topic)) | ||
| 247 | - logger.debug(f'db insert answer of {q["ref"]}') | 245 | + topic_id=student.get_current_topic())) |
| 246 | + | ||
| 247 | + return q | ||
| 248 | + | ||
| 249 | + # ------------------------------------------------------------------------ | ||
| 250 | + # get the question to show (current or new one) | ||
| 251 | + # if no more questions, save/update level in database | ||
| 252 | + # ------------------------------------------------------------------------ | ||
| 253 | + async def get_question(self, uid: str) -> Optional[Question]: | ||
| 254 | + student = self.online[uid]['state'] | ||
| 255 | + q: Optional[Question] = await student.get_question() | ||
| 248 | 256 | ||
| 249 | - # save topic if finished | 257 | + # save topic to database if finished |
| 250 | if student.topic_has_finished(): | 258 | if student.topic_has_finished(): |
| 251 | - logger.info(f'User "{uid}" finished "{topic}"') | 259 | + topic: str = student.get_previous_topic() |
| 252 | level: float = student.get_topic_level(topic) | 260 | level: float = student.get_topic_level(topic) |
| 253 | date: str = str(student.get_topic_date(topic)) | 261 | date: str = str(student.get_topic_date(topic)) |
| 262 | + logger.info(f'User "{uid}" finished "{topic}" (level={level:.2})') | ||
| 254 | 263 | ||
| 255 | with self.db_session() as s: | 264 | with self.db_session() as s: |
| 256 | a = s.query(StudentTopic) \ | 265 | a = s.query(StudentTopic) \ |
| @@ -276,12 +285,6 @@ class LearnApp(object): | @@ -276,12 +285,6 @@ class LearnApp(object): | ||
| 276 | return q | 285 | return q |
| 277 | 286 | ||
| 278 | # ------------------------------------------------------------------------ | 287 | # ------------------------------------------------------------------------ |
| 279 | - # get the question to show (current or new one) | ||
| 280 | - # ------------------------------------------------------------------------ | ||
| 281 | - async def get_question(self, uid: str) -> Optional[Question]: | ||
| 282 | - return await self.online[uid]['state'].get_question() | ||
| 283 | - | ||
| 284 | - # ------------------------------------------------------------------------ | ||
| 285 | # Start course | 288 | # Start course |
| 286 | # ------------------------------------------------------------------------ | 289 | # ------------------------------------------------------------------------ |
| 287 | def start_course(self, uid: str, course: str) -> None: | 290 | def start_course(self, uid: str, course: str) -> None: |
| @@ -409,14 +412,28 @@ class LearnApp(object): | @@ -409,14 +412,28 @@ class LearnApp(object): | ||
| 409 | fullpath: str = path.join(topicpath, t['file']) | 412 | fullpath: str = path.join(topicpath, t['file']) |
| 410 | 413 | ||
| 411 | logger.debug(f' Loading {fullpath}') | 414 | logger.debug(f' Loading {fullpath}') |
| 412 | - questions: List[QDict] = load_yaml(fullpath, default=[]) | 415 | + # questions: List[QDict] = load_yaml(fullpath, default=[]) |
| 416 | + try: | ||
| 417 | + questions: List[QDict] = load_yaml(fullpath) | ||
| 418 | + except Exception: | ||
| 419 | + raise LearnException(f'Failed to load "{fullpath}"') | ||
| 420 | + | ||
| 421 | + if not isinstance(questions, list): | ||
| 422 | + msg = f'File "{fullpath}" must be a list of questions' | ||
| 423 | + raise LearnException(msg) | ||
| 413 | 424 | ||
| 414 | # update refs to include topic as prefix. | 425 | # update refs to include topic as prefix. |
| 415 | # refs are required to be unique only within the file. | 426 | # refs are required to be unique only within the file. |
| 416 | # undefined are set to topic:n, where n is the question number | 427 | # undefined are set to topic:n, where n is the question number |
| 417 | # within the file | 428 | # within the file |
| 429 | + localrefs: Set[str] = set() # refs in current file | ||
| 418 | for i, q in enumerate(questions): | 430 | for i, q in enumerate(questions): |
| 419 | qref = q.get('ref', str(i)) # ref or number | 431 | qref = q.get('ref', str(i)) # ref or number |
| 432 | + if qref in localrefs: | ||
| 433 | + msg = f'Duplicate ref "{qref}" in "{topicpath}"' | ||
| 434 | + raise LearnException(msg) | ||
| 435 | + localrefs.add(qref) | ||
| 436 | + | ||
| 420 | q['ref'] = f'{tref}:{qref}' | 437 | q['ref'] = f'{tref}:{qref}' |
| 421 | q['path'] = topicpath | 438 | q['path'] = topicpath |
| 422 | q.setdefault('append_wrong', t['append_wrong']) | 439 | q.setdefault('append_wrong', t['append_wrong']) |
| @@ -439,7 +456,7 @@ class LearnApp(object): | @@ -439,7 +456,7 @@ class LearnApp(object): | ||
| 439 | 456 | ||
| 440 | # ------------------------------------------------------------------------ | 457 | # ------------------------------------------------------------------------ |
| 441 | def get_login_counter(self, uid: str) -> int: | 458 | def get_login_counter(self, uid: str) -> int: |
| 442 | - return self.online[uid]['counter'] | 459 | + return int(self.online[uid]['counter']) |
| 443 | 460 | ||
| 444 | # ------------------------------------------------------------------------ | 461 | # ------------------------------------------------------------------------ |
| 445 | def get_student_name(self, uid: str) -> str: | 462 | def get_student_name(self, uid: str) -> str: |
| @@ -474,7 +491,7 @@ class LearnApp(object): | @@ -474,7 +491,7 @@ class LearnApp(object): | ||
| 474 | return self.online[uid]['state'].get_current_course_title() | 491 | return self.online[uid]['state'].get_current_course_title() |
| 475 | 492 | ||
| 476 | # ------------------------------------------------------------------------ | 493 | # ------------------------------------------------------------------------ |
| 477 | - def get_student_course_id(self, uid: str) -> str: | 494 | + def get_student_course_id(self, uid: str) -> Optional[str]: |
| 478 | return self.online[uid]['state'].get_current_course_id() | 495 | return self.online[uid]['state'].get_current_course_id() |
| 479 | 496 | ||
| 480 | # ------------------------------------------------------------------------ | 497 | # ------------------------------------------------------------------------ |
| @@ -520,7 +537,7 @@ class LearnApp(object): | @@ -520,7 +537,7 @@ class LearnApp(object): | ||
| 520 | # compute topic progress | 537 | # compute topic progress |
| 521 | now = datetime.now() | 538 | now = datetime.now() |
| 522 | goals = self.courses[course_id]['goals'] | 539 | goals = self.courses[course_id]['goals'] |
| 523 | - prog = defaultdict(int) | 540 | + prog: DefaultDict[str, float] = defaultdict(int) |
| 524 | 541 | ||
| 525 | for uid, topic, level, date in student_topics: | 542 | for uid, topic, level, date in student_topics: |
| 526 | if topic in goals: | 543 | if topic in goals: |
aprendizations/questions.py
| 1 | 1 | ||
| 2 | # python standard library | 2 | # python standard library |
| 3 | import asyncio | 3 | import asyncio |
| 4 | +from datetime import datetime | ||
| 4 | import random | 5 | import random |
| 5 | import re | 6 | import re |
| 6 | from os import path | 7 | from os import path |
| @@ -44,6 +45,10 @@ class Question(dict): | @@ -44,6 +45,10 @@ class Question(dict): | ||
| 44 | 'files': {}, | 45 | 'files': {}, |
| 45 | })) | 46 | })) |
| 46 | 47 | ||
| 48 | + def set_answer(self, ans) -> None: | ||
| 49 | + self['answer'] = ans | ||
| 50 | + self['finish_time'] = datetime.now() | ||
| 51 | + | ||
| 47 | def correct(self) -> None: | 52 | def correct(self) -> None: |
| 48 | self['comments'] = '' | 53 | self['comments'] = '' |
| 49 | self['grade'] = 0.0 | 54 | self['grade'] = 0.0 |
| @@ -74,7 +79,14 @@ class QuestionRadio(Question): | @@ -74,7 +79,14 @@ class QuestionRadio(Question): | ||
| 74 | def __init__(self, q: QDict) -> None: | 79 | def __init__(self, q: QDict) -> None: |
| 75 | super().__init__(q) | 80 | super().__init__(q) |
| 76 | 81 | ||
| 77 | - n = len(self['options']) | 82 | + try: |
| 83 | + n = len(self['options']) | ||
| 84 | + except KeyError: | ||
| 85 | + msg = f'Missing `options` in radio question. See {self["path"]}' | ||
| 86 | + raise QuestionException(msg) | ||
| 87 | + except TypeError: | ||
| 88 | + msg = f'`options` must be a list. See {self["path"]}' | ||
| 89 | + raise QuestionException(msg) | ||
| 78 | 90 | ||
| 79 | self.set_defaults(QDict({ | 91 | self.set_defaults(QDict({ |
| 80 | 'text': '', | 92 | 'text': '', |
| @@ -165,7 +177,7 @@ class QuestionRadio(Question): | @@ -165,7 +177,7 @@ class QuestionRadio(Question): | ||
| 165 | # x_aver can be exactly 1.0 if all options are right | 177 | # x_aver can be exactly 1.0 if all options are right |
| 166 | if self['discount'] and x_aver != 1.0: | 178 | if self['discount'] and x_aver != 1.0: |
| 167 | x = (x - x_aver) / (1.0 - x_aver) | 179 | x = (x - x_aver) / (1.0 - x_aver) |
| 168 | - self['grade'] = x | 180 | + self['grade'] = float(x) |
| 169 | 181 | ||
| 170 | 182 | ||
| 171 | # ============================================================================ | 183 | # ============================================================================ |
| @@ -185,7 +197,14 @@ class QuestionCheckbox(Question): | @@ -185,7 +197,14 @@ class QuestionCheckbox(Question): | ||
| 185 | def __init__(self, q: QDict) -> None: | 197 | def __init__(self, q: QDict) -> None: |
| 186 | super().__init__(q) | 198 | super().__init__(q) |
| 187 | 199 | ||
| 188 | - n = len(self['options']) | 200 | + try: |
| 201 | + n = len(self['options']) | ||
| 202 | + except KeyError: | ||
| 203 | + msg = f'Missing `options` in radio question. See {self["path"]}' | ||
| 204 | + raise QuestionException(msg) | ||
| 205 | + except TypeError: | ||
| 206 | + msg = f'`options` must be a list. See {self["path"]}' | ||
| 207 | + raise QuestionException(msg) | ||
| 189 | 208 | ||
| 190 | # set defaults if missing | 209 | # set defaults if missing |
| 191 | self.set_defaults(QDict({ | 210 | self.set_defaults(QDict({ |
aprendizations/serve.py
| @@ -293,13 +293,18 @@ class QuestionHandler(BaseHandler): | @@ -293,13 +293,18 @@ class QuestionHandler(BaseHandler): | ||
| 293 | 'alert': 'question-information.html', | 293 | 'alert': 'question-information.html', |
| 294 | } | 294 | } |
| 295 | 295 | ||
| 296 | - # --- get question to render | 296 | + # ------------------------------------------------------------------------ |
| 297 | + # GET | ||
| 298 | + # gets question to render. If there are no more questions in the topic | ||
| 299 | + # shows an animated trophy | ||
| 300 | + # ------------------------------------------------------------------------ | ||
| 297 | @tornado.web.authenticated | 301 | @tornado.web.authenticated |
| 298 | async def get(self): | 302 | async def get(self): |
| 299 | logger.debug('[QuestionHandler]') | 303 | logger.debug('[QuestionHandler]') |
| 300 | user = self.current_user | 304 | user = self.current_user |
| 301 | q = await self.learn.get_question(user) | 305 | q = await self.learn.get_question(user) |
| 302 | 306 | ||
| 307 | + # show current question | ||
| 303 | if q is not None: | 308 | if q is not None: |
| 304 | qhtml = self.render_string(self.templates[q['type']], | 309 | qhtml = self.render_string(self.templates[q['type']], |
| 305 | question=q, md=md_to_html) | 310 | question=q, md=md_to_html) |
| @@ -313,6 +318,7 @@ class QuestionHandler(BaseHandler): | @@ -313,6 +318,7 @@ class QuestionHandler(BaseHandler): | ||
| 313 | } | 318 | } |
| 314 | } | 319 | } |
| 315 | 320 | ||
| 321 | + # show animated trophy | ||
| 316 | else: | 322 | else: |
| 317 | finished = self.render_string('finished_topic.html') | 323 | finished = self.render_string('finished_topic.html') |
| 318 | response = { | 324 | response = { |
| @@ -324,7 +330,11 @@ class QuestionHandler(BaseHandler): | @@ -324,7 +330,11 @@ class QuestionHandler(BaseHandler): | ||
| 324 | 330 | ||
| 325 | self.write(response) | 331 | self.write(response) |
| 326 | 332 | ||
| 327 | - # --- post answer, returns what to do next: shake, new_question, finished | 333 | + # ------------------------------------------------------------------------ |
| 334 | + # POST | ||
| 335 | + # corrects answer and returns status: right, wrong, try_again | ||
| 336 | + # does not move to next question. | ||
| 337 | + # ------------------------------------------------------------------------ | ||
| 328 | @tornado.web.authenticated | 338 | @tornado.web.authenticated |
| 329 | async def post(self) -> None: | 339 | async def post(self) -> None: |
| 330 | user = self.current_user | 340 | user = self.current_user |
| @@ -364,6 +374,7 @@ class QuestionHandler(BaseHandler): | @@ -364,6 +374,7 @@ class QuestionHandler(BaseHandler): | ||
| 364 | 374 | ||
| 365 | # --- built response to return | 375 | # --- built response to return |
| 366 | response = {'method': q['status'], 'params': {}} | 376 | response = {'method': q['status'], 'params': {}} |
| 377 | + | ||
| 367 | if q['status'] == 'right': # get next question in the topic | 378 | if q['status'] == 'right': # get next question in the topic |
| 368 | comments_html = self.render_string( | 379 | comments_html = self.render_string( |
| 369 | 'comments-right.html', comments=q['comments'], md=md_to_html) | 380 | 'comments-right.html', comments=q['comments'], md=md_to_html) |
aprendizations/student.py
| @@ -17,12 +17,21 @@ logger = logging.getLogger(__name__) | @@ -17,12 +17,21 @@ logger = logging.getLogger(__name__) | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | # ---------------------------------------------------------------------------- | 19 | # ---------------------------------------------------------------------------- |
| 20 | -# kowledge state of a student | ||
| 21 | -# Contains: | 20 | +# kowledge state of a student: |
| 21 | +# uid - string with userid, e.g. '12345' | ||
| 22 | # state - dict of unlocked topics and their levels | 22 | # state - dict of unlocked topics and their levels |
| 23 | -# deps - access to dependency graph shared between students | ||
| 24 | -# topic_sequence - list with the recommended topic sequence | ||
| 25 | -# current_topic - nameref of the current topic | 23 | +# {'topic1': {'level': 0.5, 'date': datetime}, ...} |
| 24 | +# topic_sequence - recommended topic sequence ['topic1', 'topic2', ...] | ||
| 25 | +# questions - [Question, ...] for the current topic | ||
| 26 | +# current_course - string or None | ||
| 27 | +# current_topic - string or None | ||
| 28 | +# current_question - Question or None | ||
| 29 | +# | ||
| 30 | +# | ||
| 31 | +# also has access to shared data between students: | ||
| 32 | +# courses - dictionary {course: [topic1, ...]} | ||
| 33 | +# deps - dependency graph as a networkx digraph | ||
| 34 | +# factory - dictionary {ref: QFactory} | ||
| 26 | # ---------------------------------------------------------------------------- | 35 | # ---------------------------------------------------------------------------- |
| 27 | class StudentState(object): | 36 | class StudentState(object): |
| 28 | # ======================================================================= | 37 | # ======================================================================= |
| @@ -50,6 +59,7 @@ class StudentState(object): | @@ -50,6 +59,7 @@ class StudentState(object): | ||
| 50 | self.current_course: Optional[str] = None | 59 | self.current_course: Optional[str] = None |
| 51 | self.topic_sequence: List[str] = [] | 60 | self.topic_sequence: List[str] = [] |
| 52 | self.current_topic: Optional[str] = None | 61 | self.current_topic: Optional[str] = None |
| 62 | + # self.previous_topic: Optional[str] = None | ||
| 53 | else: | 63 | else: |
| 54 | logger.debug(f'starting course {course}') | 64 | logger.debug(f'starting course {course}') |
| 55 | self.current_course = course | 65 | self.current_course = course |
| @@ -58,7 +68,7 @@ class StudentState(object): | @@ -58,7 +68,7 @@ class StudentState(object): | ||
| 58 | 68 | ||
| 59 | # ------------------------------------------------------------------------ | 69 | # ------------------------------------------------------------------------ |
| 60 | # Start a new topic. | 70 | # Start a new topic. |
| 61 | - # questions: list of generated questions to do in the topic | 71 | + # questions: list of generated questions to do in the given topic |
| 62 | # current_question: the current question to be presented | 72 | # current_question: the current question to be presented |
| 63 | # ------------------------------------------------------------------------ | 73 | # ------------------------------------------------------------------------ |
| 64 | async def start_topic(self, topic: str) -> None: | 74 | async def start_topic(self, topic: str) -> None: |
| @@ -74,9 +84,10 @@ class StudentState(object): | @@ -74,9 +84,10 @@ class StudentState(object): | ||
| 74 | logger.debug(f'is locked "{topic}"') | 84 | logger.debug(f'is locked "{topic}"') |
| 75 | return | 85 | return |
| 76 | 86 | ||
| 87 | + self.previous_topic: Optional[str] = None | ||
| 88 | + | ||
| 77 | # choose k questions | 89 | # choose k questions |
| 78 | self.current_topic = topic | 90 | self.current_topic = topic |
| 79 | - # self.current_question = None | ||
| 80 | self.correct_answers = 0 | 91 | self.correct_answers = 0 |
| 81 | self.wrong_answers = 0 | 92 | self.wrong_answers = 0 |
| 82 | t = self.deps.nodes[topic] | 93 | t = self.deps.nodes[topic] |
| @@ -96,30 +107,17 @@ class StudentState(object): | @@ -96,30 +107,17 @@ class StudentState(object): | ||
| 96 | self.next_question() | 107 | self.next_question() |
| 97 | 108 | ||
| 98 | # ------------------------------------------------------------------------ | 109 | # ------------------------------------------------------------------------ |
| 99 | - # The topic has finished and there are no more questions. | ||
| 100 | - # The topic level is updated in state and unlocks are performed. | ||
| 101 | - # The current topic is unchanged. | ||
| 102 | - # ------------------------------------------------------------------------ | ||
| 103 | - def finish_topic(self) -> None: | ||
| 104 | - logger.debug(f'finished {self.current_topic}') | ||
| 105 | - | ||
| 106 | - self.state[self.current_topic] = { | ||
| 107 | - 'date': datetime.now(), | ||
| 108 | - 'level': self.correct_answers / (self.correct_answers + | ||
| 109 | - self.wrong_answers) | ||
| 110 | - } | ||
| 111 | - self.current_topic = None | ||
| 112 | - self.current_question = None | ||
| 113 | - self.unlock_topics() | ||
| 114 | - | ||
| 115 | - # ------------------------------------------------------------------------ | ||
| 116 | # corrects current question | 110 | # corrects current question |
| 111 | + # updates keys: answer, grade, finish_time, status, tries | ||
| 117 | # ------------------------------------------------------------------------ | 112 | # ------------------------------------------------------------------------ |
| 118 | - async def check_answer(self, answer) -> Tuple[Question, str]: | 113 | + async def check_answer(self, answer) -> None: |
| 119 | q = self.current_question | 114 | q = self.current_question |
| 120 | - q['answer'] = answer | ||
| 121 | - q['finish_time'] = datetime.now() | ||
| 122 | - await q.correct_async() | 115 | + if q is None: |
| 116 | + logger.error('check_answer called but current_question is None!') | ||
| 117 | + return None | ||
| 118 | + | ||
| 119 | + q.set_answer(answer) | ||
| 120 | + await q.correct_async() # updates q['grade'] | ||
| 123 | 121 | ||
| 124 | if q['grade'] > 0.999: | 122 | if q['grade'] > 0.999: |
| 125 | self.correct_answers += 1 | 123 | self.correct_answers += 1 |
| @@ -134,13 +132,17 @@ class StudentState(object): | @@ -134,13 +132,17 @@ class StudentState(object): | ||
| 134 | q['status'] = 'wrong' | 132 | q['status'] = 'wrong' |
| 135 | 133 | ||
| 136 | logger.debug(f'ref = {q["ref"]}, status = {q["status"]}') | 134 | logger.debug(f'ref = {q["ref"]}, status = {q["status"]}') |
| 137 | - return q | ||
| 138 | 135 | ||
| 139 | # ------------------------------------------------------------------------ | 136 | # ------------------------------------------------------------------------ |
| 140 | - # get question to show, current or next | 137 | + # gets next question to show if the status is 'right' or 'wrong', |
| 138 | + # otherwise just returns the current question | ||
| 141 | # ------------------------------------------------------------------------ | 139 | # ------------------------------------------------------------------------ |
| 142 | async def get_question(self) -> Optional[Question]: | 140 | async def get_question(self) -> Optional[Question]: |
| 143 | q = self.current_question | 141 | q = self.current_question |
| 142 | + if q is None: | ||
| 143 | + logger.error('get_question called but current_question is None!') | ||
| 144 | + return None | ||
| 145 | + | ||
| 144 | logger.debug(f'{q["ref"]} status = {q["status"]}') | 146 | logger.debug(f'{q["ref"]} status = {q["status"]}') |
| 145 | 147 | ||
| 146 | if q['status'] == 'right': | 148 | if q['status'] == 'right': |
| @@ -151,10 +153,6 @@ class StudentState(object): | @@ -151,10 +153,6 @@ class StudentState(object): | ||
| 151 | new_question = await self.factory[q['ref']].gen_async() | 153 | new_question = await self.factory[q['ref']].gen_async() |
| 152 | self.questions.append(new_question) | 154 | self.questions.append(new_question) |
| 153 | self.next_question() | 155 | self.next_question() |
| 154 | - # elif q['status'] == 'new': | ||
| 155 | - # pass | ||
| 156 | - # elif q['status'] == 'try_again': | ||
| 157 | - # pass | ||
| 158 | 156 | ||
| 159 | return self.current_question | 157 | return self.current_question |
| 160 | 158 | ||
| @@ -172,7 +170,25 @@ class StudentState(object): | @@ -172,7 +170,25 @@ class StudentState(object): | ||
| 172 | q['start_time'] = datetime.now() | 170 | q['start_time'] = datetime.now() |
| 173 | q['tries'] = q.get('max_tries', t['max_tries']) | 171 | q['tries'] = q.get('max_tries', t['max_tries']) |
| 174 | q['status'] = 'new' | 172 | q['status'] = 'new' |
| 175 | - self.current_question = q | 173 | + self.current_question: Optional[Question] = q |
| 174 | + | ||
| 175 | + # ------------------------------------------------------------------------ | ||
| 176 | + # The topic has finished and there are no more questions. | ||
| 177 | + # The topic level is updated in state and unlocks are performed. | ||
| 178 | + # The current topic is unchanged. | ||
| 179 | + # ------------------------------------------------------------------------ | ||
| 180 | + def finish_topic(self) -> None: | ||
| 181 | + logger.debug(f'finished "{self.current_topic}"') | ||
| 182 | + | ||
| 183 | + self.state[self.current_topic] = { | ||
| 184 | + 'date': datetime.now(), | ||
| 185 | + 'level': self.correct_answers / (self.correct_answers + | ||
| 186 | + self.wrong_answers) | ||
| 187 | + } | ||
| 188 | + self.previous_topic = self.current_topic | ||
| 189 | + self.current_topic = None | ||
| 190 | + self.current_question = None | ||
| 191 | + self.unlock_topics() | ||
| 176 | 192 | ||
| 177 | # ------------------------------------------------------------------------ | 193 | # ------------------------------------------------------------------------ |
| 178 | # Update proficiency level of the topics using a forgetting factor | 194 | # Update proficiency level of the topics using a forgetting factor |
| @@ -211,7 +227,7 @@ class StudentState(object): | @@ -211,7 +227,7 @@ class StudentState(object): | ||
| 211 | # ======================================================================== | 227 | # ======================================================================== |
| 212 | 228 | ||
| 213 | def topic_has_finished(self) -> bool: | 229 | def topic_has_finished(self) -> bool: |
| 214 | - return self.current_topic is None | 230 | + return self.current_topic is None and self.previous_topic is not None |
| 215 | 231 | ||
| 216 | # ------------------------------------------------------------------------ | 232 | # ------------------------------------------------------------------------ |
| 217 | # compute recommended sequence of topics ['a', 'b', ...] | 233 | # compute recommended sequence of topics ['a', 'b', ...] |
| @@ -238,8 +254,12 @@ class StudentState(object): | @@ -238,8 +254,12 @@ class StudentState(object): | ||
| 238 | return self.current_topic | 254 | return self.current_topic |
| 239 | 255 | ||
| 240 | # ------------------------------------------------------------------------ | 256 | # ------------------------------------------------------------------------ |
| 241 | - def get_current_course_title(self) -> Optional[str]: | ||
| 242 | - return self.courses[self.current_course]['title'] | 257 | + def get_previous_topic(self) -> Optional[str]: |
| 258 | + return self.previous_topic | ||
| 259 | + | ||
| 260 | + # ------------------------------------------------------------------------ | ||
| 261 | + def get_current_course_title(self) -> str: | ||
| 262 | + return str(self.courses[self.current_course]['title']) | ||
| 243 | 263 | ||
| 244 | # ------------------------------------------------------------------------ | 264 | # ------------------------------------------------------------------------ |
| 245 | def get_current_course_id(self) -> Optional[str]: | 265 | def get_current_course_id(self) -> Optional[str]: |
| @@ -269,7 +289,7 @@ class StudentState(object): | @@ -269,7 +289,7 @@ class StudentState(object): | ||
| 269 | 289 | ||
| 270 | # ------------------------------------------------------------------------ | 290 | # ------------------------------------------------------------------------ |
| 271 | def get_topic_level(self, topic: str) -> float: | 291 | def get_topic_level(self, topic: str) -> float: |
| 272 | - return self.state[topic]['level'] | 292 | + return float(self.state[topic]['level']) |
| 273 | 293 | ||
| 274 | # ------------------------------------------------------------------------ | 294 | # ------------------------------------------------------------------------ |
| 275 | def get_topic_date(self, topic: str): | 295 | def get_topic_date(self, topic: str): |
aprendizations/tools.py
| @@ -111,14 +111,13 @@ class HighlightRenderer(mistune.Renderer): | @@ -111,14 +111,13 @@ class HighlightRenderer(mistune.Renderer): | ||
| 111 | 111 | ||
| 112 | def table(self, header, body): | 112 | def table(self, header, body): |
| 113 | return ('<table class="table table-sm"><thead class="thead-light">' | 113 | return ('<table class="table table-sm"><thead class="thead-light">' |
| 114 | - f'{header}</thead><tbody>{body}</tbody></table>') | 114 | + f'{header}</thead><tbody>{body}</tbody></table>') |
| 115 | 115 | ||
| 116 | def image(self, src, title, alt): | 116 | def image(self, src, title, alt): |
| 117 | alt = mistune.escape(alt, quote=True) | 117 | alt = mistune.escape(alt, quote=True) |
| 118 | title = mistune.escape(title or '', quote=True) | 118 | title = mistune.escape(title or '', quote=True) |
| 119 | return (f'<img src="/file/{src}" alt="{alt}" title="{title}"' | 119 | return (f'<img src="/file/{src}" alt="{alt}" title="{title}"' |
| 120 | - 'class="img-fluid">') | ||
| 121 | - # class="img-fluid mx-auto d-block" | 120 | + f'class="img-fluid">') # class="img-fluid mx-auto d-block" |
| 122 | 121 | ||
| 123 | # Pass math through unaltered - mathjax does the rendering in the browser | 122 | # Pass math through unaltered - mathjax does the rendering in the browser |
| 124 | def block_math(self, text): | 123 | def block_math(self, text): |
| @@ -150,25 +149,22 @@ def load_yaml(filename: str, default: Any = None) -> Any: | @@ -150,25 +149,22 @@ def load_yaml(filename: str, default: Any = None) -> Any: | ||
| 150 | filename = path.expanduser(filename) | 149 | filename = path.expanduser(filename) |
| 151 | try: | 150 | try: |
| 152 | f = open(filename, 'r', encoding='utf-8') | 151 | f = open(filename, 'r', encoding='utf-8') |
| 153 | - except FileNotFoundError: | ||
| 154 | - logger.error(f'Cannot open "{filename}": not found') | ||
| 155 | - except PermissionError: | ||
| 156 | - logger.error(f'Cannot open "{filename}": no permission') | ||
| 157 | - except OSError: | ||
| 158 | - logger.error(f'Cannot open file "{filename}"') | ||
| 159 | - else: | ||
| 160 | - with f: | ||
| 161 | - try: | ||
| 162 | - default = yaml.safe_load(f) | ||
| 163 | - except yaml.YAMLError as e: | ||
| 164 | - if hasattr(e, 'problem_mark'): | ||
| 165 | - mark = e.problem_mark | ||
| 166 | - logger.error(f'File "{filename}" near line {mark.line+1}, ' | ||
| 167 | - f'column {mark.column+1}') | ||
| 168 | - else: | ||
| 169 | - logger.error(f'File "{filename}"') | ||
| 170 | - finally: | ||
| 171 | - return default | 152 | + except Exception as e: |
| 153 | + logger.error(e) | ||
| 154 | + if default is not None: | ||
| 155 | + return default | ||
| 156 | + else: | ||
| 157 | + raise | ||
| 158 | + | ||
| 159 | + with f: | ||
| 160 | + try: | ||
| 161 | + return yaml.safe_load(f) | ||
| 162 | + except yaml.YAMLError as e: | ||
| 163 | + logger.error(str(e).replace('\n', ' ')) | ||
| 164 | + if default is not None: | ||
| 165 | + return default | ||
| 166 | + else: | ||
| 167 | + raise | ||
| 172 | 168 | ||
| 173 | 169 | ||
| 174 | # --------------------------------------------------------------------------- | 170 | # --------------------------------------------------------------------------- |