Commit 1ab9d6baf0b1ee14b05f5b29fa1db308225b78fa

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

Async question generator.

1 1
2 # BUGS 2 # BUGS
3 3
  4 +- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo)
4 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) 5 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
5 - devia mostrar timout para o aluno saber a razao. 6 - devia mostrar timout para o aluno saber a razao.
6 - permitir configuracao para escolher entre static files locais ou remotos 7 - permitir configuracao para escolher entre static files locais ou remotos
aprendizations/knowledge.py
@@ -58,7 +58,7 @@ class StudentKnowledge(object): @@ -58,7 +58,7 @@ class StudentKnowledge(object):
58 'level': 0.0, # unlocked 58 'level': 0.0, # unlocked
59 'date': datetime.now() 59 'date': datetime.now()
60 } 60 }
61 - logger.debug(f'Unlocked "{topic}".') 61 + logger.debug(f'[unlock_topics] Unlocked "{topic}".')
62 # else: # lock this topic if deps do not satisfy min_level 62 # else: # lock this topic if deps do not satisfy min_level
63 # del self.state[topic] 63 # del self.state[topic]
64 64
@@ -68,7 +68,7 @@ class StudentKnowledge(object): @@ -68,7 +68,7 @@ class StudentKnowledge(object):
68 # current_question: the current question to be presented 68 # current_question: the current question to be presented
69 # ------------------------------------------------------------------------ 69 # ------------------------------------------------------------------------
70 async def start_topic(self, topic): 70 async def start_topic(self, topic):
71 - logger.debug('StudentKnowledge.start_topic()') 71 + logger.debug(f'[start_topic] topic "{topic}"')
72 72
73 if self.current_topic == topic: 73 if self.current_topic == topic:
74 logger.info('Restarting current topic is not allowed.') 74 logger.info('Restarting current topic is not allowed.')
@@ -76,7 +76,7 @@ class StudentKnowledge(object): @@ -76,7 +76,7 @@ class StudentKnowledge(object):
76 76
77 # do not allow locked topics 77 # do not allow locked topics
78 if self.is_locked(topic): 78 if self.is_locked(topic):
79 - logger.debug(f'Topic {topic} is locked') 79 + logger.debug(f'[start_topic] topic "{topic}" is locked')
80 return False 80 return False
81 81
82 # starting new topic 82 # starting new topic
@@ -90,23 +90,20 @@ class StudentKnowledge(object): @@ -90,23 +90,20 @@ class StudentKnowledge(object):
90 questions = random.sample(t['questions'], k=k) 90 questions = random.sample(t['questions'], k=k)
91 else: 91 else:
92 questions = t['questions'][:k] 92 questions = t['questions'][:k]
93 - logger.debug(f'Questions: {", ".join(questions)}') 93 + logger.debug(f'[start_topic] questions: {", ".join(questions)}')
94 94
95 - # generate instances of questions 95 + # synchronous
  96 + # self.questions = [self.factory[ref].generate()
  97 + # for ref in questions]
96 98
97 - # synchronous:  
98 - # self.questions = [self.factory[ref].generate() for ref in questions]  
99 -  
100 - # async:  
101 - loop = asyncio.get_running_loop()  
102 - generators = [loop.run_in_executor(None, self.factory[qref].generate)  
103 - for qref in questions]  
104 - self.questions = await asyncio.gather(*generators) 99 + # asynchronous:
  100 + self.questions = [await self.factory[ref].generate_async()
  101 + for ref in questions]
