Commit 5f3daeeb3ac4e298da47a9e570544b0786861a36

Authored by Miguel Barão
1 parent 2a25e542
Exists in master and in 1 other branch dev

add lots of type annotations

1 1
2 # BUGS 2 # BUGS
3 3
  4 +- marking all options right in a radio question breaks!
  5 +- impedir que quando students.db não é encontrado, crie um ficheiro vazio.
  6 +- opcao --prefix devia afectar a base de dados?
4 - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks 7 - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks
5 - quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) 8 - quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo)
6 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) 9 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
aprendizations/learnapp.py
@@ -6,7 +6,7 @@ from contextlib import contextmanager # `with` statement in db sessions @@ -6,7 +6,7 @@ from contextlib import contextmanager # `with` statement in db sessions
6 import asyncio 6 import asyncio
7 from datetime import datetime 7 from datetime import datetime
8 from random import random 8 from random import random
9 -from typing import Dict 9 +from typing import Any, Dict, Iterable, List, Optional, Tuple
10 10
11 # third party libraries 11 # third party libraries
12 import bcrypt 12 import bcrypt
@@ -16,7 +16,7 @@ import networkx as nx @@ -16,7 +16,7 @@ import networkx as nx
16 # this project 16 # this project
17 from .models import Student, Answer, Topic, StudentTopic 17 from .models import Student, Answer, Topic, StudentTopic
18 from .student import StudentState 18 from .student import StudentState
19 -from .questions import QFactory 19 +from .questions import Question, QFactory, QDict
20 from .tools import load_yaml 20 from .tools import load_yaml
21 21
22 # setup logger for this module 22 # setup logger for this module
@@ -56,16 +56,21 @@ class LearnApp(object): @@ -56,16 +56,21 @@ class LearnApp(object):
56 # ------------------------------------------------------------------------ 56 # ------------------------------------------------------------------------
57 # init 57 # init
58 # ------------------------------------------------------------------------ 58 # ------------------------------------------------------------------------
59 - def __init__(self, config_files, prefix, db, check=False): 59 + def __init__(self,
  60 + config_files: List[str],
  61 + prefix: str, # path to topics
  62 + db: str, # database filename
  63 + check: bool = False) -> None:
  64 +
60 self.db_setup(db) # setup database and check students 65 self.db_setup(db) # setup database and check students
61 - self.online = dict() # online students 66 + self.online: Dict[str, Dict] = dict() # online students
62 67
63 self.deps = nx.DiGraph(prefix=prefix) 68 self.deps = nx.DiGraph(prefix=prefix)
64 for c in config_files: 69 for c in config_files:
65 self.populate_graph(c) 70 self.populate_graph(c)
66 71
67 # factory is a dict with question generators for all topics 72 # factory is a dict with question generators for all topics
68 - self.factory = self.make_factory() # {'qref': QFactory()} 73 + self.factory: Dict[str, QFactory] = self.make_factory()
69 74
70 # if graph has topics that are not in the database, add them 75 # if graph has topics that are not in the database, add them
71 self.db_add_missing_topics(self.deps.nodes()) 76 self.db_add_missing_topics(self.deps.nodes())
@@ -74,10 +79,10 @@ class LearnApp(object): @@ -74,10 +79,10 @@ class LearnApp(object):
74 self.sanity_check_questions() 79 self.sanity_check_questions()
75 80
76 # ------------------------------------------------------------------------ 81 # ------------------------------------------------------------------------
77 - def sanity_check_questions(self): 82 + def sanity_check_questions(self) -> None:
78 logger.info('Starting sanity checks (may take a while...)') 83 logger.info('Starting sanity checks (may take a while...)')
79 84
80 - errors = 0 85 + errors: int = 0
81 for qref in self.factory: 86 for qref in self.factory:
82 logger.debug(f'checking {qref}...') 87 logger.debug(f'checking {qref}...')
83 try: 88 try:
@@ -114,7 +119,8 @@ class LearnApp(object): @@ -114,7 +119,8 @@ class LearnApp(object):
114 # ------------------------------------------------------------------------ 119 # ------------------------------------------------------------------------
115 # login 120 # login
116 # ------------------------------------------------------------------------ 121 # ------------------------------------------------------------------------
117 - async def login(self, uid, pw): 122 + async def login(self, uid: str, pw: str) -> bool:
  123 +
