Commit ed0097b4a829ff2dffa3883081a3241f5e7ec6cd
1 parent
66ba3c16
Exists in
master
and in
1 other branch
removes redirect to /test, test is served on the root /
trying to fix redirect failure on login and grade (not stress tested) login messages explain the error on failed logins some refactoring
Showing
5 changed files
with
97 additions
and
89 deletions
Show diff stats
perguntations/app.py
@@ -113,8 +113,7 @@ class App(): | @@ -113,8 +113,7 @@ class App(): | ||
113 | else: | 113 | else: |
114 | logger.info('Students not yet allowed to login.') | 114 | logger.info('Students not yet allowed to login.') |
115 | 115 | ||
116 | - # pre-generate tests | ||
117 | - | 116 | + # pre-generate tests for allowed students |
118 | if self.allowed: | 117 | if self.allowed: |
119 | logger.info('Generating %d tests. May take awhile...', | 118 | logger.info('Generating %d tests. May take awhile...', |
120 | len(self.allowed)) | 119 | len(self.allowed)) |
@@ -125,20 +124,15 @@ class App(): | @@ -125,20 +124,15 @@ class App(): | ||
125 | # ------------------------------------------------------------------------ | 124 | # ------------------------------------------------------------------------ |
126 | async def login(self, uid, try_pw, headers=None): | 125 | async def login(self, uid, try_pw, headers=None): |
127 | '''login authentication''' | 126 | '''login authentication''' |
128 | - if uid in self.online: | ||
129 | - logger.warning('"%s" already logged in.', uid) | ||
130 | - return 'already_online' | ||
131 | if uid not in self.allowed and uid != '0': # not allowed | 127 | if uid not in self.allowed and uid != '0': # not allowed |
132 | logger.warning('"%s" unauthorized.', uid) | 128 | logger.warning('"%s" unauthorized.', uid) |
133 | return 'unauthorized' | 129 | return 'unauthorized' |
134 | 130 | ||
135 | - # get name+password from db | ||
136 | with self._db_session() as sess: | 131 | with self._db_session() as sess: |
137 | name, password = sess.query(Student.name, Student.password)\ | 132 | name, password = sess.query(Student.name, Student.password)\ |
138 | .filter_by(id=uid)\ | 133 | .filter_by(id=uid)\ |
139 | .one() | 134 | .one() |
140 | 135 | ||
141 | - # first login updates the password | ||
142 | if password == '': # update password on first login | 136 | if password == '': # update password on first login |
143 | await self.update_student_password(uid, try_pw) | 137 | await self.update_student_password(uid, try_pw) |
144 | pw_ok = True | 138 | pw_ok = True |
@@ -151,9 +145,17 @@ class App(): | @@ -151,9 +145,17 @@ class App(): | ||
151 | 145 | ||
152 | # success | 146 | # success |
153 | self.allowed.discard(uid) # remove from set of allowed students | 147 | self.allowed.discard(uid) # remove from set of allowed students |
154 | - self.online[uid] = {'student': {'name': name, 'number': uid, 'headers': headers}} | ||
155 | - logger.info('"%s" logged in from %s.', uid, headers['remote_ip']) | ||
156 | 148 | ||
149 | + if uid in self.online: | ||
150 | + logger.warning('"%s" login again from %s (reusing state).', | ||
151 | + uid, headers['remote_ip']) | ||
152 | + # FIXME invalidate previous login | ||
153 | + else: | ||
154 | + self.online[uid] = {'student': { | ||
155 | + 'name': name, | ||
156 | + 'number': uid, | ||
157 | + 'headers': headers}} | ||
158 | + logger.info('"%s" login from %s.', uid, headers['remote_ip']) | ||
157 | 159 | ||
158 | # ------------------------------------------------------------------------ | 160 | # ------------------------------------------------------------------------ |
159 | def logout(self, uid): | 161 | def logout(self, uid): |
@@ -185,7 +187,7 @@ class App(): | @@ -185,7 +187,7 @@ class App(): | ||
185 | self.testfactory = TestFactory(testconf) | 187 | self.testfactory = TestFactory(testconf) |
186 | except TestFactoryException as exc: | 188 | except TestFactoryException as exc: |
187 | logger.critical(exc) | 189 | logger.critical(exc) |
188 | - raise AppException('Failed to create test factory!') from exc | 190 | + raise AppException('Failed to create test factory!') from exc |
189 | 191 | ||
190 | logger.info('Test factory ready. No errors found.') | 192 | logger.info('Test factory ready. No errors found.') |
191 | 193 | ||
@@ -201,10 +203,32 @@ class App(): | @@ -201,10 +203,32 @@ class App(): | ||
201 | for _ in range(num)] | 203 | for _ in range(num)] |
202 | 204 | ||
203 | # ------------------------------------------------------------------------ | 205 | # ------------------------------------------------------------------------ |
204 | - async def generate_test(self, uid): | ||
205 | - '''generate a test for a given student. the student must be online''' | 206 | + async def get_test_or_generate(self, uid): |
207 | + '''get current test or generate a new one''' | ||
208 | + try: | ||
209 | + student = self.online[uid] | ||
210 | + except KeyError as exc: | ||
211 | + msg = f'"{uid}" is not online. get_test_or_generate() FAILED' | ||
212 | + logger.error(msg) | ||
213 | + raise AppException(msg) from exc | ||
214 | + | ||
215 | + # get current test. if test does not exist then generate a new one | ||
216 | + if not 'test' in student: | ||
217 | + await self._new_test(uid) | ||
206 | 218 | ||
207 | - student_id = self.online[uid]['student'] # {'name': ?, 'number': ?} | 219 | + return student['test'] |
220 | + | ||
221 | + def get_test(self, uid): | ||
222 | + '''get test from online student or raise exception''' | ||
223 | + return self.online[uid]['test'] | ||
224 | + | ||
225 | + async def _new_test(self, uid): | ||
226 | + ''' | ||
227 | + assign a test to a given student. if there are pregenerated tests then | ||
228 | + use one of them, otherwise generate one. | ||
229 | + the student must be online | ||
230 | + ''' | ||
231 | + student = self.online[uid]['student'] # {'name': ?, 'number': ?} | ||
208 | 232 | ||
209 | try: | 233 | try: |
210 | test = self.pregenerated_tests.pop() | 234 | test = self.pregenerated_tests.pop() |
@@ -215,10 +239,8 @@ class App(): | @@ -215,10 +239,8 @@ class App(): | ||
215 | else: | 239 | else: |
216 | logger.info('"%s" using a pregenerated test.', uid) | 240 | logger.info('"%s" using a pregenerated test.', uid) |
217 | 241 | ||
218 | - test.start(student_id) # student signs the test | ||
219 | - self.online[uid]['test'] = test # register test for this student | ||
220 | - | ||
221 | - return self.online[uid]['test'] | 242 | + test.register(student) # student signs the test |
243 | + self.online[uid]['test'] = test | ||
222 | 244 | ||
223 | # ------------------------------------------------------------------------ | 245 | # ------------------------------------------------------------------------ |
224 | async def correct_test(self, uid, ans): | 246 | async def correct_test(self, uid, ans): |
@@ -362,13 +384,7 @@ class App(): | @@ -362,13 +384,7 @@ class App(): | ||
362 | writer.writerows(grades) | 384 | writer.writerows(grades) |
363 | return self.testfactory['ref'], csvstr.getvalue() | 385 | return self.testfactory['ref'], csvstr.getvalue() |
364 | 386 | ||
365 | - def get_student_test(self, uid): | ||
366 | - '''get test from online student or None if no test was generated yet''' | ||
367 | - return self.online[uid].get('test', None) | ||
368 | - | ||
369 | - # def get_questions_dir(self): | ||
370 | - # return self.testfactory['questions_dir'] | ||
371 | - | 387 | + # ------------------------------------------------------------------------ |
372 | def get_student_grades_from_all_tests(self, uid): | 388 | def get_student_grades_from_all_tests(self, uid): |
373 | '''get grades of student from all tests''' | 389 | '''get grades of student from all tests''' |
374 | with self._db_session() as sess: | 390 | with self._db_session() as sess: |
perguntations/serve.py
@@ -5,8 +5,8 @@ Handles the web, http & html part of the application interface. | @@ -5,8 +5,8 @@ Handles the web, http & html part of the application interface. | ||
5 | Uses the tornadoweb framework. | 5 | Uses the tornadoweb framework. |
6 | ''' | 6 | ''' |
7 | 7 | ||
8 | - | ||
9 | # python standard library | 8 | # python standard library |
9 | +import asyncio | ||
10 | import base64 | 10 | import base64 |
11 | import functools | 11 | import functools |
12 | import json | 12 | import json |
@@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler): | @@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler): | ||
161 | # AdminSocketHandler.send_updates(chat) # send to clients | 161 | # AdminSocketHandler.send_updates(chat) # send to clients |
162 | 162 | ||
163 | # ---------------------------------------------------------------------------- | 163 | # ---------------------------------------------------------------------------- |
164 | +# pylint: disable=abstract-method | ||
165 | +class LoginHandler(BaseHandler): | ||
166 | + '''Handles /login''' | ||
167 | + | ||
168 | + _prefix = re.compile(r'[a-z]') | ||
169 | + _error_msg = { | ||
170 | + 'wrong_password': 'Password errada', | ||
171 | + 'already_online': 'Já está online, não pode entrar duas vezes', | ||
172 | + 'unauthorized': 'Não está autorizado a fazer o teste' | ||
173 | + } | ||
174 | + | ||
175 | + def get(self): | ||
176 | + '''Render login page.''' | ||
177 | + self.render('login.html', error='') | ||
178 | + | ||
179 | + async def post(self): | ||
180 | + '''Authenticates student and login.''' | ||
181 | + uid = self._prefix.sub('', self.get_body_argument('uid')) | ||
182 | + password = self.get_body_argument('pw') | ||
183 | + headers = { | ||
184 | + 'remote_ip': self.request.remote_ip, | ||
185 | + 'user_agent': self.request.headers.get('User-Agent') | ||
186 | + } | ||
187 | + | ||
188 | + error = await self.testapp.login(uid, password, headers) | ||
189 | + | ||
190 | + if error is None: | ||
191 | + self.set_secure_cookie('perguntations_user', str(uid)) | ||
192 | + self.redirect('/') | ||
193 | + else: | ||
194 | + await asyncio.sleep(3) # to avoid spamming the server... | ||
195 | + self.render('login.html', error=self._error_msg[error]) | ||
196 | + | ||
197 | + | ||
198 | +# ---------------------------------------------------------------------------- | ||
199 | +# pylint: disable=abstract-method | ||
200 | +class LogoutHandler(BaseHandler): | ||
201 | + '''Handle /logout''' | ||
202 | + | ||
203 | + @tornado.web.authenticated | ||
204 | + def get(self): | ||
205 | + '''Logs out a user.''' | ||
206 | + self.clear_cookie('perguntations_user') | ||
207 | + self.testapp.logout(self.current_user) | ||
208 | + self.render('login.html', error='') | ||
209 | + | ||
210 | + | ||
211 | +# ---------------------------------------------------------------------------- | ||
164 | # Test shown to students | 212 | # Test shown to students |
165 | # ---------------------------------------------------------------------------- | 213 | # ---------------------------------------------------------------------------- |
166 | # pylint: disable=abstract-method | 214 | # pylint: disable=abstract-method |
@@ -200,11 +248,9 @@ class RootHandler(BaseHandler): | @@ -200,11 +248,9 @@ class RootHandler(BaseHandler): | ||
200 | 248 | ||
201 | if uid == '0': | 249 | if uid == '0': |
202 | self.redirect('/admin') | 250 | self.redirect('/admin') |
251 | + return | ||
203 | 252 | ||
204 | - test = self.testapp.get_student_test(uid) # reloading returns same test | ||
205 | - if test is None: | ||
206 | - test = await self.testapp.generate_test(uid) | ||
207 | - | 253 | + test = await self.testapp.get_test_or_generate(uid) |
208 | self.render('test.html', t=test, md=md_to_html, templ=self._templates) | 254 | self.render('test.html', t=test, md=md_to_html, templ=self._templates) |
209 | 255 | ||
210 | 256 | ||
@@ -225,7 +271,7 @@ class RootHandler(BaseHandler): | @@ -225,7 +271,7 @@ class RootHandler(BaseHandler): | ||
225 | logging.debug('"%s" POST /', uid) | 271 | logging.debug('"%s" POST /', uid) |
226 | 272 | ||
227 | try: | 273 | try: |
228 | - test = self.testapp.get_student_test(uid) | 274 | + test = self.testapp.get_test(uid) |
229 | except KeyError as exc: | 275 | except KeyError as exc: |
230 | logging.warning('"%s" POST / raised 403 Forbidden', uid) | 276 | logging.warning('"%s" POST / raised 403 Forbidden', uid) |
231 | raise tornado.web.HTTPError(403) from exc # Forbidden | 277 | raise tornado.web.HTTPError(403) from exc # Forbidden |
@@ -235,7 +281,6 @@ class RootHandler(BaseHandler): | @@ -235,7 +281,6 @@ class RootHandler(BaseHandler): | ||
235 | qid = str(i) | 281 | qid = str(i) |
236 | if 'answered-' + qid in self.request.arguments: | 282 | if 'answered-' + qid in self.request.arguments: |
237 | ans[i] = self.get_body_arguments(qid) | 283 | ans[i] = self.get_body_arguments(qid) |
238 | - # print(i, ans[i]) | ||
239 | 284 | ||
240 | # remove enclosing list in some question types | 285 | # remove enclosing list in some question types |
241 | if question['type'] == 'radio': | 286 | if question['type'] == 'radio': |
@@ -260,57 +305,6 @@ class RootHandler(BaseHandler): | @@ -260,57 +305,6 @@ class RootHandler(BaseHandler): | ||
260 | timeit_finish = timer() | 305 | timeit_finish = timer() |
261 | logging.info(' correction took %fs', timeit_finish-timeit_start) | 306 | logging.info(' correction took %fs', timeit_finish-timeit_start) |
262 | 307 | ||
263 | -# ---------------------------------------------------------------------------- | ||
264 | -# pylint: disable=abstract-method | ||
265 | -class LoginHandler(BaseHandler): | ||
266 | - '''Handles /login''' | ||
267 | - | ||
268 | - _prefix = re.compile(r'[a-z]') | ||
269 | - _error_msg = { | ||
270 | - 'wrong_password': 'Password errada', | ||
271 | - 'already_online': 'Já está online, não pode entrar duas vezes', | ||
272 | - 'unauthorized': 'Não está autorizado a fazer o teste' | ||
273 | - } | ||
274 | - | ||
275 | - def get(self): | ||
276 | - '''Render login page.''' | ||
277 | - self.render('login.html', error='') | ||
278 | - | ||
279 | - async def post(self): | ||
280 | - '''Authenticates student and login.''' | ||
281 | - uid = self._prefix.sub('', self.get_body_argument('uid')) | ||
282 | - password = self.get_body_argument('pw') | ||
283 | - headers = { | ||
284 | - 'remote_ip': self.request.remote_ip, | ||
285 | - 'user_agent': self.request.headers.get('User-Agent') | ||
286 | - } | ||
287 | - | ||
288 | - error = await self.testapp.login(uid, password, headers) | ||
289 | - | ||
290 | - if error is None: | ||
291 | - self.set_secure_cookie('perguntations_user', str(uid), expires_days=1) | ||
292 | - self.redirect('/') | ||
293 | - else: | ||
294 | - self.render('login.html', error=self._error_msg[error]) | ||
295 | - | ||
296 | - | ||
297 | -# ---------------------------------------------------------------------------- | ||
298 | -# pylint: disable=abstract-method | ||
299 | -class LogoutHandler(BaseHandler): | ||
300 | - '''Handle /logout''' | ||
301 | - | ||
302 | - @tornado.web.authenticated | ||
303 | - def get(self): | ||
304 | - '''Logs out a user.''' | ||
305 | - self.clear_cookie('perguntations_user') | ||
306 | - self.testapp.logout(self.current_user) | ||
307 | - self.redirect('/') | ||
308 | - | ||
309 | - def on_finish(self): | ||
310 | - self.testapp.logout(self.current_user) | ||
311 | - | ||
312 | - | ||
313 | - | ||
314 | 308 | ||
315 | # ---------------------------------------------------------------------------- | 309 | # ---------------------------------------------------------------------------- |
316 | # pylint: disable=abstract-method | 310 | # pylint: disable=abstract-method |
@@ -469,7 +463,6 @@ class FileHandler(BaseHandler): | @@ -469,7 +463,6 @@ class FileHandler(BaseHandler): | ||
469 | break | 463 | break |
470 | 464 | ||
471 | 465 | ||
472 | - | ||
473 | # --- REVIEW ----------------------------------------------------------------- | 466 | # --- REVIEW ----------------------------------------------------------------- |
474 | # pylint: disable=abstract-method | 467 | # pylint: disable=abstract-method |
475 | class ReviewHandler(BaseHandler): | 468 | class ReviewHandler(BaseHandler): |
perguntations/templates/test.html
@@ -44,7 +44,7 @@ | @@ -44,7 +44,7 @@ | ||
44 | <!-- ===================================================================== --> | 44 | <!-- ===================================================================== --> |
45 | <body> | 45 | <body> |
46 | <!-- ===================================================================== --> | 46 | <!-- ===================================================================== --> |
47 | -<div class="progress fixed-top" style="height: 61px; border-radius: 0px;"> | 47 | +<div class="progress fixed-top" style="height: 62px; border-radius: 0px;"> |
48 | <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div> | 48 | <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div> |
49 | </div> | 49 | </div> |
50 | 50 | ||
@@ -94,10 +94,6 @@ | @@ -94,10 +94,6 @@ | ||
94 | 94 | ||
95 | <h5> | 95 | <h5> |
96 | <div class="row"> | 96 | <div class="row"> |
97 | - <label for="inicio" class="col-sm-3">Início:</label> | ||
98 | - <div class="col-sm-9" id="inicio">{{ str(t['start_time'].time())[:8]}}</div> | ||
99 | - </div> | ||
100 | - <div class="row"> | ||
101 | <label for="duracao" class="col-sm-3">Duração:</label> | 97 | <label for="duracao" class="col-sm-3">Duração:</label> |
102 | <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div> | 98 | <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div> |
103 | </div> | 99 | </div> |
perguntations/test.py
@@ -304,7 +304,7 @@ class Test(dict): | @@ -304,7 +304,7 @@ class Test(dict): | ||
304 | # super().__init__(d) | 304 | # super().__init__(d) |
305 | 305 | ||
306 | # ------------------------------------------------------------------------ | 306 | # ------------------------------------------------------------------------ |
307 | - def start(self, student): | 307 | + def register(self, student): |
308 | ''' | 308 | ''' |
309 | Write student id in the test and register start time | 309 | Write student id in the test and register start time |
310 | ''' | 310 | ''' |