105 102
106 # get first question 103 # get first question
107 self.next_question() 104 self.next_question()
108 105
109 - logger.debug(f'Generated {len(self.questions)} questions') 106 + logger.debug(f'[start_topic] generated {len(self.questions)} questions')
110 return True 107 return True
111 108
112 # ------------------------------------------------------------------------ 109 # ------------------------------------------------------------------------
@@ -115,7 +112,7 @@ class StudentKnowledge(object): @@ -115,7 +112,7 @@ class StudentKnowledge(object):
115 # The current topic is unchanged. 112 # The current topic is unchanged.
116 # ------------------------------------------------------------------------ 113 # ------------------------------------------------------------------------
117 def finish_topic(self): 114 def finish_topic(self):
118 - logger.debug(f'StudentKnowledge.finish_topic({self.current_topic})') 115 + logger.debug(f'[finish_topic] current_topic {self.current_topic}')
119 116
120 self.state[self.current_topic] = { 117 self.state[self.current_topic] = {
121 'date': datetime.now(), 118 'date': datetime.now(),
@@ -132,13 +129,13 @@ class StudentKnowledge(object): @@ -132,13 +129,13 @@ class StudentKnowledge(object):
132 # - if wrong, counts number of tries. If exceeded, moves on. 129 # - if wrong, counts number of tries. If exceeded, moves on.
133 # ------------------------------------------------------------------------ 130 # ------------------------------------------------------------------------
134 async def check_answer(self, answer): 131 async def check_answer(self, answer):
135 - logger.debug('StudentKnowledge.check_answer()') 132 + logger.debug('[check_answer]')
136 133
137 q = self.current_question 134 q = self.current_question
138 q['answer'] = answer 135 q['answer'] = answer
139 q['finish_time'] = datetime.now() 136 q['finish_time'] = datetime.now()
140 await q.correct_async() 137 await q.correct_async()
141 - logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})') 138 + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
142 139
143 if q['grade'] > 0.999: 140 if q['grade'] > 0.999:
144 self.correct_answers += 1 141 self.correct_answers += 1
@@ -154,7 +151,7 @@ class StudentKnowledge(object): @@ -154,7 +151,7 @@ class StudentKnowledge(object):
154 else: 151 else:
155 action = 'wrong' 152 action = 'wrong'
156 if self.current_question['append_wrong']: 153 if self.current_question['append_wrong']:
157 - logger.debug("Append new instance of question at the end") 154 + logger.debug('[check_answer] Wrong, append new instance')
158 self.questions.append(self.factory[q['ref']].generate()) 155 self.questions.append(self.factory[q['ref']].generate())
159 self.next_question() 156 self.next_question()
160 157
@@ -175,7 +172,7 @@ class StudentKnowledge(object): @@ -175,7 +172,7 @@ class StudentKnowledge(object):
175 default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] 172 default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
176 maxtries = self.current_question.get('max_tries', default_maxtries) 173 maxtries = self.current_question.get('max_tries', default_maxtries)
177 self.current_question['tries'] = maxtries 174 self.current_question['tries'] = maxtries
178 - logger.debug(f'Next question is "{self.current_question["ref"]}"') 175 + logger.debug(f'[next_question] "{self.current_question["ref"]}"')
179 176
180 return self.current_question # question or None 177 return self.current_question # question or None
181 178
aprendizations/learnapp.py
@@ -75,7 +75,7 @@ class LearnApp(object): @@ -75,7 +75,7 @@ class LearnApp(object):
75 75
76 errors = 0 76 errors = 0
77 for qref in self.factory: 77 for qref in self.factory:
78 - logger.debug(f'Checking "{qref}"...') 78 + logger.debug(f'[sanity_check_questions] Checking "{qref}"...')
79 try: 79 try:
80 q = self.factory[qref].generate() 80 q = self.factory[qref].generate()
81 except Exception: 81 except Exception:
@@ -204,7 +204,7 @@ class LearnApp(object): @@ -204,7 +204,7 @@ class LearnApp(object):
204 finishtime=str(q['finish_time']), 204 finishtime=str(q['finish_time']),
205 student_id=uid, 205 student_id=uid,
206 topic_id=topic)) 206 topic_id=topic))
207 - logger.debug(f'Saved "{q["ref"]}" into database') 207 + logger.debug(f'[check_answer] Saved "{q["ref"]}" into database')
208 208
209 if knowledge.topic_has_finished(): 209 if knowledge.topic_has_finished():
210 # finished topic, save into database 210 # finished topic, save into database
@@ -218,7 +218,7 @@ class LearnApp(object): @@ -218,7 +218,7 @@ class LearnApp(object):
218 .one_or_none() 218 .one_or_none()
219 if a is None: 219 if a is None:
220 # insert new studenttopic into database 220 # insert new studenttopic into database
221 - logger.debug('Database insert new studenttopic') 221 + logger.debug('[check_answer] Database insert studenttopic')
222 t = s.query(Topic).get(topic) 222 t = s.query(Topic).get(topic)
223 u = s.query(Student).get(uid) 223 u = s.query(Student).get(uid)
224 # association object 224 # association object
@@ -227,13 +227,13 @@ class LearnApp(object): @@ -227,13 +227,13 @@ class LearnApp(object):
227 u.topics.append(a) 227 u.topics.append(a)
228 else: 228 else:
229 # update studenttopic in database 229 # update studenttopic in database
230 - logger.debug('Database update studenttopic') 230 + logger.debug('[check_answer] Database update studenttopic')
231 a.level = level 231 a.level = level
232 a.date = date 232 a.date = date
233 233
234 s.add(a) 234 s.add(a)
235 235
236 - logger.debug(f'Saved topic "{topic}" into database') 236 + logger.debug(f'[check_answer] Saved topic "{topic}" into database')
237 237
238 return q, action 238 return q, action
239 239
@@ -244,8 +244,8 @@ class LearnApp(object): @@ -244,8 +244,8 @@ class LearnApp(object):
244 student = self.online[uid]['state'] 244 student = self.online[uid]['state']
245 try: 245 try:
246 await student.start_topic(topic) 246 await student.start_topic(topic)
247 - except Exception:  
248 - logger.warning(f'User "{uid}" could not start topic "{topic}"') 247 + except Exception as e:
  248 + logger.warning(f'User "{uid}" couldn\'t start "{topic}": {e}')
