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: |