Commit 065611f74a7e84ff9760b271d4f81a9303e8a80b

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

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.
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...??
@@ -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
@@ -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  
@@ -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:
@@ -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>