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