Commit e0818d92d66cce06139aff325954f2068b473d72

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

- show remaining tries for each question.

- cosmetic changes, mostly.
1 1
2 # BUGS 2 # BUGS
3 3
  4 +- max tries não avança para seguinte ao fim das tentativas.
4 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. 5 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question.
5 - nas perguntas de código, quando erra nao se devia acrescentar mesma pergunta no fim. 6 - nas perguntas de código, quando erra nao se devia acrescentar mesma pergunta no fim.
6 7
@@ -27,6 +28,7 @@ @@ -27,6 +28,7 @@
27 28
28 # FIXED 29 # FIXED
29 30
  31 +- ver se ref guardada na BD é só a da pergunta ou inclui o path. -> so ref
30 - nao esta a guardar as respostas erradas. 32 - nao esta a guardar as respostas erradas.
31 - reload do topic não gera novas perguntas (alunos abusavam do reload) 33 - reload do topic não gera novas perguntas (alunos abusavam do reload)
32 - usar codemirror no textarea 34 - usar codemirror no textarea
@@ -112,9 +112,9 @@ sudo pkg install py27-certbot # FreeBSD @@ -112,9 +112,9 @@ sudo pkg install py27-certbot # FreeBSD
112 Shutdown the firewall and any server running. Then run the script to generate the certificate: 112 Shutdown the firewall and any server running. Then run the script to generate the certificate:
113 113
114 ```sh 114 ```sh
115 -sudo pfctl -d # disable pf firewall (FreeBSD) 115 +sudo service pf stop # disable pf firewall (FreeBSD)
116 sudo certbot certonly --standalone -d www.example.com 116 sudo certbot certonly --standalone -d www.example.com
117 -sudo pfctl -e; sudo pfctl -f /etc/pf.conf # enable pf firewall 117 +sudo service pf start # enable pf firewall
118 ``` 118 ```
119 119
120 Certificates are saved under `/usr/local/etc/letsencrypt/live/www.example.com/`. Copy them to `aprendizations/certs` and change permissions to be readable: 120 Certificates are saved under `/usr/local/etc/letsencrypt/live/www.example.com/`. Copy them to `aprendizations/certs` and change permissions to be readable:
@@ -130,9 +130,10 @@ Renews can be done as follows: @@ -130,9 +130,10 @@ Renews can be done as follows:
130 ```sh 130 ```sh
131 sudo service pf stop # shutdown firewall 131 sudo service pf stop # shutdown firewall
132 sudo certbot renew 132 sudo certbot renew
  133 +sudo service pf start # start firewall
133 ``` 134 ```
134 135
135 -and then copy the `cert.pem` and `privkey.pem` files to `aprendizations/certs` directory. Change permissions as appropriate. 136 +and then copy the `cert.pem` and `privkey.pem` files to `aprendizations/certs` directory. Change permissions and ownership as appropriate.
136 137
137 138
138 ### Testing 139 ### Testing
@@ -76,7 +76,7 @@ class QFactory(object): @@ -76,7 +76,7 @@ class QFactory(object):
76 # which will print a valid question in yaml format to stdout. This 76 # which will print a valid question in yaml format to stdout. This
77 # output is then yaml parsed into a dictionary `q`. 77 # output is then yaml parsed into a dictionary `q`.
78 if q['type'] == 'generator': 78 if q['type'] == 'generator':
79 - logger.debug(f' \_ Running "{q["script"]}".') 79 + logger.debug(f' \\_ Running "{q["script"]}".')
80 q.setdefault('arg', '') # optional arguments will be sent to stdin 80 q.setdefault('arg', '') # optional arguments will be sent to stdin
81 script = path.join(q['path'], q['script']) 81 script = path.join(q['path'], q['script'])
82 out = run_script(script=script, stdin=q['arg']) 82 out = run_script(script=script, stdin=q['arg'])
@@ -73,7 +73,7 @@ def get_students_from_csv(filename): @@ -73,7 +73,7 @@ def get_students_from_csv(filename):
73 else: 73 else:
74 students = [{ 74 students = [{
75 'uid': s['N.º'], 75 'uid': s['N.º'],
76 - 'name': string.capwords(re.sub('\(.*\)', '', s['Nome']).strip()) 76 + 'name': string.capwords(re.sub(r'\(.*\)', '', s['Nome']).strip())
77 } for s in csvreader] 77 } for s in csvreader]
78 78
79 return students 79 return students
@@ -27,13 +27,13 @@ class StudentKnowledge(object): @@ -27,13 +27,13 @@ class StudentKnowledge(object):
27 def __init__(self, deps, state={}): 27 def __init__(self, deps, state={}):
28 self.deps = deps # dependency graph shared among students 28 self.deps = deps # dependency graph shared among students
29 self.state = state # {'topic': {'level':0.5, 'date': datetime}, ...} 29 self.state = state # {'topic': {'level':0.5, 'date': datetime}, ...}
  30 +
