Commit ea950c895f1217578cdb676043eb893a7baa7f56

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

- support for configuration file with more information for each node, including:

- name (user friendly name for the web)
	- deps (list of dependencies)
BUGS.md
1 1 BUGS:
2 2  
  3 +- guardar state cada vez que topico termina
  4 +- alterar password.
  5 +- logs mostram que está a gerar cada pergunta 2 vezes...??
3 6 - reload da página rebenta o estado.
4 7 - indicar o topico actual no sidebar
5 8 - session management. close after inactive time.
6   -- guardar state cada vez que topico termina
7   -- logs mostram que está a gerar cada pergunta 2 vezes...??
8 9 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
9 10  
10 11 TODO:
11 12  
  13 +- letsencrypt.org
  14 +- logs de debug devem indicar o user.
  15 +- implementar http com redirect para https.
12 16 - topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos.
13   -- usar codemirror
  17 +- usar codemirror no textarea
14 18 - mostrar comments quando falha a resposta
15 19 - generators not working: bcrypt (ver blog)
16 20  
... ...
app.py
... ... @@ -81,7 +81,6 @@ class LearnApp(object):
81 81 if a.topic_id in state:
82 82 a.level = state.pop(a.topic_id) # update
83 83 s.add(a)
84   - # s.add_all(aa)
85 84  
86 85 # insert the remaining ones
87 86 u = s.query(Student).get(uid)
... ... @@ -114,6 +113,10 @@ class LearnApp(object):
114 113 return self.depgraph.graph['title']
115 114  
116 115 # ------------------------------------------------------------------------
  116 + def get_topic_name(self, ref):
  117 + return self.depgraph.node[ref]['name']
  118 +
  119 + # ------------------------------------------------------------------------
117 120 def get_current_public_dir(self, uid):
118 121 topic = self.online[uid]['state'].get_current_topic()
119 122 p = self.depgraph.graph['path']
... ... @@ -140,19 +143,49 @@ class LearnApp(object):
140 143 return knowledge.new_question()
141 144  
142 145 # ------------------------------------------------------------------------
143   - # helper to manage db sessions using the `with` statement, for example
144   - # with self.db_session() as s: s.query(...)
145   - @contextmanager
146   - def db_session(self, **kw):
147   - session = self.Session(**kw)
148   - try:
149   - yield session
150   - session.commit()
151   - except Exception as e:
152   - session.rollback()
153   - raise e
154   - finally:
155   - session.close()
  146 + # Receives a set of topics (strings like "math/algebra"),
  147 + # and recursively adds dependencies to the dependency graph
  148 + # def build_dependency_graph_old(self, config_file):
  149 + # logger.debug(f'LearnApp.build_dependency_graph("{config_file}")')
  150 +
  151 + # # Load configuration file
  152 + # try:
  153 + # with open(config_file, 'r') as f:
  154 + # logger.info(f'Loading configuration file "{config_file}"')
  155 + # config = yaml.load(f)
  156 + # except FileNotFoundError as e:
  157 + # logger.error(f'File not found: "{config_file}"')
  158 + # sys.exit(1)
  159 + # # config file parsed
  160 +
  161 + # prefix = config.get('path', '.')
  162 + # title = config.get('title', '')
  163 + # database = config.get('database', 'students.db')
  164 + # g = nx.DiGraph(path=prefix, title=title, database=database)
  165 +
  166 + # # Build dependency graph
  167 + # deps = config.get('dependencies', {})
  168 + # for n,dd in deps.items():
  169 + # g.add_edges_from((d,n) for d in dd)
  170 +
  171 + # # Builds factories for each node
  172 + # for n in g.nodes_iter():
  173 + # fullpath = path.expanduser(path.join(prefix, n))
  174 + # if path.isdir(fullpath):
  175 + # # if directory defaults to "prefix/questions.yaml"
  176 + # filename = path.join(fullpath, "questions.yaml")
  177 + # else:
  178 + # logger.error(f'build_dependency_graph: "{fullpath}" is not a directory')
  179 +
  180 + # if path.isfile(filename):
  181 + # logger.info(f'Loading questions from "{filename}"')
  182 + # questions = load_yaml(filename, default=[])
  183 + # for q in questions:
  184 + # q['path'] = fullpath
  185 +
  186 + # g.node[n]['factory'] = [QFactory(q) for q in questions]
  187 +
  188 + # self.depgraph = g
156 189  
157 190 # ------------------------------------------------------------------------
158 191 # Receives a set of topics (strings like "math/algebra"),
... ... @@ -168,22 +201,33 @@ class LearnApp(object):
168 201 except FileNotFoundError as e:
169 202 logger.error(f'File not found: "{config_file}"')
170 203 sys.exit(1)
  204 + # config file parsed
