diff --git a/BUGS.md b/BUGS.md index dbcb1b5..881cf1f 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,6 +1,7 @@ # BUGS +- grade gives internal server error - reload do teste recomeça a contagem no inicio do tempo. - 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. - 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 @@ # TODO +- stress tests. use https://locust.io +- wait for admin to start test. (students can be allowed earlier) - impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente? - na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo - retornar None quando nao ha alteracoes relativamente à última vez. diff --git a/perguntations/app.py b/perguntations/app.py index 525acb0..c8be6f4 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -361,9 +361,9 @@ class App(): writer.writerows(grades) return self.testfactory['ref'], csvstr.getvalue() - def get_student_test(self, uid, default=None): - '''get test from online student''' - return self.online[uid].get('test', default) + def get_student_test(self, uid): + '''get test from online student or None if no test was generated yet''' + return self.online[uid].get('test', None) # def get_questions_dir(self): # return self.testfactory['questions_dir'] diff --git a/perguntations/serve.py b/perguntations/serve.py index 47cc656..e0721d7 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -13,6 +13,7 @@ import json import logging.config import mimetypes from os import path +import re import signal import sys from timeit import default_timer as timer @@ -37,7 +38,6 @@ class WebApplication(tornado.web.Application): handlers = [ (r'/login', LoginHandler), (r'/logout', LogoutHandler), - (r'/test', TestHandler), (r'/review', ReviewHandler), (r'/admin', AdminHandler), (r'/file', FileHandler), @@ -97,7 +97,7 @@ class BaseHandler(tornado.web.RequestHandler): Since HTTP is stateless, a cookie is used to identify the user. This function returns the cookie for the current user. ''' - cookie = self.get_secure_cookie('user') + cookie = self.get_secure_cookie('perguntations_user') if cookie: return cookie.decode('utf-8') return None @@ -161,6 +161,145 @@ class BaseHandler(tornado.web.RequestHandler): # AdminSocketHandler.send_updates(chat) # send to clients # ---------------------------------------------------------------------------- +# Test shown to students +# ---------------------------------------------------------------------------- +# pylint: disable=abstract-method +class RootHandler(BaseHandler): + ''' + Generates test to student. + Receives answers, corrects the test and sends back the grade. + Redirects user 0 to /admin. + ''' + + _templates = { + # -- question templates -- + 'radio': 'question-radio.html', + 'checkbox': 'question-checkbox.html', + 'text': 'question-text.html', + 'text-regex': 'question-text.html', + 'numeric-interval': 'question-text.html', + 'textarea': 'question-textarea.html', + # -- information panels -- + 'information': 'question-information.html', + 'success': 'question-information.html', + 'warning': 'question-information.html', + 'alert': 'question-information.html', + } + + # --- GET + @tornado.web.authenticated + async def get(self): + ''' + Sends test to student or redirects 0 to admin page + ''' + + uid = self.current_user + logging.info('"%s" GET /', uid) + if uid == '0': + self.redirect('/admin') + + test = self.testapp.get_student_test(uid) # reloading returns same test + if test is None: + test = await self.testapp.generate_test(uid) + + self.render('test.html', t=test, md=md_to_html, templ=self._templates) + + + # --- POST + @tornado.web.authenticated + async def post(self): + ''' + Receives answers, fixes some html weirdness, corrects test and + sends back the grade. + + self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']} + builds dictionary ans={0: 'answer0', 1:, 'answer1', ...} + unanswered questions not included. + ''' + timeit_start = timer() # performance timer + + uid = self.current_user + logging.debug('"%s" POST /', uid) + + try: + test = self.testapp.get_student_test(uid) + except KeyError as exc: + logging.warning('"%s" POST / raised 403 Forbidden', uid) + raise tornado.web.HTTPError(403) from exc # Forbidden + + ans = {} + for i, question in enumerate(test['questions']): + qid = str(i) + if 'answered-' + qid in self.request.arguments: + ans[i] = self.get_body_arguments(qid) + # print(i, ans[i]) + + # remove enclosing list in some question types + if question['type'] == 'radio': + if not ans[i]: + ans[i] = None + else: + ans[i] = ans[i][0] + elif question['type'] in ('text', 'text-regex', 'textarea', + 'numeric-interval'): + ans[i] = ans[i][0] + + # correct answered questions and logout + await self.testapp.correct_test(uid, ans) + + # show final grade and grades of other tests in the database + allgrades = self.testapp.get_student_grades_from_all_tests(uid) + + self.clear_cookie('perguntations_user') + self.render('grade.html', t=test, allgrades=allgrades) + self.testapp.logout(uid) + + timeit_finish = timer() + logging.info(' correction took %fs', timeit_finish-timeit_start) + +# ---------------------------------------------------------------------------- +# pylint: disable=abstract-method +class LoginHandler(BaseHandler): + '''Handles /login''' + + _prefix = re.compile(r'[a-z]') + + def get(self): + '''Render login page.''' + self.render('login.html', error='') + + async def post(self): + '''Authenticates student and login.''' + uid = self._prefix.sub('', self.get_body_argument('uid')) + password = self.get_body_argument('pw') + login_ok = await self.testapp.login(uid, password) + + if login_ok: + self.set_secure_cookie('perguntations_user', str(uid), expires_days=1) + self.redirect('/') + else: + self.render('login.html', error='Não autorizado ou senha inválida') + + +# ---------------------------------------------------------------------------- +# pylint: disable=abstract-method +class LogoutHandler(BaseHandler): + '''Handle /logout''' + + @tornado.web.authenticated + def get(self): + '''Logs out a user.''' + self.clear_cookie('perguntations_user') + self.testapp.logout(self.current_user) + self.redirect('/') + + def on_finish(self): + self.testapp.logout(self.current_user) + + + + +# ---------------------------------------------------------------------------- # pylint: disable=abstract-method class StudentWebservice(BaseHandler): ''' @@ -266,62 +405,6 @@ class AdminHandler(BaseHandler): # ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class LoginHandler(BaseHandler): - '''Handle /login''' - - def get(self): - '''Render login page.''' - self.render('login.html', error='') - - async def post(self): - '''Authenticates student (prefix 'l' are removed) and login.''' - - uid = self.get_body_argument('uid').lstrip('l') - password = self.get_body_argument('pw') - login_ok = await self.testapp.login(uid, password) - - if login_ok: - self.set_secure_cookie("user", str(uid), expires_days=30) - self.redirect(self.get_argument("next", "/")) - else: - self.render("login.html", error='Não autorizado ou senha inválida') - - -# ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class LogoutHandler(BaseHandler): - '''Handle /logout''' - - @tornado.web.authenticated - def get(self): - '''Logs out a user.''' - self.clear_cookie('user') - self.redirect('/') - - def on_finish(self): - self.testapp.logout(self.current_user) - - -# ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class RootHandler(BaseHandler): - ''' - Handles / to redirect students and admin to /test and /admin, resp. - ''' - - @tornado.web.authenticated - def get(self): - ''' - Redirects students to the /test and admin to the /admin page. - ''' - if self.current_user == '0': - self.redirect('/admin') - else: - self.redirect('/test') - - -# ---------------------------------------------------------------------------- # Serves files from the /public subdir of the topics. # ---------------------------------------------------------------------------- # pylint: disable=abstract-method @@ -373,88 +456,6 @@ class FileHandler(BaseHandler): break -# ---------------------------------------------------------------------------- -# Test shown to students -# ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class TestHandler(BaseHandler): - ''' - Generates test to student. - Receives answers, corrects the test and sends back the grade. - ''' - - _templates = { - # -- question templates -- - 'radio': 'question-radio.html', - 'checkbox': 'question-checkbox.html', - 'text': 'question-text.html', - 'text-regex': 'question-text.html', - 'numeric-interval': 'question-text.html', - 'textarea': 'question-textarea.html', - # -- information panels -- - 'information': 'question-information.html', - 'success': 'question-information.html', - 'warning': 'question-information.html', - 'alert': 'question-information.html', - } - - # --- GET - @tornado.web.authenticated - async def get(self): - ''' - Generates test and sends to student - ''' - uid = self.current_user - test = self.testapp.get_student_test(uid) # reloading returns same test - if test is None: - test = await self.testapp.generate_test(uid) - - self.render('test.html', t=test, md=md_to_html, templ=self._templates) - - # --- POST - @tornado.web.authenticated - async def post(self): - ''' - Receives answers, fixes some html weirdness, corrects test and - sends back the grade. - - self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']} - builds dictionary ans={0: 'answer0', 1:, 'answer1', ...} - unanswered questions not included. - ''' - timeit_start = timer() # performance timer - - uid = self.current_user - test = self.testapp.get_student_test(uid) - ans = {} - for i, question in enumerate(test['questions']): - qid = str(i) - if 'answered-' + qid in self.request.arguments: - ans[i] = self.get_body_arguments(qid) - - # remove enclosing list in some question types - if question['type'] == 'radio': - if not ans[i]: - ans[i] = None - else: - ans[i] = ans[i][0] - elif question['type'] in ('text', 'text-regex', 'textarea', - 'numeric-interval'): - ans[i] = ans[i][0] - - # correct answered questions and logout - await self.testapp.correct_test(uid, ans) - self.testapp.logout(uid) - self.clear_cookie('user') - - # show final grade and grades of other tests in the database - allgrades = self.testapp.get_student_grades_from_all_tests(uid) - - timeit_finish = timer() - logging.info(' correction took %fs', timeit_finish-timeit_start) - - self.render('grade.html', t=test, allgrades=allgrades) - # --- REVIEW ----------------------------------------------------------------- # pylint: disable=abstract-method diff --git a/perguntations/templates/grade.html b/perguntations/templates/grade.html index 51d1998..3137d07 100644 --- a/perguntations/templates/grade.html +++ b/perguntations/templates/grade.html @@ -42,11 +42,11 @@
{% if t['state'] == 'FINISHED' %}

Resultado: - {{ f'{round(t["grade"], 1)}' }} + {{ f'{round(t["grade"], 3)}' }} valores na escala de {{t['scale'][0]}} a {{t['scale'][1]}}.

O seu teste foi correctamente entregue e a nota registada.

-

Clique aqui para sair do teste

+

Clique aqui para sair do teste

{% if t['grade'] - t['scale'][0] >= 0.75*(t['scale'][1] - t['scale'][0]) %} {% end %} diff --git a/perguntations/templates/test.html b/perguntations/templates/test.html index 52d15e9..18626f5 100644 --- a/perguntations/templates/test.html +++ b/perguntations/templates/test.html @@ -108,7 +108,7 @@
-
+ {% module xsrf_form_html() %} {% for i, q in enumerate(t['questions']) %} -- libgit2 0.21.2