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