171 205  
172   - prefix = config['path'] # FIXME default if does not exist?
  206 + prefix = config.get('path', '.')
173 207 title = config.get('title', '')
174 208 database = config.get('database', 'students.db')
175 209 g = nx.DiGraph(path=prefix, title=title, database=database)
176 210  
177   - # Build dependency graph
178   - deps = config.get('dependencies', {})
179   - for n,dd in deps.items():
180   - g.add_edges_from((d,n) for d in dd)
181   -
182   - # Builds factories for each node
183   - for n in g.nodes_iter():
184   - fullpath = path.expanduser(path.join(prefix, n))
  211 + # iterate over topics and build graph
  212 + topics = config.get('topics', {})
  213 + for ref,attr in topics.items():
  214 + g.add_node(ref)
  215 + if isinstance(attr, list):
  216 + # if prop is a list, we assume it's just a list of dependencies
  217 + g.add_edges_from((d,ref) for d in attr)
  218 +
  219 + elif isinstance(attr, dict):
  220 + g.node[ref]['name'] = attr.get('name', ref)
  221 + g.add_edges_from((d,ref) for d in attr.get('deps', []))
  222 +
  223 + elif isinstance(attr, str):
  224 + g.node[ref]['name'] = attr
  225 +
  226 + # iterate over topics and create question factories
  227 + for ref in g.nodes_iter():
  228 + g.node[ref].setdefault('name', ref)
  229 + fullpath = path.expanduser(path.join(prefix, ref))
185 230 if path.isdir(fullpath):
186   - # if directory defaults to "prefix/questions.yaml"
187 231 filename = path.join(fullpath, "questions.yaml")
188 232 else:
189 233 logger.error(f'build_dependency_graph: "{fullpath}" is not a directory')
... ... @@ -193,10 +237,39 @@ class LearnApp(object):
193 237 questions = load_yaml(filename, default=[])
194 238 for q in questions:
195 239 q['path'] = fullpath
196   -
197   - g.node[n]['factory'] = [QFactory(q) for q in questions]
  240 + g.node[ref]['factory'] = [QFactory(q) for q in questions]
  241 + else:
  242 + g.node[ref]['factory'] = []
  243 + logger.error(f'build_dependency_graph: "{filename}" does not exist')
198 244  
199 245 self.depgraph = g
  246 + return g
  247 +
  248 +
  249 + # # Build dependency graph
  250 + # deps = config.get('dependencies', {})
  251 + # for n,dd in deps.items():
  252 + # g.add_edges_from((d,n) for d in dd)
  253 +
  254 + # # Builds factories for each node
  255 + # for n in g.nodes_iter():
  256 + # fullpath = path.expanduser(path.join(prefix, n))
  257 + # if path.isdir(fullpath):
  258 + # # if directory defaults to "prefix/questions.yaml"
  259 + # filename = path.join(fullpath, "questions.yaml")
  260 + # else:
  261 + # logger.error(f'build_dependency_graph: "{fullpath}" is not a directory')
  262 +
  263 + # if path.isfile(filename):
  264 + # logger.info(f'Loading questions from "{filename}"')
  265 + # questions = load_yaml(filename, default=[])
  266 + # for q in questions:
  267 + # q['path'] = fullpath
  268 +
  269 + # g.node[n]['factory'] = [QFactory(q) for q in questions]
  270 +
  271 + # self.depgraph = g
  272 +
200 273  
201 274 # ------------------------------------------------------------------------
202 275 def db_add_topics(self):
... ... @@ -220,3 +293,18 @@ class LearnApp(object):
220 293 else:
221 294 logger.info(f'Database has {n} students registered.')
222 295  
  296 + # ------------------------------------------------------------------------
  297 + # helper to manage db sessions using the `with` statement, for example
  298 + # with self.db_session() as s: s.query(...)
  299 + @contextmanager
  300 + def db_session(self, **kw):
  301 + session = self.Session(**kw)
  302 + try:
  303 + yield session
  304 + session.commit()
  305 + except Exception as e:
  306 + session.rollback()
  307 + raise e
  308 + finally:
  309 + session.close()
  310 +
