Commit d5cd0d100041fcdcb912c0dd98a4365f41e7665c
1 parent
2b30310a
Exists in
master
and in
1 other branch
- added options 'choose', 'shuffle' and 'file' in the course yaml configuration.
- question factories moved out of the graph. Now they are in a dict where the key is the question ref. - construction of the graph and questions factory was separated into two different functions. - code cleanup.
Showing
5 changed files
with
111 additions
and
121 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | -- na definicao dos topicos, indicar: | ||
| 5 | - "file: questions.yaml" (default questions.yaml) | ||
| 6 | - "shuffle: True/False" (default False) | ||
| 7 | - "choose: 6" (default tudo) | 4 | +- default prefix should be obtained from each course (yaml conf)? |
| 8 | - quando a pergunta devolve comments, este é apresentado, mas fica persistente nas tentativas seguintes. devia ser limpo apos a segunda submissao. | 5 | - quando a pergunta devolve comments, este é apresentado, mas fica persistente nas tentativas seguintes. devia ser limpo apos a segunda submissao. |
| 9 | - a opcao max_tries na especificacao das perguntas é cumbersome... usar antes tries? | 6 | - a opcao max_tries na especificacao das perguntas é cumbersome... usar antes tries? |
| 10 | - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. | 7 | - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. |
| @@ -32,7 +29,10 @@ | @@ -32,7 +29,10 @@ | ||
| 32 | - normalizar com perguntations. | 29 | - normalizar com perguntations. |
| 33 | 30 | ||
| 34 | # FIXED | 31 | # FIXED |
| 35 | - | 32 | +- na definicao dos topicos, indicar: |
| 33 | + "file: questions.yaml" (default questions.yaml) | ||
| 34 | + "shuffle: True/False" (default False) | ||
| 35 | + "choose: 6" (default tudo) | ||
| 36 | - max tries não avança para seguinte ao fim das tentativas. | 36 | - max tries não avança para seguinte ao fim das tentativas. |
| 37 | - ver se ref guardada na BD é só a da pergunta ou inclui o path. -> so ref | 37 | - ver se ref guardada na BD é só a da pergunta ou inclui o path. -> so ref |
| 38 | - nao esta a guardar as respostas erradas. | 38 | - nao esta a guardar as respostas erradas. |
knowledge.py
| @@ -24,15 +24,16 @@ class StudentKnowledge(object): | @@ -24,15 +24,16 @@ class StudentKnowledge(object): | ||
| 24 | # ======================================================================= | 24 | # ======================================================================= |
| 25 | # methods that update state | 25 | # methods that update state |
| 26 | # ======================================================================= | 26 | # ======================================================================= |
| 27 | - def __init__(self, deps, state={}): | 27 | + def __init__(self, deps, factory, state={}): |
| 28 | self.deps = deps # dependency graph shared among students | 28 | self.deps = deps # dependency graph shared among students |
| 29 | + self.factory = factory # question factory | ||
| 29 | self.state = state # {'topic': {'level':0.5, 'date': datetime}, ...} | 30 | self.state = state # {'topic': {'level':0.5, 'date': datetime}, ...} |
| 30 | 31 | ||
| 31 | self.update_topic_levels() # applies forgetting factor | 32 | self.update_topic_levels() # applies forgetting factor |
| 32 | self.topic_sequence = self.recommend_topic_sequence() # ['a', 'b', ...] | 33 | self.topic_sequence = self.recommend_topic_sequence() # ['a', 'b', ...] |
| 33 | self.unlock_topics() # whose dependencies have been completed | 34 | self.unlock_topics() # whose dependencies have been completed |
| 34 | self.current_topic = None | 35 | self.current_topic = None |
| 35 | - # self.MAX_QUESTIONS = deps.graph['config'].get('choose', None) | 36 | + |
| 36 | 37 | ||
| 37 | # ------------------------------------------------------------------------ | 38 | # ------------------------------------------------------------------------ |
| 38 | # Updates the proficiency levels of the topics, with forgetting factor | 39 | # Updates the proficiency levels of the topics, with forgetting factor |
| @@ -40,9 +41,9 @@ class StudentKnowledge(object): | @@ -40,9 +41,9 @@ class StudentKnowledge(object): | ||
| 40 | # ------------------------------------------------------------------------ | 41 | # ------------------------------------------------------------------------ |
| 41 | def update_topic_levels(self): | 42 | def update_topic_levels(self): |
| 42 | now = datetime.now() | 43 | now = datetime.now() |
| 43 | - for s in self.state.values(): | 44 | + for tref, s in self.state.items(): |
| 44 | dt = now - s['date'] | 45 | dt = now - s['date'] |
| 45 | - s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 | 46 | + s['level'] *= 0.95 ** dt.days # forgetting factor 0.95 FIXME |
| 46 | 47 | ||
| 47 | 48 | ||
| 48 | # ------------------------------------------------------------------------ | 49 | # ------------------------------------------------------------------------ |
| @@ -80,25 +81,21 @@ class StudentKnowledge(object): | @@ -80,25 +81,21 @@ class StudentKnowledge(object): | ||
| 80 | 81 | ||
| 81 | # starting new topic | 82 | # starting new topic |
| 82 | self.current_topic = topic | 83 | self.current_topic = topic |
| 83 | - | ||
| 84 | - factory = self.deps.node[topic]['factory'] | ||
| 85 | - questionlist = self.deps.node[topic]['questions'] | ||
| 86 | - | ||
| 87 | self.correct_answers = 0 | 84 | self.correct_answers = 0 |
| 88 | self.wrong_answers = 0 | 85 | self.wrong_answers = 0 |
| 89 | 86 | ||
| 90 | - # select a random set of questions for this topic | ||
| 91 | - size = len(questionlist) # number of questions FIXME get from topic config | ||
| 92 | - questionlist = random.sample(questionlist, k=size) | ||
| 93 | - logger.debug(f'Questions: {", ".join(questionlist)}') | 87 | + t = self.deps.node[topic] |
| 88 | + questions = random.sample(t['questions'], k=t['choose']) | ||
| 89 | + logger.debug(f'Questions: {", ".join(questions)}') | ||
| 94 | 90 | ||
| 95 | # generate instances of questions | 91 | # generate instances of questions |
| 96 | - self.questions = [factory[qref].generate() for qref in questionlist] | 92 | + self.questions = [self.factory[qref].generate() for qref in questions] |
| 97 | logger.debug(f'Total: {len(self.questions)} questions') | 93 | logger.debug(f'Total: {len(self.questions)} questions') |
| 98 | 94 | ||
| 99 | # get first question | 95 | # get first question |
| 100 | self.next_question() | 96 | self.next_question() |
| 101 | 97 | ||
| 98 | + | ||
| 102 | # ------------------------------------------------------------------------ | 99 | # ------------------------------------------------------------------------ |
| 103 | # The topic has finished and there are no more questions. | 100 | # The topic has finished and there are no more questions. |
| 104 | # The topic level is updated in state and unlocks are performed. | 101 | # The topic level is updated in state and unlocks are performed. |
| @@ -148,13 +145,14 @@ class StudentKnowledge(object): | @@ -148,13 +145,14 @@ class StudentKnowledge(object): | ||
| 148 | # move to the next question | 145 | # move to the next question |
| 149 | if self.current_question['tries'] <= 0: | 146 | if self.current_question['tries'] <= 0: |
| 150 | logger.debug("Appending new instance of this question to the end") | 147 | logger.debug("Appending new instance of this question to the end") |
| 151 | - factory = self.deps.node[self.current_topic]['factory'] | 148 | + factory = self.factory # self.deps.node[self.current_topic]['factory'] |
| 152 | self.questions.append(factory[q['ref']].generate()) | 149 | self.questions.append(factory[q['ref']].generate()) |
| 153 | self.next_question() | 150 | self.next_question() |
| 154 | 151 | ||
| 155 | # returns answered and corrected question (not new one) | 152 | # returns answered and corrected question (not new one) |
| 156 | return q | 153 | return q |
| 157 | 154 | ||
| 155 | + | ||
| 158 | # ------------------------------------------------------------------------ | 156 | # ------------------------------------------------------------------------ |
| 159 | # Move to next question | 157 | # Move to next question |
| 160 | # ------------------------------------------------------------------------ | 158 | # ------------------------------------------------------------------------ |
| @@ -168,25 +166,11 @@ class StudentKnowledge(object): | @@ -168,25 +166,11 @@ class StudentKnowledge(object): | ||
| 168 | self.current_question['tries'] = self.current_question.get('max_tries', 3) # FIXME hardcoded 3 | 166 | self.current_question['tries'] = self.current_question.get('max_tries', 3) # FIXME hardcoded 3 |
| 169 | logger.debug(f'Next question is "{self.current_question["ref"]}"') | 167 | logger.debug(f'Next question is "{self.current_question["ref"]}"') |
| 170 | 168 | ||
| 171 | - # def next_question_in_lesson(self): | ||
| 172 | - # try: | ||
| 173 | - # self.current_question = self.questions.pop(0) | ||
| 174 | - # except IndexError: | ||
| 175 | - # self.current_question = None | ||
| 176 | - # else: | ||
| 177 | - # logger.debug(f'Next question is "{self.current_question["ref"]}"') | ||
| 178 | - | ||
| 179 | 169 | ||
| 180 | # ======================================================================== | 170 | # ======================================================================== |
| 181 | # pure functions of the state (no side effects) | 171 | # pure functions of the state (no side effects) |
| 182 | # ======================================================================== | 172 | # ======================================================================== |
| 183 | 173 | ||
| 184 | - def get_max_questions(self, topic=None): | ||
| 185 | - if topic is not None: | ||
| 186 | - node = self.deps.nodes[topic] | ||
| 187 | - max_questions = node.get('choose', len(node['questions'])) | ||
| 188 | - | ||
| 189 | - | ||
| 190 | # ------------------------------------------------------------------------ | 174 | # ------------------------------------------------------------------------ |
| 191 | # compute recommended sequence of topics ['a', 'b', ...] | 175 | # compute recommended sequence of topics ['a', 'b', ...] |
| 192 | # ------------------------------------------------------------------------ | 176 | # ------------------------------------------------------------------------ |
| @@ -211,6 +195,7 @@ class StudentKnowledge(object): | @@ -211,6 +195,7 @@ class StudentKnowledge(object): | ||
| 211 | # Topics unlocked but not yet done have level 0.0. | 195 | # Topics unlocked but not yet done have level 0.0. |
| 212 | # ------------------------------------------------------------------------ | 196 | # ------------------------------------------------------------------------ |
| 213 | def get_knowledge_state(self): | 197 | def get_knowledge_state(self): |
| 198 | + # print(self.topic_sequence) | ||
| 214 | return [{ | 199 | return [{ |
| 215 | 'ref': ref, | 200 | 'ref': ref, |
| 216 | 'type': self.deps.nodes[ref]['type'], | 201 | 'type': self.deps.nodes[ref]['type'], |
learnapp.py
| @@ -21,10 +21,6 @@ from tools import load_yaml | @@ -21,10 +21,6 @@ from tools import load_yaml | ||
| 21 | # setup logger for this module | 21 | # setup logger for this module |
| 22 | logger = logging.getLogger(__name__) | 22 | logger = logging.getLogger(__name__) |
| 23 | 23 | ||
| 24 | -# ============================================================================ | ||
| 25 | -# class LearnAppException(Exception): | ||
| 26 | -# pass | ||
| 27 | - | ||
| 28 | 24 | ||
| 29 | # ============================================================================ | 25 | # ============================================================================ |
| 30 | # helper functions | 26 | # helper functions |
| @@ -69,6 +65,7 @@ class LearnApp(object): | @@ -69,6 +65,7 @@ class LearnApp(object): | ||
| 69 | for c in config_files: | 65 | for c in config_files: |
| 70 | self.populate_graph(c) | 66 | self.populate_graph(c) |
| 71 | 67 | ||
| 68 | + self.build_factory() # for all questions of all topics | ||
| 72 | self.db_add_missing_topics(self.deps.nodes()) | 69 | self.db_add_missing_topics(self.deps.nodes()) |
| 73 | 70 | ||
| 74 | # ------------------------------------------------------------------------ | 71 | # ------------------------------------------------------------------------ |
| @@ -103,7 +100,7 @@ class LearnApp(object): | @@ -103,7 +100,7 @@ class LearnApp(object): | ||
| 103 | self.online[uid] = { | 100 | self.online[uid] = { |
| 104 | 'number': uid, | 101 | 'number': uid, |
| 105 | 'name': name, | 102 | 'name': name, |
| 106 | - 'state': StudentKnowledge(self.deps, state=state), | 103 | + 'state': StudentKnowledge(deps=self.deps, factory=self.factory, state=state), |
| 107 | } | 104 | } |
| 108 | 105 | ||
| 109 | else: | 106 | else: |
| @@ -232,12 +229,89 @@ class LearnApp(object): | @@ -232,12 +229,89 @@ class LearnApp(object): | ||
| 232 | logger.info(f'{m:6} topics') | 229 | logger.info(f'{m:6} topics') |
| 233 | logger.info(f'{q:6} answers') | 230 | logger.info(f'{q:6} answers') |
| 234 | 231 | ||
| 232 | + # ============================================================================ | ||
| 233 | + # Populates a digraph. | ||
| 234 | + # | ||
| 235 | + # Nodes are the topic references e.g. 'my/topic' | ||
| 236 | + # g.node['my/topic']['name'] name of the topic | ||
| 237 | + # g.node['my/topic']['questions'] list of question refs | ||
| 238 | + # | ||
| 239 | + # Edges are obtained from the deps defined in the YAML file for each topic. | ||
| 240 | + # ------------------------------------------------------------------------ | ||
| 241 | + def populate_graph(self, conffile): | ||
| 242 | + logger.info(f'Populating graph from: {conffile}') | ||
| 243 | + config = load_yaml(conffile) # course configuration | ||
| 244 | + | ||
| 245 | + # default attributes that apply to the topics | ||
| 246 | + default_file = config.get('file', 'questions.yaml') | ||
| 247 | + default_shuffle = config.get('shuffle', True) | ||
| 248 | + default_choose = config.get('choose', 9999) | ||
| 249 | + default_forgetting_factor = config.get('forgetting_factor', 1.0) | ||
| 250 | + | ||
| 251 | + # iterate over topics and populate graph | ||
| 252 | + topics = config.get('topics', {}) | ||
| 253 | + g = self.deps # the dependency graph | ||
| 254 | + | ||
| 255 | + for tref, attr in topics.items(): | ||
| 256 | + # g.add_node(tref) | ||
| 257 | + g.add_edges_from((d,tref) for d in attr.get('deps', [])) | ||
| 258 | + | ||
| 259 | + t = g.node[tref] # current topic node | ||
| 260 | + t['type'] = attr.get('type', 'topic') | ||
| 261 | + t['name'] = attr.get('name', tref) | ||
| 262 | + t['path'] = path.join(g.graph['prefix'], tref) # prefix/topic | ||
| 263 | + t['file'] = attr.get('file', default_file) # questions.yaml | ||
| 264 | + t['shuffle'] = attr.get('shuffle', default_shuffle) | ||
| 265 | + t['forgetting_factor'] = attr.get('forgetting_factor', default_forgetting_factor) | ||
| 266 | + t['choose'] = attr.get('choose', default_choose) | ||
| 267 | + t['questions'] = attr.get('questions', []) | ||
| 268 | + | ||
| 269 | + logger.info(f'Loaded {g.number_of_nodes()} topics') | ||
| 270 | + | ||
| 271 | + | ||
| 272 | + # ------------------------------------------------------------------------ | ||
| 273 | + # Buils dictionary of question factories | ||
| 274 | + # ------------------------------------------------------------------------ | ||
| 275 | + def build_factory(self): | ||
| 276 | + logger.info('Building questions factory') | ||
| 277 | + self.factory = {} # {'qref': QFactory()} | ||
| 278 | + g = self.deps | ||
| 279 | + for tref in g.nodes(): | ||
| 280 | + t = g.node[tref] | ||
| 281 | + | ||
| 282 | + # load questions as list of dicts | ||
| 283 | + topicpath = path.join(g.graph['prefix'], tref) | ||
| 284 | + questions = load_yaml(path.join(topicpath, t['file']), default=[]) | ||
| 285 | + | ||
| 286 | + # update refs to include topic as prefix. | ||
| 287 | + # refs are required to be unique only within the file. | ||
| 288 | + # undefined are set to topic:n, where n is the question number | ||
| 289 | + # within the file | ||
| 290 | + for i, q in enumerate(questions): | ||
| 291 | + qref = q.get('ref', str(i)) # ref or number | ||
| 292 | + q['ref'] = tref + ':' + qref | ||
| 293 | + q['path'] = topicpath | ||
| 294 | + | ||
| 295 | + # if questions are left undefined, include all. | ||
| 296 | + if not t['questions']: | ||
| 297 | + t['questions'] = [q['ref'] for q in questions] | ||
| 298 | + | ||
| 299 | + t['choose'] = min(t['choose'], len(t['questions'])) | ||
| 300 | + | ||
| 301 | + for q in questions: | ||
| 302 | + if q['ref'] in t['questions']: | ||
| 303 | + self.factory[q['ref']] = QFactory(q) | ||
| 304 | + | ||
| 305 | + logger.info(f'{len(t["questions"]):6} {tref}') | ||
| 306 | + | ||
| 307 | + logger.info(f'Factory contains {len(self.factory)} questions') | ||
| 308 | + | ||
| 309 | + | ||
| 235 | 310 | ||
| 236 | # ======================================================================== | 311 | # ======================================================================== |
| 237 | # methods that do not change state (pure functions) | 312 | # methods that do not change state (pure functions) |
| 238 | # ======================================================================== | 313 | # ======================================================================== |
| 239 | 314 | ||
| 240 | - | ||
| 241 | # ------------------------------------------------------------------------ | 315 | # ------------------------------------------------------------------------ |
| 242 | def get_student_name(self, uid): | 316 | def get_student_name(self, uid): |
| 243 | return self.online[uid].get('name', '') | 317 | return self.online[uid].get('name', '') |
| @@ -277,82 +351,6 @@ class LearnApp(object): | @@ -277,82 +351,6 @@ class LearnApp(object): | ||
| 277 | # ------------------------------------------------------------------------ | 351 | # ------------------------------------------------------------------------ |
| 278 | def get_current_public_dir(self, uid): | 352 | def get_current_public_dir(self, uid): |
| 279 | topic = self.online[uid]['state'].get_current_topic() | 353 | topic = self.online[uid]['state'].get_current_topic() |
| 280 | - p = self.deps.graph['prefix'] # FIXME not defined!!! | ||
| 281 | - | ||
| 282 | - return path.join(p, topic, 'public') | ||
| 283 | - | ||
| 284 | - | ||
| 285 | - # ============================================================================ | ||
| 286 | - # Populates a digraph. | ||
| 287 | - # | ||
| 288 | - # First, topics such as `computer/mips/exceptions` are added as nodes | ||
| 289 | - # together with dependencies. Then, questions are loaded to a factory. | ||
| 290 | - # | ||
| 291 | - # g.graph['path'] base path where topic directories are located | ||
| 292 | - # g.graph['title'] title defined in the configuration YAML | ||
| 293 | - # g.graph['database'] sqlite3 database file to use | ||
| 294 | - # | ||
| 295 | - # Nodes are the topic references e.g. 'my/topic' | ||
| 296 | - # g.node['my/topic']['name'] name of the topic | ||
| 297 | - # g.node['my/topic']['questions'] list of question refs defined in YAML | ||
| 298 | - # g.node['my/topic']['factory'] dict with question factories | ||
| 299 | - # | ||
| 300 | - # Edges are obtained from the deps defined in the YAML file for each topic. | ||
| 301 | - # ---------------------------------------------------------------------------- | ||
| 302 | - def populate_graph(self, conffile): | ||
| 303 | - logger.info(f'Loading {conffile} and populating graph:') | ||
| 304 | - g = self.deps # the graph | ||
| 305 | - config = load_yaml(conffile) # course configuration | ||
| 306 | - | ||
| 307 | - # default attributes that apply to the topics | ||
| 308 | - default_file = config.get('file', 'questions.yaml') | ||
| 309 | - default_shuffle = config.get('shuffle', True) | ||
| 310 | - default_choose = config.get('choose', 9999) | ||
| 311 | - default_forgetting_factor = config.get('forgetting_factor', 1.0) | ||
| 312 | - | ||
| 313 | - # iterate over topics and populate graph | ||
| 314 | - topics = config.get('topics', {}) | ||
| 315 | - tcount = qcount = 0 # topic and question counters | ||
| 316 | - for tref, attr in topics.items(): | ||
| 317 | - if tref in g: | ||
| 318 | - logger.error(f'--> Topic {tref} already exists. Skipped.') | ||
| 319 | - continue | ||
| 320 | - | ||
| 321 | - # add topic to the graph | ||
| 322 | - g.add_node(tref) | ||
| 323 | - t = g.node[tref] # current topic node | ||
| 324 | - | ||
| 325 | - topicpath = path.join(g.graph['prefix'], tref) | ||
| 326 | - | ||
| 327 | - t['type'] = attr.get('type', 'topic') | ||
| 328 | - t['name'] = attr.get('name', tref) | ||
| 329 | - t['path'] = topicpath # prefix/topic | ||
| 330 | - t['file'] = attr.get('file', default_file) # questions.yaml | ||
| 331 | - t['shuffle'] = attr.get('shuffle', default_shuffle) | ||
| 332 | - t['forgetting_factor'] = attr.get('forgetting_factor', default_forgetting_factor) | ||
| 333 | - g.add_edges_from((d,tref) for d in attr.get('deps', [])) | ||
| 334 | - | ||
| 335 | - # load questions as list of dicts | ||
| 336 | - questions = load_yaml(path.join(topicpath, t['file']), default=[]) | ||
| 337 | - | ||
| 338 | - # if questions are left undefined, include all. | ||
| 339 | - # refs undefined in questions.yaml are set to topic:n | ||
| 340 | - t['questions'] = attr.get('questions', | ||
| 341 | - [q.setdefault('ref', f'{tref}:{i}') for i, q in enumerate(questions)]) | ||
| 342 | - | ||
| 343 | - # topic will generate a certain amount of questions | ||
| 344 | - t['choose'] = min(attr.get('choose', default_choose), len(t['questions'])) | ||
| 345 | - | ||
| 346 | - # make questions factory (without repeating same question) FIXME move to somewhere else? | ||
| 347 | - t['factory'] = {} | ||
| 348 | - for q in questions: | ||
| 349 | - if q['ref'] in t['questions']: | ||
| 350 | - q['path'] = topicpath # fullpath added to each question | ||
| 351 | - t['factory'][q['ref']] = QFactory(q) | ||
| 352 | - | ||
| 353 | - logger.info(f'{len(t["questions"]):6} {tref}') | ||
| 354 | - qcount += len(t["questions"]) # count total questions | ||
| 355 | - tcount += 1 | ||
| 356 | - | ||
| 357 | - logger.info(f'Total loaded: {tcount} topics, {qcount} questions') | 354 | + prefix = self.deps.graph['prefix'] |
| 355 | + return path.join(prefix, topic, 'public') | ||
| 358 | 356 |
serve.py
| @@ -60,6 +60,7 @@ class WebApplication(tornado.web.Application): | @@ -60,6 +60,7 @@ class WebApplication(tornado.web.Application): | ||
| 60 | super().__init__(handlers, **settings) | 60 | super().__init__(handlers, **settings) |
| 61 | self.learn = learnapp | 61 | self.learn = learnapp |
| 62 | 62 | ||
| 63 | + | ||
| 63 | # ============================================================================ | 64 | # ============================================================================ |
| 64 | # Handlers | 65 | # Handlers |
| 65 | # ============================================================================ | 66 | # ============================================================================ |
| @@ -100,6 +101,7 @@ class LoginHandler(BaseHandler): | @@ -100,6 +101,7 @@ class LoginHandler(BaseHandler): | ||
| 100 | else: | 101 | else: |
| 101 | self.render("login.html", error='Número ou senha incorrectos') | 102 | self.render("login.html", error='Número ou senha incorrectos') |
| 102 | 103 | ||
| 104 | + | ||
| 103 | # ---------------------------------------------------------------------------- | 105 | # ---------------------------------------------------------------------------- |
| 104 | class LogoutHandler(BaseHandler): | 106 | class LogoutHandler(BaseHandler): |
| 105 | @tornado.web.authenticated | 107 | @tornado.web.authenticated |
| @@ -110,6 +112,7 @@ class LogoutHandler(BaseHandler): | @@ -110,6 +112,7 @@ class LogoutHandler(BaseHandler): | ||
| 110 | def on_finish(self): | 112 | def on_finish(self): |
| 111 | self.learn.logout(self.current_user) | 113 | self.learn.logout(self.current_user) |
| 112 | 114 | ||
| 115 | + | ||
| 113 | # ---------------------------------------------------------------------------- | 116 | # ---------------------------------------------------------------------------- |
| 114 | class ChangePasswordHandler(BaseHandler): | 117 | class ChangePasswordHandler(BaseHandler): |
| 115 | @tornado.web.authenticated | 118 | @tornado.web.authenticated |
| @@ -124,6 +127,7 @@ class ChangePasswordHandler(BaseHandler): | @@ -124,6 +127,7 @@ class ChangePasswordHandler(BaseHandler): | ||
| 124 | notification = tornado.escape.to_unicode(self.render_string('notification.html', type='danger', msg='A password não foi alterada!')) | 127 | notification = tornado.escape.to_unicode(self.render_string('notification.html', type='danger', msg='A password não foi alterada!')) |
| 125 | self.write({'msg': notification}) | 128 | self.write({'msg': notification}) |
| 126 | 129 | ||
| 130 | + | ||
| 127 | # ---------------------------------------------------------------------------- | 131 | # ---------------------------------------------------------------------------- |
| 128 | # Main page: / | 132 | # Main page: / |
| 129 | # Shows a list of topics and proficiency (stars, locked). | 133 | # Shows a list of topics and proficiency (stars, locked). |
| @@ -140,6 +144,7 @@ class RootHandler(BaseHandler): | @@ -140,6 +144,7 @@ class RootHandler(BaseHandler): | ||
| 140 | # get_topic_type=self.learn.get_topic_type, # function | 144 | # get_topic_type=self.learn.get_topic_type, # function |
| 141 | ) | 145 | ) |
| 142 | 146 | ||
| 147 | + | ||
| 143 | # ---------------------------------------------------------------------------- | 148 | # ---------------------------------------------------------------------------- |
| 144 | # Start a given topic: /topic/... | 149 | # Start a given topic: /topic/... |
| 145 | # ---------------------------------------------------------------------------- | 150 | # ---------------------------------------------------------------------------- |
| @@ -286,7 +291,9 @@ class QuestionHandler(BaseHandler): | @@ -286,7 +291,9 @@ class QuestionHandler(BaseHandler): | ||
| 286 | logger.error(f'Unknown action {action}') | 291 | logger.error(f'Unknown action {action}') |
| 287 | 292 | ||
| 288 | 293 | ||
| 289 | -# ------------------------------------------------------------------------- | 294 | +# ---------------------------------------------------------------------------- |
| 295 | +# Signal handler to catch Ctrl-C and abort server | ||
| 296 | +# ---------------------------------------------------------------------------- | ||
| 290 | def signal_handler(signal, frame): | 297 | def signal_handler(signal, frame): |
| 291 | r = input(' --> Stop webserver? (yes/no) ').lower() | 298 | r = input(' --> Stop webserver? (yes/no) ').lower() |
| 292 | if r == 'yes': | 299 | if r == 'yes': |
tools.py
| @@ -139,11 +139,11 @@ def load_yaml(filename, default=None): | @@ -139,11 +139,11 @@ def load_yaml(filename, default=None): | ||
| 139 | try: | 139 | try: |
| 140 | f = open(filename, 'r', encoding='utf-8') | 140 | f = open(filename, 'r', encoding='utf-8') |
| 141 | except FileNotFoundError: | 141 | except FileNotFoundError: |
| 142 | - logger.error(f'Can\'t open "{filename}": not found') | 142 | + logger.error(f'Cannot open "{filename}": not found') |
| 143 | except PermissionError: | 143 | except PermissionError: |
| 144 | - logger.error(f'Can\'t open "{filename}": no permission') | 144 | + logger.error(f'Cannot open "{filename}": no permission') |
| 145 | except IOError: | 145 | except IOError: |
| 146 | - logger.error(f'Can\'t open file "{filename}"') | 146 | + logger.error(f'Cannot open file "{filename}"') |
| 147 | else: | 147 | else: |
| 148 | with f: | 148 | with f: |
| 149 | try: | 149 | try: |