Commit 894bdd0526be2fad60ac19702559c98980020848
1 parent
d91da251
Exists in
master
and in
1 other branch
- moved all application logic to app.py
- login error message on wrong user/password - general cleanup
Showing
4 changed files
with
175 additions
and
162 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,115 @@ |
| 1 | + | |
| 2 | +import random | |
| 3 | +from contextlib import contextmanager # `with` statement in db sessions | |
| 4 | + | |
| 5 | +# libs | |
| 6 | +import bcrypt | |
| 7 | +from sqlalchemy import create_engine | |
| 8 | +from sqlalchemy.orm import sessionmaker, scoped_session | |
| 9 | + | |
| 10 | +# this project | |
| 11 | +import questions | |
| 12 | +from models import Student | |
| 13 | + | |
| 14 | + | |
| 15 | +# ============================================================================ | |
| 16 | +# LearnApp - application logic | |
| 17 | +# ============================================================================ | |
| 18 | +class LearnApp(object): | |
| 19 | + def __init__(self): | |
| 20 | + print('LearnApp.__init__') | |
| 21 | + self.factory = questions.QuestionFactory() | |
| 22 | + self.factory.load_files(['questions.yaml'], 'demo') # FIXME | |
| 23 | + self.online = {} | |
| 24 | + | |
| 25 | + # connect to database and check registered students | |
| 26 | + engine = create_engine('sqlite:///{}'.format('students.db'), echo=False) | |
| 27 | + self.Session = scoped_session(sessionmaker(bind=engine)) | |
| 28 | + try: | |
| 29 | + with self.db_session() as s: | |
| 30 | + n = s.query(Student).filter(Student.id != '0').count() | |
| 31 | + except Exception as e: | |
| 32 | + print('Database not usable.') | |
| 33 | + raise e | |
| 34 | + else: | |
| 35 | + print('Database has {} students registered.'.format(n)) | |
| 36 | + | |
| 37 | + # ------------------------------------------------------------------------ | |
| 38 | + def login_ok(self, uid, try_pw): | |
| 39 | + print('LearnApp.login') | |
| 40 | + | |
| 41 | + with self.db_session() as s: | |
| 42 | + student = s.query(Student).filter(Student.id == uid).one_or_none() | |
| 43 | + | |
| 44 | + if student is None or student in self.online: | |
| 45 | + # student does not exist | |
| 46 | + return False | |
| 47 | + | |
| 48 | + # hashedtry = yield executor.submit(bcrypt.hashpw, | |
| 49 | + # try_pw.encode('utf-8'), student.password) | |
| 50 | + hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) | |
| 51 | + | |
| 52 | + if hashedtry != student.password: | |
| 53 | + # wrong password | |
| 54 | + return False | |
| 55 | + | |
| 56 | + # success | |
| 57 | + self.online[uid] = { | |
| 58 | + 'name': student.name, | |
| 59 | + 'number': student.id, | |
| 60 | + 'current': None, | |
| 61 | + } | |
| 62 | + print(self.online) | |
| 63 | + return True | |
| 64 | + | |
| 65 | + # ------------------------------------------------------------------------ | |
| 66 | + # logout | |
| 67 | + def logout(self, uid): | |
| 68 | + del self.online[uid] # FIXME save current question? | |
| 69 | + | |
| 70 | + # ------------------------------------------------------------------------ | |
| 71 | + # given the currect state, generates a new question for the student | |
| 72 | + def new_question_for(self, uid): | |
| 73 | + questions = list(self.factory) | |
| 74 | + nextquestion = self.factory.generate(random.choice(questions)) | |
| 75 | + self.online[uid]['current'] = nextquestion | |
| 76 | + return nextquestion | |
| 77 | + | |
| 78 | + # ------------------------------------------------------------------------ | |
| 79 | + def get_current_question(self, uid): | |
| 80 | + return self.online[uid].get('current', None) | |
| 81 | + | |
| 82 | + # ------------------------------------------------------------------------ | |
| 83 | + def get_student_name(self, uid): | |
| 84 | + return self.online[uid].get('name', '') | |
| 85 | + | |
| 86 | + # ------------------------------------------------------------------------ | |
| 87 | + # check answer and if correct returns new question, otherise returns None | |
| 88 | + def check_answer(self, uid, answer): | |
| 89 | + question = self.get_current_question(uid) | |
| 90 | + print('------------------------------') | |
| 91 | + print(question) | |
| 92 | + print(answer) | |
| 93 | + | |
| 94 | + if question is not None: | |
| 95 | + grade = question.correct(answer) # correct answer | |
| 96 | + correct = grade > 0.99999 | |
| 97 | + if correct: | |
| 98 | + print('CORRECT') | |
| 99 | + return self.new_question_for(uid) | |
| 100 | + else: | |
| 101 | + print('WRONG') | |
| 102 | + return None | |
| 103 | + else: | |
| 104 | + print('FIRST QUESTION') | |
| 105 | + return self.new_question_for(uid) | |
| 106 | + | |
| 107 | + # ------------------------------------------------------------------------ | |
| 108 | + # helper to manage db sessions using the `with` statement, for example | |
| 109 | + # with self.db_session() as s: s.query(...) | |
| 110 | + @contextmanager | |
| 111 | + def db_session(self): | |
| 112 | + try: | |
| 113 | + yield self.Session() | |
| 114 | + finally: | |
| 115 | + self.Session.remove() | ... | ... |
serve.py
| ... | ... | @@ -3,25 +3,17 @@ |
| 3 | 3 | # python standard library |
| 4 | 4 | import os |
| 5 | 5 | import json |
| 6 | -import random | |
| 7 | -from contextlib import contextmanager # `with` statement in db sessions | |
| 8 | 6 | |
| 9 | 7 | # installed libraries |
| 10 | -import bcrypt | |
| 11 | 8 | import markdown |
| 12 | 9 | import tornado.ioloop |
| 13 | 10 | import tornado.web |
| 14 | 11 | import tornado.httpserver |
| 15 | 12 | from tornado import template, gen |
| 16 | 13 | import concurrent.futures |
| 17 | -from sqlalchemy import create_engine | |
| 18 | -from sqlalchemy.orm import sessionmaker, scoped_session | |
| 19 | 14 | |
| 20 | 15 | # this project |
| 21 | -import questions | |
| 22 | -from models import Student # DataBase, | |
| 23 | - | |
| 24 | - | |
| 16 | +from app import LearnApp | |
| 25 | 17 | |
| 26 | 18 | # markdown helper |
| 27 | 19 | def md(text): |
| ... | ... | @@ -34,83 +26,9 @@ def md(text): |
| 34 | 26 | 'markdown.extensions.sane_lists' |
| 35 | 27 | ]) |
| 36 | 28 | |
| 37 | -# A thread pool to be used for password hashing with bcrypt. | |
| 29 | +# A thread pool to be used for password hashing with bcrypt. FIXME and other things? | |
| 38 | 30 | executor = concurrent.futures.ThreadPoolExecutor(2) |
| 39 | 31 | |
| 40 | -# ============================================================================ | |
| 41 | -# LearnApp - application logic | |
| 42 | -# ============================================================================ | |
| 43 | -class LearnApp(object): | |
| 44 | - def __init__(self): | |
| 45 | - print('LearnApp.__init__') | |
| 46 | - self.factory = questions.QuestionFactory() | |
| 47 | - self.factory.load_files(['questions.yaml'], 'demo') # FIXME | |
| 48 | - self.online = {} | |
| 49 | - | |
| 50 | - # connect to database and check registered students | |
| 51 | - engine = create_engine('sqlite:///{}'.format('students.db'), echo=False) | |
| 52 | - self.Session = scoped_session(sessionmaker(bind=engine)) | |
| 53 | - try: | |
| 54 | - with self.db_session() as s: | |
| 55 | - n = s.query(Student).filter(Student.id != '0').count() | |
| 56 | - except Exception as e: | |
| 57 | - print('Database not usable.') | |
| 58 | - raise e | |
| 59 | - else: | |
| 60 | - print('Database has {} students registered.'.format(n)) | |
| 61 | - | |
| 62 | - # ------------------------------------------------------------------------ | |
| 63 | - def login_ok(self, uid, try_pw): | |
| 64 | - print('LearnApp.login') | |
| 65 | - | |
| 66 | - with self.db_session() as s: | |
| 67 | - student = s.query(Student).filter(Student.id == uid).one_or_none() | |
| 68 | - | |
| 69 | - if student is None or student in self.online: | |
| 70 | - # student does not exist | |
| 71 | - return False | |
| 72 | - | |
| 73 | - # hashedtry = yield executor.submit(bcrypt.hashpw, | |
| 74 | - # try_pw.encode('utf-8'), student.password) | |
| 75 | - hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) | |
| 76 | - | |
| 77 | - if hashedtry != student.password: | |
| 78 | - # wrong password | |
| 79 | - return False | |
| 80 | - | |
| 81 | - # success | |
| 82 | - self.online[uid] = { | |
| 83 | - 'name': student.name, | |
| 84 | - 'number': student.id, | |
| 85 | - 'current': None, | |
| 86 | - } | |
| 87 | - print(self.online) | |
| 88 | - return True | |
| 89 | - | |
| 90 | - # ------------------------------------------------------------------------ | |
| 91 | - # logout | |
| 92 | - def logout(self, uid): | |
| 93 | - del self.online[uid] # FIXME save current question? | |
| 94 | - | |
| 95 | - # ------------------------------------------------------------------------ | |
| 96 | - # returns dictionary | |
| 97 | - def next_question(self, uid): | |
| 98 | - # print('next question') | |
| 99 | - # q = self.factory.generate('math-expressions') | |
| 100 | - questions = list(self.factory) | |
| 101 | - q = self.factory.generate(random.choice(questions)) | |
| 102 | - self.online[uid]['current'] = q | |
| 103 | - return q | |
| 104 | - | |
| 105 | - # ------------------------------------------------------------------------ | |
| 106 | - # helper to manage db sessions using the `with` statement, for example | |
| 107 | - # with self.db_session() as s: s.query(...) | |
| 108 | - @contextmanager | |
| 109 | - def db_session(self): | |
| 110 | - try: | |
| 111 | - yield self.Session() | |
| 112 | - finally: | |
| 113 | - self.Session.remove() | |
| 114 | 32 | |
| 115 | 33 | # ============================================================================ |
| 116 | 34 | # WebApplication - Tornado Web Server |
| ... | ... | @@ -121,7 +39,6 @@ class WebApplication(tornado.web.Application): |
| 121 | 39 | (r'/', LearnHandler), |
| 122 | 40 | (r'/login', LoginHandler), |
| 123 | 41 | (r'/logout', LogoutHandler), |
| 124 | - # (r'/learn', LearnHandler), | |
| 125 | 42 | (r'/question', QuestionHandler), |
| 126 | 43 | ] |
| 127 | 44 | settings = { |
| ... | ... | @@ -156,34 +73,26 @@ class BaseHandler(tornado.web.RequestHandler): |
| 156 | 73 | if user in self.learn.online: |
| 157 | 74 | return user |
| 158 | 75 | |
| 159 | -# # ---------------------------------------------------------------------------- | |
| 160 | -# class MainHandler(BaseHandler): | |
| 161 | -# @tornado.web.authenticated | |
| 162 | -# def get(self): | |
| 163 | -# self.redirect('/learn') | |
| 164 | - | |
| 165 | 76 | # ---------------------------------------------------------------------------- |
| 166 | 77 | # /auth/login and /auth/logout |
| 167 | 78 | # ---------------------------------------------------------------------------- |
| 168 | 79 | class LoginHandler(BaseHandler): |
| 169 | 80 | def get(self): |
| 170 | - self.render('login.html') | |
| 81 | + self.render('login.html', error='') | |
| 171 | 82 | |
| 172 | 83 | # @gen.coroutine |
| 173 | 84 | def post(self): |
| 174 | 85 | uid = self.get_body_argument('uid') |
| 175 | 86 | pw = self.get_body_argument('pw') |
| 176 | - print(f'login.post: user={uid}, pw={pw}') | |
| 87 | + # print(f'login.post: user={uid}, pw={pw}') | |
| 177 | 88 | |
| 178 | - x = self.learn.login_ok(uid, pw) | |
| 179 | - print(x) | |
| 180 | - if x: # hashedtry == student.password: | |
| 89 | + if self.learn.login_ok(uid, pw): | |
| 181 | 90 | print('login ok') |
| 182 | 91 | self.set_secure_cookie("user", str(uid)) |
| 183 | 92 | self.redirect(self.get_argument("next", "/")) |
| 184 | 93 | else: |
| 185 | 94 | print('login failed') |
| 186 | - self.render("login.html", error="incorrect password") | |
| 95 | + self.render("login.html", error='Número ou senha incorrectos') | |
| 187 | 96 | |
| 188 | 97 | # ---------------------------------------------------------------------------- |
| 189 | 98 | class LogoutHandler(BaseHandler): |
| ... | ... | @@ -199,65 +108,51 @@ class LogoutHandler(BaseHandler): |
| 199 | 108 | class LearnHandler(BaseHandler): |
| 200 | 109 | @tornado.web.authenticated |
| 201 | 110 | def get(self): |
| 202 | - print('GET /learn') | |
| 203 | - user = self.current_user | |
| 204 | - name = self.application.learn.online[user]['name'] | |
| 205 | - print(' user = '+user) | |
| 206 | - print(self.learn.online[user]['name']) | |
| 207 | - self.render('learn.html', name=name, uid=user) # FIXME | |
| 208 | - # self.learn.online[user]['name'] | |
| 111 | + uid = self.current_user | |
| 112 | + self.render('learn.html', | |
| 113 | + uid=uid, | |
| 114 | + name=self.learn.get_student_name(uid) | |
| 115 | + ) | |
| 116 | + | |
| 209 | 117 | # ---------------------------------------------------------------------------- |
| 210 | 118 | # respond to AJAX to get a JSON question |
| 211 | 119 | class QuestionHandler(BaseHandler): |
| 120 | + templates = { | |
| 121 | + 'checkbox': 'question-checkbox.html', | |
| 122 | + 'radio': 'question-radio.html', | |
| 123 | + 'text': 'question-text.html', | |
| 124 | + 'text_regex': 'question-text.html', | |
| 125 | + 'text_numeric': 'question-text.html', | |
| 126 | + 'textarea': 'question-textarea.html', | |
| 127 | + } | |
| 128 | + | |
| 212 | 129 | @tornado.web.authenticated |
| 213 | 130 | def get(self): |
| 214 | 131 | self.redirect('/') |
| 215 | 132 | |
| 216 | 133 | @tornado.web.authenticated |
| 217 | 134 | def post(self): |
| 218 | - print('---------------> question.post') | |
| 219 | - # experiment answering one question and correct it | |
| 220 | - ref = self.get_body_arguments('question_ref') | |
| 221 | - # print('Reference' + str(ref)) | |
| 222 | - | |
| 135 | + print('================= POST ==============') | |
| 136 | + # ref = self.get_body_arguments('question_ref') | |
| 223 | 137 | user = self.current_user |
| 224 | - userdata = self.learn.online[user] | |
| 225 | - question = userdata['current'] # get current question | |
| 226 | - print('=====================================') | |
| 227 | - print(' ' + str(question)) | |
| 228 | - print('-------------------------------------') | |
| 229 | - | |
| 230 | - if question is not None: | |
| 231 | - answer = self.get_body_arguments('answer') | |
| 232 | - print(' answer = ' + str(answer)) | |
| 233 | - # question['answer'] = ans # insert answer | |
| 234 | - grade = question.correct(answer) # correct answer | |
| 235 | - print(' grade = ' + str(grade)) | |
| 236 | - | |
| 237 | - correct = grade > 0.99999 | |
| 238 | - if correct: | |
| 239 | - question = self.application.learn.next_question(user) | |
| 240 | - | |
| 138 | + answer = self.get_body_arguments('answer') | |
| 139 | + | |
| 140 | + next_question = self.learn.check_answer(user, answer) | |
| 141 | + | |
| 142 | + if next_question is not None: | |
| 143 | + html_out = self.render_string(self.templates[next_question['type']], | |
| 144 | + question=next_question, # dictionary with the question | |
| 145 | + md=md, # function that renders markdown to html | |
| 146 | + ) | |
| 147 | + self.write({ | |
| 148 | + 'html': tornado.escape.to_unicode(html_out), | |
| 149 | + 'correct': True, | |
| 150 | + }) | |
| 241 | 151 | else: |
| 242 | - correct = True # to animate correctly | |
| 243 | - question = self.application.learn.next_question(user) | |
| 244 | - | |
| 245 | - templates = { | |
| 246 | - 'checkbox': 'question-checkbox.html', | |
| 247 | - 'radio': 'question-radio.html', | |
| 248 | - 'text': 'question-text.html', | |
| 249 | - 'text_regex': 'question-text.html', | |
| 250 | - 'text_numeric': 'question-text.html', | |
| 251 | - 'textarea': 'question-textarea.html', | |
| 252 | - } | |
| 253 | - html_out = self.render_string(templates[question['type']], | |
| 254 | - question=question, # the dictionary with the question?? | |
| 255 | - md=md, # passes function that renders markdown to html | |
| 256 | - ) | |
| 257 | - self.write({ | |
| 258 | - 'html': tornado.escape.to_unicode(html_out), | |
| 259 | - 'correct': correct, | |
| 260 | - }) | |
| 152 | + self.write({ | |
| 153 | + 'html': 'None', | |
| 154 | + 'correct': False | |
| 155 | + }) | |
| 261 | 156 | |
| 262 | 157 | |
| 263 | 158 | # ---------------------------------------------------------------------------- |
| ... | ... | @@ -276,5 +171,6 @@ def main(): |
| 276 | 171 | tornado.ioloop.IOLoop.current().stop() |
| 277 | 172 | print('\n--- stop ---') |
| 278 | 173 | |
| 174 | +# ---------------------------------------------------------------------------- | |
| 279 | 175 | if __name__ == "__main__": |
| 280 | 176 | main() |
| 281 | 177 | \ No newline at end of file | ... | ... |
templates/learn.html
| ... | ... | @@ -78,7 +78,7 @@ |
| 78 | 78 | </div> |
| 79 | 79 | |
| 80 | 80 | </form> |
| 81 | -<button class="btn btn-primary" id="submit">Chuta!</button> | |
| 81 | +<button class="btn btn-primary" id="submit">Próxima</button> | |
| 82 | 82 | |
| 83 | 83 | </div> <!-- container --> |
| 84 | 84 | |
| ... | ... | @@ -111,26 +111,27 @@ $.fn.extend({ |
| 111 | 111 | // } |
| 112 | 112 | |
| 113 | 113 | function updateQuestion(response){ |
| 114 | - $("#question_div").html(response["html"]); | |
| 115 | - MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question"]); | |
| 116 | 114 | |
| 117 | - if (response["correct"]) | |
| 115 | + if (response["correct"]) { | |
| 116 | + $("#question_div").html(response["html"]); | |
| 117 | + MathJax.Hub.Queue(["Typeset",MathJax.Hub,"question_div"]); | |
| 118 | + | |
| 119 | + $("input:text").keypress(function (e) { | |
| 120 | + if (e.keyCode == 13) { | |
| 121 | + e.preventDefault(); | |
| 122 | + getQuestion(); | |
| 123 | + } | |
| 124 | + }); | |
| 125 | + $("textarea").keydown(function (e) { | |
| 126 | + if (e.keyCode == 13 && e.shiftKey) { | |
| 127 | + e.preventDefault(); | |
| 128 | + getQuestion(); | |
| 129 | + } | |
| 130 | + }); | |
| 118 | 131 | $('#question_div').animateCSS('pulse'); |
| 132 | + } | |
| 119 | 133 | else |
| 120 | 134 | $('#question_div').animateCSS('shake'); |
| 121 | - | |
| 122 | - $("input:text").keypress(function (e) { | |
| 123 | - if (e.keyCode == 13) { | |
| 124 | - e.preventDefault(); | |
| 125 | - getQuestion(); | |
| 126 | - } | |
| 127 | - }); | |
| 128 | - $("textarea").keydown(function (e) { | |
| 129 | - if (e.keyCode == 13 && e.shiftKey) { | |
| 130 | - e.preventDefault(); | |
| 131 | - getQuestion(); | |
| 132 | - } | |
| 133 | - }); | |
| 134 | 135 | } |
| 135 | 136 | |
| 136 | 137 | function getQuestion() { | ... | ... |
templates/login.html
| ... | ... | @@ -30,6 +30,7 @@ |
| 30 | 30 | <div class="form-group"> |
| 31 | 31 | <input type="text" name="uid" class="form-control" placeholder="Número" required autofocus> |
| 32 | 32 | <input type="password" name="pw" class="form-control" placeholder="Password" required> |
| 33 | + <p> {{ error }} </p> | |
| 33 | 34 | </div> |
| 34 | 35 | <button class="btn btn-primary" type="submit"> |
| 35 | 36 | <i class="fa fa-sign-in" aria-hidden="true"></i> Entrar | ... | ... |