Commit 046086917c561613fd6612eb64e130bb057eae26
1 parent
0a68032c
Exists in
master
and in
1 other branch
- 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
Showing
5 changed files
with
150 additions
and
146 deletions
Show diff stats
BUGS.md
| 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']) %} |