Commit e8e453f8ecc1d25e9c555057b0596f94b05a9ce0
1 parent
3dd184c9
Exists in
master
and in
1 other branch
- fixed to work with multiple courses.
- set supported versions of python packages. - changes key 'topics' to 'goals' when setting a course. - fix use of deprecated 'g.node[]' from networkx.
Showing
6 changed files
with
58 additions
and
55 deletions
Show diff stats
.gitignore
@@ -3,4 +3,7 @@ | @@ -3,4 +3,7 @@ | ||
3 | /aprendizations/__pycache__/ | 3 | /aprendizations/__pycache__/ |
4 | /demo/students.db | 4 | /demo/students.db |
5 | /node_modules/ | 5 | /node_modules/ |
6 | -/.mypy_cache/ | ||
7 | \ No newline at end of file | 6 | \ No newline at end of file |
7 | +/.mypy_cache/ | ||
8 | +.DS_Store | ||
9 | +demo/.DS_Store | ||
10 | +demo/solar_system/.DS_Store |
BUGS.md
1 | 1 | ||
2 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | -- não suporta definition lists em markdown | 4 | +- obter rankings por curso GET course=course_id |
5 | +- indicar qtos topicos faltam (>=50%) para terminar o curso. | ||
6 | +- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. | ||
5 | - double click submits twice. | 7 | - double click submits twice. |
6 | - nao esta a seguir o max_tries definido no ficheiro de dependencias. | 8 | - nao esta a seguir o max_tries definido no ficheiro de dependencias. |
7 | -- obter rankings por curso GET course=course_id | ||
8 | - impedir que quando students.db não é encontrado, crie um ficheiro vazio. | 9 | - impedir que quando students.db não é encontrado, crie um ficheiro vazio. |
9 | - classificacoes so devia mostrar os que ja fizeram alguma coisa | 10 | - classificacoes so devia mostrar os que ja fizeram alguma coisa |
10 | - QFactory.generate() devia fazer run da gen_async, ou remover. | 11 | - QFactory.generate() devia fazer run da gen_async, ou remover. |
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 = '2019.07.dev3' | 33 | +APP_VERSION = '2019.10.dev1' |
34 | APP_DESCRIPTION = __doc__ | 34 | APP_DESCRIPTION = __doc__ |
35 | 35 | ||
36 | __author__ = 'Miguel Barão' | 36 | __author__ = 'Miguel Barão' |
aprendizations/learnapp.py
@@ -67,18 +67,24 @@ class LearnApp(object): | @@ -67,18 +67,24 @@ class LearnApp(object): | ||
67 | 67 | ||
68 | config: Dict[str, Any] = load_yaml(courses) | 68 | config: Dict[str, Any] = load_yaml(courses) |
69 | 69 | ||
70 | - # courses dict | 70 | + # --- courses dict |
71 | self.courses = config['courses'] | 71 | self.courses = config['courses'] |
72 | logger.info(f'Courses: {", ".join(self.courses.keys())}') | 72 | logger.info(f'Courses: {", ".join(self.courses.keys())}') |
73 | 73 | ||
74 | - # topic dependencies are shared between all courses | 74 | + # --- topic dependencies are shared between all courses |
75 | self.deps = nx.DiGraph(prefix=prefix) | 75 | self.deps = nx.DiGraph(prefix=prefix) |
76 | logger.info('Populating graph:') | 76 | logger.info('Populating graph:') |
77 | - for f in config['deps']: | ||
78 | - self.populate_graph(f) | 77 | + |
78 | + t = config.get('topics', {}) # topics defined directly in courses file | ||
79 | + self.populate_graph(t) | ||
80 | + logger.info(f'{len(t):>6} topics in {courses}') | ||
81 | + for f in config.get('topics_from', []): | ||
82 | + c = load_yaml(f) # course configuration | ||
83 | + logger.info(f'{len(c["topics"]):>6} topics from {f}') | ||
84 | + self.populate_graph(c) | ||
79 | logger.info(f'Graph has {len(self.deps)} topics') | 85 | logger.info(f'Graph has {len(self.deps)} topics') |
80 | 86 | ||
81 | - # factory is a dict with question generators for all topics | 87 | + # --- factory is a dict with question generators for all topics |
82 | self.factory: Dict[str, QFactory] = self.make_factory() | 88 | self.factory: Dict[str, QFactory] = self.make_factory() |
83 | 89 | ||
84 | # if graph has topics that are not in the database, add them | 90 | # if graph has topics that are not in the database, add them |
@@ -322,29 +328,30 @@ class LearnApp(object): | @@ -322,29 +328,30 @@ class LearnApp(object): | ||
322 | # Populates a digraph. | 328 | # Populates a digraph. |
323 | # | 329 | # |
324 | # Nodes are the topic references e.g. 'my/topic' | 330 | # Nodes are the topic references e.g. 'my/topic' |
325 | - # g.node['my/topic']['name'] name of the topic | ||
326 | - # g.node['my/topic']['questions'] list of question refs | 331 | + # g.nodes['my/topic']['name'] name of the topic |
332 | + # g.nodes['my/topic']['questions'] list of question refs | ||
327 | # | 333 | # |
328 | # Edges are obtained from the deps defined in the YAML file for each topic. | 334 | # Edges are obtained from the deps defined in the YAML file for each topic. |
329 | # ------------------------------------------------------------------------ | 335 | # ------------------------------------------------------------------------ |
330 | - def populate_graph(self, conffile: str) -> None: | ||
331 | - config: Dict[str, Any] = load_yaml(conffile) # course configuration | ||
332 | - | ||
333 | - # default attributes that apply to the topics | ||
334 | - default_file: str = config.get('file', 'questions.yaml') | ||
335 | - default_shuffle_questions: bool = config.get('shuffle_questions', True) | ||
336 | - default_choose: int = config.get('choose', 9999) | ||
337 | - default_forgetting_factor: float = config.get('forgetting_factor', 1.0) | ||
338 | - default_maxtries: int = config.get('max_tries', 1) | ||
339 | - default_append_wrong: bool = config.get('append_wrong', True) | ||
340 | - default_min_level: float = config.get('min_level', 0.01) # to unlock | 336 | + def populate_graph(self, config: Dict[str, Any]) -> None: |
337 | + g = self.deps # dependency graph | ||
338 | + defaults = { | ||
339 | + 'type': 'topic', | ||
340 | + 'file': 'questions.yaml', | ||
341 | + 'shuffle_questions': True, | ||
342 | + 'choose': 9999, | ||
343 | + 'forgetting_factor': 1.0, # no forgetting | ||
344 | + 'max_tries': 1, # in every question | ||
345 | + 'append_wrong': True, | ||
346 | + 'min_level': 0.01, # to unlock topic | ||
347 | + } | ||
348 | + defaults.update(config.get('defaults', {})) | ||
341 | 349 | ||
342 | # iterate over topics and populate graph | 350 | # iterate over topics and populate graph |
343 | topics: Dict[str, Dict] = config.get('topics', {}) | 351 | topics: Dict[str, Dict] = config.get('topics', {}) |
344 | - g = self.deps # dependency graph | ||
345 | - | ||
346 | g.add_nodes_from(topics.keys()) | 352 | g.add_nodes_from(topics.keys()) |
347 | for tref, attr in topics.items(): | 353 | for tref, attr in topics.items(): |
354 | + logger.debug(f' + {tref}...') | ||
348 | for d in attr.get('deps', []): | 355 | for d in attr.get('deps', []): |
349 | if d not in g.nodes(): | 356 | if d not in g.nodes(): |
350 | logger.error(f'Topic "{tref}" depends on "{d}" but it ' | 357 | logger.error(f'Topic "{tref}" depends on "{d}" but it ' |
@@ -353,22 +360,15 @@ class LearnApp(object): | @@ -353,22 +360,15 @@ class LearnApp(object): | ||
353 | else: | 360 | else: |
354 | g.add_edge(d, tref) | 361 | g.add_edge(d, tref) |
355 | 362 | ||
356 | - t = g.node[tref] # get current topic node | ||
357 | - t['type'] = attr.get('type', 'topic') | 363 | + t = g.nodes[tref] # get current topic node |
358 | t['name'] = attr.get('name', tref) | 364 | t['name'] = attr.get('name', tref) |
359 | - t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic | ||
360 | - t['file'] = attr.get('file', default_file) # questions.yaml | ||
361 | - t['shuffle_questions'] = attr.get('shuffle_questions', | ||
362 | - default_shuffle_questions) | ||
363 | - t['max_tries'] = attr.get('max_tries', default_maxtries) | ||
364 | - t['forgetting_factor'] = attr.get('forgetting_factor', | ||
365 | - default_forgetting_factor) | ||
366 | - t['min_level'] = attr.get('min_level', default_min_level) | ||
367 | - t['choose'] = attr.get('choose', default_choose) | ||
368 | - t['append_wrong'] = attr.get('append_wrong', default_append_wrong) | ||
369 | t['questions'] = attr.get('questions', []) | 365 | t['questions'] = attr.get('questions', []) |
370 | 366 | ||
371 | - logger.info(f'{len(topics):>6} topics in {conffile}') | 367 | + for k, default in defaults.items(): |
368 | + t[k] = attr.get(k, default) | ||
369 | + | ||
370 | + t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic | ||
371 | + | ||
372 | 372 | ||
373 | # ======================================================================== | 373 | # ======================================================================== |
374 | # methods that do not change state (pure functions) | 374 | # methods that do not change state (pure functions) |
@@ -378,12 +378,11 @@ class LearnApp(object): | @@ -378,12 +378,11 @@ class LearnApp(object): | ||
378 | # Buils dictionary of question factories | 378 | # Buils dictionary of question factories |
379 | # ------------------------------------------------------------------------ | 379 | # ------------------------------------------------------------------------ |
380 | def make_factory(self) -> Dict[str, QFactory]: | 380 | def make_factory(self) -> Dict[str, QFactory]: |
381 | - | ||
382 | logger.info('Building questions factory:') | 381 | logger.info('Building questions factory:') |
383 | factory: Dict[str, QFactory] = {} | 382 | factory: Dict[str, QFactory] = {} |
384 | g = self.deps | 383 | g = self.deps |
385 | for tref in g.nodes(): | 384 | for tref in g.nodes(): |
386 | - t = g.node[tref] | 385 | + t = g.nodes[tref] |
387 | 386 | ||
388 | # load questions as list of dicts | 387 | # load questions as list of dicts |
389 | topicpath: str = path.join(g.graph['prefix'], tref) | 388 | topicpath: str = path.join(g.graph['prefix'], tref) |
@@ -457,13 +456,8 @@ class LearnApp(object): | @@ -457,13 +456,8 @@ class LearnApp(object): | ||
457 | return self.online[uid]['state'].get_current_course_id() | 456 | return self.online[uid]['state'].get_current_course_id() |
458 | 457 | ||
459 | # ------------------------------------------------------------------------ | 458 | # ------------------------------------------------------------------------ |
460 | - # def get_title(self) -> str: | ||
461 | - # # return self.deps.graph.get('title', '') | ||
462 | - # return self. | ||
463 | - | ||
464 | - # ------------------------------------------------------------------------ | ||
465 | def get_topic_name(self, ref: str) -> str: | 459 | def get_topic_name(self, ref: str) -> str: |
466 | - return self.deps.node[ref]['name'] | 460 | + return self.deps.nodes[ref]['name'] |
467 | 461 | ||
468 | # ------------------------------------------------------------------------ | 462 | # ------------------------------------------------------------------------ |
469 | def get_current_public_dir(self, uid: str) -> str: | 463 | def get_current_public_dir(self, uid: str) -> str: |
aprendizations/student.py
@@ -53,7 +53,7 @@ class StudentState(object): | @@ -53,7 +53,7 @@ class StudentState(object): | ||
53 | else: | 53 | else: |
54 | logger.debug(f'starting course {course}') | 54 | logger.debug(f'starting course {course}') |
55 | self.current_course = course | 55 | self.current_course = course |
56 | - topics = self.courses[course]['topics'] | 56 | + topics = self.courses[course]['goals'] |
57 | self.topic_sequence = self.recommend_topic_sequence(topics) | 57 | self.topic_sequence = self.recommend_topic_sequence(topics) |
58 | 58 | ||
59 | # ------------------------------------------------------------------------ | 59 | # ------------------------------------------------------------------------ |
@@ -79,7 +79,7 @@ class StudentState(object): | @@ -79,7 +79,7 @@ class StudentState(object): | ||
79 | self.correct_answers = 0 | 79 | self.correct_answers = 0 |
80 | self.wrong_answers = 0 | 80 | self.wrong_answers = 0 |
81 | 81 | ||
82 | - t = self.deps.node[topic] | 82 | + t = self.deps.nodes[topic] |
83 | k = t['choose'] | 83 | k = t['choose'] |
84 | if t['shuffle_questions']: | 84 | if t['shuffle_questions']: |
85 | questions = random.sample(t['questions'], k=k) | 85 | questions = random.sample(t['questions'], k=k) |
@@ -174,7 +174,7 @@ class StudentState(object): | @@ -174,7 +174,7 @@ class StudentState(object): | ||
174 | now = datetime.now() | 174 | now = datetime.now() |
175 | for tref, s in self.state.items(): | 175 | for tref, s in self.state.items(): |
176 | dt = now - s['date'] | 176 | dt = now - s['date'] |
177 | - forgetting_factor = self.deps.node[tref]['forgetting_factor'] | 177 | + forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] |
178 | s['level'] *= forgetting_factor ** dt.days # forgetting factor | 178 | s['level'] *= forgetting_factor ** dt.days # forgetting factor |
179 | 179 | ||
180 | # ------------------------------------------------------------------------ | 180 | # ------------------------------------------------------------------------ |
@@ -184,7 +184,7 @@ class StudentState(object): | @@ -184,7 +184,7 @@ class StudentState(object): | ||
184 | for topic in self.deps.nodes(): | 184 | for topic in self.deps.nodes(): |
185 | if topic not in self.state: # if locked | 185 | if topic not in self.state: # if locked |
186 | pred = self.deps.predecessors(topic) | 186 | pred = self.deps.predecessors(topic) |
187 | - min_level = self.deps.node[topic]['min_level'] | 187 | + min_level = self.deps.nodes[topic]['min_level'] |
188 | if all(d in self.state and self.state[d]['level'] > min_level | 188 | if all(d in self.state and self.state[d]['level'] > min_level |
189 | for d in pred): # all deps are greater than min_level | 189 | for d in pred): # all deps are greater than min_level |
190 | 190 | ||
@@ -206,12 +206,12 @@ class StudentState(object): | @@ -206,12 +206,12 @@ class StudentState(object): | ||
206 | # ------------------------------------------------------------------------ | 206 | # ------------------------------------------------------------------------ |
207 | # compute recommended sequence of topics ['a', 'b', ...] | 207 | # compute recommended sequence of topics ['a', 'b', ...] |
208 | # ------------------------------------------------------------------------ | 208 | # ------------------------------------------------------------------------ |
209 | - def recommend_topic_sequence(self, targets: List[str] = []) -> List[str]: | 209 | + def recommend_topic_sequence(self, goals: List[str] = []) -> List[str]: |
210 | G = self.deps | 210 | G = self.deps |
211 | - ts = set(targets) | ||
212 | - for t in targets: | 211 | + ts = set(goals) |
212 | + for t in goals: | ||
213 | ts.update(nx.ancestors(G, t)) | 213 | ts.update(nx.ancestors(G, t)) |
214 | - | 214 | + # FIXME filter by level done, only level < 50% are included |
215 | tl = list(nx.topological_sort(G.subgraph(ts))) | 215 | tl = list(nx.topological_sort(G.subgraph(ts))) |
216 | 216 | ||
217 | # sort with unlocked first | 217 | # sort with unlocked first |
setup.py
@@ -20,8 +20,13 @@ setup( | @@ -20,8 +20,13 @@ setup( | ||
20 | include_package_data=True, # install files from MANIFEST.in | 20 | include_package_data=True, # install files from MANIFEST.in |
21 | python_requires='>=3.7.*', | 21 | python_requires='>=3.7.*', |
22 | install_requires=[ | 22 | install_requires=[ |
23 | - 'tornado', 'mistune', 'pyyaml', 'pygments', 'sqlalchemy', 'bcrypt', | ||
24 | - 'networkx' | 23 | + 'tornado>=6.0', |
24 | + 'mistune', | ||
25 | + 'pyyaml>=5.1', | ||
26 | + 'pygments', | ||
27 | + 'sqlalchemy', | ||
28 | + 'bcrypt>=3.1', | ||
29 | + 'networkx>=2.4' | ||
25 | ], | 30 | ], |
26 | entry_points={ | 31 | entry_points={ |
27 | 'console_scripts': [ | 32 | 'console_scripts': [ |