Commit d21baa3abab6238da38e00c2795675a019c836f7

Authored by Miguel Barão
1 parent 834adb07
Exists in master and in 1 other branch dev

- cleanup app.py a little.

@@ -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)
@@ -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()
@@ -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
@@ -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__":
@@ -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: