Commit 02299744d40277d335691a172e8547fa5bc13722

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

- 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
@@ -7,7 +7,7 @@ from contextlib import contextmanager # `with` statement in db sessions @@ -7,7 +7,7 @@ from contextlib import contextmanager # `with` statement in db sessions
7 # user installed packages 7 # user installed packages
8 import bcrypt 8 import bcrypt
9 from sqlalchemy import create_engine 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 # this project 12 # this project
13 from models import Student, Test, Question 13 from models import Student, Test, Question
@@ -21,19 +21,34 @@ class AppException(Exception): @@ -21,19 +21,34 @@ class AppException(Exception):
21 pass 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 # Application 30 # Application
25 # ============================================================================ 31 # ============================================================================
26 class App(object): 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 def __init__(self, conf={}): 49 def __init__(self, conf={}):
28 - # online = {  
29 - # uid1: {  
30 - # 'student': {'number': 123, 'name': john, ...},  
31 - # 'test': {...}  
32 - # },  
33 - # uid2: {...}  
34 - # }  
35 logger.info('Starting application') 50 logger.info('Starting application')
36 - self.online = dict() # {uid: {'student':{}}} 51 + self.online = dict() # {uid: {'student':{...}, 'test': {...}}, ...}
37 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere 52 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere
38 53
39 # build test configuration dictionary 54 # build test configuration dictionary
@@ -54,7 +69,8 @@ class App(object): @@ -54,7 +69,8 @@ class App(object):
54 # connect to database and check registered students 69 # connect to database and check registered students
55 dbfile = path.expanduser(self.testfactory['database']) 70 dbfile = path.expanduser(self.testfactory['database'])
56 engine = create_engine(f'sqlite:///{dbfile}', echo=False) 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 try: 75 try:
60 with self.db_session() as s: 76 with self.db_session() as s:
@@ -79,51 +95,37 @@ class App(object): @@ -79,51 +95,37 @@ class App(object):
79 logger.critical('----------- !!! Server terminated !!! -----------') 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 def login(self, uid, try_pw): 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 logger.warning(f'Student {uid}: not allowed to login.') 103 logger.warning(f'Student {uid}: not allowed to login.')
96 return False 104 return False
97 105
  106 + # get name+password from db
98 with self.db_session() as s: 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 if uid in self.online: 119 if uid in self.online:
121 logger.warning(f'Student {uid}: already logged in.') 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 logger.info(f'Student {uid}: logged in.') 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 def logout(self, uid): 131 def logout(self, uid):
@@ -174,7 +176,6 @@ class App(object): @@ -174,7 +176,6 @@ class App(object):
174 finishtime=str(t['finish_time']), 176 finishtime=str(t['finish_time']),
175 student_id=t['student']['number'], 177 student_id=t['student']['number'],
176 test_id=t['ref']) for q in t['questions'] if 'grade' in q]) 178 test_id=t['ref']) for q in t['questions'] if 'grade' in q])
177 - s.commit()  
178 179
179 logger.info(f'Student {uid}: finished test.') 180 logger.info(f'Student {uid}: finished test.')
180 return grade 181 return grade
@@ -203,7 +204,6 @@ class App(object): @@ -203,7 +204,6 @@ class App(object):
203 student_id=t['student']['number'], 204 student_id=t['student']['number'],
204 state=t['state'], 205 state=t['state'],
205 comment='')) 206 comment=''))
206 - s.commit()  
207 207
208 logger.info(f'Student {uid}: gave up.') 208 logger.info(f'Student {uid}: gave up.')
209 return t 209 return t
@@ -219,8 +219,11 @@ class App(object): @@ -219,8 +219,11 @@ class App(object):
219 return self.testfactory['questions_dir'] 219 return self.testfactory['questions_dir']
220 def get_student_grades_from_all_tests(self, uid): 220 def get_student_grades_from_all_tests(self, uid):
221 with self.db_session() as s: 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 def get_json_filename_of_test(self, test_id): 227 def get_json_filename_of_test(self, test_id):
225 with self.db_session() as s: 228 with self.db_session() as s:
226 return s.query(Test.filename).filter_by(id=test_id).one_or_none()[0] 229 return s.query(Test.filename).filter_by(id=test_id).one_or_none()[0]
@@ -236,12 +239,14 @@ class App(object): @@ -236,12 +239,14 @@ class App(object):
236 # list of all ('uid', 'name', 'password') sorted by uid 239 # list of all ('uid', 'name', 'password') sorted by uid
237 with self.db_session() as s: 240 with self.db_session() as s:
238 r = s.query(Student).all() 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 def get_student_grades_from_test(self, uid, testid): 244 def get_student_grades_from_test(self, uid, testid):
242 with self.db_session() as s: 245 with self.db_session() as s:
243 r = s.query(Test).filter_by(student_id=uid).filter_by(ref=testid).all() 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 def get_students_state(self): 251 def get_students_state(self):
247 # [{ 252 # [{
@@ -286,17 +291,18 @@ class App(object): @@ -286,17 +291,18 @@ class App(object):
286 self.allowed.discard(uid) 291 self.allowed.discard(uid)
287 logger.info(f'Student {uid}: denied to login') 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 with self.db_session() as s: 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 def insert_new_student(self, uid, name): 302 def insert_new_student(self, uid, name):
296 try: 303 try:
297 with self.db_session() as s: 304 with self.db_session() as s:
298 s.add(Student(id=uid, name=name, password='')) 305 s.add(Student(id=uid, name=name, password=''))
299 - s.commit()  
300 except Exception: 306 except Exception:
301 logger.error(f'Insert failed: student {uid} already exists.') 307 logger.error(f'Insert failed: student {uid} already exists.')
302 else: 308 else:
@@ -31,7 +31,7 @@ class WebApplication(tornado.web.Application): @@ -31,7 +31,7 @@ class WebApplication(tornado.web.Application):
31 (r'/test', TestHandler), 31 (r'/test', TestHandler),
32 (r'/review', ReviewHandler), 32 (r'/review', ReviewHandler),
33 (r'/admin', AdminHandler), 33 (r'/admin', AdminHandler),
34 - (r'/file', FileHandler), # FIXME 34 + (r'/file', FileHandler),
35 (r'/', RootHandler), # TODO multiple tests 35 (r'/', RootHandler), # TODO multiple tests
36 ] 36 ]
37 37
@@ -50,7 +50,7 @@ class WebApplication(tornado.web.Application): @@ -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 class BaseHandler(tornado.web.RequestHandler): 55 class BaseHandler(tornado.web.RequestHandler):
56 @property 56 @property
@@ -64,17 +64,17 @@ class BaseHandler(tornado.web.RequestHandler): @@ -64,17 +64,17 @@ class BaseHandler(tornado.web.RequestHandler):
64 64
65 65
66 # ------------------------------------------------------------------------- 66 # -------------------------------------------------------------------------
67 -# /login and /logout 67 +# /login
68 # ------------------------------------------------------------------------- 68 # -------------------------------------------------------------------------
69 class LoginHandler(BaseHandler): 69 class LoginHandler(BaseHandler):
  70 + SUPPORTED_METHODS = ['GET', 'POST']
  71 +
