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