Commit d21baa3abab6238da38e00c2795675a019c836f7

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

- cleanup app.py a little.

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