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,8 +113,7 @@ class App():
113 else: 113 else:
114 logger.info('Students not yet allowed to login.') 114 logger.info('Students not yet allowed to login.')
115 115
116 - # pre-generate tests  
117 - 116 + # pre-generate tests for allowed students
118 if self.allowed: 117 if self.allowed:
119 logger.info('Generating %d tests. May take awhile...', 118 logger.info('Generating %d tests. May take awhile...',
120 len(self.allowed)) 119 len(self.allowed))
@@ -125,20 +124,15 @@ class App(): @@ -125,20 +124,15 @@ class App():
125 # ------------------------------------------------------------------------ 124 # ------------------------------------------------------------------------
126 async def login(self, uid, try_pw, headers=None): 125 async def login(self, uid, try_pw, headers=None):
127 '''login authentication''' 126 '''login authentication'''
128 - if uid in self.online:  
129 - logger.warning('"%s" already logged in.', uid)  
130 - return 'already_online'  
131 if uid not in self.allowed and uid != '0': # not allowed 127 if uid not in self.allowed and uid != '0': # not allowed
132 logger.warning('"%s" unauthorized.', uid) 128 logger.warning('"%s" unauthorized.', uid)
133 return 'unauthorized' 129 return 'unauthorized'
134 130
135 - # get name+password from db  
136 with self._db_session() as sess: 131 with self._db_session() as sess:
137 name, password = sess.query(Student.name, Student.password)\ 132 name, password = sess.query(Student.name, Student.password)\
138 .filter_by(id=uid)\ 133 .filter_by(id=uid)\
139 .one() 134 .one()
140 135
141 - # first login updates the password  
142 if password == '': # update password on first login 136 if password == '': # update password on first login
143 await self.update_student_password(uid, try_pw) 137 await self.update_student_password(uid, try_pw)
144 pw_ok = True 138 pw_ok = True
@@ -151,9 +145,17 @@ class App(): @@ -151,9 +145,17 @@ class App():
151 145
152 # success 146 # success
153 self.allowed.discard(uid) # remove from set of allowed students 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 def logout(self, uid): 161 def logout(self, uid):
@@ -185,7 +187,7 @@ class App(): @@ -185,7 +187,7 @@ class App():
185 self.testfactory = TestFactory(testconf) 187 self.testfactory = TestFactory(testconf)
186 except TestFactoryException as exc: 188 except TestFactoryException as exc:
187 logger.critical(exc) 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 logger.info('Test factory ready. No errors found.') 192 logger.info('Test factory ready. No errors found.')
191 193
@@ -201,10 +203,32 @@ class App(): @@ -201,10 +203,32 @@ class App():
201 for _ in range(num)] 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 try: 233 try:
210 test = self.pregenerated_tests.pop() 234 test = self.pregenerated_tests.pop()
@@ -215,10 +239,8 @@ class App(): @@ -215,10 +239,8 @@ class App():
215 else: 239 else:
216 logger.info('"%s" using a pregenerated test.', uid) 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 async def correct_test(self, uid, ans): 246 async def correct_test(self, uid, ans):
@@ -362,13 +384,7 @@ class App(): @@ -362,13 +384,7 @@ class App():
362 writer.writerows(grades) 384 writer.writerows(grades)
363 return self.testfactory['ref'], csvstr.getvalue() 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 def get_student_grades_from_all_tests(self, uid): 388 def get_student_grades_from_all_tests(self, uid):
373 '''get grades of student from all tests''' 389 '''get grades of student from all tests'''
374 with self._db_session() as sess: 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,8 +5,8 @@ Handles the web, http & html part of the application interface.
5 Uses the tornadoweb framework. 5 Uses the tornadoweb framework.
6 ''' 6 '''
7 7
8 -  
9 # python standard library 8 # python standard library
  9 +import asyncio
