diff --git a/BUGS.md b/BUGS.md
index bf2d5fd..05cfa53 100644
--- a/BUGS.md
+++ b/BUGS.md
@@ -1,6 +1,9 @@
# BUGS
+- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo)
+- 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/knowledge.py b/aprendizations/knowledge.py
index cb774c6..ced124b 100644
--- a/aprendizations/knowledge.py
+++ b/aprendizations/knowledge.py
@@ -58,7 +58,7 @@ class StudentKnowledge(object):
'level': 0.0, # unlocked
'date': datetime.now()
}
- logger.debug(f'Unlocked "{topic}".')
+ logger.debug(f'[unlock_topics] Unlocked "{topic}".')
# else: # lock this topic if deps do not satisfy min_level
# del self.state[topic]
@@ -68,7 +68,7 @@ class StudentKnowledge(object):
# current_question: the current question to be presented
# ------------------------------------------------------------------------
async def start_topic(self, topic):
- logger.debug('StudentKnowledge.start_topic()')
+ logger.debug(f'[start_topic] topic "{topic}"')
if self.current_topic == topic:
logger.info('Restarting current topic is not allowed.')
@@ -76,7 +76,7 @@ class StudentKnowledge(object):
# do not allow locked topics
if self.is_locked(topic):
- logger.debug(f'Topic {topic} is locked')
+ logger.debug(f'[start_topic] topic "{topic}" is locked')
return False
# starting new topic
@@ -90,23 +90,20 @@ class StudentKnowledge(object):
questions = random.sample(t['questions'], k=k)
else:
questions = t['questions'][:k]
- logger.debug(f'Questions: {", ".join(questions)}')
+ logger.debug(f'[start_topic] questions: {", ".join(questions)}')
- # generate instances of questions
+ # synchronous
+ # self.questions = [self.factory[ref].generate()
+ # for ref in questions]
- # synchronous:
- # self.questions = [self.factory[ref].generate() for ref in questions]
-
- # async:
- loop = asyncio.get_running_loop()
- generators = [loop.run_in_executor(None, self.factory[qref].generate)
- for qref in questions]
- self.questions = await asyncio.gather(*generators)
+ # asynchronous:
+ self.questions = [await self.factory[ref].generate_async()
+ for ref in questions]
# get first question
self.next_question()
- logger.debug(f'Generated {len(self.questions)} questions')
+ logger.debug(f'[start_topic] generated {len(self.questions)} questions')
return True
# ------------------------------------------------------------------------
@@ -115,7 +112,7 @@ class StudentKnowledge(object):
# The current topic is unchanged.
# ------------------------------------------------------------------------
def finish_topic(self):
- logger.debug(f'StudentKnowledge.finish_topic({self.current_topic})')
+ logger.debug(f'[finish_topic] current_topic {self.current_topic}')
self.state[self.current_topic] = {
'date': datetime.now(),
@@ -132,13 +129,13 @@ class StudentKnowledge(object):
# - if wrong, counts number of tries. If exceeded, moves on.
# ------------------------------------------------------------------------
async def check_answer(self, answer):
- logger.debug('StudentKnowledge.check_answer()')
+ logger.debug('[check_answer]')
q = self.current_question
q['answer'] = answer
q['finish_time'] = datetime.now()
await q.correct_async()
- logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})')
+ logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
if q['grade'] > 0.999:
self.correct_answers += 1
@@ -154,7 +151,7 @@ class StudentKnowledge(object):
else:
action = 'wrong'
if self.current_question['append_wrong']:
- logger.debug("Append new instance of question at the end")
+ logger.debug('[check_answer] Wrong, append new instance')
self.questions.append(self.factory[q['ref']].generate())
self.next_question()
@@ -175,7 +172,7 @@ class StudentKnowledge(object):
default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
maxtries = self.current_question.get('max_tries', default_maxtries)
self.current_question['tries'] = maxtries
- logger.debug(f'Next question is "{self.current_question["ref"]}"')
+ logger.debug(f'[next_question] "{self.current_question["ref"]}"')
return self.current_question # question or None
diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py
index 02df6d5..8162b3a 100644
--- a/aprendizations/learnapp.py
+++ b/aprendizations/learnapp.py
@@ -75,7 +75,7 @@ class LearnApp(object):
errors = 0
for qref in self.factory:
- logger.debug(f'Checking "{qref}"...')
+ logger.debug(f'[sanity_check_questions] Checking "{qref}"...')
try:
q = self.factory[qref].generate()
except Exception:
@@ -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.')
@@ -204,7 +204,7 @@ class LearnApp(object):
finishtime=str(q['finish_time']),
student_id=uid,
topic_id=topic))
- logger.debug(f'Saved "{q["ref"]}" into database')
+ logger.debug(f'[check_answer] Saved "{q["ref"]}" into database')
if knowledge.topic_has_finished():
# finished topic, save into database
@@ -218,7 +218,7 @@ class LearnApp(object):
.one_or_none()
if a is None:
# insert new studenttopic into database
- logger.debug('Database insert new studenttopic')
+ logger.debug('[check_answer] Database insert studenttopic')
t = s.query(Topic).get(topic)
u = s.query(Student).get(uid)
# association object
@@ -227,13 +227,13 @@ class LearnApp(object):
u.topics.append(a)
else:
# update studenttopic in database
- logger.debug('Database update studenttopic')
+ logger.debug('[check_answer] Database update studenttopic')
a.level = level
a.date = date
s.add(a)
- logger.debug(f'Saved topic "{topic}" into database')
+ logger.debug(f'[check_answer] Saved topic "{topic}" into database')
return q, action
@@ -244,8 +244,8 @@ class LearnApp(object):
student = self.online[uid]['state']
try:
await student.start_topic(topic)
- except Exception:
- logger.warning(f'User "{uid}" could not start topic "{topic}"')
+ except Exception as e:
+ logger.warning(f'User "{uid}" couldn\'t start "{topic}": {e}')
else:
logger.info(f'User "{uid}" started topic "{topic}"')
diff --git a/aprendizations/questions.py b/aprendizations/questions.py
index 3bbc7df..65c7635 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
@@ -318,7 +318,6 @@ class QuestionTextArea(Question):
text (str)
correct (str with script to run)
answer (None or an actual answer)
- lines (int)
'''
# ------------------------------------------------------------------------
@@ -327,13 +326,12 @@ class QuestionTextArea(Question):
self.set_defaults(QDict({
'text': '',
- 'lines': 8,
- 'timeout': 5, # seconds
+ '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 +345,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 +402,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!
@@ -400,7 +429,7 @@ class QuestionInformation(Question):
# # answer one question and correct it
# question['answer'] = 42 # set answer
# question.correct() # correct answer
-# print(question['grade']) # print grade
+# grade = question['grade'] # get grade
# ===========================================================================
class QFactory(object):
# Depending on the type of question, a different question class will be
@@ -427,7 +456,7 @@ class QFactory(object):
# i.e. a question object (radio, checkbox, ...).
# -----------------------------------------------------------------------
def generate(self) -> Question:
- logger.debug(f'Generating "{self.question["ref"]}"...')
+ logger.debug(f'[generate] "{self.question["ref"]}"...')
# Shallow copy so that script generated questions will not replace
# the original generators
q = self.question.copy()
@@ -439,7 +468,7 @@ class QFactory(object):
if q['type'] == 'generator':
logger.debug(f' \\_ Running "{q["script"]}".')
q.setdefault('args', [])
- q.setdefault('stdin', '') # FIXME does not exist anymore?
+ q.setdefault('stdin', '') # FIXME is it really necessary?
script = path.join(q['path'], q['script'])
out = run_script(script=script, args=q['args'], stdin=q['stdin'])
q.update(out)
@@ -455,3 +484,36 @@ class QFactory(object):
raise
else:
return qinstance
+
+ # -----------------------------------------------------------------------
+ async def generate_async(self) -> Question:
+ logger.debug(f'[generate_async] "{self.question["ref"]}"...')
+ # Shallow copy so that script generated questions will not replace
+ # the original generators
+ q = self.question.copy()
+ q['qid'] = str(uuid.uuid4()) # unique for each generated question
+
+ # If question is of generator type, an external program will be run
+ # which will print a valid question in yaml format to stdout. This
+ # output is then yaml parsed into a dictionary `q`.
+ if q['type'] == 'generator':
+ logger.debug(f' \\_ Running "{q["script"]}".')
+ q.setdefault('args', [])
+ q.setdefault('stdin', '') # FIXME is it really necessary?
+ script = path.join(q['path'], q['script'])
+ out = await run_script_async(script=script, args=q['args'],
+ stdin=q['stdin'])
+ q.update(out)
+
+ # Finally we create an instance of Question()
+ try:
+ qinstance = self._types[q['type']](QDict(q)) # of matching class
+ except QuestionException as e:
+ logger.error(e)
+ raise e
+ except KeyError:
+ logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
+ raise
+ else:
+ logger.debug(f'[generate_async] Done instance of {q["ref"]}')
+ return qinstance
diff --git a/aprendizations/serve.py b/aprendizations/serve.py
index d0ebd6c..7e0601d 100644
--- a/aprendizations/serve.py
+++ b/aprendizations/serve.py
@@ -1,17 +1,18 @@
#!/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
@@ -253,7 +254,7 @@ class QuestionHandler(BaseHandler):
# --- get question to render
@tornado.web.authenticated
def get(self):
- logging.debug('QuestionHandler.get()')
+ logging.debug('[QuestionHandler.get]')
user = self.current_user
q = self.learn.get_current_question(user)
@@ -284,7 +285,7 @@ class QuestionHandler(BaseHandler):
# --- post answer, returns what to do next: shake, new_question, finished
@tornado.web.authenticated
async def post(self) -> None:
- logging.debug('QuestionHandler.post()')
+ logging.debug('[QuestionHandler.post]')
user = self.current_user
answer = self.get_body_arguments('answer') # list
diff --git a/aprendizations/templates/question-textarea.html b/aprendizations/templates/question-textarea.html
index f7063ac..9b67248 100644
--- a/aprendizations/templates/question-textarea.html
+++ b/aprendizations/templates/question-textarea.html
@@ -2,7 +2,7 @@
{% block answer %}
-
+