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