Commit 60be4ab62ff720dfc95572f99b6503a67610cf68

Authored by Miguel Barão
2 parents 2fa34906 dde72423
Exists in master and in 1 other branch dev

Merge branch 'dev'

@@ -26,6 +26,9 @@ @@ -26,6 +26,9 @@
26 26
27 ## TODO 27 ## TODO
28 28
  29 +- ordenação que faça sentido, organizada por chapters.
  30 +- chapters deviam ser automaticamente checkados...
  31 +- hints dos topicos fechados com as dependencias desse topico que ainda faltam
29 - alterar tabelas para incluir email de recuperacao de password (e outros 32 - alterar tabelas para incluir email de recuperacao de password (e outros
30 avisos) 33 avisos)
31 - shuffle das perguntas dentro de um topico 34 - shuffle das perguntas dentro de um topico
1 # Getting Started 1 # Getting Started
2 2
3 -Latest review: 2021-11-08 3 +Latest review: 2022-04-04
4 4
5 ## Installation 5 ## Installation
6 6
@@ -27,7 +27,7 @@ operating system default. @@ -27,7 +27,7 @@ operating system default.
27 ```sh 27 ```sh
28 sudo pkg install python3 py38-sqlite3 # FreeBSD 28 sudo pkg install python3 py38-sqlite3 # FreeBSD
29 sudo apt install python3 # Linux (Ubuntu) 29 sudo apt install python3 # Linux (Ubuntu)
30 -sudo port install python39 # MacOS 30 +sudo port install python310 # MacOS
31 ``` 31 ```
32 32
33 ### Install pip 33 ### Install pip
@@ -37,15 +37,18 @@ Install `pip` from the system package manager: @@ -37,15 +37,18 @@ Install `pip` from the system package manager:
37 ```sh 37 ```sh
38 sudo pkg install py38-pip # FreeBSD 38 sudo pkg install py38-pip # FreeBSD
39 sudo apt install python3-pip # Linux (Ubuntu) 39 sudo apt install python3-pip # Linux (Ubuntu)
40 -sudo port install py39-pip # MacOS 40 +sudo port install py310-pip # MacOS
41 ``` 41 ```
42 42
43 In the end you should be able to run `pip --version` and `python3 -c "import 43 In the end you should be able to run `pip --version` and `python3 -c "import
44 sqlite3"` without errors. 44 sqlite3"` without errors.
45 In some systems, `pip` can be named `pip3`, `pip3.8` or `pip-3.8`, etc. 45 In some systems, `pip` can be named `pip3`, `pip3.8` or `pip-3.8`, etc.
46 46
47 -Packages should **not** be installed system-wide. To install locally in the user  
48 -area, edit the configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or 47 +Packages should **not** be installed system-wide. Either install them to a
  48 +python virtual environment or in the user area.
  49 +
  50 +To install in the user area use `pip install --user some_package` or edit the
  51 +configuration file `~/.config/pip/pip.conf` (FreeBSD, Linux) or
49 `Library/Application Support/pip/pip.conf` (MacOS) and add the lines 52 `Library/Application Support/pip/pip.conf` (MacOS) and add the lines
50 53
51 ```ini 54 ```ini
@@ -62,7 +65,7 @@ pip install git+https://git.xdi.uevora.pt/mjsb/aprendizations.git @@ -62,7 +65,7 @@ pip install git+https://git.xdi.uevora.pt/mjsb/aprendizations.git
62 Python packages are usually installed in: 65 Python packages are usually installed in:
63 66
64 * `~/.local/lib/python3.8/site-packages/` in Linux/FreeBSD. 67 * `~/.local/lib/python3.8/site-packages/` in Linux/FreeBSD.
65 -* `~/Library/python/3.9/lib/python/site-packages/` in MacOS. 68 +* `~/Library/python/3.10/lib/python/site-packages/` in MacOS.
66 69
67 When aprendizations is installed with pip, all the dependencies are also 70 When aprendizations is installed with pip, all the dependencies are also
68 installed. 71 installed.
@@ -72,7 +75,7 @@ installed in @@ -72,7 +75,7 @@ installed in
72 75
73 ```sh 76 ```sh
74 ~/.local/bin # Linux/FreeBSD 77 ~/.local/bin # Linux/FreeBSD
75 -~/Library/Python/3.9/bin # MacOS 78 +~/Library/Python/3.10/bin # MacOS
76 ``` 79 ```
77 80
78 and can be run from the terminal: 81 and can be run from the terminal:
aprendizations/__init__.py
@@ -30,7 +30,7 @@ are progressively uncovered as the students progress. @@ -30,7 +30,7 @@ are progressively uncovered as the students progress.
30 ''' 30 '''
31 31
32 APP_NAME = 'aprendizations' 32 APP_NAME = 'aprendizations'
33 -APP_VERSION = '2021.11.dev1' 33 +APP_VERSION = '2022.08.dev1'
34 APP_DESCRIPTION = __doc__ 34 APP_DESCRIPTION = __doc__
35 35
36 __author__ = 'Miguel Barão' 36 __author__ = 'Miguel Barão'
aprendizations/initdb.py
@@ -84,25 +84,24 @@ def get_students_from_csv(filename): @@ -84,25 +84,24 @@ def get_students_from_csv(filename):
84 'skipinitialspace': True, 84 'skipinitialspace': True,
85 } 85 }
86 86
  87 + students = []
87 try: 88 try:
88 with open(filename, encoding='iso-8859-1') as file: 89 with open(filename, encoding='iso-8859-1') as file:
89 csvreader = csv.DictReader(file, **csv_settings) 90 csvreader = csv.DictReader(file, **csv_settings)
90 students = [{ 91 students = [{
91 'uid': s['N.º'], 92 'uid': s['N.º'],
92 'name': capwords(re.sub(r'\(.*\)', '', s['Nome']).strip()) 93 'name': capwords(re.sub(r'\(.*\)', '', s['Nome']).strip())
93 - } for s in csvreader] 94 + } for s in csvreader]
94 except OSError: 95 except OSError:
95 print(f'!!! Error reading file "{filename}" !!!') 96 print(f'!!! Error reading file "{filename}" !!!')
96 - students = []  
97 except csv.Error: 97 except csv.Error:
98 print(f'!!! Error parsing CSV from "{filename}" !!!') 98 print(f'!!! Error parsing CSV from "{filename}" !!!')
99 - students = []  
100 99
101 return students 100 return students
102 101
103 102
104 # =========================================================================== 103 # ===========================================================================
105 -def show_students_in_database(session, verbose=False): 104 +def show_students_in_database(session, verbose=False) -> None:
106 '''shows students in the database''' 105 '''shows students in the database'''
107 users = session.execute(select(Student)).scalars().all() 106 users = session.execute(select(Student)).scalars().all()
108 total = len(users) 107 total = len(users)
@@ -136,7 +135,7 @@ def main(): @@ -136,7 +135,7 @@ def main():
136 Base.metadata.create_all(engine) # Creates schema if needed 135 Base.metadata.create_all(engine) # Creates schema if needed
137 session = Session(engine, future=True) 136 session = Session(engine, future=True)
138 137
139 - # --- build list of students to insert/update 138 + # --- make list of students to insert/update
140 students = [] 139 students = []
141 140
142 for csvfile in args.csvfile: 141 for csvfile in args.csvfile:
@@ -145,24 +144,22 @@ def main(): @@ -145,24 +144,22 @@ def main():
145 if args.admin: 144 if args.admin:
146 students.append({'uid': '0', 'name': 'Admin'}) 145 students.append({'uid': '0', 'name': 'Admin'})
147 146
148 - if args.add: 147 + if args.add is not None:
149 for uid, name in args.add: 148 for uid, name in args.add:
150 students.append({'uid': uid, 'name': name}) 149 students.append({'uid': uid, 'name': name})
151 150
152 # --- only insert students that are not yet in the database 151 # --- only insert students that are not yet in the database
153 print('\nInserting new students:') 152 print('\nInserting new students:')
  153 + db_students = session.execute(select(Student.id)).scalars().all()
  154 + new_students = [s for s in students if s['uid'] not in set(db_students)]
  155 + for student in new_students:
  156 + print(f' {student["uid"]}, {student["name"]}')
154 157
155 - db_students = set(session.execute(select(Student.id)).scalars().all())  
156 - new_students = (s for s in students if s['uid'] not in db_students)  
157 - count = 0  
158 - for s in new_students:  
159 - print(f' {s["uid"]}, {s["name"]}')  
160 -  
161 - pw = args.pw or s['uid']  
162 - hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())  
163 -  
164 - session.add(Student(id=s['uid'], name=s['name'], password=hashed_pw))  
165 - count += 1 158 + passwd = args.pw or student['uid']
  159 + hashed_pw = bcrypt.hashpw(passwd.encode('utf-8'), bcrypt.gensalt())
  160 + session.add(Student(id=student['uid'],
  161 + name=student['name'],
  162 + password=hashed_pw))
166 163
167 try: 164 try:
168 session.commit() 165 session.commit()
@@ -170,23 +167,24 @@ def main(): @@ -170,23 +167,24 @@ def main():
170 print('!!! Integrity error. Aborted !!!\n') 167 print('!!! Integrity error. Aborted !!!\n')
171 session.rollback() 168 session.rollback()
172 else: 169 else:
173 - print(f'Total {count} new student(s).') 170 + print(f'Total {len(new_students)} new student(s).')
174 171
175 - # --- update data for student in the database 172 + # --- update data for students in the database
176 if args.update: 173 if args.update:
177 print('\nUpdating passwords of students:') 174 print('\nUpdating passwords of students:')
178 count = 0 175 count = 0
179 for sid in args.update: 176 for sid in args.update:
180 try: 177 try:
181 - s = session.execute(select(Student).filter_by(id=sid)).scalar_one() 178 + query = select(Student).filter_by(id=sid)
  179 + student = session.execute(query).scalar_one()
182 except NoResultFound: 180 except NoResultFound:
183 print(f' -> student {sid} does not exist!') 181 print(f' -> student {sid} does not exist!')
184 continue 182 continue
185 - else:  
186 - print(f' {sid}, {s.name}')  
187 - count += 1  
188 - pw = args.pw or sid  
189 - s.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) 183 + count += 1
  184 + print(f' {sid}, {student.name}')
  185 + passwd = (args.pw or sid).encode('utf-8')
  186 + student.password = bcrypt.hashpw(passwd, bcrypt.gensalt())
  187 +
190 session.commit() 188 session.commit()
191 print(f'Total {count} password(s) updated.') 189 print(f'Total {count} password(s) updated.')
192 190
aprendizations/learnapp.py
@@ -6,7 +6,6 @@ This is the main controller of the application. @@ -6,7 +6,6 @@ This is the main controller of the application.
6 # python standard library 6 # python standard library
7 import asyncio 7 import asyncio
8 from collections import defaultdict 8 from collections import defaultdict
9 -# from contextlib import contextmanager # `with` statement in db sessions  
10 from datetime import datetime 9 from datetime import datetime
11 import logging 10 import logging
12 from random import random 11 from random import random
@@ -45,26 +44,33 @@ class LearnApp(): @@ -45,26 +44,33 @@ class LearnApp():
45 ''' 44 '''
46 LearnApp - application logic 45 LearnApp - application logic
47 46
48 - self.deps - networkx topic dependencies  
49 - self.courses - dict {course_id: {'title': ...,  
50 - 'description': ...,  
51 - 'goals': ...,}, ...}  
52 - self.factory = dict {qref: QFactory()}  
53 - self.online - dict {student_id: {'number': ...,  
54 - 'name': ...,  
55 - 'state': StudentState(),  
56 - 'counter': ...}, ...} 47 + self.deps = networkx topic dependencies
  48 + self.courses = {
  49 + course_id: {
  50 + 'title': ...,
  51 + 'description': ...,
  52 + 'goals': ...,
  53 + }, ...
  54 + }
  55 + self.factory = { qref: QFactory() }
  56 + self.online = {
  57 + student_id: {
  58 + 'number': ...,
  59 + 'name': ...,
  60 + 'state': StudentState(),
  61 + 'counter': ...
  62 + }, ...
  63 + }
57 ''' 64 '''
58 65
59 - # ------------------------------------------------------------------------  
60 def __init__(self, 66 def __init__(self,
61 courses: str, # filename with course configurations 67 courses: str, # filename with course configurations
62 prefix: str, # path to topics 68 prefix: str, # path to topics
63 - db: str, # database filename 69 + dbase: str, # database filename
64 check: bool = False) -> None: 70 check: bool = False) -> None:
65 71
66 - self._db_setup(db) # setup database and check students  
67 - self.online: Dict[str, Dict] = dict() # online students 72 + self._db_setup(dbase) # setup database and check students
  73 + self.online: Dict[str, Dict] = {} # online students
68 74
69 try: 75 try:
70 config: Dict[str, Any] = load_yaml(courses) 76 config: Dict[str, Any] = load_yaml(courses)
@@ -85,7 +91,6 @@ class LearnApp(): @@ -85,7 +91,6 @@ class LearnApp():
85 # load other course files with the topics the their deps 91 # load other course files with the topics the their deps
86 for course_file in config.get('topics_from', []): 92 for course_file in config.get('topics_from', []):
87 course_conf = load_yaml(course_file) # course configuration 93 course_conf = load_yaml(course_file) # course configuration
88 - # FIXME set defaults??  
89 logger.info('%6d topics from %s', 94 logger.info('%6d topics from %s',
90 len(course_conf["topics"]), course_file) 95 len(course_conf["topics"]), course_file)
91 self._populate_graph(course_conf) 96 self._populate_graph(course_conf)
@@ -114,7 +119,6 @@ class LearnApp(): @@ -114,7 +119,6 @@ class LearnApp():
114 if check: 119 if check:
115 self._sanity_check_questions() 120 self._sanity_check_questions()
116 121
117 - # ------------------------------------------------------------------------  
118 def _sanity_check_questions(self) -> None: 122 def _sanity_check_questions(self) -> None:
119 ''' 123 '''
120 Unit tests for all questions 124 Unit tests for all questions
@@ -124,10 +128,10 @@ class LearnApp(): @@ -124,10 +128,10 @@ class LearnApp():
124 logger.info('Starting sanity checks (may take a while...)') 128 logger.info('Starting sanity checks (may take a while...)')
125 129
126 errors: int = 0 130 errors: int = 0
127 - for qref in self.factory: 131 + for qref, qfactory in self.factory.items():
128 logger.debug('checking %s...', qref) 132 logger.debug('checking %s...', qref)
129 try: 133 try:
130 - question = self.factory[qref].generate() 134 + question = qfactory.generate()
131 except QuestionException as exc: 135 except QuestionException as exc:
132 logger.error(exc) 136 logger.error(exc)
133 errors += 1 137 errors += 1
@@ -159,7 +163,6 @@ class LearnApp(): @@ -159,7 +163,6 @@ class LearnApp():
159 raise LearnException('Sanity checks') 163 raise LearnException('Sanity checks')
160 logger.info(' 0 errors found.') 164 logger.info(' 0 errors found.')
161 165
162 - # ------------------------------------------------------------------------  
163 async def login(self, uid: str, password: str) -> bool: 166 async def login(self, uid: str, password: str) -> bool:
164 '''user login''' 167 '''user login'''
165 168
@@ -212,13 +215,11 @@ class LearnApp(): @@ -212,13 +215,11 @@ class LearnApp():
212 215
213 return pw_ok 216 return pw_ok
214 217
215 - # ------------------------------------------------------------------------  
216 def logout(self, uid: str) -> None: 218 def logout(self, uid: str) -> None:
217 '''User logout''' 219 '''User logout'''
218 del self.online[uid] 220 del self.online[uid]
219 logger.info('User "%s" logged out', uid) 221 logger.info('User "%s" logged out', uid)
220 222
221 - # ------------------------------------------------------------------------  
222 async def change_password(self, uid: str, password: str) -> bool: 223 async def change_password(self, uid: str, password: str) -> bool:
223 ''' 224 '''
224 Change user Password. 225 Change user Password.
@@ -242,7 +243,6 @@ class LearnApp(): @@ -242,7 +243,6 @@ class LearnApp():
242 logger.info('User "%s" changed password', uid) 243 logger.info('User "%s" changed password', uid)
243 return True 244 return True
244 245
245 - # ------------------------------------------------------------------------  
246 async def check_answer(self, uid: str, answer) -> Question: 246 async def check_answer(self, uid: str, answer) -> Question:
247 ''' 247 '''
248 Checks answer and update database. 248 Checks answer and update database.
@@ -271,7 +271,6 @@ class LearnApp(): @@ -271,7 +271,6 @@ class LearnApp():
271 271
272 return question 272 return question
273 273
274 - # ------------------------------------------------------------------------  
275 async def get_question(self, uid: str) -> Optional[Question]: 274 async def get_question(self, uid: str) -> Optional[Question]:
276 ''' 275 '''
277 Get the question to show (current or new one) 276 Get the question to show (current or new one)
@@ -318,7 +317,6 @@ class LearnApp(): @@ -318,7 +317,6 @@ class LearnApp():
318 317
319 return question 318 return question
320 319
321 - # ------------------------------------------------------------------------  
322 def start_course(self, uid: str, course_id: str) -> None: 320 def start_course(self, uid: str, course_id: str) -> None:
323 '''Start course''' 321 '''Start course'''
324 322
@@ -331,16 +329,10 @@ class LearnApp(): @@ -331,16 +329,10 @@ class LearnApp():
331 else: 329 else:
332 logger.info('User "%s" course "%s"', uid, course_id) 330 logger.info('User "%s" course "%s"', uid, course_id)
333 331
334 - # ------------------------------------------------------------------------  
335 - #  
336 - # ------------------------------------------------------------------------  
337 async def start_topic(self, uid: str, topic: str) -> None: 332 async def start_topic(self, uid: str, topic: str) -> None:
338 '''Start new topic''' 333 '''Start new topic'''
339 334
340 student = self.online[uid]['state'] 335 student = self.online[uid]['state']
341 - # if uid == '0':  
342 - # logger.warning('Reloading "%s"', topic) # FIXME should be an option  
343 - # self.factory.update(self._factory_for(topic))  
344 336
345 try: 337 try:
346 await student.start_topic(topic) 338 await student.start_topic(topic)
@@ -350,9 +342,6 @@ class LearnApp(): @@ -350,9 +342,6 @@ class LearnApp():
350 else: 342 else:
351 logger.info('User "%s" started topic "%s"', uid, topic) 343 logger.info('User "%s" started topic "%s"', uid, topic)
352 344
353 - # ------------------------------------------------------------------------  
354 - #  
355 - # ------------------------------------------------------------------------  
356 def _add_missing_topics(self, topics: Iterable[str]) -> None: 345 def _add_missing_topics(self, topics: Iterable[str]) -> None:
357 ''' 346 '''
358 Fill db table 'Topic' with topics from the graph, if new 347 Fill db table 'Topic' with topics from the graph, if new
@@ -365,7 +354,6 @@ class LearnApp(): @@ -365,7 +354,6 @@ class LearnApp():
365 session.commit() 354 session.commit()
366 logger.info('Added %d new topic(s) to the database', len(new)) 355 logger.info('Added %d new topic(s) to the database', len(new))
367 356
368 - # ------------------------------------------------------------------------  
369 def _db_setup(self, database: str) -> None: 357 def _db_setup(self, database: str) -> None:
370 ''' 358 '''
371 Setup and check database contents 359 Setup and check database contents
@@ -373,12 +361,12 @@ class LearnApp(): @@ -373,12 +361,12 @@ class LearnApp():
373 361
374 logger.info('Checking database "%s":', database) 362 logger.info('Checking database "%s":', database)
375 if not exists(database): 363 if not exists(database):
376 - raise LearnException('Database does not exist. '  
377 - 'Use "initdb-aprendizations" to create') 364 + msg = 'Database does not exist.'
  365 + logger.error(msg)
  366 + raise LearnException(msg)
378 367
379 self._engine = create_engine(f'sqlite:///{database}', future=True) 368 self._engine = create_engine(f'sqlite:///{database}', future=True)
380 369
381 -  
382 try: 370 try:
383 query_students = select(func.count(Student.id)) 371 query_students = select(func.count(Student.id))
384 query_topics = select(func.count(Topic.id)) 372 query_topics = select(func.count(Topic.id))
@@ -395,7 +383,6 @@ class LearnApp(): @@ -395,7 +383,6 @@ class LearnApp():
395 logger.info('%6d topics', count_topics) 383 logger.info('%6d topics', count_topics)
396 logger.info('%6d answers', count_answers) 384 logger.info('%6d answers', count_answers)
397 385
398 - # ------------------------------------------------------------------------  
399 def _populate_graph(self, config: Dict[str, Any]) -> None: 386 def _populate_graph(self, config: Dict[str, Any]) -> None:
400 ''' 387 '''
401 Populates a digraph. 388 Populates a digraph.
@@ -429,7 +416,7 @@ class LearnApp(): @@ -429,7 +416,7 @@ class LearnApp():
429 416
430 topic = self.deps.nodes[tref] # get current topic node 417 topic = self.deps.nodes[tref] # get current topic node
431 topic['name'] = attr.get('name', tref) 418 topic['name'] = attr.get('name', tref)
432 - topic['questions'] = attr.get('questions', []) # FIXME unused?? 419 + topic['questions'] = attr.get('questions', [])
433 420
434 for k, default in defaults.items(): 421 for k, default in defaults.items():
435 topic[k] = attr.get(k, default) 422 topic[k] = attr.get(k, default)
@@ -437,12 +424,10 @@ class LearnApp(): @@ -437,12 +424,10 @@ class LearnApp():
437 # prefix/topic 424 # prefix/topic
438 topic['path'] = join(self.deps.graph['prefix'], tref) 425 topic['path'] = join(self.deps.graph['prefix'], tref)
439 426
440 -  
441 - # ======================================================================== 427 + # ------------------------------------------------------------------------
442 # methods that do not change state (pure functions) 428 # methods that do not change state (pure functions)
443 - # ========================================================================  
444 -  
445 # ------------------------------------------------------------------------ 429 # ------------------------------------------------------------------------
  430 +
446 def _make_factory(self) -> Dict[str, QFactory]: 431 def _make_factory(self) -> Dict[str, QFactory]:
447 ''' 432 '''
448 Buils dictionary of question factories 433 Buils dictionary of question factories
@@ -451,7 +436,7 @@ class LearnApp(): @@ -451,7 +436,7 @@ class LearnApp():
451 ''' 436 '''
452 437
453 logger.info('Building questions factory:') 438 logger.info('Building questions factory:')
454 - factory = dict() 439 + factory = {}
455 for tref in self.deps.nodes: 440 for tref in self.deps.nodes:
456 factory.update(self._factory_for(tref)) 441 factory.update(self._factory_for(tref))
457 442
@@ -462,7 +447,7 @@ class LearnApp(): @@ -462,7 +447,7 @@ class LearnApp():
462 # makes factory for a single topic 447 # makes factory for a single topic
463 # ------------------------------------------------------------------------ 448 # ------------------------------------------------------------------------
464 def _factory_for(self, tref: str) -> Dict[str, QFactory]: 449 def _factory_for(self, tref: str) -> Dict[str, QFactory]:
465 - factory: Dict[str, QFactory] = dict() 450 + factory: Dict[str, QFactory] = {}
466 topic = self.deps.nodes[tref] # get node 451 topic = self.deps.nodes[tref] # get node
467 # load questions as list of dicts 452 # load questions as list of dicts
468 try: 453 try:
@@ -519,38 +504,31 @@ class LearnApp(): @@ -519,38 +504,31 @@ class LearnApp():
519 504
520 return factory 505 return factory
521 506
522 - # ------------------------------------------------------------------------  
523 def get_login_counter(self, uid: str) -> int: 507 def get_login_counter(self, uid: str) -> int:
524 - '''login counter''' # FIXME 508 + '''login counter'''
525 return int(self.online[uid]['counter']) 509 return int(self.online[uid]['counter'])
526 510
527 - # ------------------------------------------------------------------------  
528 def get_student_name(self, uid: str) -> str: 511 def get_student_name(self, uid: str) -> str:
529 '''Get the username''' 512 '''Get the username'''
530 return self.online[uid].get('name', '') 513 return self.online[uid].get('name', '')
531 514
532 - # ------------------------------------------------------------------------  
533 def get_student_state(self, uid: str) -> List[Dict[str, Any]]: 515 def get_student_state(self, uid: str) -> List[Dict[str, Any]]:
534 '''Get the knowledge state of a given user''' 516 '''Get the knowledge state of a given user'''
535 return self.online[uid]['state'].get_knowledge_state() 517 return self.online[uid]['state'].get_knowledge_state()
536 518
537 - # ------------------------------------------------------------------------  
538 def get_student_progress(self, uid: str) -> float: 519 def get_student_progress(self, uid: str) -> float:
539 '''Get the current topic progress of a given user''' 520 '''Get the current topic progress of a given user'''
540 return float(self.online[uid]['state'].get_topic_progress()) 521 return float(self.online[uid]['state'].get_topic_progress())
541 522
542 - # ------------------------------------------------------------------------  
543 def get_current_question(self, uid: str) -> Optional[Question]: 523 def get_current_question(self, uid: str) -> Optional[Question]:
544 '''Get the current question of a given user''' 524 '''Get the current question of a given user'''
545 question: Optional[Question] = self.online[uid]['state'].get_current_question() 525 question: Optional[Question] = self.online[uid]['state'].get_current_question()
546 return question 526 return question
547 527
548 - # ------------------------------------------------------------------------  
549 def get_current_question_id(self, uid: str) -> str: 528 def get_current_question_id(self, uid: str) -> str:
550 '''Get id of the current question for a given user''' 529 '''Get id of the current question for a given user'''
551 return str(self.online[uid]['state'].get_current_question()['qid']) 530 return str(self.online[uid]['state'].get_current_question()['qid'])
552 531
553 - # ------------------------------------------------------------------------  
554 def get_student_question_type(self, uid: str) -> str: 532 def get_student_question_type(self, uid: str) -> str:
555 '''Get type of the current question for a given user''' 533 '''Get type of the current question for a given user'''
556 return str(self.online[uid]['state'].get_current_question()['type']) 534 return str(self.online[uid]['state'].get_current_question()['type'])
@@ -559,12 +537,10 @@ class LearnApp(): @@ -559,12 +537,10 @@ class LearnApp():
559 # def get_student_topic(self, uid: str) -> str: 537 # def get_student_topic(self, uid: str) -> str:
560 # return str(self.online[uid]['state'].get_current_topic()) 538 # return str(self.online[uid]['state'].get_current_topic())
561 539
562 - # ------------------------------------------------------------------------  
563 def get_student_course_title(self, uid: str) -> str: 540 def get_student_course_title(self, uid: str) -> str:
564 '''get the title of the current course for a given user''' 541 '''get the title of the current course for a given user'''
565 return str(self.online[uid]['state'].get_current_course_title()) 542 return str(self.online[uid]['state'].get_current_course_title())
566 543
567 - # ------------------------------------------------------------------------  
568 def get_current_course_id(self, uid: str) -> Optional[str]: 544 def get_current_course_id(self, uid: str) -> Optional[str]:
569 '''get the current course (id) of a given user''' 545 '''get the current course (id) of a given user'''
570 cid: Optional[str] = self.online[uid]['state'].get_current_course_id() 546 cid: Optional[str] = self.online[uid]['state'].get_current_course_id()
@@ -574,7 +550,6 @@ class LearnApp(): @@ -574,7 +550,6 @@ class LearnApp():
574 # def get_topic_name(self, ref: str) -> str: 550 # def get_topic_name(self, ref: str) -> str:
575 # return str(self.deps.nodes[ref]['name']) 551 # return str(self.deps.nodes[ref]['name'])
576 552
577 - # ------------------------------------------------------------------------  
578 def get_current_public_dir(self, uid: str) -> str: 553 def get_current_public_dir(self, uid: str) -> str:
579 ''' 554 '''
580 Get the path for the 'public' directory of the current topic of the 555 Get the path for the 'public' directory of the current topic of the
@@ -586,21 +561,18 @@ class LearnApp(): @@ -586,21 +561,18 @@ class LearnApp():
586 prefix: str = self.deps.graph['prefix'] 561 prefix: str = self.deps.graph['prefix']
587 return join(prefix, topic, 'public') 562 return join(prefix, topic, 'public')
588 563
589 - # ------------------------------------------------------------------------  
590 def get_courses(self) -> Dict[str, Dict[str, Any]]: 564 def get_courses(self) -> Dict[str, Dict[str, Any]]:
591 ''' 565 '''
592 Get dictionary with all courses {'course1': {...}, 'course2': {...}} 566 Get dictionary with all courses {'course1': {...}, 'course2': {...}}
593 ''' 567 '''
594 return self.courses 568 return self.courses
595 569
596 - # ------------------------------------------------------------------------  
597 def get_course(self, course_id: str) -> Dict[str, Any]: 570 def get_course(self, course_id: str) -> Dict[str, Any]:
598 ''' 571 '''
599 Get dictionary {'title': ..., 'description':..., 'goals':...} 572 Get dictionary {'title': ..., 'description':..., 'goals':...}
600 ''' 573 '''
601 return self.courses[course_id] 574 return self.courses[course_id]
602 575
603 - # ------------------------------------------------------------------------  
604 def get_rankings(self, uid: str, cid: str) -> List[Tuple[str, str, float]]: 576 def get_rankings(self, uid: str, cid: str) -> List[Tuple[str, str, float]]:
605 ''' 577 '''
606 Returns rankings for a certain cid (course_id). 578 Returns rankings for a certain cid (course_id).
@@ -626,18 +598,18 @@ class LearnApp(): @@ -626,18 +598,18 @@ class LearnApp():
626 student_topics = session.execute(query_student_topics).all() 598 student_topics = session.execute(query_student_topics).all()
627 599
628 # compute topic progress 600 # compute topic progress
629 - now = datetime.now()  
630 - goals = self.courses[cid]['goals']  
631 progress: DefaultDict[str, float] = defaultdict(int) 601 progress: DefaultDict[str, float] = defaultdict(int)
  602 + goals = self.courses[cid]['goals']
  603 + num_goals = len(goals)
  604 + now = datetime.now()
632 605
633 for student, topic, level, datestr in student_topics: 606 for student, topic, level, datestr in student_topics:
634 if topic in goals: 607 if topic in goals:
635 date = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S.%f") 608 date = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S.%f")
636 - progress[student] += level**(now - date).days / len(goals) 609 + elapsed_days = (now - date).days
  610 + progress[student] += level**elapsed_days / num_goals
637 611
638 return sorted(((u, name, progress[u]) 612 return sorted(((u, name, progress[u])
639 for u, name in students 613 for u, name in students
640 if u in progress and (len(u) > 2 or len(uid) <= 2)), 614 if u in progress and (len(u) > 2 or len(uid) <= 2)),
641 key=lambda x: x[2], reverse=True) 615 key=lambda x: x[2], reverse=True)
642 -  
643 - # ------------------------------------------------------------------------  
aprendizations/main.py
@@ -7,6 +7,7 @@ Setup configurations and then runs the application. @@ -7,6 +7,7 @@ Setup configurations and then runs the application.
7 7
8 # python standard library 8 # python standard library
9 import argparse 9 import argparse
  10 +import logging
10 import logging.config 11 import logging.config
11 from os import environ, path 12 from os import environ, path
12 import ssl 13 import ssl
@@ -14,10 +15,10 @@ import sys @@ -14,10 +15,10 @@ import sys
14 from typing import Any, Dict 15 from typing import Any, Dict
15 16
16 # this project 17 # this project
17 -from aprendizations.learnapp import LearnApp, DatabaseUnusableError, LearnException  
18 -from aprendizations.serve import run_webserver  
19 -from aprendizations.tools import load_yaml  
20 -from aprendizations import APP_NAME, APP_VERSION 18 +from .learnapp import LearnApp, DatabaseUnusableError, LearnException
  19 +from .serve import run_webserver
  20 +from .tools import load_yaml
  21 +from . import APP_NAME, APP_VERSION
21 22
22 23
23 # ---------------------------------------------------------------------------- 24 # ----------------------------------------------------------------------------
@@ -115,7 +116,7 @@ def get_logger_config(debug: bool = False) -&gt; Any: @@ -115,7 +116,7 @@ def get_logger_config(debug: bool = False) -&gt; Any:
115 'level': level, 116 'level': level,
116 'propagate': False, 117 'propagate': False,
117 } for module in ['learnapp', 'models', 'factory', 'tools', 'serve', 118 } for module in ['learnapp', 'models', 'factory', 'tools', 'serve',
118 - 'questions', 'student']}) 119 + 'questions', 'student', 'main']})
119 120
120 return load_yaml(config_file, default=default_config) 121 return load_yaml(config_file, default=default_config)
121 122
@@ -181,7 +182,7 @@ def main(): @@ -181,7 +182,7 @@ def main():
181 try: 182 try:
182 learnapp = LearnApp(courses=arg.courses, 183 learnapp = LearnApp(courses=arg.courses,
183 prefix=arg.prefix, 184 prefix=arg.prefix,
184 - db=arg.db, 185 + dbase=arg.db,
185 check=arg.check) 186 check=arg.check)
186 except DatabaseUnusableError: 187 except DatabaseUnusableError:
187 logging.critical('Failed to start application.') 188 logging.critical('Failed to start application.')
@@ -196,13 +197,8 @@ def main(): @@ -196,13 +197,8 @@ def main():
196 sep='\n') 197 sep='\n')
197 sys.exit(1) 198 sys.exit(1)
198 except LearnException as exc: 199 except LearnException as exc:
199 - logging.critical('Failed to start backend')  
200 - # sys.exit(1)  
201 - raise  
202 - except Exception:  
203 - logging.critical('Unknown error')  
204 - # sys.exit(1)  
205 - raise 200 + logging.critical('Failed to start backend: %s', str(exc))
  201 + sys.exit(1)
206 else: 202 else:
207 logging.info('LearnApp started') 203 logging.info('LearnApp started')
208 204
aprendizations/serve.py
@@ -4,10 +4,9 @@ Webserver @@ -4,10 +4,9 @@ Webserver
4 4
5 5
6 # python standard library 6 # python standard library
7 -import asyncio  
8 import base64 7 import base64
9 import functools 8 import functools
10 -import logging 9 +from logging import getLogger
11 import mimetypes 10 import mimetypes
12 from os.path import join, dirname, expanduser 11 from os.path import join, dirname, expanduser
13 import signal 12 import signal
@@ -28,7 +27,7 @@ from aprendizations import APP_NAME @@ -28,7 +27,7 @@ from aprendizations import APP_NAME
28 27
29 28
30 # setup logger for this module 29 # setup logger for this module
31 -logger = logging.getLogger(__name__) 30 +logger = getLogger(__name__)
32 31
33 32
34 # ---------------------------------------------------------------------------- 33 # ----------------------------------------------------------------------------
@@ -37,7 +36,7 @@ def admin_only(func): @@ -37,7 +36,7 @@ def admin_only(func):
37 Decorator used to restrict access to the administrator 36 Decorator used to restrict access to the administrator
38 ''' 37 '''
39 @functools.wraps(func) 38 @functools.wraps(func)
40 - def wrapper(self, *args, **kwargs): 39 + def wrapper(self, *args, **kwargs) -> None:
41 if self.current_user != '0': 40 if self.current_user != '0':
42 raise tornado.web.HTTPError(403) # forbidden 41 raise tornado.web.HTTPError(403) # forbidden
43 func(self, *args, **kwargs) 42 func(self, *args, **kwargs)
@@ -49,7 +48,7 @@ class WebApplication(tornado.web.Application): @@ -49,7 +48,7 @@ class WebApplication(tornado.web.Application):
49 ''' 48 '''
50 WebApplication - Tornado Web Server 49 WebApplication - Tornado Web Server
51 ''' 50 '''
52 - def __init__(self, learnapp, debug=False): 51 + def __init__(self, learnapp, debug=False) -> None:
53 handlers = [ 52 handlers = [
54 (r'/login', LoginHandler), 53 (r'/login', LoginHandler),
55 (r'/logout', LogoutHandler), 54 (r'/logout', LogoutHandler),
@@ -94,7 +93,6 @@ class BaseHandler(tornado.web.RequestHandler): @@ -94,7 +93,6 @@ class BaseHandler(tornado.web.RequestHandler):
94 counter_cookie = self.get_secure_cookie('counter') 93 counter_cookie = self.get_secure_cookie('counter')
95 if user_cookie is not None: 94 if user_cookie is not None:
96 uid = user_cookie.decode('utf-8') 95 uid = user_cookie.decode('utf-8')
97 -  
98 if counter_cookie is not None: 96 if counter_cookie is not None:
99 counter = counter_cookie.decode('utf-8') 97 counter = counter_cookie.decode('utf-8')
100 if counter == str(self.learn.get_login_counter(uid)): 98 if counter == str(self.learn.get_login_counter(uid)):
@@ -108,7 +106,7 @@ class RankingsHandler(BaseHandler): @@ -108,7 +106,7 @@ class RankingsHandler(BaseHandler):
108 Handles rankings page 106 Handles rankings page
109 ''' 107 '''
110 @tornado.web.authenticated 108 @tornado.web.authenticated
111 - def get(self): 109 + def get(self) -> None:
112 ''' 110 '''
113 Renders list of students that have answers in this course. 111 Renders list of students that have answers in this course.
114 ''' 112 '''
@@ -122,18 +120,17 @@ class RankingsHandler(BaseHandler): @@ -122,18 +120,17 @@ class RankingsHandler(BaseHandler):
122 name=self.learn.get_student_name(uid), 120 name=self.learn.get_student_name(uid),
123 rankings=rankings, 121 rankings=rankings,
124 course_id=course_id, 122 course_id=course_id,
125 - course_title=self.learn.get_student_course_title(uid), # FIXME get from course var 123 + course_title=self.learn.get_student_course_title(uid),
  124 + # FIXME get from course var
126 ) 125 )
127 126
128 127
129 # ---------------------------------------------------------------------------- 128 # ----------------------------------------------------------------------------
130 -#  
131 -# ----------------------------------------------------------------------------  
132 class LoginHandler(BaseHandler): 129 class LoginHandler(BaseHandler):
133 ''' 130 '''
134 Handles /login 131 Handles /login
135 ''' 132 '''
136 - def get(self): 133 + def get(self) -> None:
137 ''' 134 '''
138 Renders login page 135 Renders login page
139 ''' 136 '''
@@ -168,7 +165,7 @@ class LogoutHandler(BaseHandler): @@ -168,7 +165,7 @@ class LogoutHandler(BaseHandler):
168 Handles /logout 165 Handles /logout
169 ''' 166 '''
170 @tornado.web.authenticated 167 @tornado.web.authenticated
171 - def get(self): 168 + def get(self) -> None:
172 ''' 169 '''
173 clears cookies and removes user session 170 clears cookies and removes user session
174 ''' 171 '''
@@ -176,7 +173,7 @@ class LogoutHandler(BaseHandler): @@ -176,7 +173,7 @@ class LogoutHandler(BaseHandler):
176 self.clear_cookie('counter') 173 self.clear_cookie('counter')
177 self.redirect('/') 174 self.redirect('/')
178 175
179 - def on_finish(self): 176 + def on_finish(self) -> None:
180 self.learn.logout(self.current_user) 177 self.learn.logout(self.current_user)
181 178
182 179
@@ -186,7 +183,7 @@ class ChangePasswordHandler(BaseHandler): @@ -186,7 +183,7 @@ class ChangePasswordHandler(BaseHandler):
186 Handles password change 183 Handles password change
187 ''' 184 '''
188 @tornado.web.authenticated 185 @tornado.web.authenticated
189 - async def post(self): 186 + async def post(self) -> None:
190 ''' 187 '''
191 Tries to perform password change and then replies success/fail status 188 Tries to perform password change and then replies success/fail status
192 ''' 189 '''
@@ -216,7 +213,7 @@ class RootHandler(BaseHandler): @@ -216,7 +213,7 @@ class RootHandler(BaseHandler):
216 Handles root / 213 Handles root /
217 ''' 214 '''
218 @tornado.web.authenticated 215 @tornado.web.authenticated
219 - def get(self): 216 + def get(self) -> None:
220 '''Simply redirects to the main entrypoint''' 217 '''Simply redirects to the main entrypoint'''
221 self.redirect('/courses') 218 self.redirect('/courses')
222 219
@@ -226,11 +223,11 @@ class CoursesHandler(BaseHandler): @@ -226,11 +223,11 @@ class CoursesHandler(BaseHandler):
226 ''' 223 '''
227 Handles /courses 224 Handles /courses
228 ''' 225 '''
229 - def set_default_headers(self, *args, **kwargs): 226 + def set_default_headers(self, *_) -> None:
230 self.set_header('Cache-Control', 'no-cache') 227 self.set_header('Cache-Control', 'no-cache')
231 228
232 @tornado.web.authenticated 229 @tornado.web.authenticated
233 - def get(self): 230 + def get(self) -> None:
234 '''Renders list of available courses''' 231 '''Renders list of available courses'''
235 uid = self.current_user 232 uid = self.current_user
236 self.render('courses.html', 233 self.render('courses.html',
@@ -249,7 +246,7 @@ class CourseHandler(BaseHandler): @@ -249,7 +246,7 @@ class CourseHandler(BaseHandler):
249 ''' 246 '''
250 247
251 @tornado.web.authenticated 248 @tornado.web.authenticated
252 - def get(self, course_id): 249 + def get(self, course_id) -> None:
253 ''' 250 '''
254 Handles get /course/... 251 Handles get /course/...
255 Starts a given course and show list of topics 252 Starts a given course and show list of topics
@@ -278,11 +275,11 @@ class TopicHandler(BaseHandler): @@ -278,11 +275,11 @@ class TopicHandler(BaseHandler):
278 ''' 275 '''
279 Handles a topic 276 Handles a topic
280 ''' 277 '''
281 - def set_default_headers(self, *args, **kwargs): 278 + def set_default_headers(self, *_) -> None:
282 self.set_header('Cache-Control', 'no-cache') 279 self.set_header('Cache-Control', 'no-cache')
283 280
284 @tornado.web.authenticated 281 @tornado.web.authenticated
285 - async def get(self, topic): 282 + async def get(self, topic) -> None:
286 ''' 283 '''
287 Handles get /topic/... 284 Handles get /topic/...
288 Starts a given topic 285 Starts a given topic
@@ -308,7 +305,7 @@ class FileHandler(BaseHandler): @@ -308,7 +305,7 @@ class FileHandler(BaseHandler):
308 Serves files from the /public subdir of the topics. 305 Serves files from the /public subdir of the topics.
309 ''' 306 '''
310 @tornado.web.authenticated 307 @tornado.web.authenticated
311 - async def get(self, filename): 308 + async def get(self, filename) -> None:
312 ''' 309 '''
313 Serves files from /public subdirectories of a particular topic 310 Serves files from /public subdirectories of a particular topic
314 ''' 311 '''
@@ -351,7 +348,7 @@ class QuestionHandler(BaseHandler): @@ -351,7 +348,7 @@ class QuestionHandler(BaseHandler):
351 348
352 # ------------------------------------------------------------------------ 349 # ------------------------------------------------------------------------
353 @tornado.web.authenticated 350 @tornado.web.authenticated
354 - async def get(self): 351 + async def get(self) -> None:
355 ''' 352 '''
356 Gets question to render. 353 Gets question to render.
357 Shows an animated trophy if there are no more questions in the topic. 354 Shows an animated trophy if there are no more questions in the topic.
@@ -410,9 +407,6 @@ class QuestionHandler(BaseHandler): @@ -410,9 +407,6 @@ class QuestionHandler(BaseHandler):
410 }) 407 })
411 return 408 return
412 409
413 - # --- brain hacking ;)  
414 - # await asyncio.sleep(1.5)  
415 -  
416 # --- answers are in a list. fix depending on question type 410 # --- answers are in a list. fix depending on question type
417 qtype = self.learn.get_student_question_type(user) 411 qtype = self.learn.get_student_question_type(user)
418 ans: Optional[Union[List, str]] 412 ans: Optional[Union[List, str]]
@@ -489,7 +483,7 @@ def signal_handler(*_) -&gt; None: @@ -489,7 +483,7 @@ def signal_handler(*_) -&gt; None:
489 reply = input(' --> Stop webserver? (yes/no) ') 483 reply = input(' --> Stop webserver? (yes/no) ')
490 if reply.lower() == 'yes': 484 if reply.lower() == 'yes':
491 tornado.ioloop.IOLoop.current().stop() 485 tornado.ioloop.IOLoop.current().stop()
492 - logging.critical('Webserver stopped.') 486 + logger.critical('Webserver stopped.')
493 sys.exit(0) 487 sys.exit(0)
494 488
495 489
@@ -503,10 +497,9 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -&gt; None: @@ -503,10 +497,9 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -&gt; None:
503 try: 497 try:
504 webapp = WebApplication(app, debug=debug) 498 webapp = WebApplication(app, debug=debug)
505 except Exception: 499 except Exception:
506 - logger.critical('Failed to start web application.') 500 + logger.critical('Failed to start web application.', exc_info=True)
507 sys.exit(1) 501 sys.exit(1)
508 - else:  
509 - logger.info('Web application started (tornado.web.Application)') 502 + logger.info('Web application started (tornado.web.Application)')
510 503
511 # --- create tornado webserver 504 # --- create tornado webserver
512 try: 505 try:
@@ -514,19 +507,17 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -&gt; None: @@ -514,19 +507,17 @@ def run_webserver(app, ssl, port: int = 8443, debug: bool = False) -&gt; None:
514 except ValueError: 507 except ValueError:
515 logger.critical('Certificates cert.pem and privkey.pem not found') 508 logger.critical('Certificates cert.pem and privkey.pem not found')
516 sys.exit(1) 509 sys.exit(1)
517 - else:  
518 - logger.debug('HTTP server started') 510 + logger.debug('HTTP server started')
519 511
520 try: 512 try:
521 httpserver.listen(port) 513 httpserver.listen(port)
522 except OSError: 514 except OSError:
523 logger.critical('Cannot bind port %d. Already in use?', port) 515 logger.critical('Cannot bind port %d. Already in use?', port)
524 sys.exit(1) 516 sys.exit(1)
525 -  
526 - # --- run webserver  
527 logger.info('Webserver listening on %d... (Ctrl-C to stop)', port) 517 logger.info('Webserver listening on %d... (Ctrl-C to stop)', port)
528 signal.signal(signal.SIGINT, signal_handler) 518 signal.signal(signal.SIGINT, signal_handler)
529 519
  520 + # --- run webserver
530 try: 521 try:
531 tornado.ioloop.IOLoop.current().start() # running... 522 tornado.ioloop.IOLoop.current().start() # running...
532 except Exception: 523 except Exception:
aprendizations/templates/courses.html
@@ -7,18 +7,12 @@ @@ -7,18 +7,12 @@
7 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 <meta name="author" content="Miguel Barão"> 8 <meta name="author" content="Miguel Barão">
9 <link rel="icon" href="favicon.ico"> 9 <link rel="icon" href="favicon.ico">
10 - <!-- Styles -->  
11 - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">  
12 - <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"> 10 +
  11 + {% include include-libs.html %}
13 12
14 <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}"> 13 <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}">
15 <link rel="stylesheet" href="{{static_url('css/sticky-footer-navbar.css')}}"> 14 <link rel="stylesheet" href="{{static_url('css/sticky-footer-navbar.css')}}">
16 - <!-- Scripts -->  
17 - <script src="https://code.jquery.com/jquery-3.6.0.min.js"  
18 - integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>  
19 - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"  
20 - integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>  
21 - 15 +
22 <script defer src="{{static_url('js/maintopics.js')}}"></script> 16 <script defer src="{{static_url('js/maintopics.js')}}"></script>
23 17
24 <title>{{appname}}</title> 18 <title>{{appname}}</title>
@@ -42,8 +36,7 @@ @@ -42,8 +36,7 @@
42 <ul class="navbar-nav ms-auto"> 36 <ul class="navbar-nav ms-auto">
43 <li class="nav-item dropdown"> 37 <li class="nav-item dropdown">
44 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 38 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
45 - <i class="fas fa-user-graduate" aria-hidden="true"></i>  
46 - &nbsp; 39 + <i class="bi bi-person-circle"></i>&nbsp;
47 <span id="name">{{ escape(name) }}</span> 40 <span id="name">{{ escape(name) }}</span>
48 <span class="caret"></span> 41 <span class="caret"></span>
49 </a> 42 </a>
aprendizations/templates/include-libs.html 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +<!-- jquery -->
  2 +<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
  3 +
  4 +<!-- bootstrap -->
  5 +<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
  6 +<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
  7 +
  8 +<!-- bootstrap icons -->
  9 +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
aprendizations/templates/login.html
@@ -5,11 +5,7 @@ @@ -5,11 +5,7 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <meta name="author" content="Miguel Barão"> 6 <meta name="author" content="Miguel Barão">
7 7
8 - <!-- <link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/"> -->  
9 -  
10 - <!-- Bootstrap core CSS -->  
11 - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">  
12 - <!-- <link href="../assets/dist/css/bootstrap.min.css" rel="stylesheet"> --> 8 + {% include include-libs.html %}
13 9
14 <style> 10 <style>
15 .bd-placeholder-img { 11 .bd-placeholder-img {
aprendizations/templates/maintopics-table.html
@@ -8,16 +8,9 @@ @@ -8,16 +8,9 @@
8 <meta name="author" content="Miguel Barão"> 8 <meta name="author" content="Miguel Barão">
9 <link rel="icon" href="/static/favicon.ico"> 9 <link rel="icon" href="/static/favicon.ico">
10 10
11 -<!-- Styles -->  
12 - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">  
13 - <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">  
14 - 11 + {% include include-libs.html %}
  12 +
15 <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}"> 13 <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}">
16 -  
17 -<!-- Scripts -->  
18 - <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>  
19 - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>  
20 -  
21 <script defer src="{{static_url('js/maintopics.js')}}"></script> 14 <script defer src="{{static_url('js/maintopics.js')}}"></script>
22 15
23 <title>{{appname}}</title> 16 <title>{{appname}}</title>
@@ -40,8 +33,7 @@ @@ -40,8 +33,7 @@
40 <ul class="navbar-nav ms-auto"> 33 <ul class="navbar-nav ms-auto">
41 <li class="nav-item dropdown"> 34 <li class="nav-item dropdown">
42 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 35 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
43 - <i class="fas fa-user-graduate" aria-hidden="true"></i>  
44 - &nbsp; 36 + <i class="bi bi-person-circle"></i>&nbsp;
45 <span id="name">{{ escape(name) }}</span> 37 <span id="name">{{ escape(name) }}</span>
46 <span class="caret"></span> 38 <span class="caret"></span>
47 </a> 39 </a>
@@ -104,58 +96,71 @@ @@ -104,58 +96,71 @@
104 <!-- ------------------------------------------------------------- --> 96 <!-- ------------------------------------------------------------- -->
105 {% if t['level'] is None %} 97 {% if t['level'] is None %}
106 <tr> 98 <tr>
107 - <th scope="row" class="text-muted text-center">  
108 - {% if t['type']=='chapter' %}  
109 - <i class="fas fa-flag"></i>  
110 - {% elif t['type']=='learn' %}  
111 - <i class="fas fa-book"></i>&nbsp;  
112 - {% else %}  
113 - <i class="fas fa-pencil-alt"></i>&nbsp;  
114 - {% end %}  
115 - </th>  
116 - <td>  
117 - <div class="text-muted">  
118 - {% if t['ref'] not in course['goals'] %}  
119 - <i class="fas fa-puzzle-piece"></i> 99 + <td scope="row" class="text-muted text-center">
  100 + <h5>
  101 + {% if t['type']=='chapter' %}
  102 + <i class="bi bi-flag-fill"></i>
  103 + {% elif t['type']=='learn' %}
  104 + <i class="bi bi-book"></i>
  105 + {% else %}
  106 + <i class="bi bi-pencil-square"></i>
120 {% end %} 107 {% end %}
121 -  
122 - 108 + </h5>
  109 + </td>
  110 + <td class="text-muted">
  111 + {% if t['ref'] not in course['goals'] %}
  112 + <span class="text-nowrap" data-toggle="tooltip" data-placement="bottom" title="Este tópico é um pré-requisito">
  113 + <i class="bi bi-puzzle-fill"></i>
  114 + {{ t['name'] }}
  115 + </span>
  116 + {% else %}
123 {{ t['name'] }} 117 {{ t['name'] }}
124 - </div> 118 + {% end %}
125 </td> 119 </td>
126 <td class="text-center"> 120 <td class="text-center">
127 - <i class="fas fa-lock text-muted"></i> 121 + <h5><i class="bi bi-lock-fill text-muted"></i></h5>
128 </td> 122 </td>
129 </tr> 123 </tr>
130 124
131 {% else %} 125 {% else %}
132 126
133 <tr class="clickable-row " data-href="/topic/{{t['ref']}}"> 127 <tr class="clickable-row " data-href="/topic/{{t['ref']}}">
134 - <th scope="row" class="text-primary text-center">  
135 - {% if t['type']=='chapter' %}  
136 - <i class="fas fa-flag-checkered"></i>&nbsp;  
137 - {% elif t['type']=='learn' %}  
138 - <i class="fas fa-book"></i>&nbsp;  
139 - {% else %}  
140 - <i class="fas fa-pencil-alt"></i>&nbsp;  
141 - {% end %}  
142 - </th> 128 + <td scope="row" class="text-primary text-center">
  129 + <h5>
  130 + {% if t['type']=='chapter' %}
  131 + <span class="text-nowrap" data-toggle="tooltip" data-placement="bottom" title="Fim do capítulo">
  132 + <i class="bi bi-flag-fill"></i>
  133 + </span>
  134 + {% elif t['type']=='learn' %}
  135 + <span class="text-nowrap" data-toggle="tooltip" data-placement="bottom" title="Texto com matéria">
  136 + <i class="bi bi-book"></i>
  137 + </span>
  138 + {% else %}
  139 + <span class="text-nowrap" data-toggle="tooltip" data-placement="bottom" title="Exercícios">
  140 + <i class="bi bi-pencil-square"></i>
  141 + </span>
  142 + {% end %}
  143 + </h5>
  144 + </td>
143 <td class="text-primary"> 145 <td class="text-primary">
144 {% if t['ref'] not in course['goals'] %} 146 {% if t['ref'] not in course['goals'] %}
145 - <i class="fas fa-puzzle-piece"></i> 147 + <span class="text-nowrap" data-toggle="tooltip" data-placement="bottom" title="Este tópico é um pré-requisito">
  148 + <i class="bi bi-puzzle-fill"></i>
  149 + {{ t['name'] }}
  150 + </span>
  151 + {% else %}
  152 + {{ t['name'] }}
146 {% end %} 153 {% end %}
147 -  
148 - {{ t['name'] }}  
149 </td> 154 </td>
150 155
151 <td class="text-center"> 156 <td class="text-center">
152 {% if t['level'] < 0.01 %} 157 {% if t['level'] < 0.01 %}
153 - <i class="fas fa-lock-open text-success"></i> 158 + <h5><i class="bi bi-unlock-fill text-success"></i></h5>
154 {% elif t['type']=='chapter' %} 159 {% elif t['type']=='chapter' %}
155 - <h5><i class="fas fa-award text-warning"></i></h5> 160 + <h5><i class="bi bi-award-fill"></i></h5>
156 {% else %} 161 {% else %}
157 - <span class="text-nowrap text-warning" data-toggle="tooltip" data-placement="bottom" title="{{round(t['level']*5, 2)}}">  
158 - {{int(t['level']*5+0.25)*'<i class="fas fa-star"></i>'}}{% if int(t['level']*5+0.25) < 5 %}{{'<i class="fas fa-star-half-alt"></i>' if 0.25 <= t['level']*5-int(t['level']*5) < 0.75 else '<i class="far fa-star"></i>'}}{% end %}{{(4-int(t['level']*5+0.25))*'<i class="far fa-star"></i>' }} 162 + <span class="text-nowrap text-warning" data-toggle="tooltip" data-placement="bottom" title="{{round(t['level']*5, 3)}}">
  163 + <h5>{{ int(t['level']*5+0.25)*'<i class="bi bi-star-fill"></i>' }}{% if int(t['level']*5+0.25) < 5 %}{{'<i class="bi bi-star-half"></i>' if 0.25 <= t['level']*5-int(t['level']*5) < 0.75 else '<i class="bi bi-star"></i>'}}{% end %}{{ (4-int(t['level']*5+0.25))*'<i class="bi bi-star"></i>' }}</h5>
159 </span> 164 </span>
160 {% end %} <!-- if --> 165 {% end %} <!-- if -->
161 </td> 166 </td>
aprendizations/templates/rankings.html
@@ -7,14 +7,10 @@ @@ -7,14 +7,10 @@
7 <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 <meta name="viewport" content="width=device-width, initial-scale=1" />
8 <meta name="author" content="Miguel Barão"> 8 <meta name="author" content="Miguel Barão">
9 <link rel="icon" href="/static/favicon.ico"> 9 <link rel="icon" href="/static/favicon.ico">
10 - <!-- Styles -->  
11 - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">  
12 - <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">  
13 10
14 - <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}">  
15 - <!-- Scripts -->  
16 - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> 11 + {% include include-libs.html %}
17 12
  13 + <link rel="stylesheet" href="{{static_url('css/maintopics.css')}}">
18 <script defer src="{{static_url('js/maintopics.js')}}"></script> 14 <script defer src="{{static_url('js/maintopics.js')}}"></script>
19 15
20 <title>{{appname}}</title> 16 <title>{{appname}}</title>
@@ -37,8 +33,7 @@ @@ -37,8 +33,7 @@
37 <ul class="navbar-nav ms-auto"> 33 <ul class="navbar-nav ms-auto">
38 <li class="nav-item dropdown"> 34 <li class="nav-item dropdown">
39 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 35 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
40 - <i class="fas fa-user-graduate" aria-hidden="true"></i>  
41 - &nbsp; 36 + <i class="bi bi-person-circle"></i>&nbsp;
42 <span id="name">{{ escape(name) }}</span> 37 <span id="name">{{ escape(name) }}</span>
43 <span class="caret"></span> 38 <span class="caret"></span>
44 </a> 39 </a>
@@ -56,11 +51,10 @@ @@ -56,11 +51,10 @@
56 <h1 class="display-6">{{ course_title }}</h1> 51 <h1 class="display-6">{{ course_title }}</h1>
57 52
58 <table class="table table-sm table-hover"> 53 <table class="table table-sm table-hover">
59 - <!-- <col width="100"> -->  
60 <thead> 54 <thead>
61 <tr> 55 <tr>
62 <th scope="col" class="text-center">Posição</th> 56 <th scope="col" class="text-center">Posição</th>
63 - <th scope="col">Aluno</th> 57 + <th scope="col">Nome</th>
64 <th scope="col">Progresso</th> 58 <th scope="col">Progresso</th>
65 </tr> 59 </tr>
66 </thead> 60 </thead>
@@ -69,10 +63,10 @@ @@ -69,10 +63,10 @@
69 <tr class="{{ 'table-secondary' if r[0] == uid else '' }}"> 63 <tr class="{{ 'table-secondary' if r[0] == uid else '' }}">
70 <td class="text-center"> <!-- rank --> 64 <td class="text-center"> <!-- rank -->
71 <strong> 65 <strong>
72 - {{ '<i class="fas fa-crown fa-lg text-warning"></i>' if i==0 else i+1 }} 66 + {{ '<h3><i class="bi bi-trophy-fill text-warning"></i></h3>' if i==0 else i+1 }}
73 </strong> 67 </strong>
74 </td> 68 </td>
75 - <td> <!-- student name --> 69 + <td> <!-- first and last name -->
76 {{ ' '.join(r[1].split()[n] for n in (0,-1)) }} 70 {{ ' '.join(r[1].split()[n] for n in (0,-1)) }}
77 </td> 71 </td>
78 <td> <!-- progress --> 72 <td> <!-- progress -->
aprendizations/templates/topic.html
@@ -6,17 +6,9 @@ @@ -6,17 +6,9 @@
6 <meta name="author" content="Miguel Barão" /> 6 <meta name="author" content="Miguel Barão" />
7 <link rel="icon" href="/static/favicon.ico"> 7 <link rel="icon" href="/static/favicon.ico">
8 8
9 - <!-- Styles ---------------------------------------------------------- -->  
10 - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"  
11 - integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" />  
12 - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />  
13 - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />  
14 - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" />  
15 - <!-- local styles -->  
16 - <link rel="stylesheet" href="{{static_url('css/github.css')}}" />  
17 - <link rel="stylesheet" href="{{static_url('css/topic.css')}}" /> 9 + {% include include-libs.html %}
18 10
19 - <!-- Scripts --------------------------------------------------------- --> 11 + <!-- mathjax -->
20 <script> 12 <script>
21 MathJax = { 13 MathJax = {
22 tex: { 14 tex: {
@@ -27,14 +19,19 @@ @@ -27,14 +19,19 @@
27 } 19 }
28 }; 20 };
29 </script> 21 </script>
  22 + <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
30 <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> 23 <script async type="text/javascript" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
31 - <script defer src="https://code.jquery.com/jquery-3.6.0.min.js"  
32 - integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>  
33 - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"  
34 - integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> 24 +
  25 + <!-- codemirror -->
  26 + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.css" />
35 <script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script> 27 <script defer src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"></script>
36 28
37 - <!-- local scripts --> 29 + <!-- animate -->
  30 + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
  31 +
  32 + <!-- local -->
  33 + <link rel="stylesheet" href="{{static_url('css/github.css')}}" />
  34 + <link rel="stylesheet" href="{{static_url('css/topic.css')}}" />
38 <script defer src="{{static_url('js/topic.js')}}"></script> 35 <script defer src="{{static_url('js/topic.js')}}"></script>
39 36
40 <title>{{appname}}</title> 37 <title>{{appname}}</title>
@@ -58,13 +55,12 @@ @@ -58,13 +55,12 @@
58 <ul class="navbar-nav"> 55 <ul class="navbar-nav">
59 <li class="nav-item"><a class="nav-link" href="/courses">Cursos</a></li> 56 <li class="nav-item"><a class="nav-link" href="/courses">Cursos</a></li>
60 <li class="nav-item"><a class="nav-link active" aria-current="page" href="/course/{{course_id}}">Tópicos</a></li> 57 <li class="nav-item"><a class="nav-link active" aria-current="page" href="/course/{{course_id}}">Tópicos</a></li>
61 - <!-- <li class="nav-item"><a class="nav-link disabled" href="#">Classificação</a></li> --> 58 + <li class="nav-item"><a class="nav-link disabled" href="#">Classificação</a></li>
62 </ul> 59 </ul>
63 <ul class="navbar-nav ms-auto"> 60 <ul class="navbar-nav ms-auto">
64 <li class="nav-item dropdown"> 61 <li class="nav-item dropdown">
65 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> 62 <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
66 - <i class="fas fa-user-graduate" aria-hidden="true"></i>  
67 - &nbsp; 63 + <i class="bi bi-person-circle"></i>&nbsp;
68 <span id="name">{{ escape(name) }}</span> 64 <span id="name">{{ escape(name) }}</span>
69 <span class="caret"></span> 65 <span class="caret"></span>
70 </a> 66 </a>
demo/courses.yaml
@@ -15,6 +15,7 @@ courses: @@ -15,6 +15,7 @@ courses:
15 description: | 15 description: |
16 Adição, multiplicação e números primos. 16 Adição, multiplicação e números primos.
17 goals: 17 goals:
  18 + - math
18 - math/addition 19 - math/addition
19 - math/multiplication 20 - math/multiplication
20 - math/prime-numbers 21 - math/prime-numbers
demo/math.yaml
@@ -25,7 +25,23 @@ topics: @@ -25,7 +25,23 @@ topics:
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 +
28 math/prime-numbers: 35 math/prime-numbers:
29 name: Números primos 36 name: Números primos
30 deps: 37 deps:
  38 + - math/learn-prime-numbers
  39 +
  40 + math:
  41 + name: Números e operações aritméticas
  42 + type: chapter
  43 + deps:
  44 + - math/addition
31 - math/multiplication 45 - math/multiplication
  46 + - math/learn-prime-numbers
  47 + - math/prime-numbers
demo/math/learn-prime-numbers/learn.yaml 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +- type: radio
  2 + title: O que são números primos?
  3 + text: |
  4 + Um número $n \ge 2$ é primo se os únicos divisores forem $1$ e $n$.
  5 +
  6 + Por exemplo, o número $5$ é primo, pois $2,3,4$ não são divisores de $5$.
  7 +
  8 + ---
  9 +
  10 + O número $1$ é primo?
  11 + options:
  12 + - Não
  13 + - Sim
  14 + max_tries: 1
  15 + solution: |
  16 + O número $1$ não é primo. Apenas podem ser primos os números a partir do
  17 + $2$.
demo/math/questions.yaml 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +- type: info
  2 + title: Fim do capítulo
1 [mypy] 1 [mypy]
2 -python_version = 3.9 2 +python_version = 3.10
3 plugins = sqlalchemy.ext.mypy.plugin 3 plugins = sqlalchemy.ext.mypy.plugin
4 4
5 ; [mypy-pygments.*] 5 ; [mypy-pygments.*]
@@ -18,10 +18,10 @@ setup( @@ -18,10 +18,10 @@ setup(
18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", 18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git",
19 packages=find_packages(), 19 packages=find_packages(),
20 include_package_data=True, # install files from MANIFEST.in 20 include_package_data=True, # install files from MANIFEST.in
21 - python_requires='>=3.8.*', 21 + python_requires='>=3.9.*',
22 install_requires=[ 22 install_requires=[
23 'tornado>=6.0', 23 'tornado>=6.0',
24 - 'mistune', 24 + 'mistune<2',
25 'pyyaml>=5.1', 25 'pyyaml>=5.1',
26 'pygments', 26 'pygments',
27 'sqlalchemy>=1.4', 27 'sqlalchemy>=1.4',
@@ -32,7 +32,6 @@ setup( @@ -32,7 +32,6 @@ setup(
32 'console_scripts': [ 32 'console_scripts': [
33 'aprendizations = aprendizations.main:main', 33 'aprendizations = aprendizations.main:main',
34 'initdb-aprendizations = aprendizations.initdb:main', 34 'initdb-aprendizations = aprendizations.initdb:main',
35 - # 'redirect = aprendizations.redirect:main',  
36 ] 35 ]
37 }, 36 },
38 classifiers=[ 37 classifiers=[