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 +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: |
serve.py
@@ -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) |