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 | 113 | else: |
114 | 114 | logger.info('Students not yet allowed to login.') |
115 | 115 | |
116 | - # pre-generate tests | |
117 | - | |
116 | + # pre-generate tests for allowed students | |
118 | 117 | if self.allowed: |
119 | 118 | logger.info('Generating %d tests. May take awhile...', |
120 | 119 | len(self.allowed)) |
... | ... | @@ -125,20 +124,15 @@ class App(): |
125 | 124 | # ------------------------------------------------------------------------ |
126 | 125 | async def login(self, uid, try_pw, headers=None): |
127 | 126 | '''login authentication''' |
128 | - if uid in self.online: | |
129 | - logger.warning('"%s" already logged in.', uid) | |
130 | - return 'already_online' | |
131 | 127 | if uid not in self.allowed and uid != '0': # not allowed |
132 | 128 | logger.warning('"%s" unauthorized.', uid) |
133 | 129 | return 'unauthorized' |
134 | 130 | |
135 | - # get name+password from db | |
136 | 131 | with self._db_session() as sess: |
137 | 132 | name, password = sess.query(Student.name, Student.password)\ |
138 | 133 | .filter_by(id=uid)\ |
139 | 134 | .one() |
140 | 135 | |
141 | - # first login updates the password | |
142 | 136 | if password == '': # update password on first login |
143 | 137 | await self.update_student_password(uid, try_pw) |
144 | 138 | pw_ok = True |
... | ... | @@ -151,9 +145,17 @@ class App(): |
151 | 145 | |
152 | 146 | # success |
153 | 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 | 161 | def logout(self, uid): |
... | ... | @@ -185,7 +187,7 @@ class App(): |
185 | 187 | self.testfactory = TestFactory(testconf) |
186 | 188 | except TestFactoryException as exc: |
187 | 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 | 192 | logger.info('Test factory ready. No errors found.') |
191 | 193 | |
... | ... | @@ -201,10 +203,32 @@ class App(): |
201 | 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 | 233 | try: |
210 | 234 | test = self.pregenerated_tests.pop() |
... | ... | @@ -215,10 +239,8 @@ class App(): |
215 | 239 | else: |
216 | 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 | 246 | async def correct_test(self, uid, ans): |
... | ... | @@ -362,13 +384,7 @@ class App(): |
362 | 384 | writer.writerows(grades) |
363 | 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 | 388 | def get_student_grades_from_all_tests(self, uid): |
373 | 389 | '''get grades of student from all tests''' |
374 | 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 | 5 | Uses the tornadoweb framework. |
6 | 6 | ''' |
7 | 7 | |
8 | - | |
9 | 8 | # python standard library |
9 | +import asyncio | |
10 | 10 | import base64 |
11 | 11 | import functools |
12 | 12 | import json |
... | ... | @@ -161,6 +161,54 @@ class BaseHandler(tornado.web.RequestHandler): |
161 | 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 | 212 | # Test shown to students |
165 | 213 | # ---------------------------------------------------------------------------- |
166 | 214 | # pylint: disable=abstract-method |
... | ... | @@ -200,11 +248,9 @@ class RootHandler(BaseHandler): |
200 | 248 | |
201 | 249 | if uid == '0': |
202 | 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 | 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 | 271 | logging.debug('"%s" POST /', uid) |
226 | 272 | |
227 | 273 | try: |
228 | - test = self.testapp.get_student_test(uid) | |
274 | + test = self.testapp.get_test(uid) | |
229 | 275 | except KeyError as exc: |
230 | 276 | logging.warning('"%s" POST / raised 403 Forbidden', uid) |
231 | 277 | raise tornado.web.HTTPError(403) from exc # Forbidden |
... | ... | @@ -235,7 +281,6 @@ class RootHandler(BaseHandler): |
235 | 281 | qid = str(i) |
236 | 282 | if 'answered-' + qid in self.request.arguments: |
237 | 283 | ans[i] = self.get_body_arguments(qid) |
238 | - # print(i, ans[i]) | |
239 | 284 | |
240 | 285 | # remove enclosing list in some question types |
241 | 286 | if question['type'] == 'radio': |
... | ... | @@ -260,57 +305,6 @@ class RootHandler(BaseHandler): |
260 | 305 | timeit_finish = timer() |
261 | 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 | 310 | # pylint: disable=abstract-method |
... | ... | @@ -469,7 +463,6 @@ class FileHandler(BaseHandler): |
469 | 463 | break |
470 | 464 | |
471 | 465 | |
472 | - | |
473 | 466 | # --- REVIEW ----------------------------------------------------------------- |
474 | 467 | # pylint: disable=abstract-method |
475 | 468 | class ReviewHandler(BaseHandler): | ... | ... |
perguntations/templates/test.html
... | ... | @@ -44,7 +44,7 @@ |
44 | 44 | <!-- ===================================================================== --> |
45 | 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 | 48 | <div class="progress-bar bg-secondary" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div> |
49 | 49 | </div> |
50 | 50 | |
... | ... | @@ -94,10 +94,6 @@ |
94 | 94 | |
95 | 95 | <h5> |
96 | 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 | 97 | <label for="duracao" class="col-sm-3">Duração:</label> |
102 | 98 | <div class="col-sm-9" id="duracao">{{ str(t['duration'])+' minutos' if t['duration'] > 0 else 'sem limite de tempo' }}</div> |
103 | 99 | </div> | ... | ... |
perguntations/test.py
... | ... | @@ -304,7 +304,7 @@ class Test(dict): |
304 | 304 | # super().__init__(d) |
305 | 305 | |
306 | 306 | # ------------------------------------------------------------------------ |
307 | - def start(self, student): | |
307 | + def register(self, student): | |
308 | 308 | ''' |
309 | 309 | Write student id in the test and register start time |
310 | 310 | ''' | ... | ... |