30 self.update_topic_levels() # forgetting factor 31 self.update_topic_levels() # forgetting factor
31 self.topic_sequence = self.recommend_topic_sequence() # ['a', 'b', ...] 32 self.topic_sequence = self.recommend_topic_sequence() # ['a', 'b', ...]
32 - self.unlock_topics()  
33 - 33 + self.unlock_topics() # whose dependencies have been done
34 self.current_topic = None 34 self.current_topic = None
35 35
36 - self.MAX_QUESTIONS = 6 # FIXME get from configuration file?? 36 + self.MAX_QUESTIONS = 6 # FIXME get from yaml configuration file??
37 37
38 # ------------------------------------------------------------------------ 38 # ------------------------------------------------------------------------
39 # Updates the proficiency levels of the topics, with forgetting factor 39 # Updates the proficiency levels of the topics, with forgetting factor
@@ -57,9 +57,10 @@ class StudentKnowledge(object): @@ -57,9 +57,10 @@ class StudentKnowledge(object):
57 for topic in self.topic_sequence: 57 for topic in self.topic_sequence:
58 if topic not in self.state: # if locked 58 if topic not in self.state: # if locked
59 pred = self.deps.predecessors(topic) 59 pred = self.deps.predecessors(topic)
60 - if all(d in self.state and self.state[d]['level'] > min_level for d in pred): # and all dependencies are done 60 + if all(d in self.state and self.state[d]['level'] > min_level for d in pred):
  61 + # all dependencies are done
61 self.state[topic] = { 62 self.state[topic] = {
62 - 'level': 0.0, # then unlock 63 + 'level': 0.0, # unlocked
63 'date': datetime.now() 64 'date': datetime.now()
64 } 65 }
65 logger.debug(f'Unlocked "{topic}".') 66 logger.debug(f'Unlocked "{topic}".')
@@ -73,42 +74,53 @@ class StudentKnowledge(object): @@ -73,42 +74,53 @@ class StudentKnowledge(object):
73 def init_topic(self, topic=''): 74 def init_topic(self, topic=''):
74 logger.debug(f'StudentKnowledge.init_topic({topic})') 75 logger.debug(f'StudentKnowledge.init_topic({topic})')
75 76
  77 + # maybe get topic recommendation
76 if not topic: 78 if not topic:
77 topic = self.get_recommended_topic() 79 topic = self.get_recommended_topic()
  80 + logger.debug(f'Recommended topic is {topic}')
78 81
79 - # check if it's unlocked 82 + # do not allow locked topics
80 if self.is_locked(topic): 83 if self.is_locked(topic):
81 - return False  
82 -  
83 - if self.current_topic is not None and topic == self.current_topic:  
84 - return True 84 + logger.debug(f'Topic {topic} is locked')
  85 + return
85 86
  87 + # starting new topic
86 self.current_topic = topic 88 self.current_topic = topic
87 89
88 - # generate question instances for current topic  
89 factory = self.deps.node[topic]['factory'] 90 factory = self.deps.node[topic]['factory']
90 questionlist = self.deps.node[topic]['questions'] 91 questionlist = self.deps.node[topic]['questions']
91 92
92 self.correct_answers = 0 93 self.correct_answers = 0
93 self.wrong_answers = 0 94 self.wrong_answers = 0
94 95
  96 + # select a random set of questions for this topic
95 size = min(self.MAX_QUESTIONS, len(questionlist)) # number of questions 97 size = min(self.MAX_QUESTIONS, len(questionlist)) # number of questions
96 questionlist = random.sample(questionlist, k=size) 98 questionlist = random.sample(questionlist, k=size)
97 logger.debug(f'Questions: {", ".join(questionlist)}') 99 logger.debug(f'Questions: {", ".join(questionlist)}')
98 100
  101 + # generate instances of questions
99 self.questions = [factory[qref].generate() for qref in questionlist] 102 self.questions = [factory[qref].generate() for qref in questionlist]
100 logger.debug(f'Total: {len(self.questions)} questions') 103 logger.debug(f'Total: {len(self.questions)} questions')
101 104
102 - try:  
103 - self.current_question = self.questions.pop(0) # FIXME crash if empty  
104 - except IndexError:  
105 - # self.current_question = None  
106 - self.finish_topic() # FIXME if no questions, what should be done?  
107 - return False  
108 - else:  
109 - self.current_question['start_time'] = datetime.now()  
110 - return True 105 + # get first question
  106 + self.next_question()
  107 +
  108 +
  109 + # def init_learning(self, topic=''):
  110 + # logger.debug(f'StudentKnowledge.init_learning({topic})')
  111 +
  112 + # if self.is_locked(topic):
  113 + # logger.debug(f'Topic {topic} is locked')
  114 + # return False
  115 +
  116 + # self.current_topic = topic
  117 + # factory = self.deps.node[topic]['factory']
  118 + # lesson = self.deps.node[topic]['lesson']
111 119
  120 + # self.questions = [factory[qref].generate() for qref in lesson]
  121 + # logger.debug(f'Total: {len(self.questions)} questions')
  122 +
  123 + # self.next_question_in_lesson()
112 124
113 # ------------------------------------------------------------------------ 125 # ------------------------------------------------------------------------
114 # The topic has finished and there are no more questions. 126 # The topic has finished and there are no more questions.
@@ -127,7 +139,10 @@ class StudentKnowledge(object): @@ -127,7 +139,10 @@ class StudentKnowledge(object):
127 139
128 140
129 # ------------------------------------------------------------------------ 141 # ------------------------------------------------------------------------
130 - # returns the current question with correction, time and comments updated 142 + # corrects current question with provided answer.
  143 + # implements the logic:
  144 + # - if answer ok, goes to next question
  145 + # - if wrong, counts number of tries. If exceeded, moves on.
131 # ------------------------------------------------------------------------ 146 # ------------------------------------------------------------------------
132 def check_answer(self, answer): 147 def check_answer(self, answer):
133 logger.debug('StudentKnowledge.check_answer()') 148 logger.debug('StudentKnowledge.check_answer()')
@@ -142,21 +157,48 @@ class StudentKnowledge(object): @@ -142,21 +157,48 @@ class StudentKnowledge(object):
142 # if answer is correct, get next question 157 # if answer is correct, get next question
143 if grade > 0.999: 158 if grade > 0.999:
144 self.correct_answers += 1 159 self.correct_answers += 1
145 - try:  
146 - self.current_question = self.questions.pop(0) # FIXME empty?  
147 - except IndexError:  
148 - self.finish_topic()  
149 - else:  
150 - self.current_question['start_time'] = datetime.now() 160 + self.next_question()
151 161
152 # if wrong, keep same question and append a similar one at the end 162 # if wrong, keep same question and append a similar one at the end
153 else: 163 else:
154 self.wrong_answers += 1 164 self.wrong_answers += 1
155 - factory = self.deps.node[self.current_topic]['factory']  
156 - self.questions.append(factory[q['ref']].generate())  
157 - # returns answered and corrected question 165 +
  166 + self.current_question['tries'] -= 1
  167 +
  168 + logger.debug(f'Wrong answers = {self.wrong_answers}; Tries = {self.current_question["tries"]}')
  169 +
  170 + # append a new instance of the current question to the end and
  171 + # move to the next question
  172 + if self.current_question['tries'] <= 0:
  173 + logger.debug("Appending new instance of this question to the end")
  174 + factory = self.deps.node[self.current_topic]['factory']
  175 + self.questions.append(factory[q['ref']].generate())
  176 + self.next_question()
  177 +
  178 + # returns answered and corrected question (not new one)
