Commit ed0097b4a829ff2dffa3883081a3241f5e7ec6cd
1 parent
66ba3c16
Exists in
master
and in
1 other branch
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
Showing
5 changed files
with
97 additions
and
89 deletions
Show diff stats
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 | ''' | ... | ... |