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 | 25 | # setup logger for this module |
| 26 | 26 | logger = logging.getLogger(__name__) |
| 27 | 27 | |
| 28 | + | |
| 29 | +class LearnAppException(Exception): | |
| 30 | + pass | |
| 31 | + | |
| 28 | 32 | # ============================================================================ |
| 29 | 33 | # LearnApp - application logic |
| 30 | 34 | # ============================================================================ |
| ... | ... | @@ -37,19 +41,20 @@ class LearnApp(object): |
| 37 | 41 | self.build_dependency_graph(conffile) |
| 38 | 42 | |
| 39 | 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 | 46 | # add topics from depgraph to the database |
| 43 | 47 | self.db_add_topics() |
| 44 | 48 | |
| 45 | 49 | # ------------------------------------------------------------------------ |
| 50 | + # login | |
| 51 | + # ------------------------------------------------------------------------ | |
| 46 | 52 | def login(self, uid, try_pw): |
| 47 | 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 | 55 | if student is None: |
| 51 | 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 | 59 | hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) |
| 55 | 60 | if hashedtry != student.password: |
| ... | ... | @@ -76,6 +81,7 @@ class LearnApp(object): |
| 76 | 81 | |
| 77 | 82 | # ------------------------------------------------------------------------ |
| 78 | 83 | # logout |
| 84 | + # ------------------------------------------------------------------------ | |
| 79 | 85 | def logout(self, uid): |
| 80 | 86 | state = self.online[uid]['state'].state # dict {node:level,...} |
| 81 | 87 | |
| ... | ... | @@ -105,6 +111,8 @@ class LearnApp(object): |
| 105 | 111 | logger.info(f'User "{uid}" logged out') |
| 106 | 112 | |
| 107 | 113 | # ------------------------------------------------------------------------ |
| 114 | + # change_password | |
| 115 | + # ------------------------------------------------------------------------ | |
| 108 | 116 | def change_password(self, uid, pw): |
| 109 | 117 | if not pw: |
| 110 | 118 | return False |
| ... | ... | @@ -165,21 +173,26 @@ class LearnApp(object): |
| 165 | 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 | 180 | def build_dependency_graph(self, config_file): |
| 172 | 181 | |
| 173 | - # Load configuration file | |
| 182 | + # Load configuration file to a dict | |
| 174 | 183 | try: |
| 175 | 184 | with open(config_file, 'r') as f: |
| 176 | - logger.info(f'Configuration file "{config_file}"') | |
| 177 | 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 | 196 | prefix = config.get('path', '.') |
| 184 | 197 | title = config.get('title', '') |
| 185 | 198 | database = config.get('database', 'students.db') |
| ... | ... | @@ -194,49 +207,35 @@ class LearnApp(object): |
| 194 | 207 | g.node[ref]['questions'] = attr.get('questions', []) |
| 195 | 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 | 210 | # iterate over topics and create question factories |
| 205 | 211 | logger.info('Loading:') |
| 206 | 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 | 233 | self.depgraph = g |
| 237 | 234 | return g |
| 238 | 235 | |
| 239 | 236 | # ------------------------------------------------------------------------ |
| 237 | + # Fill db table 'Topic' with topics from the graph if not already there. | |
| 238 | + # ------------------------------------------------------------------------ | |
| 240 | 239 | def db_add_topics(self): |
| 241 | 240 | with self.db_session() as s: |
| 242 | 241 | tt = [t[0] for t in s.query(Topic.id)] # db list of topics |
| ... | ... | @@ -245,8 +244,8 @@ class LearnApp(object): |
| 245 | 244 | |
| 246 | 245 | # ------------------------------------------------------------------------ |
| 247 | 246 | # setup and check database |
| 247 | + # ------------------------------------------------------------------------ | |
| 248 | 248 | def db_setup(self, db): |
| 249 | - logger.debug(f'LearnApp.db_setup("{db}")') | |
| 250 | 249 | engine = create_engine(f'sqlite:///{db}', echo=False) |
| 251 | 250 | self.Session = sessionmaker(bind=engine) |
| 252 | 251 | try: |
| ... | ... | @@ -256,11 +255,12 @@ class LearnApp(object): |
| 256 | 255 | logger.critical(f'Database "{db}" not usable.') |
| 257 | 256 | sys.exit(1) |
| 258 | 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 | 261 | # helper to manage db sessions using the `with` statement, for example |
| 263 | 262 | # with self.db_session() as s: s.query(...) |
| 263 | + # ------------------------------------------------------------------------ | |
| 264 | 264 | @contextmanager |
| 265 | 265 | def db_session(self, **kw): |
| 266 | 266 | session = self.Session(**kw) | ... | ... |
initdb.py
| ... | ... | @@ -31,6 +31,9 @@ engine = create_engine(f'sqlite:///{args.db}', echo=False) |
| 31 | 31 | Base.metadata.create_all(engine) # Criate schema if needed |
| 32 | 32 | Session = sessionmaker(bind=engine) |
| 33 | 33 | |
| 34 | +# add administrator | |
| 35 | +students = {'0': 'Professor'} | |
| 36 | + | |
| 34 | 37 | if args.csvfile: |
| 35 | 38 | # add students from csv file if available |
| 36 | 39 | try: |
| ... | ... | @@ -38,12 +41,11 @@ if args.csvfile: |
| 38 | 41 | except EnvironmentError: |
| 39 | 42 | print(f'Error: CSV file "{args.csvfile}" not found.') |
| 40 | 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 | 46 | elif args.demo: |
| 45 | 47 | # add a few fake students |
| 46 | - students = { | |
| 48 | + students.update({ | |
| 47 | 49 | '1915': 'Alan Turing', |
| 48 | 50 | '1938': 'Donald Knuth', |
| 49 | 51 | '1815': 'Ada Lovelace', |
| ... | ... | @@ -51,13 +53,10 @@ elif args.demo: |
| 51 | 53 | '1955': 'Tim Burners-Lee', |
| 52 | 54 | '1916': 'Claude Shannon', |
| 53 | 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 | 60 | try: |
| 62 | 61 | # --- start db session --- |
| 63 | 62 | session = Session() | ... | ... |
knowledge.py
| ... | ... | @@ -30,7 +30,7 @@ class Knowledge(object): |
| 30 | 30 | # ------------------------------------------------------------------------ |
| 31 | 31 | def new_topic(self, topic=None): |
| 32 | 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 | 34 | for topic in self.topic_sequence: |
| 35 | 35 | if topic not in self.state or self.state[topic]['level'] < 0.8: |
| 36 | 36 | break | ... | ... |
serve.py
| ... | ... | @@ -234,15 +234,14 @@ def main(): |
| 234 | 234 | http_server.listen(8443) |
| 235 | 235 | |
| 236 | 236 | # --- start webserver |
| 237 | + logging.info('Webserver running...') | |
| 237 | 238 | try: |
| 238 | - logging.info('Webserver running...') | |
| 239 | 239 | tornado.ioloop.IOLoop.current().start() |
| 240 | - | |
| 241 | 240 | # running... |
| 242 | - | |
| 243 | 241 | except KeyboardInterrupt: |
| 244 | 242 | tornado.ioloop.IOLoop.current().stop() |
| 245 | - logging.info('Webserver stopped.') | |
| 243 | + finally: | |
| 244 | + logging.critical('Webserver stopped.') | |
| 246 | 245 | |
| 247 | 246 | # ---------------------------------------------------------------------------- |
| 248 | 247 | if __name__ == "__main__": | ... | ... |
tools.py
| ... | ... | @@ -14,7 +14,7 @@ def load_yaml(filename, default=None): |
| 14 | 14 | try: |
| 15 | 15 | f = open(filename, 'r', encoding='utf-8') |
| 16 | 16 | except IOError: |
| 17 | - logger.error('Can\'t open file "{}"'.format(filename)) | |
| 17 | + logger.error(f'Can\'t open file "{filename}"') | |
| 18 | 18 | return default |
| 19 | 19 | else: |
| 20 | 20 | with f: | ... | ... |