Commit fcdedf5af3c8f77e05d228b96446467568e4d8d9

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

- 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
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.
@@ -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
@@ -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
@@ -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 +
@@ -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
@@ -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