118 with self.db_session() as s: 124 with self.db_session() as s:
119 found = s.query(Student.name, Student.password) \ 125 found = s.query(Student.name, Student.password) \
120 .filter_by(id=uid) \ 126 .filter_by(id=uid) \
@@ -168,14 +174,14 @@ class LearnApp(object): @@ -168,14 +174,14 @@ class LearnApp(object):
168 # ------------------------------------------------------------------------ 174 # ------------------------------------------------------------------------
169 # logout 175 # logout
170 # ------------------------------------------------------------------------ 176 # ------------------------------------------------------------------------
171 - def logout(self, uid): 177 + def logout(self, uid: str) -> None:
172 del self.online[uid] 178 del self.online[uid]
173 logger.info(f'User "{uid}" logged out') 179 logger.info(f'User "{uid}" logged out')
174 180
175 # ------------------------------------------------------------------------ 181 # ------------------------------------------------------------------------
176 # change_password. returns True if password is successfully changed. 182 # change_password. returns True if password is successfully changed.
177 # ------------------------------------------------------------------------ 183 # ------------------------------------------------------------------------
178 - async def change_password(self, uid, pw): 184 + async def change_password(self, uid: str, pw: str) -> bool:
179 if not pw: 185 if not pw:
180 return False 186 return False
181 187
@@ -191,12 +197,14 @@ class LearnApp(object): @@ -191,12 +197,14 @@ class LearnApp(object):
191 return True 197 return True
192 198
193 # ------------------------------------------------------------------------ 199 # ------------------------------------------------------------------------
194 - # checks answer (updating student state) and returns grade. 200 + # checks answer (updating student state) and returns grade. FIXME type of answer
195 # ------------------------------------------------------------------------ 201 # ------------------------------------------------------------------------
196 - async def check_answer(self, uid, answer): 202 + async def check_answer(self, uid: str, answer) -> Tuple[Question, str]:
197 knowledge = self.online[uid]['state'] 203 knowledge = self.online[uid]['state']
198 topic = knowledge.get_current_topic() 204 topic = knowledge.get_current_topic()
  205 +
199 q, action = await knowledge.check_answer(answer) # may move questions 206 q, action = await knowledge.check_answer(answer) # may move questions
  207 +
200 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"') 208 logger.info(f'User "{uid}" got {q["grade"]:.2} in "{q["ref"]}"')
201 209
202 # always save grade of answered question 210 # always save grade of answered question
@@ -213,8 +221,8 @@ class LearnApp(object): @@ -213,8 +221,8 @@ class LearnApp(object):
213 if knowledge.topic_has_finished(): 221 if knowledge.topic_has_finished():
214 # finished topic, save into database 222 # finished topic, save into database
215 logger.info(f'User "{uid}" finished "{topic}"') 223 logger.info(f'User "{uid}" finished "{topic}"')
216 - level = knowledge.get_topic_level(topic)  
217 - date = str(knowledge.get_topic_date(topic)) 224 + level: float = knowledge.get_topic_level(topic)
  225 + date: str = str(knowledge.get_topic_date(topic))
