Commit 7cf84cc655a0814f8d9a5f37c2495516e4c9d6d9
1 parent
9ff40133
Exists in
master
and in
1 other branch
- fix critical error where progress was not being saved in the database
- add some type annotations
Showing
5 changed files
with
105 additions
and
60 deletions
Show diff stats
BUGS.md
... | ... | @@ -31,6 +31,7 @@ |
31 | 31 | |
32 | 32 | # FIXED |
33 | 33 | |
34 | +- CRITICAL nao esta a guardar o progresso na base de dados. | |
34 | 35 | - mesma ref no mesmo ficheiro não é detectado. |
35 | 36 | - enter nas respostas mostra json |
36 | 37 | - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | ... | ... |
aprendizations/learnapp.py
... | ... | @@ -7,7 +7,7 @@ from datetime import datetime |
7 | 7 | import logging |
8 | 8 | from random import random |
9 | 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 | 12 | # third party libraries |
13 | 13 | import bcrypt |
... | ... | @@ -88,6 +88,7 @@ class LearnApp(object): |
88 | 88 | self.courses = config['courses'] |
89 | 89 | logger.info(f'Courses: {", ".join(self.courses.keys())}') |
90 | 90 | for c, d in self.courses.items(): |
91 | + d.setdefault('title', '') # course title undefined | |
91 | 92 | for goal in d['goals']: |
92 | 93 | if goal not in self.deps.nodes(): |
93 | 94 | raise LearnException(f'Goal "{goal}" from course "{c}" ' |
... | ... | @@ -228,9 +229,8 @@ class LearnApp(object): |
228 | 229 | # ------------------------------------------------------------------------ |
229 | 230 | async def check_answer(self, uid: str, answer) -> Question: |
230 | 231 | student = self.online[uid]['state'] |
231 | - topic = student.get_current_topic() | |
232 | - | |
233 | - q = await student.check_answer(answer) | |
232 | + await student.check_answer(answer) | |
233 | + q = student.get_current_question() | |
234 | 234 | |
235 | 235 | logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') |
236 | 236 | |
... | ... | @@ -242,14 +242,24 @@ class LearnApp(object): |
242 | 242 | starttime=str(q['start_time']), |
243 | 243 | finishtime=str(q['finish_time']), |
244 | 244 | student_id=uid, |
245 | - topic_id=topic)) | |
246 | - logger.debug(f'db insert answer of {q["ref"]}') | |
245 | + topic_id=student.get_current_topic())) | |
246 | + | |
247 | + return q | |
247 | 248 | |
248 | - # save topic if finished | |
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() | |
256 | + | |
257 | + # save topic to database if finished | |
249 | 258 | if student.topic_has_finished(): |
250 | - logger.info(f'User "{uid}" finished "{topic}"') | |
259 | + topic: str = student.get_previous_topic() | |
251 | 260 | level: float = student.get_topic_level(topic) |
252 | 261 | date: str = str(student.get_topic_date(topic)) |
262 | + logger.info(f'User "{uid}" finished "{topic}" (level={level:.2})') | |
253 | 263 | |
254 | 264 | with self.db_session() as s: |
255 | 265 | a = s.query(StudentTopic) \ |
... | ... | @@ -275,12 +285,6 @@ class LearnApp(object): |
275 | 285 | return q |
276 | 286 | |
277 | 287 | # ------------------------------------------------------------------------ |
278 | - # get the question to show (current or new one) | |
279 | - # ------------------------------------------------------------------------ | |
280 | - async def get_question(self, uid: str) -> Optional[Question]: | |
281 | - return await self.online[uid]['state'].get_question() | |
282 | - | |
283 | - # ------------------------------------------------------------------------ | |
284 | 288 | # Start course |
285 | 289 | # ------------------------------------------------------------------------ |
286 | 290 | def start_course(self, uid: str, course: str) -> None: |
... | ... | @@ -414,11 +418,15 @@ class LearnApp(object): |
414 | 418 | except Exception: |
415 | 419 | raise LearnException(f'Failed to load "{fullpath}"') |
416 | 420 | |
421 | + if not isinstance(questions, list): | |
422 | + msg = f'File "{fullpath}" must be a list of questions' | |
423 | + raise LearnException(msg) | |
424 | + | |
417 | 425 | # update refs to include topic as prefix. |
418 | 426 | # refs are required to be unique only within the file. |
419 | 427 | # undefined are set to topic:n, where n is the question number |
420 | 428 | # within the file |
421 | - localrefs = set() # refs in current file | |
429 | + localrefs: Set[str] = set() # refs in current file | |
422 | 430 | for i, q in enumerate(questions): |
423 | 431 | qref = q.get('ref', str(i)) # ref or number |
424 | 432 | if qref in localrefs: |
... | ... | @@ -448,7 +456,7 @@ class LearnApp(object): |
448 | 456 | |
449 | 457 | # ------------------------------------------------------------------------ |
450 | 458 | def get_login_counter(self, uid: str) -> int: |
451 | - return self.online[uid]['counter'] | |
459 | + return int(self.online[uid]['counter']) | |
452 | 460 | |
453 | 461 | # ------------------------------------------------------------------------ |
454 | 462 | def get_student_name(self, uid: str) -> str: |
... | ... | @@ -483,7 +491,7 @@ class LearnApp(object): |
483 | 491 | return self.online[uid]['state'].get_current_course_title() |
484 | 492 | |
485 | 493 | # ------------------------------------------------------------------------ |
486 | - def get_student_course_id(self, uid: str) -> str: | |
494 | + def get_student_course_id(self, uid: str) -> Optional[str]: | |
487 | 495 | return self.online[uid]['state'].get_current_course_id() |
488 | 496 | |
489 | 497 | # ------------------------------------------------------------------------ |
... | ... | @@ -529,7 +537,7 @@ class LearnApp(object): |
529 | 537 | # compute topic progress |
530 | 538 | now = datetime.now() |
531 | 539 | goals = self.courses[course_id]['goals'] |
532 | - prog = defaultdict(int) | |
540 | + prog: DefaultDict[str, float] = defaultdict(int) | |
533 | 541 | |
534 | 542 | for uid, topic, level, date in student_topics: |
535 | 543 | if topic in goals: | ... | ... |
aprendizations/questions.py
1 | 1 | |
2 | 2 | # python standard library |
3 | 3 | import asyncio |
4 | +from datetime import datetime | |
4 | 5 | import random |
5 | 6 | import re |
6 | 7 | from os import path |
... | ... | @@ -44,6 +45,10 @@ class Question(dict): |
44 | 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 | 52 | def correct(self) -> None: |
48 | 53 | self['comments'] = '' |
49 | 54 | self['grade'] = 0.0 |
... | ... | @@ -172,7 +177,7 @@ class QuestionRadio(Question): |
172 | 177 | # x_aver can be exactly 1.0 if all options are right |
173 | 178 | if self['discount'] and x_aver != 1.0: |
174 | 179 | x = (x - x_aver) / (1.0 - x_aver) |
175 | - self['grade'] = x | |
180 | + self['grade'] = float(x) | |
176 | 181 | |
177 | 182 | |
178 | 183 | # ============================================================================ | ... | ... |
aprendizations/serve.py
... | ... | @@ -293,13 +293,18 @@ class QuestionHandler(BaseHandler): |
293 | 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 | 301 | @tornado.web.authenticated |
298 | 302 | async def get(self): |
299 | 303 | logger.debug('[QuestionHandler]') |
300 | 304 | user = self.current_user |
301 | 305 | q = await self.learn.get_question(user) |
302 | 306 | |
307 | + # show current question | |
303 | 308 | if q is not None: |
304 | 309 | qhtml = self.render_string(self.templates[q['type']], |
305 | 310 | question=q, md=md_to_html) |
... | ... | @@ -313,6 +318,7 @@ class QuestionHandler(BaseHandler): |
313 | 318 | } |
314 | 319 | } |
315 | 320 | |
321 | + # show animated trophy | |
316 | 322 | else: |
317 | 323 | finished = self.render_string('finished_topic.html') |
318 | 324 | response = { |
... | ... | @@ -324,7 +330,11 @@ class QuestionHandler(BaseHandler): |
324 | 330 | |
325 | 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 | 338 | @tornado.web.authenticated |
329 | 339 | async def post(self) -> None: |
330 | 340 | user = self.current_user |
... | ... | @@ -364,6 +374,7 @@ class QuestionHandler(BaseHandler): |
364 | 374 | |
365 | 375 | # --- built response to return |
366 | 376 | response = {'method': q['status'], 'params': {}} |
377 | + | |
367 | 378 | if q['status'] == 'right': # get next question in the topic |
368 | 379 | comments_html = self.render_string( |
369 | 380 | 'comments-right.html', comments=q['comments'], md=md_to_html) | ... | ... |
aprendizations/student.py
... | ... | @@ -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 | 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 | 36 | class StudentState(object): |
28 | 37 | # ======================================================================= |
... | ... | @@ -50,6 +59,7 @@ class StudentState(object): |
50 | 59 | self.current_course: Optional[str] = None |
51 | 60 | self.topic_sequence: List[str] = [] |
52 | 61 | self.current_topic: Optional[str] = None |
62 | + # self.previous_topic: Optional[str] = None | |
53 | 63 | else: |
54 | 64 | logger.debug(f'starting course {course}') |
55 | 65 | self.current_course = course |
... | ... | @@ -58,7 +68,7 @@ class StudentState(object): |
58 | 68 | |
59 | 69 | # ------------------------------------------------------------------------ |
60 | 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 | 72 | # current_question: the current question to be presented |
63 | 73 | # ------------------------------------------------------------------------ |
64 | 74 | async def start_topic(self, topic: str) -> None: |
... | ... | @@ -74,9 +84,10 @@ class StudentState(object): |
74 | 84 | logger.debug(f'is locked "{topic}"') |
75 | 85 | return |
76 | 86 | |
87 | + self.previous_topic: Optional[str] = None | |
88 | + | |
77 | 89 | # choose k questions |
78 | 90 | self.current_topic = topic |
79 | - # self.current_question = None | |
80 | 91 | self.correct_answers = 0 |
81 | 92 | self.wrong_answers = 0 |
82 | 93 | t = self.deps.nodes[topic] |
... | ... | @@ -96,30 +107,17 @@ class StudentState(object): |
96 | 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 | 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 | 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 | 122 | if q['grade'] > 0.999: |
125 | 123 | self.correct_answers += 1 |
... | ... | @@ -134,13 +132,17 @@ class StudentState(object): |
134 | 132 | q['status'] = 'wrong' |
135 | 133 | |
136 | 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 | 140 | async def get_question(self) -> Optional[Question]: |
143 | 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 | 146 | logger.debug(f'{q["ref"]} status = {q["status"]}') |
145 | 147 | |
146 | 148 | if q['status'] == 'right': |
... | ... | @@ -151,10 +153,6 @@ class StudentState(object): |
151 | 153 | new_question = await self.factory[q['ref']].gen_async() |
152 | 154 | self.questions.append(new_question) |
153 | 155 | self.next_question() |
154 | - # elif q['status'] == 'new': | |
155 | - # pass | |
156 | - # elif q['status'] == 'try_again': | |
157 | - # pass | |
158 | 156 | |
159 | 157 | return self.current_question |
160 | 158 | |
... | ... | @@ -172,7 +170,25 @@ class StudentState(object): |
172 | 170 | q['start_time'] = datetime.now() |
173 | 171 | q['tries'] = q.get('max_tries', t['max_tries']) |
174 | 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 | 194 | # Update proficiency level of the topics using a forgetting factor |
... | ... | @@ -211,7 +227,7 @@ class StudentState(object): |
211 | 227 | # ======================================================================== |
212 | 228 | |
213 | 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 | 233 | # compute recommended sequence of topics ['a', 'b', ...] |
... | ... | @@ -238,8 +254,12 @@ class StudentState(object): |
238 | 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 | 265 | def get_current_course_id(self) -> Optional[str]: |
... | ... | @@ -269,7 +289,7 @@ class StudentState(object): |
269 | 289 | |
270 | 290 | # ------------------------------------------------------------------------ |
271 | 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 | 295 | def get_topic_date(self, topic: str): | ... | ... |