Commit d21baa3abab6238da38e00c2795675a019c836f7
1 parent
834adb07
Exists in
master
and in
1 other branch
- cleanup app.py a little.
Showing
5 changed files
with
61 additions
and
63 deletions
Show diff stats
app.py
| @@ -25,6 +25,10 @@ from tools import load_yaml | @@ -25,6 +25,10 @@ from tools import load_yaml | ||
| 25 | # setup logger for this module | 25 | # setup logger for this module |
| 26 | logger = logging.getLogger(__name__) | 26 | logger = logging.getLogger(__name__) |
| 27 | 27 | ||
| 28 | + | ||
| 29 | +class LearnAppException(Exception): | ||
| 30 | + pass | ||
| 31 | + | ||
| 28 | # ============================================================================ | 32 | # ============================================================================ |
| 29 | # LearnApp - application logic | 33 | # LearnApp - application logic |
| 30 | # ============================================================================ | 34 | # ============================================================================ |
| @@ -37,19 +41,20 @@ class LearnApp(object): | @@ -37,19 +41,20 @@ class LearnApp(object): | ||
| 37 | self.build_dependency_graph(conffile) | 41 | self.build_dependency_graph(conffile) |
| 38 | 42 | ||
| 39 | # connect to database and check registered students | 43 | # connect to database and check registered students |
| 40 | - self.db_setup(self.depgraph.graph['database']) # FIXME | 44 | + self.db_setup(self.depgraph.graph['database']) |
| 41 | 45 | ||
| 42 | # add topics from depgraph to the database | 46 | # add topics from depgraph to the database |
| 43 | self.db_add_topics() | 47 | self.db_add_topics() |
| 44 | 48 | ||
| 45 | # ------------------------------------------------------------------------ | 49 | # ------------------------------------------------------------------------ |
| 50 | + # login | ||
| 51 | + # ------------------------------------------------------------------------ | ||
| 46 | def login(self, uid, try_pw): | 52 | def login(self, uid, try_pw): |
| 47 | with self.db_session() as s: | 53 | with self.db_session() as s: |
| 48 | - student = s.query(Student).filter(Student.id == uid).one_or_none() | ||
| 49 | - | 54 | + student = s.query(Student).filter(Student.id == uid).one_or_none() |
| 50 | if student is None: | 55 | if student is None: |
| 51 | logger.info(f'User "{uid}" does not exist.') | 56 | logger.info(f'User "{uid}" does not exist.') |
| 52 | - return False # student does not exist or already loggeg in | 57 | + return False # student does not exist |
| 53 | 58 | ||
| 54 | hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) | 59 | hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) |
| 55 | if hashedtry != student.password: | 60 | if hashedtry != student.password: |
| @@ -76,6 +81,7 @@ class LearnApp(object): | @@ -76,6 +81,7 @@ class LearnApp(object): | ||
| 76 | 81 | ||
| 77 | # ------------------------------------------------------------------------ | 82 | # ------------------------------------------------------------------------ |
| 78 | # logout | 83 | # logout |
| 84 | + # ------------------------------------------------------------------------ | ||
| 79 | def logout(self, uid): | 85 | def logout(self, uid): |
| 80 | state = self.online[uid]['state'].state # dict {node:level,...} | 86 | state = self.online[uid]['state'].state # dict {node:level,...} |
| 81 | 87 | ||
| @@ -105,6 +111,8 @@ class LearnApp(object): | @@ -105,6 +111,8 @@ class LearnApp(object): | ||
| 105 | logger.info(f'User "{uid}" logged out') | 111 | logger.info(f'User "{uid}" logged out') |
| 106 | 112 | ||
| 107 | # ------------------------------------------------------------------------ | 113 | # ------------------------------------------------------------------------ |
| 114 | + # change_password | ||
| 115 | + # ------------------------------------------------------------------------ | ||
| 108 | def change_password(self, uid, pw): | 116 | def change_password(self, uid, pw): |
| 109 | if not pw: | 117 | if not pw: |
| 110 | return False | 118 | return False |
| @@ -165,21 +173,26 @@ class LearnApp(object): | @@ -165,21 +173,26 @@ class LearnApp(object): | ||
| 165 | return knowledge.new_question() | 173 | return knowledge.new_question() |
| 166 | 174 | ||
| 167 | # ------------------------------------------------------------------------ | 175 | # ------------------------------------------------------------------------ |
| 168 | - # Receives a set of topics (strings like "math/algebra"), | ||
| 169 | - # and recursively adds dependencies to the dependency graph | 176 | + # Given configuration file, loads YAML on that file and builds the graph. |
| 177 | + # First, topics such as `computer/mips/exceptions` are added as nodes | ||
| 178 | + # together with dependencies. Then, questions are loaded to a factory. | ||
| 170 | # ------------------------------------------------------------------------ | 179 | # ------------------------------------------------------------------------ |
| 171 | def build_dependency_graph(self, config_file): | 180 | def build_dependency_graph(self, config_file): |
| 172 | 181 | ||
| 173 | - # Load configuration file | 182 | + # Load configuration file to a dict |
| 174 | try: | 183 | try: |
| 175 | with open(config_file, 'r') as f: | 184 | with open(config_file, 'r') as f: |
| 176 | - logger.info(f'Configuration file "{config_file}"') | ||
| 177 | config = yaml.load(f) | 185 | config = yaml.load(f) |
| 178 | - except FileNotFoundError as e: | ||
| 179 | - logger.error(f'File not found: "{config_file}"') | ||
| 180 | - sys.exit(1) | ||
| 181 | - # config file parsed | 186 | + except FileNotFoundError: |
| 187 | + logger.critical(f'File not found: "{config_file}"') | ||
| 188 | + raise LearnAppException | ||
| 189 | + except yaml.scanner.ScannerError as err: | ||
| 190 | + logger.critical(f'Parsing YAML file "{config_file}": {err}') | ||
| 191 | + raise LearnAppException | ||
| 192 | + else: | ||
| 193 | + logger.info(f'Configuration file "{config_file}"') | ||
| 182 | 194 | ||
| 195 | + # create graph | ||
| 183 | prefix = config.get('path', '.') | 196 | prefix = config.get('path', '.') |
| 184 | title = config.get('title', '') | 197 | title = config.get('title', '') |
| 185 | database = config.get('database', 'students.db') | 198 | database = config.get('database', 'students.db') |
| @@ -194,49 +207,35 @@ class LearnApp(object): | @@ -194,49 +207,35 @@ class LearnApp(object): | ||
| 194 | g.node[ref]['questions'] = attr.get('questions', []) | 207 | g.node[ref]['questions'] = attr.get('questions', []) |
| 195 | g.add_edges_from((d,ref) for d in attr.get('deps', [])) | 208 | g.add_edges_from((d,ref) for d in attr.get('deps', [])) |
| 196 | 209 | ||
| 197 | - elif isinstance(attr, list): | ||
| 198 | - # if prop is a list, we assume it's just a list of dependencies | ||
| 199 | - g.add_edges_from((d,ref) for d in attr) | ||
| 200 | - | ||
| 201 | - elif isinstance(attr, str): # FIXME is this really useful?? | ||
| 202 | - g.node[ref]['name'] = attr | ||
| 203 | - | ||
| 204 | # iterate over topics and create question factories | 210 | # iterate over topics and create question factories |
| 205 | logger.info('Loading:') | 211 | logger.info('Loading:') |
| 206 | for ref in g.nodes_iter(): | 212 | for ref in g.nodes_iter(): |
| 207 | - g.node[ref].setdefault('name', ref) | ||
| 208 | - prefix_ref = path.join(prefix, ref) | ||
| 209 | - fullpath = path.expanduser(prefix_ref) | ||
| 210 | - if path.isdir(fullpath): | ||
| 211 | - filename = path.join(fullpath, "questions.yaml") | ||
| 212 | - else: | ||
| 213 | - logger.error(f'Cant find directory "{prefix_ref}", ignored...') | ||
| 214 | - continue | ||
| 215 | - | ||
| 216 | - if path.isfile(filename): | ||
| 217 | - loaded_questions = load_yaml(filename, default=[]) | ||
| 218 | - | ||
| 219 | - # if 'questions' is not provided in configuration, load all | ||
| 220 | - if not g.node[ref]['questions']: | ||
| 221 | - g.node[ref]['questions'] = [q['ref'] for q in loaded_questions] | ||
| 222 | - | ||
| 223 | - | ||
| 224 | - # FIXME nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. | ||
| 225 | - g.node[ref]['factory'] = [] | ||
| 226 | - for q in loaded_questions: | ||
| 227 | - if q['ref'] in g.node[ref]['questions']: | ||
| 228 | - q['path'] = fullpath | ||
| 229 | - g.node[ref]['factory'].append(QFactory(q)) | ||
| 230 | - logger.info(f' {len(g.node[ref]["factory"])} questions from "{ref}"') | ||
| 231 | - | ||
| 232 | - else: | ||
| 233 | - g.node[ref]['factory'] = [] | ||
| 234 | - logger.error(f'Cant load "{filename}"') | 213 | + fullpath = path.expanduser(path.join(prefix, ref)) |
| 214 | + filename = path.join(fullpath, 'questions.yaml') | ||
| 215 | + | ||
| 216 | + loaded_questions = load_yaml(filename, default=[]) | ||
| 217 | + | ||
| 218 | + # make dict from list of questions for easier selection | ||
| 219 | + qdict = {q['ref']: q for q in loaded_questions} | ||
| 220 | + | ||
| 221 | + # 'questions' not provided in configuration means load all | ||
| 222 | + if not g.node[ref]['questions']: | ||
| 223 | + g.node[ref]['questions'] = qdict.keys() #[q['ref'] for q in loaded_questions] | ||
| 224 | + | ||
| 225 | + g.node[ref]['factory'] = [] | ||
| 226 | + for qref in g.node[ref]['questions']: | ||
| 227 | + q = qdict[qref] | ||
| 228 | + q['path'] = fullpath | ||
| 229 | + g.node[ref]['factory'].append(QFactory(q)) | ||
| 230 | + | ||
| 231 | + logger.info(f' {len(g.node[ref]["factory"])} questions from "{ref}"') | ||
| 235 | 232 | ||
| 236 | self.depgraph = g | 233 | self.depgraph = g |
| 237 | return g | 234 | return g |
| 238 | 235 | ||
| 239 | # ------------------------------------------------------------------------ | 236 | # ------------------------------------------------------------------------ |
| 237 | + # Fill db table 'Topic' with topics from the graph if not already there. | ||
| 238 | + # ------------------------------------------------------------------------ | ||
| 240 | def db_add_topics(self): | 239 | def db_add_topics(self): |
| 241 | with self.db_session() as s: | 240 | with self.db_session() as s: |
| 242 | tt = [t[0] for t in s.query(Topic.id)] # db list of topics | 241 | tt = [t[0] for t in s.query(Topic.id)] # db list of topics |
| @@ -245,8 +244,8 @@ class LearnApp(object): | @@ -245,8 +244,8 @@ class LearnApp(object): | ||
| 245 | 244 | ||
| 246 | # ------------------------------------------------------------------------ | 245 | # ------------------------------------------------------------------------ |
| 247 | # setup and check database | 246 | # setup and check database |
| 247 | + # ------------------------------------------------------------------------ | ||
| 248 | def db_setup(self, db): | 248 | def db_setup(self, db): |
| 249 | - logger.debug(f'LearnApp.db_setup("{db}")') | ||
| 250 | engine = create_engine(f'sqlite:///{db}', echo=False) | 249 | engine = create_engine(f'sqlite:///{db}', echo=False) |
| 251 | self.Session = sessionmaker(bind=engine) | 250 | self.Session = sessionmaker(bind=engine) |
| 252 | try: | 251 | try: |
| @@ -256,11 +255,12 @@ class LearnApp(object): | @@ -256,11 +255,12 @@ class LearnApp(object): | ||
| 256 | logger.critical(f'Database "{db}" not usable.') | 255 | logger.critical(f'Database "{db}" not usable.') |
| 257 | sys.exit(1) | 256 | sys.exit(1) |
| 258 | else: | 257 | else: |
| 259 | - logger.info(f'Database has {n} students registered.') | 258 | + logger.info(f'Database "{db}" has {n} students.') |
| 260 | 259 | ||
| 261 | # ------------------------------------------------------------------------ | 260 | # ------------------------------------------------------------------------ |
| 262 | # helper to manage db sessions using the `with` statement, for example | 261 | # helper to manage db sessions using the `with` statement, for example |
| 263 | # with self.db_session() as s: s.query(...) | 262 | # with self.db_session() as s: s.query(...) |
| 263 | + # ------------------------------------------------------------------------ | ||
| 264 | @contextmanager | 264 | @contextmanager |
| 265 | def db_session(self, **kw): | 265 | def db_session(self, **kw): |
| 266 | session = self.Session(**kw) | 266 | session = self.Session(**kw) |
initdb.py
| @@ -31,6 +31,9 @@ engine = create_engine(f'sqlite:///{args.db}', echo=False) | @@ -31,6 +31,9 @@ engine = create_engine(f'sqlite:///{args.db}', echo=False) | ||
| 31 | Base.metadata.create_all(engine) # Criate schema if needed | 31 | Base.metadata.create_all(engine) # Criate schema if needed |
| 32 | Session = sessionmaker(bind=engine) | 32 | Session = sessionmaker(bind=engine) |
| 33 | 33 | ||
| 34 | +# add administrator | ||
| 35 | +students = {'0': 'Professor'} | ||
| 36 | + | ||
| 34 | if args.csvfile: | 37 | if args.csvfile: |
| 35 | # add students from csv file if available | 38 | # add students from csv file if available |
| 36 | try: | 39 | try: |
| @@ -38,12 +41,11 @@ if args.csvfile: | @@ -38,12 +41,11 @@ if args.csvfile: | ||
| 38 | except EnvironmentError: | 41 | except EnvironmentError: |
| 39 | print(f'Error: CSV file "{args.csvfile}" not found.') | 42 | print(f'Error: CSV file "{args.csvfile}" not found.') |
| 40 | exit(1) | 43 | exit(1) |
| 41 | - | ||
| 42 | - students = {s['N.º']: fix(s['Nome']) for s in csvreader} | 44 | + students.update({s['N.º']: fix(s['Nome']) for s in csvreader}) |
| 43 | 45 | ||
| 44 | elif args.demo: | 46 | elif args.demo: |
| 45 | # add a few fake students | 47 | # add a few fake students |
| 46 | - students = { | 48 | + students.update({ |
| 47 | '1915': 'Alan Turing', | 49 | '1915': 'Alan Turing', |
| 48 | '1938': 'Donald Knuth', | 50 | '1938': 'Donald Knuth', |
| 49 | '1815': 'Ada Lovelace', | 51 | '1815': 'Ada Lovelace', |
| @@ -51,13 +53,10 @@ elif args.demo: | @@ -51,13 +53,10 @@ elif args.demo: | ||
| 51 | '1955': 'Tim Burners-Lee', | 53 | '1955': 'Tim Burners-Lee', |
| 52 | '1916': 'Claude Shannon', | 54 | '1916': 'Claude Shannon', |
| 53 | '1903': 'John von Neumann', | 55 | '1903': 'John von Neumann', |
| 54 | - } | ||
| 55 | - | ||
| 56 | -# add administrator | ||
| 57 | -students['0'] = 'Professor' | 56 | + }) |
| 58 | 57 | ||
| 58 | +print(f'Generating {len(students)} bcrypt password hashes. This will take some time...') | ||
| 59 | 59 | ||
| 60 | -print('Generating bcrypt password hashes takes time. Please be patient.') | ||
| 61 | try: | 60 | try: |
| 62 | # --- start db session --- | 61 | # --- start db session --- |
| 63 | session = Session() | 62 | session = Session() |
knowledge.py
| @@ -30,7 +30,7 @@ class Knowledge(object): | @@ -30,7 +30,7 @@ class Knowledge(object): | ||
| 30 | # ------------------------------------------------------------------------ | 30 | # ------------------------------------------------------------------------ |
| 31 | def new_topic(self, topic=None): | 31 | def new_topic(self, topic=None): |
| 32 | if topic is None: | 32 | if topic is None: |
| 33 | - # select the first topic that has level < 0.9 | 33 | + # select the first topic that has level < 0.8 |
| 34 | for topic in self.topic_sequence: | 34 | for topic in self.topic_sequence: |
| 35 | if topic not in self.state or self.state[topic]['level'] < 0.8: | 35 | if topic not in self.state or self.state[topic]['level'] < 0.8: |
| 36 | break | 36 | break |
serve.py
| @@ -234,15 +234,14 @@ def main(): | @@ -234,15 +234,14 @@ def main(): | ||
| 234 | http_server.listen(8443) | 234 | http_server.listen(8443) |
| 235 | 235 | ||
| 236 | # --- start webserver | 236 | # --- start webserver |
| 237 | + logging.info('Webserver running...') | ||
| 237 | try: | 238 | try: |
| 238 | - logging.info('Webserver running...') | ||
| 239 | tornado.ioloop.IOLoop.current().start() | 239 | tornado.ioloop.IOLoop.current().start() |
| 240 | - | ||
| 241 | # running... | 240 | # running... |
| 242 | - | ||
| 243 | except KeyboardInterrupt: | 241 | except KeyboardInterrupt: |
| 244 | tornado.ioloop.IOLoop.current().stop() | 242 | tornado.ioloop.IOLoop.current().stop() |
| 245 | - logging.info('Webserver stopped.') | 243 | + finally: |
| 244 | + logging.critical('Webserver stopped.') | ||
| 246 | 245 | ||
| 247 | # ---------------------------------------------------------------------------- | 246 | # ---------------------------------------------------------------------------- |
| 248 | if __name__ == "__main__": | 247 | if __name__ == "__main__": |
tools.py
| @@ -14,7 +14,7 @@ def load_yaml(filename, default=None): | @@ -14,7 +14,7 @@ def load_yaml(filename, default=None): | ||
| 14 | try: | 14 | try: |
| 15 | f = open(filename, 'r', encoding='utf-8') | 15 | f = open(filename, 'r', encoding='utf-8') |
| 16 | except IOError: | 16 | except IOError: |
| 17 | - logger.error('Can\'t open file "{}"'.format(filename)) | 17 | + logger.error(f'Can\'t open file "{filename}"') |
| 18 | return default | 18 | return default |
| 19 | else: | 19 | else: |
| 20 | with f: | 20 | with f: |