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 | BUGS: | 1 | BUGS: |
2 | 2 | ||
3 | +- servir ficheiros de public temporariamente | ||
3 | - se students.db não existe, rebenta. | 4 | - se students.db não existe, rebenta. |
4 | -- questions hardcoded in LearnApp. | ||
5 | - database hardcoded in LearnApp. | 5 | - database hardcoded in LearnApp. |
6 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() | 6 | - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() |
7 | 7 | ||
8 | TODO: | 8 | TODO: |
9 | 9 | ||
10 | +- mostrar comments quando falha a resposta | ||
10 | - configuração e linha de comando. | 11 | - configuração e linha de comando. |
11 | - como gerar uma sequencia de perguntas? | 12 | - como gerar uma sequencia de perguntas? |
12 | - generators not working: bcrypt (ver blog) | 13 | - generators not working: bcrypt (ver blog) |
13 | 14 | ||
14 | SOLVED: | 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 | - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. | 20 | - implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. |
17 | - logging | 21 | - logging |
18 | - textarea tem codigo para preencher o texto, mas ja não é necessário porque pergunta não é reloaded. | 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,6 +2,7 @@ | ||
2 | # python standard library | 2 | # python standard library |
3 | from contextlib import contextmanager # `with` statement in db sessions | 3 | from contextlib import contextmanager # `with` statement in db sessions |
4 | import logging | 4 | import logging |
5 | +from os import path | ||
5 | 6 | ||
6 | # user installed libraries | 7 | # user installed libraries |
7 | try: | 8 | try: |
@@ -15,8 +16,10 @@ except ImportError: | @@ -15,8 +16,10 @@ except ImportError: | ||
15 | sys.exit(1) | 16 | sys.exit(1) |
16 | 17 | ||
17 | # this project | 18 | # this project |
18 | -from models import Student, Answer | 19 | +from models import Student, Answer, Topic, StudentTopic |
19 | from knowledge import Knowledge | 20 | from knowledge import Knowledge |
21 | +from questions import QFactory | ||
22 | +from tools import load_yaml | ||
20 | 23 | ||
21 | # setup logger for this module | 24 | # setup logger for this module |
22 | logger = logging.getLogger(__name__) | 25 | logger = logging.getLogger(__name__) |
@@ -33,20 +36,7 @@ class LearnApp(object): | @@ -33,20 +36,7 @@ class LearnApp(object): | ||
33 | self.setup_db('students.db') # FIXME | 36 | self.setup_db('students.db') # FIXME |
34 | 37 | ||
35 | # build dependency graph | 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 | def login(self, uid, try_pw): | 42 | def login(self, uid, try_pw): |
@@ -62,17 +52,21 @@ class LearnApp(object): | @@ -62,17 +52,21 @@ class LearnApp(object): | ||
62 | logger.info(f'User "{uid}" wrong password.') | 52 | logger.info(f'User "{uid}" wrong password.') |
63 | return False # wrong password | 53 | return False # wrong password |
64 | 54 | ||
55 | + student_state = s.query(StudentTopic).filter(StudentTopic.student_id == uid).all() | ||
56 | + | ||
65 | # success | 57 | # success |
66 | self.online[uid] = { | 58 | self.online[uid] = { |
67 | 'name': student.name, | 59 | 'name': student.name, |
68 | 'number': student.id, | 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 | return True | 64 | return True |
72 | 65 | ||
73 | # ------------------------------------------------------------------------ | 66 | # ------------------------------------------------------------------------ |
74 | # logout | 67 | # logout |
75 | def logout(self, uid): | 68 | def logout(self, uid): |
69 | + logger.info(f'User "{uid}" logged out') | ||
76 | del self.online[uid] # FIXME save current state | 70 | del self.online[uid] # FIXME save current state |
77 | 71 | ||
78 | # ------------------------------------------------------------------------ | 72 | # ------------------------------------------------------------------------ |
@@ -82,10 +76,12 @@ class LearnApp(object): | @@ -82,10 +76,12 @@ class LearnApp(object): | ||
82 | # ------------------------------------------------------------------------ | 76 | # ------------------------------------------------------------------------ |
83 | # check answer and if correct returns new question, otherise returns None | 77 | # check answer and if correct returns new question, otherise returns None |
84 | def check_answer(self, uid, answer): | 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 | current_question = knowledge.check_answer(answer) | 81 | current_question = knowledge.check_answer(answer) |
87 | 82 | ||
88 | if current_question is not None: | 83 | if current_question is not None: |
84 | + logger.debug('check_answer: saving answer to db ...') | ||
89 | with self.db_session() as s: | 85 | with self.db_session() as s: |
90 | s.add(Answer( | 86 | s.add(Answer( |
91 | ref=current_question['ref'], | 87 | ref=current_question['ref'], |
@@ -94,7 +90,9 @@ class LearnApp(object): | @@ -94,7 +90,9 @@ class LearnApp(object): | ||
94 | finishtime=str(current_question['finish_time']), | 90 | finishtime=str(current_question['finish_time']), |
95 | student_id=uid)) | 91 | student_id=uid)) |
96 | s.commit() | 92 | s.commit() |
93 | + logger.debug('check_answer: saving done') | ||
97 | 94 | ||
95 | + logger.debug('check_answer: will return knowledge.new_question') | ||
98 | return knowledge.new_question() | 96 | return knowledge.new_question() |
99 | 97 | ||
100 | # ------------------------------------------------------------------------ | 98 | # ------------------------------------------------------------------------ |
@@ -106,42 +104,63 @@ class LearnApp(object): | @@ -106,42 +104,63 @@ class LearnApp(object): | ||
106 | try: | 104 | try: |
107 | yield session | 105 | yield session |
108 | session.commit() | 106 | session.commit() |
109 | - except: | 107 | + except Exception as e: |
110 | session.rollback() | 108 | session.rollback() |
111 | - raise | 109 | + raise e |
112 | finally: | 110 | finally: |
113 | session.close() | 111 | session.close() |
114 | 112 | ||
115 | # ------------------------------------------------------------------------ | 113 | # ------------------------------------------------------------------------ |
116 | # Receives a set of topics (strings like "math/algebra"), | 114 | # Receives a set of topics (strings like "math/algebra"), |
117 | # and recursively adds dependencies to the dependency graph | 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 | self.depgraph = g | 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,11 +5,11 @@ formatters: | ||
5 | void: | 5 | void: |
6 | format: '' | 6 | format: '' |
7 | standard: | 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 | handlers: | 10 | handlers: |
11 | default: | 11 | default: |
12 | - level: 'INFO' | 12 | + level: 'DEBUG' |
13 | class: 'logging.StreamHandler' | 13 | class: 'logging.StreamHandler' |
14 | formatter: 'standard' | 14 | formatter: 'standard' |
15 | stream: 'ext://sys.stdout' | 15 | stream: 'ext://sys.stdout' |
@@ -21,12 +21,12 @@ loggers: | @@ -21,12 +21,12 @@ loggers: | ||
21 | 21 | ||
22 | 'app': | 22 | 'app': |
23 | handlers: ['default'] | 23 | handlers: ['default'] |
24 | - level: 'INFO' | 24 | + level: 'DEBUG' |
25 | propagate: False | 25 | propagate: False |
26 | 26 | ||
27 | 'questions': | 27 | 'questions': |
28 | handlers: ['default'] | 28 | handlers: ['default'] |
29 | - level: 'INFO' | 29 | + level: 'DEBUG' |
30 | propagate: False | 30 | propagate: False |
31 | 31 | ||
32 | 'tools': | 32 | 'tools': |
@@ -34,3 +34,13 @@ loggers: | @@ -34,3 +34,13 @@ loggers: | ||
34 | level: 'INFO' | 34 | level: 'INFO' |
35 | propagate: False | 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 | \ No newline at end of file | 47 | \ No newline at end of file |
knowledge.py
@@ -2,36 +2,61 @@ | @@ -2,36 +2,61 @@ | ||
2 | # python standard library | 2 | # python standard library |
3 | import random | 3 | import random |
4 | from datetime import datetime | 4 | from datetime import datetime |
5 | +import logging | ||
6 | + | ||
7 | +# libraries | ||
8 | +import networkx as nx | ||
5 | 9 | ||
6 | # this project | 10 | # this project |
7 | import questions | 11 | import questions |
8 | 12 | ||
13 | +# setup logger for this module | ||
14 | +logger = logging.getLogger(__name__) | ||
9 | 15 | ||
16 | +# ---------------------------------------------------------------------------- | ||
10 | # contains the kowledge state of each student. | 17 | # contains the kowledge state of each student. |
11 | class Knowledge(object): | 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 | self.current_question = None | 22 | self.current_question = None |
16 | 23 | ||
24 | + # self.seq = nx.topological_sort(self.depgraph) | ||
25 | + | ||
17 | def get_current_question(self): | 26 | def get_current_question(self): |
18 | return self.current_question | 27 | return self.current_question |
19 | 28 | ||
29 | + def get_knowledge_state(self): | ||
30 | + return self.state | ||
31 | + | ||
20 | # --- generates a new question given the current state | 32 | # --- generates a new question given the current state |
21 | def new_question(self): | 33 | def new_question(self): |
22 | - # FIXME | 34 | + logger.debug('new_question()') |
23 | if self.current_question is None or self.current_question.get('grade', 0.0) > 0.9999: | 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 | self.current_question['start_time'] = datetime.now() | 49 | self.current_question['start_time'] = datetime.now() |
27 | - | ||
28 | return self.current_question | 50 | return self.current_question |
29 | 51 | ||
30 | # --- checks answer and updates knowledge state | 52 | # --- checks answer and updates knowledge state |
31 | # returns current question with correction, time and comments updated | 53 | # returns current question with correction, time and comments updated |
32 | def check_answer(self, answer): | 54 | def check_answer(self, answer): |
55 | + logger.debug(f'check_answer("{answer}")') | ||
33 | question = self.current_question | 56 | question = self.current_question |
34 | if question is not None: | 57 | if question is not None: |
35 | question['finish_time'] = datetime.now() | 58 | question['finish_time'] = datetime.now() |
36 | question.correct(answer) | 59 | question.correct(answer) |
60 | + | ||
61 | + # logger.debug(f'check_answer returning') | ||
37 | return question | 62 | return question |
models.py
@@ -10,6 +10,17 @@ from sqlalchemy.orm import relationship | @@ -10,6 +10,17 @@ from sqlalchemy.orm import relationship | ||
10 | Base = declarative_base() | 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 | # Registered students | 24 | # Registered students |
14 | # --------------------------------------------------------------------------- | 25 | # --------------------------------------------------------------------------- |
15 | class Student(Base): | 26 | class Student(Base): |
@@ -20,6 +31,7 @@ class Student(Base): | @@ -20,6 +31,7 @@ class Student(Base): | ||
20 | 31 | ||
21 | # --- | 32 | # --- |
22 | answers = relationship('Answer', back_populates='student') | 33 | answers = relationship('Answer', back_populates='student') |
34 | + topics = relationship('StudentTopic', back_populates='student') | ||
23 | 35 | ||
24 | def __repr__(self): | 36 | def __repr__(self): |
25 | return f'''Student: | 37 | return f'''Student: |
@@ -52,3 +64,15 @@ class Answer(Base): | @@ -52,3 +64,15 @@ class Answer(Base): | ||
52 | finishtime: "{self.finishtime}" | 64 | finishtime: "{self.finishtime}" |
53 | student_id: "{self.student_id}"''' | 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,8 +49,6 @@ else: | ||
49 | from tools import load_yaml, run_script | 49 | from tools import load_yaml, run_script |
50 | 50 | ||
51 | 51 | ||
52 | - | ||
53 | - | ||
54 | # =========================================================================== | 52 | # =========================================================================== |
55 | # Questions derived from Question are already instantiated and ready to be | 53 | # Questions derived from Question are already instantiated and ready to be |
56 | # presented to students. | 54 | # presented to students. |
@@ -320,6 +318,7 @@ class QuestionTextArea(Question): | @@ -320,6 +318,7 @@ class QuestionTextArea(Question): | ||
320 | 318 | ||
321 | #------------------------------------------------------------------------ | 319 | #------------------------------------------------------------------------ |
322 | def __init__(self, q): | 320 | def __init__(self, q): |
321 | + logger.debug('QuestionTextArea.__init__()') | ||
323 | super().__init__(q) | 322 | super().__init__(q) |
324 | 323 | ||
325 | self.set_defaults({ | 324 | self.set_defaults({ |
@@ -381,14 +380,14 @@ class QuestionInformation(Question): | @@ -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 | # Depending on the type of question, a different question class will be | 388 | # Depending on the type of question, a different question class will be |
390 | # instantiated. All these classes derive from the base class `Question`. | 389 | # instantiated. All these classes derive from the base class `Question`. |
391 | - types = { | 390 | + _types = { |
392 | 'radio' : QuestionRadio, | 391 | 'radio' : QuestionRadio, |
393 | 'checkbox' : QuestionCheckbox, | 392 | 'checkbox' : QuestionCheckbox, |
394 | 'text' : QuestionText, | 393 | 'text' : QuestionText, |
@@ -399,71 +398,20 @@ class QuestionFactory(dict): | @@ -399,71 +398,20 @@ class QuestionFactory(dict): | ||
399 | 'information': QuestionInformation, | 398 | 'information': QuestionInformation, |
400 | 'warning' : QuestionInformation, | 399 | 'warning' : QuestionInformation, |
401 | 'alert' : QuestionInformation, | 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 | # Given a ref returns an instance of a descendent of Question(), | 407 | # Given a ref returns an instance of a descendent of Question(), |
460 | # i.e. a question object (radio, checkbox, ...). | 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 | # Shallow copy so that script generated questions will not replace | 412 | # Shallow copy so that script generated questions will not replace |
465 | # the original generators | 413 | # the original generators |
466 | - q = self[ref].copy() | 414 | + q = self.question.copy() |
467 | 415 | ||
468 | # If question is of generator type, an external program will be run | 416 | # If question is of generator type, an external program will be run |
469 | # which will print a valid question in yaml format to stdout. This | 417 | # which will print a valid question in yaml format to stdout. This |
@@ -471,27 +419,153 @@ class QuestionFactory(dict): | @@ -471,27 +419,153 @@ class QuestionFactory(dict): | ||
471 | if q['type'] == 'generator': | 419 | if q['type'] == 'generator': |
472 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | 420 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) |
473 | q.setdefault('arg', '') # optional arguments will be sent to stdin | 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 | out = run_script(script=script, stdin=q['arg']) | 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 | # Finally we create an instance of Question() | 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,11 +84,11 @@ class LoginHandler(BaseHandler): | ||
84 | pw = self.get_body_argument('pw') | 84 | pw = self.get_body_argument('pw') |
85 | 85 | ||
86 | if self.learn.login(uid, pw): | 86 | if self.learn.login(uid, pw): |
87 | - logging.info(f'User "{uid}" login ok.') | 87 | + # logging.info(f'User "{uid}" login ok.') |
88 | self.set_secure_cookie("user", str(uid), expires_days=30) | 88 | self.set_secure_cookie("user", str(uid), expires_days=30) |
89 | self.redirect(self.get_argument("next", "/")) | 89 | self.redirect(self.get_argument("next", "/")) |
90 | else: | 90 | else: |
91 | - logging.info(f'User "{uid}" login failed.') | 91 | + # logging.info(f'User "{uid}" login failed.') |
92 | self.render("login.html", error='Número ou senha incorrectos') | 92 | self.render("login.html", error='Número ou senha incorrectos') |
93 | 93 | ||
94 | # ---------------------------------------------------------------------------- | 94 | # ---------------------------------------------------------------------------- |
@@ -132,6 +132,7 @@ class QuestionHandler(BaseHandler): | @@ -132,6 +132,7 @@ class QuestionHandler(BaseHandler): | ||
132 | # ref = self.get_body_arguments('question_ref') | 132 | # ref = self.get_body_arguments('question_ref') |
133 | user = self.current_user | 133 | user = self.current_user |
134 | answer = self.get_body_arguments('answer') | 134 | answer = self.get_body_arguments('answer') |
135 | + # logger.debug(f'Answer POST from "{user}"') | ||
135 | next_question = self.learn.check_answer(user, answer) | 136 | next_question = self.learn.check_answer(user, answer) |
136 | 137 | ||
137 | if next_question is not None: | 138 | if next_question is not None: |
@@ -167,9 +168,10 @@ def main(): | @@ -167,9 +168,10 @@ def main(): | ||
167 | # --- start application | 168 | # --- start application |
168 | try: | 169 | try: |
169 | webapp = WebApplication() | 170 | webapp = WebApplication() |
170 | - except: | 171 | + except Exception as e: |
171 | logging.critical('Can\'t start application.') | 172 | logging.critical('Can\'t start application.') |
172 | - sys.exit(1) | 173 | + # sys.exit(1) |
174 | + raise e # FIXME | ||
173 | 175 | ||
174 | # --- create webserver | 176 | # --- create webserver |
175 | http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={ | 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