Blame view

aprendizations/student.py 11.2 KB
84a1054c   Miguel Barão   - Each student is...
1

443a1eea   Miguel Barão   Update to latest ...
2
3
4
5
6
7
8
# python standard library
from datetime import datetime
import logging
import random
from typing import Dict, List, Optional, Tuple

# third party libraries
84a1054c   Miguel Barão   - Each student is...
9
import networkx as nx
8e601953   Miguel Barão   fix mostly flake8...
10

fcdedf5a   Miguel Barão   - Reads config.ya...
11
# this project
d187aad4   Miguel Barão   - adds courses
12
from .questions import Question
443a1eea   Miguel Barão   Update to latest ...
13

fcdedf5a   Miguel Barão   - Reads config.ya...
14

3259fc7c   Miguel Barão   - Modified login ...
15
# setup logger for this module
fcdedf5a   Miguel Barão   - Reads config.ya...
16
logger = logging.getLogger(__name__)
84a1054c   Miguel Barão   - Each student is...
17

720ccbfa   Miguel Barão   - more type annot...
18

443a1eea   Miguel Barão   Update to latest ...
19
# ----------------------------------------------------------------------------
720ccbfa   Miguel Barão   - more type annot...
20
21
# kowledge state of a student
# Contains:
fcdedf5a   Miguel Barão   - Reads config.ya...
22
23
#   state - dict of unlocked topics and their levels
#   deps  - access to dependency graph shared between students
84a1054c   Miguel Barão   - Each student is...
24
#   topic_sequence - list with the recommended topic sequence
8e601953   Miguel Barão   fix mostly flake8...
25
#   current_topic - nameref of the current topic
443a1eea   Miguel Barão   Update to latest ...
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ----------------------------------------------------------------------------
class StudentState(object):
    # =======================================================================
    # methods that update state
    # =======================================================================
    def __init__(self, uid, state, courses, deps, factory) -> None:
        # shared application data between all students
        self.deps = deps        # dependency graph
        self.factory = factory  # question factory
        self.courses = courses  # {'course': ['topic_id1', 'topic_id2',...]}

        # data of this student
        self.uid = uid      # user id '12345'
        self.state = state  # {'topic': {'level': 0.5, 'date': datetime}, ...}

        # prepare for running
        self.update_topic_levels()  # applies forgetting factor
        self.unlock_topics()        # whose dependencies have been completed
        self.start_course(None)
065611f7   Miguel Barão   large ammount of ...
45

443a1eea   Miguel Barão   Update to latest ...
46
    # ------------------------------------------------------------------------
d187aad4   Miguel Barão   - adds courses
47
48
49
    def start_course(self, course: Optional[str]) -> None:
        if course is None:
            logger.debug('no active course')
d5cd0d10   Miguel Barão   - added options '...
50
            self.current_course: Optional[str] = None
d187aad4   Miguel Barão   - adds courses
51
52
53
54
            self.topic_sequence: List[str] = []
            self.current_topic: Optional[str] = None
        else:
            logger.debug(f'starting course {course}')
8e601953   Miguel Barão   fix mostly flake8...
55
            self.current_course = course
e0818d92   Miguel Barão   - show remaining ...
56
            topics = self.courses[course]['topics']
d187aad4   Miguel Barão   - adds courses
57
            self.topic_sequence = self.recommend_topic_sequence(topics)
a203d3cc   Miguel Barão   - new http server...
58

8e601953   Miguel Barão   fix mostly flake8...
59
    # ------------------------------------------------------------------------
d187aad4   Miguel Barão   - adds courses
60
    # Start a new topic.
5f3daeeb   Miguel Barão   add lots of type ...
61
    #    questions: list of generated questions to do in the topic
03210039   Miguel Barao   - main page ok.
62
    #    current_question: the current question to be presented
d187aad4   Miguel Barão   - adds courses
63
    # ------------------------------------------------------------------------
443a1eea   Miguel Barão   Update to latest ...
64
65
66
67
    async def start_topic(self, topic: str) -> None:
        logger.debug(f'start topic "{topic}"')

        # avoid regenerating questions in the middle of the current topic
