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