diff --git a/BUGS.md b/BUGS.md index 578c7a5..82e3df0 100644 --- a/BUGS.md +++ b/BUGS.md @@ -3,6 +3,7 @@ BUGS: - servidor http com redirect para https. - servir imagens/ficheiros. +- codemirror em textarea. - topicos virtuais nao deveriam aparecer. na construção da árvore os sucessores seriam ligados directamente aos predecessores. @@ -29,6 +30,7 @@ TODO: FIXED: +- database: answers não tem referencia para o topico, so para question_ref - melhorar markdown das tabelas. - gravar evolucao na bd no final de cada topico. - submeter questoes radio, da erro se nao escolher nenhuma opção. diff --git a/initdb.py b/initdb.py index 26b105c..9bffd43 100755 --- a/initdb.py +++ b/initdb.py @@ -19,11 +19,29 @@ def fix(name): # =========================================================================== # Parse command line options -argparser = argparse.ArgumentParser(description='Create new database from a CSV file (SIIUE format)') -argparser.add_argument('--db', default='students.db', type=str, help='database filename') -argparser.add_argument('--demo', action='store_true', help='initialize database with a few fake students') -argparser.add_argument('--pw', default='', type=str, help='default password') -argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename') +argparser = argparse.ArgumentParser( + description='Create new database from a CSV file (SIIUE format)') + +argparser.add_argument('--db', + default='students.db', + type=str, + help='database filename') + +argparser.add_argument('--demo', + action='store_true', + help='initialize database with a few fake students') + +argparser.add_argument('--pw', + default='', + type=str, + help='default password') + +argparser.add_argument('csvfile', + nargs='?', + type=str, + default='', + help='CSV filename') + args = argparser.parse_args() # =======================================================x==================== @@ -82,7 +100,8 @@ try: except Exception as e: print(f'Error: Database "{args.db}" already exists?') session.rollback() - exit(1) + raise e + # exit(1) else: # --- end session --- diff --git a/knowledge.py b/knowledge.py index 0da2651..a78fe93 100644 --- a/knowledge.py +++ b/knowledge.py @@ -55,7 +55,7 @@ class StudentKnowledge(object): 'level': 0.0, # then unlock 'date': datetime.now() } - logger.debug(f'unlocked {topic}') + logger.debug(f'Unlocked "{topic}".') # ------------------------------------------------------------------------ diff --git a/learnapp.py b/learnapp.py index 0c3413e..7631481 100644 --- a/learnapp.py +++ b/learnapp.py @@ -10,7 +10,6 @@ import bcrypt from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import networkx as nx -import yaml # this project from models import Student, Answer, Topic, StudentTopic @@ -25,22 +24,25 @@ logger = logging.getLogger(__name__) class LearnAppException(Exception): pass + # ============================================================================ # LearnApp - application logic # ============================================================================ class LearnApp(object): - def __init__(self, conffile): + def __init__(self, config_file): # state of online students self.online = {} - # dependency graph shared by all students - self.deps = build_dependency_graph(conffile) + config = load_yaml(config_file) # connect to database and checks for registered students - self.db_setup(self.deps.graph['database']) + self.db_setup(config['database']) + + # dependency graph shared by all students + self.deps = build_dependency_graph(config) # add topics from dependency graph to the database, if missing - self.db_add_topics() + self.db_add_missing_topics(self.deps.nodes()) # ------------------------------------------------------------------------ # login @@ -79,29 +81,6 @@ class LearnApp(object): # logout # ------------------------------------------------------------------------ def logout(self, uid): - # state = self.online[uid]['state'].state # dict {node:level,...} - # # save topics state to database - # with self.db_session(autoflush=False) as s: - - # # update existing associations and remove from state dict - # for a in s.query(StudentTopic).filter_by(student_id=uid): - # if a.topic_id in state: - # d = state.pop(a.topic_id) - # a.level = d['level'] #state.pop(a.topic_id) # update - # a.date = str(d['date']) - # s.add(a) - - # # insert the remaining ones - # u = s.query(Student).get(uid) - # for n,d in state.items(): - # a = StudentTopic(level=d['level'], date=str(d['date'])) - # t = s.query(Topic).get(n) - # if t is None: # create if topic doesn't exist yet - # t = Topic(id=n) - # a.topic = t - # u.topics.append(a) - # s.add(a) - del self.online[uid] logger.info(f'User "{uid}" logged out') @@ -126,23 +105,14 @@ class LearnApp(object): knowledge = self.online[uid]['state'] grade = knowledge.check_answer(answer) - # if finished topic, save in database if knowledge.get_current_question() is None: + # finished topic, save into database finished_topic = knowledge.get_current_topic() level = knowledge.get_topic_level(finished_topic) date = str(knowledge.get_topic_date(finished_topic)) + finished_questions = knowledge.get_finished_questions() with self.db_session(autoflush=False) as s: - # save questions from finished_questions list - s.add_all([ - Answer( - ref=q['ref'], - grade=q['grade'], - starttime=str(q['start_time']), - finishtime=str(q['finish_time']), - student_id=uid) - for q in knowledge.get_finished_questions()]) - # save topic a = s.query(StudentTopic).filter_by(student_id=uid, topic_id=finished_topic).one_or_none() if a is None: @@ -152,12 +122,23 @@ class LearnApp(object): t = s.query(Topic).get(finished_topic) a.topic = t u.topics.append(a) - s.add(a) else: # update studenttopic in database a.level = level a.date = date - s.add(a) + + s.add(a) + + # save answered questions from finished_questions list + s.add_all([ + Answer( + ref=q['ref'], + grade=q['grade'], + starttime=str(q['start_time']), + finishtime=str(q['finish_time']), + student_id=uid, + topic_id=finished_topic) + for q in finished_questions]) return grade @@ -170,26 +151,33 @@ class LearnApp(object): # ------------------------------------------------------------------------ # Fill db table 'Topic' with topics from the graph if not already there. # ------------------------------------------------------------------------ - def db_add_topics(self): + def db_add_missing_topics(self, nn): with self.db_session() as s: tt = [t[0] for t in s.query(Topic.id)] # db list of topics - nn = self.deps.nodes() # topics in the graph - s.add_all([Topic(id=n) for n in nn if n not in tt]) + missing_topics = [Topic(id=n) for n in nn if n not in tt] + if missing_topics: + s.add_all(missing_topics) + logger.info(f'Added {len(missing_topics)} new topics to the database.') # ------------------------------------------------------------------------ # setup and check database # ------------------------------------------------------------------------ def db_setup(self, db): + logger.info(f'Checking database "{db}":') engine = create_engine(f'sqlite:///{db}', echo=False) self.Session = sessionmaker(bind=engine) try: with self.db_session() as s: n = s.query(Student).count() + m = s.query(Topic).count() + q = s.query(Answer).count() except Exception as e: logger.critical(f'Database "{db}" not usable.') sys.exit(1) else: - logger.info(f'Database "{db}" has {n} students.') + logger.info(f'{n:4} students.') + logger.info(f'{m:4} topics.') + logger.info(f'{q:4} questions answered.') # ------------------------------------------------------------------------ # helper to manage db sessions using the `with` statement, for example @@ -255,7 +243,8 @@ class LearnApp(object): # ============================================================================ -# Given configuration file, loads YAML on that file and builds a digraph. +# Builds a digraph. +# # First, topics such as `computer/mips/exceptions` are added as nodes # together with dependencies. Then, questions are loaded to a factory. # @@ -268,19 +257,8 @@ class LearnApp(object): # g.node['my/topic']['questions'] list of question refs defined in YAML # g.node['my/topic']['factory'] dict with question factories # ---------------------------------------------------------------------------- -def build_dependency_graph(config_file): - # Load configuration file to a dict - try: - with open(config_file, 'r') as f: - config = yaml.load(f) - except FileNotFoundError: - logger.critical(f'File not found: "{config_file}"') - raise LearnAppException - except yaml.scanner.ScannerError as err: - logger.critical(f'Parsing YAML file "{config_file}": {err}') - raise LearnAppException - else: - logger.info(f'Configuration file "{config_file}"') +def build_dependency_graph(config={}): + logger.info('Building topic dependency graph.') # create graph prefix = config.get('path', '.') diff --git a/models.py b/models.py index 99fe742..5dcd3a0 100644 --- a/models.py +++ b/models.py @@ -51,9 +51,11 @@ class Answer(Base): starttime = Column(String) finishtime = Column(String) student_id = Column(String, ForeignKey('students.id')) + topic_id = Column(String, ForeignKey('topics.id')) # --- student = relationship('Student', back_populates='answers') + topic = relationship('Topic', back_populates='answers') def __repr__(self): return '''Question: @@ -73,6 +75,7 @@ class Topic(Base): # --- students = relationship('StudentTopic', back_populates='topic') + answers = relationship('Answer', back_populates='topic') # def __init__(self, id): # self.id = id diff --git a/serve.py b/serve.py index a45ad27..bcd7470 100755 --- a/serve.py +++ b/serve.py @@ -197,7 +197,7 @@ class QuestionHandler(BaseHandler): return { 'method': 'finished_topic', 'params': { # FIXME no html here please! - 'question': f'trophy' + 'question': f'trophy' } } diff --git a/static/trophy.png b/static/trophy.png deleted file mode 100644 index 14df155..0000000 Binary files a/static/trophy.png and /dev/null differ diff --git a/templates/maintopics.html b/templates/maintopics.html index db0a8fd..1be0937 100644 --- a/templates/maintopics.html +++ b/templates/maintopics.html @@ -1,6 +1,6 @@ {% autoescape %} - + iLearn @@ -11,7 +11,7 @@ - + @@ -27,7 +27,7 @@ -