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