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
1 1
2 # BUGS 2 # BUGS
3 3
  4 +- grade gives internal server error
4 - reload do teste recomeça a contagem no inicio do tempo. 5 - reload do teste recomeça a contagem no inicio do tempo.
5 - 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 - 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 - 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). 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,6 +14,8 @@
13 14
14 # TODO 15 # TODO
15 16
  17 +- stress tests. use https://locust.io
  18 +- wait for admin to start test. (students can be allowed earlier)
16 - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente? 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 - na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo 20 - na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo
18 - retornar None quando nao ha alteracoes relativamente à última vez. 21 - retornar None quando nao ha alteracoes relativamente à última vez.
perguntations/app.py
@@ -361,9 +361,9 @@ class App(): @@ -361,9 +361,9 @@ class App():
361 writer.writerows(grades) 361 writer.writerows(grades)
362 return self.testfactory['ref'], csvstr.getvalue() 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 # def get_questions_dir(self): 368 # def get_questions_dir(self):
369 # return self.testfactory['questions_dir'] 369 # return self.testfactory['questions_dir']
perguntations/serve.py
@@ -13,6 +13,7 @@ import json @@ -13,6 +13,7 @@ import json
13 import logging.config 13 import logging.config
14 import mimetypes 14 import mimetypes
15 from os import path 15 from os import path
  16 +import re
16 import signal 17 import signal
17 import sys 18 import sys
18 from timeit import default_timer as timer 19 from timeit import default_timer as timer
@@ -37,7 +38,6 @@ class WebApplication(tornado.web.Application): @@ -37,7 +38,6 @@ class WebApplication(tornado.web.Application):
37 handlers = [ 38 handlers = [
38 (r'/login', LoginHandler), 39 (r'/login', LoginHandler),
39 (r'/logout', LogoutHandler), 40 (r'/logout', LogoutHandler),
40 - (r'/test', TestHandler),  
41 (r'/review', ReviewHandler), 41 (r'/review', ReviewHandler),
42 (r'/admin', AdminHandler), 42 (r'/admin', AdminHandler),
43 (r'/file', FileHandler), 43 (r'/file', FileHandler),
@@ -97,7 +97,7 @@ class BaseHandler(tornado.web.RequestHandler): @@ -97,7 +97,7 @@ class BaseHandler(tornado.web.RequestHandler):
97 Since HTTP is stateless, a cookie is used to identify the user. 97 Since HTTP is stateless, a cookie is used to identify the user.
98 This function returns the cookie for the current user. 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 if cookie: 101 if cookie:
102 return cookie.decode('utf-8') 102 return cookie.decode('utf-8')
103 return None 103 return None
@@ -161,6 +161,145 @@ class BaseHandler(tornado.web.RequestHandler): @@ -161,6 +161,145 @@ class BaseHandler(tornado.web.RequestHandler):
161 # AdminSocketHandler.send_updates(chat) # send to clients 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 # pylint: disable=abstract-method 303 # pylint: disable=abstract-method
165 class StudentWebservice(BaseHandler): 304 class StudentWebservice(BaseHandler):
166 ''' 305 '''
@@ -266,62 +405,6 @@ class AdminHandler(BaseHandler): @@ -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 # Serves files from the /public subdir of the topics. 408 # Serves files from the /public subdir of the topics.
326 # ---------------------------------------------------------------------------- 409 # ----------------------------------------------------------------------------
327 # pylint: disable=abstract-method 410 # pylint: disable=abstract-method
@@ -373,88 +456,6 @@ class FileHandler(BaseHandler): @@ -373,88 +456,6 @@ class FileHandler(BaseHandler):
373 break 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 # --- REVIEW ----------------------------------------------------------------- 460 # --- REVIEW -----------------------------------------------------------------
460 # pylint: disable=abstract-method 461 # pylint: disable=abstract-method
perguntations/templates/grade.html
@@ -42,11 +42,11 @@ @@ -42,11 +42,11 @@
42 <div class="jumbotron"> 42 <div class="jumbotron">
43 {% if t['state'] == 'FINISHED' %} 43 {% if t['state'] == 'FINISHED' %}
44 <h1>Resultado: 44 <h1>Resultado:
45 - <strong>{{ f'{round(t["grade"], 1)}' }}</strong> 45 + <strong>{{ f'{round(t["grade"], 3)}' }}</strong>
46 valores na escala de {{t['scale'][0]}} a {{t['scale'][1]}}. 46 valores na escala de {{t['scale'][0]}} a {{t['scale'][1]}}.
47 </h1> 47 </h1>
48 <p>O seu teste foi correctamente entregue e a nota registada.</p> 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 {% if t['grade'] - t['scale'][0] >= 0.75*(t['scale'][1] - t['scale'][0]) %} 50 {% if t['grade'] - t['scale'][0] >= 0.75*(t['scale'][1] - t['scale'][0]) %}
51 <i class="fas fa-thumbs-up fa-5x text-success" aria-hidden="true"></i> 51 <i class="fas fa-thumbs-up fa-5x text-success" aria-hidden="true"></i>
52 {% end %} 52 {% end %}
perguntations/templates/test.html
@@ -108,7 +108,7 @@ @@ -108,7 +108,7 @@
108 </h5> 108 </h5>
109 </div> 109 </div>
110 110
111 - <form action="/test" method="post" id="test" autocomplete="off"> 111 + <form action="/" method="post" id="test" autocomplete="off">
112 {% module xsrf_form_html() %} 112 {% module xsrf_form_html() %}
113 113
114 {% for i, q in enumerate(t['questions']) %} 114 {% for i, q in enumerate(t['questions']) %}