d187aad4   Miguel Barão   - adds courses
68
69
70
71
72
        if self.current_topic == topic:
            logger.info('Restarting current topic is not allowed.')
            return

        # do not allow locked topics
d187aad4   Miguel Barão   - adds courses
73
        if self.is_locked(topic):
cc63d37e   Miguel Barão   fix error where t...
74
75
76
            logger.debug(f'is locked "{topic}"')
            return

443a1eea   Miguel Barão   Update to latest ...
77
        # starting new topic
cc63d37e   Miguel Barão   fix error where t...
78
        self.current_topic = topic
443a1eea   Miguel Barão   Update to latest ...
79
        self.correct_answers = 0
d187aad4   Miguel Barão   - adds courses
80
        self.wrong_answers = 0
443a1eea   Miguel Barão   Update to latest ...
81

03210039   Miguel Barao   - main page ok.
82
        t = self.deps.node[topic]
03210039   Miguel Barao   - main page ok.
83
        k = t['choose']
443a1eea   Miguel Barão   Update to latest ...
84
85
86
87
88
89
90
91
        if t['shuffle_questions']:
            questions = random.sample(t['questions'], k=k)
        else:
            questions = t['questions'][:k]
        logger.debug(f'selected questions:  {", ".join(questions)}')

        self.questions: List[Question] = [await self.factory[ref].gen_async()
                                          for ref in questions]
39cd1cfe   Miguel Barão   add config/logger...
92

2b709b19   Miguel Barão   fixed several err...
93
        n = len(self.questions)
443a1eea   Miguel Barão   Update to latest ...
94
        logger.debug(f'generated {n} questions')
2b709b19   Miguel Barão   fixed several err...
95
96

        # get first question
68528695   Miguel Barao   - working! but cr...
97
        self.next_question()
e0818d92   Miguel Barão   - show remaining ...
98

443a1eea   Miguel Barão   Update to latest ...
99
100
    # ------------------------------------------------------------------------
    # The topic has finished and there are no more questions.
88531302   Miguel Barão   code cleaning in ...
101
    # The topic level is updated in state and unlocks are performed.
31affef2   Miguel Barão   - reload page no ...
102
    # The current topic is unchanged.
a6b50da0   Miguel Barão   fixes error where...
103
104
    # ------------------------------------------------------------------------
    def finish_topic(self) -> None:
d823a4d8   Miguel Barão   Lots of changes:
105
        logger.debug(f'finished {self.current_topic}')
443a1eea   Miguel Barão   Update to latest ...
106

dd4d2655   Miguel Barão   - number of stars...
107
108
        self.state[self.current_topic] = {
            'date': datetime.now(),
443a1eea   Miguel Barão   Update to latest ...
109
110
111
112
            'level': self.correct_answers / (self.correct_answers +
                                             self.wrong_answers)
            }
        self.current_topic = None
dbdd58fe   Miguel Barão   - added option 'a...
113
        self.unlock_topics()
443a1eea   Miguel Barão   Update to latest ...
114
115

    # ------------------------------------------------------------------------
6f0ef3e3   Miguel Barão   - fixed bug where...
116
    # corrects current question with provided answer.
d187aad4   Miguel Barão   - adds courses
117
118
    # implements the logic:
    #   - if answer ok, goes to next question
fcdedf5a   Miguel Barão   - Reads config.ya...
119
    #   - if wrong, counts number of tries. If exceeded, moves on.
443a1eea   Miguel Barão   Update to latest ...
120
    # ------------------------------------------------------------------------
88531302   Miguel Barão   code cleaning in ...
121
    async def check_answer(self, answer) -> Tuple[Question, str]:
e0818d92   Miguel Barão   - show remaining ...
122
123
        q: Question = self.current_question
        q['answer'] = answer
39cd1cfe   Miguel Barão   add config/logger...
124
        q['finish_time'] = datetime.now()
db2aceed   Miguel Barão   - generates quest...
125
        logger.debug(f'checking answer of {q["ref"]}...')
a6b50da0   Miguel Barão   fixes error where...
126
        await q.correct_async()
