Commit 046086917c561613fd6612eb64e130bb057eae26

Authored by Miguel Barão
1 parent 0a68032c
Exists in master and in 1 other branch dev

- the address for test is now the root instead of /test (removes a redirection that sometimes fails)

- cookie is now perguntations_user
- removes any letter prefix from login number
BUGS.md
1 1  
2 2 # BUGS
3 3  
  4 +- grade gives internal server error
4 5 - reload do teste recomeça a contagem no inicio do tempo.
5 6 - em admin, quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20. a tabela teste na DB não tem a escala desse teste.
6 7 - em grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao dos testes realizados no passado (tabela test devia guardar a escala).
... ... @@ -13,6 +14,8 @@
13 14  
14 15 # TODO
15 16  
  17 +- stress tests. use https://locust.io
  18 +- wait for admin to start test. (students can be allowed earlier)
16 19 - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente?
17 20 - na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo
18 21 - retornar None quando nao ha alteracoes relativamente à última vez.
... ...
perguntations/app.py
... ... @@ -361,9 +361,9 @@ class App():
361 361 writer.writerows(grades)
362 362 return self.testfactory['ref'], csvstr.getvalue()
363 363  
364   - def get_student_test(self, uid, default=None):
365   - '''get test from online student'''
366   - return self.online[uid].get('test', default)
  364 + def get_student_test(self, uid):
  365 + '''get test from online student or None if no test was generated yet'''
  366 + return self.online[uid].get('test', None)
367 367  
368 368 # def get_questions_dir(self):
369 369 # return self.testfactory['questions_dir']
... ...
perguntations/serve.py
... ... @@ -13,6 +13,7 @@ import json
13 13 import logging.config
14 14 import mimetypes
15 15 from os import path
  16 +import re
