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