443a1eea   Miguel Barão   Update to latest ...
127
128
129
130
131
132
133
        logger.debug(f'grade = {q["grade"]:.2}')

        if q['grade'] > 0.999:
            self.correct_answers += 1
            self.next_question()
            action = 'right'

a6b50da0   Miguel Barão   fixes error where...
134
135
136
        else:
            self.wrong_answers += 1
            self.current_question['tries'] -= 1
443a1eea   Miguel Barão   Update to latest ...
137
138

            if self.current_question['tries'] > 0:
6f0ef3e3   Miguel Barão   - fixed bug where...
139
                action = 'try_again'
443a1eea   Miguel Barão   Update to latest ...
140
            else:
dd4d2655   Miguel Barão   - number of stars...
141
                action = 'wrong'
443a1eea   Miguel Barão   Update to latest ...
142
                if self.current_question['append_wrong']:
d1410958   Miguel Barão   - starts do do so...
143
                    logger.debug('wrong answer, append new question')
065611f7   Miguel Barão   large ammount of ...
144
                    # self.questions.append(self.factory[q['ref']].generate())
dd4d2655   Miguel Barão   - number of stars...
145
                    new_question = await self.factory[q['ref']].gen_async()
443a1eea   Miguel Barão   Update to latest ...
146
147
148
                    self.questions.append(new_question)
                self.next_question()

775dd8eb   Miguel Barão   - reorganized how...
149
        # returns corrected question (not new one) which might include comments
443a1eea   Miguel Barão   Update to latest ...
150
        return q, action
d823a4d8   Miguel Barão   Lots of changes:
151

443a1eea   Miguel Barão   Update to latest ...
152
153
    # ------------------------------------------------------------------------
    # Move to next question, or None
d823a4d8   Miguel Barão   Lots of changes:
154
155
    # ------------------------------------------------------------------------
    def next_question(self) -> Optional[Question]:
d823a4d8   Miguel Barão   Lots of changes:
156
        try:
443a1eea   Miguel Barão   Update to latest ...
157
158
159
160
161
162
163
            self.current_question = self.questions.pop(0)
        except IndexError:
            self.current_question = None
            self.finish_topic()
        else:
            self.current_question['start_time'] = datetime.now()
            default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
a6b50da0   Miguel Barão   fixes error where...
164
165
166
            maxtries = self.current_question.get('max_tries', default_maxtries)
            self.current_question['tries'] = maxtries
            logger.debug(f'current_question = {self.current_question["ref"]}')
443a1eea   Miguel Barão   Update to latest ...
167

d823a4d8   Miguel Barão   Lots of changes:
168
        return self.current_question  # question or None
443a1eea   Miguel Barão   Update to latest ...
169

d823a4d8   Miguel Barão   Lots of changes:
170
    # ------------------------------------------------------------------------
443a1eea   Miguel Barão   Update to latest ...
171
172
    # Update proficiency level of the topics using a forgetting factor
    # ------------------------------------------------------------------------
d823a4d8   Miguel Barão   Lots of changes:
173
    def update_topic_levels(self) -> None:
443a1eea   Miguel Barão   Update to latest ...
174
        now = datetime.now()
d823a4d8   Miguel Barão   Lots of changes:
175
176
        for tref, s in self.state.items():
            dt = now - s['date']
e0818d92   Miguel Barão   - show remaining ...
177
            forgetting_factor = self.deps.node[tref]['forgetting_factor']
d823a4d8   Miguel Barão   Lots of changes:
178
            s['level'] *= forgetting_factor ** dt.days   # forgetting factor
065611f7   Miguel Barão   large ammount of ...
179

e0818d92   Miguel Barão   - show remaining ...
180
    # ------------------------------------------------------------------------
d823a4d8   Miguel Barão   Lots of changes:
181
    # Unlock topics whose dependencies are satisfied (> min_level)
443a1eea   Miguel Barão   Update to latest ...
182
183
184
185
    # ------------------------------------------------------------------------
    def unlock_topics(self) -> None:
        for topic in self.deps.nodes():
            if topic not in self.state:  # if locked
