Commit 43fecd172e1679eed85faa2b1ac009c45eda9172
1 parent
88531302
Exists in
master
and in
1 other branch
code cleaning and refactoring.
changed filename knowledge.py to student.py
Showing
4 changed files
with
248 additions
and
248 deletions
Show diff stats
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.05.dev3' | 33 | +APP_VERSION = '2019.07.dev1' |
34 | APP_DESCRIPTION = __doc__ | 34 | APP_DESCRIPTION = __doc__ |
35 | 35 | ||
36 | __author__ = 'Miguel Barão' | 36 | __author__ = 'Miguel Barão' |
aprendizations/knowledge.py
@@ -1,236 +0,0 @@ | @@ -1,236 +0,0 @@ | ||
1 | - | ||
2 | -# python standard library | ||
3 | -import random | ||
4 | -from datetime import datetime | ||
5 | -import logging | ||
6 | - | ||
7 | -# third party libraries | ||
8 | -import networkx as nx | ||
9 | - | ||
10 | -# setup logger for this module | ||
11 | -logger = logging.getLogger(__name__) | ||
12 | - | ||
13 | - | ||
14 | -# ---------------------------------------------------------------------------- | ||
15 | -# kowledge state of each student....?? | ||
16 | -# Contains: | ||
17 | -# state - dict of topics with state of unlocked topics | ||
18 | -# deps - access to dependency graph shared between students | ||
19 | -# topic_sequence - list with the order of recommended topics | ||
20 | -# ---------------------------------------------------------------------------- | ||
21 | -class StudentKnowledge(object): | ||
22 | - # ======================================================================= | ||
23 | - # methods that update state | ||
24 | - # ======================================================================= | ||
25 | - def __init__(self, deps, factory, state={}): | ||
26 | - self.deps = deps # shared dependency graph | ||
27 | - self.factory = factory # question factory | ||
28 | - self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} | ||
29 | - | ||
30 | - self.update_topic_levels() # applies forgetting factor | ||
31 | - self.unlock_topics() # whose dependencies have been completed | ||
32 | - self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...] | ||
33 | - self.current_topic = None | ||
34 | - | ||
35 | - # ------------------------------------------------------------------------ | ||
36 | - # Updates the proficiency levels of the topics, with forgetting factor | ||
37 | - # FIXME no dependencies are considered yet... | ||
38 | - # ------------------------------------------------------------------------ | ||
39 | - def update_topic_levels(self): | ||
40 | - now = datetime.now() | ||
41 | - for tref, s in self.state.items(): | ||
42 | - dt = now - s['date'] | ||
43 | - s['level'] *= 0.98 ** dt.days # forgetting factor | ||
44 | - | ||
45 | - # ------------------------------------------------------------------------ | ||
46 | - # Unlock topics whose dependencies are satisfied (> min_level) | ||
47 | - # ------------------------------------------------------------------------ | ||
48 | - def unlock_topics(self): | ||
49 | - for topic in self.deps.nodes(): | ||
50 | - if topic not in self.state: # if locked | ||
51 | - pred = self.deps.predecessors(topic) | ||
52 | - min_level = self.deps.node[topic]['min_level'] | ||
53 | - if all(d in self.state and self.state[d]['level'] > min_level | ||
54 | - for d in pred): # all deps are greater than min_level | ||
55 | - | ||
56 | - self.state[topic] = { | ||
57 | - 'level': 0.0, # unlocked | ||
58 | - 'date': datetime.now() | ||
59 | - } | ||
60 | - logger.debug(f'[unlock_topics] Unlocked "{topic}".') | ||
61 | - # else: # lock this topic if deps do not satisfy min_level | ||
62 | - # del self.state[topic] | ||
63 | - | ||
64 | - # ------------------------------------------------------------------------ | ||
65 | - # Start a new topic. | ||
66 | - # questions: list of generated questions to do in the topic | ||
67 | - # current_question: the current question to be presented | ||
68 | - # ------------------------------------------------------------------------ | ||
69 | - async def start_topic(self, topic): | ||
70 | - logger.debug(f'[start_topic] topic "{topic}"') | ||
71 | - | ||
72 | - if self.current_topic == topic: | ||
73 | - logger.info('Restarting current topic is not allowed.') | ||
74 | - return | ||
75 | - | ||
76 | - # do not allow locked topics | ||
77 | - if self.is_locked(topic): | ||
78 | - logger.debug(f'[start_topic] topic "{topic}" is locked') | ||
79 | - return | ||
80 | - | ||
81 | - # starting new topic | ||
82 | - self.current_topic = topic | ||
83 | - self.correct_answers = 0 | ||
84 | - self.wrong_answers = 0 | ||
85 | - | ||
86 | - t = self.deps.node[topic] | ||
87 | - k = t['choose'] | ||
88 | - if t['shuffle_questions']: | ||
89 | - questions = random.sample(t['questions'], k=k) | ||
90 | - else: | ||
91 | - questions = t['questions'][:k] | ||
92 | - logger.debug(f'[start_topic] questions: {", ".join(questions)}') | ||
93 | - | ||
94 | - # synchronous | ||
95 | - # self.questions = [self.factory[ref].generate() | ||
96 | - # for ref in questions] | ||
97 | - | ||
98 | - # asynchronous: | ||
99 | - self.questions = [await self.factory[ref].generate_async() | ||
100 | - for ref in questions] | ||
101 | - | ||
102 | - n = len(self.questions) | ||
103 | - logger.debug(f'[start_topic] generated {n} questions') | ||
104 | - | ||
105 | - # get first question | ||
106 | - self.next_question() | ||
107 | - | ||
108 | - # ------------------------------------------------------------------------ | ||
109 | - # The topic has finished and there are no more questions. | ||
110 | - # The topic level is updated in state and unlocks are performed. | ||
111 | - # The current topic is unchanged. | ||
112 | - # ------------------------------------------------------------------------ | ||
113 | - def finish_topic(self): | ||
114 | - logger.debug(f'[finish_topic] current_topic {self.current_topic}') | ||
115 | - | ||
116 | - self.state[self.current_topic] = { | ||
117 | - 'date': datetime.now(), | ||
118 | - 'level': self.correct_answers / (self.correct_answers + | ||
119 | - self.wrong_answers) | ||
120 | - } | ||
121 | - # self.current_topic = None | ||
122 | - self.unlock_topics() | ||
123 | - | ||
124 | - # ------------------------------------------------------------------------ | ||
125 | - # corrects current question with provided answer. | ||
126 | - # implements the logic: | ||
127 | - # - if answer ok, goes to next question | ||
128 | - # - if wrong, counts number of tries. If exceeded, moves on. | ||
129 | - # ------------------------------------------------------------------------ | ||
130 | - async def check_answer(self, answer): | ||
131 | - logger.debug('[check_answer]') | ||
132 | - | ||
133 | - q = self.current_question | ||
134 | - q['answer'] = answer | ||
135 | - q['finish_time'] = datetime.now() | ||
136 | - await q.correct_async() | ||
137 | - logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}') | ||
138 | - | ||
139 | - if q['grade'] > 0.999: | ||
140 | - self.correct_answers += 1 | ||
141 | - self.next_question() | ||
142 | - action = 'right' | ||
143 | - | ||
144 | - else: | ||
145 | - self.wrong_answers += 1 | ||
146 | - self.current_question['tries'] -= 1 | ||
147 | - | ||
148 | - if self.current_question['tries'] > 0: | ||
149 | - action = 'try_again' | ||
150 | - else: | ||
151 | - action = 'wrong' | ||
152 | - if self.current_question['append_wrong']: | ||
153 | - logger.debug('[check_answer] Wrong, append new instance') | ||
154 | - self.questions.append(self.factory[q['ref']].generate()) | ||
155 | - self.next_question() | ||
156 | - | ||
157 | - # returns corrected question (not new one) which might include comments | ||
158 | - return q, action | ||
159 | - | ||
160 | - # ------------------------------------------------------------------------ | ||
161 | - # Move to next question | ||
162 | - # ------------------------------------------------------------------------ | ||
163 | - def next_question(self): | ||
164 | - try: | ||
165 | - self.current_question = self.questions.pop(0) | ||
166 | - except IndexError: | ||
167 | - self.current_question = None | ||
168 | - self.finish_topic() | ||
169 | - else: | ||
170 | - self.current_question['start_time'] = datetime.now() | ||
171 | - default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] | ||
172 | - maxtries = self.current_question.get('max_tries', default_maxtries) | ||
173 | - self.current_question['tries'] = maxtries | ||
174 | - logger.debug(f'[next_question] "{self.current_question["ref"]}"') | ||
175 | - | ||
176 | - return self.current_question # question or None | ||
177 | - | ||
178 | - # ======================================================================== | ||
179 | - # pure functions of the state (no side effects) | ||
180 | - # ======================================================================== | ||
181 | - | ||
182 | - def topic_has_finished(self): | ||
183 | - return self.current_question is None | ||
184 | - | ||
185 | - # ------------------------------------------------------------------------ | ||
186 | - # compute recommended sequence of topics ['a', 'b', ...] | ||
187 | - # ------------------------------------------------------------------------ | ||
188 | - def recommend_topic_sequence(self, target=None): | ||
189 | - tt = list(nx.topological_sort(self.deps)) | ||
190 | - unlocked = [t for t in tt if t in self.state] | ||
191 | - locked = [t for t in tt if t not in unlocked] | ||
192 | - return unlocked + locked | ||
193 | - | ||
194 | - # ------------------------------------------------------------------------ | ||
195 | - def get_current_question(self): | ||
196 | - return self.current_question | ||
197 | - | ||
198 | - # ------------------------------------------------------------------------ | ||
199 | - def get_current_topic(self): | ||
200 | - return self.current_topic | ||
201 | - | ||
202 | - # ------------------------------------------------------------------------ | ||
203 | - def is_locked(self, topic): | ||
204 | - return topic not in self.state | ||
205 | - | ||
206 | - # ------------------------------------------------------------------------ | ||
207 | - # Return list of {ref: 'xpto', name: 'long name', leve: 0.5} | ||
208 | - # Levels are in the interval [0, 1] if unlocked or None if locked. | ||
209 | - # Topics unlocked but not yet done have level 0.0. | ||
210 | - # ------------------------------------------------------------------------ | ||
211 | - def get_knowledge_state(self): | ||
212 | - return [{ | ||
213 | - 'ref': ref, | ||
214 | - 'type': self.deps.nodes[ref]['type'], | ||
215 | - 'name': self.deps.nodes[ref]['name'], | ||
216 | - 'level': self.state[ref]['level'] if ref in self.state else None | ||
217 | - } for ref in self.topic_sequence] | ||
218 | - | ||
219 | - # ------------------------------------------------------------------------ | ||
220 | - def get_topic_progress(self): | ||
221 | - return self.correct_answers / (1 + self.correct_answers + | ||
222 | - len(self.questions)) | ||
223 | - | ||
224 | - # ------------------------------------------------------------------------ | ||
225 | - def get_topic_level(self, topic): | ||
226 | - return self.state[topic]['level'] | ||
227 | - | ||
228 | - # ------------------------------------------------------------------------ | ||
229 | - def get_topic_date(self, topic): | ||
230 | - return self.state[topic]['date'] | ||
231 | - | ||
232 | - # ------------------------------------------------------------------------ | ||
233 | - # Recommends a topic to practice/learn from the state. | ||
234 | - # ------------------------------------------------------------------------ | ||
235 | - # def get_recommended_topic(self): # FIXME untested | ||
236 | - # return min(self.state.items(), key=lambda x: x[1]['level'])[0] |
aprendizations/learnapp.py
@@ -15,7 +15,7 @@ import networkx as nx | @@ -15,7 +15,7 @@ import networkx as nx | ||
15 | 15 | ||
16 | # this project | 16 | # this project |
17 | from .models import Student, Answer, Topic, StudentTopic | 17 | from .models import Student, Answer, Topic, StudentTopic |
18 | -from .knowledge import StudentKnowledge | 18 | +from .student import StudentState |
19 | from .questions import QFactory | 19 | from .questions import QFactory |
20 | from .tools import load_yaml | 20 | from .tools import load_yaml |
21 | 21 | ||
@@ -155,8 +155,8 @@ class LearnApp(object): | @@ -155,8 +155,8 @@ class LearnApp(object): | ||
155 | self.online[uid] = { | 155 | self.online[uid] = { |
156 | 'number': uid, | 156 | 'number': uid, |
157 | 'name': name, | 157 | 'name': name, |
158 | - 'state': StudentKnowledge(deps=self.deps, factory=self.factory, | ||
159 | - state=state), | 158 | + 'state': StudentState(deps=self.deps, factory=self.factory, |
159 | + state=state), | ||
160 | 'counter': counter + 1, # counts simultaneous logins | 160 | 'counter': counter + 1, # counts simultaneous logins |
161 | } | 161 | } |
162 | 162 | ||
@@ -441,16 +441,16 @@ class LearnApp(object): | @@ -441,16 +441,16 @@ class LearnApp(object): | ||
441 | total_topics = s.query(Topic).count() | 441 | total_topics = s.query(Topic).count() |
442 | 442 | ||
443 | # answer performance | 443 | # answer performance |
444 | - totalans = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)). | ||
445 | - group_by(Answer.student_id). | ||
446 | - all()) | ||
447 | - rightans = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)). | ||
448 | - filter(Answer.grade == 1.0). | ||
449 | - group_by(Answer.student_id). | ||
450 | - all()) | 444 | + total = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)). |
445 | + group_by(Answer.student_id). | ||
446 | + all()) | ||
447 | + right = dict(s.query(Answer.student_id, sa.func.count(Answer.ref)). | ||
448 | + filter(Answer.grade == 1.0). | ||
449 | + group_by(Answer.student_id). | ||
450 | + all()) | ||
451 | 451 | ||
452 | # compute percentage of right answers | 452 | # compute percentage of right answers |
453 | - perf = {uid: rightans.get(uid, 0.0)/totalans[uid] for uid in totalans} | 453 | + perf = {uid: right.get(uid, 0.0)/total[uid] for uid in total} |
454 | 454 | ||
455 | # compute topic progress | 455 | # compute topic progress |
456 | prog = {s[0]: 0.0 for s in students} | 456 | prog = {s[0]: 0.0 for s in students} |
@@ -0,0 +1,236 @@ | @@ -0,0 +1,236 @@ | ||
1 | + | ||
2 | +# python standard library | ||
3 | +import random | ||
4 | +from datetime import datetime | ||
5 | +import logging | ||
6 | + | ||
7 | +# third party libraries | ||
8 | +import networkx as nx | ||
9 | + | ||
10 | +# setup logger for this module | ||
11 | +logger = logging.getLogger(__name__) | ||
12 | + | ||
13 | + | ||
14 | +# ---------------------------------------------------------------------------- | ||
15 | +# kowledge state of each student....?? | ||
16 | +# Contains: | ||
17 | +# state - dict of topics with state of unlocked topics | ||
18 | +# deps - access to dependency graph shared between students | ||
19 | +# topic_sequence - list with the order of recommended topics | ||
20 | +# ---------------------------------------------------------------------------- | ||
21 | +class StudentState(object): | ||
22 | + # ======================================================================= | ||
23 | + # methods that update state | ||
24 | + # ======================================================================= | ||
25 | + def __init__(self, deps, factory, state={}): | ||
26 | + self.deps = deps # shared dependency graph | ||
27 | + self.factory = factory # question factory | ||
28 | + self.state = state # {'topic': {'level': 0.5, 'date': datetime}, ...} | ||
29 | + | ||
30 | + self.update_topic_levels() # applies forgetting factor | ||
31 | + self.unlock_topics() # whose dependencies have been completed | ||
32 | + self.topic_sequence = self.recommend_topic_sequence() # ['ref1', ...] | ||
33 | + self.current_topic = None | ||
34 | + | ||
35 | + # ------------------------------------------------------------------------ | ||
36 | + # Updates the proficiency levels of the topics, with forgetting factor | ||
37 | + # FIXME no dependencies are considered yet... | ||
38 | + # ------------------------------------------------------------------------ | ||
39 | + def update_topic_levels(self): | ||
40 | + now = datetime.now() | ||
41 | + for tref, s in self.state.items(): | ||
42 | + dt = now - s['date'] | ||
43 | + s['level'] *= 0.98 ** dt.days # forgetting factor | ||
44 | + | ||
45 | + # ------------------------------------------------------------------------ | ||
46 | + # Unlock topics whose dependencies are satisfied (> min_level) | ||
47 | + # ------------------------------------------------------------------------ | ||
48 | + def unlock_topics(self): | ||
49 | + for topic in self.deps.nodes(): | ||
50 | + if topic not in self.state: # if locked | ||
51 | + pred = self.deps.predecessors(topic) | ||
52 | + min_level = self.deps.node[topic]['min_level'] | ||
53 | + if all(d in self.state and self.state[d]['level'] > min_level | ||
54 | + for d in pred): # all deps are greater than min_level | ||
55 | + | ||
56 | + self.state[topic] = { | ||
57 | + 'level': 0.0, # unlocked | ||
58 | + 'date': datetime.now() | ||
59 | + } | ||
60 | + logger.debug(f'[unlock_topics] Unlocked "{topic}".') | ||
61 | + # else: # lock this topic if deps do not satisfy min_level | ||
62 | + # del self.state[topic] | ||
63 | + | ||
64 | + # ------------------------------------------------------------------------ | ||
65 | + # Start a new topic. | ||
66 | + # questions: list of generated questions to do in the topic | ||
67 | + # current_question: the current question to be presented | ||
68 | + # ------------------------------------------------------------------------ | ||
69 | + async def start_topic(self, topic): | ||
70 | + logger.debug(f'[start_topic] topic "{topic}"') | ||
71 | + | ||
72 | + if self.current_topic == topic: | ||
73 | + logger.info('Restarting current topic is not allowed.') | ||
74 | + return | ||
75 | + | ||
76 | + # do not allow locked topics | ||
77 | + if self.is_locked(topic): | ||
78 | + logger.debug(f'[start_topic] topic "{topic}" is locked') | ||
79 | + return | ||
80 | + | ||
81 | + # starting new topic | ||
82 | + self.current_topic = topic | ||
83 | + self.correct_answers = 0 | ||
84 | + self.wrong_answers = 0 | ||
85 | + | ||
86 | + t = self.deps.node[topic] | ||
87 | + k = t['choose'] | ||
88 | + if t['shuffle_questions']: | ||
89 | + questions = random.sample(t['questions'], k=k) | ||
90 | + else: | ||
91 | + questions = t['questions'][:k] | ||
92 | + logger.debug(f'[start_topic] questions: {", ".join(questions)}') | ||
93 | + | ||
94 | + # synchronous | ||
95 | + # self.questions = [self.factory[ref].generate() | ||
96 | + # for ref in questions] | ||
97 | + | ||
98 | + # asynchronous: | ||
99 | + self.questions = [await self.factory[ref].generate_async() | ||
100 | + for ref in questions] | ||
101 | + | ||
102 | + n = len(self.questions) | ||
103 | + logger.debug(f'[start_topic] generated {n} questions') | ||
104 | + | ||
105 | + # get first question | ||
106 | + self.next_question() | ||
107 | + | ||
108 | + # ------------------------------------------------------------------------ | ||
109 | + # The topic has finished and there are no more questions. | ||
110 | + # The topic level is updated in state and unlocks are performed. | ||
111 | + # The current topic is unchanged. | ||
112 | + # ------------------------------------------------------------------------ | ||
113 | + def finish_topic(self): | ||
114 | + logger.debug(f'[finish_topic] current_topic {self.current_topic}') | ||
115 | + | ||
116 | + self.state[self.current_topic] = { | ||
117 | + 'date': datetime.now(), | ||
118 | + 'level': self.correct_answers / (self.correct_answers + | ||
119 | + self.wrong_answers) | ||
120 | + } | ||
121 | + # self.current_topic = None | ||
122 | + self.unlock_topics() | ||
123 | + | ||
124 | + # ------------------------------------------------------------------------ | ||
125 | + # corrects current question with provided answer. | ||
126 | + # implements the logic: | ||
127 | + # - if answer ok, goes to next question | ||
128 | + # - if wrong, counts number of tries. If exceeded, moves on. | ||
129 | + # ------------------------------------------------------------------------ | ||
130 | + async def check_answer(self, answer): | ||
131 | + logger.debug('[check_answer]') | ||
132 | + | ||
133 | + q = self.current_question | ||
134 | + q['answer'] = answer | ||
135 | + q['finish_time'] = datetime.now() | ||
136 | + await q.correct_async() | ||
137 | + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}') | ||
138 | + | ||
139 | + if q['grade'] > 0.999: | ||
140 | + self.correct_answers += 1 | ||
141 | + self.next_question() | ||
142 | + action = 'right' | ||
143 | + | ||
144 | + else: | ||
145 | + self.wrong_answers += 1 | ||
146 | + self.current_question['tries'] -= 1 | ||
147 | + | ||
148 | + if self.current_question['tries'] > 0: | ||
149 | + action = 'try_again' | ||
150 | + else: | ||
151 | + action = 'wrong' | ||
152 | + if self.current_question['append_wrong']: | ||
153 | + logger.debug('[check_answer] Wrong, append new instance') | ||
154 | + self.questions.append(self.factory[q['ref']].generate()) | ||
155 | + self.next_question() | ||
156 | + | ||
157 | + # returns corrected question (not new one) which might include comments | ||
158 | + return q, action | ||
159 | + | ||
160 | + # ------------------------------------------------------------------------ | ||
161 | + # Move to next question | ||
162 | + # ------------------------------------------------------------------------ | ||
163 | + def next_question(self): | ||
164 | + try: | ||
165 | + self.current_question = self.questions.pop(0) | ||
166 | + except IndexError: | ||
167 | + self.current_question = None | ||
168 | + self.finish_topic() | ||
169 | + else: | ||
170 | + self.current_question['start_time'] = datetime.now() | ||
171 | + default_maxtries = self.deps.nodes[self.current_topic]['max_tries'] | ||
172 | + maxtries = self.current_question.get('max_tries', default_maxtries) | ||
173 | + self.current_question['tries'] = maxtries | ||
174 | + logger.debug(f'[next_question] "{self.current_question["ref"]}"') | ||
175 | + | ||
176 | + return self.current_question # question or None | ||
177 | + | ||
178 | + # ======================================================================== | ||
179 | + # pure functions of the state (no side effects) | ||
180 | + # ======================================================================== | ||
181 | + | ||
182 | + def topic_has_finished(self): | ||
183 | + return self.current_question is None | ||
184 | + | ||
185 | + # ------------------------------------------------------------------------ | ||
186 | + # compute recommended sequence of topics ['a', 'b', ...] | ||
187 | + # ------------------------------------------------------------------------ | ||
188 | + def recommend_topic_sequence(self, target=None): | ||
189 | + tt = list(nx.topological_sort(self.deps)) | ||
190 | + unlocked = [t for t in tt if t in self.state] | ||
191 | + locked = [t for t in tt if t not in unlocked] | ||
192 | + return unlocked + locked | ||
193 | + | ||
194 | + # ------------------------------------------------------------------------ | ||
195 | + def get_current_question(self): | ||
196 | + return self.current_question | ||
197 | + | ||
198 | + # ------------------------------------------------------------------------ | ||
199 | + def get_current_topic(self): | ||
200 | + return self.current_topic | ||
201 | + | ||
202 | + # ------------------------------------------------------------------------ | ||
203 | + def is_locked(self, topic): | ||
204 | + return topic not in self.state | ||
205 | + | ||
206 | + # ------------------------------------------------------------------------ | ||
207 | + # Return list of {ref: 'xpto', name: 'long name', leve: 0.5} | ||
208 | + # Levels are in the interval [0, 1] if unlocked or None if locked. | ||
209 | + # Topics unlocked but not yet done have level 0.0. | ||
210 | + # ------------------------------------------------------------------------ | ||
211 | + def get_knowledge_state(self): | ||
212 | + return [{ | ||
213 | + 'ref': ref, | ||
214 | + 'type': self.deps.nodes[ref]['type'], | ||
215 | + 'name': self.deps.nodes[ref]['name'], | ||
216 | + 'level': self.state[ref]['level'] if ref in self.state else None | ||
217 | + } for ref in self.topic_sequence] | ||
218 | + | ||
219 | + # ------------------------------------------------------------------------ | ||
220 | + def get_topic_progress(self): | ||
221 | + return self.correct_answers / (1 + self.correct_answers + | ||
222 | + len(self.questions)) | ||
223 | + | ||
224 | + # ------------------------------------------------------------------------ | ||
225 | + def get_topic_level(self, topic): | ||
226 | + return self.state[topic]['level'] | ||
227 | + | ||
228 | + # ------------------------------------------------------------------------ | ||
229 | + def get_topic_date(self, topic): | ||
230 | + return self.state[topic]['date'] | ||
231 | + | ||
232 | + # ------------------------------------------------------------------------ | ||
233 | + # Recommends a topic to practice/learn from the state. | ||
234 | + # ------------------------------------------------------------------------ | ||
235 | + # def get_recommended_topic(self): # FIXME untested | ||
236 | + # return min(self.state.items(), key=lambda x: x[1]['level'])[0] |