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)
1 BUGS: 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 - reload da página rebenta o estado. 6 - reload da página rebenta o estado.
4 - indicar o topico actual no sidebar 7 - indicar o topico actual no sidebar
5 - session management. close after inactive time. 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 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]() 9 - implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()
9 10
10 TODO: 11 TODO:
11 12
  13 +- letsencrypt.org
  14 +- logs de debug devem indicar o user.
  15 +- implementar http com redirect para https.
12 - topicos no sidebar devem ser links para iniciar um topico acessivel. os inacessiveis devem estar inactivos. 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 - mostrar comments quando falha a resposta 18 - mostrar comments quando falha a resposta
15 - generators not working: bcrypt (ver blog) 19 - generators not working: bcrypt (ver blog)
16 20
@@ -81,7 +81,6 @@ class LearnApp(object): @@ -81,7 +81,6 @@ class LearnApp(object):
81 if a.topic_id in state: 81 if a.topic_id in state:
82 a.level = state.pop(a.topic_id) # update 82 a.level = state.pop(a.topic_id) # update
83 s.add(a) 83 s.add(a)
84 - # s.add_all(aa)  
85 84
86 # insert the remaining ones 85 # insert the remaining ones
87 u = s.query(Student).get(uid) 86 u = s.query(Student).get(uid)
@@ -114,6 +113,10 @@ class LearnApp(object): @@ -114,6 +113,10 @@ class LearnApp(object):
114 return self.depgraph.graph['title'] 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 def get_current_public_dir(self, uid): 120 def get_current_public_dir(self, uid):
118 topic = self.online[uid]['state'].get_current_topic() 121 topic = self.online[uid]['state'].get_current_topic()
119 p = self.depgraph.graph['path'] 122 p = self.depgraph.graph['path']
@@ -140,19 +143,49 @@ class LearnApp(object): @@ -140,19 +143,49 @@ class LearnApp(object):
140 return knowledge.new_question() 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 # Receives a set of topics (strings like "math/algebra"), 191 # Receives a set of topics (strings like "math/algebra"),
@@ -168,22 +201,33 @@ class LearnApp(object): @@ -168,22 +201,33 @@ class LearnApp(object):
168 except FileNotFoundError as e: 201 except FileNotFoundError as e:
169 logger.error(f'File not found: "{config_file}"') 202 logger.error(f'File not found: "{config_file}"')
170 sys.exit(1) 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 title = config.get('title', '') 207 title = config.get('title', '')
174 database = config.get('database', 'students.db') 208 database = config.get('database', 'students.db')
175 g = nx.DiGraph(path=prefix, title=title, database=database) 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 if path.isdir(fullpath): 230 if path.isdir(fullpath):
186 - # if directory defaults to "prefix/questions.yaml"  
187 filename = path.join(fullpath, "questions.yaml") 231 filename = path.join(fullpath, "questions.yaml")
188 else: 232 else:
189 logger.error(f'build_dependency_graph: "{fullpath}" is not a directory') 233 logger.error(f'build_dependency_graph: "{fullpath}" is not a directory')
@@ -193,10 +237,39 @@ class LearnApp(object): @@ -193,10 +237,39 @@ class LearnApp(object):
193 questions = load_yaml(filename, default=[]) 237 questions = load_yaml(filename, default=[])
194 for q in questions: 238 for q in questions:
195 q['path'] = fullpath 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 self.depgraph = g 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 def db_add_topics(self): 275 def db_add_topics(self):
@@ -220,3 +293,18 @@ class LearnApp(object): @@ -220,3 +293,18 @@ class LearnApp(object):
220 else: 293 else:
221 logger.info(f'Database has {n} students registered.') 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 +
@@ -40,7 +40,6 @@ class Student(Base): @@ -40,7 +40,6 @@ class Student(Base):
40 name: "{self.name}" 40 name: "{self.name}"
41 password: "{self.password}"''' 41 password: "{self.password}"'''
42 42
43 -  
44 # --------------------------------------------------------------------------- 43 # ---------------------------------------------------------------------------
45 # Table with every answer given 44 # Table with every answer given
46 # --------------------------------------------------------------------------- 45 # ---------------------------------------------------------------------------
@@ -153,11 +153,16 @@ class QuestionHandler(BaseHandler): @@ -153,11 +153,16 @@ class QuestionHandler(BaseHandler):
153 progress = self.learn.get_student_progress(user) # in the current topic 153 progress = self.learn.get_student_progress(user) # in the current topic
154 154
155 if next_question is not None: 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 question=next_question, # dictionary with the question 158 question=next_question, # dictionary with the question
158 md=md, # function that renders markdown to html 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 self.write({ 167 self.write({
163 'method': 'new_question', 168 'method': 'new_question',
@@ -175,7 +180,6 @@ class QuestionHandler(BaseHandler): @@ -175,7 +180,6 @@ class QuestionHandler(BaseHandler):
175 }, 180 },
176 }) 181 })
177 182
178 -  
179 # ---------------------------------------------------------------------------- 183 # ----------------------------------------------------------------------------
180 def main(): 184 def main():
181 SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) 185 SERVER_PATH = os.path.dirname(os.path.realpath(__file__))
templates/learn.html
@@ -34,11 +34,10 @@ @@ -34,11 +34,10 @@
34 </head> 34 </head>
35 <!-- ===================================================================== --> 35 <!-- ===================================================================== -->
36 <body> 36 <body>
37 -<!-- ===================================================================== -->  
38 <!-- Navbar --> 37 <!-- Navbar -->
39 <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> 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 <div class="navbar-header"> 41 <div class="navbar-header">
43 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar" aria-expanded="false"> 42 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar" aria-expanded="false">
44 <span class="sr-only">Toggle navigation</span> 43 <span class="sr-only">Toggle navigation</span>
@@ -48,7 +47,7 @@ @@ -48,7 +47,7 @@
48 </button> 47 </button>
49 <a class="navbar-brand" href="#">{{ title }}</a> 48 <a class="navbar-brand" href="#">{{ title }}</a>
50 </div> 49 </div>
51 - 50 +<!-- nav links and other content for toggling -->
52 <div class="collapse navbar-collapse" id="myNavbar"> 51 <div class="collapse navbar-collapse" id="myNavbar">
53 <ul class="nav navbar-nav navbar-right"> 52 <ul class="nav navbar-nav navbar-right">
54 <li class="dropdown"> 53 <li class="dropdown">
@@ -63,26 +62,24 @@ @@ -63,26 +62,24 @@
63 </li> 62 </li>
64 </ul> 63 </ul>
65 </div> 64 </div>
66 - <!-- </div> --> 65 + </div>
67 </nav> 66 </nav>
68 <!-- ===================================================================== --> 67 <!-- ===================================================================== -->
69 <div class="row-offcanvas row-offcanvas-left"> 68 <div class="row-offcanvas row-offcanvas-left">
  69 + <!-- topics sidebar -->
