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 | 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 | 9 | - guardar o estado a meio de um nível. |
6 | 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 | 11 | - click numa opcao checkbox fora da checkbox+label não está a funcionar. |
... | ... | @@ -32,6 +36,7 @@ |
32 | 36 | |
33 | 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 | 40 | - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. |
36 | 41 | - não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes |
37 | 42 | - max tries nas perguntas. | ... | ... |
aprendizations/knowledge.py
... | ... | @@ -92,13 +92,24 @@ class StudentKnowledge(object): |
92 | 92 | logger.debug(f'Questions: {", ".join(questions)}') |
93 | 93 | |
94 | 94 | # generate instances of questions |
95 | + | |
96 | + # synchronous: | |
95 | 97 | # self.questions = [self.factory[ref].generate() for ref in questions] |
98 | + | |
96 | 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 | 114 | # get first question |
104 | 115 | self.next_question() |
... | ... | @@ -208,7 +219,6 @@ class StudentKnowledge(object): |
208 | 219 | # Topics unlocked but not yet done have level 0.0. |
209 | 220 | # ------------------------------------------------------------------------ |
210 | 221 | def get_knowledge_state(self): |
211 | - # print(self.topic_sequence) | |
212 | 222 | return [{ |
213 | 223 | 'ref': ref, |
214 | 224 | 'type': self.deps.nodes[ref]['type'], | ... | ... |
aprendizations/learnapp.py
... | ... | @@ -81,8 +81,7 @@ class LearnApp(object): |
81 | 81 | except Exception: |
82 | 82 | logger.error(f'Failed to generate "{qref}".') |
83 | 83 | errors += 1 |
84 | - raise LearnException('Sanity checks') | |
85 | - continue | |
84 | + continue # to next question | |
86 | 85 | |
87 | 86 | if 'tests_right' in q: |
88 | 87 | for t in q['tests_right']: |
... | ... | @@ -91,7 +90,7 @@ class LearnApp(object): |
91 | 90 | if q['grade'] < 1.0: |
92 | 91 | logger.error(f'Failed right answer in "{qref}".') |
93 | 92 | errors += 1 |
94 | - continue # to next right test | |
93 | + continue # to next test | |
95 | 94 | |
96 | 95 | if 'tests_wrong' in q: |
97 | 96 | for t in q['tests_wrong']: |
... | ... | @@ -100,7 +99,7 @@ class LearnApp(object): |
100 | 99 | if q['grade'] >= 1.0: |
101 | 100 | logger.error(f'Failed wrong answer in "{qref}".') |
102 | 101 | errors += 1 |
103 | - continue # to next wrong test | |
102 | + continue # to next test | |
104 | 103 | |
105 | 104 | if errors > 0: |
106 | 105 | logger.info(f'{errors:>6} errors found.') |
... | ... | @@ -400,6 +399,10 @@ class LearnApp(object): |
400 | 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 | 406 | def get_student_question_type(self, uid: str) -> str: |
404 | 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 | import logging |
7 | 7 | import asyncio |
8 | 8 | from typing import Any, Dict, NewType |
9 | +import uuid | |
9 | 10 | |
10 | 11 | # this project |
11 | 12 | from .tools import run_script |
... | ... | @@ -430,6 +431,7 @@ class QFactory(object): |
430 | 431 | # Shallow copy so that script generated questions will not replace |
431 | 432 | # the original generators |
432 | 433 | q = self.question.copy() |
434 | + q['qid'] = str(uuid.uuid4()) # unique for each generated question | |
433 | 435 | |
434 | 436 | # If question is of generator type, an external program will be run |
435 | 437 | # which will print a valid question in yaml format to stdout. This | ... | ... |
aprendizations/serve.py
... | ... | @@ -153,16 +153,19 @@ class ChangePasswordHandler(BaseHandler): |
153 | 153 | |
154 | 154 | changed_ok = await self.learn.change_password(uid, pw) |
155 | 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 | 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 | 260 | q = self.learn.get_current_question(user) |
258 | 261 | |
259 | 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 | 265 | response = { |
263 | 266 | 'method': 'new_question', |
264 | 267 | 'params': { |
265 | 268 | 'type': q['type'], |
266 | - 'question': question_html, | |
269 | + 'question': to_unicode(qhtml), | |
267 | 270 | 'progress': self.learn.get_student_progress(user), |
268 | 271 | 'tries': q['tries'], |
269 | 272 | } |
270 | 273 | } |
271 | 274 | |
272 | 275 | else: |
273 | - finished = to_unicode(self.render_string('finished_topic.html')) | |
276 | + finished = self.render_string('finished_topic.html') | |
274 | 277 | response = { |
275 | 278 | 'method': 'finished_topic', |
276 | 279 | 'params': { |
277 | - 'question': finished | |
280 | + 'question': to_unicode(finished) | |
278 | 281 | } |
279 | 282 | } |
280 | 283 | |
... | ... | @@ -282,15 +285,29 @@ class QuestionHandler(BaseHandler): |
282 | 285 | |
283 | 286 | # --- post answer, returns what to do next: shake, new_question, finished |
284 | 287 | @tornado.web.authenticated |
285 | - async def post(self): | |
288 | + async def post(self) -> None: | |
286 | 289 | logging.debug('QuestionHandler.post()') |
287 | 290 | user = self.current_user |
288 | 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 | 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 | 311 | qtype = self.learn.get_student_question_type(user) |
295 | 312 | if qtype in ('success', 'information', 'info'): |
296 | 313 | answer = None |
... | ... | @@ -299,52 +316,49 @@ class QuestionHandler(BaseHandler): |
299 | 316 | elif qtype != 'checkbox': # radio, text, textarea, ... |
300 | 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 | 320 | q, action = await self.learn.check_answer(user, answer) |
304 | 321 | |
322 | + # --- built response to return | |
305 | 323 | response = {'method': action, 'params': {}} |
306 | - | |
307 | 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 | 331 | response['params'] = { |
315 | 332 | 'type': q['type'], |
316 | 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 | 336 | 'tries': q['tries'], |
320 | 337 | } |
321 | - | |
322 | 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 | 342 | response['params'] = { |
327 | 343 | 'type': q['type'], |
328 | 344 | 'progress': self.learn.get_student_progress(user), |
329 | - 'comments': comments_html, | |
345 | + 'comments': to_unicode(comments_html), | |
330 | 346 | 'tries': q['tries'], |
331 | 347 | } |
332 | - | |
333 | 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 | 355 | response['params'] = { |
341 | 356 | 'type': q['type'], |
342 | 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 | 360 | 'tries': q['tries'], |
346 | 361 | } |
347 | - | |
348 | 362 | else: |
349 | 363 | logging.error(f'Unknown action: {action}') |
350 | 364 | ... | ... |
aprendizations/static/js/topic.js
aprendizations/templates/question-checkbox.html
aprendizations/templates/question-information.html
aprendizations/templates/question-radio.html
aprendizations/templates/question-text.html
... | ... | @@ -8,5 +8,5 @@ |
8 | 8 | <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> |
9 | 9 | {% end %} |
10 | 10 | </fieldset><br /> |
11 | -<input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | |
11 | +<input type="hidden" name="qid" value="{{ question['qid'] }}"> | |
12 | 12 | {% end %} | ... | ... |
aprendizations/templates/question-textarea.html
... | ... | @@ -3,7 +3,7 @@ |
3 | 3 | {% block answer %} |
4 | 4 | |
5 | 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 | 8 | <script> |
9 | 9 | var editor = CodeMirror.fromTextArea(document.getElementById("code"), { | ... | ... |