249 else: 249 else:
250 logger.info(f'User "{uid}" started topic "{topic}"') 250 logger.info(f'User "{uid}" started topic "{topic}"')
251 251
aprendizations/questions.py
@@ -429,7 +429,7 @@ class QuestionInformation(Question): @@ -429,7 +429,7 @@ class QuestionInformation(Question):
429 # # answer one question and correct it 429 # # answer one question and correct it
430 # question['answer'] = 42 # set answer 430 # question['answer'] = 42 # set answer
431 # question.correct() # correct answer 431 # question.correct() # correct answer
432 -# print(question['grade']) # print grade 432 +# grade = question['grade'] # get grade
433 # =========================================================================== 433 # ===========================================================================
434 class QFactory(object): 434 class QFactory(object):
435 # Depending on the type of question, a different question class will be 435 # Depending on the type of question, a different question class will be
@@ -456,7 +456,7 @@ class QFactory(object): @@ -456,7 +456,7 @@ class QFactory(object):
456 # i.e. a question object (radio, checkbox, ...). 456 # i.e. a question object (radio, checkbox, ...).
457 # ----------------------------------------------------------------------- 457 # -----------------------------------------------------------------------
458 def generate(self) -> Question: 458 def generate(self) -> Question:
459 - logger.debug(f'Generating "{self.question["ref"]}"...') 459 + logger.debug(f'[generate] "{self.question["ref"]}"...')
460 # Shallow copy so that script generated questions will not replace 460 # Shallow copy so that script generated questions will not replace
461 # the original generators 461 # the original generators
462 q = self.question.copy() 462 q = self.question.copy()
@@ -468,7 +468,7 @@ class QFactory(object): @@ -468,7 +468,7 @@ class QFactory(object):
468 if q['type'] == 'generator': 468 if q['type'] == 'generator':
469 logger.debug(f' \\_ Running "{q["script"]}".') 469 logger.debug(f' \\_ Running "{q["script"]}".')
470 q.setdefault('args', []) 470 q.setdefault('args', [])
471 - q.setdefault('stdin', '') # FIXME does not exist anymore? 471 + q.setdefault('stdin', '') # FIXME is it really necessary?
472 script = path.join(q['path'], q['script']) 472 script = path.join(q['path'], q['script'])
473 out = run_script(script=script, args=q['args'], stdin=q['stdin']) 473 out = run_script(script=script, args=q['args'], stdin=q['stdin'])
474 q.update(out) 474 q.update(out)
@@ -484,3 +484,36 @@ class QFactory(object): @@ -484,3 +484,36 @@ class QFactory(object):
484 raise 484 raise
485 else: 485 else:
486 return qinstance 486 return qinstance
  487 +
  488 + # -----------------------------------------------------------------------
  489 + async def generate_async(self) -> Question:
  490 + logger.debug(f'[generate_async] "{self.question["ref"]}"...')
  491 + # Shallow copy so that script generated questions will not replace
  492 + # the original generators
  493 + q = self.question.copy()
  494 + q['qid'] = str(uuid.uuid4()) # unique for each generated question
  495 +
  496 + # If question is of generator type, an external program will be run
  497 + # which will print a valid question in yaml format to stdout. This
  498 + # output is then yaml parsed into a dictionary `q`.
  499 + if q['type'] == 'generator':
  500 + logger.debug(f' \\_ Running "{q["script"]}".')
  501 + q.setdefault('args', [])
  502 + q.setdefault('stdin', '') # FIXME is it really necessary?
  503 + script = path.join(q['path'], q['script'])
  504 + out = await run_script_async(script=script, args=q['args'],
  505 + stdin=q['stdin'])
  506 + q.update(out)
  507 +
  508 + # Finally we create an instance of Question()
  509 + try:
  510 + qinstance = self._types[q['type']](QDict(q)) # of matching class
  511 + except QuestionException as e:
  512 + logger.error(e)
  513 + raise e
  514 + except KeyError:
  515 + logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
  516 + raise
  517 + else:
  518 + logger.debug(f'[generate_async] Done instance of {q["ref"]}')
  519 + return qinstance
