Commit 065611f74a7e84ff9760b271d4f81a9303e8a80b
1 parent
987cf289
Exists in
master
and in
1 other branch
large ammount of changes, main ones:
- allow multiple instances of same question - wrong answer => new instance of question added at the end of topic - show current topic on sidebar - fixed reload crash - fixed text show previous answer if incorrect not yet working.
Showing
9 changed files
with
284 additions
and
247 deletions
Show diff stats
BUGS.md
1 | 1 | ||
2 | BUGS: | 2 | BUGS: |
3 | 3 | ||
4 | -- nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. ver app.py linha 223. | ||
5 | -- pymips: activar/desactivar instruções | 4 | +- topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. |
6 | - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4) | 5 | - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4) |
7 | - reportar comentarios após submeter. | 6 | - reportar comentarios após submeter. |
8 | -- logs debug mostrar user | ||
9 | -- logs mostrar fim de topico | ||
10 | -- textarea, text devem mostrar no html os valores iniciais de ans, se existir | 7 | +- textarea deve mostrar no html os valores iniciais de ans, se existir |
11 | - detect questions in questions.yaml without ref -> error ou generate default. | 8 | - detect questions in questions.yaml without ref -> error ou generate default. |
12 | - error if demo.yaml has no topics | 9 | - error if demo.yaml has no topics |
13 | -- reload da página rebenta o estado. | ||
14 | - guardar state cada vez que topico termina | 10 | - guardar state cada vez que topico termina |
15 | -- indicar o topico actual no sidebar | ||
16 | - session management. close after inactive time. | 11 | - session management. close after inactive time. |
17 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() | 12 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
18 | - titulos das perguntas não suportam markdown | 13 | - titulos das perguntas não suportam markdown |
19 | 14 | ||
20 | TODO: | 15 | TODO: |
21 | 16 | ||
22 | -- implementar http com redirect para https. | ||
23 | -- topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. | 17 | +- pymips: activar/desactivar instruções |
18 | +- implementar servidor http com redirect para https. | ||
24 | - usar codemirror no textarea | 19 | - usar codemirror no textarea |
25 | -- mostrar comments quando falha a resposta | ||
26 | - generators not working: bcrypt (ver blog) | 20 | - generators not working: bcrypt (ver blog) |
27 | 21 | ||
28 | FIXED: | 22 | FIXED: |
29 | 23 | ||
24 | +- logs inicio de topico | ||
25 | +- indicar o topico actual no sidebar | ||
26 | +- reload da página rebenta o estado. | ||
27 | +- text deve mostrar no html os valores iniciais de ans, se existir | ||
28 | +- nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. ver app.py linha 223. | ||
30 | - level depender do numero de respostas correctas | 29 | - level depender do numero de respostas correctas |
31 | - pymips a funcionar | 30 | - pymips a funcionar |
32 | - logs mostram que está a gerar cada pergunta 2 vezes...?? | 31 | - logs mostram que está a gerar cada pergunta 2 vezes...?? |
app.py
@@ -37,8 +37,8 @@ class LearnApp(object): | @@ -37,8 +37,8 @@ class LearnApp(object): | ||
37 | # online students | 37 | # online students |
38 | self.online = {} | 38 | self.online = {} |
39 | 39 | ||
40 | - # build dependency graph | ||
41 | - self.build_dependency_graph(conffile) | 40 | + self.depgraph = build_dependency_graph(conffile) |
41 | + | ||
42 | 42 | ||
43 | # connect to database and check registered students | 43 | # connect to database and check registered students |
44 | self.db_setup(self.depgraph.graph['database']) | 44 | self.db_setup(self.depgraph.graph['database']) |
@@ -62,6 +62,7 @@ class LearnApp(object): | @@ -62,6 +62,7 @@ class LearnApp(object): | ||
62 | return False # wrong password | 62 | return False # wrong password |
63 | 63 | ||
64 | # success | 64 | # success |
65 | + logger.info(f'User "{uid}" logged in') | ||
65 | 66 | ||
66 | tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) | 67 | tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) |
67 | state = {} | 68 | state = {} |
@@ -76,7 +77,6 @@ class LearnApp(object): | @@ -76,7 +77,6 @@ class LearnApp(object): | ||
76 | 'number': student.id, | 77 | 'number': student.id, |
77 | 'state': Knowledge(self.depgraph, state=state, student=student.id) | 78 | 'state': Knowledge(self.depgraph, state=state, student=student.id) |
78 | } | 79 | } |
79 | - logger.info(f'User "{uid}" logged in') | ||
80 | return True | 80 | return True |
81 | 81 | ||
82 | # ------------------------------------------------------------------------ | 82 | # ------------------------------------------------------------------------ |
@@ -124,115 +124,23 @@ class LearnApp(object): | @@ -124,115 +124,23 @@ class LearnApp(object): | ||
124 | return True | 124 | return True |
125 | 125 | ||
126 | # ------------------------------------------------------------------------ | 126 | # ------------------------------------------------------------------------ |
127 | - def get_student_name(self, uid): | ||
128 | - return self.online[uid].get('name', '') | ||
129 | - | ||
130 | - # ------------------------------------------------------------------------ | ||
131 | - def get_student_state(self, uid): | ||
132 | - return self.online[uid]['state'].get_knowledge_state() | ||
133 | - | ||
134 | - # ------------------------------------------------------------------------ | ||
135 | - def get_student_progress(self, uid): | ||
136 | - return self.online[uid]['state'].get_topic_progress() | ||
137 | - | ||
138 | - # ------------------------------------------------------------------------ | ||
139 | - def get_student_question(self, uid): | ||
140 | - return self.online[uid]['state'].get_current_question() # dict | ||
141 | - | ||
142 | - # ------------------------------------------------------------------------ | ||
143 | - def get_title(self): | ||
144 | - return self.depgraph.graph['title'] | ||
145 | - | ||
146 | - # ------------------------------------------------------------------------ | ||
147 | - def get_topic_name(self, ref): | ||
148 | - return self.depgraph.node[ref]['name'] | ||
149 | - | ||
150 | - # ------------------------------------------------------------------------ | ||
151 | - def get_current_public_dir(self, uid): | ||
152 | - topic = self.online[uid]['state'].get_current_topic() | ||
153 | - p = self.depgraph.graph['path'] | ||
154 | - return path.join(p, topic, 'public') | ||
155 | - | ||
156 | - # ------------------------------------------------------------------------ | ||
157 | # check answer and if correct returns new question, otherwise returns None | 127 | # check answer and if correct returns new question, otherwise returns None |
158 | # ------------------------------------------------------------------------ | 128 | # ------------------------------------------------------------------------ |
159 | def check_answer(self, uid, answer): | 129 | def check_answer(self, uid, answer): |
160 | knowledge = self.online[uid]['state'] | 130 | knowledge = self.online[uid]['state'] |
161 | - current_question = knowledge.check_answer(answer) | 131 | + q = knowledge.check_answer(answer) |
162 | 132 | ||
163 | - if current_question is not None: | ||
164 | - logger.debug('check_answer: saving answer to db ...') | ||
165 | - with self.db_session() as s: | ||
166 | - s.add(Answer( | ||
167 | - ref=current_question['ref'], | ||
168 | - grade=current_question['grade'], | ||
169 | - starttime=str(current_question['start_time']), | ||
170 | - finishtime=str(current_question['finish_time']), | ||
171 | - student_id=uid)) | ||
172 | - s.commit() | ||
173 | - | ||
174 | - return knowledge.new_question() | ||
175 | - | ||
176 | - # ------------------------------------------------------------------------ | ||
177 | - # Given configuration file, loads YAML on that file and builds the graph. | ||
178 | - # First, topics such as `computer/mips/exceptions` are added as nodes | ||
179 | - # together with dependencies. Then, questions are loaded to a factory. | ||
180 | - # ------------------------------------------------------------------------ | ||
181 | - def build_dependency_graph(self, config_file): | ||
182 | - | ||
183 | - # Load configuration file to a dict | ||
184 | - try: | ||
185 | - with open(config_file, 'r') as f: | ||
186 | - config = yaml.load(f) | ||
187 | - except FileNotFoundError: | ||
188 | - logger.critical(f'File not found: "{config_file}"') | ||
189 | - raise LearnAppException | ||
190 | - except yaml.scanner.ScannerError as err: | ||
191 | - logger.critical(f'Parsing YAML file "{config_file}": {err}') | ||
192 | - raise LearnAppException | ||
193 | - else: | ||
194 | - logger.info(f'Configuration file "{config_file}"') | ||
195 | - | ||
196 | - # create graph | ||
197 | - prefix = config.get('path', '.') | ||
198 | - title = config.get('title', '') | ||
199 | - database = config.get('database', 'students.db') | ||
200 | - g = nx.DiGraph(path=prefix, title=title, database=database) | ||
201 | - | ||
202 | - # iterate over topics and build graph | ||
203 | - topics = config.get('topics', {}) | ||
204 | - for ref,attr in topics.items(): | ||
205 | - g.add_node(ref) | ||
206 | - if isinstance(attr, dict): | ||
207 | - g.node[ref]['name'] = attr.get('name', ref) | ||
208 | - g.node[ref]['questions'] = attr.get('questions', []) | ||
209 | - g.add_edges_from((d,ref) for d in attr.get('deps', [])) | ||
210 | - | ||
211 | - # iterate over topics and create question factories | ||
212 | - logger.info('Loading:') | ||
213 | - for ref in g.nodes_iter(): | ||
214 | - fullpath = path.expanduser(path.join(prefix, ref)) | ||
215 | - filename = path.join(fullpath, 'questions.yaml') | ||
216 | - | ||
217 | - loaded_questions = load_yaml(filename, default=[]) | ||
218 | - | ||
219 | - # make dict from list of questions for easier selection | ||
220 | - qdict = {q['ref']: q for q in loaded_questions} | ||
221 | - | ||
222 | - # 'questions' not provided in configuration means load all | ||
223 | - if not g.node[ref]['questions']: | ||
224 | - g.node[ref]['questions'] = qdict.keys() #[q['ref'] for q in loaded_questions] | ||
225 | - | ||
226 | - g.node[ref]['factory'] = [] | ||
227 | - for qref in g.node[ref]['questions']: | ||
228 | - q = qdict[qref] | ||
229 | - q['path'] = fullpath | ||
230 | - g.node[ref]['factory'].append(QFactory(q)) | 133 | + with self.db_session() as s: |
134 | + s.add(Answer( | ||
135 | + ref=q['ref'], | ||
136 | + grade=q['grade'], | ||
137 | + starttime=str(q['start_time']), | ||
138 | + finishtime=str(q['finish_time']), | ||
139 | + student_id=uid)) | ||
140 | + s.commit() | ||
231 | 141 | ||
232 | - logger.info(f' {len(g.node[ref]["factory"])} questions from "{ref}"') | 142 | + return q['grade'] |
233 | 143 | ||
234 | - self.depgraph = g | ||
235 | - return g | ||
236 | 144 | ||
237 | # ------------------------------------------------------------------------ | 145 | # ------------------------------------------------------------------------ |
238 | # Fill db table 'Topic' with topics from the graph if not already there. | 146 | # Fill db table 'Topic' with topics from the graph if not already there. |
@@ -274,3 +182,113 @@ class LearnApp(object): | @@ -274,3 +182,113 @@ class LearnApp(object): | ||
274 | finally: | 182 | finally: |
275 | session.close() | 183 | session.close() |
276 | 184 | ||
185 | + | ||
186 | + | ||
187 | + # ======================================================================== | ||
188 | + # methods that do not change state (pure functions) | ||
189 | + # ======================================================================== | ||
190 | + | ||
191 | + | ||
192 | + # ------------------------------------------------------------------------ | ||
193 | + def get_student_name(self, uid): | ||
194 | + return self.online[uid].get('name', '') | ||
195 | + | ||
196 | + # ------------------------------------------------------------------------ | ||
197 | + def get_student_state(self, uid): | ||
198 | + return self.online[uid]['state'].get_knowledge_state() | ||
199 | + | ||
200 | + # ------------------------------------------------------------------------ | ||
201 | + def get_student_progress(self, uid): | ||
202 | + return self.online[uid]['state'].get_topic_progress() | ||
203 | + | ||
204 | + # ------------------------------------------------------------------------ | ||
205 | + def get_student_question(self, uid): | ||
206 | + return self.online[uid]['state'].get_current_question() # dict | ||
207 | + | ||
208 | + # ------------------------------------------------------------------------ | ||
209 | + def get_student_topic(self, uid): | ||
210 | + return self.online[uid]['state'].get_current_topic() # str | ||
211 | + | ||
212 | + # ------------------------------------------------------------------------ | ||
213 | + def get_title(self): | ||
214 | + return self.depgraph.graph['title'] | ||
215 | + | ||
216 | + # ------------------------------------------------------------------------ | ||
217 | + def get_topic_name(self, ref): | ||
218 | + return self.depgraph.node[ref]['name'] | ||
219 | + | ||
220 | + # ------------------------------------------------------------------------ | ||
221 | + def get_current_public_dir(self, uid): | ||
222 | + topic = self.online[uid]['state'].get_current_topic() | ||
223 | + p = self.depgraph.graph['path'] | ||
224 | + return path.join(p, topic, 'public') | ||
225 | + | ||
226 | + | ||
227 | + | ||
228 | +# ============================================================================ | ||
229 | +# Given configuration file, loads YAML on that file and builds a digraph. | ||
230 | +# First, topics such as `computer/mips/exceptions` are added as nodes | ||
231 | +# together with dependencies. Then, questions are loaded to a factory. | ||
232 | +# | ||
233 | +# g.graph['path'] base path where topic directories are located | ||
234 | +# g.graph['title'] title defined in the configuration YAML | ||
235 | +# g.graph['database'] sqlite3 database file to use | ||
236 | +# | ||
237 | +# Nodes are the topic references e.g. 'my/topic' | ||
238 | +# g.node['my/topic']['name'] name of the topic | ||
239 | +# g.node['my/topic']['questions'] list of question refs defined in YAML | ||
240 | +# g.node['my/topic']['factory'] dict with question factories | ||
241 | +# ---------------------------------------------------------------------------- | ||
242 | +def build_dependency_graph(config_file): | ||
243 | + | ||
244 | + # Load configuration file to a dict | ||
245 | + try: | ||
246 | + with open(config_file, 'r') as f: | ||
247 | + config = yaml.load(f) | ||
248 | + except FileNotFoundError: | ||
249 | + logger.critical(f'File not found: "{config_file}"') | ||
250 | + raise LearnAppException | ||
251 | + except yaml.scanner.ScannerError as err: | ||
252 | + logger.critical(f'Parsing YAML file "{config_file}": {err}') | ||
253 | + raise LearnAppException | ||
254 | + else: | ||
255 | + logger.info(f'Configuration file "{config_file}"') | ||
256 | + | ||
257 | + # create graph | ||
258 | + prefix = config.get('path', '.') | ||
259 | + title = config.get('title', '') | ||
260 | + database = config.get('database', 'students.db') | ||
261 | + g = nx.DiGraph(path=prefix, title=title, database=database) | ||
262 | + | ||
263 | + # iterate over topics and build graph | ||
264 | + topics = config.get('topics', {}) | ||
265 | + for ref,attr in topics.items(): | ||
266 | + g.add_node(ref) | ||
267 | + if isinstance(attr, dict): | ||
268 | + g.node[ref]['name'] = attr.get('name', ref) | ||
269 | + g.node[ref]['questions'] = attr.get('questions', []) | ||
270 | + g.add_edges_from((d,ref) for d in attr.get('deps', [])) | ||
271 | + | ||
272 | + # iterate over topics and create question factories | ||
273 | + logger.info('Loading:') | ||
274 | + for tref in g.nodes_iter(): | ||
275 | + tnode = g.node[tref] # current node (topic) | ||
276 | + fullpath = path.expanduser(path.join(prefix, tref)) | ||
277 | + filename = path.join(fullpath, 'questions.yaml') | ||
278 | + | ||
279 | + loaded_questions = load_yaml(filename, default=[]) | ||
280 | + | ||
281 | + # if questions not in configuration then load all, preserve order | ||
282 | + if not tnode['questions']: | ||
283 | + tnode['questions'] = [q['ref'] for q in loaded_questions] | ||
284 | + | ||
285 | + # make questions factory (without repeting same question) | ||
286 | + tnode['factory'] = {} | ||
287 | + for q in loaded_questions: | ||
288 | + if q['ref'] in tnode['questions']: | ||
289 | + q['path'] = fullpath | ||
290 | + tnode['factory'][q['ref']] = QFactory(q) | ||
291 | + | ||
292 | + logger.info(f' {len(tnode["questions"])} questions from "{tref}"') | ||
293 | + | ||
294 | + return g |
knowledge.py
@@ -17,41 +17,87 @@ logger = logging.getLogger(__name__) | @@ -17,41 +17,87 @@ logger = logging.getLogger(__name__) | ||
17 | # kowledge state of each student....?? | 17 | # kowledge state of each student....?? |
18 | # ---------------------------------------------------------------------------- | 18 | # ---------------------------------------------------------------------------- |
19 | class Knowledge(object): | 19 | class Knowledge(object): |
20 | + # ======================================================================= | ||
21 | + # methods that update state | ||
22 | + # ======================================================================= | ||
23 | + | ||
20 | def __init__(self, depgraph, state={}, student=''): | 24 | def __init__(self, depgraph, state={}, student=''): |
21 | self.depgraph = depgraph | 25 | self.depgraph = depgraph |
22 | self.state = state # {'topic_id': {'level':0.5, 'date': datetime}, ...} | 26 | self.state = state # {'topic_id': {'level':0.5, 'date': datetime}, ...} |
23 | self.student = student | 27 | self.student = student |
24 | 28 | ||
25 | - # compute recommended sequence of topics (FIXME) | 29 | + # compute recommended sequence of topics ['a', 'b',...] |
26 | self.topic_sequence = nx.topological_sort(self.depgraph) | 30 | self.topic_sequence = nx.topological_sort(self.depgraph) |
27 | 31 | ||
28 | - # select a topic to do | ||
29 | - self.new_topic() | 32 | + print(self.topic_sequence) |
33 | + print(self.depgraph.edges()) | ||
34 | + | ||
35 | + # select a topic to do and initialize questions | ||
36 | + self.start_topic() | ||
30 | 37 | ||
31 | # ------------------------------------------------------------------------ | 38 | # ------------------------------------------------------------------------ |
32 | # Start a new topic. If not provided, selects the first with level < 0.8 | 39 | # Start a new topic. If not provided, selects the first with level < 0.8 |
33 | # If all levels > 0.8, will stay in the last one forever... | 40 | # If all levels > 0.8, will stay in the last one forever... |
34 | # ------------------------------------------------------------------------ | 41 | # ------------------------------------------------------------------------ |
35 | - def new_topic(self, topic=None): | 42 | + def start_topic(self, topic=None): |
36 | if topic is None: | 43 | if topic is None: |
37 | for topic in self.topic_sequence: | 44 | for topic in self.topic_sequence: |
38 | - if topic not in self.state or self.state[topic]['level'] < 0.8: | 45 | + unlocked = topic in self.state |
46 | + needs_work = unlocked and self.state[topic]['level'] < 0.8 | ||
47 | + factory = self.depgraph.node[topic]['factory'] | ||
48 | + if needs_work and factory: | ||
39 | break | 49 | break |
40 | - | ||
41 | - logger.info(f'Student {self.student} new topic "{topic}"') | 50 | + # logger.info(f'{self.student} skipped topic "{topic}"') |
51 | + else: | ||
52 | + factory = self.depgraph.node[topic]['factory'] | ||
53 | + # FIXME if factory is empty??? | ||
42 | 54 | ||
43 | self.current_topic = topic | 55 | self.current_topic = topic |
44 | - # self.current_topic_idx = self.topic_sequence.index(topic) | ||
45 | - self.questions = self.generate_questions_for_topic(topic) | ||
46 | - self.current_question = None | 56 | + logger.info(f'User "{self.student}" topic set to "{topic}"') |
57 | + | ||
58 | + # generate question instances for current topic | ||
59 | + questionlist = self.depgraph.node[topic]['questions'] | ||
60 | + factory = self.depgraph.node[topic]['factory'] | ||
61 | + self.questions = [factory[qref].generate() for qref in questionlist] | ||
62 | + | ||
63 | + self.current_question = self.questions.pop(0) | ||
64 | + self.current_question['start_time'] = datetime.now() | ||
47 | self.finished_questions = [] | 65 | self.finished_questions = [] |
48 | - self.correct_answers = 1 | ||
49 | - self.wrong_answers = 0 | ||
50 | 66 | ||
51 | # ------------------------------------------------------------------------ | 67 | # ------------------------------------------------------------------------ |
52 | - def generate_questions_for_topic(self, topic): | ||
53 | - factory = self.depgraph.node[topic]['factory'] | ||
54 | - return [q.generate() for q in factory] | 68 | + # returns the current question with correction, time and comments updated |
69 | + # ------------------------------------------------------------------------ | ||
70 | + def check_answer(self, answer): | ||
71 | + q = self.current_question | ||
72 | + q['finish_time'] = datetime.now() | ||
73 | + grade = q.correct(answer) | ||
74 | + logger.debug(f'User {self.student}: grade = {grade}') | ||
75 | + | ||
76 | + # new question if answer is correct | ||
77 | + if grade > 0.999: | ||
78 | + self.finished_questions.append(q) | ||
79 | + try: | ||
80 | + self.current_question = self.questions.pop(0) # FIXME empty? | ||
81 | + except IndexError: | ||
82 | + self.current_question = None | ||
83 | + self.state[self.current_topic] = { | ||
84 | + 'level': 1.0, | ||
85 | + 'date': datetime.now() | ||
86 | + } | ||
87 | + else: | ||
88 | + self.current_question['start_time'] = datetime.now() | ||
89 | + else: | ||
90 | + # FIXME debug this | ||
91 | + factory = self.depgraph.node[self.current_topic]['factory'] | ||
92 | + self.questions.append(factory[q['ref']].generate()) | ||
93 | + print([q['ref'] for q in self.questions]) | ||
94 | + | ||
95 | + return q | ||
96 | + | ||
97 | + | ||
98 | + # ======================================================================== | ||
99 | + # pure functions of the state (no side effects) | ||
100 | + # ======================================================================== | ||
55 | 101 | ||
56 | # ------------------------------------------------------------------------ | 102 | # ------------------------------------------------------------------------ |
57 | def get_current_question(self): | 103 | def get_current_question(self): |
@@ -62,8 +108,8 @@ class Knowledge(object): | @@ -62,8 +108,8 @@ class Knowledge(object): | ||
62 | return self.current_topic | 108 | return self.current_topic |
63 | 109 | ||
64 | # ------------------------------------------------------------------------ | 110 | # ------------------------------------------------------------------------ |
65 | - def get_knowledge_state(self): | ||
66 | - ts = [] # FIXME why list?? | 111 | + def get_knowledge_state(self): # [('topic', 0.9), ...] |
112 | + ts = [] | ||
67 | for t in self.topic_sequence: | 113 | for t in self.topic_sequence: |
68 | if t in self.state: | 114 | if t in self.state: |
69 | ts.append((t, self.state[t]['level'])) | 115 | ts.append((t, self.state[t]['level'])) |
@@ -73,44 +119,5 @@ class Knowledge(object): | @@ -73,44 +119,5 @@ class Knowledge(object): | ||
73 | 119 | ||
74 | # ------------------------------------------------------------------------ | 120 | # ------------------------------------------------------------------------ |
75 | def get_topic_progress(self): | 121 | def get_topic_progress(self): |
76 | - return len(self.finished_questions) / (len(self.finished_questions) + len(self.questions)) | ||
77 | - | ||
78 | - # ------------------------------------------------------------------------ | ||
79 | - # if answer to current question is correct generates a new question | ||
80 | - # otherwise returns None | ||
81 | - def new_question(self): | ||
82 | - if self.current_question is None or \ | ||
83 | - self.current_question.get('grade', 0.0) > 0.9: | ||
84 | - | ||
85 | - # if no more questions in this topic, go to the next one | ||
86 | - # keep going if there are no questions in the next topics | ||
87 | - while not self.questions: | ||
88 | - self.state[self.current_topic] = { | ||
89 | - 'level': self.correct_answers / (self.correct_answers + self.wrong_answers), | ||
90 | - 'date': datetime.now() | ||
91 | - } | ||
92 | - self.new_topic() | ||
93 | - | ||
94 | - self.current_question = self.questions.pop(0) | ||
95 | - self.current_question['start_time'] = datetime.now() | ||
96 | - self.finished_questions.append(self.current_question) | ||
97 | - | ||
98 | - logger.debug(f'Student {self.student}: new_question({self.current_question["ref"]})') | ||
99 | - return self.current_question | ||
100 | - | ||
101 | - # ------------------------------------------------------------------------ | ||
102 | - # returns the current question with correction, time and comments updated | ||
103 | - # ------------------------------------------------------------------------ | ||
104 | - def check_answer(self, answer): | ||
105 | - question = self.current_question | ||
106 | - if question is not None: | ||
107 | - question['finish_time'] = datetime.now() | ||
108 | - grade = question.correct(answer) | ||
109 | - if grade > 0.9: | ||
110 | - self.correct_answers += 1 | ||
111 | - else: | ||
112 | - self.wrong_answers +=1 | ||
113 | - | ||
114 | - logger.debug(f'Student {self.student}: check_answer({answer}) = {grade}') | 122 | + return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions)) |
115 | 123 | ||
116 | - return question |
questions.py
@@ -416,20 +416,11 @@ class QFactory(object): | @@ -416,20 +416,11 @@ class QFactory(object): | ||
416 | # which will print a valid question in yaml format to stdout. This | 416 | # which will print a valid question in yaml format to stdout. This |
417 | # output is then yaml parsed into a dictionary `q`. | 417 | # output is then yaml parsed into a dictionary `q`. |
418 | if q['type'] == 'generator': | 418 | if q['type'] == 'generator': |
419 | - logger.debug(f'Running script "{q["script"]}"...') | 419 | + logger.debug(f' \_ Running script "{q["script"]}"...') |
420 | q.setdefault('arg', '') # optional arguments will be sent to stdin | 420 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
421 | script = path.join(q['path'], q['script']) | 421 | script = path.join(q['path'], q['script']) |
422 | out = run_script(script=script, stdin=q['arg']) | 422 | out = run_script(script=script, stdin=q['arg']) |
423 | q.update(out) | 423 | q.update(out) |
424 | - # try: | ||
425 | - # q.update(out) | ||
426 | - # except: | ||
427 | - # logger.error(f'Question generator "{q["ref"]}"') | ||
428 | - # q.update({ | ||
429 | - # 'type': 'alert', | ||
430 | - # 'title': 'Erro interno', | ||
431 | - # 'text': 'Ocorreu um erro a gerar esta pergunta.' | ||
432 | - # }) | ||
433 | 424 | ||
434 | # Finally we create an instance of Question() | 425 | # Finally we create an instance of Question() |
435 | try: | 426 | try: |
serve.py
@@ -160,70 +160,68 @@ class QuestionHandler(BaseHandler): | @@ -160,70 +160,68 @@ class QuestionHandler(BaseHandler): | ||
160 | # 'alert': '', FIXME | 160 | # 'alert': '', FIXME |
161 | } | 161 | } |
162 | 162 | ||
163 | - @tornado.web.authenticated | ||
164 | - def get(self): | ||
165 | - user = self.current_user | ||
166 | - state = self.learn.get_student_state(user) # all topics | 163 | + def new_question(self, user): |
164 | + state = self.learn.get_student_state(user) # all topics [('a', 0.1), ...] | ||
165 | + current_topic = self.learn.get_student_topic(user) # str | ||
167 | progress = self.learn.get_student_progress(user) # float | 166 | progress = self.learn.get_student_progress(user) # float |
168 | - question = self.learn.get_current_question(user) | ||
169 | - print(question) # FIXME cant get current question | 167 | + question = self.learn.get_student_question(user) # dict? |
168 | + | ||
169 | + question_html = self.render_string(self.templates[question['type']],question=question, md=md) | ||
170 | + topics_html = self.render_string('topics.html', state=state, current_topic=current_topic, gettopicname=self.learn.get_topic_name) | ||
171 | + | ||
172 | + return { | ||
173 | + 'method': 'new_question', | ||
174 | + 'params': { | ||
175 | + 'question': tornado.escape.to_unicode(question_html), | ||
176 | + 'state': tornado.escape.to_unicode(topics_html), | ||
177 | + 'progress': progress, | ||
178 | + } | ||
179 | + } | ||
180 | + | ||
181 | + def shake(self, user): | ||
182 | + progress = self.learn.get_student_progress(user) # in the current topic | ||
183 | + return { | ||
184 | + 'method': 'shake', | ||
185 | + 'params': { | ||
186 | + 'progress': progress, | ||
187 | + } | ||
188 | + } | ||
189 | + | ||
190 | + def finished_topic(self, user): | ||
191 | + state = self.learn.get_student_state(user) # all topics | ||
192 | + current_topic = self.learn.get_student_topic(uid) | ||
170 | 193 | ||
171 | - question_html = self.render_string( | ||
172 | - self.templates[question['type']], | ||
173 | - question=question, # dictionary with the question | ||
174 | - md=md, # function that renders markdown to html | ||
175 | - ) | ||
176 | topics_html = self.render_string('topics.html', | 194 | topics_html = self.render_string('topics.html', |
177 | state=state, | 195 | state=state, |
196 | + current_topic=current_topic, | ||
178 | topicname=self.learn.get_topic_name, # translate ref to names | 197 | topicname=self.learn.get_topic_name, # translate ref to names |
179 | ) | 198 | ) |
180 | - print(topics_html) | ||
181 | - print(question_html) | ||
182 | - self.write({ | ||
183 | - 'method': 'new_question', | 199 | + return { |
200 | + 'method': 'finished_topic', | ||
184 | 'params': { | 201 | 'params': { |
185 | - 'question': tornado.escape.to_unicode(question_html), | ||
186 | 'state': tornado.escape.to_unicode(topics_html), | 202 | 'state': tornado.escape.to_unicode(topics_html), |
187 | - 'progress': progress, | ||
188 | } | 203 | } |
189 | - }) | 204 | + } |
190 | 205 | ||
191 | - # posting can change state and return new question | 206 | + @tornado.web.authenticated |
207 | + def get(self): | ||
208 | + self.write(self.new_question(self.current_user)) | ||
209 | + | ||
210 | + # handles answer posted | ||
192 | @tornado.web.authenticated | 211 | @tornado.web.authenticated |
193 | def post(self): | 212 | def post(self): |
194 | user = self.current_user | 213 | user = self.current_user |
195 | answer = self.get_body_arguments('answer') | 214 | answer = self.get_body_arguments('answer') |
196 | - next_question = self.learn.check_answer(user, answer) | ||
197 | - state = self.learn.get_student_state(user) # all topics | ||
198 | - progress = self.learn.get_student_progress(user) # in the current topic | ||
199 | - | ||
200 | - if next_question is not None: | ||
201 | - question_html = self.render_string( | ||
202 | - self.templates[next_question['type']], | ||
203 | - question=next_question, # dictionary with the question | ||
204 | - md=md, # function that renders markdown to html | ||
205 | - ) | ||
206 | - topics_html = self.render_string( | ||
207 | - 'topics.html', | ||
208 | - state=state, | ||
209 | - topicname=self.learn.get_topic_name, # function that translates topic references to names | ||
210 | - ) | ||
211 | - | ||
212 | - self.write({ | ||
213 | - 'method': 'new_question', | ||
214 | - 'params': { | ||
215 | - 'question': tornado.escape.to_unicode(question_html), | ||
216 | - 'state': tornado.escape.to_unicode(topics_html), | ||
217 | - 'progress': progress, | ||
218 | - }, | ||
219 | - }) | 215 | + grade = self.learn.check_answer(user, answer) |
216 | + question = self.learn.get_student_question(user) # same, new or None | ||
217 | + | ||
218 | + if question is None: | ||
219 | + self.write(self.finished_topic(user)) | ||
220 | + elif grade > 0.999: | ||
221 | + self.write(self.new_question(user)) | ||
220 | else: | 222 | else: |
221 | - self.write({ | ||
222 | - 'method': 'shake', | ||
223 | - 'params': { | ||
224 | - 'progress': progress, | ||
225 | - }, | ||
226 | - }) | 223 | + self.write(self.shake(user)) |
224 | + | ||
227 | 225 | ||
228 | # ---------------------------------------------------------------------------- | 226 | # ---------------------------------------------------------------------------- |
229 | def main(): | 227 | def main(): |
@@ -262,15 +260,15 @@ def main(): | @@ -262,15 +260,15 @@ def main(): | ||
262 | }) | 260 | }) |
263 | http_server.listen(8443) | 261 | http_server.listen(8443) |
264 | 262 | ||
265 | - # --- start webserver | 263 | + # --- run webserver |
266 | logging.info('Webserver running...') | 264 | logging.info('Webserver running...') |
265 | + | ||
267 | try: | 266 | try: |
268 | - tornado.ioloop.IOLoop.current().start() | ||
269 | - # running... | 267 | + tornado.ioloop.IOLoop.current().start() # running... |
270 | except KeyboardInterrupt: | 268 | except KeyboardInterrupt: |
271 | tornado.ioloop.IOLoop.current().stop() | 269 | tornado.ioloop.IOLoop.current().stop() |
272 | - finally: | ||
273 | - logging.critical('Webserver stopped.') | 270 | + |
271 | + logging.critical('Webserver stopped.') | ||
274 | 272 | ||
275 | # ---------------------------------------------------------------------------- | 273 | # ---------------------------------------------------------------------------- |
276 | if __name__ == "__main__": | 274 | if __name__ == "__main__": |
templates/learn.html
@@ -153,7 +153,7 @@ $.fn.extend({ | @@ -153,7 +153,7 @@ $.fn.extend({ | ||
153 | } | 153 | } |
154 | }); | 154 | }); |
155 | 155 | ||
156 | -// Processes the response given by the server after an answer is submitted. | 156 | +// Process the response given by the server |
157 | function updateQuestion(response){ | 157 | function updateQuestion(response){ |
158 | switch (response["method"]) { | 158 | switch (response["method"]) { |
159 | case "new_question": | 159 | case "new_question": |
@@ -194,12 +194,20 @@ function updateQuestion(response){ | @@ -194,12 +194,20 @@ function updateQuestion(response){ | ||
194 | // audio.play(); | 194 | // audio.play(); |
195 | $('#question_div').animateCSS('zoomIn'); | 195 | $('#question_div').animateCSS('zoomIn'); |
196 | break; | 196 | break; |
197 | + | ||
197 | case "shake": | 198 | case "shake": |
198 | // var audio = new Audio('/static/sounds/wrong.mp3'); | 199 | // var audio = new Audio('/static/sounds/wrong.mp3'); |
199 | // audio.play(); | 200 | // audio.play(); |
200 | $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); | 201 | $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); |
201 | $('#question_div').animateCSS('shake'); | 202 | $('#question_div').animateCSS('shake'); |
202 | break; | 203 | break; |
204 | + | ||
205 | + case "finished_topic": | ||
206 | + $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100); | ||
207 | + $("#topics").html(response["params"]["state"]); | ||
208 | + | ||
209 | + $("#question_div").html('<img src="/static/trophy.png" alt="trophy" class="img-rounded img-responsive center-block">'); // FIXME size | ||
210 | + break; | ||
203 | } | 211 | } |
204 | } | 212 | } |
205 | 213 | ||
@@ -217,14 +225,12 @@ function postQuestion() { | @@ -217,14 +225,12 @@ function postQuestion() { | ||
217 | }); | 225 | }); |
218 | } | 226 | } |
219 | 227 | ||
220 | -// Send answer and receive a response. | ||
221 | -// The response can be a new_question or a shake if the answer is wrong. | 228 | +// Get current question |
222 | function getQuestion() { | 229 | function getQuestion() { |
223 | $.ajax({ | 230 | $.ajax({ |
224 | // type: "GET", | 231 | // type: "GET", |
225 | url: "/question", | 232 | url: "/question", |
226 | // headers: {"X-XSRFToken": token}, | 233 | // headers: {"X-XSRFToken": token}, |
227 | - // data: $("#question_form").serialize(), // {'a':10,'b':20}, | ||
228 | dataType: "json", // expected from server | 234 | dataType: "json", // expected from server |
229 | success: updateQuestion, | 235 | success: updateQuestion, |
230 | error: function() {alert("O servidor não responde.");} | 236 | error: function() {alert("O servidor não responde.");} |
templates/question-text.html
@@ -2,7 +2,11 @@ | @@ -2,7 +2,11 @@ | ||
2 | 2 | ||
3 | {% block answer %} | 3 | {% block answer %} |
4 | <fieldset data-role="controlgroup"> | 4 | <fieldset data-role="controlgroup"> |
5 | - <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'] or '' }}" autofocus> | 5 | + {% if question['answer'] %} |
6 | + <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'][0] }}" autofocus> | ||
7 | + {% else %} | ||
8 | + <input type="text" class="form-control" id="answer" name="answer" value="" autofocus> | ||
9 | + {% end %} | ||
6 | </fieldset><br /> | 10 | </fieldset><br /> |
7 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | 11 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
8 | {% end %} | 12 | {% end %} |
templates/question-textarea.html
1 | {% extends "question.html" %} | 1 | {% extends "question.html" %} |
2 | 2 | ||
3 | {% block answer %} | 3 | {% block answer %} |
4 | + | ||
4 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" autofocus>{{ question['answer'] or '' }}</textarea><br /> | 5 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" autofocus>{{ question['answer'] or '' }}</textarea><br /> |
5 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> | 6 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
7 | + | ||
6 | {% end %} | 8 | {% end %} |
templates/topics.html
@@ -4,10 +4,22 @@ | @@ -4,10 +4,22 @@ | ||
4 | 4 | ||
5 | <ul class="nav nav-pills nav-stacked"> | 5 | <ul class="nav nav-pills nav-stacked"> |
6 | {% for t in state %} | 6 | {% for t in state %} |
7 | - <li role="presentation" class="disabled"> <!-- class="active" --> | ||
8 | - <a href="#" class="disabled">{{ topicname(t[0]) }}<br> | ||
9 | - {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | ||
10 | - </a> | ||
11 | - </li> | 7 | + |
8 | + {% if t[0] == current_topic %} | ||
9 | + <li class="active"> <!-- class="active" class="disabled" --> | ||
10 | + | ||
11 | + <a> {{ gettopicname(t[0]) }}<br> | ||
12 | + {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | ||
13 | + </a> | ||
14 | + | ||
15 | + {% else %} | ||
16 | + <li> <!-- class="active" class="disabled" --> | ||
17 | + | ||
18 | + <a href="#"> {{ gettopicname(t[0]) }}<br> | ||
19 | + {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }} | ||
20 | + </a> | ||
21 | + | ||
22 | + {% end %} | ||
23 | + </li> | ||
12 | {% end %} | 24 | {% end %} |
13 | -</ul> | ||
14 | \ No newline at end of file | 25 | \ No newline at end of file |
26 | +</ul> |