Commit c3bb2c291c65db5240db0f63714297e0502f00b1
1 parent
ea950c89
Exists in
master
and in
1 other branch
- new: change password
Showing
7 changed files
with
121 additions
and
102 deletions
Show diff stats
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">×</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 | <!-- ===================================================================== --> | ... | ... |
| ... | ... | @@ -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">×</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 %} | ... | ... |