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 | BUGS: | 2 | BUGS: |
3 | 3 | ||
4 | -- gravar evolucao na bd no final de cada topico. | 4 | +- melhorar markdown das tabelas. |
5 | - servir imagens/ficheiros. | 5 | - servir imagens/ficheiros. |
6 | - topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores. | 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,6 +29,7 @@ TODO: | ||
29 | 29 | ||
30 | FIXED: | 30 | FIXED: |
31 | 31 | ||
32 | +- gravar evolucao na bd no final de cada topico. | ||
32 | - submeter questoes radio, da erro se nao escolher nenhuma opção. | 33 | - submeter questoes radio, da erro se nao escolher nenhuma opção. |
33 | - indentação da primeira linha de código não funciona. | 34 | - indentação da primeira linha de código não funciona. |
34 | - markdown com o mistune. | 35 | - markdown com o mistune. |
knowledge.py
@@ -24,7 +24,7 @@ class StudentKnowledge(object): | @@ -24,7 +24,7 @@ class StudentKnowledge(object): | ||
24 | # graph with topic dependencies shared between all students | 24 | # graph with topic dependencies shared between all students |
25 | self.deps = deps | 25 | self.deps = deps |
26 | 26 | ||
27 | - # state only contains information on unlocked topics | 27 | + # state only contains unlocked topics |
28 | # {'topic_id': {'level':0.5, 'date': datetime}, ...} | 28 | # {'topic_id': {'level':0.5, 'date': datetime}, ...} |
29 | self.state = state | 29 | self.state = state |
30 | 30 | ||
@@ -32,7 +32,7 @@ class StudentKnowledge(object): | @@ -32,7 +32,7 @@ class StudentKnowledge(object): | ||
32 | now = datetime.now() | 32 | now = datetime.now() |
33 | for s in state.values(): | 33 | for s in state.values(): |
34 | dt = now - s['date'] | 34 | dt = now - s['date'] |
35 | - s['level'] *= 0.975 ** dt.days | 35 | + s['level'] *= 0.975 ** dt.days # forgetting factor |
36 | 36 | ||
37 | # compute recommended sequence of topics ['a', 'b', ...] | 37 | # compute recommended sequence of topics ['a', 'b', ...] |
38 | self.topic_sequence = list(nx.topological_sort(self.deps)) | 38 | self.topic_sequence = list(nx.topological_sort(self.deps)) |
@@ -43,44 +43,22 @@ class StudentKnowledge(object): | @@ -43,44 +43,22 @@ class StudentKnowledge(object): | ||
43 | # Unlock topics whose dependencies are satisfied (> min_level) | 43 | # Unlock topics whose dependencies are satisfied (> min_level) |
44 | # ------------------------------------------------------------------------ | 44 | # ------------------------------------------------------------------------ |
45 | def unlock_topics(self): | 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 | for topic in self.topic_sequence: | 50 | for topic in self.topic_sequence: |
48 | if topic not in self.state: # if locked | 51 | if topic not in self.state: # if locked |
49 | pred = self.deps.predecessors(topic) | 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 | self.state[topic] = { | 54 | self.state[topic] = { |
52 | - 'level': 0.0, | 55 | + 'level': 0.0, # then unlock |
53 | 'date': datetime.now() | 56 | 'date': datetime.now() |
54 | } | 57 | } |
55 | logger.debug(f'unlocked {topic}') | 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 | # Start a new topic. If not provided, gets a recommendation. | 62 | # Start a new topic. If not provided, gets a recommendation. |
85 | # questions: list of generated questions to do in the topic | 63 | # questions: list of generated questions to do in the topic |
86 | # finished_questions: [] will contain correctly answered questions | 64 | # finished_questions: [] will contain correctly answered questions |
@@ -90,7 +68,7 @@ class StudentKnowledge(object): | @@ -90,7 +68,7 @@ class StudentKnowledge(object): | ||
90 | logger.debug(f'StudentKnowledge.init_topic({topic})') | 68 | logger.debug(f'StudentKnowledge.init_topic({topic})') |
91 | 69 | ||
92 | if not topic: | 70 | if not topic: |
93 | - topic = self.recommended_topic() | 71 | + topic = self.get_recommended_topic() |
94 | 72 | ||
95 | self.current_topic = topic | 73 | self.current_topic = topic |
96 | logger.info(f'Topic set to "{topic}"') | 74 | logger.info(f'Topic set to "{topic}"') |
@@ -106,6 +84,22 @@ class StudentKnowledge(object): | @@ -106,6 +84,22 @@ class StudentKnowledge(object): | ||
106 | self.current_question['start_time'] = datetime.now() | 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 | # returns the current question with correction, time and comments updated | 103 | # returns the current question with correction, time and comments updated |
110 | # ------------------------------------------------------------------------ | 104 | # ------------------------------------------------------------------------ |
111 | def check_answer(self, answer): | 105 | def check_answer(self, answer): |
@@ -124,13 +118,7 @@ class StudentKnowledge(object): | @@ -124,13 +118,7 @@ class StudentKnowledge(object): | ||
124 | try: | 118 | try: |
125 | self.current_question = self.questions.pop(0) # FIXME empty? | 119 | self.current_question = self.questions.pop(0) # FIXME empty? |
126 | except IndexError: | 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 | else: | 122 | else: |
135 | self.current_question['start_time'] = datetime.now() | 123 | self.current_question['start_time'] = datetime.now() |
136 | 124 | ||
@@ -141,7 +129,7 @@ class StudentKnowledge(object): | @@ -141,7 +129,7 @@ class StudentKnowledge(object): | ||
141 | 129 | ||
142 | 130 | ||
143 | # returns answered and corrected question | 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,6 +140,9 @@ class StudentKnowledge(object): | ||
152 | def get_current_question(self): | 140 | def get_current_question(self): |
153 | return self.current_question | 141 | return self.current_question |
154 | 142 | ||
143 | + def get_finished_questions(self): | ||
144 | + return self.finished_questions | ||
145 | + | ||
155 | # ------------------------------------------------------------------------ | 146 | # ------------------------------------------------------------------------ |
156 | def get_current_topic(self): | 147 | def get_current_topic(self): |
157 | return self.current_topic | 148 | return self.current_topic |
@@ -172,3 +163,16 @@ class StudentKnowledge(object): | @@ -172,3 +163,16 @@ class StudentKnowledge(object): | ||
172 | def get_topic_progress(self): | 163 | def get_topic_progress(self): |
173 | return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions)) | 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,8 +69,8 @@ class LearnApp(object): | ||
69 | } | 69 | } |
70 | 70 | ||
71 | self.online[uid] = { | 71 | self.online[uid] = { |
72 | - 'name': student.name, | ||
73 | 'number': student.id, | 72 | 'number': student.id, |
73 | + 'name': student.name, | ||
74 | 'state': StudentKnowledge(self.deps, state=state) | 74 | 'state': StudentKnowledge(self.deps, state=state) |
75 | } | 75 | } |
76 | return True | 76 | return True |
@@ -79,29 +79,28 @@ class LearnApp(object): | @@ -79,29 +79,28 @@ class LearnApp(object): | ||
79 | # logout | 79 | # logout |
80 | # ------------------------------------------------------------------------ | 80 | # ------------------------------------------------------------------------ |
81 | def logout(self, uid): | 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 | del self.online[uid] | 105 | del self.online[uid] |
107 | logger.info(f'User "{uid}" logged out') | 106 | logger.info(f'User "{uid}" logged out') |
@@ -116,26 +115,51 @@ class LearnApp(object): | @@ -116,26 +115,51 @@ class LearnApp(object): | ||
116 | with self.db_session() as s: | 115 | with self.db_session() as s: |
117 | u = s.query(Student).get(uid) | 116 | u = s.query(Student).get(uid) |
118 | u.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) | 117 | u.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) |
118 | + | ||
119 | logger.info(f'User "{uid}" changed password') | 119 | logger.info(f'User "{uid}" changed password') |
120 | return True | 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 | def check_answer(self, uid, answer): | 125 | def check_answer(self, uid, answer): |
126 | knowledge = self.online[uid]['state'] | 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 | # Start new topic | 165 | # Start new topic |
serve.py
@@ -119,7 +119,8 @@ class RootHandler(BaseHandler): | @@ -119,7 +119,8 @@ class RootHandler(BaseHandler): | ||
119 | self.render('maintopics.html', | 119 | self.render('maintopics.html', |
120 | uid=uid, | 120 | uid=uid, |
121 | name=self.learn.get_student_name(uid), | 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,14 +61,14 @@ | ||
61 | 61 | ||
62 | <div id="notifications"></div> | 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 | {% for t in state %} | 67 | {% for t in state %} |
68 | {% if t['level'] is None %} | 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 | <div class="d-flex justify-content-start"> | 70 | <div class="d-flex justify-content-start"> |
71 | - <div class="p-2"> | 71 | + <div class="p-2 font-italic"> |
72 | {{ t['name'] }} | 72 | {{ t['name'] }} |
73 | </div> | 73 | </div> |
74 | <div class="ml-auto p-2"> | 74 | <div class="ml-auto p-2"> |
@@ -87,7 +87,9 @@ | @@ -87,7 +87,9 @@ | ||
87 | 87 | ||
88 | <i class="fa fa-unlock text-success" aria-hidden="true"></i> | 88 | <i class="fa fa-unlock text-success" aria-hidden="true"></i> |
89 | {% else %} | 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 | {% end %} | 93 | {% end %} |
92 | </div> | 94 | </div> |
93 | </div> | 95 | </div> |