10 import base64 10 import base64
11 import functools 11 import functools
12 import json 12 import json
@@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler): @@ -161,6 +161,54 @@ 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 +# 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 # Test shown to students 212 # Test shown to students
165 # ---------------------------------------------------------------------------- 213 # ----------------------------------------------------------------------------
166 # pylint: disable=abstract-method 214 # pylint: disable=abstract-method
@@ -200,11 +248,9 @@ class RootHandler(BaseHandler): @@ -200,11 +248,9 @@ class RootHandler(BaseHandler):
200 248
201 if uid == '0': 249 if uid == '0':
202 self.redirect('/admin') 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 self.render('test.html', t=test, md=md_to_html, templ=self._templates) 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,7 +271,7 @@ class RootHandler(BaseHandler):
225 logging.debug('"%s" POST /', uid) 271 logging.debug('"%s" POST /', uid)
226 272
227 try: 273 try:
228 - test = self.testapp.get_student_test(uid) 274 + test = self.testapp.get_test(uid)
229 except KeyError as exc: 275 except KeyError as exc:
230 logging.warning('"%s" POST / raised 403 Forbidden', uid) 276 logging.warning('"%s" POST / raised 403 Forbidden', uid)
231 raise tornado.web.HTTPError(403) from exc # Forbidden 277 raise tornado.web.HTTPError(403) from exc # Forbidden
@@ -235,7 +281,6 @@ class RootHandler(BaseHandler): @@ -235,7 +281,6 @@ class RootHandler(BaseHandler):
235 qid = str(i) 281 qid = str(i)
236 if 'answered-' + qid in self.request.arguments: 282 if 'answered-' + qid in self.request.arguments:
237 ans[i] = self.get_body_arguments(qid) 283 ans[i] = self.get_body_arguments(qid)
238 - # print(i, ans[i])  
239 284
240 # remove enclosing list in some question types 285 # remove enclosing list in some question types
241 if question['type'] == 'radio': 286 if question['type'] == 'radio':
@@ -260,57 +305,6 @@ class RootHandler(BaseHandler): @@ -260,57 +305,6 @@ class RootHandler(BaseHandler):
260 timeit_finish = timer() 305 timeit_finish = timer()
261 logging.info(' correction took %fs', timeit_finish-timeit_start) 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 # pylint: disable=abstract-method 310 # pylint: disable=abstract-method
@@ -469,7 +463,6 @@ class FileHandler(BaseHandler): @@ -469,7 +463,6 @@ class FileHandler(BaseHandler):
469 break 463 break
470 464
471 465
472 -  
473 # --- REVIEW ----------------------------------------------------------------- 466 # --- REVIEW -----------------------------------------------------------------
474 # pylint: disable=abstract-method 467 # pylint: disable=abstract-method
475 class ReviewHandler(BaseHandler): 468 class ReviewHandler(BaseHandler):
perguntations/templates/test.html
@@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
44 <!-- ===================================================================== --> 44 <!-- ===================================================================== -->
45 <body> 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 <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div> 48 <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
49 </div> 49 </div>
50 50
@@ -94,10 +94,6 @@ @@ -94,10 +94,6 @@
94 94
95 <h5> 95 <h5>
96 <div class="row"> 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 <label for="duracao" class="col-sm-3">Duração:</label> 97 <label for="duracao" class="col-sm-3">Duração:</label>
102 <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div> 98 <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div>
103 </div> 99 </div>
perguntations/test.py
@@ -304,7 +304,7 @@ class Test(dict): @@ -304,7 +304,7 @@ class Test(dict):
304 # super().__init__(d) 304 # super().__init__(d)
305 305
306 # ------------------------------------------------------------------------ 306 # ------------------------------------------------------------------------
307 - def start(self, student): 307 + def register(self, student):
308 ''' 308 '''
309 Write student id in the test and register start time 309 Write student id in the test and register start time
310 ''' 310 '''
  1 +'''
  2 +Perguntations setup
  3 +'''
1 from setuptools import setup, find_packages 4 from setuptools import setup, find_packages
2 5
3 from perguntations import (__author__, __license__, 6 from perguntations import (__author__, __license__,