Commit ae2dc2b800b823cee22a2e9af129429f92948804

Authored by Miguel Barão
1 parent dda6f838
Exists in master and in 1 other branch dev

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