diff --git a/BUGS.md b/BUGS.md index bf2d5fd..483457c 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,6 +1,8 @@ # BUGS +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) +- devia mostrar timout para o aluno saber a razao. - permitir configuracao para escolher entre static files locais ou remotos - sqlalchemy.pool.impl.NullPool: Exception during reset or similar sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread. diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index 02df6d5..ae9950c 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -102,7 +102,7 @@ class LearnApp(object): continue # to next test if errors > 0: - logger.info(f'{errors:>6} errors found.') + logger.error(f'{errors:>6} errors found.') raise LearnException('Sanity checks') else: logger.info('No errors found.') diff --git a/aprendizations/questions.py b/aprendizations/questions.py index 3bbc7df..81128e5 100644 --- a/aprendizations/questions.py +++ b/aprendizations/questions.py @@ -4,12 +4,11 @@ import random import re from os import path import logging -import asyncio from typing import Any, Dict, NewType import uuid # this project -from .tools import run_script +from .tools import run_script, run_script_async # setup logger for this module logger = logging.getLogger(__name__) @@ -50,8 +49,9 @@ class Question(dict): self['grade'] = 0.0 async def correct_async(self) -> None: - loop = asyncio.get_running_loop() - await loop.run_in_executor(None, self.correct) + self.correct() + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(None, self.correct) def set_defaults(self, d: QDict) -> None: 'Add k:v pairs from default dict d for nonexistent keys' @@ -305,7 +305,7 @@ class QuestionNumericInterval(Question): answer = float(self['answer'].replace(',', '.', 1)) except ValueError: self['comments'] = ('A resposta tem de ser numérica, ' - 'por exemplo 12.345.') + 'por exemplo `12.345`.') self['grade'] = 0.0 else: self['grade'] = 1.0 if lower <= answer <= upper else 0.0 @@ -327,13 +327,13 @@ class QuestionTextArea(Question): self.set_defaults(QDict({ 'text': '', - 'lines': 8, + # 'lines': 8, 'timeout': 5, # seconds 'correct': '', # trying to execute this will fail => grade 0.0 'args': [] })) - self['correct'] = path.join(self['path'], self['correct']) # FIXME + self['correct'] = path.join(self['path'], self['correct']) # ------------------------------------------------------------------------ def correct(self) -> None: @@ -347,7 +347,39 @@ class QuestionTextArea(Question): timeout=self['timeout'] ) - if isinstance(out, dict): + if out is None: + logger.warning(f'No grade after running "{self["correct"]}".') + self['grade'] = 0.0 + elif isinstance(out, dict): + self['comments'] = out.get('comments', '') + try: + self['grade'] = float(out['grade']) + except ValueError: + logger.error(f'Output error in "{self["correct"]}".') + except KeyError: + logger.error(f'No grade in "{self["correct"]}".') + else: + try: + self['grade'] = float(out) + except (TypeError, ValueError): + logger.error(f'Invalid grade in "{self["correct"]}".') + + # ------------------------------------------------------------------------ + async def correct_async(self) -> None: + super().correct() + + if self['answer'] is not None: # correct answer and parse yaml ouput + out = await run_script_async( + script=self['correct'], + args=self['args'], + stdin=self['answer'], + timeout=self['timeout'] + ) + + if out is None: + logger.warning(f'No grade after running "{self["correct"]}".') + self['grade'] = 0.0 + elif isinstance(out, dict): self['comments'] = out.get('comments', '') try: self['grade'] = float(out['grade']) @@ -372,7 +404,6 @@ class QuestionInformation(Question): })) # ------------------------------------------------------------------------ - # can return negative values for wrong answers def correct(self) -> None: super().correct() self['grade'] = 1.0 # always "correct" but points should be zero! diff --git a/aprendizations/serve.py b/aprendizations/serve.py index d0ebd6c..6822962 100644 --- a/aprendizations/serve.py +++ b/aprendizations/serve.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # python standard library -import sys +import argparse +import asyncio import base64 -import uuid +import functools import logging.config -import argparse import mimetypes +from os import path, environ import signal -import functools import ssl -import asyncio -from os import path, environ +import sys +import uuid # third party libraries import tornado.ioloop @@ -20,9 +20,9 @@ import tornado.web from tornado.escape import to_unicode # this project +from . import APP_NAME from .learnapp import LearnApp from .tools import load_yaml, md_to_html -from . import APP_NAME # ---------------------------------------------------------------------------- diff --git a/aprendizations/tools.py b/aprendizations/tools.py index d08d1c1..b91a239 100644 --- a/aprendizations/tools.py +++ b/aprendizations/tools.py @@ -1,17 +1,18 @@ # python standard library -from os import path -import subprocess +import asyncio import logging +from os import path import re +import subprocess from typing import Any, List # third party libraries -import yaml import mistune from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter +import yaml # setup logger for this module logger = logging.getLogger(__name__) @@ -100,14 +101,14 @@ class HighlightRenderer(mistune.Renderer): return highlight(code, lexer, formatter) def table(self, header, body): - return '' \ - + header + '' + body + "
" + return (f'' + f'{header}{body}
') def image(self, src, title, alt): alt = mistune.escape(alt, quote=True) title = mistune.escape(title or '', quote=True) - return f'' + return (f'') # Pass math through unaltered - mathjax does the rendering in the browser def block_math(self, text): @@ -200,3 +201,41 @@ def run_script(script: str, logger.error(f'Error parsing yaml output of "{script}"') else: return output + + +# ---------------------------------------------------------------------------- +# Same as above, but asynchronous +# ---------------------------------------------------------------------------- +async def run_script_async(script: str, + args: List[str] = [], + stdin: str = '', + timeout: int = 5) -> Any: + + script = path.expanduser(script) + cmd = [script] + [str(a) for a in args] + + p = await asyncio.create_subprocess_shell( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, + ) + + try: + stdout, stderr = await asyncio.wait_for( + p.communicate(input=stdin.encode('utf-8')), + timeout=timeout + ) + except asyncio.TimeoutError: + logger.warning(f'Timeout {timeout}s running script "{script}".') + return + + if p.returncode != 0: + logger.error(f'Return code {p.returncode} running "{script}".') + else: + try: + output = yaml.safe_load(stdout.decode('utf-8')) + except Exception: + logger.error(f'Error parsing yaml output of "{script}"') + else: + return output diff --git a/demo/math/questions.yaml b/demo/math/questions.yaml index c354cf6..4ac167f 100644 --- a/demo/math/questions.yaml +++ b/demo/math/questions.yaml @@ -16,6 +16,7 @@ - $2\times(9-2\times 3)$ correct: [1, 1, 0, 0, 0] choose: 3 + max_tries: 1 solution: | Colocando o 2 em evidência, obtém-se a resposta correcta $2\times(9 - 3)$ ou $(9-3)\times 2$. @@ -50,6 +51,7 @@ - 12 - 14 - 1, a **unidade** + max_tries: 1 solution: | O único número primo é o 13. @@ -76,9 +78,10 @@ números negativos são menores que os positivos. # --------------------------------------------------------------------------- +# the program should print a question in yaml format. The args will be +# sent as command line options when the program is run. - type: generator ref: overflow script: gen-multiples-of-3.py args: [11, 120] - # the program should print a question in yaml format. The args will be - # sent as command line options when the program is run. + choose: 3 diff --git a/demo/solar_system/questions.yaml b/demo/solar_system/questions.yaml index 17a0094..5c2feab 100644 --- a/demo/solar_system/questions.yaml +++ b/demo/solar_system/questions.yaml @@ -64,7 +64,6 @@ # correct: correct-timeout.py # opcional answer: Vulcano, Krypton, Plutão - lines: 3 - timeout: 50 + timeout: 3 solution: | Os 3 planetas mais perto do Sol são: Mercúrio, Vénus e Terra. -- libgit2 0.21.2