70 def get(self): 72 def get(self):
71 self.render('login.html', error='') 73 self.render('login.html', error='')
72 74
73 # async 75 # async
74 def post(self): 76 def post(self):
75 uid = self.get_body_argument('uid') 77 uid = self.get_body_argument('uid')
76 - if uid.startswith('l'): # remove prefix 'l'  
77 - uid = uid[1:]  
78 pw = self.get_body_argument('pw') 78 pw = self.get_body_argument('pw')
79 79
80 # loop = asyncio.get_event_loop() 80 # loop = asyncio.get_event_loop()
@@ -88,7 +88,8 @@ class LoginHandler(BaseHandler): @@ -88,7 +88,8 @@ class LoginHandler(BaseHandler):
88 self.render("login.html", 88 self.render("login.html",
89 error='Não autorizado ou número/senha inválido') 89 error='Não autorizado ou número/senha inválido')
90 90
91 - 91 +# -------------------------------------------------------------------------
  92 +# /logout
92 # ------------------------------------------------------------------------- 93 # -------------------------------------------------------------------------
93 class LogoutHandler(BaseHandler): 94 class LogoutHandler(BaseHandler):
94 @tornado.web.authenticated 95 @tornado.web.authenticated
@@ -113,6 +114,8 @@ class FileHandler(BaseHandler): @@ -113,6 +114,8 @@ class FileHandler(BaseHandler):
113 ref = self.get_query_argument('ref', None) 114 ref = self.get_query_argument('ref', None)
114 image = self.get_query_argument('image', None) 115 image = self.get_query_argument('image', None)
115 116
  117 + # FIXME does not work when user 0 is reviewing a test
  118 +
116 t = self.testapp.get_student_test(uid) 119 t = self.testapp.get_student_test(uid)
117 if t is not None: 120 if t is not None:
118 for q in t['questions']: 121 for q in t['questions']:
@@ -277,10 +280,12 @@ class RootHandler(BaseHandler): @@ -277,10 +280,12 @@ class RootHandler(BaseHandler):
277 280
278 # ------------------------------------------------------------------------- 281 # -------------------------------------------------------------------------
279 class AdminHandler(BaseHandler): 282 class AdminHandler(BaseHandler):
  283 + SUPPORTED_METHODS = ['GET', 'POST']
  284 +
280 @tornado.web.authenticated 285 @tornado.web.authenticated
281 def get(self): 286 def get(self):
282 if self.current_user != '0': 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 cmd = self.get_query_argument('cmd', default=None) 290 cmd = self.get_query_argument('cmd', default=None)
286 291
@@ -318,7 +323,7 @@ class AdminHandler(BaseHandler): @@ -318,7 +323,7 @@ class AdminHandler(BaseHandler):
318 self.testapp.deny_student(value) 323 self.testapp.deny_student(value)
319 324
320 elif cmd == 'reset_password': 325 elif cmd == 'reset_password':
321 - self.testapp.reset_password(value) 326 + self.testapp.update_student_password(uid=value, pw='')
322 327
323 elif cmd == 'insert_student': 328 elif cmd == 'insert_student':
324 s = json.loads(value) 329 s = json.loads(value)