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) | ... | ... |