diff --git a/perguntations/app.py b/perguntations/app.py index 672ce3d..325e630 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -113,8 +113,7 @@ class App(): else: logger.info('Students not yet allowed to login.') - # pre-generate tests - + # pre-generate tests for allowed students if self.allowed: logger.info('Generating %d tests. May take awhile...', len(self.allowed)) @@ -125,20 +124,15 @@ class App(): # ------------------------------------------------------------------------ async def login(self, uid, try_pw, headers=None): '''login authentication''' - if uid in self.online: - logger.warning('"%s" already logged in.', uid) - return 'already_online' if uid not in self.allowed and uid != '0': # not allowed logger.warning('"%s" unauthorized.', uid) return 'unauthorized' - # get name+password from db with self._db_session() as sess: name, password = sess.query(Student.name, Student.password)\ .filter_by(id=uid)\ .one() - # first login updates the password if password == '': # update password on first login await self.update_student_password(uid, try_pw) pw_ok = True @@ -151,9 +145,17 @@ class App(): # success self.allowed.discard(uid) # remove from set of allowed students - self.online[uid] = {'student': {'name': name, 'number': uid, 'headers': headers}} - logger.info('"%s" logged in from %s.', uid, headers['remote_ip']) + if uid in self.online: + logger.warning('"%s" login again from %s (reusing state).', + uid, headers['remote_ip']) + # FIXME invalidate previous login + else: + self.online[uid] = {'student': { + 'name': name, + 'number': uid, + 'headers': headers}} + logger.info('"%s" login from %s.', uid, headers['remote_ip']) # ------------------------------------------------------------------------ def logout(self, uid): @@ -185,7 +187,7 @@ class App(): self.testfactory = TestFactory(testconf) except TestFactoryException as exc: logger.critical(exc) - raise AppException('Failed to create test factory!') from exc + raise AppException('Failed to create test factory!') from exc logger.info('Test factory ready. No errors found.') @@ -201,10 +203,32 @@ class App(): for _ in range(num)] # ------------------------------------------------------------------------ - async def generate_test(self, uid): - '''generate a test for a given student. the student must be online''' + async def get_test_or_generate(self, uid): + '''get current test or generate a new one''' + try: + student = self.online[uid] + except KeyError as exc: + msg = f'"{uid}" is not online. get_test_or_generate() FAILED' + logger.error(msg) + raise AppException(msg) from exc + + # get current test. if test does not exist then generate a new one + if not 'test' in student: + await self._new_test(uid) - student_id = self.online[uid]['student'] # {'name': ?, 'number': ?} + return student['test'] + + def get_test(self, uid): + '''get test from online student or raise exception''' + return self.online[uid]['test'] + + async def _new_test(self, uid): + ''' + assign a test to a given student. if there are pregenerated tests then + use one of them, otherwise generate one. + the student must be online + ''' + student = self.online[uid]['student'] # {'name': ?, 'number': ?} try: test = self.pregenerated_tests.pop() @@ -215,10 +239,8 @@ class App(): else: logger.info('"%s" using a pregenerated test.', uid) - test.start(student_id) # student signs the test - self.online[uid]['test'] = test # register test for this student - - return self.online[uid]['test'] + test.register(student) # student signs the test + self.online[uid]['test'] = test # ------------------------------------------------------------------------ async def correct_test(self, uid, ans): @@ -362,13 +384,7 @@ class App(): writer.writerows(grades) return self.testfactory['ref'], csvstr.getvalue() - def get_student_test(self, uid): - '''get test from online student or None if no test was generated yet''' - return self.online[uid].get('test', None) - - # def get_questions_dir(self): - # return self.testfactory['questions_dir'] - + # ------------------------------------------------------------------------ def get_student_grades_from_all_tests(self, uid): '''get grades of student from all tests''' with self._db_session() as sess: diff --git a/perguntations/serve.py b/perguntations/serve.py index f1a4008..1f6e521 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -5,8 +5,8 @@ Handles the web, http & html part of the application interface. Uses the tornadoweb framework. ''' - # python standard library +import asyncio import base64 import functools import json @@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler): # AdminSocketHandler.send_updates(chat) # send to clients # ---------------------------------------------------------------------------- +# pylint: disable=abstract-method +class LoginHandler(BaseHandler): + '''Handles /login''' + + _prefix = re.compile(r'[a-z]') + _error_msg = { + 'wrong_password': 'Password errada', + 'already_online': 'Já está online, não pode entrar duas vezes', + 'unauthorized': 'Não está autorizado a fazer o teste' + } + + def get(self): + '''Render login page.''' + self.render('login.html', error='') + + async def post(self): + '''Authenticates student and login.''' + uid = self._prefix.sub('', self.get_body_argument('uid')) + password = self.get_body_argument('pw') + headers = { + 'remote_ip': self.request.remote_ip, + 'user_agent': self.request.headers.get('User-Agent') + } + + error = await self.testapp.login(uid, password, headers) + + if error is None: + self.set_secure_cookie('perguntations_user', str(uid)) + self.redirect('/') + else: + await asyncio.sleep(3) # to avoid spamming the server... + self.render('login.html', error=self._error_msg[error]) + + +# ---------------------------------------------------------------------------- +# pylint: disable=abstract-method +class LogoutHandler(BaseHandler): + '''Handle /logout''' + + @tornado.web.authenticated + def get(self): + '''Logs out a user.''' + self.clear_cookie('perguntations_user') + self.testapp.logout(self.current_user) + self.render('login.html', error='') + + +# ---------------------------------------------------------------------------- # Test shown to students # ---------------------------------------------------------------------------- # pylint: disable=abstract-method @@ -200,11 +248,9 @@ class RootHandler(BaseHandler): if uid == '0': self.redirect('/admin') + return - test = self.testapp.get_student_test(uid) # reloading returns same test - if test is None: - test = await self.testapp.generate_test(uid) - + test = await self.testapp.get_test_or_generate(uid) self.render('test.html', t=test, md=md_to_html, templ=self._templates) @@ -225,7 +271,7 @@ class RootHandler(BaseHandler): logging.debug('"%s" POST /', uid) try: - test = self.testapp.get_student_test(uid) + test = self.testapp.get_test(uid) except KeyError as exc: logging.warning('"%s" POST / raised 403 Forbidden', uid) raise tornado.web.HTTPError(403) from exc # Forbidden @@ -235,7 +281,6 @@ class RootHandler(BaseHandler): qid = str(i) if 'answered-' + qid in self.request.arguments: ans[i] = self.get_body_arguments(qid) - # print(i, ans[i]) # remove enclosing list in some question types if question['type'] == 'radio': @@ -260,57 +305,6 @@ class RootHandler(BaseHandler): timeit_finish = timer() logging.info(' correction took %fs', timeit_finish-timeit_start) -# ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class LoginHandler(BaseHandler): - '''Handles /login''' - - _prefix = re.compile(r'[a-z]') - _error_msg = { - 'wrong_password': 'Password errada', - 'already_online': 'Já está online, não pode entrar duas vezes', - 'unauthorized': 'Não está autorizado a fazer o teste' - } - - def get(self): - '''Render login page.''' - self.render('login.html', error='') - - async def post(self): - '''Authenticates student and login.''' - uid = self._prefix.sub('', self.get_body_argument('uid')) - password = self.get_body_argument('pw') - headers = { - 'remote_ip': self.request.remote_ip, - 'user_agent': self.request.headers.get('User-Agent') - } - - error = await self.testapp.login(uid, password, headers) - - if error is None: - self.set_secure_cookie('perguntations_user', str(uid), expires_days=1) - self.redirect('/') - else: - self.render('login.html', error=self._error_msg[error]) - - -# ---------------------------------------------------------------------------- -# pylint: disable=abstract-method -class LogoutHandler(BaseHandler): - '''Handle /logout''' - - @tornado.web.authenticated - def get(self): - '''Logs out a user.''' - self.clear_cookie('perguntations_user') - self.testapp.logout(self.current_user) - self.redirect('/') - - def on_finish(self): - self.testapp.logout(self.current_user) - - - # ---------------------------------------------------------------------------- # pylint: disable=abstract-method @@ -469,7 +463,6 @@ class FileHandler(BaseHandler): break - # --- REVIEW ----------------------------------------------------------------- # pylint: disable=abstract-method class ReviewHandler(BaseHandler): diff --git a/perguntations/templates/test.html b/perguntations/templates/test.html index 18626f5..6a02285 100644 --- a/perguntations/templates/test.html +++ b/perguntations/templates/test.html @@ -44,7 +44,7 @@
-