Compare View

switch
from
...
to
 
Commits (4)
@@ -9,4 +9,4 @@ node_modules/ @@ -9,4 +9,4 @@ node_modules/
9 demo/.DS_Store 9 demo/.DS_Store
10 demo/solar_system/.DS_Store 10 demo/solar_system/.DS_Store
11 .venv/ 11 .venv/
12 -build 12 +build/
@@ -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(*_) -&gt; None: @@ -391,7 +389,7 @@ def signal_handler(*_) -&gt; 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) -&gt; None: @@ -403,23 +401,25 @@ async def webserver(app, ssl, port: int = 8443, debug: bool = False) -&gt; 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 - ![Terra](earth.jpg)  
14 -  
15 -# ----------------------------------------------------------------------------  
16 -- type: radio  
17 - ref: largest-planet  
18 - title: Sistema solar  
19 - text: |  
20 - ![planetas](planets.png "Planetas do Sistema Solar")  
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 - ![Júpiter](jupiter.gif)  
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 - ![Saturno](saturn.jpg)  
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  
demo/math/addition/addition.py 0 → 100755
@@ -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
1 - type: info 1 - type: info
2 - title: Fim do capítulo 2 + title: End of chapter
  3 + text: |
  4 + You should now be able to add, subtract, multiply and divide whole numbers.
  5 + With these operations, prime numbers where also introduced.
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
@@ -7,36 +7,36 @@ with open(&quot;README.md&quot;, &quot;r&quot;) as f: @@ -7,36 +7,36 @@ with open(&quot;README.md&quot;, &quot;r&quot;) 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',