218 226
219 with self.db_session() as s: 227 with self.db_session() as s:
220 a = s.query(StudentTopic) \ 228 a = s.query(StudentTopic) \
@@ -242,19 +250,19 @@ class LearnApp(object): @@ -242,19 +250,19 @@ class LearnApp(object):
242 # ------------------------------------------------------------------------ 250 # ------------------------------------------------------------------------
243 # Start new topic 251 # Start new topic
244 # ------------------------------------------------------------------------ 252 # ------------------------------------------------------------------------
245 - async def start_topic(self, uid, topic): 253 + async def start_topic(self, uid: str, topic: str) -> None:
246 student = self.online[uid]['state'] 254 student = self.online[uid]['state']
247 try: 255 try:
248 await student.start_topic(topic) 256 await student.start_topic(topic)
249 except Exception as e: 257 except Exception as e:
250 - logger.warning(f'User "{uid}" couldn\'t start "{topic}": {e}') 258 + logger.warning(f'User "{uid}" could not start "{topic}": {e}')
251 else: 259 else:
252 logger.info(f'User "{uid}" started topic "{topic}"') 260 logger.info(f'User "{uid}" started topic "{topic}"')
253 261
254 # ------------------------------------------------------------------------ 262 # ------------------------------------------------------------------------
255 # Fill db table 'Topic' with topics from the graph if not already there. 263 # Fill db table 'Topic' with topics from the graph if not already there.
256 # ------------------------------------------------------------------------ 264 # ------------------------------------------------------------------------
257 - def db_add_missing_topics(self, topics): 265 + def db_add_missing_topics(self, topics: List[str]) -> None:
258 with self.db_session() as s: 266 with self.db_session() as s:
259 new_topics = [Topic(id=t) for t in topics 267 new_topics = [Topic(id=t) for t in topics
260 if (t,) not in s.query(Topic.id)] 268 if (t,) not in s.query(Topic.id)]
@@ -267,15 +275,16 @@ class LearnApp(object): @@ -267,15 +275,16 @@ class LearnApp(object):
267 # ------------------------------------------------------------------------ 275 # ------------------------------------------------------------------------
268 # setup and check database contents 276 # setup and check database contents
269 # ------------------------------------------------------------------------ 277 # ------------------------------------------------------------------------
270 - def db_setup(self, db): 278 + def db_setup(self, db: str) -> None:
  279 +
271 logger.info(f'Checking database "{db}":') 280 logger.info(f'Checking database "{db}":')
272 engine = sa.create_engine(f'sqlite:///{db}', echo=False) 281 engine = sa.create_engine(f'sqlite:///{db}', echo=False)
273 self.Session = sa.orm.sessionmaker(bind=engine) 282 self.Session = sa.orm.sessionmaker(bind=engine)
274 try: 283 try:
275 with self.db_session() as s: 284 with self.db_session() as s:
276 - n = s.query(Student).count()  
277 - m = s.query(Topic).count()  
278 - q = s.query(Answer).count() 285 + n: int = s.query(Student).count()
  286 + m: int = s.query(Topic).count()
  287 + q: int = s.query(Answer).count()
279 except Exception: 288 except Exception:
280 logger.error(f'Database "{db}" not usable!') 289 logger.error(f'Database "{db}" not usable!')
281 raise DatabaseUnusableError() 290 raise DatabaseUnusableError()
@@ -293,21 +302,22 @@ class LearnApp(object): @@ -293,21 +302,22 @@ class LearnApp(object):
293 # 302 #
294 # Edges are obtained from the deps defined in the YAML file for each topic. 303 # Edges are obtained from the deps defined in the YAML file for each topic.
295 # ------------------------------------------------------------------------ 304 # ------------------------------------------------------------------------
296 - def populate_graph(self, conffile: str): 305 + def populate_graph(self, conffile: str) -> None:
  306 +
297 logger.info(f'Populating graph from: {conffile}...') 307 logger.info(f'Populating graph from: {conffile}...')
298 - config = load_yaml(conffile) # course configuration 308 + config: Dict[str, Any] = load_yaml(conffile) # course configuration
299 309
300 # default attributes that apply to the topics 310 # default attributes that apply to the topics
301 - default_file = config.get('file', 'questions.yaml')  
302 - default_shuffle_questions = config.get('shuffle_questions', True)  
303 - default_choose = config.get('choose', 9999)  
304 - default_forgetting_factor = config.get('forgetting_factor', 1.0)  
305 - default_maxtries = config.get('max_tries', 3)  
306 - default_append_wrong = config.get('append_wrong', True)  
307 - default_min_level = config.get('min_level', 0.01) # to unlock topic 311 + default_file: str = config.get('file', 'questions.yaml')
  312 + default_shuffle_questions: bool = config.get('shuffle_questions', True)
  313 + default_choose: int = config.get('choose', 9999)
  314 + default_forgetting_factor: float = config.get('forgetting_factor', 1.0)
  315 + default_maxtries: int = config.get('max_tries', 3)
  316 + default_append_wrong: bool = config.get('append_wrong', True)
  317 + default_min_level: float = config.get('min_level', 0.01) # to unlock
