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,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) -> Any: | @@ -212,36 +212,42 @@ def load_yaml(filename: str, default: Any = None) -> 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 |
mypy.ini
setup.py
@@ -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', |