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