308 318
309 # iterate over topics and populate graph 319 # iterate over topics and populate graph
310 - topics = config.get('topics', {}) 320 + topics: Dict[str, Dict] = config.get('topics', {})
311 g = self.deps # dependency graph 321 g = self.deps # dependency graph
312 322
313 g.add_nodes_from(topics.keys()) 323 g.add_nodes_from(topics.keys())
@@ -345,15 +355,18 @@ class LearnApp(object): @@ -345,15 +355,18 @@ class LearnApp(object):
345 # Buils dictionary of question factories 355 # Buils dictionary of question factories
346 # ------------------------------------------------------------------------ 356 # ------------------------------------------------------------------------
347 def make_factory(self) -> Dict[str, QFactory]: 357 def make_factory(self) -> Dict[str, QFactory]:
  358 +
348 logger.info('Building questions factory:') 359 logger.info('Building questions factory:')
349 - factory = {} # {'qref': QFactory()} 360 + factory: Dict[str, QFactory] = {}
350 g = self.deps 361 g = self.deps
351 for tref in g.nodes(): 362 for tref in g.nodes():
352 t = g.node[tref] 363 t = g.node[tref]
353 364
354 # load questions as list of dicts 365 # load questions as list of dicts
355 - topicpath = path.join(g.graph['prefix'], tref)  
356 - questions = load_yaml(path.join(topicpath, t['file']), default=[]) 366 + topicpath: str = path.join(g.graph['prefix'], tref)
  367 + fullpath: str = path.join(topicpath, t['file'])
  368 +
  369 + questions: List[QDict] = load_yaml(fullpath, default=[])
357 370
358 # update refs to include topic as prefix. 371 # update refs to include topic as prefix.
359 # refs are required to be unique only within the file. 372 # refs are required to be unique only within the file.
@@ -389,16 +402,16 @@ class LearnApp(object): @@ -389,16 +402,16 @@ class LearnApp(object):
389 return self.online[uid].get('name', '') 402 return self.online[uid].get('name', '')
390 403
391 # ------------------------------------------------------------------------ 404 # ------------------------------------------------------------------------
392 - def get_student_state(self, uid: str): 405 + def get_student_state(self, uid: str) -> List[Dict[str, Any]]:
393 return self.online[uid]['state'].get_knowledge_state() 406 return self.online[uid]['state'].get_knowledge_state()
394 407
395 # ------------------------------------------------------------------------ 408 # ------------------------------------------------------------------------
396 - def get_student_progress(self, uid: str): 409 + def get_student_progress(self, uid: str) -> float:
397 return self.online[uid]['state'].get_topic_progress() 410 return self.online[uid]['state'].get_topic_progress()
398 411
399 # ------------------------------------------------------------------------ 412 # ------------------------------------------------------------------------
400 - def get_current_question(self, uid: str):  
401 - return self.online[uid]['state'].get_current_question() # dict 413 + def get_current_question(self, uid: str) -> Optional[Question]:
  414 + return self.online[uid]['state'].get_current_question()
402 415
403 # ------------------------------------------------------------------------ 416 # ------------------------------------------------------------------------
404 def get_current_question_id(self, uid: str) -> str: 417 def get_current_question_id(self, uid: str) -> str:
@@ -410,27 +423,29 @@ class LearnApp(object): @@ -410,27 +423,29 @@ class LearnApp(object):
410 423
411 # ------------------------------------------------------------------------ 424 # ------------------------------------------------------------------------
412 def get_student_topic(self, uid: str) -> str: 425 def get_student_topic(self, uid: str) -> str:
413 - return self.online[uid]['state'].get_current_topic() # str 426 + return self.online[uid]['state'].get_current_topic()
414 427
415 # ------------------------------------------------------------------------ 428 # ------------------------------------------------------------------------
416 def get_title(self) -> str: 429 def get_title(self) -> str:
417 - return self.deps.graph.get('title', '') # FIXME 430 + return self.deps.graph.get('title', '')
418 431
419 # ------------------------------------------------------------------------ 432 # ------------------------------------------------------------------------
420 def get_topic_name(self, ref: str) -> str: 433 def get_topic_name(self, ref: str) -> str:
421 return self.deps.node[ref]['name'] 434 return self.deps.node[ref]['name']
422 435
423 # ------------------------------------------------------------------------ 436 # ------------------------------------------------------------------------
424 - def get_current_public_dir(self, uid):  
425 - topic = self.online[uid]['state'].get_current_topic()  
426 - prefix = self.deps.graph['prefix'] 437 + def get_current_public_dir(self, uid: str) -> str:
  438 + topic: str = self.online[uid]['state'].get_current_topic()
  439 + prefix: str = self.deps.graph['prefix']
