Commit 1841cc32611063ab725281eee6a665248a1bba09

Authored by Miguel Barão
1 parent a333dc72
Exists in master and in 1 other branch dev

- fix desynchronized questions when multiple browser windows are open.

- remove dead code in html templates
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
... ... @@ -162,6 +162,10 @@ function getFeedback(response) {
162 162 $("fieldset").attr("disabled", "disabled");
163 163 $("#submit").html("Continuar").off().click(getQuestion);
164 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 15 {% end %}
16 16 </div>
17 17 </fieldset>
18   -<input type="hidden" name="question_ref" value="{{ question['ref'] }}">
  18 +<input type="hidden" name="qid" value="{{ question['qid'] }}">
19 19  
20 20 {% end %}
21 21 \ No newline at end of file
... ...
aprendizations/templates/question-information.html
... ... @@ -5,5 +5,4 @@
5 5 <div id="text">
6 6 {{ md(question['text']) }}
7 7 </div>
8   -
9   -<input type="hidden" name="question_ref" value="{{ question['ref'] }}">
  8 +<input type="hidden" name="qid" value="{{ question['qid'] }}">
... ...
aprendizations/templates/question-radio.html
... ... @@ -15,6 +15,5 @@
15 15 {% end %}
16 16 </div>
17 17 </fieldset>
18   -<input type="hidden" name="question_ref" value="{{ question['ref'] }}">
19   -
  18 +<input type="hidden" name="qid" value="{{ question['qid'] }}">
20 19 {% end %}
21 20 \ No newline at end of file
... ...
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"), {
... ...