158 return q 179 return q
159 180
  181 + # ------------------------------------------------------------------------
  182 + # Move to next question
  183 + # ------------------------------------------------------------------------
  184 + def next_question(self):
  185 + try:
  186 + self.current_question = self.questions.pop(0)
  187 + except IndexError:
  188 + self.finish_topic()
  189 + else:
  190 + self.current_question['start_time'] = datetime.now()
  191 + self.current_question['tries'] = self.current_question.get('max_tries', 3) # FIXME hardcoded 3
  192 + logger.debug(f'Next question is "{self.current_question["ref"]}"')
  193 +
  194 + # def next_question_in_lesson(self):
  195 + # try:
  196 + # self.current_question = self.questions.pop(0)
  197 + # except IndexError:
  198 + # self.current_question = None
  199 + # else:
  200 + # logger.debug(f'Next question is "{self.current_question["ref"]}"')
  201 +
160 202
161 # ======================================================================== 203 # ========================================================================
162 # pure functions of the state (no side effects) 204 # pure functions of the state (no side effects)
@@ -143,23 +143,28 @@ class LearnApp(object): @@ -143,23 +143,28 @@ class LearnApp(object):
143 s.add(a) 143 s.add(a)
144 logger.debug(f'Saved topic "{topic}" into database') 144 logger.debug(f'Saved topic "{topic}" into database')
145 145
146 - return q['grade'] 146 +
  147 + if knowledge.get_current_question() is None:
  148 + return 'finished_topic'
  149 + if q['tries'] > 0 and q['grade'] <= 0.999:
  150 + return 'wrong'
  151 + # elif q['tries'] <= 0 and q['grade'] <= 0.999:
  152 + # return 'max_tries_exceeded'
  153 + else:
  154 + return 'new_question'
  155 + # return q['grade']
147 156
148 # ------------------------------------------------------------------------ 157 # ------------------------------------------------------------------------
149 # Start new topic 158 # Start new topic
150 # ------------------------------------------------------------------------ 159 # ------------------------------------------------------------------------
151 def start_topic(self, uid, topic): 160 def start_topic(self, uid, topic):
152 try: 161 try:
153 - ok = self.online[uid]['state'].init_topic(topic) 162 + self.online[uid]['state'].init_topic(topic)
154 except KeyError as e: 163 except KeyError as e:
155 logger.warning(f'User "{uid}" tried to open nonexistent topic: "{topic}"') 164 logger.warning(f'User "{uid}" tried to open nonexistent topic: "{topic}"')
156 raise e 165 raise e
157 else: 166 else:
158 - if ok:  
159 - logger.info(f'User "{uid}" started "{topic}"')  
160 - else:  
161 - logger.warning(f'User "{uid}" restarted "{topic}"')  
162 - return ok 167 + logger.info(f'User "{uid}" started "{topic}"')
163 168
164 # ------------------------------------------------------------------------ 169 # ------------------------------------------------------------------------
165 # Start new topic 170 # Start new topic
@@ -245,7 +250,7 @@ class LearnApp(object): @@ -245,7 +250,7 @@ class LearnApp(object):
245 return self.online[uid]['state'].get_topic_progress() 250 return self.online[uid]['state'].get_topic_progress()
246 251
247 # ------------------------------------------------------------------------ 252 # ------------------------------------------------------------------------
248 - def get_student_question(self, uid): 253 + def get_current_question(self, uid):
249 return self.online[uid]['state'].get_current_question() # dict 254 return self.online[uid]['state'].get_current_question() # dict
250 255
251 # ------------------------------------------------------------------------ 256 # ------------------------------------------------------------------------
@@ -316,11 +321,12 @@ def build_dependency_graph(config={}): @@ -316,11 +321,12 @@ def build_dependency_graph(config={}):
316 g.add_edges_from((d,ref) for d in attr.get('deps', [])) 321 g.add_edges_from((d,ref) for d in attr.get('deps', []))
317 322
318 fullpath = path.expanduser(path.join(prefix, ref)) 323 fullpath = path.expanduser(path.join(prefix, ref))
  324 +
  325 + # load questions