e0818d92   Miguel Barão   - show remaining ...
186
                pred = self.deps.predecessors(topic)
443a1eea   Miguel Barão   Update to latest ...
187
                min_level = self.deps.node[topic]['min_level']
e0818d92   Miguel Barão   - show remaining ...
188
                if all(d in self.state and self.state[d]['level'] > min_level
e0818d92   Miguel Barão   - show remaining ...
189
                       for d in pred):  # all deps are greater than min_level
d823a4d8   Miguel Barão   Lots of changes:
190

e0818d92   Miguel Barão   - show remaining ...
191
                    self.state[topic] = {
443a1eea   Miguel Barão   Update to latest ...
192
193
194
195
196
                        'level': 0.0,           # unlock
                        'date': datetime.now()
                        }
                    logger.debug(f'unlocked "{topic}"')
                # else:  # lock this topic if deps do not satisfy min_level
a6b50da0   Miguel Barão   fixes error where...
197
198
                #     del self.state[topic]

a6b50da0   Miguel Barão   fixes error where...
199
    # ========================================================================
443a1eea   Miguel Barão   Update to latest ...
200
201
202
203
204
205
206
    # pure functions of the state (no side effects)
    # ========================================================================

    def topic_has_finished(self) -> bool:
        return self.current_topic is None

    # ------------------------------------------------------------------------
a6b50da0   Miguel Barão   fixes error where...
207
208
209
210
211
212
213
214
215
216
    # compute recommended sequence of topics  ['a', 'b', ...]
    # ------------------------------------------------------------------------
    def recommend_topic_sequence(self, targets: List[str] = []) -> List[str]:
        G = self.deps
        ts = set(targets)
        for t in targets:
            ts.update(nx.ancestors(G, t))

        tl = list(nx.topological_sort(G.subgraph(ts)))

065611f7   Miguel Barão   large ammount of ...
217
        # sort with unlocked first
d187aad4   Miguel Barão   - adds courses
218
        unlocked = [t for t in tl if t in self.state]
d187aad4   Miguel Barão   - adds courses
219
        locked = [t for t in tl if t not in unlocked]
443a1eea   Miguel Barão   Update to latest ...
220
221
222
223
        return unlocked + locked

    # ------------------------------------------------------------------------
    def get_current_question(self) -> Optional[Question]:
d187aad4   Miguel Barão   - adds courses
224
        return self.current_question
443a1eea   Miguel Barão   Update to latest ...
225
226

    # ------------------------------------------------------------------------
2c260807   Miguel Barão   - fix initdb-apre...
227
228
    def get_current_topic(self) -> Optional[str]:
        return self.current_topic
443a1eea   Miguel Barão   Update to latest ...
229

2c260807   Miguel Barão   - fix initdb-apre...
230
    # ------------------------------------------------------------------------
443a1eea   Miguel Barão   Update to latest ...
231
    def get_current_course_title(self) -> Optional[str]:
d187aad4   Miguel Barão   - adds courses
232
233
        return self.courses[self.current_course]['title']

d187aad4   Miguel Barão   - adds courses
234
    # ------------------------------------------------------------------------
443a1eea   Miguel Barão   Update to latest ...
235
236
237
    def is_locked(self, topic: str) -> bool:
        return topic not in self.state

d187aad4   Miguel Barão   - adds courses
238
239
240
    # ------------------------------------------------------------------------
    # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}
    # Levels are in the interval [0, 1] if unlocked or None if locked.
289991dc   Miguel Barão   - several fixes, ...
241
    # Topics unlocked but not yet done have level 0.0.
d187aad4   Miguel Barão   - adds courses
242
243
244
245
246
247
248
    # ------------------------------------------------------------------------
    def get_knowledge_state(self):
        return [{
            'ref': ref,
            'type': self.deps.nodes[ref]['type'],
            'name': self.deps.nodes[ref]['name'],
            'level': self.state[ref]['level'] if ref in self.state else None
443a1eea   Miguel Barão   Update to latest ...
249
            } for ref in self.topic_sequence]
