Commit c3bb2c291c65db5240db0f63714297e0502f00b1

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

- new: change password

1 BUGS: 1 BUGS:
2 2
3 - guardar state cada vez que topico termina 3 - guardar state cada vez que topico termina
4 -- alterar password.  
5 - logs mostram que está a gerar cada pergunta 2 vezes...?? 4 - logs mostram que está a gerar cada pergunta 2 vezes...??
6 - reload da página rebenta o estado. 5 - reload da página rebenta o estado.
7 - indicar o topico actual no sidebar 6 - indicar o topico actual no sidebar
@@ -20,6 +19,7 @@ TODO: @@ -20,6 +19,7 @@ TODO:
20 19
21 SOLVED: 20 SOLVED:
22 21
  22 +- alterar password.
23 - barra de progresso a funcionar 23 - barra de progresso a funcionar
24 - mostra tópicos do lado esquerdo, indicando quais estão feitos 24 - mostra tópicos do lado esquerdo, indicando quais estão feitos
25 - database hardcoded in LearnApp. 25 - database hardcoded in LearnApp.
@@ -97,6 +97,17 @@ class LearnApp(object): @@ -97,6 +97,17 @@ class LearnApp(object):
97 logger.info(f'User "{uid}" logged out') 97 logger.info(f'User "{uid}" logged out')
98 98
99 # ------------------------------------------------------------------------ 99 # ------------------------------------------------------------------------
  100 + def change_password(self, uid, pw):
  101 + if not pw:
  102 + return False
  103 +
  104 + with self.db_session() as s:
  105 + u = s.query(Student).get(uid)
  106 + u.password = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
  107 + logger.info(f'User "{uid}" changed password')
  108 + return True
  109 +
  110 + # ------------------------------------------------------------------------
