# python standard library from contextlib import contextmanager # `with` statement in db sessions import logging from os import path # user installed libraries try: import bcrypt from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import networkx as nx import yaml except ImportError: logger.critical('Python package missing. See README.md for instructions.') sys.exit(1) # this project from models import Student, Answer, Topic, StudentTopic from knowledge import Knowledge from questions import QFactory from tools import load_yaml # setup logger for this module logger = logging.getLogger(__name__) # ============================================================================ # LearnApp - application logic # ============================================================================ class LearnApp(object): def __init__(self): # online students self.online = {} # connect to database and check registered students self.setup_db('students.db') # FIXME # build dependency graph self.build_dependency_graph('demo/config.yaml') # FIXME # ------------------------------------------------------------------------ def login(self, uid, try_pw): with self.db_session() as s: student = s.query(Student).filter(Student.id == uid).one_or_none() if student is None: logger.info(f'User "{uid}" does not exist.') return False # student does not exist or already loggeg in hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) if hashedtry != student.password: logger.info(f'User "{uid}" wrong password.') return False # wrong password student_state = s.query(StudentTopic).filter(StudentTopic.student_id == uid).all() # success self.online[uid] = { 'name': student.name, 'number': student.id, 'state': Knowledge(self.depgraph, student_state), } logger.info(f'User "{uid}" logged in') return True # ------------------------------------------------------------------------ # logout def logout(self, uid): state = self.online[uid]['state'].state # dict {node:level,...} print(state) with self.db_session(autoflush=False) as s: # update existing associations and remove from state dict aa = s.query(StudentTopic).filter_by(student_id=uid).all() for a in aa: print('update ', a) a.level = state.pop(a.topic_id) # update s.add_all(aa) # insert the remaining ones u = s.query(Student).get(uid) for n,l in state.items(): a = StudentTopic(level=l) t = s.query(Topic).get(n) if t is None: # create if topic doesn't exist yet t = Topic(n) a.topic = t u.topics.append(a) s.add(a) del self.online[uid] logger.info(f'User "{uid}" logged out') # ------------------------------------------------------------------------ def get_student_name(self, uid): return self.online[uid].get('name', '') # ------------------------------------------------------------------------ def get_current_public_dir(self, uid): topic = self.online[uid]['state'].topic p = self.depgraph.graph['path'] return path.join(p, topic, 'public') # ------------------------------------------------------------------------ # check answer and if correct returns new question, otherise returns None def check_answer(self, uid, answer): logger.debug(f'check_answer("{uid}", "{answer}")') knowledge = self.online[uid]['state'] current_question = knowledge.check_answer(answer) if current_question is not None: logger.debug('check_answer: saving answer to db ...') with self.db_session() as s: s.add(Answer( ref=current_question['ref'], grade=current_question['grade'], starttime=str(current_question['start_time']), finishtime=str(current_question['finish_time']), student_id=uid)) s.commit() logger.debug('check_answer: saving done') logger.debug('check_answer: will return knowledge.new_question') return knowledge.new_question() # ------------------------------------------------------------------------ # helper to manage db sessions using the `with` statement, for example # with self.db_session() as s: s.query(...) @contextmanager def db_session(self, **kw): session = self.Session(**kw) try: yield session session.commit() except Exception as e: session.rollback() raise e finally: session.close() # ------------------------------------------------------------------------ # Receives a set of topics (strings like "math/algebra"), # and recursively adds dependencies to the dependency graph def build_dependency_graph(self, config_file): logger.debug(f'build_dependency_graph("{config_file}")') # Load configuration file try: with open(config_file, 'r') as f: logger.info(f'Loading configuration file "{config_file}"') config = yaml.load(f) except FileNotFoundError as e: logger.error(f'File not found: "{config_file}"') raise e prefix = config['path'] # FIXME default if does not exist? g = nx.DiGraph(path=prefix) # Build dependency graph deps = config.get('dependencies', {}) for n,dd in deps.items(): g.add_edges_from((n,d) for d in dd) # Builds factories for each node for n in g.nodes_iter(): fullpath = path.join(prefix, n) # if name is directory defaults to "prefix/questions.yaml" if path.isdir(fullpath): filename = path.join(fullpath, "questions.yaml") if path.isfile(filename): logger.info(f'Loading questions from "{filename}"') questions = load_yaml(filename, default=[]) for q in questions: q['path'] = fullpath g.node[n]['factory'] = [QFactory(q) for q in questions] self.depgraph = g # ------------------------------------------------------------------------ # setup and check database def setup_db(self, 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() except Exception as e: logger.critical('Database not usable.') raise e else: logger.info(f'Database has {n} students registered.')