Commit 1245246ac1b7668cb075807e6957a17443734437

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

- correction of tests is now asynchronous. Each question is sent to the default executor.

BUGS.md
... ... @@ -9,14 +9,14 @@ ou usar push (websockets?)
9 9 - pymips: nao pode executar syscalls do spim.
10 10 - perguntas checkbox [right,wrong] com pelo menos uma opção correcta.
11 11 - questions.py textarea has a abspath which does not make sense! why is it there? not working for perguntations, but seems to work for aprendizations
12   -- o eventloop está a bloquear. correção do teste é blocking. usar threadpoolexecutor?
13 12 - submissao faz um post ajax.
14 13 - eventos unfocus?
15 14 - servidor nao esta a lidar com eventos scroll/resize. ignorar?
16 15  
17 16 # TODO
18 17  
19   -- gerar teste qd o prof autoriza.
  18 +- decorador para user 0, evita o "if uid==0" em muitas funcoes.
  19 +- gerar teste qd o prof autoriza? melhor nao, pode apagar o teste em curso.
20 20 - enviar resposta de cada pergunta individualmente.
21 21 - experimentar gerador de svg que inclua no markdown da pergunta e ver se funciona.
22 22 - suportar cotacao to teste diferente de 20 (e.g. para juntar perguntas em papel). opcao "points: 18" que normaliza total para 18 em vez de 20.
... ... @@ -37,7 +37,6 @@ ou usar push (websockets?)
37 37 - fazer renderer para linguagem assembly mips?
38 38 - permitir eliminar teste a decorrer
39 39 - cancelar teste no menu admin. Dado o numero de aluno remove teste e faz logout do aluno.
40   -- decorador para user 0, evita o "if uid==0" em muitas funcoes.
41 40 - mathjax-node:
42 41 sudo pkg install node npm
43 42 npm install mathjax-node mathjax-node-cli # pacotes em ~/node_modules
... ... @@ -61,6 +60,8 @@ ou usar push (websockets?)
61 60  
62 61 # FIXED
63 62  
  63 +- o eventloop está a bloquear. correção do teste é blocking. usar threadpoolexecutor?
  64 +- substituir get_event_loop por get_runnint_loop (ver https://docs.python.org/3/library/asyncio-eventloop.html)
64 65 - review nao esta a funcionar
65 66 - servir imagens das perguntas não funciona. Necessario passar a ref da pergunta no link para poder ajustar o path no FileHandler.
66 67 - a primeira coluna da tabela admin deveria estar sempre ordenada.
... ...
app.py
... ... @@ -26,7 +26,7 @@ class AppException(Exception):
26 26 # ============================================================================
27 27 async def check_password(try_pw, password):
28 28 try_pw = try_pw.encode('utf-8')
29   - loop = asyncio.get_event_loop()
  29 + loop = asyncio.get_running_loop()
30 30 hashed_pw = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password)
31 31 return password == hashed_pw
32 32  
... ... @@ -150,10 +150,10 @@ class App(object):
150 150 # -----------------------------------------------------------------------
151 151 # ans is a dictionary {question_index: answer, ...}
152 152 # for example: {0:'hello', 1:[1,2]}
153   - def correct_test(self, uid, ans):
  153 + async def correct_test(self, uid, ans):
154 154 t = self.online[uid]['test']
155 155 t.update_answers(ans)
156   - grade = t.correct()
  156 + grade = await t.correct()
157 157  
158 158 # save test in JSON format
159 159 fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json'
... ...
questions.py
... ... @@ -4,6 +4,7 @@ import random
4 4 import re
5 5 from os import path
6 6 import logging
  7 +import asyncio
7 8  
8 9 # user installed libraries
9 10 import yaml
... ... @@ -49,6 +50,11 @@ class Question(dict):
49 50 self['grade'] = 0.0
50 51 return 0.0
51 52  
  53 + async def correct_async(self):
  54 + loop = asyncio.get_running_loop()
  55 + grade = await loop.run_in_executor(None, self.correct)
  56 + return grade
  57 +
52 58 def set_defaults(self, d):
53 59 'Add k:v pairs from default dict d for nonexistent keys'
54 60 for k,v in d.items():
... ...
serve.py
... ... @@ -75,7 +75,6 @@ class LoginHandler(BaseHandler):
75 75 def get(self):
76 76 self.render('login.html', error='')
77 77  
78   - # async
79 78 async def post(self):
80 79 uid = self.get_body_argument('uid')
81 80 pw = self.get_body_argument('pw')
... ... @@ -181,8 +180,7 @@ class TestHandler(BaseHandler):
181 180  
182 181 # POST
183 182 @tornado.web.authenticated
184   - # async
185   - def post(self):
  183 + async def post(self):
186 184 uid = self.current_user
187 185  
188 186 # self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']}
... ... @@ -205,9 +203,7 @@ class TestHandler(BaseHandler):
205 203 'numeric-interval'):
206 204 ans[i] = ans[i][0]
207 205  
208   - # loop = asyncio.get_event_loop()
209   - # await loop.run_in_executor(None, self.testapp.correct_test, uid, ans)
210   - self.testapp.correct_test(uid, ans)
  206 + await self.testapp.correct_test(uid, ans)
211 207  
212 208 self.testapp.logout(uid)
213 209 self.clear_cookie('user')
... ...
test.py
... ... @@ -231,12 +231,15 @@ class Test(dict):
231 231  
232 232 # -----------------------------------------------------------------------
233 233 # Corrects all the answers and computes the final grade
234   - def correct(self):
  234 + async def correct(self):
235 235 self['finish_time'] = datetime.now()
236 236 self['state'] = 'FINISHED'
237 237  
238   - grade = sum(q.correct()*q['points'] for q in self['questions'])
239   - self['grade'] = max(0, round(grade, 1)) # truncate FIXME scale?
  238 + grade = 0.0
  239 + for q in self['questions']:
  240 + grade += await q.correct_async() * q['points']
  241 +
  242 + self['grade'] = max(0, round(grade, 1)) # avoid negative final grades
240 243  
241 244 logger.info(f'Student {self["student"]["number"]}: correction gave {self["grade"]} points.')
242 245 return self['grade']
... ...