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,10 +30,10 @@ are progressively uncovered as the students progress.
30 ''' 30 '''
31 31
32 APP_NAME = 'aprendizations' 32 APP_NAME = 'aprendizations'
33 -APP_VERSION = '2022.12.dev1' 33 +APP_VERSION = '2023.2.dev1'
34 APP_DESCRIPTION = __doc__ 34 APP_DESCRIPTION = __doc__
35 35
36 __author__ = 'Miguel Barão' 36 __author__ = 'Miguel Barão'
37 -__copyright__ = 'Copyright © 2022, Miguel Barão' 37 +__copyright__ = 'Copyright © 2023, Miguel Barão'
38 __license__ = 'MIT license' 38 __license__ = 'MIT license'
39 __version__ = APP_VERSION 39 __version__ = APP_VERSION
aprendizations/learnapp.py
1 ''' 1 '''
2 -Learn application.  
3 This is the main controller of the application. 2 This is the main controller of the application.
4 ''' 3 '''
5 4
@@ -34,10 +33,6 @@ class LearnException(Exception): @@ -34,10 +33,6 @@ class LearnException(Exception):
34 '''Exceptions raised from the LearnApp class''' 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 class LearnApp(): 37 class LearnApp():
43 ''' 38 '''
@@ -57,7 +52,7 @@ class LearnApp(): @@ -57,7 +52,7 @@ class LearnApp():
57 'number': ..., 52 'number': ...,
58 'name': ..., 53 'name': ...,
59 'state': StudentState(), 54 'state': StudentState(),
60 - 'counter': ... 55 + # 'counter': ...
61 }, ... 56 }, ...
62 } 57 }
63 ''' 58 '''
@@ -157,7 +152,7 @@ class LearnApp(): @@ -157,7 +152,7 @@ class LearnApp():
157 logger.info(' 0 errors found.') 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 '''user login''' 156 '''user login'''
162 157
163 # wait random time to minimize timing attacks 158 # wait random time to minimize timing attacks
@@ -179,7 +174,7 @@ class LearnApp(): @@ -179,7 +174,7 @@ class LearnApp():
179 if pw_ok: 174 if pw_ok:
180 if uid in self.online: 175 if uid in self.online:
181 logger.warning('User "%s" already logged in', uid) 176 logger.warning('User "%s" already logged in', uid)
182 - counter = self.online[uid]['counter'] 177 + counter = self.online[uid]['counter'] # FIXME
183 else: 178 else:
184 logger.info('User "%s" logged in', uid) 179 logger.info('User "%s" logged in', uid)
185 counter = 0 180 counter = 0
@@ -200,7 +195,7 @@ class LearnApp(): @@ -200,7 +195,7 @@ class LearnApp():
200 'state': StudentState(uid=uid, state=state, 195 'state': StudentState(uid=uid, state=state,
201 courses=self.courses, deps=self.deps, 196 courses=self.courses, deps=self.deps,
202 factory=self.factory), 197 factory=self.factory),
203 - 'counter': counter + 1, # count simultaneous logins 198 + 'counter': counter + 1, # count simultaneous logins FIXME
204 } 199 }
205 200
206 else: 201 else:
@@ -231,7 +226,7 @@ class LearnApp(): @@ -231,7 +226,7 @@ class LearnApp():
231 226
232 query = select(Student).where(Student.id == uid) 227 query = select(Student).where(Student.id == uid)
233 with Session(self._engine) as session: 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 session.commit() 230 session.commit()
236 231
237 logger.info('User "%s" changed password', uid) 232 logger.info('User "%s" changed password', uid)
@@ -338,7 +333,6 @@ class LearnApp(): @@ -338,7 +333,6 @@ class LearnApp():
338 logger.info('User "%s" started topic "%s"', uid, topic) 333 logger.info('User "%s" started topic "%s"', uid, topic)
339 334
340 # ------------------------------------------------------------------------ 335 # ------------------------------------------------------------------------
341 - # ------------------------------------------------------------------------  
342 def _add_missing_topics(self, topics: Iterable[str]) -> None: 336 def _add_missing_topics(self, topics: Iterable[str]) -> None:
343 ''' 337 '''
344 Fill table 'Topic' with topics from the graph, if new 338 Fill table 'Topic' with topics from the graph, if new
@@ -500,9 +494,9 @@ class LearnApp(): @@ -500,9 +494,9 @@ class LearnApp():
500 494
501 return factory 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 def get_student_name(self, uid: str) -> str: 501 def get_student_name(self, uid: str) -> str:
508 '''Get the username''' 502 '''Get the username'''
aprendizations/questions.py
@@ -15,7 +15,7 @@ from typing import Any, Dict, NewType @@ -15,7 +15,7 @@ from typing import Any, Dict, NewType
15 import uuid 15 import uuid
16 16
17 # this project 17 # this project
18 -from aprendizations.tools import run_script_async 18 +from aprendizations.tools import run_script
19 19
20 # setup logger for this module 20 # setup logger for this module
21 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
@@ -545,7 +545,7 @@ class QuestionTextArea(Question): @@ -545,7 +545,7 @@ class QuestionTextArea(Question):
545 super().correct() 545 super().correct()
546 546
547 if self['answer'] is not None: # correct answer and parse yaml ouput 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 script=self['correct'], 549 script=self['correct'],
550 args=self['args'], 550 args=self['args'],
551 stdin=self['answer'], 551 stdin=self['answer'],
@@ -687,7 +687,7 @@ class QFactory(): @@ -687,7 +687,7 @@ class QFactory():
687 qdict.setdefault('args', []) 687 qdict.setdefault('args', [])
688 qdict.setdefault('stdin', '') 688 qdict.setdefault('stdin', '')
689 script = path.join(qdict['path'], qdict['script']) 689 script = path.join(qdict['path'], qdict['script'])
690 - out = await run_script_async(script=script, 690 + out = await run_script(script=script,
691 args=qdict['args'], 691 args=qdict['args'],
692 stdin=qdict['stdin']) 692 stdin=qdict['stdin'])
693 qdict.update(out) 693 qdict.update(out)
aprendizations/serve.py
1 ''' 1 '''
2 -Webserver 2 +Tornado Webserver
3 ''' 3 '''
4 4
5 5
6 # python standard library 6 # python standard library
7 import asyncio 7 import asyncio
8 import base64 8 import base64
9 -import functools  
10 from logging import getLogger 9 from logging import getLogger
11 import mimetypes 10 import mimetypes
12 from os.path import join, dirname, expanduser 11 from os.path import join, dirname, expanduser
13 import signal 12 import signal
14 import sys 13 import sys
15 -import re  
16 from typing import List, Optional, Union 14 from typing import List, Optional, Union
17 import uuid 15 import uuid
18 16
@@ -20,7 +18,7 @@ import uuid @@ -20,7 +18,7 @@ import uuid
20 import tornado.httpserver 18 import tornado.httpserver
21 import tornado.ioloop 19 import tornado.ioloop
22 import tornado.web 20 import tornado.web
23 -from tornado.escape import to_unicode 21 +from tornado.escape import to_unicode, utf8
24 22
25 # this project 23 # this project
26 from aprendizations.renderer_markdown import md_to_html 24 from aprendizations.renderer_markdown import md_to_html
@@ -32,152 +30,54 @@ from aprendizations import APP_NAME @@ -32,152 +30,54 @@ from aprendizations import APP_NAME
32 logger = getLogger(__name__) 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 # Handlers 34 # Handlers
82 # ============================================================================ 35 # ============================================================================
83 # pylint: disable=abstract-method 36 # pylint: disable=abstract-method
84 class BaseHandler(tornado.web.RequestHandler): 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 def get_current_user(self): 43 def get_current_user(self):
95 '''called on every method decorated with @tornado.web.authenticated''' 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 class LoginHandler(BaseHandler): 50 class LoginHandler(BaseHandler):
135 - '''  
136 - Handles /login  
137 - ''' 51 + '''Handles /login'''
138 52
139 def get(self) -> None: 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 async def post(self): 57 async def post(self):
146 - '''  
147 - Authenticate and redirect to application if successful  
148 - ''' 58 + '''Authenticate and redirect to application if successful'''
149 userid = self.get_body_argument('uid') or '' 59 userid = self.get_body_argument('uid') or ''
150 passwd = self.get_body_argument('pw') 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 class LogoutHandler(BaseHandler): 71 class LogoutHandler(BaseHandler):
166 - '''  
167 - Handles /logout  
168 - ''' 72 + '''Handle /logout'''
  73 +
