Commit ae2dc2b800b823cee22a2e9af129429f92948804
1 parent
dda6f838
Exists in
master
and in
1 other branch
- finished topic and its questions are added to the database imediatly as the topic is finished.
This avoids losing state when student closes the browser. Exit menu option no longer saves in the database.
Showing
5 changed files
with
114 additions
and
82 deletions
Show diff stats
BUGS.md
1 | 1 | |
2 | 2 | BUGS: |
3 | 3 | |
4 | -- gravar evolucao na bd no final de cada topico. | |
4 | +- melhorar markdown das tabelas. | |
5 | 5 | - servir imagens/ficheiros. |
6 | 6 | - topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores. |
7 | 7 | |
... | ... | @@ -29,6 +29,7 @@ TODO: |
29 | 29 | |
30 | 30 | FIXED: |
31 | 31 | |
32 | +- gravar evolucao na bd no final de cada topico. | |
32 | 33 | - submeter questoes radio, da erro se nao escolher nenhuma opção. |
33 | 34 | - indentação da primeira linha de código não funciona. |
34 | 35 | - markdown com o mistune. | ... | ... |
knowledge.py
... | ... | @@ -24,7 +24,7 @@ class StudentKnowledge(object): |
24 | 24 | # graph with topic dependencies shared between all students |
25 | 25 | self.deps = deps |
26 | 26 | |
27 | - # state only contains information on unlocked topics | |
27 | + # state only contains unlocked topics | |
28 | 28 | # {'topic_id': {'level':0.5, 'date': datetime}, ...} |
29 | 29 | self.state = state |
30 | 30 | |
... | ... | @@ -32,7 +32,7 @@ class StudentKnowledge(object): |
32 | 32 | now = datetime.now() |
33 | 33 | for s in state.values(): |
34 | 34 | dt = now - s['date'] |
35 | - s['level'] *= 0.975 ** dt.days | |
35 | + s['level'] *= 0.975 ** dt.days # forgetting factor | |
36 | 36 | |
37 | 37 | # compute recommended sequence of topics ['a', 'b', ...] |
38 | 38 | self.topic_sequence = list(nx.topological_sort(self.deps)) |
... | ... | @@ -43,44 +43,22 @@ class StudentKnowledge(object): |
43 | 43 | # Unlock topics whose dependencies are satisfied (> min_level) |
44 | 44 | # ------------------------------------------------------------------------ |
45 | 45 | def unlock_topics(self): |
46 | - min_level = 0.01 # minimum level to unlock | |
46 | + # minimum level that the dependencies of a topic must have | |
47 | + # for the topic to be unlocked. | |
48 | + min_level = 0.01 | |
49 | + | |
47 | 50 | for topic in self.topic_sequence: |
48 | 51 | if topic not in self.state: # if locked |
49 | 52 | pred = self.deps.predecessors(topic) |
50 | - if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # dependencies done | |
53 | + if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # and all dependencies are done | |
51 | 54 | self.state[topic] = { |
52 | - 'level': 0.0, | |
55 | + 'level': 0.0, # then unlock | |
53 | 56 | 'date': datetime.now() |
54 | 57 | } |
55 | 58 | logger.debug(f'unlocked {topic}') |
56 | 59 | |
57 | 60 | |
58 | 61 | # ------------------------------------------------------------------------ |
59 | - # Recommends a topic to practice/learn from the state. | |
60 | - # FIXME untested | |
61 | - # ------------------------------------------------------------------------ | |
62 | - def recommended_topic(self): | |
63 | - return min(self.state.items(), key=lambda x: x[1]['level'])[0] | |
64 | - | |
65 | - | |
66 | - # if not topic: | |
67 | - # for topic in self.topic_sequence: | |
68 | - # unlocked = topic in self.state | |
69 | - # needs_work = unlocked and self.state[topic]['level'] < 0.8 | |
70 | - # factory = self.deps.node[topic]['factory'] | |
71 | - # if factory and (not unlocked or needs_work): | |
72 | - # break | |
73 | - | |
74 | - # use given topic if possible | |
75 | - # else: | |
76 | - # unlocked = topic in self.state | |
77 | - # factory = self.deps.node[topic]['factory'] | |
78 | - # if not factory or not unlocked: | |
79 | - # logger.debug(f'Can\'t start topic "{topic}".') | |
80 | - # return | |
81 | - | |
82 | - | |
83 | - # ------------------------------------------------------------------------ | |
84 | 62 | # Start a new topic. If not provided, gets a recommendation. |
85 | 63 | # questions: list of generated questions to do in the topic |
86 | 64 | # finished_questions: [] will contain correctly answered questions |
... | ... | @@ -90,7 +68,7 @@ class StudentKnowledge(object): |
90 | 68 | logger.debug(f'StudentKnowledge.init_topic({topic})') |
91 | 69 | |
92 | 70 | if not topic: |
93 | - topic = self.recommended_topic() | |
71 | + topic = self.get_recommended_topic() | |
94 | 72 | |
95 | 73 | self.current_topic = topic |
96 | 74 | logger.info(f'Topic set to "{topic}"') |
... | ... | @@ -106,6 +84,22 @@ class StudentKnowledge(object): |
106 | 84 | self.current_question['start_time'] = datetime.now() |
107 | 85 | |
108 | 86 | # ------------------------------------------------------------------------ |
87 | + # The topic has finished and there are no more questions. | |
88 | + # The topic level is updated in state and unlocks are performed. | |
89 | + # The current topic is unchanged. | |
90 | + # ------------------------------------------------------------------------ | |
91 | + def finish_topic(self): | |
92 | + logger.debug(f'StudentKnowledge.finish_topic({self.current_topic})') | |
93 | + | |
94 | + self.current_question = None | |
95 | + self.state[self.current_topic] = { | |
96 | + 'level': 1.0, | |
97 | + 'date': datetime.now() | |
98 | + } | |
99 | + self.unlock_topics() | |
100 | + | |
101 | + | |
102 | + # ------------------------------------------------------------------------ | |
109 | 103 | # returns the current question with correction, time and comments updated |
110 | 104 | # ------------------------------------------------------------------------ |
111 | 105 | def check_answer(self, answer): |
... | ... | @@ -124,13 +118,7 @@ class StudentKnowledge(object): |
124 | 118 | try: |
125 | 119 | self.current_question = self.questions.pop(0) # FIXME empty? |
126 | 120 | except IndexError: |
127 | - # finished topic, no more questions | |
128 | - self.current_question = None | |
129 | - self.state[self.current_topic] = { | |
130 | - 'level': 1.0, | |
131 | - 'date': datetime.now() | |
132 | - } | |
133 | - self.unlock_topics() | |
121 | + self.finish_topic() | |
134 | 122 | else: |
135 | 123 | self.current_question['start_time'] = datetime.now() |
136 | 124 | |
... | ... | @@ -141,7 +129,7 @@ class StudentKnowledge(object): |
141 | 129 | |
142 | 130 | |
143 | 131 | # returns answered and corrected question |
144 | - return q | |
132 | + return grade | |
145 | 133 | |
146 | 134 | |
147 | 135 | # ======================================================================== |
... | ... | @@ -152,6 +140,9 @@ class StudentKnowledge(object): |
152 | 140 | def get_current_question(self): |
153 | 141 | return self.current_question |
154 | 142 | |
143 | + def get_finished_questions(self): | |
144 | + return self.finished_questions | |
145 | + | |
155 | 146 | # ------------------------------------------------------------------------ |
156 | 147 | def get_current_topic(self): |
157 | 148 | return self.current_topic |
... | ... | @@ -172,3 +163,16 @@ class StudentKnowledge(object): |
172 | 163 | def get_topic_progress(self): |
173 | 164 | return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions)) |
174 | 165 | |
166 | + # ------------------------------------------------------------------------ | |
167 | + def get_topic_level(self, topic): | |
168 | + return self.state[topic]['level'] | |
169 | + | |
170 | + # ------------------------------------------------------------------------ | |
171 | + def get_topic_date(self, topic): | |
172 | + return self.state[topic]['date'] | |
173 | + | |
174 | + # ------------------------------------------------------------------------ | |
175 | + # Recommends a topic to practice/learn from the state. | |
176 | + # ------------------------------------------------------------------------ | |
177 | + def get_recommended_topic(self): # FIXME untested | |
178 | + return min(self.state.items(), key=lambda x: x[1]['level'])[0] | ... | ... |
learnapp.py
... | ... | @@ -69,8 +69,8 @@ class LearnApp(object): |
69 | 69 | } |
70 | 70 | |
71 | 71 | self.online[uid] = { |
72 | - 'name': student.name, | |
73 | 72 | 'number': student.id, |
73 | + 'name': student.name, | |
74 | 74 | 'state': StudentKnowledge(self.deps, state=state) |
75 | 75 | } |
76 | 76 | return True |
... | ... | @@ -79,29 +79,28 @@ class LearnApp(object): |
79 | 79 | # logout |
80 | 80 | # ------------------------------------------------------------------------ |
81 | 81 | def logout(self, uid): |
82 | - state = self.online[uid]['state'].state # dict {node:level,...} | |
83 | - | |
84 | - # save state to database | |
85 | - with self.db_session(autoflush=False) as s: | |
86 | - | |
87 | - # update existing associations and remove from state dict | |
88 | - for a in s.query(StudentTopic).filter_by(student_id=uid): | |
89 | - if a.topic_id in state: | |
90 | - d = state.pop(a.topic_id) | |
91 | - a.level = d['level'] #state.pop(a.topic_id) # update | |
92 | - a.date = str(d['date']) | |
93 | - s.add(a) | |
94 | - | |
95 | - # insert the remaining ones | |
96 | - u = s.query(Student).get(uid) | |
97 | - for n,d in state.items(): | |
98 | - a = StudentTopic(level=d['level'], date=str(d['date'])) | |
99 | - t = s.query(Topic).get(n) | |
100 | - if t is None: # create if topic doesn't exist yet | |
101 | - t = Topic(id=n) | |
102 | - a.topic = t | |
103 | - u.topics.append(a) | |
104 | - s.add(a) | |
82 | + # state = self.online[uid]['state'].state # dict {node:level,...} | |
83 | + # # save topics state to database | |
84 | + # with self.db_session(autoflush=False) as s: | |
85 | + | |
86 | + # # update existing associations and remove from state dict | |
87 | + # for a in s.query(StudentTopic).filter_by(student_id=uid): | |
88 | + # if a.topic_id in state: | |
89 | + # d = state.pop(a.topic_id) | |
90 | + # a.level = d['level'] #state.pop(a.topic_id) # update | |
91 | + # a.date = str(d['date']) | |
92 | + # s.add(a) | |
93 | + | |
94 | + # # insert the remaining ones | |
95 | + # u = s.query(Student).get(uid) | |
96 | + # for n,d in state.items(): | |
97 | + # a = StudentTopic(level=d['level'], date=str(d['date'])) | |
98 | + # t = s.query(Topic).get(n) | |
99 | + # if t is None: # create if topic doesn't exist yet | |
100 | + # t = Topic(id=n) | |
101 | + # a.topic = t | |
102 | + # u.topics.append(a) | |
103 | + # s.add(a) | |
105 | 104 | |
106 | 105 | del self.online[uid] |
107 | 106 | logger.info(f'User "{uid}" logged out') |
... | ... | @@ -116,26 +115,51 @@ class LearnApp(object): |
116 | 115 | with self.db_session() as s: |
117 | 116 | u = s.query(Student).get(uid) |
118 | 117 | u.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) |
118 | + | |
119 | 119 | logger.info(f'User "{uid}" changed password') |
120 | 120 | return True |
121 | 121 | |
122 | 122 | # ------------------------------------------------------------------------ |
123 | - # check answer and if correct returns new question, otherwise returns None | |
123 | + # checks answer (updating student state) and returns grade. | |
124 | 124 | # ------------------------------------------------------------------------ |
125 | 125 | def check_answer(self, uid, answer): |
126 | 126 | knowledge = self.online[uid]['state'] |
127 | - q = knowledge.check_answer(answer) | |
127 | + grade = knowledge.check_answer(answer) | |
128 | + | |
129 | + # if finished topic, save in database | |
130 | + if knowledge.get_current_question() is None: | |
131 | + finished_topic = knowledge.get_current_topic() | |
132 | + level = knowledge.get_topic_level(finished_topic) | |
133 | + date = str(knowledge.get_topic_date(finished_topic)) | |
134 | + | |
135 | + with self.db_session(autoflush=False) as s: | |
136 | + # save questions from finished_questions list | |
137 | + s.add_all([ | |
138 | + Answer( | |
139 | + ref=q['ref'], | |
140 | + grade=q['grade'], | |
141 | + starttime=str(q['start_time']), | |
142 | + finishtime=str(q['finish_time']), | |
143 | + student_id=uid) | |
144 | + for q in knowledge.get_finished_questions()]) | |
145 | + | |
146 | + # save topic | |
147 | + a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=finished_topic).one_or_none() | |
148 | + if a is None: | |
149 | + # insert new studenttopic into database | |
150 | + u = s.query(Student).get(uid) | |
151 | + a = StudentTopic(level=level, date=date) | |
152 | + t = s.query(Topic).get(finished_topic) | |
153 | + a.topic = t | |
154 | + u.topics.append(a) | |
155 | + s.add(a) | |
156 | + else: | |
157 | + # update studenttopic in database | |
158 | + a.level = level | |
159 | + a.date = date | |
160 | + s.add(a) | |
128 | 161 | |
129 | - with self.db_session() as s: | |
130 | - s.add(Answer( | |
131 | - ref=q['ref'], | |
132 | - grade=q['grade'], | |
133 | - starttime=str(q['start_time']), | |
134 | - finishtime=str(q['finish_time']), | |
135 | - student_id=uid)) | |
136 | - s.commit() | |
137 | - | |
138 | - return q['grade'] | |
162 | + return grade | |
139 | 163 | |
140 | 164 | # ------------------------------------------------------------------------ |
141 | 165 | # Start new topic | ... | ... |
serve.py
... | ... | @@ -119,7 +119,8 @@ class RootHandler(BaseHandler): |
119 | 119 | self.render('maintopics.html', |
120 | 120 | uid=uid, |
121 | 121 | name=self.learn.get_student_name(uid), |
122 | - state=self.learn.get_student_state(uid) | |
122 | + state=self.learn.get_student_state(uid), | |
123 | + title=self.learn.get_title() | |
123 | 124 | ) |
124 | 125 | |
125 | 126 | # ---------------------------------------------------------------------------- | ... | ... |
templates/maintopics.html
... | ... | @@ -61,14 +61,14 @@ |
61 | 61 | |
62 | 62 | <div id="notifications"></div> |
63 | 63 | |
64 | - <h3>Tópicos</h3> | |
64 | + <h4>{{ title }}</h4> | |
65 | 65 | |
66 | - <div class="list-group"> | |
66 | + <div class="list-group my-3"> | |
67 | 67 | {% for t in state %} |
68 | 68 | {% if t['level'] is None %} |
69 | - <a class="list-group-item list-group-item-action disabled" href="#"> | |
69 | + <a class="list-group-item list-group-item-action bg-light disabled" href="#"> | |
70 | 70 | <div class="d-flex justify-content-start"> |
71 | - <div class="p-2"> | |
71 | + <div class="p-2 font-italic"> | |
72 | 72 | {{ t['name'] }} |
73 | 73 | </div> |
74 | 74 | <div class="ml-auto p-2"> |
... | ... | @@ -87,7 +87,9 @@ |
87 | 87 | |
88 | 88 | <i class="fa fa-unlock text-success" aria-hidden="true"></i> |
89 | 89 | {% else %} |
90 | - {{ round(t['level']*5)*'<i class="fa fa-star text-warning" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | |
90 | + <span class="text-nowrap"> | |
91 | + {{ round(t['level']*5)*'<i class="fa fa-star text-warning" aria-hidden="true"></i>' + round(5-t['level']*5)*'<i class="fa fa-star-o text-muted" aria-hidden="true"></i>' }} | |
92 | + </span> | |
91 | 93 | {% end %} |
92 | 94 | </div> |
93 | 95 | </div> | ... | ... |