427 return path.join(prefix, topic, 'public') 440 return path.join(prefix, topic, 'public')
428 441
429 # ------------------------------------------------------------------------ 442 # ------------------------------------------------------------------------
430 - def get_rankings(self, uid): 443 + def get_rankings(self, uid: str) -> Iterable[Tuple[str, str, float, float]]:
  444 +
431 logger.info(f'User "{uid}" get rankings') 445 logger.info(f'User "{uid}" get rankings')
432 with self.db_session() as s: 446 with self.db_session() as s:
433 students = s.query(Student.id, Student.name).all() 447 students = s.query(Student.id, Student.name).all()
  448 +
434 # topic progress 449 # topic progress
435 student_topics = s.query(StudentTopic.student_id, 450 student_topics = s.query(StudentTopic.student_id,
436 StudentTopic.topic_id, 451 StudentTopic.topic_id,
@@ -448,10 +463,11 @@ class LearnApp(object): @@ -448,10 +463,11 @@ class LearnApp(object):
448 all()) 463 all())
449 464
450 # compute percentage of right answers 465 # compute percentage of right answers
451 - perf = {uid: right.get(uid, 0.0)/total[uid] for uid in total} 466 + perf: Dict[str, float] = {uid: right.get(uid, 0.0)/total[uid]
  467 + for uid in total}
452 468
453 # compute topic progress 469 # compute topic progress
454 - prog = {s[0]: 0.0 for s in students} 470 + prog: Dict[str, float] = {s[0]: 0.0 for s in students}
455 now = datetime.now() 471 now = datetime.now()
456 for uid, topic, level, date in student_topics: 472 for uid, topic, level, date in student_topics:
457 date = datetime.strptime(date, "%Y-%m-%d %H:%M:%S.%f") 473 date = datetime.strptime(date, "%Y-%m-%d %H:%M:%S.%f")
aprendizations/main.py
@@ -6,6 +6,7 @@ import logging @@ -6,6 +6,7 @@ import logging
6 from os import environ, path 6 from os import environ, path
7 import ssl 7 import ssl
8 import sys 8 import sys
  9 +from typing import Any, Dict
