Commit 1841cc32611063ab725281eee6a665248a1bba09
1 parent
a333dc72
Exists in
master
and in
1 other branch
- fix desynchronized questions when multiple browser windows are open.
- remove dead code in html templates
Showing
11 changed files
with
88 additions
and
52 deletions
Show diff stats
BUGS.md
1 | 1 | ||
2 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | -- 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 /. | 4 | +- sqlalchemy.pool.impl.NullPool: Exception during reset or similar |
5 | +sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. | ||
6 | + | ||
7 | +- porque é que md() é usado nos templates e não directamente no codigo python? | ||
8 | +- templates question-*.html tem input hidden question_ref que não é usado. remover? | ||
5 | - guardar o estado a meio de um nível. | 9 | - guardar o estado a meio de um nível. |
6 | - 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. | 10 | - 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. |
7 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. | 11 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. |
@@ -32,6 +36,7 @@ | @@ -32,6 +36,7 @@ | ||
32 | 36 | ||
33 | # FIXED | 37 | # FIXED |
34 | 38 | ||
39 | +- 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 /. | ||
35 | - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. | 40 | - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. |
36 | - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes | 41 | - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes |
37 | - max tries nas perguntas. | 42 | - max tries nas perguntas. |
aprendizations/knowledge.py
@@ -92,13 +92,24 @@ class StudentKnowledge(object): | @@ -92,13 +92,24 @@ class StudentKnowledge(object): | ||
92 | logger.debug(f'Questions: {", ".join(questions)}') | 92 | logger.debug(f'Questions: {", ".join(questions)}') |
93 | 93 | ||
94 | # generate instances of questions | 94 | # generate instances of questions |
95 | + | ||
96 | + # synchronous: | ||
95 | # self.questions = [self.factory[ref].generate() for ref in questions] | 97 | # self.questions = [self.factory[ref].generate() for ref in questions] |
98 | + | ||
96 | loop = asyncio.get_running_loop() | 99 | loop = asyncio.get_running_loop() |
97 | - generators = [loop.run_in_executor(None, self.factory[qref].generate) | ||
98 | - for qref in questions] | ||
99 | - self.questions = await asyncio.gather(*generators) | ||
100 | 100 | ||
101 | - logger.debug(f'Total: {len(self.questions)} questions') | 101 | + # async: |
102 | + # generators = [loop.run_in_executor(None, self.factory[qref].generate) | ||
103 | + # for qref in questions] | ||
104 | + # self.questions = await asyncio.gather(*generators) | ||
105 | + | ||
106 | + # another async: | ||
107 | + self.questions = [] | ||
108 | + for qref in questions: | ||
109 | + q = await loop.run_in_executor(None, self.factory[qref].generate) | ||
110 | + self.questions.append(q) | ||
111 | + | ||
112 | + logger.debug(f'Generated {len(self.questions)} questions') | ||
102 | 113 | ||
103 | # get first question | 114 | # get first question |
104 | self.next_question() | 115 | self.next_question() |
@@ -208,7 +219,6 @@ class StudentKnowledge(object): | @@ -208,7 +219,6 @@ class StudentKnowledge(object): | ||
208 | # Topics unlocked but not yet done have level 0.0. | 219 | # Topics unlocked but not yet done have level 0.0. |
209 | # ------------------------------------------------------------------------ | 220 | # ------------------------------------------------------------------------ |
210 | def get_knowledge_state(self): | 221 | def get_knowledge_state(self): |
211 | - # print(self.topic_sequence) | ||
212 | return [{ | 222 | return [{ |
213 | 'ref': ref, | 223 | 'ref': ref, |
214 | 'type': self.deps.nodes[ref]['type'], | 224 | 'type': self.deps.nodes[ref]['type'], |
aprendizations/learnapp.py
@@ -81,8 +81,7 @@ class LearnApp(object): | @@ -81,8 +81,7 @@ class LearnApp(object): | ||
81 | except Exception: | 81 | except Exception: |
82 | logger.error(f'Failed to generate "{qref}".') | 82 | logger.error(f'Failed to generate "{qref}".') |
83 | errors += 1 | 83 | errors += 1 |
84 | - raise LearnException('Sanity checks') | ||
85 | - continue | 84 | + continue # to next question |
86 | 85 | ||
87 | if 'tests_right' in q: | 86 | if 'tests_right' in q: |
88 | for t in q['tests_right']: | 87 | for t in q['tests_right']: |
@@ -91,7 +90,7 @@ class LearnApp(object): | @@ -91,7 +90,7 @@ class LearnApp(object): | ||
91 | if q['grade'] < 1.0: | 90 | if q['grade'] < 1.0: |
92 | logger.error(f'Failed right answer in "{qref}".') | 91 | logger.error(f'Failed right answer in "{qref}".') |
93 | errors += 1 | 92 | errors += 1 |
94 | - continue # to next right test | 93 | + continue # to next test |
95 | 94 | ||
96 | if 'tests_wrong' in q: | 95 | if 'tests_wrong' in q: |
97 | for t in q['tests_wrong']: | 96 | for t in q['tests_wrong']: |
@@ -100,7 +99,7 @@ class LearnApp(object): | @@ -100,7 +99,7 @@ class LearnApp(object): | ||
100 | if q['grade'] >= 1.0: | 99 | if q['grade'] >= 1.0: |
101 | logger.error(f'Failed wrong answer in "{qref}".') | 100 | logger.error(f'Failed wrong answer in "{qref}".') |
102 | errors += 1 | 101 | errors += 1 |
103 | - continue # to next wrong test | 102 | + continue # to next test |
104 | 103 | ||
105 | if errors > 0: | 104 | if errors > 0: |
106 | logger.info(f'{errors:>6} errors found.') | 105 | logger.info(f'{errors:>6} errors found.') |
@@ -400,6 +399,10 @@ class LearnApp(object): | @@ -400,6 +399,10 @@ class LearnApp(object): | ||
400 | return self.online[uid]['state'].get_current_question() # dict | 399 | return self.online[uid]['state'].get_current_question() # dict |
401 | 400 | ||
402 | # ------------------------------------------------------------------------ | 401 | # ------------------------------------------------------------------------ |
402 | + def get_current_question_id(self, uid: str) -> str: | ||
403 | + return self.online[uid]['state'].get_current_question()['qid'] | ||
404 | + | ||
405 | + # ------------------------------------------------------------------------ | ||
403 | def get_student_question_type(self, uid: str) -> str: | 406 | def get_student_question_type(self, uid: str) -> str: |
404 | return self.online[uid]['state'].get_current_question()['type'] | 407 | return self.online[uid]['state'].get_current_question()['type'] |
405 | 408 |
aprendizations/questions.py
@@ -6,6 +6,7 @@ from os import path | @@ -6,6 +6,7 @@ from os import path | ||
6 | import logging | 6 | import logging |
7 | import asyncio | 7 | import asyncio |
8 | from typing import Any, Dict, NewType | 8 | from typing import Any, Dict, NewType |
9 | +import uuid | ||
9 | 10 | ||
10 | # this project | 11 | # this project |
11 | from .tools import run_script | 12 | from .tools import run_script |
@@ -430,6 +431,7 @@ class QFactory(object): | @@ -430,6 +431,7 @@ class QFactory(object): | ||
430 | # Shallow copy so that script generated questions will not replace | 431 | # Shallow copy so that script generated questions will not replace |
431 | # the original generators | 432 | # the original generators |
432 | q = self.question.copy() | 433 | q = self.question.copy() |
434 | + q['qid'] = str(uuid.uuid4()) # unique for each generated question | ||
433 | 435 | ||
434 | # If question is of generator type, an external program will be run | 436 | # If question is of generator type, an external program will be run |
435 | # which will print a valid question in yaml format to stdout. This | 437 | # which will print a valid question in yaml format to stdout. This |
aprendizations/serve.py
@@ -153,16 +153,19 @@ class ChangePasswordHandler(BaseHandler): | @@ -153,16 +153,19 @@ class ChangePasswordHandler(BaseHandler): | ||
153 | 153 | ||
154 | changed_ok = await self.learn.change_password(uid, pw) | 154 | changed_ok = await self.learn.change_password(uid, pw) |
155 | if changed_ok: | 155 | if changed_ok: |
156 | - notification = to_unicode(self.render_string( | ||
157 | - 'notification.html', type='success', | ||
158 | - msg='A password foi alterada!') | 156 | + notification = self.render_string( |
157 | + 'notification.html', | ||
158 | + type='success', | ||
159 | + msg='A password foi alterada!' | ||
159 | ) | 160 | ) |
160 | else: | 161 | else: |
161 | - notification = to_unicode(self.render_string( | ||
162 | - 'notification.html', type='danger', | ||
163 | - msg='A password não foi alterada!') | 162 | + notification = self.render_string( |
163 | + 'notification.html', | ||
164 | + type='danger', | ||
165 | + msg='A password não foi alterada!' | ||
164 | ) | 166 | ) |
165 | - self.write({'msg': notification}) | 167 | + |
168 | + self.write({'msg': to_unicode(notification)}) | ||
166 | 169 | ||
167 | 170 | ||
168 | # ---------------------------------------------------------------------------- | 171 | # ---------------------------------------------------------------------------- |
@@ -257,24 +260,24 @@ class QuestionHandler(BaseHandler): | @@ -257,24 +260,24 @@ class QuestionHandler(BaseHandler): | ||
257 | q = self.learn.get_current_question(user) | 260 | q = self.learn.get_current_question(user) |
258 | 261 | ||
259 | if q is not None: | 262 | if q is not None: |
260 | - question_html = to_unicode(self.render_string( | ||
261 | - self.templates[q['type']], question=q, md=md_to_html)) | 263 | + qhtml = self.render_string(self.templates[q['type']], |
264 | + question=q, md=md_to_html) | ||
262 | response = { | 265 | response = { |
263 | 'method': 'new_question', | 266 | 'method': 'new_question', |
264 | 'params': { | 267 | 'params': { |
265 | 'type': q['type'], | 268 | 'type': q['type'], |
266 | - 'question': question_html, | 269 | + 'question': to_unicode(qhtml), |
267 | 'progress': self.learn.get_student_progress(user), | 270 | 'progress': self.learn.get_student_progress(user), |
268 | 'tries': q['tries'], | 271 | 'tries': q['tries'], |
269 | } | 272 | } |
270 | } | 273 | } |
271 | 274 | ||
272 | else: | 275 | else: |
273 | - finished = to_unicode(self.render_string('finished_topic.html')) | 276 | + finished = self.render_string('finished_topic.html') |
274 | response = { | 277 | response = { |
275 | 'method': 'finished_topic', | 278 | 'method': 'finished_topic', |
276 | 'params': { | 279 | 'params': { |
277 | - 'question': finished | 280 | + 'question': to_unicode(finished) |
278 | } | 281 | } |
279 | } | 282 | } |
280 | 283 | ||
@@ -282,15 +285,29 @@ class QuestionHandler(BaseHandler): | @@ -282,15 +285,29 @@ class QuestionHandler(BaseHandler): | ||
282 | 285 | ||
283 | # --- post answer, returns what to do next: shake, new_question, finished | 286 | # --- post answer, returns what to do next: shake, new_question, finished |
284 | @tornado.web.authenticated | 287 | @tornado.web.authenticated |
285 | - async def post(self): | 288 | + async def post(self) -> None: |
286 | logging.debug('QuestionHandler.post()') | 289 | logging.debug('QuestionHandler.post()') |
287 | user = self.current_user | 290 | user = self.current_user |
288 | answer = self.get_body_arguments('answer') # list | 291 | answer = self.get_body_arguments('answer') # list |
289 | 292 | ||
290 | - # brain hacking ;) | 293 | + # --- check if browser opened different questions simultaneously |
294 | + answer_qid = self.get_body_arguments('qid')[0] | ||
295 | + current_qid = self.learn.get_current_question_id(user) | ||
296 | + if answer_qid != current_qid: | ||
297 | + logging.debug(f'User {user} desynchronized questions') | ||
298 | + self.write({ | ||
299 | + 'method': 'invalid', | ||
300 | + 'params': { | ||
301 | + 'msg': ('Esta pergunta já não está activa. ' | ||
302 | + 'Tem outra janela aberta?') | ||
303 | + } | ||
304 | + }) | ||
305 | + return | ||
306 | + | ||
307 | + # --- brain hacking ;) | ||
291 | await asyncio.sleep(1) | 308 | await asyncio.sleep(1) |
292 | 309 | ||
293 | - # answers are returned in a list. fix depending on question type | 310 | + # --- answers are in a list. fix depending on question type |
294 | qtype = self.learn.get_student_question_type(user) | 311 | qtype = self.learn.get_student_question_type(user) |
295 | if qtype in ('success', 'information', 'info'): | 312 | if qtype in ('success', 'information', 'info'): |
296 | answer = None | 313 | answer = None |
@@ -299,52 +316,49 @@ class QuestionHandler(BaseHandler): | @@ -299,52 +316,49 @@ class QuestionHandler(BaseHandler): | ||
299 | elif qtype != 'checkbox': # radio, text, textarea, ... | 316 | elif qtype != 'checkbox': # radio, text, textarea, ... |
300 | answer = answer[0] | 317 | answer = answer[0] |
301 | 318 | ||
302 | - # check answer (nonblocking) and get corrected question | 319 | + # --- check answer (nonblocking) and get corrected question and action |
303 | q, action = await self.learn.check_answer(user, answer) | 320 | q, action = await self.learn.check_answer(user, answer) |
304 | 321 | ||
322 | + # --- built response to return | ||
305 | response = {'method': action, 'params': {}} | 323 | response = {'method': action, 'params': {}} |
306 | - | ||
307 | if action == 'right': # get next question in the topic | 324 | if action == 'right': # get next question in the topic |
308 | - comments_html = to_unicode(self.render_string( | ||
309 | - 'comments-right.html', comments=q['comments'], md=md_to_html)) | 325 | + comments_html = self.render_string( |
326 | + 'comments-right.html', comments=q['comments'], md=md_to_html) | ||
310 | 327 | ||
311 | - solution_html = to_unicode(self.render_string( | ||
312 | - 'solution.html', solution=q['solution'], md=md_to_html)) | 328 | + solution_html = self.render_string( |
329 | + 'solution.html', solution=q['solution'], md=md_to_html) | ||
313 | 330 | ||
314 | response['params'] = { | 331 | response['params'] = { |
315 | 'type': q['type'], | 332 | 'type': q['type'], |
316 | 'progress': self.learn.get_student_progress(user), | 333 | 'progress': self.learn.get_student_progress(user), |
317 | - 'comments': comments_html, | ||
318 | - 'solution': solution_html, | 334 | + 'comments': to_unicode(comments_html), |
335 | + 'solution': to_unicode(solution_html), | ||
319 | 'tries': q['tries'], | 336 | 'tries': q['tries'], |
320 | } | 337 | } |
321 | - | ||
322 | elif action == 'try_again': | 338 | elif action == 'try_again': |
323 | - comments_html = to_unicode(self.render_string( | ||
324 | - 'comments.html', comments=q['comments'], md=md_to_html)) | 339 | + comments_html = self.render_string( |
340 | + 'comments.html', comments=q['comments'], md=md_to_html) | ||
325 | 341 | ||
326 | response['params'] = { | 342 | response['params'] = { |
327 | 'type': q['type'], | 343 | 'type': q['type'], |
328 | 'progress': self.learn.get_student_progress(user), | 344 | 'progress': self.learn.get_student_progress(user), |
329 | - 'comments': comments_html, | 345 | + 'comments': to_unicode(comments_html), |
330 | 'tries': q['tries'], | 346 | 'tries': q['tries'], |
331 | } | 347 | } |
332 | - | ||
333 | elif action == 'wrong': # no more tries | 348 | elif action == 'wrong': # no more tries |
334 | - comments_html = to_unicode(self.render_string( | ||
335 | - 'comments.html', comments=q['comments'], md=md_to_html)) | 349 | + comments_html = self.render_string( |
350 | + 'comments.html', comments=q['comments'], md=md_to_html) | ||
336 | 351 | ||
337 | - solution_html = to_unicode(self.render_string( | ||
338 | - 'solution.html', solution=q['solution'], md=md_to_html)) | 352 | + solution_html = self.render_string( |
353 | + 'solution.html', solution=q['solution'], md=md_to_html) | ||
339 | 354 | ||
340 | response['params'] = { | 355 | response['params'] = { |
341 | 'type': q['type'], | 356 | 'type': q['type'], |
342 | 'progress': self.learn.get_student_progress(user), | 357 | 'progress': self.learn.get_student_progress(user), |
343 | - 'comments': comments_html, | ||
344 | - 'solution': solution_html, | 358 | + 'comments': to_unicode(comments_html), |
359 | + 'solution': to_unicode(solution_html), | ||
345 | 'tries': q['tries'], | 360 | 'tries': q['tries'], |
346 | } | 361 | } |
347 | - | ||
348 | else: | 362 | else: |
349 | logging.error(f'Unknown action: {action}') | 363 | logging.error(f'Unknown action: {action}') |
350 | 364 |
aprendizations/static/js/topic.js
@@ -162,6 +162,10 @@ function getFeedback(response) { | @@ -162,6 +162,10 @@ function getFeedback(response) { | ||
162 | $("fieldset").attr("disabled", "disabled"); | 162 | $("fieldset").attr("disabled", "disabled"); |
163 | $("#submit").html("Continuar").off().click(getQuestion); | 163 | $("#submit").html("Continuar").off().click(getQuestion); |
164 | break; | 164 | break; |
165 | + | ||
166 | + case "invalid": | ||
167 | + alert(params["msg"]); | ||
168 | + break; | ||
165 | } | 169 | } |
166 | } | 170 | } |
167 | 171 |
aprendizations/templates/question-checkbox.html
@@ -15,6 +15,6 @@ | @@ -15,6 +15,6 @@ | ||
15 | {% end %} | 15 | {% end %} |
16 | </div> | 16 | </div> |
17 | </fieldset> | 17 | </fieldset> |
18 | -<input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | 18 | +<input type="hidden" name="qid" value="{{ question['qid'] }}"> |
19 | 19 | ||
20 | {% end %} | 20 | {% end %} |
21 | \ No newline at end of file | 21 | \ No newline at end of file |
aprendizations/templates/question-information.html
aprendizations/templates/question-radio.html
@@ -15,6 +15,5 @@ | @@ -15,6 +15,5 @@ | ||
15 | {% end %} | 15 | {% end %} |
16 | </div> | 16 | </div> |
17 | </fieldset> | 17 | </fieldset> |
18 | -<input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | ||
19 | - | 18 | +<input type="hidden" name="qid" value="{{ question['qid'] }}"> |
20 | {% end %} | 19 | {% end %} |
21 | \ No newline at end of file | 20 | \ No newline at end of file |
aprendizations/templates/question-text.html
@@ -8,5 +8,5 @@ | @@ -8,5 +8,5 @@ | ||
8 | <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> | 8 | <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> |
9 | {% end %} | 9 | {% end %} |
10 | </fieldset><br /> | 10 | </fieldset><br /> |
11 | -<input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | 11 | +<input type="hidden" name="qid" value="{{ question['qid'] }}"> |
12 | {% end %} | 12 | {% end %} |
aprendizations/templates/question-textarea.html
@@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
3 | {% block answer %} | 3 | {% block answer %} |
4 | 4 | ||
5 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" id="code" autofocus>{{ question['answer'] or '' }}</textarea><br /> | 5 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" id="code" autofocus>{{ question['answer'] or '' }}</textarea><br /> |
6 | -<input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | 6 | +<input type="hidden" name="qid" value="{{ question['qid'] }}"> |
7 | 7 | ||
8 | <script> | 8 | <script> |
9 | var editor = CodeMirror.fromTextArea(document.getElementById("code"), { | 9 | var editor = CodeMirror.fromTextArea(document.getElementById("code"), { |