Commit c3bb2c291c65db5240db0f63714297e0502f00b1

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

- new: change password

BUGS.md
1 1 BUGS:
2 2  
3 3 - guardar state cada vez que topico termina
4   -- alterar password.
5 4 - logs mostram que está a gerar cada pergunta 2 vezes...??
6 5 - reload da página rebenta o estado.
7 6 - indicar o topico actual no sidebar
... ... @@ -20,6 +19,7 @@ TODO:
20 19  
21 20 SOLVED:
22 21  
  22 +- alterar password.
23 23 - barra de progresso a funcionar
24 24 - mostra tópicos do lado esquerdo, indicando quais estão feitos
25 25 - database hardcoded in LearnApp.
... ...
app.py
... ... @@ -97,6 +97,17 @@ class LearnApp(object):
97 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 111 def get_student_name(self, uid):
101 112 return self.online[uid].get('name', '')
102 113  
... ... @@ -125,7 +136,6 @@ class LearnApp(object):
125 136 # ------------------------------------------------------------------------
126 137 # check answer and if correct returns new question, otherise returns None
127 138 def check_answer(self, uid, answer):
128   - # logger.debug(f'check_answer("{uid}", "{answer}")')
129 139 knowledge = self.online[uid]['state']
130 140 current_question = knowledge.check_answer(answer)
131 141  
... ... @@ -145,51 +155,6 @@ class LearnApp(object):
145 155 # ------------------------------------------------------------------------
146 156 # Receives a set of topics (strings like "math/algebra"),
147 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 158 def build_dependency_graph(self, config_file):
194 159 logger.debug(f'LearnApp.build_dependency_graph("{config_file}")')
195 160  
... ... @@ -245,32 +210,6 @@ class LearnApp(object):
245 210 self.depgraph = g
246 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 214 def db_add_topics(self):
276 215 with self.db_session() as s:
... ...
serve.py
... ... @@ -35,11 +35,12 @@ from tools import load_yaml, md
35 35 class WebApplication(tornado.web.Application):
36 36 def __init__(self, learnapp):
37 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 45 settings = {
45 46 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
... ... @@ -86,11 +87,9 @@ class LoginHandler(BaseHandler):
86 87 pw = self.get_body_argument('pw')
87 88  
88 89 if self.learn.login(uid, pw):
89   - # logging.info(f'User "{uid}" login ok.')
90 90 self.set_secure_cookie("user", str(uid), expires_days=30)
91 91 self.redirect(self.get_argument("next", "/"))
92 92 else:
93   - # logging.info(f'User "{uid}" login failed.')
94 93 self.render("login.html", error='Número ou senha incorrectos')
95 94  
96 95 # ----------------------------------------------------------------------------
... ... @@ -102,6 +101,25 @@ class LogoutHandler(BaseHandler):
102 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 123 # /learn
106 124 # ----------------------------------------------------------------------------
107 125 class LearnHandler(BaseHandler):
... ...
templates/learn.html
... ... @@ -34,6 +34,12 @@
34 34 </head>
35 35 <!-- ===================================================================== -->
36 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 43 <!-- Navbar -->
38 44 <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
39 45 <div class="container-fluid">
... ... @@ -52,11 +58,12 @@
52 58 <ul class="nav navbar-nav navbar-right">
53 59 <li class="dropdown">
54 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 62 <span id="name"> {{ name }}</span>
57 63 <span class="caret"></span>
58 64 </a>
59 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 67 <li><a href="/logout"><i class="fa fa-sign-out" aria-hidden="true"></i> Sair</a></li>
61 68 </ul>
62 69 </li>
... ... @@ -74,16 +81,9 @@
74 81 </div>
75 82 <!-- main panel with questions -->
76 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 84 <div id="body">
85 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 87 </div>
88 88  
89 89 <div class="col-md-12">
... ... @@ -92,11 +92,16 @@
92 92 <button type="button" class="btn btn-primary btn-xs" data-toggle="offcanvas"><i class="glyphicon glyphicon-chevron-left"></i></button>
93 93 </p>
94 94  
  95 + <div id="notifications">
  96 + <!-- notifications go here -->
  97 + </div>
  98 +
  99 + <!-- Question body -->
95 100 <form action="/question" method="post" id="question_form" autocomplete="off">
96 101 {% module xsrf_form_html() %}
97 102  
98 103 <div id="question_div">
99   - FIXME
  104 + <!-- questions go here -->
100 105 </div>
101 106  
102 107 </form>
... ... @@ -106,6 +111,33 @@
106 111 </div> <!-- main -->
107 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 191 type: "POST",
160 192 url: "/question",
161 193 // headers: {"X-XSRFToken": token},
162   - data: $("#question_form").serialize(),//{'a':10,'b':20},
  194 + data: $("#question_form").serialize(), // {'a':10,'b':20},
163 195 dataType: "json", // expected from server
164 196 success: updateQuestion,
165 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 220 $(document).ready(function() {
170 221 // var audio = new Audio('/static/sounds/intro.mp3');
171 222 // audio.play();
... ... @@ -174,6 +225,7 @@ $(document).ready(function() {
174 225 $('[data-toggle=offcanvas]').click(function() {
175 226 $('.row-offcanvas').toggleClass('active');
176 227 });
  228 + $("#change_password").click(change_password);
177 229 });
178 230 </script>
179 231  
... ...
templates/login.html
... ... @@ -5,14 +5,16 @@
5 5 <title>Teste</title>
6 6 <link rel="icon" href="/static/favicon.ico" />
7 7  
8   - <!-- Bootstrap -->
  8 +<!-- Bootstrap -->
9 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 18 <script src="/static/bootstrap/js/bootstrap.min.js"></script>
17 19 </head>
18 20 <!-- ===================================================================== -->
... ...
templates/notification.html 0 → 100644
... ... @@ -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 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 %}
... ...