Commit 02299744d40277d335691a172e8547fa5bc13722
1 parent
d36a0c3e
Exists in
master
and in
1 other branch
- code reorganization in serve.py and app.py.
- login handler rewritten. Empty passwords are kept empty (ie, undefined). - fix: sqlalchemy use to work with Session instead of scoped_session, since tornado is single threaded. - fix: several helper functions were using the db_session contextmanager wrongly.
Showing
2 changed files
with
76 additions
and
65 deletions
Show diff stats
app.py
| ... | ... | @@ -7,7 +7,7 @@ from contextlib import contextmanager # `with` statement in db sessions |
| 7 | 7 | # user installed packages |
| 8 | 8 | import bcrypt |
| 9 | 9 | from sqlalchemy import create_engine |
| 10 | -from sqlalchemy.orm import sessionmaker, scoped_session | |
| 10 | +from sqlalchemy.orm import sessionmaker #, scoped_session | |
| 11 | 11 | |
| 12 | 12 | # this project |
| 13 | 13 | from models import Student, Test, Question |
| ... | ... | @@ -21,19 +21,34 @@ class AppException(Exception): |
| 21 | 21 | pass |
| 22 | 22 | |
| 23 | 23 | # ============================================================================ |
| 24 | +# helper functions | |
| 25 | +# ============================================================================ | |
| 26 | +def check_password(try_pw, password): | |
| 27 | + return password == bcrypt.hashpw(try_pw.encode('utf-8'), password) | |
| 28 | + | |
| 29 | +# ============================================================================ | |
| 24 | 30 | # Application |
| 25 | 31 | # ============================================================================ |
| 26 | 32 | class App(object): |
| 33 | + # ----------------------------------------------------------------------- | |
| 34 | + # helper to manage db sessions using the `with` statement, for example | |
| 35 | + # with self.db_session() as s: s.query(...) | |
| 36 | + @contextmanager | |
| 37 | + def db_session(self): | |
| 38 | + session = self.Session() | |
| 39 | + try: | |
| 40 | + yield session | |
| 41 | + session.commit() | |
| 42 | + except: | |
| 43 | + session.rollback() | |
| 44 | + logger.error('DB rollback!!!') | |
| 45 | + finally: | |
| 46 | + session.close() | |
| 47 | + | |
| 48 | + # ------------------------------------------------------------------------ | |
| 27 | 49 | def __init__(self, conf={}): |
| 28 | - # online = { | |
| 29 | - # uid1: { | |
| 30 | - # 'student': {'number': 123, 'name': john, ...}, | |
| 31 | - # 'test': {...} | |
| 32 | - # }, | |
| 33 | - # uid2: {...} | |
| 34 | - # } | |
| 35 | 50 | logger.info('Starting application') |
| 36 | - self.online = dict() # {uid: {'student':{}}} | |
| 51 | + self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...} | |
| 37 | 52 | self.allowed = set([]) # '0' is hardcoded to allowed elsewhere |
| 38 | 53 | |
| 39 | 54 | # build test configuration dictionary |
| ... | ... | @@ -54,7 +69,8 @@ class App(object): |
| 54 | 69 | # connect to database and check registered students |
| 55 | 70 | dbfile = path.expanduser(self.testfactory['database']) |
| 56 | 71 | engine = create_engine(f'sqlite:///{dbfile}', echo=False) |
| 57 | - self.Session = scoped_session(sessionmaker(bind=engine)) # FIXME not scoped in tornado | |
| 72 | + # self.Session = scoped_session(sessionmaker(bind=engine)) # FIXME not scoped in tornado | |
| 73 | + self.Session = sessionmaker(bind=engine) | |
| 58 | 74 | |
| 59 | 75 | try: |
| 60 | 76 | with self.db_session() as s: |
| ... | ... | @@ -79,51 +95,37 @@ class App(object): |
| 79 | 95 | logger.critical('----------- !!! Server terminated !!! -----------') |
| 80 | 96 | |
| 81 | 97 | # ----------------------------------------------------------------------- |
| 82 | - # helper to manage db sessions using the `with` statement, for example | |
| 83 | - # with self.db_session() as s: s.query(...) | |
| 84 | - @contextmanager | |
| 85 | - def db_session(self): | |
| 86 | - try: | |
| 87 | - yield self.Session() | |
| 88 | - finally: | |
| 89 | - self.Session.remove() | |
| 90 | - | |
| 91 | - # ----------------------------------------------------------------------- | |
| 92 | 98 | def login(self, uid, try_pw): |
| 93 | - if uid not in self.allowed and uid != '0': | |
| 94 | - # not allowed | |
| 99 | + if uid.startswith('l'): # remove prefix 'l' | |
| 100 | + uid = uid[1:] | |
| 101 | + | |
| 102 | + if uid not in self.allowed and uid != '0': # not allowed | |
| 95 | 103 | logger.warning(f'Student {uid}: not allowed to login.') |
| 96 | 104 | return False |
| 97 | 105 | |
| 106 | + # get name+password from db | |
| 98 | 107 | with self.db_session() as s: |
| 99 | - student = s.query(Student).filter(Student.id == uid).one_or_none() | |
| 100 | - | |
| 101 | - if student is None: | |
| 102 | - # not found | |
| 103 | - logger.warning(f'Student {uid}: not found in database.') | |
| 104 | - return False | |
| 105 | - | |
| 106 | - if student.password == '': | |
| 107 | - # update password on first login | |
| 108 | - hashed_pw = bcrypt.hashpw(try_pw.encode('utf-8'), bcrypt.gensalt()) | |
| 109 | - student.password = hashed_pw | |
| 110 | - s.commit() | |
| 111 | - logger.info(f'Student {uid}: first login, password updated.') | |
| 112 | - | |
| 113 | - elif bcrypt.hashpw(try_pw.encode('utf-8'), student.password) != student.password: | |
| 114 | - # wrong password | |
| 115 | - logger.info(f'Student {uid}: wrong password.') | |
| 116 | - return False | |
| 117 | - | |
| 118 | - # success | |
| 119 | - self.allowed.discard(uid) | |
| 108 | + name, password = s.query(Student.name, Student.password).filter_by(id=uid).one() | |
| 109 | + | |
| 110 | + # first login updates the password | |
| 111 | + if password == '': # update password on first login | |
| 112 | + self.update_student_password(uid, try_pw) | |
| 113 | + pw_ok = True | |
| 114 | + else: # check password | |
| 115 | + pw_ok = check_password(try_pw, password) | |
| 116 | + | |
| 117 | + if pw_ok: # success | |
| 118 | + self.allowed.discard(uid) # remove from set of allowed students | |
| 120 | 119 | if uid in self.online: |
| 121 | 120 | logger.warning(f'Student {uid}: already logged in.') |
| 122 | - else: | |
| 123 | - self.online[uid] = {'student': {'name': student.name, 'number': uid}} | |
| 121 | + else: # make student online | |
| 122 | + self.online[uid] = {'student': {'name': name, 'number': uid}} | |
| 124 | 123 | logger.info(f'Student {uid}: logged in.') |
| 124 | + return True | |
| 125 | + else: # wrong password | |
| 126 | + logger.info(f'Student {uid}: wrong password.') | |
| 127 | + return False | |
| 125 | 128 | |
| 126 | - return True | |
| 127 | 129 | |
| 128 | 130 | # ----------------------------------------------------------------------- |
| 129 | 131 | def logout(self, uid): |
| ... | ... | @@ -174,7 +176,6 @@ class App(object): |
| 174 | 176 | finishtime=str(t['finish_time']), |
| 175 | 177 | student_id=t['student']['number'], |
| 176 | 178 | test_id=t['ref']) for q in t['questions'] if 'grade' in q]) |
| 177 | - s.commit() | |
| 178 | 179 | |
| 179 | 180 | logger.info(f'Student {uid}: finished test.') |
| 180 | 181 | return grade |
| ... | ... | @@ -203,7 +204,6 @@ class App(object): |
| 203 | 204 | student_id=t['student']['number'], |
| 204 | 205 | state=t['state'], |
| 205 | 206 | comment='')) |
| 206 | - s.commit() | |
| 207 | 207 | |
| 208 | 208 | logger.info(f'Student {uid}: gave up.') |
| 209 | 209 | return t |
| ... | ... | @@ -219,8 +219,11 @@ class App(object): |
| 219 | 219 | return self.testfactory['questions_dir'] |
| 220 | 220 | def get_student_grades_from_all_tests(self, uid): |
| 221 | 221 | with self.db_session() as s: |
| 222 | - r = s.query(Test).filter_by(student_id=uid).order_by(Test.finishtime).all() | |
| 223 | - return [(t.title, t.grade, t.finishtime) for t in r] | |
| 222 | + # r = s.query(Test).filter_by(student_id=uid).order_by(Test.finishtime).all() | |
| 223 | + # return [(t.title, t.grade, t.finishtime) for t in r] | |
| 224 | + print('here') | |
| 225 | + return s.query(Test.title, Test.grade, Test.finishtime).filter_by(student_id=uid).order_by(Test.finishtime).all() | |
| 226 | + | |
| 224 | 227 | def get_json_filename_of_test(self, test_id): |
| 225 | 228 | with self.db_session() as s: |
| 226 | 229 | return s.query(Test.filename).filter_by(id=test_id).one_or_none()[0] |
| ... | ... | @@ -236,12 +239,14 @@ class App(object): |
| 236 | 239 | # list of all ('uid', 'name', 'password') sorted by uid |
| 237 | 240 | with self.db_session() as s: |
| 238 | 241 | r = s.query(Student).all() |
| 239 | - return sorted(((u.id, u.name, u.password) for u in r if u.id != '0'), key=lambda k: k[0]) | |
| 242 | + return sorted(((u.id, u.name, u.password) for u in r if u.id != '0'), key=lambda k: k[0]) | |
| 240 | 243 | |
| 241 | 244 | def get_student_grades_from_test(self, uid, testid): |
| 242 | 245 | with self.db_session() as s: |
| 243 | 246 | r = s.query(Test).filter_by(student_id=uid).filter_by(ref=testid).all() |
| 244 | - return [(u.grade, u.finishtime, u.id) for u in r] | |
| 247 | + return [(u.grade, u.finishtime, u.id) for u in r] | |
| 248 | + | |
| 249 | + | |
| 245 | 250 | |
| 246 | 251 | def get_students_state(self): |
| 247 | 252 | # [{ |
| ... | ... | @@ -286,17 +291,18 @@ class App(object): |
| 286 | 291 | self.allowed.discard(uid) |
| 287 | 292 | logger.info(f'Student {uid}: denied to login') |
| 288 | 293 | |
| 289 | - def reset_password(self, uid): | |
| 294 | + def update_student_password(self, uid, pw=''): | |
| 295 | + if pw: | |
| 296 | + pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) | |
| 290 | 297 | with self.db_session() as s: |
| 291 | - u = s.query(Student).filter(Student.id == uid).update({'password': ''}) | |
| 292 | - s.commit() | |
| 293 | - logger.info(f'Student {uid}: password reset') | |
| 298 | + student = s.query(Student).filter_by(id=uid).one() | |
| 299 | + student.password = pw | |
| 300 | + logger.info(f'Student {uid}: password updated.') | |
| 294 | 301 | |
| 295 | 302 | def insert_new_student(self, uid, name): |
| 296 | 303 | try: |
| 297 | 304 | with self.db_session() as s: |
| 298 | 305 | s.add(Student(id=uid, name=name, password='')) |
| 299 | - s.commit() | |
| 300 | 306 | except Exception: |
| 301 | 307 | logger.error(f'Insert failed: student {uid} already exists.') |
| 302 | 308 | else: | ... | ... |
serve.py
| ... | ... | @@ -31,7 +31,7 @@ class WebApplication(tornado.web.Application): |
| 31 | 31 | (r'/test', TestHandler), |
| 32 | 32 | (r'/review', ReviewHandler), |
| 33 | 33 | (r'/admin', AdminHandler), |
| 34 | - (r'/file', FileHandler), # FIXME | |
| 34 | + (r'/file', FileHandler), | |
| 35 | 35 | (r'/', RootHandler), # TODO multiple tests |
| 36 | 36 | ] |
| 37 | 37 | |
| ... | ... | @@ -50,7 +50,7 @@ class WebApplication(tornado.web.Application): |
| 50 | 50 | |
| 51 | 51 | |
| 52 | 52 | # ------------------------------------------------------------------------- |
| 53 | -# Base handler common to all handlers. | |
| 53 | +# Base handler. Other handlers will inherit this one. | |
| 54 | 54 | # ------------------------------------------------------------------------- |
| 55 | 55 | class BaseHandler(tornado.web.RequestHandler): |
| 56 | 56 | @property |
| ... | ... | @@ -64,17 +64,17 @@ class BaseHandler(tornado.web.RequestHandler): |
| 64 | 64 | |
| 65 | 65 | |
| 66 | 66 | # ------------------------------------------------------------------------- |
| 67 | -# /login and /logout | |
| 67 | +# /login | |
| 68 | 68 | # ------------------------------------------------------------------------- |
| 69 | 69 | class LoginHandler(BaseHandler): |
| 70 | + SUPPORTED_METHODS = ['GET', 'POST'] | |
| 71 | + | |
| 70 | 72 | def get(self): |
| 71 | 73 | self.render('login.html', error='') |
| 72 | 74 | |
| 73 | 75 | # async |
| 74 | 76 | def post(self): |
| 75 | 77 | uid = self.get_body_argument('uid') |
| 76 | - if uid.startswith('l'): # remove prefix 'l' | |
| 77 | - uid = uid[1:] | |
| 78 | 78 | pw = self.get_body_argument('pw') |
| 79 | 79 | |
| 80 | 80 | # loop = asyncio.get_event_loop() |
| ... | ... | @@ -88,7 +88,8 @@ class LoginHandler(BaseHandler): |
| 88 | 88 | self.render("login.html", |
| 89 | 89 | error='Não autorizado ou número/senha inválido') |
| 90 | 90 | |
| 91 | - | |
| 91 | +# ------------------------------------------------------------------------- | |
| 92 | +# /logout | |
| 92 | 93 | # ------------------------------------------------------------------------- |
| 93 | 94 | class LogoutHandler(BaseHandler): |
| 94 | 95 | @tornado.web.authenticated |
| ... | ... | @@ -113,6 +114,8 @@ class FileHandler(BaseHandler): |
| 113 | 114 | ref = self.get_query_argument('ref', None) |
| 114 | 115 | image = self.get_query_argument('image', None) |
| 115 | 116 | |
| 117 | + # FIXME does not work when user 0 is reviewing a test | |
| 118 | + | |
| 116 | 119 | t = self.testapp.get_student_test(uid) |
| 117 | 120 | if t is not None: |
| 118 | 121 | for q in t['questions']: |
| ... | ... | @@ -277,10 +280,12 @@ class RootHandler(BaseHandler): |
| 277 | 280 | |
| 278 | 281 | # ------------------------------------------------------------------------- |
| 279 | 282 | class AdminHandler(BaseHandler): |
| 283 | + SUPPORTED_METHODS = ['GET', 'POST'] | |
| 284 | + | |
| 280 | 285 | @tornado.web.authenticated |
| 281 | 286 | def get(self): |
| 282 | 287 | if self.current_user != '0': |
| 283 | - raise tornado.web.HTTPError(404) # FIXME denied or not found?? | |
| 288 | + raise tornado.web.HTTPError(403) | |
| 284 | 289 | |
| 285 | 290 | cmd = self.get_query_argument('cmd', default=None) |
| 286 | 291 | |
| ... | ... | @@ -318,7 +323,7 @@ class AdminHandler(BaseHandler): |
| 318 | 323 | self.testapp.deny_student(value) |
| 319 | 324 | |
| 320 | 325 | elif cmd == 'reset_password': |
| 321 | - self.testapp.reset_password(value) | |
| 326 | + self.testapp.update_student_password(uid=value, pw='') | |
| 322 | 327 | |
| 323 | 328 | elif cmd == 'insert_student': |
| 324 | 329 | s = json.loads(value) | ... | ... |