Commit ed0097b4a829ff2dffa3883081a3241f5e7ec6cd

Authored by Miguel Barão
1 parent 66ba3c16
Exists in master and in 1 other branch dev

removes redirect to /test, test is served on the root /

trying to fix redirect failure on login and grade (not stress tested)
login messages explain the error on failed logins
some refactoring
perguntations/app.py
... ... @@ -113,8 +113,7 @@ class App():
113 113 else:
114 114 logger.info('Students not yet allowed to login.')
115 115  
116   - # pre-generate tests
117   -
  116 + # pre-generate tests for allowed students
118 117 if self.allowed:
119 118 logger.info('Generating %d tests. May take awhile...',
120 119 len(self.allowed))
... ... @@ -125,20 +124,15 @@ class App():
125 124 # ------------------------------------------------------------------------
126 125 async def login(self, uid, try_pw, headers=None):
127 126 '''login authentication'''
128   - if uid in self.online:
129   - logger.warning('"%s" already logged in.', uid)
130   - return 'already_online'
131 127 if uid not in self.allowed and uid != '0': # not allowed
132 128 logger.warning('"%s" unauthorized.', uid)
133 129 return 'unauthorized'
134 130  
135   - # get name+password from db
136 131 with self._db_session() as sess:
137 132 name, password = sess.query(Student.name, Student.password)\
138 133 .filter_by(id=uid)\
139 134 .one()
140 135  
141   - # first login updates the password
142 136 if password == '': # update password on first login
143 137 await self.update_student_password(uid, try_pw)
144 138 pw_ok = True
... ... @@ -151,9 +145,17 @@ class App():
151 145  
152 146 # success
153 147 self.allowed.discard(uid) # remove from set of allowed students
154   - self.online[uid] = {'student': {'name': name, 'number': uid, 'headers': headers}}
155   - logger.info('"%s" logged in from %s.', uid, headers['remote_ip'])
156 148  
  149 + if uid in self.online:
  150 + logger.warning('"%s" login again from %s (reusing state).',
  151 + uid, headers['remote_ip'])
  152 + # FIXME invalidate previous login
  153 + else:
  154 + self.online[uid] = {'student': {
  155 + 'name': name,
  156 + 'number': uid,
  157 + 'headers': headers}}
  158 + logger.info('"%s" login from %s.', uid, headers['remote_ip'])
157 159  
158 160 # ------------------------------------------------------------------------
159 161 def logout(self, uid):
... ... @@ -185,7 +187,7 @@ class App():
185 187 self.testfactory = TestFactory(testconf)
186 188 except TestFactoryException as exc:
187 189 logger.critical(exc)
188   - raise AppException('Failed to create test factory!') from exc
  190 + raise AppException('Failed to create test factory!') from exc
189 191  
190 192 logger.info('Test factory ready. No errors found.')
191 193  
... ... @@ -201,10 +203,32 @@ class App():
201 203 for _ in range(num)]
202 204  
203 205 # ------------------------------------------------------------------------
204   - async def generate_test(self, uid):
205   - '''generate a test for a given student. the student must be online'''
  206 + async def get_test_or_generate(self, uid):
  207 + '''get current test or generate a new one'''
  208 + try:
  209 + student = self.online[uid]
  210 + except KeyError as exc:
  211 + msg = f'"{uid}" is not online. get_test_or_generate() FAILED'
  212 + logger.error(msg)
  213 + raise AppException(msg) from exc
  214 +
  215 + # get current test. if test does not exist then generate a new one
  216 + if not 'test' in student:
  217 + await self._new_test(uid)
206 218  
207   - student_id = self.online[uid]['student'] # {'name': ?, 'number': ?}
  219 + return student['test']
  220 +
  221 + def get_test(self, uid):
  222 + '''get test from online student or raise exception'''
  223 + return self.online[uid]['test']
  224 +
  225 + async def _new_test(self, uid):
  226 + '''
  227 + assign a test to a given student. if there are pregenerated tests then
  228 + use one of them, otherwise generate one.
  229 + the student must be online
  230 + '''
  231 + student = self.online[uid]['student'] # {'name': ?, 'number': ?}