319 filename = path.join(fullpath, 'questions.yaml') 326 filename = path.join(fullpath, 'questions.yaml')
320 loaded_questions = load_yaml(filename, default=[]) # list 327 loaded_questions = load_yaml(filename, default=[]) # list
321 -  
322 - # if questions not in configuration then load all, preserving order  
323 if not tnode['questions']: 328 if not tnode['questions']:
  329 + # if questions not in configuration then load all, preserving order
324 tnode['questions'] = [q.setdefault('ref', f'{ref}:{i}') for i,q in enumerate(loaded_questions)] 330 tnode['questions'] = [q.setdefault('ref', f'{ref}:{i}') for i,q in enumerate(loaded_questions)]
325 331
326 # make questions factory (without repeating same question) 332 # make questions factory (without repeating same question)
1 -#!/usr/bin/env python3.6 1 +#!/usr/bin/env python3
2 2
3 # python standard library 3 # python standard library
4 from os import path 4 from os import path
@@ -35,7 +35,7 @@ class WebApplication(tornado.web.Application): @@ -35,7 +35,7 @@ class WebApplication(tornado.web.Application):
35 (r'/question', QuestionHandler), # renders each question 35 (r'/question', QuestionHandler), # renders each question
36 (r'/topic/(.+)', TopicHandler), # page for exercising a topic 36 (r'/topic/(.+)', TopicHandler), # page for exercising a topic
37 # (r'/learn/(.+)', LearnHandler), # page for learning a topic 37 # (r'/learn/(.+)', LearnHandler), # page for learning a topic
38 - (r'/file/(.+)', FileHandler), # FIXME 38 + (r'/file/(.+)', FileHandler), # serve files, images, etc
39 (r'/', RootHandler), # show list of topics 39 (r'/', RootHandler), # show list of topics
40 ] 40 ]
41 settings = { 41 settings = {
@@ -139,18 +139,14 @@ class TopicHandler(BaseHandler): @@ -139,18 +139,14 @@ class TopicHandler(BaseHandler):
139 uid = self.current_user 139 uid = self.current_user
140 140
141 try: 141 try:
142 - ok = self.learn.start_topic(uid, topic) 142 + self.learn.start_topic(uid, topic)
143 except KeyError: 143 except KeyError:
144 self.redirect('/') 144 self.redirect('/')
145 else: 145 else:
146 - if ok:  
147 - self.render('topic.html',  
148 - uid=uid,  
149 - name=self.learn.get_student_name(uid),  
150 - )  
151 - else:  
152 - self.redirect('/')  
153 - 146 + self.render('topic.html',
  147 + uid=uid,
  148 + name=self.learn.get_student_name(uid),
  149 + )
154 150
155 # class LearnHandler(BaseHandler): 151 # class LearnHandler(BaseHandler):
156 # @tornado.web.authenticated 152 # @tornado.web.authenticated
@@ -225,9 +221,6 @@ class QuestionHandler(BaseHandler): @@ -225,9 +221,6 @@ class QuestionHandler(BaseHandler):
225 'information': 'question-information.html', 221 'information': 'question-information.html',
226 'info': 'question-information.html', 222 'info': 'question-information.html',
227 'success': 'question-success.html', 223 'success': 'question-success.html',
228 - # 'warning': '', FIXME  
229 - # 'warn': '', FIXME  
230 - # 'alert': '', FIXME  
231 } 224 }
232 225
233 # --- get question to render 226 # --- get question to render
@@ -236,16 +229,20 @@ class QuestionHandler(BaseHandler): @@ -236,16 +229,20 @@ class QuestionHandler(BaseHandler):
236 logging.debug('QuestionHandler.get()') 229 logging.debug('QuestionHandler.get()')
237 user = self.current_user 230 user = self.current_user
238 231
239 - question = self.learn.get_student_question(user)  
240 - template = self.templates[question['type']]  
241 - question_html = self.render_string(template, question=question, md=md_to_html) 232 + question = self.learn.get_current_question(user)
  233 +
  234 + question_html = self.render_string(self.templates[question['type']],
  235 + question=question, md=md_to_html)
  236 +
  237 + print(question['tries'])
