Commit a6b50da08def113bd299f00b1f8933ab59062153

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

fixes error where progress is not being saved in the database

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) -&gt; Any: @@ -150,25 +149,22 @@ def load_yaml(filename: str, default: Any = None) -&gt; 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 # ---------------------------------------------------------------------------