169 @tornado.web.authenticated 74 @tornado.web.authenticated
170 def get(self) -> None: 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 self.redirect('/') 79 self.redirect('/')
177 80
178 - def on_finish(self) -> None:  
179 - self.learn.logout(self.current_user)  
180 -  
181 81
182 # ---------------------------------------------------------------------------- 82 # ----------------------------------------------------------------------------
183 class ChangePasswordHandler(BaseHandler): 83 class ChangePasswordHandler(BaseHandler):
@@ -191,20 +91,9 @@ class ChangePasswordHandler(BaseHandler): @@ -191,20 +91,9 @@ class ChangePasswordHandler(BaseHandler):
191 Try to change password and show success/fail status 91 Try to change password and show success/fail status
192 ''' 92 '''
193 userid = self.current_user 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 self.write({'msg': to_unicode(notification)}) 97 self.write({'msg': to_unicode(notification)})
209 98
210 99
@@ -213,7 +102,7 @@ class RootHandler(BaseHandler): @@ -213,7 +102,7 @@ class RootHandler(BaseHandler):
213 ''' 102 '''
214 Handles root / 103 Handles root /
215 ''' 104 '''
216 - 105 +
217 @tornado.web.authenticated 106 @tornado.web.authenticated
218 def get(self) -> None: 107 def get(self) -> None:
219 '''Redirect to main entrypoint''' 108 '''Redirect to main entrypoint'''
@@ -225,7 +114,6 @@ class CoursesHandler(BaseHandler): @@ -225,7 +114,6 @@ class CoursesHandler(BaseHandler):
225 ''' 114 '''
226 Handles /courses 115 Handles /courses
227 ''' 116 '''
228 -  
229 def set_default_headers(self, *_) -> None: 117 def set_default_headers(self, *_) -> None:
230 self.set_header('Cache-Control', 'no-cache') 118 self.set_header('Cache-Control', 'no-cache')
231 119
@@ -236,8 +124,8 @@ class CoursesHandler(BaseHandler): @@ -236,8 +124,8 @@ class CoursesHandler(BaseHandler):
236 self.render('courses.html', 124 self.render('courses.html',
237 appname=APP_NAME, 125 appname=APP_NAME,
238 uid=uid, 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 # courses_progress= 129 # courses_progress=
242 ) 130 )
243 131
@@ -254,21 +142,20 @@ class CourseHandler2(BaseHandler): @@ -254,21 +142,20 @@ class CourseHandler2(BaseHandler):
254 logger.debug('[CourseHandler2] uid="%s", course_id="%s"', uid, course_id) 142 logger.debug('[CourseHandler2] uid="%s", course_id="%s"', uid, course_id)
255 143
256 if course_id == '': 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 try: 147 try:
260 - self.learn.start_course(uid, course_id) 148 + self.app.start_course(uid, course_id)
261 except LearnException: 149 except LearnException:
262 self.redirect('/courses') 150 self.redirect('/courses')
263 151
264 - # print(self.learn.get_course(course_id))  
265 self.render('maintopics-table2.html', 152 self.render('maintopics-table2.html',
266 appname=APP_NAME, 153 appname=APP_NAME,
267 uid=uid, 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 course_id=course_id, 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,20 +174,20 @@ class CourseHandler(BaseHandler):
287 logger.debug('[CourseHandler] uid="%s", course_id="%s"', uid, course_id) 174 logger.debug('[CourseHandler] uid="%s", course_id="%s"', uid, course_id)
288 175
289 if course_id == '': 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 try: 179 try:
293 - self.learn.start_course(uid, course_id) 180 + self.app.start_course(uid, course_id)
294 except LearnException: 181 except LearnException:
295 self.redirect('/courses') 182 self.redirect('/courses')
296 183
297 self.render('maintopics-table.html', 184 self.render('maintopics-table.html',
298 appname=APP_NAME, 185 appname=APP_NAME,
299 uid=uid, 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 course_id=course_id, 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,15 +208,15 @@ class TopicHandler(BaseHandler):
321 uid = self.current_user 208 uid = self.current_user
322 logger.debug('[TopicHandler] %s', topic) 209 logger.debug('[TopicHandler] %s', topic)
323 try: 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 except KeyError: 212 except KeyError:
326 self.redirect('/topics') 213 self.redirect('/topics')
327 214
328 self.render('topic.html', 215 self.render('topic.html',
329 appname=APP_NAME, 216 appname=APP_NAME,
330 uid=uid, 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,7 +232,7 @@ class FileHandler(BaseHandler):
345 Serve file from the /public subdirectory of a particular topic 232 Serve file from the /public subdirectory of a particular topic
346 ''' 233 '''
347 uid = self.current_user 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 filepath = expanduser(join(public_dir, filename)) 236 filepath = expanduser(join(public_dir, filename))
350 237
351 logger.debug('[FileHandler] uid=%s, public_dir=%s, filepath=%s', 238 logger.debug('[FileHandler] uid=%s, public_dir=%s, filepath=%s',
@@ -391,7 +278,7 @@ class QuestionHandler(BaseHandler): @@ -391,7 +278,7 @@ class QuestionHandler(BaseHandler):
391 ''' 278 '''
392 logger.debug('[QuestionHandler]') 279 logger.debug('[QuestionHandler]')
393 user = self.current_user 280 user = self.current_user
394 - question = await self.learn.get_question(user) 281 + question = await self.app.get_question(user)
395 282
396 # show current question 283 # show current question
397 if question is not None: 284 if question is not None:
@@ -402,7 +289,7 @@ class QuestionHandler(BaseHandler): @@ -402,7 +289,7 @@ class QuestionHandler(BaseHandler):
402 'params': { 289 'params': {
403 'type': question['type'], 290 'type': question['type'],
404 'question': to_unicode(qhtml), 291 'question': to_unicode(qhtml),
405 - 'progress': self.learn.get_student_progress(user), 292 + 'progress': self.app.get_student_progress(user),
406 'tries': question['tries'], 293 'tries': question['tries'],
407 } 294 }
408 } 295 }
@@ -432,7 +319,7 @@ class QuestionHandler(BaseHandler): @@ -432,7 +319,7 @@ class QuestionHandler(BaseHandler):
432 logger.debug('[QuestionHandler] answer=%s', answer) 319 logger.debug('[QuestionHandler] answer=%s', answer)
433 320
434 # --- check if browser opened different questions simultaneously 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 logger.warning('User %s desynchronized questions', user) 323 logger.warning('User %s desynchronized questions', user)
437 self.write({ 324 self.write({
438 'method': 'invalid', 325 'method': 'invalid',
@@ -444,7 +331,7 @@ class QuestionHandler(BaseHandler): @@ -444,7 +331,7 @@ class QuestionHandler(BaseHandler):
444 return 331 return
445 332
446 # --- answers are in a list. fix depending on question type 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 ans: Optional[Union[List, str]] 335 ans: Optional[Union[List, str]]
449 if qtype in ('success', 'information', 'info'): 336 if qtype in ('success', 'information', 'info'):
450 ans = None 337 ans = None
@@ -456,7 +343,7 @@ class QuestionHandler(BaseHandler): @@ -456,7 +343,7 @@ class QuestionHandler(BaseHandler):
456 ans = answer 343 ans = answer
457 344
458 # --- check answer (nonblocking) and get corrected question and action 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 # --- build response 348 # --- build response
462 response = {'method': question['status'], 'params': {}} 349 response = {'method': question['status'], 'params': {}}
@@ -470,7 +357,7 @@ class QuestionHandler(BaseHandler): @@ -470,7 +357,7 @@ class QuestionHandler(BaseHandler):
470 md=md_to_html) 357 md=md_to_html)
471 response['params'] = { 358 response['params'] = {
472 'type': question['type'], 359 'type': question['type'],
473 - 'progress': self.learn.get_student_progress(user), 360 + 'progress': self.app.get_student_progress(user),
474 'comments': to_unicode(comments), 361 'comments': to_unicode(comments),
475 'solution': to_unicode(solution), 362 'solution': to_unicode(solution),
476 'tries': question['tries'], 363 'tries': question['tries'],
@@ -481,7 +368,7 @@ class QuestionHandler(BaseHandler): @@ -481,7 +368,7 @@ class QuestionHandler(BaseHandler):
481 md=md_to_html) 368 md=md_to_html)
482 response['params'] = { 369 response['params'] = {
483 'type': question['type'], 370 'type': question['type'],
484 - 'progress': self.learn.get_student_progress(user), 371 + 'progress': self.app.get_student_progress(user),
485 'comments': to_unicode(comments), 372 'comments': to_unicode(comments),
486 'tries': question['tries'], 373 'tries': question['tries'],
487 } 374 }
@@ -493,7 +380,7 @@ class QuestionHandler(BaseHandler): @@ -493,7 +380,7 @@ class QuestionHandler(BaseHandler):
493 'solution.html', solution=question['solution'], md=md_to_html) 380 'solution.html', solution=question['solution'], md=md_to_html)
494 response['params'] = { 381 response['params'] = {
495 'type': question['type'], 382 'type': question['type'],
496 - 'progress': self.learn.get_student_progress(user), 383 + 'progress': self.app.get_student_progress(user),
497 'comments': to_unicode(comments), 384 'comments': to_unicode(comments),
498 'solution': to_unicode(solution), 385 'solution': to_unicode(solution),
499 'tries': question['tries'], 386 'tries': question['tries'],
@@ -505,6 +392,32 @@ class QuestionHandler(BaseHandler): @@ -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 # Signal handler to catch Ctrl-C and abort server 421 # Signal handler to catch Ctrl-C and abort server
509 # ---------------------------------------------------------------------------- 422 # ----------------------------------------------------------------------------
510 def signal_handler(*_) -> None: 423 def signal_handler(*_) -> None:
@@ -525,11 +438,29 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: @@ -525,11 +438,29 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None:
525 ''' 438 '''
526 439
527 # --- create web application 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 logger.info('Web application started (tornado.web.Application)') 464 logger.info('Web application started (tornado.web.Application)')
534 465
535 # --- create tornado http server 466 # --- create tornado http server
@@ -538,14 +469,14 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: @@ -538,14 +469,14 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None:
538 except ValueError: 469 except ValueError:
539 logger.critical('Certificates cert.pem and privkey.pem not found') 470 logger.critical('Certificates cert.pem and privkey.pem not found')
540 sys.exit(1) 471 sys.exit(1)
541 - logger.debug('HTTP server started') 472 + logger.debug('HTTPS server started')
542 473
543 try: 474 try:
544 httpserver.listen(port) 475 httpserver.listen(port)
545 except OSError: 476 except OSError:
546 logger.critical('Cannot bind port %d. Already in use?', port) 477 logger.critical('Cannot bind port %d. Already in use?', port)
547 sys.exit(1) 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 # --- set signal handler for Control-C 481 # --- set signal handler for Control-C
551 signal.signal(signal.SIGINT, signal_handler) 482 signal.signal(signal.SIGINT, signal_handler)
aprendizations/templates/login.html
@@ -25,7 +25,7 @@ @@ -25,7 +25,7 @@
25 25
26 <link href="{{static_url('css/signin.css')}}" rel="stylesheet"> 26 <link href="{{static_url('css/signin.css')}}" rel="stylesheet">
27 27
28 - <title>{{appname}}</title> 28 + <title>aprendizations</title>
29 </head> 29 </head>
30 <body class="text-center"> 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,7 +4,7 @@ import asyncio
4 import logging 4 import logging
5 from os import path 5 from os import path
6 # import re 6 # import re
7 -import subprocess 7 +# import subprocess
8 from typing import Any, List 8 from typing import Any, List
9 9
10 # third party libraries 10 # third party libraries
@@ -212,36 +212,42 @@ def load_yaml(filename: str, default: Any = None) -&gt; Any: @@ -212,36 +212,42 @@ def load_yaml(filename: str, default: Any = None) -&gt; Any:
212 # ---------------------------------------------------------------------------- 212 # ----------------------------------------------------------------------------
213 # Same as above, but asynchronous 213 # Same as above, but asynchronous
214 # ---------------------------------------------------------------------------- 214 # ----------------------------------------------------------------------------
215 -async def run_script_async(script: str, 215 +async def run_script(script: str,
216 args: List[str] = [], 216 args: List[str] = [],
217 stdin: str = '', 217 stdin: str = '',
218 timeout: int = 2) -> Any: 218 timeout: int = 2) -> Any:
219 219
  220 + # normalize args
220 script = path.expanduser(script) 221 script = path.expanduser(script)
  222 + input_bytes = stdin.encode('utf-8')
221 args = [str(a) for a in args] 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 try: 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 else: 238 else:
242 try: 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 else: 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
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 ; [mypy-pygments.*] 5 ; [mypy-pygments.*]
6 ; ignore_missing_imports = True 6 ; ignore_missing_imports = True
@@ -18,7 +18,7 @@ setup( @@ -18,7 +18,7 @@ setup(
18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", 18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git",
19 packages=find_packages(), 19 packages=find_packages(),
20 include_package_data=True, # install files from MANIFEST.in 20 include_package_data=True, # install files from MANIFEST.in
21 - python_requires='>=3.9.*', 21 + python_requires='>=3.9',
22 install_requires=[ 22 install_requires=[
23 'tornado>=6.2', 23 'tornado>=6.2',
24 'mistune>=3.0.0rc4', 24 'mistune>=3.0.0rc4',