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 | 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 | 5 | - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4) |
7 | 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 | 8 | - detect questions in questions.yaml without ref -> error ou generate default. |
12 | 9 | - error if demo.yaml has no topics |
13 | -- reload da página rebenta o estado. | |
14 | 10 | - guardar state cada vez que topico termina |
15 | -- indicar o topico actual no sidebar | |
16 | 11 | - session management. close after inactive time. |
17 | 12 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
18 | 13 | - titulos das perguntas não suportam markdown |
19 | 14 | |
20 | 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 | 19 | - usar codemirror no textarea |
25 | -- mostrar comments quando falha a resposta | |
26 | 20 | - generators not working: bcrypt (ver blog) |
27 | 21 | |
28 | 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 | 29 | - level depender do numero de respostas correctas |
31 | 30 | - pymips a funcionar |
32 | 31 | - logs mostram que está a gerar cada pergunta 2 vezes...?? | ... | ... |
app.py
... | ... | @@ -37,8 +37,8 @@ class LearnApp(object): |
37 | 37 | # online students |
38 | 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 | 43 | # connect to database and check registered students |
44 | 44 | self.db_setup(self.depgraph.graph['database']) |
... | ... | @@ -62,6 +62,7 @@ class LearnApp(object): |
62 | 62 | return False # wrong password |
63 | 63 | |
64 | 64 | # success |
65 | + logger.info(f'User "{uid}" logged in') | |
65 | 66 | |
66 | 67 | tt = s.query(StudentTopic).filter(StudentTopic.student_id == uid) |
67 | 68 | state = {} |
... | ... | @@ -76,7 +77,6 @@ class LearnApp(object): |
76 | 77 | 'number': student.id, |
77 | 78 | 'state': Knowledge(self.depgraph, state=state, student=student.id) |
78 | 79 | } |
79 | - logger.info(f'User "{uid}" logged in') | |
80 | 80 | return True |
81 | 81 | |
82 | 82 | # ------------------------------------------------------------------------ |
... | ... | @@ -124,115 +124,23 @@ class LearnApp(object): |
124 | 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 | 127 | # check answer and if correct returns new question, otherwise returns None |
158 | 128 | # ------------------------------------------------------------------------ |
159 | 129 | def check_answer(self, uid, answer): |
160 | 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 | 146 | # Fill db table 'Topic' with topics from the graph if not already there. |
... | ... | @@ -274,3 +182,113 @@ class LearnApp(object): |
274 | 182 | finally: |
275 | 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 | 17 | # kowledge state of each student....?? |
18 | 18 | # ---------------------------------------------------------------------------- |
19 | 19 | class Knowledge(object): |
20 | + # ======================================================================= | |
21 | + # methods that update state | |
22 | + # ======================================================================= | |
23 | + | |
20 | 24 | def __init__(self, depgraph, state={}, student=''): |
21 | 25 | self.depgraph = depgraph |
22 | 26 | self.state = state # {'topic_id': {'level':0.5, 'date': datetime}, ...} |
23 | 27 | self.student = student |
24 | 28 | |
25 | - # compute recommended sequence of topics (FIXME) | |
29 | + # compute recommended sequence of topics ['a', 'b',...] | |
26 | 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 | 39 | # Start a new topic. If not provided, selects the first with level < 0.8 |
33 | 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 | 43 | if topic is None: |
37 | 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 | 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 | 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 | 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 | 103 | def get_current_question(self): |
... | ... | @@ -62,8 +108,8 @@ class Knowledge(object): |
62 | 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 | 113 | for t in self.topic_sequence: |
68 | 114 | if t in self.state: |
69 | 115 | ts.append((t, self.state[t]['level'])) |
... | ... | @@ -73,44 +119,5 @@ class Knowledge(object): |
73 | 119 | |
74 | 120 | # ------------------------------------------------------------------------ |
75 | 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 | 416 | # which will print a valid question in yaml format to stdout. This |
417 | 417 | # output is then yaml parsed into a dictionary `q`. |
418 | 418 | if q['type'] == 'generator': |
419 | - logger.debug(f'Running script "{q["script"]}"...') | |
419 | + logger.debug(f' \_ Running script "{q["script"]}"...') | |
420 | 420 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
421 | 421 | script = path.join(q['path'], q['script']) |
422 | 422 | out = run_script(script=script, stdin=q['arg']) |
423 | 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 | 425 | # Finally we create an instance of Question() |
435 | 426 | try: | ... | ... |
serve.py
... | ... | @@ -160,70 +160,68 @@ class QuestionHandler(BaseHandler): |
160 | 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 | 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 | 194 | topics_html = self.render_string('topics.html', |
177 | 195 | state=state, |
196 | + current_topic=current_topic, | |
178 | 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 | 201 | 'params': { |
185 | - 'question': tornado.escape.to_unicode(question_html), | |
186 | 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 | 211 | @tornado.web.authenticated |
193 | 212 | def post(self): |
194 | 213 | user = self.current_user |
195 | 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 | 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 | 227 | def main(): |
... | ... | @@ -262,15 +260,15 @@ def main(): |
262 | 260 | }) |
263 | 261 | http_server.listen(8443) |
264 | 262 | |
265 | - # --- start webserver | |
263 | + # --- run webserver | |
266 | 264 | logging.info('Webserver running...') |
265 | + | |
267 | 266 | try: |
268 | - tornado.ioloop.IOLoop.current().start() | |
269 | - # running... | |
267 | + tornado.ioloop.IOLoop.current().start() # running... | |
270 | 268 | except KeyboardInterrupt: |
271 | 269 | tornado.ioloop.IOLoop.current().stop() |
272 | - finally: | |
273 | - logging.critical('Webserver stopped.') | |
270 | + | |
271 | + logging.critical('Webserver stopped.') | |
274 | 272 | |
275 | 273 | # ---------------------------------------------------------------------------- |
276 | 274 | if __name__ == "__main__": | ... | ... |
templates/learn.html
... | ... | @@ -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 | 157 | function updateQuestion(response){ |
158 | 158 | switch (response["method"]) { |
159 | 159 | case "new_question": |
... | ... | @@ -194,12 +194,20 @@ function updateQuestion(response){ |
194 | 194 | // audio.play(); |
195 | 195 | $('#question_div').animateCSS('zoomIn'); |
196 | 196 | break; |
197 | + | |
197 | 198 | case "shake": |
198 | 199 | // var audio = new Audio('/static/sounds/wrong.mp3'); |
199 | 200 | // audio.play(); |
200 | 201 | $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); |
201 | 202 | $('#question_div').animateCSS('shake'); |
202 | 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 | 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 | 229 | function getQuestion() { |
223 | 230 | $.ajax({ |
224 | 231 | // type: "GET", |
225 | 232 | url: "/question", |
226 | 233 | // headers: {"X-XSRFToken": token}, |
227 | - // data: $("#question_form").serialize(), // {'a':10,'b':20}, | |
228 | 234 | dataType: "json", // expected from server |
229 | 235 | success: updateQuestion, |
230 | 236 | error: function() {alert("O servidor não responde.");} | ... | ... |
templates/question-text.html
... | ... | @@ -2,7 +2,11 @@ |
2 | 2 | |
3 | 3 | {% block answer %} |
4 | 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 | 10 | </fieldset><br /> |
7 | 11 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
8 | 12 | {% end %} | ... | ... |
templates/question-textarea.html
1 | 1 | {% extends "question.html" %} |
2 | 2 | |
3 | 3 | {% block answer %} |
4 | + | |
4 | 5 | <textarea class="form-control" rows="{{ question['lines'] }}" name="answer" autofocus>{{ question['answer'] or '' }}</textarea><br /> |
5 | 6 | <input type="hidden" name="question_ref" value="{{ question['ref'] }}"> |
7 | + | |
6 | 8 | {% end %} | ... | ... |
templates/topics.html
... | ... | @@ -4,10 +4,22 @@ |
4 | 4 | |
5 | 5 | <ul class="nav nav-pills nav-stacked"> |
6 | 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 | 24 | {% end %} |
13 | -</ul> | |
14 | 25 | \ No newline at end of file |
26 | +</ul> | ... | ... |