Commit 19861c70afc10a5da4ffd410c22bd40f7493d790
1 parent
c2abd6bf
Exists in
master
and in
1 other branch
Asynchronous run_script_async using asyncio.subprocess.
Asynchronous question generators. Asynchronous correction of textarea questions. Removes 'lines' option in textarea questions (unused in codemirror). The async implementations fixes errors with threading and sqlalchemy.
Showing
10 changed files
with
174 additions
and
68 deletions
Show diff stats
BUGS.md
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) | ||
5 | +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | ||
6 | +- devia mostrar timout para o aluno saber a razao. | ||
4 | - permitir configuracao para escolher entre static files locais ou remotos | 7 | - permitir configuracao para escolher entre static files locais ou remotos |
5 | - sqlalchemy.pool.impl.NullPool: Exception during reset or similar | 8 | - sqlalchemy.pool.impl.NullPool: Exception during reset or similar |
6 | sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. | 9 | sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. |
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: |
@@ -102,7 +102,7 @@ class LearnApp(object): | @@ -102,7 +102,7 @@ class LearnApp(object): | ||
102 | continue # to next test | 102 | continue # to next test |
103 | 103 | ||
104 | if errors > 0: | 104 | if errors > 0: |
105 | - logger.info(f'{errors:>6} errors found.') | 105 | + logger.error(f'{errors:>6} errors found.') |
106 | raise LearnException('Sanity checks') | 106 | raise LearnException('Sanity checks') |
107 | else: | 107 | else: |
108 | logger.info('No errors found.') | 108 | logger.info('No errors found.') |
@@ -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
@@ -4,12 +4,11 @@ import random | @@ -4,12 +4,11 @@ import random | ||
4 | import re | 4 | import re |
5 | from os import path | 5 | from os import path |
6 | import logging | 6 | import logging |
7 | -import asyncio | ||
8 | from typing import Any, Dict, NewType | 7 | from typing import Any, Dict, NewType |
9 | import uuid | 8 | import uuid |
10 | 9 | ||
11 | # this project | 10 | # this project |
12 | -from .tools import run_script | 11 | +from .tools import run_script, run_script_async |
13 | 12 | ||
14 | # setup logger for this module | 13 | # setup logger for this module |
15 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |
@@ -50,8 +49,9 @@ class Question(dict): | @@ -50,8 +49,9 @@ class Question(dict): | ||
50 | self['grade'] = 0.0 | 49 | self['grade'] = 0.0 |
51 | 50 | ||
52 | async def correct_async(self) -> None: | 51 | async def correct_async(self) -> None: |
53 | - loop = asyncio.get_running_loop() | ||
54 | - await loop.run_in_executor(None, self.correct) | 52 | + self.correct() |
53 | + # loop = asyncio.get_running_loop() | ||
54 | + # await loop.run_in_executor(None, self.correct) | ||
55 | 55 | ||
56 | def set_defaults(self, d: QDict) -> None: | 56 | def set_defaults(self, d: QDict) -> None: |
57 | 'Add k:v pairs from default dict d for nonexistent keys' | 57 | 'Add k:v pairs from default dict d for nonexistent keys' |
@@ -305,7 +305,7 @@ class QuestionNumericInterval(Question): | @@ -305,7 +305,7 @@ class QuestionNumericInterval(Question): | ||
305 | answer = float(self['answer'].replace(',', '.', 1)) | 305 | answer = float(self['answer'].replace(',', '.', 1)) |
306 | except ValueError: | 306 | except ValueError: |
307 | self['comments'] = ('A resposta tem de ser numérica, ' | 307 | self['comments'] = ('A resposta tem de ser numérica, ' |
308 | - 'por exemplo 12.345.') | 308 | + 'por exemplo `12.345`.') |
309 | self['grade'] = 0.0 | 309 | self['grade'] = 0.0 |
310 | else: | 310 | else: |
311 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 | 311 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
@@ -318,7 +318,6 @@ class QuestionTextArea(Question): | @@ -318,7 +318,6 @@ class QuestionTextArea(Question): | ||
318 | text (str) | 318 | text (str) |
319 | correct (str with script to run) | 319 | correct (str with script to run) |
320 | answer (None or an actual answer) | 320 | answer (None or an actual answer) |
321 | - lines (int) | ||
322 | ''' | 321 | ''' |
323 | 322 | ||
324 | # ------------------------------------------------------------------------ | 323 | # ------------------------------------------------------------------------ |
@@ -327,13 +326,12 @@ class QuestionTextArea(Question): | @@ -327,13 +326,12 @@ class QuestionTextArea(Question): | ||
327 | 326 | ||
328 | self.set_defaults(QDict({ | 327 | self.set_defaults(QDict({ |
329 | 'text': '', | 328 | 'text': '', |
330 | - 'lines': 8, | ||
331 | - 'timeout': 5, # seconds | 329 | + 'timeout': 5, # seconds |
332 | 'correct': '', # trying to execute this will fail => grade 0.0 | 330 | 'correct': '', # trying to execute this will fail => grade 0.0 |
333 | 'args': [] | 331 | 'args': [] |
334 | })) | 332 | })) |
335 | 333 | ||
336 | - self['correct'] = path.join(self['path'], self['correct']) # FIXME | 334 | + self['correct'] = path.join(self['path'], self['correct']) |
337 | 335 | ||
338 | # ------------------------------------------------------------------------ | 336 | # ------------------------------------------------------------------------ |
339 | def correct(self) -> None: | 337 | def correct(self) -> None: |
@@ -347,7 +345,39 @@ class QuestionTextArea(Question): | @@ -347,7 +345,39 @@ class QuestionTextArea(Question): | ||
347 | timeout=self['timeout'] | 345 | timeout=self['timeout'] |
348 | ) | 346 | ) |
349 | 347 | ||
350 | - if isinstance(out, dict): | 348 | + if out is None: |
349 | + logger.warning(f'No grade after running "{self["correct"]}".') | ||
350 | + self['grade'] = 0.0 | ||
351 | + elif isinstance(out, dict): | ||
352 | + self['comments'] = out.get('comments', '') | ||
353 | + try: | ||
354 | + self['grade'] = float(out['grade']) | ||
355 | + except ValueError: | ||
356 | + logger.error(f'Output error in "{self["correct"]}".') | ||
357 | + except KeyError: | ||
358 | + logger.error(f'No grade in "{self["correct"]}".') | ||
359 | + else: | ||
360 | + try: | ||
361 | + self['grade'] = float(out) | ||
362 | + except (TypeError, ValueError): | ||
363 | + logger.error(f'Invalid grade in "{self["correct"]}".') | ||
364 | + | ||
365 | + # ------------------------------------------------------------------------ | ||
366 | + async def correct_async(self) -> None: | ||
367 | + super().correct() | ||
368 | + | ||
369 | + if self['answer'] is not None: # correct answer and parse yaml ouput | ||
370 | + out = await run_script_async( | ||
371 | + script=self['correct'], | ||
372 | + args=self['args'], | ||
373 | + stdin=self['answer'], | ||
374 | + timeout=self['timeout'] | ||
375 | + ) | ||
376 | + | ||
377 | + if out is None: | ||
378 | + logger.warning(f'No grade after running "{self["correct"]}".') | ||
379 | + self['grade'] = 0.0 | ||
380 | + elif isinstance(out, dict): | ||
351 | self['comments'] = out.get('comments', '') | 381 | self['comments'] = out.get('comments', '') |
352 | try: | 382 | try: |
353 | self['grade'] = float(out['grade']) | 383 | self['grade'] = float(out['grade']) |
@@ -372,7 +402,6 @@ class QuestionInformation(Question): | @@ -372,7 +402,6 @@ class QuestionInformation(Question): | ||
372 | })) | 402 | })) |
373 | 403 | ||
374 | # ------------------------------------------------------------------------ | 404 | # ------------------------------------------------------------------------ |
375 | - # can return negative values for wrong answers | ||
376 | def correct(self) -> None: | 405 | def correct(self) -> None: |
377 | super().correct() | 406 | super().correct() |
378 | self['grade'] = 1.0 # always "correct" but points should be zero! | 407 | self['grade'] = 1.0 # always "correct" but points should be zero! |
@@ -400,7 +429,7 @@ class QuestionInformation(Question): | @@ -400,7 +429,7 @@ class QuestionInformation(Question): | ||
400 | # # answer one question and correct it | 429 | # # answer one question and correct it |
401 | # question['answer'] = 42 # set answer | 430 | # question['answer'] = 42 # set answer |
402 | # question.correct() # correct answer | 431 | # question.correct() # correct answer |
403 | -# print(question['grade']) # print grade | 432 | +# grade = question['grade'] # get grade |
404 | # =========================================================================== | 433 | # =========================================================================== |
405 | class QFactory(object): | 434 | class QFactory(object): |
406 | # 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 |
@@ -427,7 +456,7 @@ class QFactory(object): | @@ -427,7 +456,7 @@ class QFactory(object): | ||
427 | # i.e. a question object (radio, checkbox, ...). | 456 | # i.e. a question object (radio, checkbox, ...). |
428 | # ----------------------------------------------------------------------- | 457 | # ----------------------------------------------------------------------- |
429 | def generate(self) -> Question: | 458 | def generate(self) -> Question: |
430 | - logger.debug(f'Generating "{self.question["ref"]}"...') | 459 | + logger.debug(f'[generate] "{self.question["ref"]}"...') |
431 | # Shallow copy so that script generated questions will not replace | 460 | # Shallow copy so that script generated questions will not replace |
432 | # the original generators | 461 | # the original generators |
433 | q = self.question.copy() | 462 | q = self.question.copy() |
@@ -439,7 +468,7 @@ class QFactory(object): | @@ -439,7 +468,7 @@ class QFactory(object): | ||
439 | if q['type'] == 'generator': | 468 | if q['type'] == 'generator': |
440 | logger.debug(f' \\_ Running "{q["script"]}".') | 469 | logger.debug(f' \\_ Running "{q["script"]}".') |
441 | q.setdefault('args', []) | 470 | q.setdefault('args', []) |
442 | - q.setdefault('stdin', '') # FIXME does not exist anymore? | 471 | + q.setdefault('stdin', '') # FIXME is it really necessary? |
443 | script = path.join(q['path'], q['script']) | 472 | script = path.join(q['path'], q['script']) |
444 | out = run_script(script=script, args=q['args'], stdin=q['stdin']) | 473 | out = run_script(script=script, args=q['args'], stdin=q['stdin']) |
445 | q.update(out) | 474 | q.update(out) |
@@ -455,3 +484,36 @@ class QFactory(object): | @@ -455,3 +484,36 @@ class QFactory(object): | ||
455 | raise | 484 | raise |
456 | else: | 485 | else: |
457 | 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
1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
2 | 2 | ||
3 | # python standard library | 3 | # python standard library |
4 | -import sys | 4 | +import argparse |
5 | +import asyncio | ||
5 | import base64 | 6 | import base64 |
6 | -import uuid | 7 | +import functools |
7 | import logging.config | 8 | import logging.config |
8 | -import argparse | ||
9 | import mimetypes | 9 | import mimetypes |
10 | +from os import path, environ | ||
10 | import signal | 11 | import signal |
11 | -import functools | ||
12 | import ssl | 12 | import ssl |
13 | -import asyncio | ||
14 | -from os import path, environ | 13 | +import sys |
14 | +import uuid | ||
15 | + | ||
15 | 16 | ||
16 | # third party libraries | 17 | # third party libraries |
17 | import tornado.ioloop | 18 | import tornado.ioloop |
@@ -253,7 +254,7 @@ class QuestionHandler(BaseHandler): | @@ -253,7 +254,7 @@ class QuestionHandler(BaseHandler): | ||
253 | # --- get question to render | 254 | # --- get question to render |
254 | @tornado.web.authenticated | 255 | @tornado.web.authenticated |
255 | def get(self): | 256 | def get(self): |
256 | - logging.debug('QuestionHandler.get()') | 257 | + logging.debug('[QuestionHandler.get]') |
257 | user = self.current_user | 258 | user = self.current_user |
258 | q = self.learn.get_current_question(user) | 259 | q = self.learn.get_current_question(user) |
259 | 260 | ||
@@ -284,7 +285,7 @@ class QuestionHandler(BaseHandler): | @@ -284,7 +285,7 @@ class QuestionHandler(BaseHandler): | ||
284 | # --- post answer, returns what to do next: shake, new_question, finished | 285 | # --- post answer, returns what to do next: shake, new_question, finished |
285 | @tornado.web.authenticated | 286 | @tornado.web.authenticated |
286 | async def post(self) -> None: | 287 | async def post(self) -> None: |
287 | - logging.debug('QuestionHandler.post()') | 288 | + logging.debug('[QuestionHandler.post]') |
288 | user = self.current_user | 289 | user = self.current_user |
289 | answer = self.get_body_arguments('answer') # list | 290 | answer = self.get_body_arguments('answer') # list |
290 | 291 |
aprendizations/templates/question-textarea.html
@@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
2 | 2 | ||
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" name="answer" id="code" autofocus>{{ question['answer'] or '' }}</textarea><br /> |
6 | <input type="hidden" name="qid" value="{{ question['qid'] }}"> | 6 | <input type="hidden" name="qid" value="{{ question['qid'] }}"> |
7 | 7 | ||
8 | <script> | 8 | <script> |
aprendizations/tools.py
1 | 1 | ||
2 | # python standard library | 2 | # python standard library |
3 | -from os import path | ||
4 | -import subprocess | 3 | +import asyncio |
5 | import logging | 4 | import logging |
5 | +from os import path | ||
6 | import re | 6 | import re |
7 | +import subprocess | ||
7 | from typing import Any, List | 8 | from typing import Any, List |
8 | 9 | ||
9 | # third party libraries | 10 | # third party libraries |
10 | -import yaml | ||
11 | import mistune | 11 | import mistune |
12 | from pygments import highlight | 12 | from pygments import highlight |
13 | from pygments.lexers import get_lexer_by_name | 13 | from pygments.lexers import get_lexer_by_name |
14 | from pygments.formatters import HtmlFormatter | 14 | from pygments.formatters import HtmlFormatter |
15 | +import yaml | ||
16 | + | ||
15 | 17 | ||
16 | # setup logger for this module | 18 | # setup logger for this module |
17 | logger = logging.getLogger(__name__) | 19 | logger = logging.getLogger(__name__) |
@@ -101,7 +103,7 @@ class HighlightRenderer(mistune.Renderer): | @@ -101,7 +103,7 @@ class HighlightRenderer(mistune.Renderer): | ||
101 | 103 | ||
102 | def table(self, header, body): | 104 | def table(self, header, body): |
103 | return '<table class="table table-sm"><thead class="thead-light">' \ | 105 | return '<table class="table table-sm"><thead class="thead-light">' \ |
104 | - + header + '</thead><tbody>' + body + "</tbody></table>" | 106 | + + header + '</thead><tbody>' + body + '</tbody></table>' |
105 | 107 | ||
106 | def image(self, src, title, alt): | 108 | def image(self, src, title, alt): |
107 | alt = mistune.escape(alt, quote=True) | 109 | alt = mistune.escape(alt, quote=True) |
@@ -168,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any: | @@ -168,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any: | ||
168 | def run_script(script: str, | 170 | def run_script(script: str, |
169 | args: List[str] = [], | 171 | args: List[str] = [], |
170 | stdin: str = '', | 172 | stdin: str = '', |
171 | - timeout: int = 5) -> Any: | 173 | + timeout: int = 2) -> Any: |
172 | 174 | ||
173 | script = path.expanduser(script) | 175 | script = path.expanduser(script) |
174 | try: | 176 | try: |
@@ -200,3 +202,41 @@ def run_script(script: str, | @@ -200,3 +202,41 @@ def run_script(script: str, | ||
200 | logger.error(f'Error parsing yaml output of "{script}"') | 202 | logger.error(f'Error parsing yaml output of "{script}"') |
201 | else: | 203 | else: |
202 | return output | 204 | return output |
205 | + | ||
206 | + | ||
207 | +# ---------------------------------------------------------------------------- | ||
208 | +# Same as above, but asynchronous | ||
209 | +# ---------------------------------------------------------------------------- | ||
210 | +async def run_script_async(script: str, | ||
211 | + args: List[str] = [], | ||
212 | + stdin: str = '', | ||
213 | + timeout: int = 2) -> Any: | ||
214 | + | ||
215 | + script = path.expanduser(script) | ||
216 | + args = [str(a) for a in args] | ||
217 | + | ||
218 | + p = await asyncio.create_subprocess_exec( | ||
219 | + script, *args, | ||
220 | + stdin=asyncio.subprocess.PIPE, | ||
221 | + stdout=asyncio.subprocess.PIPE, | ||
222 | + stderr=asyncio.subprocess.DEVNULL, | ||
223 | + ) | ||
224 | + | ||
225 | + try: | ||
226 | + stdout, stderr = await asyncio.wait_for( | ||
227 | + p.communicate(input=stdin.encode('utf-8')), | ||
228 | + timeout=timeout | ||
229 | + ) | ||
230 | + except asyncio.TimeoutError: | ||
231 | + logger.warning(f'Timeout {timeout}s running script "{script}".') | ||
232 | + return | ||
233 | + | ||
234 | + if p.returncode != 0: | ||
235 | + logger.error(f'Return code {p.returncode} running "{script}".') | ||
236 | + else: | ||
237 | + try: | ||
238 | + output = yaml.safe_load(stdout.decode('utf-8', 'ignore')) | ||
239 | + except Exception: | ||
240 | + logger.error(f'Error parsing yaml output of "{script}"') | ||
241 | + else: | ||
242 | + return output |
demo/math/questions.yaml
@@ -16,6 +16,7 @@ | @@ -16,6 +16,7 @@ | ||
16 | - $2\times(9-2\times 3)$ | 16 | - $2\times(9-2\times 3)$ |
17 | correct: [1, 1, 0, 0, 0] | 17 | correct: [1, 1, 0, 0, 0] |
18 | choose: 3 | 18 | choose: 3 |
19 | + max_tries: 1 | ||
19 | solution: | | 20 | solution: | |
20 | Colocando o 2 em evidência, obtém-se a resposta correcta $2\times(9 - 3)$ | 21 | Colocando o 2 em evidência, obtém-se a resposta correcta $2\times(9 - 3)$ |
21 | ou $(9-3)\times 2$. | 22 | ou $(9-3)\times 2$. |
@@ -50,6 +51,7 @@ | @@ -50,6 +51,7 @@ | ||
50 | - 12 | 51 | - 12 |
51 | - 14 | 52 | - 14 |
52 | - 1, a **unidade** | 53 | - 1, a **unidade** |
54 | + max_tries: 1 | ||
53 | solution: | | 55 | solution: | |
54 | O único número primo é o 13. | 56 | O único número primo é o 13. |
55 | 57 | ||
@@ -76,9 +78,10 @@ | @@ -76,9 +78,10 @@ | ||
76 | números negativos são menores que os positivos. | 78 | números negativos são menores que os positivos. |
77 | 79 | ||
78 | # --------------------------------------------------------------------------- | 80 | # --------------------------------------------------------------------------- |
81 | +# the program should print a question in yaml format. The args will be | ||
82 | +# sent as command line options when the program is run. | ||
79 | - type: generator | 83 | - type: generator |
80 | ref: overflow | 84 | ref: overflow |
81 | script: gen-multiples-of-3.py | 85 | script: gen-multiples-of-3.py |
82 | args: [11, 120] | 86 | args: [11, 120] |
83 | - # the program should print a question in yaml format. The args will be | ||
84 | - # sent as command line options when the program is run. | 87 | + choose: 3 |
demo/solar_system/correct-first_3_planets.py
@@ -2,26 +2,27 @@ | @@ -2,26 +2,27 @@ | ||
2 | 2 | ||
3 | import re | 3 | import re |
4 | import sys | 4 | import sys |
5 | +import time | ||
5 | 6 | ||
6 | s = sys.stdin.read() | 7 | s = sys.stdin.read() |
7 | 8 | ||
8 | -# set of words converted to lowercase | ||
9 | -answer = set(re.findall(r'[\w]+', s.lower())) | ||
10 | -answer.difference_update({'e', 'a', 'planeta', 'planetas'}) # ignore these | 9 | +ans = set(re.findall(r'[\w]+', s.lower())) # convert answer to lowercase |
10 | +ans.difference_update({'e', 'a', 'o', 'planeta', 'planetas'}) # ignore words | ||
11 | 11 | ||
12 | # correct set of planets | 12 | # correct set of planets |
13 | planets = {'mercúrio', 'vénus', 'terra'} | 13 | planets = {'mercúrio', 'vénus', 'terra'} |
14 | 14 | ||
15 | -correct = set.intersection(answer, planets) # the ones I got right | ||
16 | -wrong = set.difference(answer, planets) # the ones I got wrong | 15 | +correct = set.intersection(ans, planets) # the ones I got right |
16 | +wrong = set.difference(ans, planets) # the ones I got wrong | ||
17 | 17 | ||
18 | grade = (len(correct) - len(wrong)) / len(planets) | 18 | grade = (len(correct) - len(wrong)) / len(planets) |
19 | 19 | ||
20 | +comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | ||
21 | + | ||
20 | out = f'''--- | 22 | out = f'''--- |
21 | -grade: {grade}''' | 23 | +grade: {grade} |
24 | +comments: {comments}''' | ||
22 | 25 | ||
23 | -if grade < 1.0: | ||
24 | - out += '\ncomments: Vou dar uma ajuda, as iniciais são M, V e T...' | 26 | +time.sleep(2) # simulate computation time (may generate timeout) |
25 | 27 | ||
26 | print(out) | 28 | print(out) |
27 | -exit(0) |
demo/solar_system/questions.yaml
@@ -64,7 +64,6 @@ | @@ -64,7 +64,6 @@ | ||
64 | # correct: correct-timeout.py | 64 | # correct: correct-timeout.py |
65 | # opcional | 65 | # opcional |
66 | answer: Vulcano, Krypton, Plutão | 66 | answer: Vulcano, Krypton, Plutão |
67 | - lines: 3 | ||
68 | - timeout: 50 | 67 | + timeout: 3 |
69 | solution: | | 68 | solution: | |
70 | Os 3 planetas mais perto do Sol são: Mercúrio, Vénus e Terra. | 69 | Os 3 planetas mais perto do Sol são: Mercúrio, Vénus e Terra. |