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 | 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']) %} | ... | ... |