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
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
@@ -5,5 +5,4 @@ @@ -5,5 +5,4 @@
5 <div id="text"> 5 <div id="text">
6 {{ md(question['text']) }} 6 {{ md(question['text']) }}
7 </div> 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,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"), {