100 def get_student_name(self, uid): 111 def get_student_name(self, uid):
101 return self.online[uid].get('name', '') 112 return self.online[uid].get('name', '')
102 113
@@ -125,7 +136,6 @@ class LearnApp(object): @@ -125,7 +136,6 @@ class LearnApp(object):
125 # ------------------------------------------------------------------------ 136 # ------------------------------------------------------------------------
126 # check answer and if correct returns new question, otherise returns None 137 # check answer and if correct returns new question, otherise returns None
127 def check_answer(self, uid, answer): 138 def check_answer(self, uid, answer):
128 - # logger.debug(f'check_answer("{uid}", "{answer}")')  
129 knowledge = self.online[uid]['state'] 139 knowledge = self.online[uid]['state']
130 current_question = knowledge.check_answer(answer) 140 current_question = knowledge.check_answer(answer)
131 141
@@ -145,51 +155,6 @@ class LearnApp(object): @@ -145,51 +155,6 @@ class LearnApp(object):
145 # ------------------------------------------------------------------------ 155 # ------------------------------------------------------------------------
146 # Receives a set of topics (strings like "math/algebra"), 156 # Receives a set of topics (strings like "math/algebra"),
147 # and recursively adds dependencies to the dependency graph 157 # 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  
189 -  
190 - # ------------------------------------------------------------------------  
191 - # Receives a set of topics (strings like "math/algebra"),  
192 - # and recursively adds dependencies to the dependency graph  
193 def build_dependency_graph(self, config_file): 158 def build_dependency_graph(self, config_file):
194 logger.debug(f'LearnApp.build_dependency_graph("{config_file}")') 159 logger.debug(f'LearnApp.build_dependency_graph("{config_file}")')
195 160
@@ -245,32 +210,6 @@ class LearnApp(object): @@ -245,32 +210,6 @@ class LearnApp(object):
245 self.depgraph = g 210 self.depgraph = g
246 return g 211 return g
247 212
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 -  
273 -  
274 # ------------------------------------------------------------------------ 213 # ------------------------------------------------------------------------
275 def db_add_topics(self): 214 def db_add_topics(self):
276 with self.db_session() as s: 215 with self.db_session() as s:
@@ -35,11 +35,12 @@ from tools import load_yaml, md @@ -35,11 +35,12 @@ from tools import load_yaml, md
35 class WebApplication(tornado.web.Application): 35 class WebApplication(tornado.web.Application):
36 def __init__(self, learnapp): 36 def __init__(self, learnapp):
37 handlers = [ 37 handlers = [
38 - (r'/login', LoginHandler),  
39 - (r'/logout', LogoutHandler),  
40 - (r'/question', QuestionHandler),  
41 - (r'/', LearnHandler),  
42 - (r'/(.+)', FileHandler), 38 + (r'/login', LoginHandler),
  39 + (r'/logout', LogoutHandler),
  40 + (r'/change_password', ChangePasswordHandler),
  41 + (r'/question', QuestionHandler),
  42 + (r'/', LearnHandler),
  43 + (r'/(.+)', FileHandler),
43 ] 44 ]
44 settings = { 45 settings = {
45 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 46 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
@@ -86,11 +87,9 @@ class LoginHandler(BaseHandler): @@ -86,11 +87,9 @@ class LoginHandler(BaseHandler):
86 pw = self.get_body_argument('pw') 87 pw = self.get_body_argument('pw')
87 88
88 if self.learn.login(uid, pw): 89 if self.learn.login(uid, pw):
89 - # logging.info(f'User "{uid}" login ok.')  
90 self.set_secure_cookie("user", str(uid), expires_days=30) 90 self.set_secure_cookie("user", str(uid), expires_days=30)
91 self.redirect(self.get_argument("next", "/")) 91 self.redirect(self.get_argument("next", "/"))
92 else: 92 else:
93 - # logging.info(f'User "{uid}" login failed.')  
94 self.render("login.html", error='Número ou senha incorrectos') 93 self.render("login.html", error='Número ou senha incorrectos')
95 94
96 # ---------------------------------------------------------------------------- 95 # ----------------------------------------------------------------------------
@@ -102,6 +101,25 @@ class LogoutHandler(BaseHandler): @@ -102,6 +101,25 @@ class LogoutHandler(BaseHandler):
102 self.redirect(self.get_argument('next', '/')) 101 self.redirect(self.get_argument('next', '/'))
103 102
104 # ---------------------------------------------------------------------------- 103 # ----------------------------------------------------------------------------
  104 +class ChangePasswordHandler(BaseHandler):
  105 + @tornado.web.authenticated
  106 + def post(self):
  107 + uid = self.current_user
  108 + pw = self.get_body_arguments('new_password')[0];
  109 +
  110 + if self.learn.change_password(uid, pw):
  111 + notification = tornado.escape.to_unicode(
  112 + self.render_string(
  113 + 'notification.html',
  114 + type='success',
  115 + msg='A password foi alterada!'
  116 + )
  117 + )
  118 + else:
  119 + notification = tornado.escape.to_unicode(self.render_string('notification.html', type='danger', msg='A password não foi alterada!'))
  120 + self.write({'msg': notification})
  121 +
  122 +# ----------------------------------------------------------------------------
105 # /learn 123 # /learn
106 # ---------------------------------------------------------------------------- 124 # ----------------------------------------------------------------------------
107 class LearnHandler(BaseHandler): 125 class LearnHandler(BaseHandler):
templates/learn.html
@@ -34,6 +34,12 @@ @@ -34,6 +34,12 @@
34 </head> 34 </head>
35 <!-- ===================================================================== --> 35 <!-- ===================================================================== -->
36 <body> 36 <body>
  37 + <!-- <audio>
  38 + <source id="snd-intro" src="/static/sounds/intro.mp3" type="audio/mpeg">
  39 + <source id="snd-correct" src="/static/sounds/correct.mp3" type="audio/mpeg">
  40 + <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg">
  41 + </audio> -->
  42 +
37 <!-- Navbar --> 43 <!-- Navbar -->
38 <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> 44 <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
39 <div class="container-fluid"> 45 <div class="container-fluid">
@@ -52,11 +58,12 @@ @@ -52,11 +58,12 @@
52 <ul class="nav navbar-nav navbar-right"> 58 <ul class="nav navbar-nav navbar-right">
53 <li class="dropdown"> 59 <li class="dropdown">
54 <a class="dropdown-toggle" data-toggle="dropdown" href="#"> 60 <a class="dropdown-toggle" data-toggle="dropdown" href="#">
55 - <i class="fa fa-user" aria-hidden="true"></i> 61 + <i class="fa fa-graduation-cap" aria-hidden="true"></i>
56 <span id="name"> {{ name }}</span> 62 <span id="name"> {{ name }}</span>
57 <span class="caret"></span> 63 <span class="caret"></span>
58 </a> 64 </a>
59 <ul class="dropdown-menu"> 65 <ul class="dropdown-menu">
  66 + <li><a data-toggle="modal" href="#password_modal"><i class="fa fa-key" aria-hidden="true"></i> Alterar password</a></li>
60 <li><a href="/logout"><i class="fa fa-sign-out" aria-hidden="true"></i> Sair</a></li> 67 <li><a href="/logout"><i class="fa fa-sign-out" aria-hidden="true"></i> Sair</a></li>
61 </ul> 68 </ul>
62 </li> 69 </li>
@@ -74,16 +81,9 @@ @@ -74,16 +81,9 @@
74 </div> 81 </div>
75 <!-- main panel with questions --> 82 <!-- main panel with questions -->
76 <div id="main"> 83 <div id="main">
77 -  
78 - <!-- <audio>  
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">  
81 - <source id="snd-wrong" src="/static/sounds/wrong.mp3" type="audio/mpeg">  
82 - </audio> -->  
83 -  
84 <div id="body"> 84 <div id="body">
85 <div class="progress"> 85 <div class="progress">
86 - <div class="progress-bar progress-bar-success" id="topic_progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 0em;width: 0%"></div> 86 + <div class="progress-bar progress-bar-info" id="topic_progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 0em;width: 0%"></div>
87 </div> 87 </div>
88 88
89 <div class="col-md-12"> 89 <div class="col-md-12">
@@ -92,11 +92,16 @@ @@ -92,11 +92,16 @@
92 <button type="button" class="btn btn-primary btn-xs" data-toggle="offcanvas"><i class="glyphicon glyphicon-chevron-left"></i></button> 92 <button type="button" class="btn btn-primary btn-xs" data-toggle="offcanvas"><i class="glyphicon glyphicon-chevron-left"></i></button>
93 </p> 93 </p>
94 94
  95 + <div id="notifications">
  96 + <!-- notifications go here -->
  97 + </div>
  98 +
  99 + <!-- Question body -->
95 <form action="/question" method="post" id="question_form" autocomplete="off"> 100 <form action="/question" method="post" id="question_form" autocomplete="off">
96 {% module xsrf_form_html() %} 101 {% module xsrf_form_html() %}
97 102
98 <div id="question_div"> 103 <div id="question_div">
99 - FIXME 104 + <!-- questions go here -->
100 </div> 105 </div>
101 106
102 </form> 107 </form>
@@ -106,6 +111,33 @@ @@ -106,6 +111,33 @@
106 </div> <!-- main --> 111 </div> <!-- main -->
107 </div> 112 </div>
108 113
  114 +<!-- === Change Password Modal =========================================== -->
  115 +<div id="password_modal" class="modal fade" tabindex="-1" role="dialog">
  116 + <div class="modal-dialog modal-sm" role="document">
  117 + <div class="modal-content">
  118 +<!-- header -->
  119 + <div class="modal-header">
  120 + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  121 + <h4 class="modal-title">Alterar Password</h4>
  122 + </div>
  123 +<!-- body -->
  124 + <div class="modal-body">
  125 + <div class="control-group">
  126 + <label for="new_password" class="control-label">Nova Password</label>
  127 + <div class="controls">
  128 + <input type="password" id="new_password" name="new_password" autocomplete="new-password">
  129 + </div>
  130 + </div>
  131 + </div>
  132 +<!-- footer -->
  133 + <div class="modal-footer">
  134 + <button type="button" class="btn btn-default" data-dismiss="modal">Cancelar</button>
  135 + <button id="change_password" type="button" class="btn btn-danger" data-dismiss="modal">Alterar</button>
  136 + </div>
  137 +
  138 + </div><!-- /.modal-content -->
  139 + </div><!-- /.modal-dialog -->
  140 +</div><!-- /.modal -->
109 141
110 142
111 <!-- ===================================================================== --> 143 <!-- ===================================================================== -->
@@ -159,13 +191,32 @@ function getQuestion() { @@ -159,13 +191,32 @@ function getQuestion() {
159 type: "POST", 191 type: "POST",
160 url: "/question", 192 url: "/question",
161 // headers: {"X-XSRFToken": token}, 193 // headers: {"X-XSRFToken": token},
162 - data: $("#question_form").serialize(),//{'a':10,'b':20}, 194 + data: $("#question_form").serialize(), // {'a':10,'b':20},
163 dataType: "json", // expected from server 195 dataType: "json", // expected from server
164 success: updateQuestion, 196 success: updateQuestion,
165 error: function() {alert("O servidor não responde.");} 197 error: function() {alert("O servidor não responde.");}
166 }); 198 });
167 } 199 }
168 200
  201 +function change_password() {
  202 + $.ajax({
  203 + type: "POST",
  204 + url: "/change_password",
  205 + data: {
  206 + "new_password": $("#new_password").val(),
  207 + },
  208 + dataType: "json",
  209 + success: function(r) {
  210 + $("#notifications").html(r["msg"]);
  211 + $("#notification").fadeIn(250).delay(5000).fadeOut(500);
  212 + },
  213 + error: function(r) {
  214 + $("#notifications").html(r["msg"]);
  215 + $("#notification").fadeIn(250).delay(5000).fadeOut(500);
  216 + },
  217 + });
  218 +}
  219 +
