Commit 939be670c9e323d2967031b2ea4ba56c32c4d729
1 parent
cad28d33
Exists in
dev
update jquery. move links to include-libs.html
Showing
8 changed files
with
188 additions
and
197 deletions
Show diff stats
aprendizations/__init__.py
| 1 | -# Copyright (C) 2019 Miguel Barão | 1 | +# Copyright © 2022 Miguel Barão |
| 2 | # | 2 | # |
| 3 | # THE MIT License | 3 | # THE MIT License |
| 4 | # | 4 | # |
| @@ -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.08.dev1' | 33 | +APP_VERSION = '2022.12.dev1' |
| 34 | APP_DESCRIPTION = __doc__ | 34 | APP_DESCRIPTION = __doc__ |
| 35 | 35 | ||
| 36 | __author__ = 'Miguel Barão' | 36 | __author__ = 'Miguel Barão' |
| 37 | -__copyright__ = 'Copyright 2021, Miguel Barão' | 37 | +__copyright__ = 'Copyright © 2022, Miguel Barão' |
| 38 | __license__ = 'MIT license' | 38 | __license__ = 'MIT license' |
| 39 | __version__ = APP_VERSION | 39 | __version__ = APP_VERSION |
aprendizations/serve.py
| @@ -11,6 +11,7 @@ import mimetypes | @@ -11,6 +11,7 @@ import mimetypes | ||
| 11 | from os.path import join, dirname, expanduser | 11 | from os.path import join, dirname, expanduser |
| 12 | import signal | 12 | import signal |
| 13 | import sys | 13 | import sys |
| 14 | +import re | ||
| 14 | from typing import List, Optional, Union | 15 | from typing import List, Optional, Union |
| 15 | import uuid | 16 | import uuid |
| 16 | 17 | ||
| @@ -21,7 +22,7 @@ import tornado.web | @@ -21,7 +22,7 @@ import tornado.web | ||
| 21 | from tornado.escape import to_unicode | 22 | from tornado.escape import to_unicode |
| 22 | 23 | ||
| 23 | # this project | 24 | # this project |
| 24 | -from aprendizations.tools import md_to_html | 25 | +from aprendizations.renderer_markdown import md_to_html |
| 25 | from aprendizations.learnapp import LearnException | 26 | from aprendizations.learnapp import LearnException |
| 26 | from aprendizations import APP_NAME | 27 | from aprendizations import APP_NAME |
| 27 | 28 | ||
| @@ -82,6 +83,7 @@ class BaseHandler(tornado.web.RequestHandler): | @@ -82,6 +83,7 @@ class BaseHandler(tornado.web.RequestHandler): | ||
| 82 | ''' | 83 | ''' |
| 83 | Base handler common to all handlers. | 84 | Base handler common to all handlers. |
| 84 | ''' | 85 | ''' |
| 86 | + | ||
| 85 | @property | 87 | @property |
| 86 | def learn(self): | 88 | def learn(self): |
| 87 | '''easier access to learnapp''' | 89 | '''easier access to learnapp''' |
| @@ -105,6 +107,7 @@ class RankingsHandler(BaseHandler): | @@ -105,6 +107,7 @@ class RankingsHandler(BaseHandler): | ||
| 105 | ''' | 107 | ''' |
| 106 | Handles rankings page | 108 | Handles rankings page |
| 107 | ''' | 109 | ''' |
| 110 | + | ||
| 108 | @tornado.web.authenticated | 111 | @tornado.web.authenticated |
| 109 | def get(self) -> None: | 112 | def get(self) -> None: |
| 110 | ''' | 113 | ''' |
| @@ -130,33 +133,30 @@ class LoginHandler(BaseHandler): | @@ -130,33 +133,30 @@ class LoginHandler(BaseHandler): | ||
| 130 | ''' | 133 | ''' |
| 131 | Handles /login | 134 | Handles /login |
| 132 | ''' | 135 | ''' |
| 136 | + | ||
| 133 | def get(self) -> None: | 137 | def get(self) -> None: |
| 134 | ''' | 138 | ''' |
| 135 | - Renders login page | 139 | + Render login page |
| 136 | ''' | 140 | ''' |
| 137 | - self.render('login.html', | ||
| 138 | - appname=APP_NAME, | ||
| 139 | - error='') | 141 | + self.render('login.html', appname=APP_NAME, error='') |
| 140 | 142 | ||
| 141 | async def post(self): | 143 | async def post(self): |
| 142 | ''' | 144 | ''' |
| 143 | - Perform authentication and redirects to application if successful | 145 | + Authenticate and redirect to application if successful |
| 144 | ''' | 146 | ''' |
| 145 | - | ||
| 146 | - userid = (self.get_body_argument('uid') or '').lstrip('l') | 147 | + userid = self.get_body_argument('uid') or '' |
| 147 | passwd = self.get_body_argument('pw') | 148 | passwd = self.get_body_argument('pw') |
| 148 | - | ||
| 149 | - login_ok = await self.learn.login(userid, passwd) | ||
| 150 | - | ||
| 151 | - if login_ok: | ||
| 152 | - counter = str(self.learn.get_login_counter(userid)) | ||
| 153 | - self.set_secure_cookie('aprendizations_user', userid) | ||
| 154 | - self.set_secure_cookie('counter', counter) | ||
| 155 | - self.redirect('/') | ||
| 156 | - else: | ||
| 157 | - self.render('login.html', | ||
| 158 | - appname=APP_NAME, | ||
| 159 | - error='Número ou senha incorrectos') | 149 | + match = re.search(r'[0-9]+', userid) # extract number |
| 150 | + if match is not None: | ||
| 151 | + userid = match.group(0) # get string with number | ||
| 152 | + if await self.learn.login(userid, passwd): | ||
| 153 | + counter = str(self.learn.get_login_counter(userid)) | ||
| 154 | + self.set_secure_cookie('aprendizations_user', userid) | ||
| 155 | + self.set_secure_cookie('counter', counter) | ||
| 156 | + self.redirect('/') | ||
| 157 | + self.render('login.html', | ||
| 158 | + appname=APP_NAME, | ||
| 159 | + error='Número ou senha incorrectos') | ||
| 160 | 160 | ||
| 161 | 161 | ||
| 162 | # ---------------------------------------------------------------------------- | 162 | # ---------------------------------------------------------------------------- |
| @@ -167,7 +167,7 @@ class LogoutHandler(BaseHandler): | @@ -167,7 +167,7 @@ class LogoutHandler(BaseHandler): | ||
| 167 | @tornado.web.authenticated | 167 | @tornado.web.authenticated |
| 168 | def get(self) -> None: | 168 | def get(self) -> None: |
| 169 | ''' | 169 | ''' |
| 170 | - clears cookies and removes user session | 170 | + clear cookies and user session |
| 171 | ''' | 171 | ''' |
| 172 | self.clear_cookie('user') | 172 | self.clear_cookie('user') |
| 173 | self.clear_cookie('counter') | 173 | self.clear_cookie('counter') |
| @@ -182,14 +182,14 @@ class ChangePasswordHandler(BaseHandler): | @@ -182,14 +182,14 @@ class ChangePasswordHandler(BaseHandler): | ||
| 182 | ''' | 182 | ''' |
| 183 | Handles password change | 183 | Handles password change |
| 184 | ''' | 184 | ''' |
| 185 | + | ||
| 185 | @tornado.web.authenticated | 186 | @tornado.web.authenticated |
| 186 | async def post(self) -> None: | 187 | async def post(self) -> None: |
| 187 | ''' | 188 | ''' |
| 188 | - Tries to perform password change and then replies success/fail status | 189 | + Try to change password and show success/fail status |
| 189 | ''' | 190 | ''' |
| 190 | userid = self.current_user | 191 | userid = self.current_user |
| 191 | passwd = self.get_body_arguments('new_password')[0] | 192 | passwd = self.get_body_arguments('new_password')[0] |
| 192 | - | ||
| 193 | changed_ok = await self.learn.change_password(userid, passwd) | 193 | changed_ok = await self.learn.change_password(userid, passwd) |
| 194 | if changed_ok: | 194 | if changed_ok: |
| 195 | notification = self.render_string( | 195 | notification = self.render_string( |
| @@ -203,7 +203,6 @@ class ChangePasswordHandler(BaseHandler): | @@ -203,7 +203,6 @@ class ChangePasswordHandler(BaseHandler): | ||
| 203 | type='danger', | 203 | type='danger', |
| 204 | msg='A password não foi alterada!' | 204 | msg='A password não foi alterada!' |
| 205 | ) | 205 | ) |
| 206 | - | ||
| 207 | self.write({'msg': to_unicode(notification)}) | 206 | self.write({'msg': to_unicode(notification)}) |
| 208 | 207 | ||
| 209 | 208 | ||
| @@ -212,9 +211,10 @@ class RootHandler(BaseHandler): | @@ -212,9 +211,10 @@ class RootHandler(BaseHandler): | ||
| 212 | ''' | 211 | ''' |
| 213 | Handles root / | 212 | Handles root / |
| 214 | ''' | 213 | ''' |
| 214 | + | ||
| 215 | @tornado.web.authenticated | 215 | @tornado.web.authenticated |
| 216 | def get(self) -> None: | 216 | def get(self) -> None: |
| 217 | - '''Simply redirects to the main entrypoint''' | 217 | + '''Redirect to main entrypoint''' |
| 218 | self.redirect('/courses') | 218 | self.redirect('/courses') |
| 219 | 219 | ||
| 220 | 220 | ||
| @@ -223,12 +223,13 @@ class CoursesHandler(BaseHandler): | @@ -223,12 +223,13 @@ class CoursesHandler(BaseHandler): | ||
| 223 | ''' | 223 | ''' |
| 224 | Handles /courses | 224 | Handles /courses |
| 225 | ''' | 225 | ''' |
| 226 | + | ||
| 226 | def set_default_headers(self, *_) -> None: | 227 | def set_default_headers(self, *_) -> None: |
| 227 | self.set_header('Cache-Control', 'no-cache') | 228 | self.set_header('Cache-Control', 'no-cache') |
| 228 | 229 | ||
| 229 | @tornado.web.authenticated | 230 | @tornado.web.authenticated |
| 230 | def get(self) -> None: | 231 | def get(self) -> None: |
| 231 | - '''Renders list of available courses''' | 232 | + '''Render available courses''' |
| 232 | uid = self.current_user | 233 | uid = self.current_user |
| 233 | self.render('courses.html', | 234 | self.render('courses.html', |
| 234 | appname=APP_NAME, | 235 | appname=APP_NAME, |
| @@ -285,7 +286,7 @@ class TopicHandler(BaseHandler): | @@ -285,7 +286,7 @@ class TopicHandler(BaseHandler): | ||
| 285 | Starts a given topic | 286 | Starts a given topic |
| 286 | ''' | 287 | ''' |
| 287 | uid = self.current_user | 288 | uid = self.current_user |
| 288 | - | 289 | + logger.debug('[TopicHandler.get] %s', topic) |
| 289 | try: | 290 | try: |
| 290 | await self.learn.start_topic(uid, topic) # FIXME GET should not modify state... | 291 | await self.learn.start_topic(uid, topic) # FIXME GET should not modify state... |
| 291 | except KeyError: | 292 | except KeyError: |
| @@ -304,15 +305,18 @@ class FileHandler(BaseHandler): | @@ -304,15 +305,18 @@ class FileHandler(BaseHandler): | ||
| 304 | ''' | 305 | ''' |
| 305 | Serves files from the /public subdir of the topics. | 306 | Serves files from the /public subdir of the topics. |
| 306 | ''' | 307 | ''' |
| 308 | + | ||
| 307 | @tornado.web.authenticated | 309 | @tornado.web.authenticated |
| 308 | async def get(self, filename) -> None: | 310 | async def get(self, filename) -> None: |
| 309 | ''' | 311 | ''' |
| 310 | - Serves files from /public subdirectories of a particular topic | 312 | + Serve file from the /public subdirectory of a particular topic |
| 311 | ''' | 313 | ''' |
| 312 | uid = self.current_user | 314 | uid = self.current_user |
| 313 | public_dir = self.learn.get_current_public_dir(uid) | 315 | public_dir = self.learn.get_current_public_dir(uid) |
| 314 | filepath = expanduser(join(public_dir, filename)) | 316 | filepath = expanduser(join(public_dir, filename)) |
| 315 | 317 | ||
| 318 | + logger.debug('uid=%s, public_dir=%s, filepath=%s', uid, public_dir, | ||
| 319 | + filepath) | ||
| 316 | try: | 320 | try: |
| 317 | with open(filepath, 'rb') as file: | 321 | with open(filepath, 'rb') as file: |
| 318 | data = file.read() | 322 | data = file.read() |
| @@ -350,10 +354,9 @@ class QuestionHandler(BaseHandler): | @@ -350,10 +354,9 @@ class QuestionHandler(BaseHandler): | ||
| 350 | @tornado.web.authenticated | 354 | @tornado.web.authenticated |
| 351 | async def get(self) -> None: | 355 | async def get(self) -> None: |
| 352 | ''' | 356 | ''' |
| 353 | - Gets question to render. | ||
| 354 | - Shows an animated trophy if there are no more questions in the topic. | 357 | + Get question to render or an animated trophy if no more questions. |
| 355 | ''' | 358 | ''' |
| 356 | - logger.debug('[QuestionHandler]') | 359 | + logger.debug('[QuestionHandler.get]') |
| 357 | user = self.current_user | 360 | user = self.current_user |
| 358 | question = await self.learn.get_question(user) | 361 | question = await self.learn.get_question(user) |
| 359 | 362 | ||
| @@ -387,7 +390,7 @@ class QuestionHandler(BaseHandler): | @@ -387,7 +390,7 @@ class QuestionHandler(BaseHandler): | ||
| 387 | @tornado.web.authenticated | 390 | @tornado.web.authenticated |
| 388 | async def post(self) -> None: | 391 | async def post(self) -> None: |
| 389 | ''' | 392 | ''' |
| 390 | - Corrects answer and returns status: right, wrong, try_again | 393 | + Correct answer and return status: right, wrong, try_again |
| 391 | Does not move to next question. | 394 | Does not move to next question. |
| 392 | ''' | 395 | ''' |
| 393 | user = self.current_user | 396 | user = self.current_user |
| @@ -422,18 +425,16 @@ class QuestionHandler(BaseHandler): | @@ -422,18 +425,16 @@ class QuestionHandler(BaseHandler): | ||
| 422 | # --- check answer (nonblocking) and get corrected question and action | 425 | # --- check answer (nonblocking) and get corrected question and action |
| 423 | question = await self.learn.check_answer(user, ans) | 426 | question = await self.learn.check_answer(user, ans) |
| 424 | 427 | ||
| 425 | - # --- built response to return | 428 | + # --- build response |
| 426 | response = {'method': question['status'], 'params': {}} | 429 | response = {'method': question['status'], 'params': {}} |
| 427 | 430 | ||
| 428 | if question['status'] == 'right': # get next question in the topic | 431 | if question['status'] == 'right': # get next question in the topic |
| 429 | comments = self.render_string('comments-right.html', | 432 | comments = self.render_string('comments-right.html', |
| 430 | comments=question['comments'], | 433 | comments=question['comments'], |
| 431 | md=md_to_html) | 434 | md=md_to_html) |
| 432 | - | ||
| 433 | solution = self.render_string('solution.html', | 435 | solution = self.render_string('solution.html', |
| 434 | solution=question['solution'], | 436 | solution=question['solution'], |
| 435 | md=md_to_html) | 437 | md=md_to_html) |
| 436 | - | ||
| 437 | response['params'] = { | 438 | response['params'] = { |
| 438 | 'type': question['type'], | 439 | 'type': question['type'], |
| 439 | 'progress': self.learn.get_student_progress(user), | 440 | 'progress': self.learn.get_student_progress(user), |
| @@ -445,7 +446,6 @@ class QuestionHandler(BaseHandler): | @@ -445,7 +446,6 @@ class QuestionHandler(BaseHandler): | ||
| 445 | comments = self.render_string('comments.html', | 446 | comments = self.render_string('comments.html', |
| 446 | comments=question['comments'], | 447 | comments=question['comments'], |
| 447 | md=md_to_html) | 448 | md=md_to_html) |
| 448 | - | ||
| 449 | response['params'] = { | 449 | response['params'] = { |
| 450 | 'type': question['type'], | 450 | 'type': question['type'], |
| 451 | 'progress': self.learn.get_student_progress(user), | 451 | 'progress': self.learn.get_student_progress(user), |
| @@ -456,10 +456,8 @@ class QuestionHandler(BaseHandler): | @@ -456,10 +456,8 @@ class QuestionHandler(BaseHandler): | ||
| 456 | comments = self.render_string('comments.html', | 456 | comments = self.render_string('comments.html', |
| 457 | comments=question['comments'], | 457 | comments=question['comments'], |
| 458 | md=md_to_html) | 458 | md=md_to_html) |
| 459 | - | ||
| 460 | solution = self.render_string( | 459 | solution = self.render_string( |
| 461 | 'solution.html', solution=question['solution'], md=md_to_html) | 460 | 'solution.html', solution=question['solution'], md=md_to_html) |
| 462 | - | ||
| 463 | response['params'] = { | 461 | response['params'] = { |
| 464 | 'type': question['type'], | 462 | 'type': question['type'], |
| 465 | 'progress': self.learn.get_student_progress(user), | 463 | 'progress': self.learn.get_student_progress(user), |
| @@ -501,7 +499,7 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | @@ -501,7 +499,7 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | ||
| 501 | sys.exit(1) | 499 | sys.exit(1) |
| 502 | logger.info('Web application started (tornado.web.Application)') | 500 | logger.info('Web application started (tornado.web.Application)') |
| 503 | 501 | ||
| 504 | - # --- create tornado webserver | 502 | + # --- create tornado http server |
| 505 | try: | 503 | try: |
| 506 | httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl) | 504 | httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl) |
| 507 | except ValueError: | 505 | except ValueError: |
| @@ -515,9 +513,11 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | @@ -515,9 +513,11 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | ||
| 515 | logger.critical('Cannot bind port %d. Already in use?', port) | 513 | logger.critical('Cannot bind port %d. Already in use?', port) |
| 516 | sys.exit(1) | 514 | sys.exit(1) |
| 517 | logger.info('Webserver listening on %d... (Ctrl-C to stop)', port) | 515 | logger.info('Webserver listening on %d... (Ctrl-C to stop)', port) |
| 516 | + | ||
| 517 | + # --- set signal handler for Control-C | ||
| 518 | signal.signal(signal.SIGINT, signal_handler) | 518 | signal.signal(signal.SIGINT, signal_handler) |
| 519 | 519 | ||
| 520 | - # --- run webserver | 520 | + # --- run tornado webserver |
| 521 | try: | 521 | try: |
| 522 | tornado.ioloop.IOLoop.current().start() # running... | 522 | tornado.ioloop.IOLoop.current().start() # running... |
| 523 | except Exception: | 523 | except Exception: |
aprendizations/templates/include-libs.html
| 1 | <!-- jquery --> | 1 | <!-- jquery --> |
| 2 | -<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script> | 2 | +<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> |
| 3 | 3 | ||
| 4 | <!-- bootstrap --> | 4 | <!-- bootstrap --> |
| 5 | <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> | 5 | <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> |
| @@ -7,3 +7,14 @@ | @@ -7,3 +7,14 @@ | ||
| 7 | 7 | ||
| 8 | <!-- bootstrap icons --> | 8 | <!-- bootstrap icons --> |
| 9 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> | 9 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> |
| 10 | + | ||
| 11 | +<!-- MathJax --> | ||
| 12 | +<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> | ||
| 13 | +<script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | ||
| 14 | + | ||
| 15 | +<!-- codemirror --> | ||
| 16 | +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" /> | ||
| 17 | +<script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script> | ||
| 18 | + | ||
| 19 | +<!-- animate --> | ||
| 20 | +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" /> |
aprendizations/templates/question-checkbox.html
| @@ -3,13 +3,14 @@ | @@ -3,13 +3,14 @@ | ||
| 3 | 3 | ||
| 4 | {% block answer %} | 4 | {% block answer %} |
| 5 | <fieldset data-role="controlgroup"> | 5 | <fieldset data-role="controlgroup"> |
| 6 | - <div class="list-group"> | ||
| 7 | - {% for n,opt in enumerate(question['options']) %} | ||
| 8 | - <label class="list-group-item list-group-item-action"> | ||
| 9 | - <input type="checkbox" class="form-check-input" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> | ||
| 10 | - {{ md(opt, strip_p_tag=True) }} | ||
| 11 | - </label> | ||
| 12 | - {% end %} | 6 | + <div class="list-group"> |
| 7 | + {% for n,opt in enumerate(question['options']) %} | ||
| 8 | + <li class="list-group-item"> | ||
| 9 | + <input class="form-check-input" type="checkbox" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> | ||
| 10 | + <label class="form-check-label" for="{{ n }}"> | ||
| 11 | + {{ md(opt).removeprefix('<p>').removesuffix('</p>') }} | ||
| 12 | + </label> | ||
| 13 | + {% end %} | ||
| 13 | </div> | 14 | </div> |
| 14 | </fieldset> | 15 | </fieldset> |
| 15 | <input type="hidden" name="qid" value="{{ question['qid'] }}"> | 16 | <input type="hidden" name="qid" value="{{ question['qid'] }}"> |
aprendizations/templates/question-radio.html
| @@ -5,12 +5,12 @@ | @@ -5,12 +5,12 @@ | ||
| 5 | <fieldset data-role="controlgroup"> | 5 | <fieldset data-role="controlgroup"> |
| 6 | <div class="list-group"> | 6 | <div class="list-group"> |
| 7 | {% for n,opt in enumerate(question['options']) %} | 7 | {% for n,opt in enumerate(question['options']) %} |
| 8 | - <label class="list-group-item list-group-item-action"> | ||
| 9 | - <input type="radio" class="form-check-input" id="{{ n }}" accesskey="{{ n+1 }}" name="answer" value="{{ n }}"> | ||
| 10 | - <label for="{{ n }}" class="custom-control-label"> | ||
| 11 | - {{ md(opt, strip_p_tag=True) }} | 8 | + <li class="list-group-item"> |
| 9 | + <input class="form-check-input" type="radio" name="answer" value="{{ n }}" id="{{ n }}"> | ||
| 10 | + <label class="form-check-label stretched-link" for="{{ n }}"> | ||
| 11 | + {{ md(opt).removeprefix('<p>').removesuffix('</p>') }} | ||
| 12 | </label> | 12 | </label> |
| 13 | - </label> | 13 | + </li> |
| 14 | {% end %} | 14 | {% end %} |
| 15 | </div> | 15 | </div> |
| 16 | </fieldset> | 16 | </fieldset> |
aprendizations/templates/topic.html
| @@ -8,27 +8,6 @@ | @@ -8,27 +8,6 @@ | ||
| 8 | 8 | ||
| 9 | {% include include-libs.html %} | 9 | {% include include-libs.html %} |
| 10 | 10 | ||
| 11 | - <!-- mathjax --> | ||
| 12 | - <script> | ||
| 13 | - MathJax = { | ||
| 14 | - tex: { | ||
| 15 | - inlineMath: [['$$$', '$$$'], ['\\(', '\\)']] | ||
| 16 | - }, | ||
| 17 | - svg: { | ||
| 18 | - fontCache: 'global' | ||
| 19 | - } | ||
| 20 | - }; | ||
| 21 | - </script> | ||
| 22 | - <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> | ||
| 23 | - <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | ||
| 24 | - | ||
| 25 | - <!-- codemirror --> | ||
| 26 | - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" /> | ||
| 27 | - <script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script> | ||
| 28 | - | ||
| 29 | - <!-- animate --> | ||
| 30 | - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" /> | ||
| 31 | - | ||
| 32 | <!-- local --> | 11 | <!-- local --> |
| 33 | <link rel="stylesheet" href="{{static_url('css/github.css')}}" /> | 12 | <link rel="stylesheet" href="{{static_url('css/github.css')}}" /> |
| 34 | <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> | 13 | <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> |
| @@ -99,7 +78,7 @@ | @@ -99,7 +78,7 @@ | ||
| 99 | <div id="solution_wrong"></div> | 78 | <div id="solution_wrong"></div> |
| 100 | </div> | 79 | </div> |
| 101 | </div> | 80 | </div> |
| 102 | - <!-- button reponder / continuar --> | 81 | + <!-- button responder / continuar --> |
| 103 | <div class="d-grid gap-2"> | 82 | <div class="d-grid gap-2"> |
| 104 | <button type="submit" class="btn btn-primary btn-lg btn-block my-5 shadow bg-gradient" id="submit" data-bs-toggle="button" href="#solution" style="display: none"></button> | 83 | <button type="submit" class="btn btn-primary btn-lg btn-block my-5 shadow bg-gradient" id="submit" data-bs-toggle="button" href="#solution" style="display: none"></button> |
| 105 | </div> | 84 | </div> |
aprendizations/tools.py
| @@ -3,15 +3,15 @@ | @@ -3,15 +3,15 @@ | ||
| 3 | import asyncio | 3 | 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 |
| 11 | -import mistune | ||
| 12 | -from pygments import highlight | ||
| 13 | -from pygments.lexers import get_lexer_by_name | ||
| 14 | -from pygments.formatters import HtmlFormatter | 11 | +# import mistune |
| 12 | +# from pygments import highlight | ||
| 13 | +# from pygments.lexers import get_lexer_by_name | ||
| 14 | +# from pygments.formatters import HtmlFormatter | ||
| 15 | import yaml | 15 | import yaml |
| 16 | 16 | ||
| 17 | 17 | ||
| @@ -28,119 +28,119 @@ logger = logging.getLogger(__name__) | @@ -28,119 +28,119 @@ logger = logging.getLogger(__name__) | ||
| 28 | # Block math: | 28 | # Block math: |
| 29 | # $$x$$ or \begin{equation}x\end{equation} | 29 | # $$x$$ or \begin{equation}x\end{equation} |
| 30 | # ------------------------------------------------------------------------- | 30 | # ------------------------------------------------------------------------- |
| 31 | -class MathBlockGrammar(mistune.BlockGrammar): | ||
| 32 | - block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) | ||
| 33 | - latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', | ||
| 34 | - re.DOTALL) | ||
| 35 | - | ||
| 36 | - | ||
| 37 | -class MathBlockLexer(mistune.BlockLexer): | ||
| 38 | - default_rules = ['block_math', 'latex_environment'] \ | ||
| 39 | - + mistune.BlockLexer.default_rules | ||
| 40 | - | ||
| 41 | - def __init__(self, rules=None, **kwargs): | ||
| 42 | - if rules is None: | ||
| 43 | - rules = MathBlockGrammar() | ||
| 44 | - super().__init__(rules, **kwargs) | ||
| 45 | - | ||
| 46 | - def parse_block_math(self, m): | ||
| 47 | - '''Parse a $$math$$ block''' | ||
| 48 | - self.tokens.append({ | ||
| 49 | - 'type': 'block_math', | ||
| 50 | - 'text': m.group(1) | ||
| 51 | - }) | ||
| 52 | - | ||
| 53 | - def parse_latex_environment(self, m): | ||
| 54 | - r'''Parse an environment \begin{name}text\end{name}''' | ||
| 55 | - self.tokens.append({ | ||
| 56 | - 'type': 'latex_environment', | ||
| 57 | - 'name': m.group(1), | ||
| 58 | - 'text': m.group(2) | ||
| 59 | - }) | ||
| 60 | - | ||
| 61 | - | ||
| 62 | -# ------------------------------------------------------------------------- | ||
| 63 | -# Inline math: $x$ | ||
| 64 | -# ------------------------------------------------------------------------- | ||
| 65 | -class MathInlineGrammar(mistune.InlineGrammar): | ||
| 66 | - math = re.compile(r'^\$(.+?)\$', re.DOTALL) | ||
| 67 | - block_math = re.compile(r'^\$\$(.+?)\$\$', re.DOTALL) | ||
| 68 | - text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)') | ||
| 69 | - | ||
| 70 | - | ||
| 71 | -class MathInlineLexer(mistune.InlineLexer): | ||
| 72 | - default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules | ||
| 73 | - | ||
| 74 | - def __init__(self, renderer, rules=None, **kwargs): | ||
| 75 | - if rules is None: | ||
| 76 | - rules = MathInlineGrammar() | ||
| 77 | - super().__init__(renderer, rules, **kwargs) | ||
| 78 | - | ||
| 79 | - def output_math(self, m): | ||
| 80 | - return self.renderer.inline_math(m.group(1)) | ||
| 81 | - | ||
| 82 | - def output_block_math(self, m): | ||
| 83 | - return self.renderer.block_math(m.group(1)) | ||
| 84 | - | ||
| 85 | - | ||
| 86 | -class MarkdownWithMath(mistune.Markdown): | ||
| 87 | - def __init__(self, renderer, **kwargs): | ||
| 88 | - if 'inline' not in kwargs: | ||
| 89 | - kwargs['inline'] = MathInlineLexer | ||
| 90 | - if 'block' not in kwargs: | ||
| 91 | - kwargs['block'] = MathBlockLexer | ||
| 92 | - super().__init__(renderer, **kwargs) | ||
| 93 | - | ||
| 94 | - def output_block_math(self): | ||
| 95 | - return self.renderer.block_math(self.token['text']) | ||
| 96 | - | ||
| 97 | - def output_latex_environment(self): | ||
| 98 | - return self.renderer.latex_environment(self.token['name'], | ||
| 99 | - self.token['text']) | ||
| 100 | - | ||
| 101 | - | ||
| 102 | -class HighlightRenderer(mistune.Renderer): | ||
| 103 | - def block_code(self, code, lang='text'): | ||
| 104 | - try: | ||
| 105 | - lexer = get_lexer_by_name(lang, stripall=False) | ||
| 106 | - except Exception: | ||
| 107 | - lexer = get_lexer_by_name('text', stripall=False) | ||
| 108 | - | ||
| 109 | - formatter = HtmlFormatter() | ||
| 110 | - return highlight(code, lexer, formatter) | ||
| 111 | - | ||
| 112 | - def table(self, header, body): | ||
| 113 | - return ('<table class="table table-sm"><thead class="thead-light">' | ||
| 114 | - f'{header}</thead><tbody>{body}</tbody></table>') | ||
| 115 | - | ||
| 116 | - def image(self, src, title, alt): | ||
| 117 | - alt = mistune.escape(alt, quote=True) | ||
| 118 | - title = mistune.escape(title or '', quote=True) | ||
| 119 | - return (f'<img src="/file/{src}" alt="{alt}" title="{title}"' | ||
| 120 | - f'class="img-fluid">') # class="img-fluid mx-auto d-block" | ||
| 121 | - | ||
| 122 | - # Pass math through unaltered - mathjax does the rendering in the browser | ||
| 123 | - def block_math(self, text): | ||
| 124 | - return fr'\[ {text} \]' | ||
| 125 | - | ||
| 126 | - def latex_environment(self, name, text): | ||
| 127 | - return fr'\begin{{{name}}} {text} \end{{{name}}}' | ||
| 128 | - | ||
| 129 | - def inline_math(self, text): | ||
| 130 | - return fr'\( {text} \)' | ||
| 131 | - | ||
| 132 | - | ||
| 133 | -# hard_wrap=True to insert <br> on newline | ||
| 134 | -markdown = MarkdownWithMath(HighlightRenderer(escape=True)) | ||
| 135 | - | ||
| 136 | - | ||
| 137 | -def md_to_html(text: str, strip_p_tag: bool = False) -> str: | ||
| 138 | - md: str = markdown(text) | ||
| 139 | - if strip_p_tag and md.startswith('<p>') and md.endswith('</p>\n'): | ||
| 140 | - return md[3:-5] | ||
| 141 | - else: | ||
| 142 | - return md | ||
| 143 | - | 31 | +# class MathBlockGrammar(mistune.BlockGrammar): |
| 32 | +# block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) | ||
| 33 | +# latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', | ||
| 34 | +# re.DOTALL) | ||
| 35 | +# | ||
| 36 | +# | ||
| 37 | +# class MathBlockLexer(mistune.BlockLexer): | ||
| 38 | +# default_rules = ['block_math', 'latex_environment'] \ | ||
| 39 | +# + mistune.BlockLexer.default_rules | ||
| 40 | +# | ||
| 41 | +# def __init__(self, rules=None, **kwargs): | ||
| 42 | +# if rules is None: | ||
| 43 | +# rules = MathBlockGrammar() | ||
| 44 | +# super().__init__(rules, **kwargs) | ||
| 45 | +# | ||
| 46 | +# def parse_block_math(self, m): | ||
| 47 | +# '''Parse a $$math$$ block''' | ||
| 48 | +# self.tokens.append({ | ||
| 49 | +# 'type': 'block_math', | ||
| 50 | +# 'text': m.group(1) | ||
| 51 | +# }) | ||
| 52 | +# | ||
| 53 | +# def parse_latex_environment(self, m): | ||
| 54 | +# r'''Parse an environment \begin{name}text\end{name}''' | ||
| 55 | +# self.tokens.append({ | ||
| 56 | +# 'type': 'latex_environment', | ||
| 57 | +# 'name': m.group(1), | ||
| 58 | +# 'text': m.group(2) | ||
| 59 | +# }) | ||
| 60 | +# | ||
| 61 | +# | ||
| 62 | +# # ------------------------------------------------------------------------- | ||
| 63 | +# # Inline math: $x$ | ||
| 64 | +# # ------------------------------------------------------------------------- | ||
| 65 | +# class MathInlineGrammar(mistune.InlineGrammar): | ||
| 66 | +# math = re.compile(r'^\$(.+?)\$', re.DOTALL) | ||
| 67 | +# block_math = re.compile(r'^\$\$(.+?)\$\$', re.DOTALL) | ||
| 68 | +# text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|https?://| {2,}\n|$)') | ||
| 69 | +# | ||
| 70 | +# | ||
| 71 | +# class MathInlineLexer(mistune.InlineLexer): | ||
| 72 | +# default_rules = ['block_math', 'math'] + mistune.InlineLexer.default_rules | ||
| 73 | +# | ||
| 74 | +# def __init__(self, renderer, rules=None, **kwargs): | ||
| 75 | +# if rules is None: | ||
| 76 | +# rules = MathInlineGrammar() | ||
| 77 | +# super().__init__(renderer, rules, **kwargs) | ||
| 78 | +# | ||
| 79 | +# def output_math(self, m): | ||
| 80 | +# return self.renderer.inline_math(m.group(1)) | ||
| 81 | +# | ||
| 82 | +# def output_block_math(self, m): | ||
| 83 | +# return self.renderer.block_math(m.group(1)) | ||
| 84 | +# | ||
| 85 | +# | ||
| 86 | +# class MarkdownWithMath(mistune.Markdown): | ||
| 87 | +# def __init__(self, renderer, **kwargs): | ||
| 88 | +# if 'inline' not in kwargs: | ||
| 89 | +# kwargs['inline'] = MathInlineLexer | ||
| 90 | +# if 'block' not in kwargs: | ||
| 91 | +# kwargs['block'] = MathBlockLexer | ||
| 92 | +# super().__init__(renderer, **kwargs) | ||
| 93 | +# | ||
| 94 | +# def output_block_math(self): | ||
| 95 | +# return self.renderer.block_math(self.token['text']) | ||
| 96 | +# | ||
| 97 | +# def output_latex_environment(self): | ||
| 98 | +# return self.renderer.latex_environment(self.token['name'], | ||
| 99 | +# self.token['text']) | ||
| 100 | +# | ||
| 101 | +# | ||
| 102 | +# class HighlightRenderer(mistune.Renderer): | ||
| 103 | +# def block_code(self, code, lang='text'): | ||
| 104 | +# try: | ||
| 105 | +# lexer = get_lexer_by_name(lang, stripall=False) | ||
| 106 | +# except Exception: | ||
| 107 | +# lexer = get_lexer_by_name('text', stripall=False) | ||
| 108 | +# | ||
| 109 | +# formatter = HtmlFormatter() | ||
| 110 | +# return highlight(code, lexer, formatter) | ||
| 111 | +# | ||
| 112 | +# def table(self, header, body): | ||
| 113 | +# return ('<table class="table table-sm"><thead class="thead-light">' | ||
| 114 | +# f'{header}</thead><tbody>{body}</tbody></table>') | ||
| 115 | +# | ||
| 116 | +# def image(self, src, title, alt): | ||
| 117 | +# alt = mistune.escape(alt, quote=True) | ||
| 118 | +# title = mistune.escape(title or '', quote=True) | ||
| 119 | +# return (f'<img src="/file/{src}" alt="{alt}" title="{title}"' | ||
| 120 | +# f'class="img-fluid">') # class="img-fluid mx-auto d-block" | ||
| 121 | +# | ||
| 122 | +# # Pass math through unaltered - mathjax does the rendering in the browser | ||
| 123 | +# def block_math(self, text): | ||
| 124 | +# return fr'\[ {text} \]' | ||
| 125 | +# | ||
| 126 | +# def latex_environment(self, name, text): | ||
| 127 | +# return fr'\begin{{{name}}} {text} \end{{{name}}}' | ||
| 128 | +# | ||
| 129 | +# def inline_math(self, text): | ||
| 130 | +# return fr'\( {text} \)' | ||
| 131 | +# | ||
| 132 | +# | ||
| 133 | +# # hard_wrap=True to insert <br> on newline | ||
| 134 | +# markdown = MarkdownWithMath(HighlightRenderer(escape=True)) | ||
| 135 | +# | ||
| 136 | +# | ||
| 137 | +# def md_to_html(text: str, strip_p_tag: bool = False) -> str: | ||
| 138 | +# md: str = markdown(text) | ||
| 139 | +# if strip_p_tag and md.startswith('<p>') and md.endswith('</p>\n'): | ||
| 140 | +# return md[3:-5] | ||
| 141 | +# else: | ||
| 142 | +# return md | ||
| 143 | +# | ||
| 144 | 144 | ||
| 145 | # --------------------------------------------------------------------------- | 145 | # --------------------------------------------------------------------------- |
| 146 | # load data from yaml file | 146 | # load data from yaml file |
setup.py
| @@ -21,7 +21,7 @@ setup( | @@ -21,7 +21,7 @@ setup( | ||
| 21 | python_requires='>=3.9.*', | 21 | python_requires='>=3.9.*', |
| 22 | install_requires=[ | 22 | install_requires=[ |
| 23 | 'tornado>=6.0', | 23 | 'tornado>=6.0', |
| 24 | - 'mistune<2', | 24 | + 'mistune>=3', |
| 25 | 'pyyaml>=5.1', | 25 | 'pyyaml>=5.1', |
| 26 | 'pygments', | 26 | 'pygments', |
| 27 | 'sqlalchemy>=1.4', | 27 | 'sqlalchemy>=1.4', |