... ...
models.py
... ... @@ -40,7 +40,6 @@ class Student(Base):
40 40 name: "{self.name}"
41 41 password: "{self.password}"'''
42 42  
43   -
44 43 # ---------------------------------------------------------------------------
45 44 # Table with every answer given
46 45 # ---------------------------------------------------------------------------
... ...
serve.py
... ... @@ -153,11 +153,16 @@ class QuestionHandler(BaseHandler):
153 153 progress = self.learn.get_student_progress(user) # in the current topic
154 154  
155 155 if next_question is not None:
156   - question_html = self.render_string(self.templates[next_question['type']],
  156 + question_html = self.render_string(
  157 + self.templates[next_question['type']],
157 158 question=next_question, # dictionary with the question
158 159 md=md, # function that renders markdown to html
159 160 )
160   - topics_html = self.render_string('topics.html', state=state)
  161 + topics_html = self.render_string(
  162 + 'topics.html',
  163 + state=state,
  164 + topicname=self.learn.get_topic_name, # function that translates topic references to names
  165 + )
161 166  
162 167 self.write({
163 168 'method': 'new_question',
... ... @@ -175,7 +180,6 @@ class QuestionHandler(BaseHandler):
175 180 },
176 181 })
177 182  
178   -
179 183 # ----------------------------------------------------------------------------
180 184 def main():
181 185 SERVER_PATH = os.path.dirname(os.path.realpath(__file__))
... ...
templates/learn.html
... ... @@ -34,11 +34,10 @@
34 34 </head>
35 35 <!-- ===================================================================== -->
36 36 <body>
37   -<!-- ===================================================================== -->
38 37 <!-- Navbar -->
39 38 <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
40   - <!-- <div class="container-fluid"> -->
41   -
  39 + <div class="container-fluid">
  40 +<!-- Brand and toggle get grouped for better mobile display -->
42 41 <div class="navbar-header">
43 42 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar" aria-expanded="false">
44 43 <span class="sr-only">Toggle navigation</span>
... ... @@ -48,7 +47,7 @@
48 47 </button>
49 48 <a class="navbar-brand" href="#">{{ title }}</a>
50 49 </div>
51   -
  50 +<!-- nav links and other content for toggling -->
52 51 <div class="collapse navbar-collapse" id="myNavbar">
53 52 <ul class="nav navbar-nav navbar-right">
54 53 <li class="dropdown">
... ... @@ -63,26 +62,24 @@
63 62 </li>
64 63 </ul>
65 64 </div>
66   - <!-- </div> -->
  65 + </div>
67 66 </nav>
68 67 <!-- ===================================================================== -->
69 68 <div class="row-offcanvas row-offcanvas-left">
  69 + <!-- topics sidebar -->
70 70 <div id="sidebar" class="sidebar-offcanvas">
71 71 <div class="col-md-12">
72 72 <div id="topics"></div>
73 73 </div>
74 74 </div>
75   -
  75 +<!-- main panel with questions -->
76 76 <div id="main">
77 77  
78 78 <!-- <audio>
79 79 <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg">
80 80 <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg">
81 81 <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg">
82   -</audio>
83   - -->
84   -
85   -
  82 + </audio> -->
86 83  
87 84 <div id="body">
88 85 <div class="progress">
... ... @@ -99,29 +96,18 @@
99 96 {% module xsrf_form_html() %}
100 97  
101 98 <div id="question_div">
102   - Pronto?
  99 + FIXME
103 100 </div>
104 101  
105 102 </form>
106 103 <button class="btn btn-primary" id="submit">Continuar</button>
107   -
108 104 </div>
109 105 </div> <!-- body -->
110   -
111   -
112   - <!-- <footer class="footer"> -->
113   - <!-- </footer> -->
114   -
115 106 </div> <!-- main -->
116   -
117 107 </div>
118 108  
119 109  
120 110  
121   -
122   -
123   -
124   -
125 111 <!-- ===================================================================== -->
126 112 <!-- JAVASCRIP -->
127 113 <!-- ===================================================================== -->
... ...
templates/question.html
1 1 {% autoescape %}
2 2  
3   -<!-- <div class="panel panel-default">
4   - <div class="panel-body">
5   - -->
6 3 <h1 class="page-header">{{ question['title'] }}</h1>
7 4 <div id="text">
8 5 {{ md(question['text']) }}
9 6 </div>
10 7  
11 8 {% block answer %}{% end %}
12   -
13   -<!-- </div>
14   -</div>
15   - -->
16 9 \ No newline at end of file
... ...
templates/topics.html
... ... @@ -5,7 +5,7 @@
5 5 <ul class="nav nav-pills nav-stacked">
6 6 {% for t in state %}
7 7 <li role="presentation" class="disabled"> <!-- class="active" -->
8   - <a href="#" class="disabled">{{ t[0] }}<br>
  8 + <a href="#" class="disabled">{{ topicname(t[0]) }}<br>
9 9 {{ round(t[1]*5)*'<i class="fa fa-star text-success" aria-hidden="true"></i>' + round(5-t[1]*5)*'<i class="fa fa-star-o" aria-hidden="true"></i>' }}
10 10 </a>
11 11 </li>
... ...