242 238
243 self.write({ 239 self.write({
244 'method': 'new_question', 240 'method': 'new_question',
245 'params': { 241 'params': {
246 'question': tornado.escape.to_unicode(question_html), 242 'question': tornado.escape.to_unicode(question_html),
247 - 'progress': self.learn.get_student_progress(user) ,  
248 - } 243 + 'progress': self.learn.get_student_progress(user),
  244 + 'tries': question['tries'],
  245 + },
249 }) 246 })
250 247
251 # --- post answer, returns what to do next: shake, new_question, finished 248 # --- post answer, returns what to do next: shake, new_question, finished
@@ -253,13 +250,11 @@ class QuestionHandler(BaseHandler): @@ -253,13 +250,11 @@ class QuestionHandler(BaseHandler):
253 async def post(self): 250 async def post(self):
254 logging.debug('QuestionHandler.post()') 251 logging.debug('QuestionHandler.post()')
255 user = self.current_user 252 user = self.current_user
256 -  
257 - # check answer and get next question (same, new or None)  
258 answer = self.get_body_arguments('answer') # list 253 answer = self.get_body_arguments('answer') # list
259 254
260 - # answers returned in a list. fix depending on question type 255 + # answers are returned in a list. fix depending on question type
261 qtype = self.learn.get_student_question_type(user) 256 qtype = self.learn.get_student_question_type(user)
262 - if qtype in ('success', 'information', 'info'): # FIXME unused? 257 + if qtype in ('success', 'information', 'info'):
263 answer = None 258 answer = None
264 elif qtype == 'radio' and not answer: 259 elif qtype == 'radio' and not answer:
265 answer = None 260 answer = None
@@ -267,20 +262,25 @@ class QuestionHandler(BaseHandler): @@ -267,20 +262,25 @@ class QuestionHandler(BaseHandler):
267 answer = answer[0] 262 answer = answer[0]
268 263
269 # check answer in another thread (nonblocking) 264 # check answer in another thread (nonblocking)
270 - loop = asyncio.get_event_loop()  
271 - grade = await loop.run_in_executor(None, self.learn.check_answer, user, answer)  
272 - question = self.learn.get_student_question(user) 265 + action = await asyncio.get_event_loop().run_in_executor(None,
  266 + self.learn.check_answer, user, answer)
  267 +
  268 + # get next question (same, new or None)
  269 + question = self.learn.get_current_question(user)
273 270
274 - if grade <= 0.999: # wrong answer  
275 - comments_html = self.render_string('comments.html', comments=question['comments'], md=md_to_html) 271 + if action == 'wrong':
  272 + comments_html = self.render_string('comments.html',
  273 + comments=question['comments'], md=md_to_html)
276 self.write({ 274 self.write({
277 - 'method': 'shake', 275 + 'method': action,
278 'params': { 276 'params': {
279 'progress': self.learn.get_student_progress(user), 277 'progress': self.learn.get_student_progress(user),
280 'comments': tornado.escape.to_unicode(comments_html), # FIXME 278 'comments': tornado.escape.to_unicode(comments_html), # FIXME
  279 + 'tries': question['tries'],
281 } 280 }
282 }) 281 })
283 - elif question is None: # right answer, finished topic 282 +
  283 + elif action == 'finished_topic': # right answer, finished topic
284 finished_topic_html = self.render_string('finished_topic.html') 284 finished_topic_html = self.render_string('finished_topic.html')
285 self.write({ 285 self.write({
286 'method': 'finished_topic', 286 'method': 'finished_topic',
@@ -288,18 +288,23 @@ class QuestionHandler(BaseHandler): @@ -288,18 +288,23 @@ class QuestionHandler(BaseHandler):
288 'question': tornado.escape.to_unicode(finished_topic_html) 288 'question': tornado.escape.to_unicode(finished_topic_html)
289 } 289 }
290 }) 290 })
291 - else: # right answer, get next question in the topic 291 +
  292 + elif action == 'new_question': # get next question in the topic
292 template = self.templates[question['type']] 293 template = self.templates[question['type']]
293 - question_html = self.render_string(  
294 - template, question=question, md=md_to_html) 294 + question_html = self.render_string(template,
  295 + question=question, md=md_to_html)