16 17 import signal
17 18 import sys
18 19 from timeit import default_timer as timer
... ... @@ -37,7 +38,6 @@ class WebApplication(tornado.web.Application):
37 38 handlers = [
38 39 (r'/login', LoginHandler),
39 40 (r'/logout', LogoutHandler),
40   - (r'/test', TestHandler),
41 41 (r'/review', ReviewHandler),
42 42 (r'/admin', AdminHandler),
43 43 (r'/file', FileHandler),
... ... @@ -97,7 +97,7 @@ class BaseHandler(tornado.web.RequestHandler):
97 97 Since HTTP is stateless, a cookie is used to identify the user.
98 98 This function returns the cookie for the current user.
99 99 '''
100   - cookie = self.get_secure_cookie('user')
  100 + cookie = self.get_secure_cookie('perguntations_user')
101 101 if cookie:
102 102 return cookie.decode('utf-8')
103 103 return None
... ... @@ -161,6 +161,145 @@ class BaseHandler(tornado.web.RequestHandler):
161 161 # AdminSocketHandler.send_updates(chat) # send to clients
162 162  
163 163 # ----------------------------------------------------------------------------
  164 +# Test shown to students
  165 +# ----------------------------------------------------------------------------
  166 +# pylint: disable=abstract-method
  167 +class RootHandler(BaseHandler):
  168 + '''
  169 + Generates test to student.
  170 + Receives answers, corrects the test and sends back the grade.
  171 + Redirects user 0 to /admin.
  172 + '''
  173 +
  174 + _templates = {
  175 + # -- question templates --
  176 + 'radio': 'question-radio.html',
  177 + 'checkbox': 'question-checkbox.html',
  178 + 'text': 'question-text.html',
  179 + 'text-regex': 'question-text.html',
  180 + 'numeric-interval': 'question-text.html',
  181 + 'textarea': 'question-textarea.html',
  182 + # -- information panels --
  183 + 'information': 'question-information.html',
  184 + 'success': 'question-information.html',
  185 + 'warning': 'question-information.html',
  186 + 'alert': 'question-information.html',
  187 + }
  188 +
  189 + # --- GET
  190 + @tornado.web.authenticated
  191 + async def get(self):
  192 + '''
  193 + Sends test to student or redirects 0 to admin page
  194 + '''
  195 +
  196 + uid = self.current_user
  197 + logging.info('"%s" GET /', uid)
  198 + if uid == '0':
  199 + self.redirect('/admin')
  200 +
  201 + test = self.testapp.get_student_test(uid) # reloading returns same test
  202 + if test is None:
  203 + test = await self.testapp.generate_test(uid)
  204 +
  205 + self.render('test.html', t=test, md=md_to_html, templ=self._templates)
  206 +
  207 +
  208 + # --- POST
  209 + @tornado.web.authenticated
  210 + async def post(self):
  211 + '''
  212 + Receives answers, fixes some html weirdness, corrects test and
  213 + sends back the grade.
  214 +
  215 + self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']}
  216 + builds dictionary ans={0: 'answer0', 1:, 'answer1', ...}
  217 + unanswered questions not included.
  218 + '''
  219 + timeit_start = timer() # performance timer
  220 +
  221 + uid = self.current_user
  222 + logging.debug('"%s" POST /', uid)
  223 +
  224 + try:
  225 + test = self.testapp.get_student_test(uid)
  226 + except KeyError as exc:
  227 + logging.warning('"%s" POST / raised 403 Forbidden', uid)
  228 + raise tornado.web.HTTPError(403) from exc # Forbidden
  229 +
  230 + ans = {}
  231 + for i, question in enumerate(test['questions']):
  232 + qid = str(i)
  233 + if 'answered-' + qid in self.request.arguments:
  234 + ans[i] = self.get_body_arguments(qid)
  235 + # print(i, ans[i])
  236 +
  237 + # remove enclosing list in some question types
  238 + if question['type'] == 'radio':
  239 + if not ans[i]:
  240 + ans[i] = None
  241 + else:
  242 + ans[i] = ans[i][0]
  243 + elif question['type'] in ('text', 'text-regex', 'textarea',
  244 + 'numeric-interval'):
  245 + ans[i] = ans[i][0]
  246 +
  247 + # correct answered questions and logout
  248 + await self.testapp.correct_test(uid, ans)
  249 +
  250 + # show final grade and grades of other tests in the database
  251 + allgrades = self.testapp.get_student_grades_from_all_tests(uid)
  252 +
  253 + self.clear_cookie('perguntations_user')
  254 + self.render('grade.html', t=test, allgrades=allgrades)
  255 + self.testapp.logout(uid)
  256 +
  257 + timeit_finish = timer()
  258 + logging.info(' correction took %fs', timeit_finish-timeit_start)
  259 +
  260 +# ----------------------------------------------------------------------------
  261 +# pylint: disable=abstract-method
  262 +class LoginHandler(BaseHandler):
  263 + '''Handles /login'''
  264 +
  265 + _prefix = re.compile(r'[a-z]')
  266 +
  267 + def get(self):
  268 + '''Render login page.'''
  269 + self.render('login.html', error='')
  270 +
  271 + async def post(self):
  272 + '''Authenticates student and login.'''
  273 + uid = self._prefix.sub('', self.get_body_argument('uid'))
  274 + password = self.get_body_argument('pw')
  275 + login_ok = await self.testapp.login(uid, password)
  276 +
  277 + if login_ok:
  278 + self.set_secure_cookie('perguntations_user', str(uid), expires_days=1)
  279 + self.redirect('/')
  280 + else:
  281 + self.render('login.html', error='Não autorizado ou senha inválida')
  282 +
  283 +
  284 +# ----------------------------------------------------------------------------
  285 +# pylint: disable=abstract-method
  286 +class LogoutHandler(BaseHandler):
  287 + '''Handle /logout'''
  288 +
  289 + @tornado.web.authenticated
  290 + def get(self):
  291 + '''Logs out a user.'''
  292 + self.clear_cookie('perguntations_user')
  293 + self.testapp.logout(self.current_user)
  294 + self.redirect('/')
  295 +
  296 + def on_finish(self):
  297 + self.testapp.logout(self.current_user)
  298 +
  299 +
  300 +
  301 +
  302 +# ----------------------------------------------------------------------------
164 303 # pylint: disable=abstract-method
165 304 class StudentWebservice(BaseHandler):
166 305 '''
... ... @@ -266,62 +405,6 @@ class AdminHandler(BaseHandler):
266 405  
267 406  
268 407 # ----------------------------------------------------------------------------
269   -# pylint: disable=abstract-method
270   -class LoginHandler(BaseHandler):
271   - '''Handle /login'''
272   -
273   - def get(self):
274   - '''Render login page.'''
275   - self.render('login.html', error='')
276   -
277   - async def post(self):
278   - '''Authenticates student (prefix 'l' are removed) and login.'''
279   -
280   - uid = self.get_body_argument('uid').lstrip('l')
281   - password = self.get_body_argument('pw')
282   - login_ok = await self.testapp.login(uid, password)
283   -
284   - if login_ok:
285   - self.set_secure_cookie("user", str(uid), expires_days=30)
286   - self.redirect(self.get_argument("next", "/"))
287   - else:
288   - self.render("login.html", error='Não autorizado ou senha inválida')
289   -
290   -
291   -# ----------------------------------------------------------------------------
292   -# pylint: disable=abstract-method
293   -class LogoutHandler(BaseHandler):
294   - '''Handle /logout'''
295   -
296   - @tornado.web.authenticated
297   - def get(self):
298   - '''Logs out a user.'''
299   - self.clear_cookie('user')
300   - self.redirect('/')
301   -
302   - def on_finish(self):
303   - self.testapp.logout(self.current_user)
304   -
305   -
306   -# ----------------------------------------------------------------------------
307   -# pylint: disable=abstract-method
308   -class RootHandler(BaseHandler):
309   - '''
310   - Handles / to redirect students and admin to /test and /admin, resp.
311   - '''
312   -
313   - @tornado.web.authenticated
314   - def get(self):
315   - '''
316   - Redirects students to the /test and admin to the /admin page.
317   - '''
318   - if self.current_user == '0':
319   - self.redirect('/admin')
320   - else:
321   - self.redirect('/test')
322   -
323   -
324   -# ----------------------------------------------------------------------------
325 408 # Serves files from the /public subdir of the topics.
326 409 # ----------------------------------------------------------------------------
327 410 # pylint: disable=abstract-method
... ... @@ -373,88 +456,6 @@ class FileHandler(BaseHandler):
373 456 break
374 457  
375 458  
376   -# ----------------------------------------------------------------------------
377   -# Test shown to students
378   -# ----------------------------------------------------------------------------
379   -# pylint: disable=abstract-method
380   -class TestHandler(BaseHandler):
381   - '''
382   - Generates test to student.
383   - Receives answers, corrects the test and sends back the grade.
384   - '''
385   -
386   - _templates = {
387   - # -- question templates --
388   - 'radio': 'question-radio.html',
389   - 'checkbox': 'question-checkbox.html',
390   - 'text': 'question-text.html',
391   - 'text-regex': 'question-text.html',
392   - 'numeric-interval': 'question-text.html',
393   - 'textarea': 'question-textarea.html',
394   - # -- information panels --
395   - 'information': 'question-information.html',
396   - 'success': 'question-information.html',
397   - 'warning': 'question-information.html',
398   - 'alert': 'question-information.html',
399   - }
400   -
401   - # --- GET
402   - @tornado.web.authenticated
403   - async def get(self):
404   - '''
405   - Generates test and sends to student
406   - '''
407   - uid = self.current_user
408   - test = self.testapp.get_student_test(uid) # reloading returns same test
409   - if test is None:
410   - test = await self.testapp.generate_test(uid)
411   -
412   - self.render('test.html', t=test, md=md_to_html, templ=self._templates)
413   -
414   - # --- POST
415   - @tornado.web.authenticated
416   - async def post(self):
417   - '''
418   - Receives answers, fixes some html weirdness, corrects test and
419   - sends back the grade.
420   -
421   - self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']}
422   - builds dictionary ans={0: 'answer0', 1:, 'answer1', ...}
423   - unanswered questions not included.
424   - '''
425   - timeit_start = timer() # performance timer
426   -
427   - uid = self.current_user
428   - test = self.testapp.get_student_test(uid)
429   - ans = {}
430   - for i, question in enumerate(test['questions']):
431   - qid = str(i)
432   - if 'answered-' + qid in self.request.arguments:
433   - ans[i] = self.get_body_arguments(qid)
434   -
435   - # remove enclosing list in some question types
436   - if question['type'] == 'radio':
437   - if not ans[i]:
438   - ans[i] = None
439   - else:
440   - ans[i] = ans[i][0]
441   - elif question['type'] in ('text', 'text-regex', 'textarea',
442   - 'numeric-interval'):
443   - ans[i] = ans[i][0]
444   -
445   - # correct answered questions and logout
446   - await self.testapp.correct_test(uid, ans)
447   - self.testapp.logout(uid)
448   - self.clear_cookie('user')
449   -
450   - # show final grade and grades of other tests in the database
451   - allgrades = self.testapp.get_student_grades_from_all_tests(uid)
452   -
453   - timeit_finish = timer()
454   - logging.info(' correction took %fs', timeit_finish-timeit_start)
455   -
456   - self.render('grade.html', t=test, allgrades=allgrades)
457   -
458 459  
459 460 # --- REVIEW -----------------------------------------------------------------
460 461 # pylint: disable=abstract-method
... ...
perguntations/templates/grade.html
... ... @@ -42,11 +42,11 @@
42 42 <div class="jumbotron">
43 43 {% if t['state'] == 'FINISHED' %}
44 44 <h1>Resultado:
45   - <strong>{{ f'{round(t["grade"], 1)}' }}</strong>
  45 + <strong>{{ f'{round(t["grade"], 3)}' }}</strong>
46 46 valores na escala de {{t['scale'][0]}} a {{t['scale'][1]}}.
47 47 </h1>
48 48 <p>O seu teste foi correctamente entregue e a nota registada.</p>
49   - <p><a href="/" class="btn btn-primary btn-lg active" role="button">Clique aqui para sair do teste</a></p>
  49 + <p><a href="/logout" class="btn btn-primary btn-lg active" role="button">Clique aqui para sair do teste</a></p>
50 50 {% if t['grade'] - t['scale'][0] >= 0.75*(t['scale'][1] - t['scale'][0]) %}
51 51 <i class="fas fa-thumbs-up fa-5x text-success" aria-hidden="true"></i>
52 52 {% end %}
... ...
perguntations/templates/test.html
... ... @@ -108,7 +108,7 @@
108 108 </h5>
109 109 </div>
110 110  
111   - <form action="/test" method="post" id="test" autocomplete="off">
  111 + <form action="/" method="post" id="test" autocomplete="off">
112 112 {% module xsrf_form_html() %}
113 113  
114 114 {% for i, q in enumerate(t['questions']) %}
... ...