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