295 self.write({ 296 self.write({
296 'method': 'new_question', 297 'method': 'new_question',
297 'params': { 298 'params': {
298 'question': tornado.escape.to_unicode(question_html), 299 'question': tornado.escape.to_unicode(question_html),
299 'progress': self.learn.get_student_progress(user), 300 'progress': self.learn.get_student_progress(user),
  301 + 'tries': question['tries'],
300 } 302 }
301 }) 303 })
302 304
  305 + else:
  306 + logger.error(f'Unknown action {action}')
  307 +
303 308
304 # ------------------------------------------------------------------------- 309 # -------------------------------------------------------------------------
305 def signal_handler(signal, frame): 310 def signal_handler(signal, frame):
@@ -332,6 +337,7 @@ def main(): @@ -332,6 +337,7 @@ def main():
332 except: 337 except:
333 print('An error ocurred while setting up the logging system.') 338 print('An error ocurred while setting up the logging system.')
334 sys.exit(1) 339 sys.exit(1)
  340 +
335 logging.info('====================================================') 341 logging.info('====================================================')
336 342
337 # --- start application 343 # --- start application
static/fontawesome
1 -libs/fontawesome-free-5.0.13/svg-with-js/js/  
2 \ No newline at end of file 1 \ No newline at end of file
  2 +libs/fontawesome-free-5.1.1-web/
3 \ No newline at end of file 3 \ No newline at end of file
static/js/maintopics.js
1 function notify(msg) { 1 function notify(msg) {
2 - $("#notifications").html(msg);  
3 - $("#notifications").fadeIn(250).delay(3000).fadeOut(500); 2 + $("#notifications").html(msg);
  3 + $("#notifications").fadeIn(250).delay(3000).fadeOut(500);
4 } 4 }
5 5
6 function getCookie(name) { 6 function getCookie(name) {
7 - var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");  
8 - return r ? r[1] : undefined; 7 + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
  8 + return r ? r[1] : undefined;
9 } 9 }
10 10
11 function change_password() { 11 function change_password() {
12 - var token = getCookie('_xsrf');  
13 - $.ajax({  
14 - type: "POST",  
15 - url: "/change_password",  
16 - headers: {'X-XSRFToken' : token },  
17 - data: {  
18 - "new_password": $("#new_password").val(),  
19 - },  
20 - dataType: "json",  
21 - success: function(r) {  
22 - notify(r['msg']);  
23 - },  
24 - error: function(r) {  
25 - notify(r['msg']);  
26 - },  
27 - }); 12 + var token = getCookie('_xsrf');
  13 + $.ajax({
  14 + type: "POST",
  15 + url: "/change_password",
  16 + headers: {'X-XSRFToken': token},
  17 + data: {
  18 + "new_password": $("#new_password").val(),
  19 + },
  20 + dataType: "json",
  21 + success: function(r) {
  22 + notify(r['msg']);
  23 + },
  24 + error: function(r) {
  25 + notify(r['msg']);
  26 + },
  27 + });
28 } 28 }
29 29
30 $(document).ready(function() { 30 $(document).ready(function() {
31 - $("#change_password").click(change_password); 31 + $("#change_password").click(change_password);
32 }); 32 });
static/js/topic.js
@@ -7,12 +7,15 @@ $.fn.extend({ @@ -7,12 +7,15 @@ $.fn.extend({
7 } 7 }
8 }); 8 });
9 9
10 -// Process response given by the server 10 +// updates question according to the response given by the server
11 function updateQuestion(response){ 11 function updateQuestion(response){
  12 +
12 switch (response["method"]) { 13 switch (response["method"]) {
13 case "new_question": 14 case "new_question":
14 $("#question_div").html(response["params"]["question"]); 15 $("#question_div").html(response["params"]["question"]);
15 $("#comments").html(""); 16 $("#comments").html("");
  17 + $("#tries").html(response["params"]["tries"]);
  18 +
16 $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); 19 $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]);
17 20
18 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]); 21 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]);
@@ -28,10 +31,11 @@ function updateQuestion(response){ @@ -28,10 +31,11 @@ function updateQuestion(response){
28 $('#question_div').animateCSS('bounceInDown'); 31 $('#question_div').animateCSS('bounceInDown');
29 break; 32 break;
30 33
31 - case "shake": 34 + case "wrong":
32 $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]); 35 $('#topic_progress').css('width', (100*response["params"]["progress"])+'%').attr('aria-valuenow', 100*response["params"]["progress"]);
33 $('#question_div').animateCSS('shake'); 36 $('#question_div').animateCSS('shake');
34 $('#comments').html(response['params']['comments']); 37 $('#comments').html(response['params']['comments']);
  38 + $("#tries").html(response["params"]["tries"]);