208 232  
209 233 try:
210 234 test = self.pregenerated_tests.pop()
... ... @@ -215,10 +239,8 @@ class App():
215 239 else:
216 240 logger.info('"%s" using a pregenerated test.', uid)
217 241  
218   - test.start(student_id) # student signs the test
219   - self.online[uid]['test'] = test # register test for this student
220   -
221   - return self.online[uid]['test']
  242 + test.register(student) # student signs the test
  243 + self.online[uid]['test'] = test
222 244  
223 245 # ------------------------------------------------------------------------
224 246 async def correct_test(self, uid, ans):
... ... @@ -362,13 +384,7 @@ class App():
362 384 writer.writerows(grades)
363 385 return self.testfactory['ref'], csvstr.getvalue()
364 386  
365   - def get_student_test(self, uid):
366   - '''get test from online student or None if no test was generated yet'''
367   - return self.online[uid].get('test', None)
368   -
369   - # def get_questions_dir(self):
370   - # return self.testfactory['questions_dir']
371   -
  387 + # ------------------------------------------------------------------------
372 388 def get_student_grades_from_all_tests(self, uid):
373 389 '''get grades of student from all tests'''
374 390 with self._db_session() as sess:
... ...
perguntations/serve.py
... ... @@ -5,8 +5,8 @@ Handles the web, http & html part of the application interface.
5 5 Uses the tornadoweb framework.
6 6 '''
7 7  
8   -
9 8 # python standard library
  9 +import asyncio
10 10 import base64
11 11 import functools
12 12 import json
... ... @@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler):
161 161 # AdminSocketHandler.send_updates(chat) # send to clients
162 162  
163 163 # ----------------------------------------------------------------------------
  164 +# pylint: disable=abstract-method
  165 +class LoginHandler(BaseHandler):
  166 + '''Handles /login'''
  167 +
  168 + _prefix = re.compile(r'[a-z]')
  169 + _error_msg = {
  170 + 'wrong_password': 'Password errada',
  171 + 'already_online': 'Já está online, não pode entrar duas vezes',
  172 + 'unauthorized': 'Não está autorizado a fazer o teste'
  173 + }
  174 +
  175 + def get(self):
  176 + '''Render login page.'''
  177 + self.render('login.html', error='')
  178 +
  179 + async def post(self):
  180 + '''Authenticates student and login.'''
  181 + uid = self._prefix.sub('', self.get_body_argument('uid'))
  182 + password = self.get_body_argument('pw')
  183 + headers = {
  184 + 'remote_ip': self.request.remote_ip,
  185 + 'user_agent': self.request.headers.get('User-Agent')
  186 + }
  187 +
  188 + error = await self.testapp.login(uid, password, headers)
  189 +
  190 + if error is None:
  191 + self.set_secure_cookie('perguntations_user', str(uid))
  192 + self.redirect('/')
  193 + else:
  194 + await asyncio.sleep(3) # to avoid spamming the server...
  195 + self.render('login.html', error=self._error_msg[error])
  196 +
  197 +
  198 +# ----------------------------------------------------------------------------
  199 +# pylint: disable=abstract-method
  200 +class LogoutHandler(BaseHandler):
  201 + '''Handle /logout'''
  202 +
  203 + @tornado.web.authenticated
  204 + def get(self):
  205 + '''Logs out a user.'''
  206 + self.clear_cookie('perguntations_user')
  207 + self.testapp.logout(self.current_user)
  208 + self.render('login.html', error='')
  209 +
  210 +
  211 +# ----------------------------------------------------------------------------
164 212 # Test shown to students
165 213 # ----------------------------------------------------------------------------
166 214 # pylint: disable=abstract-method
... ... @@ -200,11 +248,9 @@ class RootHandler(BaseHandler):
200 248  
201 249 if uid == '0':
202 250 self.redirect('/admin')
  251 + return
203 252  
204   - test = self.testapp.get_student_test(uid) # reloading returns same test
205   - if test is None:
206   - test = await self.testapp.generate_test(uid)
207   -
  253 + test = await self.testapp.get_test_or_generate(uid)
208 254 self.render('test.html', t=test, md=md_to_html, templ=self._templates)
209 255  
210 256  
... ... @@ -225,7 +271,7 @@ class RootHandler(BaseHandler):
225 271 logging.debug('"%s" POST /', uid)
226 272  
227 273 try:
228   - test = self.testapp.get_student_test(uid)
  274 + test = self.testapp.get_test(uid)
229 275 except KeyError as exc:
230 276 logging.warning('"%s" POST / raised 403 Forbidden', uid)
231 277 raise tornado.web.HTTPError(403) from exc # Forbidden
... ... @@ -235,7 +281,6 @@ class RootHandler(BaseHandler):
235 281 qid = str(i)
236 282 if 'answered-' + qid in self.request.arguments:
237 283 ans[i] = self.get_body_arguments(qid)
238   - # print(i, ans[i])
239 284  
240 285 # remove enclosing list in some question types
241 286 if question['type'] == 'radio':
... ... @@ -260,57 +305,6 @@ class RootHandler(BaseHandler):
260 305 timeit_finish = timer()
261 306 logging.info(' correction took %fs', timeit_finish-timeit_start)
262 307  
263   -# ----------------------------------------------------------------------------
264   -# pylint: disable=abstract-method
265   -class LoginHandler(BaseHandler):
266   - '''Handles /login'''
267   -
268   - _prefix = re.compile(r'[a-z]')
269   - _error_msg = {
270   - 'wrong_password': 'Password errada',
271   - 'already_online': 'Já está online, não pode entrar duas vezes',
272   - 'unauthorized': 'Não está autorizado a fazer o teste'
273   - }
274   -
275   - def get(self):
276   - '''Render login page.'''
277   - self.render('login.html', error='')
278   -
279   - async def post(self):
280   - '''Authenticates student and login.'''
281   - uid = self._prefix.sub('', self.get_body_argument('uid'))
282   - password = self.get_body_argument('pw')
283   - headers = {
284   - 'remote_ip': self.request.remote_ip,
285   - 'user_agent': self.request.headers.get('User-Agent')
286   - }
287   -
288   - error = await self.testapp.login(uid, password, headers)
289   -
290   - if error is None:
291   - self.set_secure_cookie('perguntations_user', str(uid), expires_days=1)
292   - self.redirect('/')
293   - else:
294   - self.render('login.html', error=self._error_msg[error])
295   -
296   -
297   -# ----------------------------------------------------------------------------
298   -# pylint: disable=abstract-method
299   -class LogoutHandler(BaseHandler):
300   - '''Handle /logout'''
301   -
302   - @tornado.web.authenticated
303   - def get(self):
304   - '''Logs out a user.'''
305   - self.clear_cookie('perguntations_user')
306   - self.testapp.logout(self.current_user)
307   - self.redirect('/')
308   -
309   - def on_finish(self):
310   - self.testapp.logout(self.current_user)
311   -
312   -
313   -
314 308  
315 309 # ----------------------------------------------------------------------------
316 310 # pylint: disable=abstract-method
... ... @@ -469,7 +463,6 @@ class FileHandler(BaseHandler):
469 463 break
470 464  
471 465  
472   -
473 466 # --- REVIEW -----------------------------------------------------------------
474 467 # pylint: disable=abstract-method
475 468 class ReviewHandler(BaseHandler):
... ...
perguntations/templates/test.html
... ... @@ -44,7 +44,7 @@
44 44 <!-- ===================================================================== -->
45 45 <body>
46 46 <!-- ===================================================================== -->
47   -<div class="progress fixed-top" style="height: 61px; border-radius: 0px;">
  47 +<div class="progress fixed-top" style="height: 62px; border-radius: 0px;">
48 48 <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
49 49 </div>
50 50  
... ... @@ -94,10 +94,6 @@
94 94  
95 95 <h5>
96 96 <div class="row">
97   - <label for="inicio" class="col-sm-3">Início:</label>
98   - <div class="col-sm-9" id="inicio">{{ str(t['start_time'].time())[:8]}}</div>
99   - </div>
100   - <div class="row">
101 97 <label for="duracao" class="col-sm-3">Duração:</label>
102 98 <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div>
103 99 </div>
... ...
perguntations/test.py
... ... @@ -304,7 +304,7 @@ class Test(dict):
304 304 # super().__init__(d)
305 305  
306 306 # ------------------------------------------------------------------------
307   - def start(self, student):
  307 + def register(self, student):
308 308 '''
309 309 Write student id in the test and register start time
310 310 '''
... ...
setup.py
  1 +'''
  2 +Perguntations setup
  3 +'''
1 4 from setuptools import setup, find_packages
2 5  
3 6 from perguntations import (__author__, __license__,
... ...