70 <div id="sidebar" class="sidebar-offcanvas"> 70 <div id="sidebar" class="sidebar-offcanvas">
71 <div class="col-md-12"> 71 <div class="col-md-12">
72 <div id="topics"></div> 72 <div id="topics"></div>
73 </div> 73 </div>
74 </div> 74 </div>
75 - 75 +<!-- main panel with questions -->
76 <div id="main"> 76 <div id="main">
77 77
78 <!-- <audio> 78 <!-- <audio>
79 <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg"> 79 <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg">
80 <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg"> 80 <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg">
81 <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg"> 81 <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg">
82 -</audio>  
83 - -->  
84 -  
85 - 82 + </audio> -->
86 83
87 <div id="body"> 84 <div id="body">
88 <div class="progress"> 85 <div class="progress">
@@ -99,29 +96,18 @@ @@ -99,29 +96,18 @@
99 {% module xsrf_form_html() %} 96 {% module xsrf_form_html() %}
100 97
101 <div id="question_div"> 98 <div id="question_div">
102 - Pronto? 99 + FIXME
103 </div> 100 </div>
104 101
105 </form> 102 </form>
106 <button class="btn btn-primary" id="submit">Continuar</button> 103 <button class="btn btn-primary" id="submit">Continuar</button>
107 -  
108 </div> 104 </div>
109 </div> <!-- body --> 105 </div> <!-- body -->
110 -  
111 -  
112 - <!-- <footer class="footer"> -->  
113 - <!-- </footer> -->  
114 -  
115 </div> <!-- main --> 106 </div> <!-- main -->
116 -  
117 </div> 107 </div>
118 108
119 109
120 110
121 -  
122 -  
123 -  
124 -  
125 <!-- ===================================================================== --> 111 <!-- ===================================================================== -->
126 <!-- JAVASCRIP --> 112 <!-- JAVASCRIP -->
127 <!-- ===================================================================== --> 113 <!-- ===================================================================== -->
templates/question.html
1 {% autoescape %} 1 {% autoescape %}
2 2
3 -<!-- <div class="panel panel-default">  
4 - <div class="panel-body">  
5 - -->  
6 <h1 class="page-header">{{ question['title'] }}</h1> 3 <h1 class="page-header">{{ question['title'] }}</h1>
7 <div id="text"> 4 <div id="text">
8 {{ md(question['text']) }} 5 {{ md(question['text']) }}
9 </div> 6 </div>
10 7
11 {% block answer %}{% end %} 8 {% block answer %}{% end %}
12 -  
13 -<!-- </div>  
14 -</div>  
15 - -->  
16 \ No newline at end of file 9 \ No newline at end of file
templates/topics.html
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 <ul class="nav nav-pills nav-stacked"> 5 <ul class="nav nav-pills nav-stacked">
6 {% for t in state %} 6 {% for t in state %}
7 <li role="presentation" class="disabled"> <!-- class="active" --> 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 {{ 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>' }} 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 </a> 10 </a>
11 </li> 11 </li>