Commit 4307381694f71f25ff80f9b14a046ddb6fbfbd30
1 parent
1b5ab4ba
Exists in
dev
Large refactoring
removed counter cookie (to be fixed later) use tornado ioloop for bcrypt fix password string sent to sqlalchemy no longer extract number for user login webserver initialization now follows tornado 6.2 recommendations simplified several places
Showing
9 changed files
with
161 additions
and
222 deletions
Show diff stats
aprendizations/__init__.py
... | ... | @@ -30,10 +30,10 @@ are progressively uncovered as the students progress. |
30 | 30 | ''' |
31 | 31 | |
32 | 32 | APP_NAME = 'aprendizations' |
33 | -APP_VERSION = '2022.12.dev1' | |
33 | +APP_VERSION = '2023.2.dev1' | |
34 | 34 | APP_DESCRIPTION = __doc__ |
35 | 35 | |
36 | 36 | __author__ = 'Miguel Barão' |
37 | -__copyright__ = 'Copyright © 2022, Miguel Barão' | |
37 | +__copyright__ = 'Copyright © 2023, Miguel Barão' | |
38 | 38 | __license__ = 'MIT license' |
39 | 39 | __version__ = APP_VERSION | ... | ... |
aprendizations/learnapp.py
1 | 1 | ''' |
2 | -Learn application. | |
3 | 2 | This is the main controller of the application. |
4 | 3 | ''' |
5 | 4 | |
... | ... | @@ -34,10 +33,6 @@ class LearnException(Exception): |
34 | 33 | '''Exceptions raised from the LearnApp class''' |
35 | 34 | |
36 | 35 | |
37 | -class DatabaseUnusableError(LearnException): | |
38 | - '''Exception raised if the database fails in the initialization''' | |
39 | - | |
40 | - | |
41 | 36 | # ============================================================================ |
42 | 37 | class LearnApp(): |
43 | 38 | ''' |
... | ... | @@ -57,7 +52,7 @@ class LearnApp(): |
57 | 52 | 'number': ..., |
58 | 53 | 'name': ..., |
59 | 54 | 'state': StudentState(), |
60 | - 'counter': ... | |
55 | + # 'counter': ... | |
61 | 56 | }, ... |
62 | 57 | } |
63 | 58 | ''' |
... | ... | @@ -157,7 +152,7 @@ class LearnApp(): |
157 | 152 | logger.info(' 0 errors found.') |
158 | 153 | |
159 | 154 | # ------------------------------------------------------------------------ |
160 | - async def login(self, uid: str, password: str) -> bool: | |
155 | + async def login(self, uid: str, password: str, loop) -> bool: | |
161 | 156 | '''user login''' |
162 | 157 | |
163 | 158 | # wait random time to minimize timing attacks |
... | ... | @@ -179,7 +174,7 @@ class LearnApp(): |
179 | 174 | if pw_ok: |
180 | 175 | if uid in self.online: |
181 | 176 | logger.warning('User "%s" already logged in', uid) |
182 | - counter = self.online[uid]['counter'] | |
177 | + counter = self.online[uid]['counter'] # FIXME | |
183 | 178 | else: |
184 | 179 | logger.info('User "%s" logged in', uid) |
185 | 180 | counter = 0 |
... | ... | @@ -200,7 +195,7 @@ class LearnApp(): |
200 | 195 | 'state': StudentState(uid=uid, state=state, |
201 | 196 | courses=self.courses, deps=self.deps, |
202 | 197 | factory=self.factory), |
203 | - 'counter': counter + 1, # count simultaneous logins | |
198 | + 'counter': counter + 1, # count simultaneous logins FIXME | |
204 | 199 | } |
205 | 200 | |
206 | 201 | else: |
... | ... | @@ -231,7 +226,7 @@ class LearnApp(): |
231 | 226 | |
232 | 227 | query = select(Student).where(Student.id == uid) |
233 | 228 | with Session(self._engine) as session: |
234 | - session.execute(query).scalar_one().password = hashed_pw | |
229 | + session.execute(query).scalar_one().password = str(hashed_pw) | |
235 | 230 | session.commit() |
236 | 231 | |
237 | 232 | logger.info('User "%s" changed password', uid) |
... | ... | @@ -338,7 +333,6 @@ class LearnApp(): |
338 | 333 | logger.info('User "%s" started topic "%s"', uid, topic) |
339 | 334 | |
340 | 335 | # ------------------------------------------------------------------------ |
341 | - # ------------------------------------------------------------------------ | |
342 | 336 | def _add_missing_topics(self, topics: Iterable[str]) -> None: |
343 | 337 | ''' |
344 | 338 | Fill table 'Topic' with topics from the graph, if new |
... | ... | @@ -500,9 +494,9 @@ class LearnApp(): |
500 | 494 | |
501 | 495 | return factory |
502 | 496 | |
503 | - def get_login_counter(self, uid: str) -> int: | |
504 | - '''login counter''' | |
505 | - return int(self.online[uid]['counter']) | |
497 | + # def get_login_counter(self, uid: str) -> int: | |
498 | + # '''login counter''' | |
499 | + # return int(self.online[uid]['counter']) | |
506 | 500 | |
507 | 501 | def get_student_name(self, uid: str) -> str: |
508 | 502 | '''Get the username''' | ... | ... |
aprendizations/questions.py
... | ... | @@ -15,7 +15,7 @@ from typing import Any, Dict, NewType |
15 | 15 | import uuid |
16 | 16 | |
17 | 17 | # this project |
18 | -from aprendizations.tools import run_script_async | |
18 | +from aprendizations.tools import run_script | |
19 | 19 | |
20 | 20 | # setup logger for this module |
21 | 21 | logger = logging.getLogger(__name__) |
... | ... | @@ -545,7 +545,7 @@ class QuestionTextArea(Question): |
545 | 545 | super().correct() |
546 | 546 | |
547 | 547 | if self['answer'] is not None: # correct answer and parse yaml ouput |
548 | - out = await run_script_async( | |
548 | + out = await run_script( | |
549 | 549 | script=self['correct'], |
550 | 550 | args=self['args'], |
551 | 551 | stdin=self['answer'], |
... | ... | @@ -687,7 +687,7 @@ class QFactory(): |
687 | 687 | qdict.setdefault('args', []) |
688 | 688 | qdict.setdefault('stdin', '') |
689 | 689 | script = path.join(qdict['path'], qdict['script']) |
690 | - out = await run_script_async(script=script, | |
690 | + out = await run_script(script=script, | |
691 | 691 | args=qdict['args'], |
692 | 692 | stdin=qdict['stdin']) |
693 | 693 | qdict.update(out) | ... | ... |
aprendizations/serve.py
1 | 1 | ''' |
2 | -Webserver | |
2 | +Tornado Webserver | |
3 | 3 | ''' |
4 | 4 | |
5 | 5 | |
6 | 6 | # python standard library |
7 | 7 | import asyncio |
8 | 8 | import base64 |
9 | -import functools | |
10 | 9 | from logging import getLogger |
11 | 10 | import mimetypes |
12 | 11 | from os.path import join, dirname, expanduser |
13 | 12 | import signal |
14 | 13 | import sys |
15 | -import re | |
16 | 14 | from typing import List, Optional, Union |
17 | 15 | import uuid |
18 | 16 | |
... | ... | @@ -20,7 +18,7 @@ import uuid |
20 | 18 | import tornado.httpserver |
21 | 19 | import tornado.ioloop |
22 | 20 | import tornado.web |
23 | -from tornado.escape import to_unicode | |
21 | +from tornado.escape import to_unicode, utf8 | |
24 | 22 | |
25 | 23 | # this project |
26 | 24 | from aprendizations.renderer_markdown import md_to_html |
... | ... | @@ -32,152 +30,54 @@ from aprendizations import APP_NAME |
32 | 30 | logger = getLogger(__name__) |
33 | 31 | |
34 | 32 | |
35 | -# ---------------------------------------------------------------------------- | |
36 | -def admin_only(func): | |
37 | - ''' | |
38 | - Decorator used to restrict access to the administrator | |
39 | - ''' | |
40 | - @functools.wraps(func) | |
41 | - def wrapper(self, *args, **kwargs) -> None: | |
42 | - if self.current_user != '0': | |
43 | - raise tornado.web.HTTPError(403) # forbidden | |
44 | - func(self, *args, **kwargs) | |
45 | - return wrapper | |
46 | - | |
47 | - | |
48 | -# ============================================================================ | |
49 | -class WebApplication(tornado.web.Application): | |
50 | - ''' | |
51 | - WebApplication - Tornado Web Server | |
52 | - ''' | |
53 | - def __init__(self, learnapp, debug=False) -> None: | |
54 | - handlers = [ | |
55 | - (r'/login', LoginHandler), | |
56 | - (r'/logout', LogoutHandler), | |
57 | - (r'/change_password', ChangePasswordHandler), | |
58 | - (r'/question', QuestionHandler), # render question | |
59 | - (r'/rankings', RankingsHandler), # rankings table | |
60 | - (r'/topic/(.+)', TopicHandler), # start topic | |
61 | - (r'/file/(.+)', FileHandler), # serve file | |
62 | - (r'/courses', CoursesHandler), # show available courses | |
63 | - (r'/course/(.*)', CourseHandler), # show topics from course | |
64 | - (r'/course2/(.*)', CourseHandler2), # show topics from course FIXME | |
65 | - (r'/', RootHandler), # redirects | |
66 | - ] | |
67 | - settings = { | |
68 | - 'template_path': join(dirname(__file__), 'templates'), | |
69 | - 'static_path': join(dirname(__file__), 'static'), | |
70 | - 'static_url_prefix': '/static/', | |
71 | - 'xsrf_cookies': True, | |
72 | - 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), | |
73 | - 'login_url': '/login', | |
74 | - 'debug': debug, | |
75 | - } | |
76 | - super().__init__(handlers, **settings) | |
77 | - self.learn = learnapp | |
78 | - | |
79 | - | |
80 | 33 | # ============================================================================ |
81 | 34 | # Handlers |
82 | 35 | # ============================================================================ |
83 | 36 | # pylint: disable=abstract-method |
84 | 37 | class BaseHandler(tornado.web.RequestHandler): |
85 | - ''' | |
86 | - Base handler common to all handlers. | |
87 | - ''' | |
38 | + '''Base handler common to all handlers.''' | |
39 | + | |
40 | + def initialize(self, app): | |
41 | + self.app = app | |
88 | 42 | |
89 | - @property | |
90 | - def learn(self): | |
91 | - '''easier access to learnapp''' | |
92 | - return self.application.learn | |
93 | - | |
94 | 43 | def get_current_user(self): |
95 | 44 | '''called on every method decorated with @tornado.web.authenticated''' |
96 | - user_cookie = self.get_secure_cookie('aprendizations_user') | |
97 | - counter_cookie = self.get_secure_cookie('counter') | |
98 | - if user_cookie is not None: | |
99 | - uid = user_cookie.decode('utf-8') | |
100 | - if counter_cookie is not None: | |
101 | - counter = counter_cookie.decode('utf-8') | |
102 | - if counter == str(self.learn.get_login_counter(uid)): | |
103 | - return uid | |
104 | - return None | |
105 | - | |
106 | - | |
107 | -# ---------------------------------------------------------------------------- | |
108 | -class RankingsHandler(BaseHandler): | |
109 | - ''' | |
110 | - Handles rankings page | |
111 | - ''' | |
112 | - | |
113 | - @tornado.web.authenticated | |
114 | - def get(self) -> None: | |
115 | - ''' | |
116 | - Renders list of students that have answers in this course. | |
117 | - ''' | |
118 | - uid = self.current_user | |
119 | - current_course = self.learn.get_current_course_id(uid) | |
120 | - course_id = self.get_query_argument('course', default=current_course) | |
121 | - rankings = self.learn.get_rankings(uid, course_id) | |
122 | - self.render('rankings.html', | |
123 | - appname=APP_NAME, | |
124 | - uid=uid, | |
125 | - name=self.learn.get_student_name(uid), | |
126 | - rankings=rankings, | |
127 | - course_id=course_id, | |
128 | - course_title=self.learn.get_student_course_title(uid), | |
129 | - # FIXME get from course var | |
130 | - ) | |
45 | + cookie = self.get_secure_cookie('aprendizations_user') | |
46 | + return None if cookie is None else to_unicode(cookie) | |
131 | 47 | |
132 | 48 | |
133 | 49 | # ---------------------------------------------------------------------------- |
134 | 50 | class LoginHandler(BaseHandler): |
135 | - ''' | |
136 | - Handles /login | |
137 | - ''' | |
51 | + '''Handles /login''' | |
138 | 52 | |
139 | 53 | def get(self) -> None: |
140 | - ''' | |
141 | - Render login page | |
142 | - ''' | |
143 | - self.render('login.html', appname=APP_NAME, error='') | |
54 | + '''Login page''' | |
55 | + self.render('login.html', error='') | |
144 | 56 | |
145 | 57 | async def post(self): |
146 | - ''' | |
147 | - Authenticate and redirect to application if successful | |
148 | - ''' | |
58 | + '''Authenticate and redirect to application if successful''' | |
149 | 59 | userid = self.get_body_argument('uid') or '' |
150 | 60 | passwd = self.get_body_argument('pw') |
151 | - match = re.search(r'[0-9]+', userid) # extract number | |
152 | - if match is not None: | |
153 | - userid = match.group(0) # get string with number | |
154 | - if await self.learn.login(userid, passwd): | |
155 | - counter = str(self.learn.get_login_counter(userid)) | |
156 | - self.set_secure_cookie('aprendizations_user', userid) | |
157 | - self.set_secure_cookie('counter', counter) | |
158 | - self.redirect('/') | |
159 | - self.render('login.html', | |
160 | - appname=APP_NAME, | |
161 | - error='Número ou senha incorrectos') | |
61 | + loop = tornado.ioloop.IOLoop.current() | |
62 | + login_ok = await self.app.login(userid, passwd, loop) | |
63 | + if login_ok: | |
64 | + self.set_secure_cookie('aprendizations_user', userid) | |
65 | + self.redirect('/') | |
66 | + else: | |
67 | + self.render('login.html', error='Número ou senha incorrectos') | |
162 | 68 | |
163 | 69 | |
164 | 70 | # ---------------------------------------------------------------------------- |
165 | 71 | class LogoutHandler(BaseHandler): |
166 | - ''' | |
167 | - Handles /logout | |
168 | - ''' | |
72 | + '''Handle /logout''' | |
73 | + | |
169 | 74 | @tornado.web.authenticated |
170 | 75 | def get(self) -> None: |
171 | - ''' | |
172 | - clear cookies and user session | |
173 | - ''' | |
174 | - self.clear_cookie('user') | |
175 | - self.clear_cookie('counter') | |
76 | + '''Clear cookies and user session''' | |
77 | + self.app.logout(self.current_user) # FIXME | |
78 | + self.clear_cookie('aprendizations_user') | |
176 | 79 | self.redirect('/') |
177 | 80 | |
178 | - def on_finish(self) -> None: | |
179 | - self.learn.logout(self.current_user) | |
180 | - | |
181 | 81 | |
182 | 82 | # ---------------------------------------------------------------------------- |
183 | 83 | class ChangePasswordHandler(BaseHandler): |
... | ... | @@ -191,20 +91,9 @@ class ChangePasswordHandler(BaseHandler): |
191 | 91 | Try to change password and show success/fail status |
192 | 92 | ''' |
193 | 93 | userid = self.current_user |
194 | - passwd = self.get_body_arguments('new_password')[0] | |
195 | - changed_ok = await self.learn.change_password(userid, passwd) | |
196 | - if changed_ok: | |
197 | - notification = self.render_string( | |
198 | - 'notification.html', | |
199 | - type='success', | |
200 | - msg='A password foi alterada!' | |
201 | - ) | |
202 | - else: | |
203 | - notification = self.render_string( | |
204 | - 'notification.html', | |
205 | - type='danger', | |
206 | - msg='A password não foi alterada!' | |
207 | - ) | |
94 | + passwd = self.get_body_arguments('new_password')[0] # FIXME porque [0]? | |
95 | + ok = await self.app.change_password(userid, passwd) | |
96 | + notification = self.render_string('notification.html', ok=ok) | |
208 | 97 | self.write({'msg': to_unicode(notification)}) |
209 | 98 | |
210 | 99 | |
... | ... | @@ -213,7 +102,7 @@ class RootHandler(BaseHandler): |
213 | 102 | ''' |
214 | 103 | Handles root / |
215 | 104 | ''' |
216 | - | |
105 | + | |
217 | 106 | @tornado.web.authenticated |
218 | 107 | def get(self) -> None: |
219 | 108 | '''Redirect to main entrypoint''' |
... | ... | @@ -225,7 +114,6 @@ class CoursesHandler(BaseHandler): |
225 | 114 | ''' |
226 | 115 | Handles /courses |
227 | 116 | ''' |
228 | - | |
229 | 117 | def set_default_headers(self, *_) -> None: |
230 | 118 | self.set_header('Cache-Control', 'no-cache') |
231 | 119 | |
... | ... | @@ -236,8 +124,8 @@ class CoursesHandler(BaseHandler): |
236 | 124 | self.render('courses.html', |
237 | 125 | appname=APP_NAME, |
238 | 126 | uid=uid, |
239 | - name=self.learn.get_student_name(uid), | |
240 | - courses=self.learn.get_courses(), | |
127 | + name=self.app.get_student_name(uid), | |
128 | + courses=self.app.get_courses(), | |
241 | 129 | # courses_progress= |
242 | 130 | ) |
243 | 131 | |
... | ... | @@ -254,21 +142,20 @@ class CourseHandler2(BaseHandler): |
254 | 142 | logger.debug('[CourseHandler2] uid="%s", course_id="%s"', uid, course_id) |
255 | 143 | |
256 | 144 | if course_id == '': |
257 | - course_id = self.learn.get_current_course_id(uid) | |
145 | + course_id = self.app.get_current_course_id(uid) | |
258 | 146 | |
259 | 147 | try: |
260 | - self.learn.start_course(uid, course_id) | |
148 | + self.app.start_course(uid, course_id) | |
261 | 149 | except LearnException: |
262 | 150 | self.redirect('/courses') |
263 | 151 | |
264 | - # print(self.learn.get_course(course_id)) | |
265 | 152 | self.render('maintopics-table2.html', |
266 | 153 | appname=APP_NAME, |
267 | 154 | uid=uid, |
268 | - name=self.learn.get_student_name(uid), | |
269 | - state=self.learn.get_student_state(uid), | |
155 | + name=self.app.get_student_name(uid), | |
156 | + state=self.app.get_student_state(uid), | |
270 | 157 | course_id=course_id, |
271 | - course=self.learn.get_course(course_id) | |
158 | + course=self.app.get_course(course_id) | |
272 | 159 | ) |
273 | 160 | |
274 | 161 | # ============================================================================ |
... | ... | @@ -287,20 +174,20 @@ class CourseHandler(BaseHandler): |
287 | 174 | logger.debug('[CourseHandler] uid="%s", course_id="%s"', uid, course_id) |
288 | 175 | |
289 | 176 | if course_id == '': |
290 | - course_id = self.learn.get_current_course_id(uid) | |
177 | + course_id = self.app.get_current_course_id(uid) | |
291 | 178 | |
292 | 179 | try: |
293 | - self.learn.start_course(uid, course_id) | |
180 | + self.app.start_course(uid, course_id) | |
294 | 181 | except LearnException: |
295 | 182 | self.redirect('/courses') |
296 | 183 | |
297 | 184 | self.render('maintopics-table.html', |
298 | 185 | appname=APP_NAME, |
299 | 186 | uid=uid, |
300 | - name=self.learn.get_student_name(uid), | |
301 | - state=self.learn.get_student_state(uid), | |
187 | + name=self.app.get_student_name(uid), | |
188 | + state=self.app.get_student_state(uid), | |
302 | 189 | course_id=course_id, |
303 | - course=self.learn.get_course(course_id) | |
190 | + course=self.app.get_course(course_id) | |
304 | 191 | ) |
305 | 192 | |
306 | 193 | |
... | ... | @@ -321,15 +208,15 @@ class TopicHandler(BaseHandler): |
321 | 208 | uid = self.current_user |
322 | 209 | logger.debug('[TopicHandler] %s', topic) |
323 | 210 | try: |
324 | - await self.learn.start_topic(uid, topic) # FIXME GET should not modify state... | |
211 | + await self.app.start_topic(uid, topic) # FIXME GET should not modify state... | |
325 | 212 | except KeyError: |
326 | 213 | self.redirect('/topics') |
327 | 214 | |
328 | 215 | self.render('topic.html', |
329 | 216 | appname=APP_NAME, |
330 | 217 | uid=uid, |
331 | - name=self.learn.get_student_name(uid), | |
332 | - course_id=self.learn.get_current_course_id(uid), | |
218 | + name=self.app.get_student_name(uid), | |
219 | + course_id=self.app.get_current_course_id(uid), | |
333 | 220 | ) |
334 | 221 | |
335 | 222 | |
... | ... | @@ -345,7 +232,7 @@ class FileHandler(BaseHandler): |
345 | 232 | Serve file from the /public subdirectory of a particular topic |
346 | 233 | ''' |
347 | 234 | uid = self.current_user |
348 | - public_dir = self.learn.get_current_public_dir(uid) | |
235 | + public_dir = self.app.get_current_public_dir(uid) | |
349 | 236 | filepath = expanduser(join(public_dir, filename)) |
350 | 237 | |
351 | 238 | logger.debug('[FileHandler] uid=%s, public_dir=%s, filepath=%s', |
... | ... | @@ -391,7 +278,7 @@ class QuestionHandler(BaseHandler): |
391 | 278 | ''' |
392 | 279 | logger.debug('[QuestionHandler]') |
393 | 280 | user = self.current_user |
394 | - question = await self.learn.get_question(user) | |
281 | + question = await self.app.get_question(user) | |
395 | 282 | |
396 | 283 | # show current question |
397 | 284 | if question is not None: |
... | ... | @@ -402,7 +289,7 @@ class QuestionHandler(BaseHandler): |
402 | 289 | 'params': { |
403 | 290 | 'type': question['type'], |
404 | 291 | 'question': to_unicode(qhtml), |
405 | - 'progress': self.learn.get_student_progress(user), | |
292 | + 'progress': self.app.get_student_progress(user), | |
406 | 293 | 'tries': question['tries'], |
407 | 294 | } |
408 | 295 | } |
... | ... | @@ -432,7 +319,7 @@ class QuestionHandler(BaseHandler): |
432 | 319 | logger.debug('[QuestionHandler] answer=%s', answer) |
433 | 320 | |
434 | 321 | # --- check if browser opened different questions simultaneously |
435 | - if qid != self.learn.get_current_question_id(user): | |
322 | + if qid != self.app.get_current_question_id(user): | |
436 | 323 | logger.warning('User %s desynchronized questions', user) |
437 | 324 | self.write({ |
438 | 325 | 'method': 'invalid', |
... | ... | @@ -444,7 +331,7 @@ class QuestionHandler(BaseHandler): |
444 | 331 | return |
445 | 332 | |
446 | 333 | # --- answers are in a list. fix depending on question type |
447 | - qtype = self.learn.get_student_question_type(user) | |
334 | + qtype = self.app.get_student_question_type(user) | |
448 | 335 | ans: Optional[Union[List, str]] |
449 | 336 | if qtype in ('success', 'information', 'info'): |
450 | 337 | ans = None |
... | ... | @@ -456,7 +343,7 @@ class QuestionHandler(BaseHandler): |
456 | 343 | ans = answer |
457 | 344 | |
458 | 345 | # --- check answer (nonblocking) and get corrected question and action |
459 | - question = await self.learn.check_answer(user, ans) | |
346 | + question = await self.app.check_answer(user, ans) | |
460 | 347 | |
461 | 348 | # --- build response |
462 | 349 | response = {'method': question['status'], 'params': {}} |
... | ... | @@ -470,7 +357,7 @@ class QuestionHandler(BaseHandler): |
470 | 357 | md=md_to_html) |
471 | 358 | response['params'] = { |
472 | 359 | 'type': question['type'], |
473 | - 'progress': self.learn.get_student_progress(user), | |
360 | + 'progress': self.app.get_student_progress(user), | |
474 | 361 | 'comments': to_unicode(comments), |
475 | 362 | 'solution': to_unicode(solution), |
476 | 363 | 'tries': question['tries'], |
... | ... | @@ -481,7 +368,7 @@ class QuestionHandler(BaseHandler): |
481 | 368 | md=md_to_html) |
482 | 369 | response['params'] = { |
483 | 370 | 'type': question['type'], |
484 | - 'progress': self.learn.get_student_progress(user), | |
371 | + 'progress': self.app.get_student_progress(user), | |
485 | 372 | 'comments': to_unicode(comments), |
486 | 373 | 'tries': question['tries'], |
487 | 374 | } |
... | ... | @@ -493,7 +380,7 @@ class QuestionHandler(BaseHandler): |
493 | 380 | 'solution.html', solution=question['solution'], md=md_to_html) |
494 | 381 | response['params'] = { |
495 | 382 | 'type': question['type'], |
496 | - 'progress': self.learn.get_student_progress(user), | |
383 | + 'progress': self.app.get_student_progress(user), | |
497 | 384 | 'comments': to_unicode(comments), |
498 | 385 | 'solution': to_unicode(solution), |
499 | 386 | 'tries': question['tries'], |
... | ... | @@ -505,6 +392,32 @@ class QuestionHandler(BaseHandler): |
505 | 392 | |
506 | 393 | |
507 | 394 | # ---------------------------------------------------------------------------- |
395 | +class RankingsHandler(BaseHandler): | |
396 | + ''' | |
397 | + Handles rankings page | |
398 | + ''' | |
399 | + | |
400 | + @tornado.web.authenticated | |
401 | + def get(self) -> None: | |
402 | + ''' | |
403 | + Renders list of students that have answers in this course. | |
404 | + ''' | |
405 | + uid = self.current_user | |
406 | + current_course = self.app.get_current_course_id(uid) | |
407 | + course_id = self.get_query_argument('course', default=current_course) | |
408 | + rankings = self.app.get_rankings(uid, course_id) | |
409 | + self.render('rankings.html', | |
410 | + appname=APP_NAME, | |
411 | + uid=uid, | |
412 | + name=self.app.get_student_name(uid), | |
413 | + rankings=rankings, | |
414 | + course_id=course_id, | |
415 | + course_title=self.app.get_student_course_title(uid), | |
416 | + # FIXME get from course var | |
417 | + ) | |
418 | + | |
419 | + | |
420 | +# ---------------------------------------------------------------------------- | |
508 | 421 | # Signal handler to catch Ctrl-C and abort server |
509 | 422 | # ---------------------------------------------------------------------------- |
510 | 423 | def signal_handler(*_) -> None: |
... | ... | @@ -525,11 +438,29 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: |
525 | 438 | ''' |
526 | 439 | |
527 | 440 | # --- create web application |
528 | - try: | |
529 | - webapp = WebApplication(app, debug=debug) | |
530 | - except Exception: | |
531 | - logger.critical('Failed to start web application.', exc_info=True) | |
532 | - sys.exit(1) | |
441 | + handlers = [ | |
442 | + (r'/login', LoginHandler, dict(app=app)), | |
443 | + (r'/logout', LogoutHandler, dict(app=app)), | |
444 | + (r'/change_password', ChangePasswordHandler, dict(app=app)), | |
445 | + (r'/question', QuestionHandler, dict(app=app)), # render question | |
446 | + (r'/rankings', RankingsHandler, dict(app=app)), # rankings table | |
447 | + (r'/topic/(.+)', TopicHandler, dict(app=app)), # start topic | |
448 | + (r'/file/(.+)', FileHandler, dict(app=app)), # serve file | |
449 | + (r'/courses', CoursesHandler, dict(app=app)), # show available courses | |
450 | + (r'/course/(.*)', CourseHandler, dict(app=app)), # show topics from course | |
451 | + (r'/course2/(.*)', CourseHandler2, dict(app=app)), # show topics from course FIXME | |
452 | + (r'/', RootHandler, dict(app=app)), # redirects | |
453 | + ] | |
454 | + settings = { | |
455 | + 'template_path': join(dirname(__file__), 'templates'), | |
456 | + 'static_path': join(dirname(__file__), 'static'), | |
457 | + 'static_url_prefix': '/static/', | |
458 | + 'xsrf_cookies': True, | |
459 | + 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), | |
460 | + 'login_url': '/login', | |
461 | + 'debug': debug, | |
462 | + } | |
463 | + webapp = tornado.web.Application(handlers, **settings) | |
533 | 464 | logger.info('Web application started (tornado.web.Application)') |
534 | 465 | |
535 | 466 | # --- create tornado http server |
... | ... | @@ -538,14 +469,14 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: |
538 | 469 | except ValueError: |
539 | 470 | logger.critical('Certificates cert.pem and privkey.pem not found') |
540 | 471 | sys.exit(1) |
541 | - logger.debug('HTTP server started') | |
472 | + logger.debug('HTTPS server started') | |
542 | 473 | |
543 | 474 | try: |
544 | 475 | httpserver.listen(port) |
545 | 476 | except OSError: |
546 | 477 | logger.critical('Cannot bind port %d. Already in use?', port) |
547 | 478 | sys.exit(1) |
548 | - logger.info('Webserver listening on %d... (Ctrl-C to stop)', port) | |
479 | + logger.info('Listening on port %d... (Ctrl-C to stop)', port) | |
549 | 480 | |
550 | 481 | # --- set signal handler for Control-C |
551 | 482 | signal.signal(signal.SIGINT, signal_handler) | ... | ... |
aprendizations/templates/login.html
aprendizations/templates/notification.html
1 | -<div class="alert alert-{{ type }}" role="alert" id="notification"> | |
2 | - <i class="fas fa-key" aria-hidden="true"></i> | |
3 | - {{ msg }} | |
4 | -</div> | |
1 | +{% if ok %} | |
2 | + <div class="alert alert-success" role="alert" id="notification"> | |
3 | + <i class="fas fa-key" aria-hidden="true"></i> | |
4 | + A password foi alterada! | |
5 | + </div> | |
6 | +{% else %} | |
7 | + <div class="alert alert-danger" role="alert" id="notification"> | |
8 | + <i class="fas fa-key" aria-hidden="true"></i> | |
9 | + A password não foi alterada! | |
10 | + </div> | |
11 | +{% end %} | |
12 | + | ... | ... |
aprendizations/tools.py
... | ... | @@ -4,7 +4,7 @@ import asyncio |
4 | 4 | import logging |
5 | 5 | from os import path |
6 | 6 | # import re |
7 | -import subprocess | |
7 | +# import subprocess | |
8 | 8 | from typing import Any, List |
9 | 9 | |
10 | 10 | # third party libraries |
... | ... | @@ -212,36 +212,42 @@ def load_yaml(filename: str, default: Any = None) -> Any: |
212 | 212 | # ---------------------------------------------------------------------------- |
213 | 213 | # Same as above, but asynchronous |
214 | 214 | # ---------------------------------------------------------------------------- |
215 | -async def run_script_async(script: str, | |
215 | +async def run_script(script: str, | |
216 | 216 | args: List[str] = [], |
217 | 217 | stdin: str = '', |
218 | 218 | timeout: int = 2) -> Any: |
219 | 219 | |
220 | + # normalize args | |
220 | 221 | script = path.expanduser(script) |
222 | + input_bytes = stdin.encode('utf-8') | |
221 | 223 | args = [str(a) for a in args] |
222 | 224 | |
223 | - p = await asyncio.create_subprocess_exec( | |
224 | - script, *args, | |
225 | - stdin=asyncio.subprocess.PIPE, | |
226 | - stdout=asyncio.subprocess.PIPE, | |
227 | - stderr=asyncio.subprocess.DEVNULL, | |
228 | - ) | |
229 | - | |
230 | 225 | try: |
231 | - stdout, _ = await asyncio.wait_for( | |
232 | - p.communicate(input=stdin.encode('utf-8')), | |
233 | - timeout=timeout | |
226 | + p = await asyncio.create_subprocess_exec( | |
227 | + script, *args, | |
228 | + stdin=asyncio.subprocess.PIPE, | |
229 | + stdout=asyncio.subprocess.PIPE, | |
230 | + stderr=asyncio.subprocess.DEVNULL, | |
234 | 231 | ) |
235 | - except asyncio.TimeoutError: | |
236 | - logger.warning(f'Timeout {timeout}s running script "{script}".') | |
237 | - return | |
238 | - | |
239 | - if p.returncode != 0: | |
240 | - logger.error(f'Return code {p.returncode} running "{script}".') | |
232 | + except FileNotFoundError: | |
233 | + logger.error(f'Can not execute script "{script}": not found.') | |
234 | + except PermissionError: | |
235 | + logger.error(f'Can not execute script "{script}": wrong permissions.') | |
236 | + except OSError: | |
237 | + logger.error(f'Can not execute script "{script}": unknown reason.') | |
241 | 238 | else: |
242 | 239 | try: |
243 | - output = yaml.safe_load(stdout.decode('utf-8', 'ignore')) | |
244 | - except Exception: | |
245 | - logger.error(f'Error parsing yaml output of "{script}"') | |
240 | + stdout, _ = await asyncio.wait_for(p.communicate(input_bytes), timeout) | |
241 | + except asyncio.TimeoutError: | |
242 | + logger.warning(f'Timeout {timeout}s exceeded running "{script}".') | |
243 | + return | |
244 | + | |
245 | + if p.returncode != 0: | |
246 | + logger.error(f'Return code {p.returncode} running "{script}".') | |
246 | 247 | else: |
247 | - return output | |
248 | + try: | |
249 | + output = yaml.safe_load(stdout.decode('utf-8', 'ignore')) | |
250 | + except Exception: | |
251 | + logger.error(f'Error parsing yaml output of "{script}"') | |
252 | + else: | |
253 | + return output | ... | ... |
mypy.ini
setup.py
... | ... | @@ -18,7 +18,7 @@ setup( |
18 | 18 | url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", |
19 | 19 | packages=find_packages(), |
20 | 20 | include_package_data=True, # install files from MANIFEST.in |
21 | - python_requires='>=3.9.*', | |
21 | + python_requires='>=3.9', | |
22 | 22 | install_requires=[ |
23 | 23 | 'tornado>=6.2', |
24 | 24 | 'mistune>=3.0.0rc4', | ... | ... |