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 ...
|
|
|