diff --git a/BUGS.md b/BUGS.md index 2ea1eeb..04af846 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,18 +1,16 @@ # BUGS -- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado. -- change password modal nao aparece no ipad (safari e firefox) - on start topic, logs show questionhandler.get() twice. +- change password modal nao aparece no ipad (safari e firefox) - detect questions in questions.yaml without ref -> error ou generate default. -- Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos... -- generators not working: bcrypt (ver blog) - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. # TODO -- session management. close after inactive time. +- Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos... - each topic only loads a sample of K questions (max) in random order. +- session management. close after inactive time. - radio e checkboxes, aceitar numeros como seleccao das opcoes. - reload das perguntas enquanto online. ver signal em http://stackabuse.com/python-async-await-tutorial/ - pertuntas tipo tristate: (sim, não, não sei @@ -29,6 +27,7 @@ # FIXED +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado. - servir imagens/ficheiros. - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. diff --git a/demo/solar_system/questions.yaml b/demo/solar_system/questions.yaml index 63bd8bd..0525d08 100644 --- a/demo/solar_system/questions.yaml +++ b/demo/solar_system/questions.yaml @@ -6,7 +6,7 @@ type: radio title: Sistema solar text: | - ![planetas](planetsa.png " Planetas do Sistema Solar") + ![planetas](planets.png " Planetas do Sistema Solar") Qual é o maior planeta do Sistema Solar? options: diff --git a/learnapp.py b/learnapp.py index 03c3414..0977226 100644 --- a/learnapp.py +++ b/learnapp.py @@ -152,7 +152,7 @@ class LearnApp(object): try: ok = self.online[uid]['state'].init_topic(topic) except KeyError as e: - logger.warning(f'User "{uid}" denied nonexistent "{topic}"') + logger.warning(f'User "{uid}" tried to open nonexistent topic: "{topic}"') raise e else: if ok: diff --git a/serve.py b/serve.py index a735fb0..8aae900 100755 --- a/serve.py +++ b/serve.py @@ -7,6 +7,7 @@ import base64 import uuid import logging.config import argparse +from concurrent.futures import ThreadPoolExecutor # user installed libraries import tornado.ioloop @@ -14,6 +15,9 @@ import tornado.web import tornado.httpserver from tornado import template #, gen +from tornado.concurrent import run_on_executor +from tornado.platform.asyncio import to_tornado_future + # this project from learnapp import LearnApp from tools import load_yaml, md_to_html @@ -142,15 +146,18 @@ class TopicHandler(BaseHandler): else: self.redirect('/') + # ---------------------------------------------------------------------------- +# Serves files from the /public subdir of the topics. # Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/ +# ---------------------------------------------------------------------------- class FileHandler(BaseHandler): @tornado.web.authenticated async def get(self, filename): uid = self.current_user public_dir = self.learn.get_current_public_dir(uid) filepath = path.expanduser(path.join(public_dir, filename)) - chunk_size = 1024 * 1024 # serve 1MiB multiple times + chunk_size = 1024 * 1024 # serve up to 1MiB multiple times try: f = open(filepath, 'rb') except FileNotFoundError: @@ -159,9 +166,8 @@ class FileHandler(BaseHandler): logging.error(f'No permission: {filepath}') else: with f: - while True: - chunk = f.read(chunk_size) - if not chunk: break + chunk = f.read(chunk_size) + while chunk: try: self.write(chunk) # write the cunk to response await self.flush() # flush the current chunk to socket @@ -172,12 +178,15 @@ class FileHandler(BaseHandler): del chunk await gen.sleep(0.000000001) # 1 nanosecond (hack) # in tornnado 5.0 use `await asyncio.sleep(0)` instead + chunk = f.read(chunk_size) # ---------------------------------------------------------------------------- # respond to AJAX to get a JSON question # ---------------------------------------------------------------------------- class QuestionHandler(BaseHandler): + executor = ThreadPoolExecutor(max_workers=2) + templates = { 'checkbox': 'question-checkbox.html', 'radio': 'question-radio.html', @@ -194,6 +203,13 @@ class QuestionHandler(BaseHandler): # 'alert': '', FIXME } + # Blocking function to be run on the executor + @run_on_executor() + def check_answer(self, user, answer): + return self.learn.check_answer(user, answer) + + + # --- get question to render @tornado.web.authenticated def get(self): logging.debug('QuestionHandler.get()') @@ -211,9 +227,9 @@ class QuestionHandler(BaseHandler): } }) - # handles answer posted + # --- post answer, returns what to do next: shake, new_question, finished @tornado.web.authenticated - def post(self): + async def post(self): logging.debug('QuestionHandler.post()') user = self.current_user @@ -229,7 +245,8 @@ class QuestionHandler(BaseHandler): elif qtype != 'checkbox': # radio, text, textarea, ... answer = answer[0] - grade = self.learn.check_answer(user, answer) + # check answer in another thread (nonblocking) + grade = await to_tornado_future(self.check_answer(user, answer)) question = self.learn.get_student_question(user) if grade <= 0.999: # wrong answer diff --git a/templates/topic.html b/templates/topic.html index 106d119..c92aea2 100644 --- a/templates/topic.html +++ b/templates/topic.html @@ -91,9 +91,7 @@ -- libgit2 0.21.2