Commit 4307381694f71f25ff80f9b14a046ddb6fbfbd30

Authored by Miguel Barão
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
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
... ... @@ -25,7 +25,7 @@
25 25  
26 26 <link href="{{static_url('css/signin.css')}}" rel="stylesheet">
27 27  
28   - <title>{{appname}}</title>
  28 + <title>aprendizations</title>
29 29 </head>
30 30 <body class="text-center">
31 31  
... ...
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) -&gt; 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
1   -[mypy]
2   -python_version = 3.10
3   -plugins = sqlalchemy.ext.mypy.plugin
  1 +; [mypy]
  2 +; python_version = 3.10
  3 +; plugins = sqlalchemy.ext.mypy.plugin
4 4  
5 5 ; [mypy-pygments.*]
6 6 ; ignore_missing_imports = True
... ...
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',
... ...