aprendizations/serve.py
@@ -254,7 +254,7 @@ class QuestionHandler(BaseHandler): @@ -254,7 +254,7 @@ class QuestionHandler(BaseHandler):
254 # --- get question to render 254 # --- get question to render
255 @tornado.web.authenticated 255 @tornado.web.authenticated
256 def get(self): 256 def get(self):
257 - logging.debug('QuestionHandler.get()') 257 + logging.debug('[QuestionHandler.get]')
258 user = self.current_user 258 user = self.current_user
259 q = self.learn.get_current_question(user) 259 q = self.learn.get_current_question(user)
260 260
@@ -285,7 +285,7 @@ class QuestionHandler(BaseHandler): @@ -285,7 +285,7 @@ class QuestionHandler(BaseHandler):
285 # --- post answer, returns what to do next: shake, new_question, finished 285 # --- post answer, returns what to do next: shake, new_question, finished
286 @tornado.web.authenticated 286 @tornado.web.authenticated
287 async def post(self) -> None: 287 async def post(self) -> None:
288 - logging.debug('QuestionHandler.post()') 288 + logging.debug('[QuestionHandler.post]')
289 user = self.current_user 289 user = self.current_user
290 answer = self.get_body_arguments('answer') # list 290 answer = self.get_body_arguments('answer') # list
291 291
aprendizations/tools.py
@@ -170,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any: @@ -170,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any:
170 def run_script(script: str, 170 def run_script(script: str,
171 args: List[str] = [], 171 args: List[str] = [],
172 stdin: str = '', 172 stdin: str = '',
173 - timeout: int = 5) -> Any: 173 + timeout: int = 2) -> Any:
174 174
175 script = path.expanduser(script) 175 script = path.expanduser(script)
176 try: 176 try:
@@ -210,13 +210,13 @@ def run_script(script: str, @@ -210,13 +210,13 @@ def run_script(script: str,
210 async def run_script_async(script: str, 210 async def run_script_async(script: str,
211 args: List[str] = [], 211 args: List[str] = [],
212 stdin: str = '', 212 stdin: str = '',
213 - timeout: int = 5) -> Any: 213 + timeout: int = 2) -> Any:
214 214
215 script = path.expanduser(script) 215 script = path.expanduser(script)
216 - cmd = [script] + [str(a) for a in args] 216 + args = [str(a) for a in args]
217 217
218 - p = await asyncio.create_subprocess_shell(  
219 - *cmd, 218 + p = await asyncio.create_subprocess_exec(
  219 + script, *args,
220 stdin=asyncio.subprocess.PIPE, 220 stdin=asyncio.subprocess.PIPE,
221 stdout=asyncio.subprocess.PIPE, 221 stdout=asyncio.subprocess.PIPE,
222 stderr=asyncio.subprocess.DEVNULL, 222 stderr=asyncio.subprocess.DEVNULL,
@@ -235,7 +235,7 @@ async def run_script_async(script: str, @@ -235,7 +235,7 @@ async def run_script_async(script: str,
235 logger.error(f'Return code {p.returncode} running "{script}".') 235 logger.error(f'Return code {p.returncode} running "{script}".')
236 else: 236 else:
237 try: 237 try:
238 - output = yaml.safe_load(stdout.decode('utf-8')) 238 + output = yaml.safe_load(stdout.decode('utf-8', 'ignore'))
239 except Exception: 239 except Exception:
240 logger.error(f'Error parsing yaml output of "{script}"') 240 logger.error(f'Error parsing yaml output of "{script}"')
241 else: 241 else: