Compare View
Commits (4)
Showing
29 changed files
Show diff stats
.gitignore
BUGS.md
| @@ -2,6 +2,15 @@ | @@ -2,6 +2,15 @@ | ||
| 2 | 2 | ||
| 3 | ## BUGS | 3 | ## BUGS |
| 4 | 4 | ||
| 5 | +- usar [xsrf_cookie_name](https://www.tornadoweb.org/en/stable/releases/v6.3.0.html) | ||
| 6 | +- usar [asyncio.loop.add_signal_handler](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.add_signal_handler) | ||
| 7 | + em vez de `signal.signal`. | ||
| 8 | +- fim do topico "servidor nao responde": /Users/mjsb/Repos/aprendizations/aprendizations/learnapp.py:296: SAWarning: Multiple rows returned with uselist=False for lazily-loaded attribute 'Topic.students' (This warning originated from the Session 'autoflush' process, which was invoked automatically in response to a user-initiated operation.) | ||
| 9 | + student.topics.append(student_topic) | ||
| 10 | + /Users/mjsb/Repos/aprendizations/aprendizations/learnapp.py:296: SAWarning: Object of type <StudentTopic> not in session, add operation along 'Topic.students' will not proceed (This warning originated from the Session 'autoflush' process, which was invoked automatically in response to a user-initiated operation.) | ||
| 11 | + student.topics.append(student_topic) | ||
| 12 | +- rendering das tabelas errado, e.g. computer/logic/operators:equivalence | ||
| 13 | +- warning ao arrancar: could not start course "favicon.ico" | ||
| 5 | - nao esta a respeitar o numero de tentativas `max_tries`. | 14 | - nao esta a respeitar o numero de tentativas `max_tries`. |
| 6 | - se na especificacao de um curso, a referencia do topico nao existir como | 15 | - se na especificacao de um curso, a referencia do topico nao existir como |
| 7 | directorio, rebenta. | 16 | directorio, rebenta. |
| @@ -23,6 +32,8 @@ | @@ -23,6 +32,8 @@ | ||
| 23 | 32 | ||
| 24 | ## TODO | 33 | ## TODO |
| 25 | 34 | ||
| 35 | +- RGPD: possibilidade ter alunos anonimos (sem numero e nome), poderem | ||
| 36 | +unregister. | ||
| 26 | - ordenação que faça sentido, organizada por chapters. | 37 | - ordenação que faça sentido, organizada por chapters. |
| 27 | - chapters deviam ser automaticamente checkados... | 38 | - chapters deviam ser automaticamente checkados... |
| 28 | - hints dos topicos fechados com as dependencias desse topico que ainda faltam | 39 | - hints dos topicos fechados com as dependencias desse topico que ainda faltam |
aprendizations/__init__.py
| 1 | -# Copyright © 2022 Miguel Barão | 1 | +# Copyright © 2025 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 = '2023.2.dev1' | 33 | +APP_VERSION = '2025.1.dev1' |
| 34 | APP_DESCRIPTION = __doc__ | 34 | APP_DESCRIPTION = __doc__ |
| 35 | 35 | ||
| 36 | __author__ = 'Miguel Barão' | 36 | __author__ = 'Miguel Barão' |
| 37 | -__copyright__ = 'Copyright © 2023, Miguel Barão' | 37 | +__copyright__ = 'Copyright © 2025, Miguel Barão' |
| 38 | __license__ = 'MIT license' | 38 | __license__ = 'MIT license' |
| 39 | __version__ = APP_VERSION | 39 | __version__ = APP_VERSION |
aprendizations/learnapp.py
| @@ -110,7 +110,7 @@ class Application(): | @@ -110,7 +110,7 @@ class Application(): | ||
| 110 | def _sanity_check_questions(self) -> None: | 110 | def _sanity_check_questions(self) -> None: |
| 111 | ''' | 111 | ''' |
| 112 | Unit tests for all questions | 112 | Unit tests for all questions |
| 113 | - | 113 | + |
| 114 | Generates all questions, give right and wrong answers and corrects. | 114 | Generates all questions, give right and wrong answers and corrects. |
| 115 | ''' | 115 | ''' |
| 116 | 116 | ||
| @@ -274,7 +274,7 @@ class Application(): | @@ -274,7 +274,7 @@ class Application(): | ||
| 274 | tid: str = student_state.get_previous_topic() | 274 | tid: str = student_state.get_previous_topic() |
| 275 | level: float = student_state.get_topic_level(tid) | 275 | level: float = student_state.get_topic_level(tid) |
| 276 | date: str = str(student_state.get_topic_date(tid)) | 276 | date: str = str(student_state.get_topic_date(tid)) |
| 277 | - logger.info('"%s" finished "%s" (level=%.2f)', uid, tid, level) | 277 | + logger.info('User "%s" finished topic "%s" (level=%.2f)', uid, tid, level) |
| 278 | 278 | ||
| 279 | with Session(self._engine) as session: | 279 | with Session(self._engine) as session: |
| 280 | query = select(StudentTopic) \ | 280 | query = select(StudentTopic) \ |
aprendizations/main.py
| @@ -168,9 +168,9 @@ def main(): | @@ -168,9 +168,9 @@ def main(): | ||
| 168 | logger.info('LearnApp started') | 168 | logger.info('LearnApp started') |
| 169 | 169 | ||
| 170 | # --- run webserver forever ---------------------------------------------- | 170 | # --- run webserver forever ---------------------------------------------- |
| 171 | - asyncio.run(webserver(app=app, | ||
| 172 | - ssl=ssl_ctx, | ||
| 173 | - port=arg.port, | 171 | + asyncio.run(webserver(app=app, |
| 172 | + ssl=ssl_ctx, | ||
| 173 | + port=arg.port, | ||
| 174 | debug=arg.debug)) | 174 | debug=arg.debug)) |
| 175 | logger.critical('Webserver stopped.') | 175 | logger.critical('Webserver stopped.') |
| 176 | 176 |
aprendizations/renderer_markdown.py
| @@ -10,6 +10,9 @@ from pygments.formatters import html | @@ -10,6 +10,9 @@ from pygments.formatters import html | ||
| 10 | 10 | ||
| 11 | class Renderer(mistune.HTMLRenderer): | 11 | class Renderer(mistune.HTMLRenderer): |
| 12 | def block_code(self, code, info=None): | 12 | def block_code(self, code, info=None): |
| 13 | + ''' | ||
| 14 | + Code syntax highlight using pygments | ||
| 15 | + ''' | ||
| 13 | if info is not None: | 16 | if info is not None: |
| 14 | lexer = get_lexer_by_name(info, stripall=True) | 17 | lexer = get_lexer_by_name(info, stripall=True) |
| 15 | formatter = html.HtmlFormatter() | 18 | formatter = html.HtmlFormatter() |
| @@ -17,6 +20,9 @@ class Renderer(mistune.HTMLRenderer): | @@ -17,6 +20,9 @@ class Renderer(mistune.HTMLRenderer): | ||
| 17 | return f'<pre><code>{mistune.escape(code)}</code></pre>' | 20 | return f'<pre><code>{mistune.escape(code)}</code></pre>' |
| 18 | 21 | ||
| 19 | def image(self, text, url, title=None): | 22 | def image(self, text, url, title=None): |
| 23 | + ''' | ||
| 24 | + Include image title and alternative text | ||
| 25 | + ''' | ||
| 20 | text = mistune.escape(text, quote=True) | 26 | text = mistune.escape(text, quote=True) |
| 21 | title = mistune.escape(title or '', quote=True) | 27 | title = mistune.escape(title or '', quote=True) |
| 22 | return (f'<img src="/file/{url}" alt="{text}" title="{title}"' | 28 | return (f'<img src="/file/{url}" alt="{text}" title="{title}"' |
aprendizations/serve.py
| @@ -6,18 +6,16 @@ Tornado Webserver | @@ -6,18 +6,16 @@ Tornado Webserver | ||
| 6 | # python standard library | 6 | # python standard library |
| 7 | import asyncio | 7 | import asyncio |
| 8 | import base64 | 8 | import base64 |
| 9 | -from logging import getLogger | ||
| 10 | import mimetypes | 9 | import mimetypes |
| 11 | -from os.path import join, dirname, expanduser | ||
| 12 | import signal | 10 | import signal |
| 13 | import sys | 11 | import sys |
| 14 | -from typing import List, Optional, Union | ||
| 15 | import uuid | 12 | import uuid |
| 13 | +import logging | ||
| 14 | +from os.path import join, dirname, expanduser | ||
| 15 | +from typing import List, Optional, Union | ||
| 16 | 16 | ||
| 17 | # third party libraries | 17 | # third party libraries |
| 18 | -import tornado.httpserver | ||
| 19 | -import tornado.ioloop | ||
| 20 | -import tornado.web | 18 | +import tornado |
| 21 | from tornado.escape import to_unicode | 19 | from tornado.escape import to_unicode |
| 22 | 20 | ||
| 23 | # this project | 21 | # this project |
| @@ -26,7 +24,7 @@ from aprendizations.learnapp import LearnException | @@ -26,7 +24,7 @@ from aprendizations.learnapp import LearnException | ||
| 26 | 24 | ||
| 27 | 25 | ||
| 28 | # setup logger for this module | 26 | # setup logger for this module |
| 29 | -logger = getLogger(__name__) | 27 | +logger = logging.getLogger(__name__) |
| 30 | 28 | ||
| 31 | 29 | ||
| 32 | # ============================================================================ | 30 | # ============================================================================ |
| @@ -40,7 +38,7 @@ class BaseHandler(tornado.web.RequestHandler): | @@ -40,7 +38,7 @@ class BaseHandler(tornado.web.RequestHandler): | ||
| 40 | 38 | ||
| 41 | def get_current_user(self): | 39 | def get_current_user(self): |
| 42 | '''called on every method decorated with @tornado.web.authenticated''' | 40 | '''called on every method decorated with @tornado.web.authenticated''' |
| 43 | - cookie = self.get_secure_cookie('aprendizations_user') | 41 | + cookie = self.get_signed_cookie('aprendizations_user') |
| 44 | return None if cookie is None else to_unicode(cookie) | 42 | return None if cookie is None else to_unicode(cookie) |
| 45 | 43 | ||
| 46 | 44 | ||
| @@ -59,7 +57,7 @@ class LoginHandler(BaseHandler): | @@ -59,7 +57,7 @@ class LoginHandler(BaseHandler): | ||
| 59 | loop = tornado.ioloop.IOLoop.current() | 57 | loop = tornado.ioloop.IOLoop.current() |
| 60 | login_ok = await self.app.login(uid, pw, loop) | 58 | login_ok = await self.app.login(uid, pw, loop) |
| 61 | if login_ok: | 59 | if login_ok: |
| 62 | - self.set_secure_cookie('aprendizations_user', uid) | 60 | + self.set_signed_cookie('aprendizations_user', uid) |
| 63 | self.redirect('/') | 61 | self.redirect('/') |
| 64 | else: | 62 | else: |
| 65 | self.render('login.html', error='Número ou senha incorrectos') | 63 | self.render('login.html', error='Número ou senha incorrectos') |
| @@ -391,7 +389,7 @@ def signal_handler(*_) -> None: | @@ -391,7 +389,7 @@ def signal_handler(*_) -> None: | ||
| 391 | ''' | 389 | ''' |
| 392 | reply = input(' --> Stop webserver? (yes/no) ') | 390 | reply = input(' --> Stop webserver? (yes/no) ') |
| 393 | if reply.lower() == 'yes': | 391 | if reply.lower() == 'yes': |
| 394 | - tornado.ioloop.IOLoop.current().stop() | 392 | + tornado.ioloop.IOLoop.current().stop() # FIXME: is there a recent alternative? |
| 395 | logger.critical('Webserver stopped.') | 393 | logger.critical('Webserver stopped.') |
| 396 | sys.exit(0) | 394 | sys.exit(0) |
| 397 | 395 | ||
| @@ -403,23 +401,25 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | @@ -403,23 +401,25 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -> None: | ||
| 403 | ''' | 401 | ''' |
| 404 | 402 | ||
| 405 | # --- create web application | 403 | # --- create web application |
| 404 | + opts = { 'app': app } | ||
| 406 | handlers = [ | 405 | handlers = [ |
| 407 | - (r'/login', LoginHandler, dict(app=app)), | ||
| 408 | - (r'/logout', LogoutHandler, dict(app=app)), | ||
| 409 | - (r'/change_password', ChangePasswordHandler, dict(app=app)), | ||
| 410 | - (r'/question', QuestionHandler, dict(app=app)), # render question | ||
| 411 | - (r'/rankings', RankingsHandler, dict(app=app)), # rankings table | ||
| 412 | - (r'/topic/(.+)', TopicHandler, dict(app=app)), # start topic | ||
| 413 | - (r'/file/(.+)', FileHandler, dict(app=app)), # serve file | ||
| 414 | - (r'/courses', CoursesHandler, dict(app=app)), # show available courses | ||
| 415 | - (r'/course/(.*)', CourseHandler, dict(app=app)), # show topics from course | ||
| 416 | - (r'/course2/(.*)', CourseHandler2, dict(app=app)), # show topics from course FIXME | ||
| 417 | - (r'/', RootHandler, dict(app=app)), # redirects | 406 | + (r'/login', LoginHandler, opts), |
| 407 | + (r'/logout', LogoutHandler, opts), | ||
| 408 | + (r'/change_password', ChangePasswordHandler, opts), | ||
| 409 | + (r'/question', QuestionHandler, opts), # render question | ||
| 410 | + (r'/rankings', RankingsHandler, opts), # rankings table | ||
| 411 | + (r'/topic/(.+)', TopicHandler, opts), # start topic | ||
| 412 | + (r'/file/(.+)', FileHandler, opts), # serve file | ||
| 413 | + (r'/courses', CoursesHandler, opts), # show available courses | ||
| 414 | + (r'/course/(.*)', CourseHandler, opts), # show topics from course | ||
| 415 | + (r'/course2/(.*)', CourseHandler2, opts), # show topics from course FIXME: | ||
| 416 | + (r'/', RootHandler, opts), # redirects | ||
| 418 | ] | 417 | ] |
| 419 | settings = { | 418 | settings = { |
| 420 | 'template_path': join(dirname(__file__), 'templates'), | 419 | 'template_path': join(dirname(__file__), 'templates'), |
| 421 | 'static_path': join(dirname(__file__), 'static'), | 420 | 'static_path': join(dirname(__file__), 'static'), |
| 422 | 'static_url_prefix': '/static/', | 421 | 'static_url_prefix': '/static/', |
| 422 | + # TODO: xsrf_cookie_name | ||
| 423 | 'xsrf_cookies': True, | 423 | 'xsrf_cookies': True, |
| 424 | 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), | 424 | 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), |
| 425 | 'login_url': '/login', | 425 | 'login_url': '/login', |
aprendizations/student.py
| @@ -50,35 +50,34 @@ class StudentState(): | @@ -50,35 +50,34 @@ class StudentState(): | ||
| 50 | self.factory = factory # question factory | 50 | self.factory = factory # question factory |
| 51 | self.courses = courses # {'course': ['topic_id1', 'topic_id2',...]} | 51 | self.courses = courses # {'course': ['topic_id1', 'topic_id2',...]} |
| 52 | 52 | ||
| 53 | - # data of this student | 53 | + # student data |
| 54 | self.uid = uid # user id '12345' | 54 | self.uid = uid # user id '12345' |
| 55 | self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} | 55 | self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} |
| 56 | 56 | ||
| 57 | # prepare for running | 57 | # prepare for running |
| 58 | self.update_topic_levels() # applies forgetting factor | 58 | self.update_topic_levels() # applies forgetting factor |
| 59 | self.unlock_topics() # whose dependencies have been completed | 59 | self.unlock_topics() # whose dependencies have been completed |
| 60 | - self.start_course(None) | 60 | + |
| 61 | + logger.debug('no active course') | ||
| 62 | + self.current_course: Optional[str] = None | ||
| 63 | + self.topic_sequence: List[str] = [] | ||
| 64 | + self.current_topic: Optional[str] = None | ||
| 65 | + self.previous_topic: Optional[str] = None | ||
| 61 | 66 | ||
| 62 | # ------------------------------------------------------------------------ | 67 | # ------------------------------------------------------------------------ |
| 63 | - def start_course(self, course: Optional[str]) -> None: | 68 | + def start_course(self, course: str) -> None: |
| 64 | ''' | 69 | ''' |
| 65 | Tries to start a course. | 70 | Tries to start a course. |
| 66 | Finds the recommended sequence of topics for the student. | 71 | Finds the recommended sequence of topics for the student. |
| 67 | ''' | 72 | ''' |
| 68 | - if course is None: | ||
| 69 | - logger.debug('no active course') | ||
| 70 | - self.current_course: Optional[str] = None | ||
| 71 | - self.topic_sequence: List[str] = [] | ||
| 72 | - self.current_topic: Optional[str] = None | ||
| 73 | - else: | ||
| 74 | - try: | ||
| 75 | - topics = self.courses[course]['goals'] | ||
| 76 | - except KeyError: | ||
| 77 | - logger.debug('course "%s" does not exist', course) | ||
| 78 | - raise | ||
| 79 | - logger.debug('starting course "%s"', course) | ||
| 80 | - self.current_course = course | ||
| 81 | - self.topic_sequence = self._recommend_sequence(topics) | 73 | + try: |
| 74 | + topics = self.courses[course]['goals'] | ||
| 75 | + except KeyError: | ||
| 76 | + logger.debug('course "%s" does not exist', course) | ||
| 77 | + raise | ||
| 78 | + logger.debug('starting course "%s"', course) | ||
| 79 | + self.current_course = course | ||
| 80 | + self.topic_sequence = self._recommend_sequence(topics) | ||
| 82 | 81 | ||
| 83 | # ------------------------------------------------------------------------ | 82 | # ------------------------------------------------------------------------ |
| 84 | async def start_topic(self, topic_ref: str) -> None: | 83 | async def start_topic(self, topic_ref: str) -> None: |
aprendizations/templates/include-head.html
| 1 | <meta charset="utf-8" /> | 1 | <meta charset="utf-8" /> |
| 2 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 2 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 3 | <meta name="author" content="Miguel Barão"> | 3 | <meta name="author" content="Miguel Barão"> |
| 4 | -<link rel="icon" href="favicon.ico"> | 4 | +<!-- <link href="{{static_url('favicon.ico')}}" rel="icon" /> --> |
| 5 | <title>aprendizations</title> | 5 | <title>aprendizations</title> |
aprendizations/templates/include-libs.html
| 1 | <!-- jquery --> | 1 | <!-- jquery --> |
| 2 | -<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> | 2 | +<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> |
| 3 | +<!-- <script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script> --> | ||
| 4 | +<!-- <script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> --> | ||
| 3 | 5 | ||
| 4 | <!-- bootstrap --> | 6 | <!-- 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"> | ||
| 6 | -<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> | 7 | +<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"> |
| 8 | +<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script> | ||
| 9 | +<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous"> --> | ||
| 10 | +<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script> --> | ||
| 11 | +<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> --> | ||
| 12 | +<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> --> | ||
| 7 | 13 | ||
| 8 | <!-- bootstrap icons --> | 14 | <!-- bootstrap icons --> |
| 9 | -<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> | 15 | +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css"> |
| 16 | +<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"> --> | ||
| 17 | +<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"> --> | ||
| 10 | 18 | ||
| 11 | <!-- MathJax --> | 19 | <!-- MathJax --> |
| 12 | <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> | 20 | <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> | 21 | <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> |
| 14 | 22 | ||
| 15 | <!-- codemirror --> | 23 | <!-- codemirror --> |
| 16 | -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||
| 17 | -<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.js" integrity="sha512-rdFIN28+neM8H8zNsjRClhJb1fIYby2YCNmoqwnqBDEvZgpcp7MJiX8Wd+Oi6KcJOMOuvGztjrsI59rly9BsVQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | 24 | +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> |
| 25 | +<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js" integrity="sha512-sSWQXoxIkE0G4/xqLngx5C53oOZCgFRxWE79CvMX2X0IKx14W3j9Dpz/2MpRh58xb2W/h+Y4WAHJQA0qMMuxJg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | ||
| 26 | +<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> --> | ||
| 27 | +<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.11/codemirror.min.js" integrity="sha512-rdFIN28+neM8H8zNsjRClhJb1fIYby2YCNmoqwnqBDEvZgpcp7MJiX8Wd+Oi6KcJOMOuvGztjrsI59rly9BsVQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> --> | ||
| 18 | 28 | ||
| 19 | <!-- animate --> | 29 | <!-- animate --> |
| 20 | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" integrity="sha512-c42qTSw/wPZ3/5LBzD+Bw5f7bSF2oxou6wEb+I/lqeaKV5FDIfMvvRp772y4jcJLKuGUOpbJMdg/BTl50fJYAw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | 30 | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" integrity="sha512-c42qTSw/wPZ3/5LBzD+Bw5f7bSF2oxou6wEb+I/lqeaKV5FDIfMvvRp772y4jcJLKuGUOpbJMdg/BTl50fJYAw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> |
aprendizations/templates/topic.html
| @@ -5,6 +5,7 @@ | @@ -5,6 +5,7 @@ | ||
| 5 | {% include include-libs.html %} | 5 | {% include include-libs.html %} |
| 6 | 6 | ||
| 7 | <!-- local --> | 7 | <!-- local --> |
| 8 | + <link rel="icon" href="{{static_url('favicon.ico')}}" /> | ||
| 8 | <link rel="stylesheet" href="{{static_url('css/github.css')}}" /> | 9 | <link rel="stylesheet" href="{{static_url('css/github.css')}}" /> |
| 9 | <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> | 10 | <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> |
| 10 | <script defer src="{{static_url('js/topic.js')}}"></script> | 11 | <script defer src="{{static_url('js/topic.js')}}"></script> |
aprendizations/tools.py
| 1 | 1 | ||
| 2 | # python standard library | 2 | # python standard library |
| 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 |
| 9 | 9 | ||
| 10 | # third party libraries | 10 | # third party libraries |
| 11 | # import mistune | 11 | # import mistune |
| 12 | # from pygments import highlight | 12 | # from pygments import highlight |
| 13 | # from pygments.lexers import get_lexer_by_name | 13 | # from pygments.lexers import get_lexer_by_name |
| 14 | # from pygments.formatters import HtmlFormatter | 14 | # from pygments.formatters import HtmlFormatter |
| 15 | -import yaml | ||
| 16 | 15 | ||
| 17 | 16 | ||
| 18 | # setup logger for this module | 17 | # setup logger for this module |
| 19 | logger = logging.getLogger(__name__) | 18 | logger = logging.getLogger(__name__) |
| 20 | 19 | ||
| 21 | 20 | ||
| 22 | -# ------------------------------------------------------------------------- | ||
| 23 | -# Block math: | ||
| 24 | -# $$x$$ or \begin{equation}x\end{equation} | ||
| 25 | -# ------------------------------------------------------------------------- | 21 | +# # ------------------------------------------------------------------------- |
| 22 | +# # Block math: | ||
| 23 | +# # $$x$$ or \begin{equation}x\end{equation} | ||
| 24 | +# # ------------------------------------------------------------------------- | ||
| 26 | # class MathBlockGrammar(mistune.BlockGrammar): | 25 | # class MathBlockGrammar(mistune.BlockGrammar): |
| 27 | # block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) | 26 | # block_math = re.compile(r'^\$\$(.*?)\$\$', re.DOTALL) |
| 28 | # latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', | 27 | # latex_environment = re.compile(r'^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}', |
| @@ -53,7 +52,6 @@ logger = logging.getLogger(__name__) | @@ -53,7 +52,6 @@ logger = logging.getLogger(__name__) | ||
| 53 | # 'text': m.group(2) | 52 | # 'text': m.group(2) |
| 54 | # }) | 53 | # }) |
| 55 | # | 54 | # |
| 56 | -# | ||
| 57 | # # ------------------------------------------------------------------------- | 55 | # # ------------------------------------------------------------------------- |
| 58 | # # Inline math: $x$ | 56 | # # Inline math: $x$ |
| 59 | # # ------------------------------------------------------------------------- | 57 | # # ------------------------------------------------------------------------- |
| @@ -94,6 +92,7 @@ logger = logging.getLogger(__name__) | @@ -94,6 +92,7 @@ logger = logging.getLogger(__name__) | ||
| 94 | # self.token['text']) | 92 | # self.token['text']) |
| 95 | # | 93 | # |
| 96 | # | 94 | # |
| 95 | +# | ||
| 97 | # class HighlightRenderer(mistune.Renderer): | 96 | # class HighlightRenderer(mistune.Renderer): |
| 98 | # def block_code(self, code, lang='text'): | 97 | # def block_code(self, code, lang='text'): |
| 99 | # try: | 98 | # try: |
| @@ -136,118 +135,32 @@ logger = logging.getLogger(__name__) | @@ -136,118 +135,32 @@ logger = logging.getLogger(__name__) | ||
| 136 | # else: | 135 | # else: |
| 137 | # return md | 136 | # return md |
| 138 | # | 137 | # |
| 138 | +# | ||
| 139 | 139 | ||
| 140 | -# --------------------------------------------------------------------------- | ||
| 141 | -# load data from yaml file | ||
| 142 | -# --------------------------------------------------------------------------- | ||
| 143 | -def load_yaml(filename: str, default: Any = None) -> Any: | ||
| 144 | - filename = path.expanduser(filename) | ||
| 145 | - try: | ||
| 146 | - f = open(filename, 'r', encoding='utf-8') | ||
| 147 | - except Exception as e: | ||
| 148 | - logger.error(e) | ||
| 149 | - if default is not None: | ||
| 150 | - return default | ||
| 151 | - else: | ||
| 152 | - raise | ||
| 153 | - | ||
| 154 | - with f: | ||
| 155 | - try: | ||
| 156 | - return yaml.safe_load(f) | ||
| 157 | - except yaml.YAMLError as e: | ||
| 158 | - logger.error(str(e).replace('\n', ' ')) | ||
| 159 | - if default is not None: | ||
| 160 | - return default | ||
| 161 | - else: | ||
| 162 | - raise | ||
| 163 | 140 | ||
| 164 | 141 | ||
| 165 | # --------------------------------------------------------------------------- | 142 | # --------------------------------------------------------------------------- |
| 166 | -# Runs a script and returns its stdout parsed as yaml, or None on error. | ||
| 167 | -# The script is run in another process but this function blocks waiting | ||
| 168 | -# for its termination. | ||
| 169 | -# --------------------------------------------------------------------------- | ||
| 170 | -# def run_script(script: str, | ||
| 171 | -# args: List[str] = [], | ||
| 172 | -# stdin: str = '', | ||
| 173 | -# timeout: int = 2) -> Any: | ||
| 174 | -# | ||
| 175 | -# script = path.expanduser(script) | 143 | +# TODO: unused, to be removed |
| 144 | +# def load_yaml(filename: str, default: Any = None) -> Any: | ||
| 145 | +# ''' | ||
| 146 | +# Load data from yaml file | ||
| 147 | +# ''' | ||
| 148 | +# filename = path.expanduser(filename) | ||
| 176 | # try: | 149 | # try: |
| 177 | -# cmd = [script] + [str(a) for a in args] | ||
| 178 | -# p = subprocess.run(cmd, | ||
| 179 | -# input=stdin, | ||
| 180 | -# stdout=subprocess.PIPE, | ||
| 181 | -# stderr=subprocess.STDOUT, | ||
| 182 | -# text=True, # same as universal_newlines=True | ||
| 183 | -# timeout=timeout, | ||
| 184 | -# ) | ||
| 185 | -# except FileNotFoundError: | ||
| 186 | -# logger.error(f'Can not execute script "{script}": not found.') | ||
| 187 | -# except PermissionError: | ||
| 188 | -# logger.error(f'Can not execute script "{script}": wrong permissions.') | ||
| 189 | -# except OSError: | ||
| 190 | -# logger.error(f'Can not execute script "{script}": unknown reason.') | ||
| 191 | -# except subprocess.TimeoutExpired: | ||
| 192 | -# logger.error(f'Timeout {timeout}s exceeded while running "{script}".') | ||
| 193 | -# except Exception: | ||
| 194 | -# logger.error(f'An Exception ocurred running {script}.') | ||
| 195 | -# else: | ||
| 196 | -# if p.returncode != 0: | ||
| 197 | -# logger.error(f'Return code {p.returncode} running "{script}".') | 150 | +# f = open(filename, 'r', encoding='utf-8') |
| 151 | +# except Exception as e: | ||
| 152 | +# logger.error(e) | ||
| 153 | +# if default is not None: | ||
| 154 | +# return default | ||
| 198 | # else: | 155 | # else: |
| 199 | -# try: | ||
| 200 | -# output = yaml.safe_load(p.stdout) | ||
| 201 | -# except Exception: | ||
| 202 | -# logger.error(f'Error parsing yaml output of "{script}"') | ||
| 203 | -# else: | ||
| 204 | -# return output | ||
| 205 | -# | ||
| 206 | - | ||
| 207 | -# def run_script(script: str, | ||
| 208 | -# args: List[str] = [], | ||
| 209 | -# stdin: str = '', | ||
| 210 | -# timeout: int = 2) -> Any: | ||
| 211 | -# asyncio.run(run_script_async(script, args, stdin, timeout)) | ||
| 212 | -# ---------------------------------------------------------------------------- | ||
| 213 | -# Same as above, but asynchronous | ||
| 214 | -# ---------------------------------------------------------------------------- | ||
| 215 | -# async def run_script(script: str, | ||
| 216 | -# args: List[str] = [], | ||
| 217 | -# stdin: str = '', | ||
| 218 | -# timeout: int = 5) -> Any: | ||
| 219 | -# | ||
| 220 | -# # normalize args | ||
| 221 | -# script = path.expanduser(script) | ||
| 222 | -# input_bytes = stdin.encode('utf-8') | ||
| 223 | -# args = [str(a) for a in args] | 156 | +# raise |
| 224 | # | 157 | # |
| 225 | -# try: | ||
| 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, | ||
| 231 | -# ) | ||
| 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.') | ||
| 238 | -# else: | 158 | +# with f: |
| 239 | # try: | 159 | # try: |
| 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}".') | ||
| 247 | -# else: | ||
| 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}"') | 160 | +# return yaml.safe_load(f) |
| 161 | +# except yaml.YAMLError as e: | ||
| 162 | +# logger.error(str(e).replace('\n', ' ')) | ||
| 163 | +# if default is not None: | ||
| 164 | +# return default | ||
| 252 | # else: | 165 | # else: |
| 253 | -# return output | 166 | +# raise |
demo/astronomy.yaml
| @@ -17,8 +17,14 @@ | @@ -17,8 +17,14 @@ | ||
| 17 | # ---------------------------------------------------------------------------- | 17 | # ---------------------------------------------------------------------------- |
| 18 | 18 | ||
| 19 | topics: | 19 | topics: |
| 20 | - astronomy/solar-system: | ||
| 21 | - name: Sistema solar | 20 | + astronomy/solar-system/planets: |
| 21 | + name: Planets | ||
| 22 | + | ||
| 23 | + astronomy/solar-system/natural_satellites: | ||
| 24 | + name: Natural satellites | ||
| 25 | + deps: | ||
| 26 | + - astronomy/solar-system/planets | ||
| 27 | + | ||
| 22 | 28 | ||
| 23 | # astronomy/milky-way: | 29 | # astronomy/milky-way: |
| 24 | # name: Via Láctea | 30 | # name: Via Láctea |
demo/astronomy/solar-system/correct-first_3_planets.py
| @@ -1,28 +0,0 @@ | @@ -1,28 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | - | ||
| 3 | -import re | ||
| 4 | -import sys | ||
| 5 | -import time | ||
| 6 | - | ||
| 7 | -s = sys.stdin.read() | ||
| 8 | - | ||
| 9 | -ans = set(re.findall(r'[\w]+', s.lower())) # convert answer to lowercase | ||
| 10 | -ans.difference_update({'e', 'a', 'o', 'planeta', 'planetas'}) # ignore words | ||
| 11 | - | ||
| 12 | -# correct set of planets | ||
| 13 | -planets = {'mercúrio', 'vénus', 'terra'} | ||
| 14 | - | ||
| 15 | -correct = set.intersection(ans, planets) # the ones I got right | ||
| 16 | -wrong = set.difference(ans, planets) # the ones I got wrong | ||
| 17 | - | ||
| 18 | -grade = (len(correct) - len(wrong)) / len(planets) | ||
| 19 | - | ||
| 20 | -comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | ||
| 21 | - | ||
| 22 | -out = f'''--- | ||
| 23 | -grade: {grade} | ||
| 24 | -comments: {comments}''' | ||
| 25 | - | ||
| 26 | -time.sleep(2) # simulate computation time (may generate timeout) | ||
| 27 | - | ||
| 28 | -print(out) |
demo/astronomy/solar-system/public/earth.jpg
1.02 MB
demo/astronomy/solar-system/public/jupiter.gif
2.96 MB
demo/astronomy/solar-system/public/planets.png
419 KB
demo/astronomy/solar-system/public/saturn.jpg
117 KB
demo/astronomy/solar-system/questions.yaml
| @@ -1,67 +0,0 @@ | @@ -1,67 +0,0 @@ | ||
| 1 | ---- | ||
| 2 | -# ---------------------------------------------------------------------------- | ||
| 3 | -- type: text | ||
| 4 | - ref: planet-earth | ||
| 5 | - title: Sistema solar | ||
| 6 | - text: O nosso planeta chama-se planeta... | ||
| 7 | - correct: ['Terra', 'terra'] | ||
| 8 | - # opcional | ||
| 9 | - answer: dos macacos? | ||
| 10 | - solution: | | ||
| 11 | - O nosso planeta é o planeta **Terra**. | ||
| 12 | - | ||
| 13 | -  | ||
| 14 | - | ||
| 15 | -# ---------------------------------------------------------------------------- | ||
| 16 | -- type: radio | ||
| 17 | - ref: largest-planet | ||
| 18 | - title: Sistema solar | ||
| 19 | - text: | | ||
| 20 | -  | ||
| 21 | - | ||
| 22 | - Qual é o maior planeta do Sistema Solar? | ||
| 23 | - options: | ||
| 24 | - - Mercúrio | ||
| 25 | - - Marte | ||
| 26 | - - Júpiter | ||
| 27 | - - Saturno | ||
| 28 | - - Têm todos o mesmo tamanho | ||
| 29 | - # opcional | ||
| 30 | - correct: 2 | ||
| 31 | - # discount: true | ||
| 32 | - shuffle: false | ||
| 33 | - solution: | | ||
| 34 | - O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | ||
| 35 | - ainda assim 2.5 vezes maior que a massa de todos os outros planetas juntos. | ||
| 36 | - É um gigante gasoso maioritariamente composto por hidrogénio. | ||
| 37 | - | ||
| 38 | -  | ||
| 39 | - | ||
| 40 | -# ---------------------------------------------------------------------------- | ||
| 41 | -- type: text-regex | ||
| 42 | - ref: saturn | ||
| 43 | - title: Sistema solar | ||
| 44 | - text: O planeta do sistema solar conhecido pelos seus aneis é o planeta... | ||
| 45 | - correct: '[Ss]aturno' | ||
| 46 | - solution: | | ||
| 47 | - O planeta Saturno é famoso pelos seus anéis. | ||
| 48 | - É o segundo maior planeta do Sistema Solar. | ||
| 49 | - Tal como Júpiter, é um gigante gasoso. | ||
| 50 | - Os seus anéis são formados por partículas de gelo. | ||
| 51 | - | ||
| 52 | -  | ||
| 53 | - | ||
| 54 | -# ---------------------------------------------------------------------------- | ||
| 55 | -- type: textarea | ||
| 56 | - ref: first_3_planets | ||
| 57 | - title: Sistema solar | ||
| 58 | - text: | | ||
| 59 | - Qual o nome dos três planetas mais próximos do Sol? | ||
| 60 | - Exemplo `Ceres, Krypton e Vulcano` | ||
| 61 | - correct: correct-first_3_planets.py | ||
| 62 | - # correct: correct-timeout.py | ||
| 63 | - # opcional | ||
| 64 | - answer: Ceres, Krypton e Vulcano | ||
| 65 | - timeout: 3 | ||
| 66 | - solution: | | ||
| 67 | - Os 3 planetas mais perto do Sol são Mercúrio, Vénus e Terra. |
demo/courses.yaml
| @@ -11,19 +11,20 @@ topics_from: | @@ -11,19 +11,20 @@ topics_from: | ||
| 11 | # ---------------------------------------------------------------------------- | 11 | # ---------------------------------------------------------------------------- |
| 12 | courses: | 12 | courses: |
| 13 | math: | 13 | math: |
| 14 | - title: Matemática | 14 | + title: Elementary math |
| 15 | description: | | 15 | description: | |
| 16 | - Adição, multiplicação e números primos. | 16 | + Arithmetic operations: add, subtract, mulitply and divide numbers |
| 17 | goals: | 17 | goals: |
| 18 | - - math | ||
| 19 | - math/addition | 18 | - math/addition |
| 19 | + - math/subtraction | ||
| 20 | - math/multiplication | 20 | - math/multiplication |
| 21 | - - math/prime-numbers | 21 | + - math/division |
| 22 | 22 | ||
| 23 | astronomy: | 23 | astronomy: |
| 24 | - title: Astronomia | 24 | + title: Astronomy |
| 25 | description: | | 25 | description: | |
| 26 | - Sistema Solar e Via Láctea. | 26 | + This course is about the planets and natural satellites in our Solar |
| 27 | + System. | ||
| 27 | goals: | 28 | goals: |
| 28 | - - astronomy/solar-system | ||
| 29 | - # - astronomy/milky-way | 29 | + - astronomy/solar-system/planets |
| 30 | + - astronomy/solar-system/natural_satellites |
demo/math.yaml
| @@ -18,30 +18,20 @@ | @@ -18,30 +18,20 @@ | ||
| 18 | 18 | ||
| 19 | topics: | 19 | topics: |
| 20 | math/addition: | 20 | math/addition: |
| 21 | - name: Adição | 21 | + name: Addition |
| 22 | 22 | ||
| 23 | - math/multiplication: | ||
| 24 | - name: Multiplicação | 23 | + math/subtraction: |
| 24 | + name: Subtraction | ||
| 25 | deps: | 25 | deps: |
| 26 | - math/addition | 26 | - math/addition |
| 27 | 27 | ||
| 28 | - math/learn-prime-numbers: | ||
| 29 | - name: O que são números primos? | ||
| 30 | - type: learn | ||
| 31 | - file: learn.yaml | ||
| 32 | - deps: | ||
| 33 | - - math/multiplication | ||
| 34 | - | ||
| 35 | - math/prime-numbers: | ||
| 36 | - name: Números primos | 28 | + math/multiplication: |
| 29 | + name: Multiplication | ||
| 37 | deps: | 30 | deps: |
| 38 | - - math/learn-prime-numbers | 31 | + - math/addition |
| 39 | 32 | ||
| 40 | - math: | ||
| 41 | - name: Números e operações aritméticas | ||
| 42 | - type: chapter | 33 | + math/division: |
| 34 | + name: Division | ||
| 43 | deps: | 35 | deps: |
| 44 | - - math/addition | 36 | + - math/subtraction |
| 45 | - math/multiplication | 37 | - math/multiplication |
| 46 | - - math/learn-prime-numbers | ||
| 47 | - - math/prime-numbers |
| @@ -0,0 +1,34 @@ | @@ -0,0 +1,34 @@ | ||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | + | ||
| 3 | +''' | ||
| 4 | +This question generator expects two integer arguments. | ||
| 5 | +These are the range for the number of coins in each pocket. | ||
| 6 | +''' | ||
| 7 | + | ||
| 8 | +import random | ||
| 9 | +import sys | ||
| 10 | + | ||
| 11 | +a = int(sys.argv[1]) | ||
| 12 | +b = int(sys.argv[2]) | ||
| 13 | + | ||
| 14 | +x, y = random.sample(range(a, b), k=2) | ||
| 15 | +r = x + y | ||
| 16 | + | ||
| 17 | +pocket1, pocket2 = random.sample(['left pocket', 'right pocket', 'wallet', | ||
| 18 | + 'safe at home'], k=2) | ||
| 19 | + | ||
| 20 | +currency = random.choice(['Euros', 'US dollars', 'British pounds']) | ||
| 21 | + | ||
| 22 | +print(f'''--- | ||
| 23 | +type: text | ||
| 24 | +title: Adding two numbers | ||
| 25 | +text: | | ||
| 26 | + Suppose you have {x} {currency} in your {pocket1} and {y} in your | ||
| 27 | + {pocket2}. | ||
| 28 | + How many {currency} do you have? | ||
| 29 | + | ||
| 30 | + Just answer the number, for example `42`. | ||
| 31 | +transform: ['trim'] | ||
| 32 | +correct: ['{r}'] | ||
| 33 | +solution: | | ||
| 34 | + You have a total of {r} {currency}.''') |
demo/math/addition/questions.yaml
| 1 | --- | 1 | --- |
| 2 | # --------------------------------------------------------------------------- | 2 | # --------------------------------------------------------------------------- |
| 3 | - type: generator | 3 | - type: generator |
| 4 | - ref: addition-two-digits | ||
| 5 | - script: addition-two-digits.py | ||
| 6 | - args: [10, 20] | ||
| 7 | - | ||
| 8 | -# --------------------------------------------------------------------------- | ||
| 9 | -- type: checkbox | ||
| 10 | - ref: addition-properties | ||
| 11 | - title: Propriedades da adição | ||
| 12 | - text: Indique quais as propriedades que a adição satisfaz. | ||
| 13 | - options: | ||
| 14 | - # right | ||
| 15 | - - Existência de elemento neutro, $x+0=x$. | ||
| 16 | - - Existência de inverso aditivo (simétrico), $x+(-x)=0$. | ||
| 17 | - - Propriedade associativa, $(x+y)+z = x+(y+z)$. | ||
| 18 | - - Propriedade comutativa, $x+y=y+x$. | ||
| 19 | - # wrong | ||
| 20 | - - Existência de elemento absorvente, $x+1=1$. | ||
| 21 | - correct: [1, 1, 1, 1, 0] | ||
| 22 | - solution: | | ||
| 23 | - A adição não tem elemento absorvente. | 4 | + ref: addition |
| 5 | + script: addition.py | ||
| 6 | + args: [2, 20] |
demo/math/multiplication/multiplication-table.py
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | 2 | ||
| 3 | +import json | ||
| 3 | import random | 4 | import random |
| 4 | 5 | ||
| 5 | x, y = random.sample(range(2,10), k=2) | 6 | x, y = random.sample(range(2,10), k=2) |
| @@ -8,15 +9,38 @@ r = x * y | @@ -8,15 +9,38 @@ r = x * y | ||
| 8 | yy = '+'.join([str(y)]*x) | 9 | yy = '+'.join([str(y)]*x) |
| 9 | xx = '+'.join([str(x)]*y) | 10 | xx = '+'.join([str(x)]*y) |
| 10 | 11 | ||
| 11 | -print(f'''--- | ||
| 12 | -type: text | ||
| 13 | -title: Multiplicação (tabuada) | ||
| 14 | -text: | | ||
| 15 | - Qual o resultado da multiplicação ${x}\\times {y}$? | ||
| 16 | -transform: ['trim'] | ||
| 17 | -correct: ['{r}'] | ||
| 18 | -solution: | | ||
| 19 | - A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: | ||
| 20 | - $$ {x}\\times {y} = {yy} = {r} $$ | ||
| 21 | - ou | ||
| 22 | - $$ {y}\\times {x} = {xx} = {r}. $$''') | 12 | +xbullets = x * '*' # ⏺ |
| 13 | +xxbullets = '\n'.join(y * [xbullets]) | ||
| 14 | + | ||
| 15 | +ybullets = y * '*' | ||
| 16 | +yybullets = '\n'.join(x * [ybullets]) | ||
| 17 | + | ||
| 18 | +solution = f''' | ||
| 19 | +Multiplying is essentially repeating the addition multiple times. | ||
| 20 | +We can do it in two ways: | ||
| 21 | + | ||
| 22 | +$$ | ||
| 23 | +{x}\\times {y} = {yy} = {r} | ||
| 24 | +$$ | ||
| 25 | +or | ||
| 26 | +$$ | ||
| 27 | +{y}\\times {x} = {xx} = {r}. | ||
| 28 | +$$ | ||
| 29 | + | ||
| 30 | +Multiplication can also be seen as the number of dots organised in rectangular | ||
| 31 | +shape: | ||
| 32 | + | ||
| 33 | +{xxbullets} | ||
| 34 | + | ||
| 35 | +{yybullets} | ||
| 36 | +''' | ||
| 37 | + | ||
| 38 | +question = { | ||
| 39 | + 'type': 'text', | ||
| 40 | + 'title': 'Multiplication (table)', | ||
| 41 | + 'text': f'What is the result of ${x}\\times {y}$?', | ||
| 42 | + 'transform': ['trim'], | ||
| 43 | + 'correct': [f'{r}'], | ||
| 44 | + 'solution': solution, | ||
| 45 | +} | ||
| 46 | +print(json.dumps(question)) |
demo/math/multiplication/questions.yaml
| 1 | --- | 1 | --- |
| 2 | # ---------------------------------------------------------------------------- | 2 | # ---------------------------------------------------------------------------- |
| 3 | -- type: generator | ||
| 4 | - ref: multiplication-table | ||
| 5 | - script: multiplication-table.py | 3 | +# - type: generator |
| 4 | +# ref: multiplication-table | ||
| 5 | +# script: multiplication-table.py | ||
| 6 | 6 | ||
| 7 | # ---------------------------------------------------------------------------- | 7 | # ---------------------------------------------------------------------------- |
| 8 | - type: checkbox | 8 | - type: checkbox |
| 9 | - ref: multiplication-properties | ||
| 10 | - title: Propriedades da multiplicação | ||
| 11 | - text: Indique quais as propriedades que a multiplicação satisfaz. | 9 | + ref: multiplication |
| 10 | + title: Multiplication | ||
| 11 | + text: | | ||
| 12 | + Which of the following multiplications have the result $12$? | ||
| 12 | options: | 13 | options: |
| 13 | - # right | ||
| 14 | - - Existência de elemento neutro, $1x=x$. | ||
| 15 | - - Propriedade associativa, $(xy)z = x(yz)$. | ||
| 16 | - - Propriedade comutativa, $xy=yx$. | ||
| 17 | - - Existência de elemento absorvente, $0x=0$. | ||
| 18 | - # wrong | ||
| 19 | - - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que | ||
| 20 | - $x(1/x)=1$. | ||
| 21 | - correct: [1, 1, 1, 1, 0] | 14 | + - $6\times 2$ |
| 15 | + - $2\times 6$ | ||
| 16 | + - $3\times 4$ | ||
| 17 | + - $12\times 0$ | ||
| 18 | + correct: [1, 1, 1, 0] | ||
| 22 | solution: | | 19 | solution: | |
| 23 | - Na multiplicação nem todos os números têm inverso. Só têm inverso os números | ||
| 24 | - diferentes de zero. | ||
| 25 | - As outras propriedades são satisfeitas para todos os números. | 20 | + Multiplying by zero is always zero. |
| 21 | + The others are correct. | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +# # ---------------------------------------------------------------------------- | ||
| 25 | +# - type: checkbox | ||
| 26 | +# ref: multiplication-properties | ||
| 27 | +# title: Multiplication Properties | ||
| 28 | +# text: Which properties does the multiplication have? | ||
| 29 | +# options: | ||
| 30 | +# # right | ||
| 31 | +# - 'Existence of an identity element: $1 x = x$.' | ||
| 32 | +# - 'Associative property: $(x y) z = x (y z)$.' | ||
| 33 | +# - 'Commutative property: $x y = y x$.' | ||
| 34 | +# - 'Existence of an absorbing element: $0 x = 0$.' | ||
| 35 | +# # wrong | ||
| 36 | +# - 'Inverse property: all numbers have an inverse $1/x$ such that | ||
| 37 | +# $x(1/x)=1$.' | ||
| 38 | +# correct: [1, 1, 1, 1, 0] | ||
| 39 | +# solution: | | ||
| 40 | +# Not all numbers have an inverse: *zero* does not have an inverse. | ||
| 41 | +# The other properties are true. |
demo/math/questions.yaml
mypy.ini
| 1 | -; [mypy] | ||
| 2 | -; python_version = 3.10 | 1 | +[mypy] |
| 2 | +python_version = 3.11 | ||
| 3 | ; plugins = sqlalchemy.ext.mypy.plugin | 3 | ; plugins = sqlalchemy.ext.mypy.plugin |
| 4 | 4 | ||
| 5 | -; [mypy-pygments.*] | ||
| 6 | -; ignore_missing_imports = True | 5 | +[mypy-networkx.*] |
| 6 | +ignore_missing_imports = True | ||
| 7 | 7 | ||
| 8 | -; [mypy-networkx.*] | ||
| 9 | -; ignore_missing_imports = True | ||
| 10 | - | ||
| 11 | -; [mypy-bcrypt.*] | ||
| 12 | -; ignore_missing_imports = True | ||
| 13 | - | ||
| 14 | -; [mypy-mistune.*] | ||
| 15 | -; ignore_missing_imports = True | 8 | +[mypy-mistune.*] |
| 9 | +ignore_missing_imports = True |
setup.py
| @@ -7,36 +7,36 @@ with open("README.md", "r") as f: | @@ -7,36 +7,36 @@ with open("README.md", "r") as f: | ||
| 7 | long_description = f.read() | 7 | long_description = f.read() |
| 8 | 8 | ||
| 9 | setup( | 9 | setup( |
| 10 | - name=APP_NAME, | ||
| 11 | - version=APP_VERSION, | ||
| 12 | - author=__author__, | ||
| 13 | - author_email="mjsb@uevora.pt", | ||
| 14 | - license=__license__, | ||
| 15 | - description=APP_DESCRIPTION.split('\n')[0], | ||
| 16 | - long_description=APP_DESCRIPTION, | ||
| 17 | - long_description_content_type="text/markdown", | ||
| 18 | - url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", | ||
| 19 | - packages=find_packages(), | ||
| 20 | - include_package_data=True, # install files from MANIFEST.in | ||
| 21 | - python_requires='>=3.9', | ||
| 22 | - install_requires=[ | ||
| 23 | - 'tornado>=6.2', | ||
| 24 | - 'mistune>=3.0.0rc4', | 10 | + name = APP_NAME, |
| 11 | + version = APP_VERSION, | ||
| 12 | + author = __author__, | ||
| 13 | + author_email = "mjsb@uevora.pt", | ||
| 14 | + license = __license__, | ||
| 15 | + description = APP_DESCRIPTION.split('\n')[0], | ||
| 16 | + long_description = APP_DESCRIPTION, | ||
| 17 | + long_description_content_type = "text/markdown", | ||
| 18 | + url = "https://git.xdi.uevora.pt/mjsb/aprendizations.git", | ||
| 19 | + packages = find_packages(), | ||
| 20 | + include_package_data = True, # install files from MANIFEST.in | ||
| 21 | + python_requires = '>=3.11.1', | ||
| 22 | + install_requires = [ | ||
| 23 | + 'tornado>=6.4', | ||
| 24 | + 'mistune>=3.1', | ||
| 25 | 'pyyaml>=6.0', | 25 | 'pyyaml>=6.0', |
| 26 | - 'pygments>=2.14', | ||
| 27 | - 'sqlalchemy>=2.0.0', | ||
| 28 | - 'bcrypt>=4.0.1', | ||
| 29 | - 'networkx>=3.0', | 26 | + 'pygments>=2.19', |
| 27 | + 'sqlalchemy>=2.0.37', | ||
| 28 | + 'bcrypt>=4.2.1', | ||
| 29 | + 'networkx>=3.4.2' | ||
| 30 | 'pandas>=2.3', | 30 | 'pandas>=2.3', |
| 31 | 'openpyxl' | 31 | 'openpyxl' |
| 32 | ], | 32 | ], |
| 33 | - entry_points={ | 33 | + entry_points = { |
| 34 | 'console_scripts': [ | 34 | 'console_scripts': [ |
| 35 | 'aprendizations = aprendizations.main:main', | 35 | 'aprendizations = aprendizations.main:main', |
| 36 | 'initdb-aprendizations = aprendizations.initdb:main', | 36 | 'initdb-aprendizations = aprendizations.initdb:main', |
| 37 | ] | 37 | ] |
| 38 | }, | 38 | }, |
| 39 | - classifiers=[ | 39 | + classifiers = [ |
| 40 | 'Development Status :: 4 - Beta', | 40 | 'Development Status :: 4 - Beta', |
| 41 | 'Environment :: Console', | 41 | 'Environment :: Console', |
| 42 | 'Intended Audience :: Education', | 42 | 'Intended Audience :: Education', |