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