9 10
10 # this project 11 # this project
11 from .learnapp import LearnApp, DatabaseUnusableError 12 from .learnapp import LearnApp, DatabaseUnusableError
@@ -61,7 +62,7 @@ def parse_cmdline_arguments(): @@ -61,7 +62,7 @@ def parse_cmdline_arguments():
61 62
62 63
63 # ---------------------------------------------------------------------------- 64 # ----------------------------------------------------------------------------
64 -def get_logger_config(debug=False): 65 +def get_logger_config(debug: bool = False) -> Dict[str, Any]:
65 if debug: 66 if debug:
66 filename, level = 'logger-debug.yaml', 'DEBUG' 67 filename, level = 'logger-debug.yaml', 'DEBUG'
67 else: 68 else:
@@ -70,7 +71,7 @@ def get_logger_config(debug=False): @@ -70,7 +71,7 @@ def get_logger_config(debug=False):
70 config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/') 71 config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
71 config_file = path.join(path.expanduser(config_dir), APP_NAME, filename) 72 config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)
72 73
73 - default_config = { 74 + default_config: Dict[str, Any] = {
74 'version': 1, 75 'version': 1,
75 'formatters': { 76 'formatters': {
76 'standard': { 77 'standard': {
aprendizations/serve.py
@@ -8,6 +8,7 @@ import mimetypes @@ -8,6 +8,7 @@ import mimetypes
8 from os import path 8 from os import path
9 import signal 9 import signal
10 import sys 10 import sys
  11 +from typing import List, Optional, Tuple, Union
11 import uuid 12 import uuid
12 13
13 # third party libraries 14 # third party libraries
@@ -39,6 +40,7 @@ def admin_only(func): @@ -39,6 +40,7 @@ def admin_only(func):
39 # WebApplication - Tornado Web Server 40 # WebApplication - Tornado Web Server
40 # ============================================================================ 41 # ============================================================================
41 class WebApplication(tornado.web.Application): 42 class WebApplication(tornado.web.Application):
  43 +
42 def __init__(self, learnapp, debug=False): 44 def __init__(self, learnapp, debug=False):
43 handlers = [ 45 handlers = [
44 (r'/login', LoginHandler), 46 (r'/login', LoginHandler),
@@ -304,6 +306,7 @@ class QuestionHandler(BaseHandler): @@ -304,6 +306,7 @@ class QuestionHandler(BaseHandler):
304 306
305 # --- answers are in a list. fix depending on question type 307 # --- answers are in a list. fix depending on question type
306 qtype = self.learn.get_student_question_type(user) 308 qtype = self.learn.get_student_question_type(user)
  309 + ans: Optional[Union[List, str]]
307 if qtype in ('success', 'information', 'info'): 310 if qtype in ('success', 'information', 'info'):
308 ans = None 311 ans = None
309 elif qtype == 'radio' and not answer: 312 elif qtype == 'radio' and not answer:
@@ -365,7 +368,7 @@ class QuestionHandler(BaseHandler): @@ -365,7 +368,7 @@ class QuestionHandler(BaseHandler):
365 # ---------------------------------------------------------------------------- 368 # ----------------------------------------------------------------------------
366 # Signal handler to catch Ctrl-C and abort server 369 # Signal handler to catch Ctrl-C and abort server
367 # ---------------------------------------------------------------------------- 370 # ----------------------------------------------------------------------------
368 -def signal_handler(signal, frame): 371 +def signal_handler(signal, frame) -> None:
369 r = input(' --> Stop webserver? (yes/no) ').lower() 372 r = input(' --> Stop webserver? (yes/no) ').lower()
370 if r == 'yes': 373 if r == 'yes':
371 tornado.ioloop.IOLoop.current().stop() 374 tornado.ioloop.IOLoop.current().stop()
@@ -376,7 +379,11 @@ def signal_handler(signal, frame): @@ -376,7 +379,11 @@ def signal_handler(signal, frame):
376 379
377 380
378 # ---------------------------------------------------------------------------- 381 # ----------------------------------------------------------------------------
379 -def run_webserver(app, ssl, port=8443, debug=False): 382 +def run_webserver(app,
  383 + ssl,
  384 + port: int = 8443,
  385 + debug: bool = False) -> None:
  386 +
380 # --- create web application 387 # --- create web application
381 try: 388 try:
382 webapp = WebApplication(app, debug=debug) 389 webapp = WebApplication(app, debug=debug)
aprendizations/student.py
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 import random 3 import random
4 from datetime import datetime 4 from datetime import datetime
5 import logging 5 import logging
6 -from typing import List, Optional 6 +from typing import List, Optional, Tuple
7 7
8 # third party libraries 8 # third party libraries
9 import networkx as nx 9 import networkx as nx
@@ -28,20 +28,21 @@ class StudentState(object): @@ -28,20 +28,21 @@ class StudentState(object):
28 # ======================================================================= 28 # =======================================================================
29 # methods that update state 29 # methods that update state
30 # ======================================================================= 30 # =======================================================================
31 - def __init__(self, deps, factory, state={}): 31 + def __init__(self, deps, factory, state={}) -> None:
32 self.deps = deps # shared dependency graph 32 self.deps = deps # shared dependency graph
33 self.factory = factory # question factory 33 self.factory = factory # question factory
34 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} 34 self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...}
35 35
36 self.update_topic_levels() # applies forgetting factor 36 self.update_topic_levels() # applies forgetting factor
37 self.unlock_topics() # whose dependencies have been completed 37 self.unlock_topics() # whose dependencies have been completed
38 - self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...]  
39 - self.current_topic = None 38 +
  39 + self.topic_sequence: List[str] = self.recommend_topic_sequence()
  40 + self.current_topic: Optional[str] = None
40 41
41 # ------------------------------------------------------------------------ 42 # ------------------------------------------------------------------------
42 # Updates the proficiency levels of the topics, with forgetting factor 43 # Updates the proficiency levels of the topics, with forgetting factor
43 # ------------------------------------------------------------------------ 44 # ------------------------------------------------------------------------
44 - def update_topic_levels(self): 45 + def update_topic_levels(self) -> None:
45 now = datetime.now() 46 now = datetime.now()
46 for tref, s in self.state.items(): 47 for tref, s in self.state.items():
47 dt = now - s['date'] 48 dt = now - s['date']
@@ -51,7 +52,7 @@ class StudentState(object): @@ -51,7 +52,7 @@ class StudentState(object):
51 # ------------------------------------------------------------------------ 52 # ------------------------------------------------------------------------
52 # Unlock topics whose dependencies are satisfied (> min_level) 53 # Unlock topics whose dependencies are satisfied (> min_level)
53 # ------------------------------------------------------------------------ 54 # ------------------------------------------------------------------------
54 - def unlock_topics(self): 55 + def unlock_topics(self) -> None:
55 for topic in self.deps.nodes(): 56 for topic in self.deps.nodes():
56 if topic not in self.state: # if locked 57 if topic not in self.state: # if locked
57 pred = self.deps.predecessors(topic) 58 pred = self.deps.predecessors(topic)
@@ -72,8 +73,8 @@ class StudentState(object): @@ -72,8 +73,8 @@ class StudentState(object):
72 # questions: list of generated questions to do in the topic 73 # questions: list of generated questions to do in the topic
73 # current_question: the current question to be presented 74 # current_question: the current question to be presented
74 # ------------------------------------------------------------------------ 75 # ------------------------------------------------------------------------
75 - async def start_topic(self, topic: str):  
76 - logger.debug(f'starting "{topic}"') 76 + async def start_topic(self, topic: str) -> None:
  77 + logger.debug(f'start topic "{topic}"')
77 78
78 # avoid regenerating questions in the middle of the current topic 79 # avoid regenerating questions in the middle of the current topic
79 if self.current_topic == topic: 80 if self.current_topic == topic:
@@ -112,7 +113,7 @@ class StudentState(object): @@ -112,7 +113,7 @@ class StudentState(object):
112 # The topic level is updated in state and unlocks are performed. 113 # The topic level is updated in state and unlocks are performed.
113 # The current topic is unchanged. 114 # The current topic is unchanged.
114 # ------------------------------------------------------------------------ 115 # ------------------------------------------------------------------------
115 - def finish_topic(self): 116 + def finish_topic(self) -> None:
116 logger.debug(f'finished {self.current_topic}') 117 logger.debug(f'finished {self.current_topic}')
117 118
118 self.state[self.current_topic] = { 119 self.state[self.current_topic] = {
@@ -129,8 +130,8 @@ class StudentState(object): @@ -129,8 +130,8 @@ class StudentState(object):
129 # - if answer ok, goes to next question 130 # - if answer ok, goes to next question
130 # - if wrong, counts number of tries. If exceeded, moves on. 131 # - if wrong, counts number of tries. If exceeded, moves on.
131 # ------------------------------------------------------------------------ 132 # ------------------------------------------------------------------------
132 - async def check_answer(self, answer):  
133 - q = self.current_question 133 + async def check_answer(self, answer) -> Tuple[Question, str]:
  134 + q: Question = self.current_question
134 q['answer'] = answer 135 q['answer'] = answer
135 q['finish_time'] = datetime.now() 136 q['finish_time'] = datetime.now()
136 logger.debug(f'checking answer of {q["ref"]}...') 137 logger.debug(f'checking answer of {q["ref"]}...')