From 7cf84cc655a0814f8d9a5f37c2495516e4c9d6d9 Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Wed, 11 Dec 2019 19:15:00 +0000 Subject: [PATCH] - fix critical error where progress was not being saved in the database - add some type annotations --- BUGS.md | 1 + aprendizations/learnapp.py | 44 ++++++++++++++++++++++++++------------------ aprendizations/questions.py | 7 ++++++- aprendizations/serve.py | 15 +++++++++++++-- aprendizations/student.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------- 5 files changed, 105 insertions(+), 60 deletions(-) diff --git a/BUGS.md b/BUGS.md index 8b4c93e..5a756e4 100644 --- a/BUGS.md +++ b/BUGS.md @@ -31,6 +31,7 @@ # FIXED +- CRITICAL nao esta a guardar o progresso na base de dados. - mesma ref no mesmo ficheiro não é detectado. - enter nas respostas mostra json - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index bbb3083..51c4eda 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -7,7 +7,7 @@ from datetime import datetime import logging from random import random from os import path -from typing import Any, Dict, Iterable, List, Optional, Tuple +from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict # third party libraries import bcrypt @@ -88,6 +88,7 @@ class LearnApp(object): self.courses = config['courses'] logger.info(f'Courses: {", ".join(self.courses.keys())}') for c, d in self.courses.items(): + d.setdefault('title', '') # course title undefined for goal in d['goals']: if goal not in self.deps.nodes(): raise LearnException(f'Goal "{goal}" from course "{c}" ' @@ -228,9 +229,8 @@ class LearnApp(object): # ------------------------------------------------------------------------ async def check_answer(self, uid: str, answer) -> Question: student = self.online[uid]['state'] - topic = student.get_current_topic() - - q = await student.check_answer(answer) + await student.check_answer(answer) + q = student.get_current_question() logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') @@ -242,14 +242,24 @@ class LearnApp(object): starttime=str(q['start_time']), finishtime=str(q['finish_time']), student_id=uid, - topic_id=topic)) - logger.debug(f'db insert answer of {q["ref"]}') + topic_id=student.get_current_topic())) + + return q - # save topic if finished + # ------------------------------------------------------------------------ + # get the question to show (current or new one) + # if no more questions, save/update level in database + # ------------------------------------------------------------------------ + async def get_question(self, uid: str) -> Optional[Question]: + student = self.online[uid]['state'] + q: Optional[Question] = await student.get_question() + + # save topic to database if finished if student.topic_has_finished(): - logger.info(f'User "{uid}" finished "{topic}"') + topic: str = student.get_previous_topic() level: float = student.get_topic_level(topic) date: str = str(student.get_topic_date(topic)) + logger.info(f'User "{uid}" finished "{topic}" (level={level:.2})') with self.db_session() as s: a = s.query(StudentTopic) \ @@ -275,12 +285,6 @@ class LearnApp(object): return q # ------------------------------------------------------------------------ - # get the question to show (current or new one) - # ------------------------------------------------------------------------ - async def get_question(self, uid: str) -> Optional[Question]: - return await self.online[uid]['state'].get_question() - - # ------------------------------------------------------------------------ # Start course # ------------------------------------------------------------------------ def start_course(self, uid: str, course: str) -> None: @@ -414,11 +418,15 @@ class LearnApp(object): except Exception: raise LearnException(f'Failed to load "{fullpath}"') + if not isinstance(questions, list): + msg = f'File "{fullpath}" must be a list of questions' + raise LearnException(msg) + # update refs to include topic as prefix. # refs are required to be unique only within the file. # undefined are set to topic:n, where n is the question number # within the file - localrefs = set() # refs in current file + localrefs: Set[str] = set() # refs in current file for i, q in enumerate(questions): qref = q.get('ref', str(i)) # ref or number if qref in localrefs: @@ -448,7 +456,7 @@ class LearnApp(object): # ------------------------------------------------------------------------ def get_login_counter(self, uid: str) -> int: - return self.online[uid]['counter'] + return int(self.online[uid]['counter']) # ------------------------------------------------------------------------ def get_student_name(self, uid: str) -> str: @@ -483,7 +491,7 @@ class LearnApp(object): return self.online[uid]['state'].get_current_course_title() # ------------------------------------------------------------------------ - def get_student_course_id(self, uid: str) -> str: + def get_student_course_id(self, uid: str) -> Optional[str]: return self.online[uid]['state'].get_current_course_id() # ------------------------------------------------------------------------ @@ -529,7 +537,7 @@ class LearnApp(object): # compute topic progress now = datetime.now() goals = self.courses[course_id]['goals'] - prog = defaultdict(int) + prog: DefaultDict[str, float] = defaultdict(int) for uid, topic, level, date in student_topics: if topic in goals: diff --git a/aprendizations/questions.py b/aprendizations/questions.py index 9a266ef..0ae4475 100644 --- a/aprendizations/questions.py +++ b/aprendizations/questions.py @@ -1,6 +1,7 @@ # python standard library import asyncio +from datetime import datetime import random import re from os import path @@ -44,6 +45,10 @@ class Question(dict): 'files': {}, })) + def set_answer(self, ans) -> None: + self['answer'] = ans + self['finish_time'] = datetime.now() + def correct(self) -> None: self['comments'] = '' self['grade'] = 0.0 @@ -172,7 +177,7 @@ class QuestionRadio(Question): # x_aver can be exactly 1.0 if all options are right if self['discount'] and x_aver != 1.0: x = (x - x_aver) / (1.0 - x_aver) - self['grade'] = x + self['grade'] = float(x) # ============================================================================ diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 6767c3d..5acc6a7 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -293,13 +293,18 @@ class QuestionHandler(BaseHandler): 'alert': 'question-information.html', } - # --- get question to render + # ------------------------------------------------------------------------ + # GET + # gets question to render. If there are no more questions in the topic + # shows an animated trophy + # ------------------------------------------------------------------------ @tornado.web.authenticated async def get(self): logger.debug('[QuestionHandler]') user = self.current_user q = await self.learn.get_question(user) + # show current question if q is not None: qhtml = self.render_string(self.templates[q['type']], question=q, md=md_to_html) @@ -313,6 +318,7 @@ class QuestionHandler(BaseHandler): } } + # show animated trophy else: finished = self.render_string('finished_topic.html') response = { @@ -324,7 +330,11 @@ class QuestionHandler(BaseHandler): self.write(response) - # --- post answer, returns what to do next: shake, new_question, finished + # ------------------------------------------------------------------------ + # POST + # corrects answer and returns status: right, wrong, try_again + # does not move to next question. + # ------------------------------------------------------------------------ @tornado.web.authenticated async def post(self) -> None: user = self.current_user @@ -364,6 +374,7 @@ class QuestionHandler(BaseHandler): # --- built response to return response = {'method': q['status'], 'params': {}} + if q['status'] == 'right': # get next question in the topic comments_html = self.render_string( 'comments-right.html', comments=q['comments'], md=md_to_html) diff --git a/aprendizations/student.py b/aprendizations/student.py index 4def72d..6f747bb 100644 --- a/aprendizations/student.py +++ b/aprendizations/student.py @@ -17,12 +17,21 @@ logger = logging.getLogger(__name__) # ---------------------------------------------------------------------------- -# kowledge state of a student -# Contains: +# kowledge state of a student: +# uid - string with userid, e.g. '12345' # state - dict of unlocked topics and their levels -# deps - access to dependency graph shared between students -# topic_sequence - list with the recommended topic sequence -# current_topic - nameref of the current topic +# {'topic1': {'level': 0.5, 'date': datetime}, ...} +# topic_sequence - recommended topic sequence ['topic1', 'topic2', ...] +# questions - [Question, ...] for the current topic +# current_course - string or None +# current_topic - string or None +# current_question - Question or None +# +# +# also has access to shared data between students: +# courses - dictionary {course: [topic1, ...]} +# deps - dependency graph as a networkx digraph +# factory - dictionary {ref: QFactory} # ---------------------------------------------------------------------------- class StudentState(object): # ======================================================================= @@ -50,6 +59,7 @@ class StudentState(object): self.current_course: Optional[str] = None self.topic_sequence: List[str] = [] self.current_topic: Optional[str] = None + # self.previous_topic: Optional[str] = None else: logger.debug(f'starting course {course}') self.current_course = course @@ -58,7 +68,7 @@ class StudentState(object): # ------------------------------------------------------------------------ # Start a new topic. - # questions: list of generated questions to do in the topic + # questions: list of generated questions to do in the given topic # current_question: the current question to be presented # ------------------------------------------------------------------------ async def start_topic(self, topic: str) -> None: @@ -74,9 +84,10 @@ class StudentState(object): logger.debug(f'is locked "{topic}"') return + self.previous_topic: Optional[str] = None + # choose k questions self.current_topic = topic - # self.current_question = None self.correct_answers = 0 self.wrong_answers = 0 t = self.deps.nodes[topic] @@ -96,30 +107,17 @@ class StudentState(object): self.next_question() # ------------------------------------------------------------------------ - # The topic has finished and there are no more questions. - # The topic level is updated in state and unlocks are performed. - # The current topic is unchanged. - # ------------------------------------------------------------------------ - def finish_topic(self) -> None: - logger.debug(f'finished {self.current_topic}') - - self.state[self.current_topic] = { - 'date': datetime.now(), - 'level': self.correct_answers / (self.correct_answers + - self.wrong_answers) - } - self.current_topic = None - self.current_question = None - self.unlock_topics() - - # ------------------------------------------------------------------------ # corrects current question + # updates keys: answer, grade, finish_time, status, tries # ------------------------------------------------------------------------ - async def check_answer(self, answer) -> Tuple[Question, str]: + async def check_answer(self, answer) -> None: q = self.current_question - q['answer'] = answer - q['finish_time'] = datetime.now() - await q.correct_async() + if q is None: + logger.error('check_answer called but current_question is None!') + return None + + q.set_answer(answer) + await q.correct_async() # updates q['grade'] if q['grade'] > 0.999: self.correct_answers += 1 @@ -134,13 +132,17 @@ class StudentState(object): q['status'] = 'wrong' logger.debug(f'ref = {q["ref"]}, status = {q["status"]}') - return q # ------------------------------------------------------------------------ - # get question to show, current or next + # gets next question to show if the status is 'right' or 'wrong', + # otherwise just returns the current question # ------------------------------------------------------------------------ async def get_question(self) -> Optional[Question]: q = self.current_question + if q is None: + logger.error('get_question called but current_question is None!') + return None + logger.debug(f'{q["ref"]} status = {q["status"]}') if q['status'] == 'right': @@ -151,10 +153,6 @@ class StudentState(object): new_question = await self.factory[q['ref']].gen_async() self.questions.append(new_question) self.next_question() - # elif q['status'] == 'new': - # pass - # elif q['status'] == 'try_again': - # pass return self.current_question @@ -172,7 +170,25 @@ class StudentState(object): q['start_time'] = datetime.now() q['tries'] = q.get('max_tries', t['max_tries']) q['status'] = 'new' - self.current_question = q + self.current_question: Optional[Question] = q + + # ------------------------------------------------------------------------ + # The topic has finished and there are no more questions. + # The topic level is updated in state and unlocks are performed. + # The current topic is unchanged. + # ------------------------------------------------------------------------ + def finish_topic(self) -> None: + logger.debug(f'finished "{self.current_topic}"') + + self.state[self.current_topic] = { + 'date': datetime.now(), + 'level': self.correct_answers / (self.correct_answers + + self.wrong_answers) + } + self.previous_topic = self.current_topic + self.current_topic = None + self.current_question = None + self.unlock_topics() # ------------------------------------------------------------------------ # Update proficiency level of the topics using a forgetting factor @@ -211,7 +227,7 @@ class StudentState(object): # ======================================================================== def topic_has_finished(self) -> bool: - return self.current_topic is None + return self.current_topic is None and self.previous_topic is not None # ------------------------------------------------------------------------ # compute recommended sequence of topics ['a', 'b', ...] @@ -238,8 +254,12 @@ class StudentState(object): return self.current_topic # ------------------------------------------------------------------------ - def get_current_course_title(self) -> Optional[str]: - return self.courses[self.current_course]['title'] + def get_previous_topic(self) -> Optional[str]: + return self.previous_topic + + # ------------------------------------------------------------------------ + def get_current_course_title(self) -> str: + return str(self.courses[self.current_course]['title']) # ------------------------------------------------------------------------ def get_current_course_id(self) -> Optional[str]: @@ -269,7 +289,7 @@ class StudentState(object): # ------------------------------------------------------------------------ def get_topic_level(self, topic: str) -> float: - return self.state[topic]['level'] + return float(self.state[topic]['level']) # ------------------------------------------------------------------------ def get_topic_date(self, topic: str): -- libgit2 0.21.2