169 $(document).ready(function() { 220 $(document).ready(function() {
170 // var audio = new Audio('/static/sounds/intro.mp3'); 221 // var audio = new Audio('/static/sounds/intro.mp3');
171 // audio.play(); 222 // audio.play();
@@ -174,6 +225,7 @@ $(document).ready(function() { @@ -174,6 +225,7 @@ $(document).ready(function() {
174 $('[data-toggle=offcanvas]').click(function() { 225 $('[data-toggle=offcanvas]').click(function() {
175 $('.row-offcanvas').toggleClass('active'); 226 $('.row-offcanvas').toggleClass('active');
176 }); 227 });
  228 + $("#change_password").click(change_password);
177 }); 229 });
178 </script> 230 </script>
179 231
templates/login.html
@@ -5,14 +5,16 @@ @@ -5,14 +5,16 @@
5 <title>Teste</title> 5 <title>Teste</title>
6 <link rel="icon" href="/static/favicon.ico" /> 6 <link rel="icon" href="/static/favicon.ico" />
7 7
8 - <!-- Bootstrap --> 8 +<!-- Bootstrap -->
9 <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> 9 <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
10 - <link rel="stylesheet" href="/static/bootstrap/css/bootstrap-theme.min.css"> <!-- optional -->  
11 - <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css">  
12 - <!-- <link rel="stylesheet" href="/static/css/github.css">  
13 - <link rel="stylesheet" href="/static/css/test.css"> --> 10 + <link rel="stylesheet" href="/static/css/bootstrap-theme.min.css"> <!-- optional -->
14 11
15 - <!-- <script src="/static/js/jquery.min.js"></script> --> 12 +<!-- other -->
  13 + <!-- <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> -->
  14 + <!-- <link rel="stylesheet" href="/static/css/github.css"> -->
  15 + <!-- <link rel="stylesheet" href="/static/css/test.css"> -->
  16 +
  17 + <script src="/static/js/jquery.min.js"></script>
16 <script src="/static/bootstrap/js/bootstrap.min.js"></script> 18 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
17 </head> 19 </head>
18 <!-- ===================================================================== --> 20 <!-- ===================================================================== -->
templates/notification.html 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +
  2 +<div class="alert alert-{{ type }} alert-dismissible" role="alert" id="notification" style="position:absolute;
  3 + top: 0px; right: 1em;">
  4 + <!-- <i class="fa fa-check" aria-hidden="true"></i> -->
  5 + <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  6 + {{ msg }}
  7 +</div>
templates/question.html
1 {% autoescape %} 1 {% autoescape %}
2 2
3 - <h1 class="page-header">{{ question['title'] }}</h1>  
4 - <div id="text">  
5 - {{ md(question['text']) }}  
6 - </div> 3 +<h1 class="page-header">{{ question['title'] }}</h1>
7 4
8 - {% block answer %}{% end %} 5 +<div id="text">
  6 + {{ md(question['text']) }}
  7 +</div>
  8 +
  9 +{% block answer %}{% end %}