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 | 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. |
app.py
| @@ -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: |
serve.py
| @@ -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">×</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 | <!-- ===================================================================== --> |
| @@ -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">×</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 %} |