d187aad4   Miguel Barão   - adds courses
250
251
252

    # ------------------------------------------------------------------------
    def get_topic_progress(self) -> float:
065611f7   Miguel Barão   large ammount of ...
253
254
255
        return self.correct_answers / (1 + self.correct_answers +
                                       len(self.questions))

ed34db4c   Miguel Barão   - save topic stat...
256
    # ------------------------------------------------------------------------
b6f3badf   Miguel Barão   - fix some mypy e...
257
    def get_topic_level(self, topic: str) -> float:
443a1eea   Miguel Barão   Update to latest ...
258
259
260
261
        return self.state[topic]['level']

    # ------------------------------------------------------------------------
    def get_topic_date(self, topic: str):
a6b50da0   Miguel Barão   fixes error where...
262
        return self.state[topic]['date']
2285f4a5   Miguel Barão   - fixed finished ...
263

760d12cc   Miguel Barão   - created static/...
264
    # ------------------------------------------------------------------------
443a1eea   Miguel Barão   Update to latest ...
265
266
267
268
    # Recommends a topic to practice/learn from the state.
    # ------------------------------------------------------------------------
    # def get_recommended_topic(self):    # FIXME untested
    #     return min(self.state.items(), key=lambda x: x[1]['level'])[0]
a9131008   Miguel Barão   - allow chapters ...

443a1eea   Miguel Barão   Update to latest ...

a9131008   Miguel Barão   - allow chapters ...

443a1eea   Miguel Barão   Update to latest ...

cc63d37e   Miguel Barão   fix error where t...

c4200a77   Miguel Barão   changed FIXME to ...

443a1eea   Miguel Barão   Update to latest ...

d187aad4   Miguel Barão   - adds courses

443a1eea   Miguel Barão   Update to latest ...

fa091c84   Miguel Barão   Allow generator t...

8e601953   Miguel Barão   fix mostly flake8...

db2aceed   Miguel Barão   - generates quest...

720ccbfa   Miguel Barão   - more type annot...

443a1eea   Miguel Barão   Update to latest ...

84a1054c   Miguel Barão   - Each student is...

e5b363cc   Miguel Barão   - checkbox option...

720ccbfa   Miguel Barão   - more type annot...

443a1eea   Miguel Barão   Update to latest ...

0b1675b0   Miguel Barão   - fix browser red...

a6b50da0   Miguel Barão   fixes error where...

443a1eea   Miguel Barão   Update to latest ...

a6b50da0   Miguel Barão   fixes error where...

443a1eea   Miguel Barão   Update to latest ...

a6b50da0   Miguel Barão   fixes error where...

d187aad4   Miguel Barão   - adds courses

8b4ac80a   Miguel Barão   - fixed menus

443a1eea   Miguel Barão   Update to latest ...

8b4ac80a   Miguel Barão   - fixed menus

b6f3badf   Miguel Barão   - fix some mypy e...

443a1eea   Miguel Barão   Update to latest ...

b9094cfb   Miguel Barão   - fixed direct li...

68528695   Miguel Barao   - working! but cr...

443a1eea   Miguel Barão   Update to latest ...

13773677   Miguel Barão   under development...

af990045   Miguel Barão   - added support f...

13773677   Miguel Barão   under development...

8e601953   Miguel Barão   fix mostly flake8...

13773677   Miguel Barão   under development...

3e4621f7   Miguel Barao   - added progress ...

720ccbfa   Miguel Barão   - more type annot...

443a1eea   Miguel Barão   Update to latest ...

8e601953   Miguel Barão   fix mostly flake8...

4f21e22a   Miguel Barão   - changed debug l...

ae2dc2b8   Miguel Barão   - finished topic ...

720ccbfa   Miguel Barão   - more type annot...

443a1eea   Miguel Barão   Update to latest ...

a6b50da0   Miguel Barão   fixes error where...

ae2dc2b8   Miguel Barão   - finished topic ...

b6f3badf   Miguel Barão   - fix some mypy e...

443a1eea   Miguel Barão   Update to latest ...

ae2dc2b8   Miguel Barão   - finished topic ...