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
BUGS.md
1 | 1 | |
2 | 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 | 7 | - double click submits twice. |
6 | 8 | - nao esta a seguir o max_tries definido no ficheiro de dependencias. |
7 | -- obter rankings por curso GET course=course_id | |
8 | 9 | - impedir que quando students.db não é encontrado, crie um ficheiro vazio. |
9 | 10 | - classificacoes so devia mostrar os que ja fizeram alguma coisa |
10 | 11 | - QFactory.generate() devia fazer run da gen_async, ou remover. | ... | ... |
aprendizations/__init__.py
aprendizations/learnapp.py
... | ... | @@ -67,18 +67,24 @@ class LearnApp(object): |
67 | 67 | |
68 | 68 | config: Dict[str, Any] = load_yaml(courses) |
69 | 69 | |
70 | - # courses dict | |
70 | + # --- courses dict | |
71 | 71 | self.courses = config['courses'] |
72 | 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 | 75 | self.deps = nx.DiGraph(prefix=prefix) |
76 | 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 | 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 | 88 | self.factory: Dict[str, QFactory] = self.make_factory() |
83 | 89 | |
84 | 90 | # if graph has topics that are not in the database, add them |
... | ... | @@ -322,29 +328,30 @@ class LearnApp(object): |
322 | 328 | # Populates a digraph. |
323 | 329 | # |
324 | 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 | 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 | 350 | # iterate over topics and populate graph |
343 | 351 | topics: Dict[str, Dict] = config.get('topics', {}) |
344 | - g = self.deps # dependency graph | |
345 | - | |
346 | 352 | g.add_nodes_from(topics.keys()) |
347 | 353 | for tref, attr in topics.items(): |
354 | + logger.debug(f' + {tref}...') | |
348 | 355 | for d in attr.get('deps', []): |
349 | 356 | if d not in g.nodes(): |
350 | 357 | logger.error(f'Topic "{tref}" depends on "{d}" but it ' |
... | ... | @@ -353,22 +360,15 @@ class LearnApp(object): |
353 | 360 | else: |
354 | 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 | 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 | 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 | 374 | # methods that do not change state (pure functions) |
... | ... | @@ -378,12 +378,11 @@ class LearnApp(object): |
378 | 378 | # Buils dictionary of question factories |
379 | 379 | # ------------------------------------------------------------------------ |
380 | 380 | def make_factory(self) -> Dict[str, QFactory]: |
381 | - | |
382 | 381 | logger.info('Building questions factory:') |
383 | 382 | factory: Dict[str, QFactory] = {} |
384 | 383 | g = self.deps |
385 | 384 | for tref in g.nodes(): |
386 | - t = g.node[tref] | |
385 | + t = g.nodes[tref] | |
387 | 386 | |
388 | 387 | # load questions as list of dicts |
389 | 388 | topicpath: str = path.join(g.graph['prefix'], tref) |
... | ... | @@ -457,13 +456,8 @@ class LearnApp(object): |
457 | 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 | 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 | 463 | def get_current_public_dir(self, uid: str) -> str: | ... | ... |
aprendizations/student.py
... | ... | @@ -53,7 +53,7 @@ class StudentState(object): |
53 | 53 | else: |
54 | 54 | logger.debug(f'starting course {course}') |
55 | 55 | self.current_course = course |
56 | - topics = self.courses[course]['topics'] | |
56 | + topics = self.courses[course]['goals'] | |
57 | 57 | self.topic_sequence = self.recommend_topic_sequence(topics) |
58 | 58 | |
59 | 59 | # ------------------------------------------------------------------------ |
... | ... | @@ -79,7 +79,7 @@ class StudentState(object): |
79 | 79 | self.correct_answers = 0 |
80 | 80 | self.wrong_answers = 0 |
81 | 81 | |
82 | - t = self.deps.node[topic] | |
82 | + t = self.deps.nodes[topic] | |
83 | 83 | k = t['choose'] |
84 | 84 | if t['shuffle_questions']: |
85 | 85 | questions = random.sample(t['questions'], k=k) |
... | ... | @@ -174,7 +174,7 @@ class StudentState(object): |
174 | 174 | now = datetime.now() |
175 | 175 | for tref, s in self.state.items(): |
176 | 176 | dt = now - s['date'] |
177 | - forgetting_factor = self.deps.node[tref]['forgetting_factor'] | |
177 | + forgetting_factor = self.deps.nodes[tref]['forgetting_factor'] | |
178 | 178 | s['level'] *= forgetting_factor ** dt.days # forgetting factor |
179 | 179 | |
180 | 180 | # ------------------------------------------------------------------------ |
... | ... | @@ -184,7 +184,7 @@ class StudentState(object): |
184 | 184 | for topic in self.deps.nodes(): |
185 | 185 | if topic not in self.state: # if locked |
186 | 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 | 188 | if all(d in self.state and self.state[d]['level'] > min_level |
189 | 189 | for d in pred): # all deps are greater than min_level |
190 | 190 | |
... | ... | @@ -206,12 +206,12 @@ class StudentState(object): |
206 | 206 | # ------------------------------------------------------------------------ |
207 | 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 | 210 | G = self.deps |
211 | - ts = set(targets) | |
212 | - for t in targets: | |
211 | + ts = set(goals) | |
212 | + for t in goals: | |
213 | 213 | ts.update(nx.ancestors(G, t)) |
214 | - | |
214 | + # FIXME filter by level done, only level < 50% are included | |
215 | 215 | tl = list(nx.topological_sort(G.subgraph(ts))) |
216 | 216 | |
217 | 217 | # sort with unlocked first | ... | ... |
setup.py
... | ... | @@ -20,8 +20,13 @@ setup( |
20 | 20 | include_package_data=True, # install files from MANIFEST.in |
21 | 21 | python_requires='>=3.7.*', |
22 | 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 | 31 | entry_points={ |
27 | 32 | 'console_scripts': [ | ... | ... |