Commit fcdedf5af3c8f77e05d228b96446467568e4d8d9
1 parent
5f7d3068
Exists in
master
and in
1 other branch
- Reads config.yaml with dependency graph
- Each question has it's own factory - Questions are added to a list in each node of the graph - Database has a topics table and N-M relationship to students
Showing
10 changed files
with
304 additions
and
146 deletions
Show diff stats
BUGS.md
1 | 1 | BUGS: |
2 | 2 | |
3 | +- servir ficheiros de public temporariamente | |
3 | 4 | - se students.db não existe, rebenta. |
4 | -- questions hardcoded in LearnApp. | |
5 | 5 | - database hardcoded in LearnApp. |
6 | 6 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
7 | 7 | |
8 | 8 | TODO: |
9 | 9 | |
10 | +- mostrar comments quando falha a resposta | |
10 | 11 | - configuração e linha de comando. |
11 | 12 | - como gerar uma sequencia de perguntas? |
12 | 13 | - generators not working: bcrypt (ver blog) |
13 | 14 | |
14 | 15 | SOLVED: |
15 | 16 | |
17 | +- path dos generators scripts mal construido | |
18 | +- questions hardcoded in LearnApp. | |
19 | +- Factory para cada pergunta individual em vez de pool | |
16 | 20 | - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. |
17 | 21 | - logging |
18 | 22 | - textarea tem codigo para preencher o texto, mas ja não é necessário porque pergunta não é reloaded. | ... | ... |
app.py
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | # python standard library |
3 | 3 | from contextlib import contextmanager # `with` statement in db sessions |
4 | 4 | import logging |
5 | +from os import path | |
5 | 6 | |
6 | 7 | # user installed libraries |
7 | 8 | try: |
... | ... | @@ -15,8 +16,10 @@ except ImportError: |
15 | 16 | sys.exit(1) |
16 | 17 | |
17 | 18 | # this project |
18 | -from models import Student, Answer | |
19 | +from models import Student, Answer, Topic, StudentTopic | |
19 | 20 | from knowledge import Knowledge |
21 | +from questions import QFactory | |
22 | +from tools import load_yaml | |
20 | 23 | |
21 | 24 | # setup logger for this module |
22 | 25 | logger = logging.getLogger(__name__) |
... | ... | @@ -33,20 +36,7 @@ class LearnApp(object): |
33 | 36 | self.setup_db('students.db') # FIXME |
34 | 37 | |
35 | 38 | # build dependency graph |
36 | - self.build_dependency_graph({'uevora/lei'}) # FIXME | |
37 | - | |
38 | - # ------------------------------------------------------------------------ | |
39 | - def setup_db(self, db): | |
40 | - engine = create_engine(f'sqlite:///{db}', echo=False) | |
41 | - self.Session = sessionmaker(bind=engine) | |
42 | - try: | |
43 | - with self.db_session() as s: | |
44 | - n = s.query(Student).count() | |
45 | - except Exception as e: | |
46 | - logger.critical('Database not usable.') | |
47 | - raise e | |
48 | - else: | |
49 | - logger.info(f'Database has {n} students registered.') | |
39 | + self.build_dependency_graph('demo/config.yaml') # FIXME | |
50 | 40 | |
51 | 41 | # ------------------------------------------------------------------------ |
52 | 42 | def login(self, uid, try_pw): |
... | ... | @@ -62,17 +52,21 @@ class LearnApp(object): |
62 | 52 | logger.info(f'User "{uid}" wrong password.') |
63 | 53 | return False # wrong password |
64 | 54 | |
55 | + student_state = s.query(StudentTopic).filter(StudentTopic.student_id == uid).all() | |
56 | + | |
65 | 57 | # success |
66 | 58 | self.online[uid] = { |
67 | 59 | 'name': student.name, |
68 | 60 | 'number': student.id, |
69 | - 'knowledge': Knowledge(), # FIXME initial state? | |
61 | + 'state': Knowledge(self.depgraph, student_state), | |
70 | 62 | } |
63 | + logger.info(f'User "{uid}" logged in') | |
71 | 64 | return True |
72 | 65 | |
73 | 66 | # ------------------------------------------------------------------------ |
74 | 67 | # logout |
75 | 68 | def logout(self, uid): |
69 | + logger.info(f'User "{uid}" logged out') | |
76 | 70 | del self.online[uid] # FIXME save current state |
77 | 71 | |
78 | 72 | # ------------------------------------------------------------------------ |
... | ... | @@ -82,10 +76,12 @@ class LearnApp(object): |
82 | 76 | # ------------------------------------------------------------------------ |
83 | 77 | # check answer and if correct returns new question, otherise returns None |
84 | 78 | def check_answer(self, uid, answer): |
85 | - knowledge = self.online[uid]['knowledge'] | |
79 | + logger.debug(f'check_answer("{uid}", "{answer}")') | |
80 | + knowledge = self.online[uid]['state'] | |
86 | 81 | current_question = knowledge.check_answer(answer) |
87 | 82 | |
88 | 83 | if current_question is not None: |
84 | + logger.debug('check_answer: saving answer to db ...') | |
89 | 85 | with self.db_session() as s: |
90 | 86 | s.add(Answer( |
91 | 87 | ref=current_question['ref'], |
... | ... | @@ -94,7 +90,9 @@ class LearnApp(object): |
94 | 90 | finishtime=str(current_question['finish_time']), |
95 | 91 | student_id=uid)) |
96 | 92 | s.commit() |
93 | + logger.debug('check_answer: saving done') | |
97 | 94 | |
95 | + logger.debug('check_answer: will return knowledge.new_question') | |
98 | 96 | return knowledge.new_question() |
99 | 97 | |
100 | 98 | # ------------------------------------------------------------------------ |
... | ... | @@ -106,42 +104,63 @@ class LearnApp(object): |
106 | 104 | try: |
107 | 105 | yield session |
108 | 106 | session.commit() |
109 | - except: | |
107 | + except Exception as e: | |
110 | 108 | session.rollback() |
111 | - raise | |
109 | + raise e | |
112 | 110 | finally: |
113 | 111 | session.close() |
114 | 112 | |
115 | 113 | # ------------------------------------------------------------------------ |
116 | 114 | # Receives a set of topics (strings like "math/algebra"), |
117 | 115 | # and recursively adds dependencies to the dependency graph |
118 | - def build_dependency_graph(self, topics=set()): | |
119 | - g = nx.DiGraph() | |
120 | - | |
121 | - # add nodes "recursively" following the config.yaml files | |
122 | - while topics: | |
123 | - t = topics.pop() # take one topic | |
124 | - if t in g.nodes(): # skip it if already in the graph | |
125 | - continue | |
126 | - | |
127 | - # get configuration dictionary for this topic | |
128 | - # dictionary has keys: type, title, depends | |
129 | - try: | |
130 | - with open(f'topics/{t}/config.yaml', 'r') as f: | |
131 | - logger.info(f'Loading {t}') | |
132 | - config = yaml.load(f) | |
133 | - except FileNotFoundError: | |
134 | - logger.error(f'Not found: topics/{t}/config.yaml') | |
135 | - continue | |
136 | - | |
137 | - config.setdefault('depends', set()) # make sure 'depends' key exists | |
138 | - topics.update(config['depends']) | |
139 | - g.add_node(t, config=config) | |
140 | - | |
141 | - # add edges topic -> dependency | |
142 | - for t in g.nodes(): | |
143 | - deps = g.node[t]['config']['depends'] | |
144 | - g.add_edges_from([(t, d) for d in deps]) | |
116 | + def build_dependency_graph(self, config_file): | |
117 | + logger.debug(f'build_dependency_graph("{config_file}")') | |
118 | + | |
119 | + # Load configuration file | |
120 | + try: | |
121 | + with open(config_file, 'r') as f: | |
122 | + logger.info(f'Loading configuration file "{config_file}"') | |
123 | + config = yaml.load(f) | |
124 | + except FileNotFoundError as e: | |
125 | + logger.error(f'File not found: "{config_file}"') | |
126 | + raise e | |
127 | + | |
128 | + prefix = config['path'] # FIXME default if does not exist? | |
129 | + g = nx.DiGraph(path=prefix) | |
130 | + | |
131 | + # Build dependency graph | |
132 | + deps = config.get('dependencies', {}) | |
133 | + for n,dd in deps.items(): | |
134 | + g.add_edges_from((n,d) for d in dd) | |
135 | + | |
136 | + # Builds factories for each node | |
137 | + for n in g.nodes_iter(): | |
138 | + fullpath = path.join(prefix, n) | |
139 | + # if name is directory defaults to "prefix/questions.yaml" | |
140 | + if path.isdir(fullpath): | |
141 | + filename = path.join(fullpath, "questions.yaml") | |
142 | + | |
143 | + if path.isfile(filename): | |
144 | + logger.info(f'Loading questions from "{filename}"') | |
145 | + questions = load_yaml(filename, default=[]) | |
146 | + for q in questions: | |
147 | + q['path'] = fullpath | |
148 | + | |
149 | + g.node[n]['factory'] = [QFactory(q) for q in questions] | |
145 | 150 | |
146 | 151 | self.depgraph = g |
147 | - logger.info(f'Graph has {g.number_of_nodes()} nodes and {g.number_of_edges()} edges') | |
152 | + | |
153 | + # ------------------------------------------------------------------------ | |
154 | + # setup and check database | |
155 | + def setup_db(self, db): | |
156 | + engine = create_engine(f'sqlite:///{db}', echo=False) | |
157 | + self.Session = sessionmaker(bind=engine) | |
158 | + try: | |
159 | + with self.db_session() as s: | |
160 | + n = s.query(Student).count() | |
161 | + except Exception as e: | |
162 | + logger.critical('Database not usable.') | |
163 | + raise e | |
164 | + else: | |
165 | + logger.info(f'Database has {n} students registered.') | |
166 | + | ... | ... |
config/logger.yaml
... | ... | @@ -5,11 +5,11 @@ formatters: |
5 | 5 | void: |
6 | 6 | format: '' |
7 | 7 | standard: |
8 | - format: '%(asctime)s | %(levelname)-8s | %(name)-8s | %(message)s' | |
8 | + format: '%(asctime)s | %(levelname)-8s | %(name)-9s | %(message)s' | |
9 | 9 | |
10 | 10 | handlers: |
11 | 11 | default: |
12 | - level: 'INFO' | |
12 | + level: 'DEBUG' | |
13 | 13 | class: 'logging.StreamHandler' |
14 | 14 | formatter: 'standard' |
15 | 15 | stream: 'ext://sys.stdout' |
... | ... | @@ -21,12 +21,12 @@ loggers: |
21 | 21 | |
22 | 22 | 'app': |
23 | 23 | handlers: ['default'] |
24 | - level: 'INFO' | |
24 | + level: 'DEBUG' | |
25 | 25 | propagate: False |
26 | 26 | |
27 | 27 | 'questions': |
28 | 28 | handlers: ['default'] |
29 | - level: 'INFO' | |
29 | + level: 'DEBUG' | |
30 | 30 | propagate: False |
31 | 31 | |
32 | 32 | 'tools': |
... | ... | @@ -34,3 +34,13 @@ loggers: |
34 | 34 | level: 'INFO' |
35 | 35 | propagate: False |
36 | 36 | |
37 | + 'knowledge': | |
38 | + handlers: ['default'] | |
39 | + level: 'DEBUG' | |
40 | + propagate: False | |
41 | + | |
42 | + # 'root': | |
43 | + # handlers: ['default'] | |
44 | + # level: 'DEBUG' | |
45 | + # propagate: False | |
46 | + # | |
37 | 47 | \ No newline at end of file | ... | ... |
knowledge.py
... | ... | @@ -2,36 +2,61 @@ |
2 | 2 | # python standard library |
3 | 3 | import random |
4 | 4 | from datetime import datetime |
5 | +import logging | |
6 | + | |
7 | +# libraries | |
8 | +import networkx as nx | |
5 | 9 | |
6 | 10 | # this project |
7 | 11 | import questions |
8 | 12 | |
13 | +# setup logger for this module | |
14 | +logger = logging.getLogger(__name__) | |
9 | 15 | |
16 | +# ---------------------------------------------------------------------------- | |
10 | 17 | # contains the kowledge state of each student. |
11 | 18 | class Knowledge(object): |
12 | - def __init__(self): | |
13 | - self.factory = questions.QuestionFactory() | |
14 | - self.factory.load_files(['questions.yaml'], 'demo') # FIXME | |
19 | + def __init__(self, depgraph, state={}): | |
20 | + self.depgraph = depgraph | |
21 | + self.state = state # {node: level, node: level, ...} | |
15 | 22 | self.current_question = None |
16 | 23 | |
24 | + # self.seq = nx.topological_sort(self.depgraph) | |
25 | + | |
17 | 26 | def get_current_question(self): |
18 | 27 | return self.current_question |
19 | 28 | |
29 | + def get_knowledge_state(self): | |
30 | + return self.state | |
31 | + | |
20 | 32 | # --- generates a new question given the current state |
21 | 33 | def new_question(self): |
22 | - # FIXME | |
34 | + logger.debug('new_question()') | |
23 | 35 | if self.current_question is None or self.current_question.get('grade', 0.0) > 0.9999: |
24 | - questions = list(self.factory) | |
25 | - self.current_question = self.factory.generate(random.choice(questions)) | |
36 | + g = self.depgraph | |
37 | + # choose topic, ie, node of the graph | |
38 | + # print(g.nodes()) | |
39 | + topic = random.choice(g.nodes()) # FIXME | |
40 | + # print(topic) | |
41 | + # choose question from that topic | |
42 | + question = random.choice(g.node[topic]['factory']) | |
43 | + # print(question) | |
44 | + | |
45 | + self.current_question = question.generate() | |
46 | + # print(self.current_question) | |
47 | + | |
48 | + # self.current_question = g.node[topic]['factory'].generate(ref) | |
26 | 49 | self.current_question['start_time'] = datetime.now() |
27 | - | |
28 | 50 | return self.current_question |
29 | 51 | |
30 | 52 | # --- checks answer and updates knowledge state |
31 | 53 | # returns current question with correction, time and comments updated |
32 | 54 | def check_answer(self, answer): |
55 | + logger.debug(f'check_answer("{answer}")') | |
33 | 56 | question = self.current_question |
34 | 57 | if question is not None: |
35 | 58 | question['finish_time'] = datetime.now() |
36 | 59 | question.correct(answer) |
60 | + | |
61 | + # logger.debug(f'check_answer returning') | |
37 | 62 | return question | ... | ... |
models.py
... | ... | @@ -10,6 +10,17 @@ from sqlalchemy.orm import relationship |
10 | 10 | Base = declarative_base() |
11 | 11 | |
12 | 12 | # --------------------------------------------------------------------------- |
13 | +class StudentTopic(Base): | |
14 | + __tablename__ = 'studenttopic' | |
15 | + student_id = Column(String, ForeignKey('students.id'), primary_key=True) | |
16 | + topic_id = Column(String, ForeignKey('topics.id'), primary_key=True) | |
17 | + level = Column(Float) | |
18 | + | |
19 | + # --- | |
20 | + student = relationship('Student', back_populates='topics') | |
21 | + topic = relationship('Topic', back_populates='students') | |
22 | + | |
23 | +# --------------------------------------------------------------------------- | |
13 | 24 | # Registered students |
14 | 25 | # --------------------------------------------------------------------------- |
15 | 26 | class Student(Base): |
... | ... | @@ -20,6 +31,7 @@ class Student(Base): |
20 | 31 | |
21 | 32 | # --- |
22 | 33 | answers = relationship('Answer', back_populates='student') |
34 | + topics = relationship('StudentTopic', back_populates='student') | |
23 | 35 | |
24 | 36 | def __repr__(self): |
25 | 37 | return f'''Student: |
... | ... | @@ -52,3 +64,15 @@ class Answer(Base): |
52 | 64 | finishtime: "{self.finishtime}" |
53 | 65 | student_id: "{self.student_id}"''' |
54 | 66 | |
67 | +# --------------------------------------------------------------------------- | |
68 | +# Table with student state | |
69 | +# --------------------------------------------------------------------------- | |
70 | +class Topic(Base): | |
71 | + __tablename__ = 'topics' | |
72 | + id = Column(String, primary_key=True) | |
73 | + | |
74 | + # --- | |
75 | + students = relationship('StudentTopic', back_populates='topic') | |
76 | + | |
77 | + | |
78 | + | ... | ... |
questions.py
... | ... | @@ -49,8 +49,6 @@ else: |
49 | 49 | from tools import load_yaml, run_script |
50 | 50 | |
51 | 51 | |
52 | - | |
53 | - | |
54 | 52 | # =========================================================================== |
55 | 53 | # Questions derived from Question are already instantiated and ready to be |
56 | 54 | # presented to students. |
... | ... | @@ -320,6 +318,7 @@ class QuestionTextArea(Question): |
320 | 318 | |
321 | 319 | #------------------------------------------------------------------------ |
322 | 320 | def __init__(self, q): |
321 | + logger.debug('QuestionTextArea.__init__()') | |
323 | 322 | super().__init__(q) |
324 | 323 | |
325 | 324 | self.set_defaults({ |
... | ... | @@ -381,14 +380,14 @@ class QuestionInformation(Question): |
381 | 380 | |
382 | 381 | |
383 | 382 | |
383 | + | |
384 | 384 | # =========================================================================== |
385 | -# This class contains a pool of questions generators from which particular | |
386 | -# Question() instances are generated using QuestionsFactory.generate(ref). | |
385 | +# Question Factory | |
387 | 386 | # =========================================================================== |
388 | -class QuestionFactory(dict): | |
387 | +class QFactory(object): | |
389 | 388 | # Depending on the type of question, a different question class will be |
390 | 389 | # instantiated. All these classes derive from the base class `Question`. |
391 | - types = { | |
390 | + _types = { | |
392 | 391 | 'radio' : QuestionRadio, |
393 | 392 | 'checkbox' : QuestionCheckbox, |
394 | 393 | 'text' : QuestionText, |
... | ... | @@ -399,71 +398,20 @@ class QuestionFactory(dict): |
399 | 398 | 'information': QuestionInformation, |
400 | 399 | 'warning' : QuestionInformation, |
401 | 400 | 'alert' : QuestionInformation, |
402 | - } | |
403 | - | |
404 | - # ----------------------------------------------------------------------- | |
405 | - def __init__(self): | |
406 | - super().__init__() | |
407 | - | |
408 | - # ----------------------------------------------------------------------- | |
409 | - # Add single question provided in a dictionary. | |
410 | - # After this, each question will have at least 'ref' and 'type' keys. | |
411 | - # ----------------------------------------------------------------------- | |
412 | - def add(self, question): | |
413 | - # if ref missing try ref='/path/file.yaml:3' | |
414 | - try: | |
415 | - question.setdefault('ref', question['filename'] + ':' + str(question['index'])) | |
416 | - except KeyError: | |
417 | - logger.error('Missing "ref". Cannot add question to the pool.') | |
418 | - return | |
419 | - | |
420 | - # check duplicate references | |
421 | - if question['ref'] in self: | |
422 | - logger.error('Duplicate reference "{0}". Replacing the original one!'.format(question['ref'])) | |
423 | - | |
424 | - question.setdefault('type', 'information') | |
425 | - | |
426 | - self[question['ref']] = question | |
427 | - logger.debug('Added question "{0}" to the pool.'.format(question['ref'])) | |
428 | - | |
429 | - # ----------------------------------------------------------------------- | |
430 | - # load single YAML questions file | |
431 | - # ----------------------------------------------------------------------- | |
432 | - def load_file(self, filename, questions_dir=''): | |
433 | - f = path.normpath(path.join(questions_dir, filename)) | |
434 | - questions = load_yaml(f, default=[]) | |
435 | - | |
436 | - n = 0 | |
437 | - for i, q in enumerate(questions): | |
438 | - if isinstance(q, dict): | |
439 | - q.update({ | |
440 | - 'filename': filename, | |
441 | - 'path': questions_dir, | |
442 | - 'index': i # position in the file, 0 based | |
443 | - }) | |
444 | - self.add(q) # add question | |
445 | - n += 1 # counter | |
446 | - else: | |
447 | - logger.error('Question index {0} from file {1} is not a dictionary. Skipped!'.format(i, filename)) | |
401 | + } | |
448 | 402 | |
449 | - logger.info('Loaded {0} questions from "{1}".'.format(n, filename)) | |
450 | - | |
451 | - # ----------------------------------------------------------------------- | |
452 | - # load multiple YAML question files | |
453 | - # ----------------------------------------------------------------------- | |
454 | - def load_files(self, files, questions_dir=''): | |
455 | - for filename in files: | |
456 | - self.load_file(filename, questions_dir) | |
403 | + def __init__(self, question_dict): | |
404 | + self.question = question_dict | |
457 | 405 | |
458 | 406 | # ----------------------------------------------------------------------- |
459 | 407 | # Given a ref returns an instance of a descendent of Question(), |
460 | 408 | # i.e. a question object (radio, checkbox, ...). |
461 | 409 | # ----------------------------------------------------------------------- |
462 | - def generate(self, ref): | |
463 | - | |
410 | + def generate(self): | |
411 | + logger.debug('generate()') | |
464 | 412 | # Shallow copy so that script generated questions will not replace |
465 | 413 | # the original generators |
466 | - q = self[ref].copy() | |
414 | + q = self.question.copy() | |
467 | 415 | |
468 | 416 | # If question is of generator type, an external program will be run |
469 | 417 | # which will print a valid question in yaml format to stdout. This |
... | ... | @@ -471,27 +419,153 @@ class QuestionFactory(dict): |
471 | 419 | if q['type'] == 'generator': |
472 | 420 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) |
473 | 421 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
474 | - script = path.normpath(path.join(q['path'], q['script'])) | |
422 | + print(q['path']) | |
423 | + print(q['script']) | |
424 | + script = path.join(q['path'], q['script']) | |
475 | 425 | out = run_script(script=script, stdin=q['arg']) |
476 | - try: | |
477 | - q.update(out) | |
478 | - except: | |
479 | - q.update({ | |
480 | - 'type': 'alert', | |
481 | - 'title': 'Erro interno', | |
482 | - 'text': 'Ocorreu um erro a gerar esta pergunta.' | |
483 | - }) | |
484 | - # The generator was replaced by a question but not yet instantiated | |
426 | + q.update(out) | |
427 | + # try: | |
428 | + # q.update(out) | |
429 | + # except: | |
430 | + # logger.error(f'Question generator "{q["ref"]}"') | |
431 | + # q.update({ | |
432 | + # 'type': 'alert', | |
433 | + # 'title': 'Erro interno', | |
434 | + # 'text': 'Ocorreu um erro a gerar esta pergunta.' | |
435 | + # }) | |
485 | 436 | |
486 | 437 | # Finally we create an instance of Question() |
487 | - try: | |
488 | - qinstance = self.types[q['type']](q) # instance with correct class | |
489 | - except KeyError as e: | |
490 | - logger.error('Unknown question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | |
491 | - raise e | |
492 | - except: | |
493 | - logger.error('Failed to create question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | |
494 | - else: | |
495 | - logger.debug('Generated question "{}".'.format(ref)) | |
496 | - return qinstance | |
438 | + logger.debug('create instance...') | |
439 | + # try: | |
440 | + # qinstance = self._types[q['type']](q) # instance with correct class | |
441 | + # except KeyError as e: | |
442 | + # logger.error('Unknown question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | |
443 | + # raise e | |
444 | + # except: | |
445 | + # logger.error('Failed to create question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | |
446 | + # else: | |
447 | + # logger.debug('Generated question "{}".'.format(ref)) | |
448 | + # return qinstance | |
449 | + qinstance = self._types[q['type']](q) # instance with correct class | |
450 | + logger.debug('returning') | |
451 | + return qinstance | |
452 | + | |
453 | + | |
454 | +# =========================================================================== | |
455 | +# This class contains a pool of questions generators from which particular | |
456 | +# Question() instances are generated using QuestionsFactory.generate(ref). | |
457 | +# =========================================================================== | |
458 | +# class QuestionFactory(dict): | |
459 | +# # Depending on the type of question, a different question class will be | |
460 | +# # instantiated. All these classes derive from the base class `Question`. | |
461 | +# _types = { | |
462 | +# 'radio' : QuestionRadio, | |
463 | +# 'checkbox' : QuestionCheckbox, | |
464 | +# 'text' : QuestionText, | |
465 | +# 'text_regex': QuestionTextRegex, | |
466 | +# 'text_numeric': QuestionTextNumeric, | |
467 | +# 'textarea' : QuestionTextArea, | |
468 | +# # informative panels | |
469 | +# 'information': QuestionInformation, | |
470 | +# 'warning' : QuestionInformation, | |
471 | +# 'alert' : QuestionInformation, | |
472 | +# } | |
473 | + | |
474 | +# # ----------------------------------------------------------------------- | |
475 | +# def __init__(self, questions=None): | |
476 | +# super().__init__() | |
477 | +# if isinstance(questions, dict): | |
478 | +# self.add(questions) | |
479 | +# elif isinstance(questions, str): | |
480 | +# self.load_file(questions) | |
481 | + | |
482 | +# # ----------------------------------------------------------------------- | |
483 | +# # Add single question provided in a dictionary. | |
484 | +# # After this, each question will have at least 'ref' and 'type' keys. | |
485 | +# # ----------------------------------------------------------------------- | |
486 | +# def add(self, question): | |
487 | +# # if ref missing try ref='/path/file.yaml:3' | |
488 | +# try: | |
489 | +# question.setdefault('ref', question['filename'] + ':' + str(question['index'])) | |
490 | +# except KeyError: | |
491 | +# logger.error('Missing "ref". Cannot add question to the pool.') | |
492 | +# return | |
493 | + | |
494 | +# # check duplicate references | |
495 | +# if question['ref'] in self: | |
496 | +# logger.error('Duplicate reference "{0}". Replacing the original one!'.format(question['ref'])) | |
497 | + | |
498 | +# question.setdefault('type', 'information') | |
499 | + | |
500 | +# self[question['ref']] = question | |
501 | +# logger.debug('Added question "{0}" to the pool.'.format(question['ref'])) | |
502 | + | |
503 | +# # ----------------------------------------------------------------------- | |
504 | +# # load single YAML questions file | |
505 | +# # ----------------------------------------------------------------------- | |
506 | +# def load_file(self, filename, questions_dir=''): | |
507 | +# f = path.normpath(path.join(questions_dir, filename)) | |
508 | +# questions = load_yaml(f, default=[]) | |
509 | + | |
510 | +# n = 0 | |
511 | +# for i, q in enumerate(questions): | |
512 | +# if isinstance(q, dict): | |
513 | +# q.update({ | |
514 | +# 'filename': filename, | |
515 | +# 'path': questions_dir, | |
516 | +# 'index': i # position in the file, 0 based | |
517 | +# }) | |
518 | +# self.add(q) # add question | |
519 | +# n += 1 # counter | |
520 | +# else: | |
521 | +# logger.error('Question index {0} from file {1} is not a dictionary. Skipped!'.format(i, filename)) | |
522 | + | |
523 | +# logger.info('Loaded {0} questions from "{1}".'.format(n, filename)) | |
524 | + | |
525 | +# # ----------------------------------------------------------------------- | |
526 | +# # load multiple YAML question files | |
527 | +# # ----------------------------------------------------------------------- | |
528 | +# def load_files(self, files, questions_dir=''): | |
529 | +# for filename in files: | |
530 | +# self.load_file(filename, questions_dir) | |
531 | + | |
532 | +# # ----------------------------------------------------------------------- | |
533 | +# # Given a ref returns an instance of a descendent of Question(), | |
534 | +# # i.e. a question object (radio, checkbox, ...). | |
535 | +# # ----------------------------------------------------------------------- | |
536 | +# def generate(self, ref): | |
537 | + | |
538 | +# # Shallow copy so that script generated questions will not replace | |
539 | +# # the original generators | |
540 | +# q = self[ref].copy() | |
541 | + | |
542 | +# # If question is of generator type, an external program will be run | |
543 | +# # which will print a valid question in yaml format to stdout. This | |
544 | +# # output is then converted to a dictionary and `q` becomes that dict. | |
545 | +# if q['type'] == 'generator': | |
546 | +# logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | |
547 | +# q.setdefault('arg', '') # optional arguments will be sent to stdin | |
548 | +# script = path.normpath(path.join(q['path'], q['script'])) | |
549 | +# out = run_script(script=script, stdin=q['arg']) | |
550 | +# try: | |
551 | +# q.update(out) | |
552 | +# except: | |
553 | +# q.update({ | |
554 | +# 'type': 'alert', | |
555 | +# 'title': 'Erro interno', | |
556 | +# 'text': 'Ocorreu um erro a gerar esta pergunta.' | |
557 | +# }) | |
558 | +# # The generator was replaced by a question but not yet instantiated | |
559 | + | |
560 | +# # Finally we create an instance of Question() | |
561 | +# try: | |
562 | +# qinstance = self._types[q['type']](q) # instance with correct class | |
563 | +# except KeyError as e: | |
564 | +# logger.error('Unknown question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | |
565 | +# raise e | |
566 | +# except: | |
567 | +# logger.error('Failed to create question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | |
568 | +# else: | |
569 | +# logger.debug('Generated question "{}".'.format(ref)) | |
570 | +# return qinstance | |
497 | 571 | ... | ... |
serve.py
... | ... | @@ -84,11 +84,11 @@ class LoginHandler(BaseHandler): |
84 | 84 | pw = self.get_body_argument('pw') |
85 | 85 | |
86 | 86 | if self.learn.login(uid, pw): |
87 | - logging.info(f'User "{uid}" login ok.') | |
87 | + # logging.info(f'User "{uid}" login ok.') | |
88 | 88 | self.set_secure_cookie("user", str(uid), expires_days=30) |
89 | 89 | self.redirect(self.get_argument("next", "/")) |
90 | 90 | else: |
91 | - logging.info(f'User "{uid}" login failed.') | |
91 | + # logging.info(f'User "{uid}" login failed.') | |
92 | 92 | self.render("login.html", error='Número ou senha incorrectos') |
93 | 93 | |
94 | 94 | # ---------------------------------------------------------------------------- |
... | ... | @@ -132,6 +132,7 @@ class QuestionHandler(BaseHandler): |
132 | 132 | # ref = self.get_body_arguments('question_ref') |
133 | 133 | user = self.current_user |
134 | 134 | answer = self.get_body_arguments('answer') |
135 | + # logger.debug(f'Answer POST from "{user}"') | |
135 | 136 | next_question = self.learn.check_answer(user, answer) |
136 | 137 | |
137 | 138 | if next_question is not None: |
... | ... | @@ -167,9 +168,10 @@ def main(): |
167 | 168 | # --- start application |
168 | 169 | try: |
169 | 170 | webapp = WebApplication() |
170 | - except: | |
171 | + except Exception as e: | |
171 | 172 | logging.critical('Can\'t start application.') |
172 | - sys.exit(1) | |
173 | + # sys.exit(1) | |
174 | + raise e # FIXME | |
173 | 175 | |
174 | 176 | # --- create webserver |
175 | 177 | http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={ | ... | ... |
static/sounds/correct.mp3
No preview for this file type
static/sounds/intro.mp3
No preview for this file type
static/sounds/wrong.mp3
No preview for this file type