35 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"#comments"]); 39 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"#comments"]);
36 break; 40 break;
37 41
@@ -57,7 +61,8 @@ function getQuestion() { @@ -57,7 +61,8 @@ function getQuestion() {
57 } 61 }
58 62
59 // Send answer and receive a response. 63 // Send answer and receive a response.
60 -// The response can be a new_question or a shake if the answer is wrong. 64 +// The response can be a new_question or a shake if the answer is wrong, which
  65 +// is then passed to updateQuestion()
61 function postQuestion() { 66 function postQuestion() {
62 if (typeof editor === 'object') 67 if (typeof editor === 'object')
63 editor.save(); 68 editor.save();
templates/login.html
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 <!-- Scripts --> 14 <!-- Scripts -->
15 <script defer src="/static/libs/jquery-3.3.1.min.js"></script> 15 <script defer src="/static/libs/jquery-3.3.1.min.js"></script>
16 <script defer src="/static/popper/popper.min.js"></script> 16 <script defer src="/static/popper/popper.min.js"></script>
17 - <script defer src="/static/fontawesome/fontawesome-all.min.js"></script> 17 + <script defer src="/static/fontawesome/js/all.js"></script>
18 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> 18 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script>
19 <script defer src="/static/MDB/js/mdb.min.js"></script> 19 <script defer src="/static/MDB/js/mdb.min.js"></script>
20 20
templates/maintopics-table.html
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 <!-- Scripts --> 18 <!-- Scripts -->
19 <script defer src="/static/libs/jquery-3.3.1.min.js"></script> 19 <script defer src="/static/libs/jquery-3.3.1.min.js"></script>
20 <script defer src="/static/popper/popper.min.js"></script> 20 <script defer src="/static/popper/popper.min.js"></script>
21 - <script defer src="/static/fontawesome/fontawesome-all.min.js"></script> 21 + <script defer src="/static/fontawesome/js/all.js"></script>
22 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> 22 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script>
23 <script defer src="/static/MDB/js/mdb.min.js"></script> 23 <script defer src="/static/MDB/js/mdb.min.js"></script>
24 <script defer src="/static/js/maintopics.js"></script> 24 <script defer src="/static/js/maintopics.js"></script>
templates/question.html
@@ -7,3 +7,5 @@ @@ -7,3 +7,5 @@
7 </div> 7 </div>
8 8
9 {% block answer %}{% end %} 9 {% block answer %}{% end %}
  10 +
  11 +<p class="text-right font-italic">(<span id="tries"></span> tentativas)</p>
templates/topic.html
@@ -29,7 +29,7 @@ @@ -29,7 +29,7 @@
29 <!-- Scripts --> 29 <!-- Scripts -->
30 <script defer src="/static/libs/jquery-3.3.1.min.js"></script> 30 <script defer src="/static/libs/jquery-3.3.1.min.js"></script>
31 <script defer src="/static/popper/popper.min.js"></script> 31 <script defer src="/static/popper/popper.min.js"></script>
32 - <script defer src="/static/fontawesome/fontawesome-all.min.js"></script> 32 + <script defer src="/static/fontawesome/js/all.js"></script>
33 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> 33 <script defer src="/static/bootstrap/js/bootstrap.min.js"></script>
34 <script defer src="/static/MDB/js/mdb.min.js"></script> 34 <script defer src="/static/MDB/js/mdb.min.js"></script>
35 <script defer src="/static/codemirror/codemirror.js"></script> 35 <script defer src="/static/codemirror/codemirror.js"></script>
@@ -86,6 +86,8 @@ @@ -86,6 +86,8 @@
86 86
87 <div id="comments"></div> 87 <div id="comments"></div>
88 88
  89 +
  90 +
89 </div> 91 </div>
90 </div> 92 </div>
91 93