diff --git a/BUGS.md b/BUGS.md index 013b61f..0ad4c29 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,7 +1,11 @@ # BUGS -- se aluno abre dois tabs no browser, conseque navegar em simultaneo para perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um campo hidden que tenha um céodigo único que indique qual a pergunta. do lado do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz redirect para /. +- sqlalchemy.pool.impl.NullPool: Exception during reset or similar +sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. + +- porque é que md() é usado nos templates e não directamente no codigo python? +- templates question-*.html tem input hidden question_ref que não é usado. remover? - guardar o estado a meio de um nível. - safari as vezes envia dois gets no inicio do topico. nesses casos, a segunda pergunta não é actualizada no browser... o topico tem de ser gerado qd se escolhe o topico em main_topics. O get nao deve alterar o estado. - click numa opcao checkbox fora da checkbox+label não está a funcionar. @@ -32,6 +36,7 @@ # FIXED +- se aluno abre dois tabs no browser, conseque navegar em simultaneo para perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um campo hidden que tenha um céodigo único que indique qual a pergunta. do lado do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz redirect para /. - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes - max tries nas perguntas. diff --git a/aprendizations/knowledge.py b/aprendizations/knowledge.py index fd781ad..ec7c73f 100644 --- a/aprendizations/knowledge.py +++ b/aprendizations/knowledge.py @@ -92,13 +92,24 @@ class StudentKnowledge(object): logger.debug(f'Questions: {", ".join(questions)}') # generate instances of questions + + # synchronous: # self.questions = [self.factory[ref].generate() for ref in questions] + loop = asyncio.get_running_loop() - generators = [loop.run_in_executor(None, self.factory[qref].generate) - for qref in questions] - self.questions = await asyncio.gather(*generators) - logger.debug(f'Total: {len(self.questions)} questions') + # async: + # generators = [loop.run_in_executor(None, self.factory[qref].generate) + # for qref in questions] + # self.questions = await asyncio.gather(*generators) + + # another async: + self.questions = [] + for qref in questions: + q = await loop.run_in_executor(None, self.factory[qref].generate) + self.questions.append(q) + + logger.debug(f'Generated {len(self.questions)} questions') # get first question self.next_question() @@ -208,7 +219,6 @@ class StudentKnowledge(object): # Topics unlocked but not yet done have level 0.0. # ------------------------------------------------------------------------ def get_knowledge_state(self): - # print(self.topic_sequence) return [{ 'ref': ref, 'type': self.deps.nodes[ref]['type'], diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index b1ecfa2..1d3fb98 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -81,8 +81,7 @@ class LearnApp(object): except Exception: logger.error(f'Failed to generate "{qref}".') errors += 1 - raise LearnException('Sanity checks') - continue + continue # to next question if 'tests_right' in q: for t in q['tests_right']: @@ -91,7 +90,7 @@ class LearnApp(object): if q['grade'] < 1.0: logger.error(f'Failed right answer in "{qref}".') errors += 1 - continue # to next right test + continue # to next test if 'tests_wrong' in q: for t in q['tests_wrong']: @@ -100,7 +99,7 @@ class LearnApp(object): if q['grade'] >= 1.0: logger.error(f'Failed wrong answer in "{qref}".') errors += 1 - continue # to next wrong test + continue # to next test if errors > 0: logger.info(f'{errors:>6} errors found.') @@ -400,6 +399,10 @@ class LearnApp(object): return self.online[uid]['state'].get_current_question() # dict # ------------------------------------------------------------------------ + def get_current_question_id(self, uid: str) -> str: + return self.online[uid]['state'].get_current_question()['qid'] + + # ------------------------------------------------------------------------ def get_student_question_type(self, uid: str) -> str: return self.online[uid]['state'].get_current_question()['type'] diff --git a/aprendizations/questions.py b/aprendizations/questions.py index 6eb765b..fc83b70 100644 --- a/aprendizations/questions.py +++ b/aprendizations/questions.py @@ -6,6 +6,7 @@ from os import path import logging import asyncio from typing import Any, Dict, NewType +import uuid # this project from .tools import run_script @@ -430,6 +431,7 @@ class QFactory(object): # Shallow copy so that script generated questions will not replace # the original generators q = self.question.copy() + q['qid'] = str(uuid.uuid4()) # unique for each generated question # If question is of generator type, an external program will be run # which will print a valid question in yaml format to stdout. This diff --git a/aprendizations/serve.py b/aprendizations/serve.py index 98cbd2e..fb45113 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -153,16 +153,19 @@ class ChangePasswordHandler(BaseHandler): changed_ok = await self.learn.change_password(uid, pw) if changed_ok: - notification = to_unicode(self.render_string( - 'notification.html', type='success', - msg='A password foi alterada!') + notification = self.render_string( + 'notification.html', + type='success', + msg='A password foi alterada!' ) else: - notification = to_unicode(self.render_string( - 'notification.html', type='danger', - msg='A password não foi alterada!') + notification = self.render_string( + 'notification.html', + type='danger', + msg='A password não foi alterada!' ) - self.write({'msg': notification}) + + self.write({'msg': to_unicode(notification)}) # ---------------------------------------------------------------------------- @@ -257,24 +260,24 @@ class QuestionHandler(BaseHandler): q = self.learn.get_current_question(user) if q is not None: - question_html = to_unicode(self.render_string( - self.templates[q['type']], question=q, md=md_to_html)) + qhtml = self.render_string(self.templates[q['type']], + question=q, md=md_to_html) response = { 'method': 'new_question', 'params': { 'type': q['type'], - 'question': question_html, + 'question': to_unicode(qhtml), 'progress': self.learn.get_student_progress(user), 'tries': q['tries'], } } else: - finished = to_unicode(self.render_string('finished_topic.html')) + finished = self.render_string('finished_topic.html') response = { 'method': 'finished_topic', 'params': { - 'question': finished + 'question': to_unicode(finished) } } @@ -282,15 +285,29 @@ class QuestionHandler(BaseHandler): # --- post answer, returns what to do next: shake, new_question, finished @tornado.web.authenticated - async def post(self): + async def post(self) -> None: logging.debug('QuestionHandler.post()') user = self.current_user answer = self.get_body_arguments('answer') # list - # brain hacking ;) + # --- check if browser opened different questions simultaneously + answer_qid = self.get_body_arguments('qid')[0] + current_qid = self.learn.get_current_question_id(user) + if answer_qid != current_qid: + logging.debug(f'User {user} desynchronized questions') + self.write({ + 'method': 'invalid', + 'params': { + 'msg': ('Esta pergunta já não está activa. ' + 'Tem outra janela aberta?') + } + }) + return + + # --- brain hacking ;) await asyncio.sleep(1) - # answers are returned in a list. fix depending on question type + # --- answers are in a list. fix depending on question type qtype = self.learn.get_student_question_type(user) if qtype in ('success', 'information', 'info'): answer = None @@ -299,52 +316,49 @@ class QuestionHandler(BaseHandler): elif qtype != 'checkbox': # radio, text, textarea, ... answer = answer[0] - # check answer (nonblocking) and get corrected question + # --- check answer (nonblocking) and get corrected question and action q, action = await self.learn.check_answer(user, answer) + # --- built response to return response = {'method': action, 'params': {}} - if action == 'right': # get next question in the topic - comments_html = to_unicode(self.render_string( - 'comments-right.html', comments=q['comments'], md=md_to_html)) + comments_html = self.render_string( + 'comments-right.html', comments=q['comments'], md=md_to_html) - solution_html = to_unicode(self.render_string( - 'solution.html', solution=q['solution'], md=md_to_html)) + solution_html = self.render_string( + 'solution.html', solution=q['solution'], md=md_to_html) response['params'] = { 'type': q['type'], 'progress': self.learn.get_student_progress(user), - 'comments': comments_html, - 'solution': solution_html, + 'comments': to_unicode(comments_html), + 'solution': to_unicode(solution_html), 'tries': q['tries'], } - elif action == 'try_again': - comments_html = to_unicode(self.render_string( - 'comments.html', comments=q['comments'], md=md_to_html)) + comments_html = self.render_string( + 'comments.html', comments=q['comments'], md=md_to_html) response['params'] = { 'type': q['type'], 'progress': self.learn.get_student_progress(user), - 'comments': comments_html, + 'comments': to_unicode(comments_html), 'tries': q['tries'], } - elif action == 'wrong': # no more tries - comments_html = to_unicode(self.render_string( - 'comments.html', comments=q['comments'], md=md_to_html)) + comments_html = self.render_string( + 'comments.html', comments=q['comments'], md=md_to_html) - solution_html = to_unicode(self.render_string( - 'solution.html', solution=q['solution'], md=md_to_html)) + solution_html = self.render_string( + 'solution.html', solution=q['solution'], md=md_to_html) response['params'] = { 'type': q['type'], 'progress': self.learn.get_student_progress(user), - 'comments': comments_html, - 'solution': solution_html, + 'comments': to_unicode(comments_html), + 'solution': to_unicode(solution_html), 'tries': q['tries'], } - else: logging.error(f'Unknown action: {action}') diff --git a/aprendizations/static/js/topic.js b/aprendizations/static/js/topic.js index 08e4e2d..dcdce20 100644 --- a/aprendizations/static/js/topic.js +++ b/aprendizations/static/js/topic.js @@ -162,6 +162,10 @@ function getFeedback(response) { $("fieldset").attr("disabled", "disabled"); $("#submit").html("Continuar").off().click(getQuestion); break; + + case "invalid": + alert(params["msg"]); + break; } } diff --git a/aprendizations/templates/question-checkbox.html b/aprendizations/templates/question-checkbox.html index f3027eb..78e3fd2 100644 --- a/aprendizations/templates/question-checkbox.html +++ b/aprendizations/templates/question-checkbox.html @@ -15,6 +15,6 @@ {% end %} - + {% end %} \ No newline at end of file diff --git a/aprendizations/templates/question-information.html b/aprendizations/templates/question-information.html index 8dd60a6..8f6a67d 100644 --- a/aprendizations/templates/question-information.html +++ b/aprendizations/templates/question-information.html @@ -5,5 +5,4 @@
{{ md(question['text']) }}
- - + diff --git a/aprendizations/templates/question-radio.html b/aprendizations/templates/question-radio.html index 0df78bc..6d13057 100644 --- a/aprendizations/templates/question-radio.html +++ b/aprendizations/templates/question-radio.html @@ -15,6 +15,5 @@ {% end %} - - + {% end %} \ No newline at end of file diff --git a/aprendizations/templates/question-text.html b/aprendizations/templates/question-text.html index 0c71825..f71f861 100644 --- a/aprendizations/templates/question-text.html +++ b/aprendizations/templates/question-text.html @@ -8,5 +8,5 @@ {% end %}
- + {% end %} diff --git a/aprendizations/templates/question-textarea.html b/aprendizations/templates/question-textarea.html index 895dcbf..f7063ac 100644 --- a/aprendizations/templates/question-textarea.html +++ b/aprendizations/templates/question-textarea.html @@ -3,7 +3,7 @@ {% block answer %}
- +