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
aprendizations/knowledge.py
| ... | ... | @@ -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 | 15 | |
| 16 | 16 | # this project |
| 17 | 17 | from .models import Student, Answer, Topic, StudentTopic |
| 18 | -from .knowledge import StudentKnowledge | |
| 18 | +from .student import StudentState | |
| 19 | 19 | from .questions import QFactory |
| 20 | 20 | from .tools import load_yaml |
| 21 | 21 | |
| ... | ... | @@ -155,8 +155,8 @@ class LearnApp(object): |
| 155 | 155 | self.online[uid] = { |
| 156 | 156 | 'number': uid, |
| 157 | 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 | 160 | 'counter': counter + 1, # counts simultaneous logins |
| 161 | 161 | } |
| 162 | 162 | |
| ... | ... | @@ -441,16 +441,16 @@ class LearnApp(object): |
| 441 | 441 | total_topics = s.query(Topic).count() |
| 442 | 442 | |
| 443 | 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 | 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 | 455 | # compute topic progress |
| 456 | 456 | prog = {s[0]: 0.0 for s in students} | ... | ... |
| ... | ... | @@ -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] | ... | ... |