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 @@ | @@ -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,25 +3,17 @@ | ||
| 3 | # python standard library | 3 | # python standard library |
| 4 | import os | 4 | import os |
| 5 | import json | 5 | import json |
| 6 | -import random | ||
| 7 | -from contextlib import contextmanager # `with` statement in db sessions | ||
| 8 | 6 | ||
| 9 | # installed libraries | 7 | # installed libraries |
| 10 | -import bcrypt | ||
| 11 | import markdown | 8 | import markdown |
| 12 | import tornado.ioloop | 9 | import tornado.ioloop |
| 13 | import tornado.web | 10 | import tornado.web |
| 14 | import tornado.httpserver | 11 | import tornado.httpserver |
| 15 | from tornado import template, gen | 12 | from tornado import template, gen |
| 16 | import concurrent.futures | 13 | import concurrent.futures |
| 17 | -from sqlalchemy import create_engine | ||
| 18 | -from sqlalchemy.orm import sessionmaker, scoped_session | ||
| 19 | 14 | ||
| 20 | # this project | 15 | # this project |
| 21 | -import questions | ||
| 22 | -from models import Student # DataBase, | ||
| 23 | - | ||
| 24 | - | 16 | +from app import LearnApp |
| 25 | 17 | ||
| 26 | # markdown helper | 18 | # markdown helper |
| 27 | def md(text): | 19 | def md(text): |
| @@ -34,83 +26,9 @@ def md(text): | @@ -34,83 +26,9 @@ def md(text): | ||
| 34 | 'markdown.extensions.sane_lists' | 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 | executor = concurrent.futures.ThreadPoolExecutor(2) | 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 | # WebApplication - Tornado Web Server | 34 | # WebApplication - Tornado Web Server |
| @@ -121,7 +39,6 @@ class WebApplication(tornado.web.Application): | @@ -121,7 +39,6 @@ class WebApplication(tornado.web.Application): | ||
| 121 | (r'/', LearnHandler), | 39 | (r'/', LearnHandler), |
| 122 | (r'/login', LoginHandler), | 40 | (r'/login', LoginHandler), |
| 123 | (r'/logout', LogoutHandler), | 41 | (r'/logout', LogoutHandler), |
| 124 | - # (r'/learn', LearnHandler), | ||
| 125 | (r'/question', QuestionHandler), | 42 | (r'/question', QuestionHandler), |
| 126 | ] | 43 | ] |
| 127 | settings = { | 44 | settings = { |
| @@ -156,34 +73,26 @@ class BaseHandler(tornado.web.RequestHandler): | @@ -156,34 +73,26 @@ class BaseHandler(tornado.web.RequestHandler): | ||
| 156 | if user in self.learn.online: | 73 | if user in self.learn.online: |
| 157 | return user | 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 | # /auth/login and /auth/logout | 77 | # /auth/login and /auth/logout |
| 167 | # ---------------------------------------------------------------------------- | 78 | # ---------------------------------------------------------------------------- |
| 168 | class LoginHandler(BaseHandler): | 79 | class LoginHandler(BaseHandler): |
| 169 | def get(self): | 80 | def get(self): |
| 170 | - self.render('login.html') | 81 | + self.render('login.html', error='') |
| 171 | 82 | ||
| 172 | # @gen.coroutine | 83 | # @gen.coroutine |
| 173 | def post(self): | 84 | def post(self): |
| 174 | uid = self.get_body_argument('uid') | 85 | uid = self.get_body_argument('uid') |
| 175 | pw = self.get_body_argument('pw') | 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 | print('login ok') | 90 | print('login ok') |
| 182 | self.set_secure_cookie("user", str(uid)) | 91 | self.set_secure_cookie("user", str(uid)) |
| 183 | self.redirect(self.get_argument("next", "/")) | 92 | self.redirect(self.get_argument("next", "/")) |
| 184 | else: | 93 | else: |
| 185 | print('login failed') | 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 | class LogoutHandler(BaseHandler): | 98 | class LogoutHandler(BaseHandler): |
| @@ -199,65 +108,51 @@ class LogoutHandler(BaseHandler): | @@ -199,65 +108,51 @@ class LogoutHandler(BaseHandler): | ||
| 199 | class LearnHandler(BaseHandler): | 108 | class LearnHandler(BaseHandler): |
| 200 | @tornado.web.authenticated | 109 | @tornado.web.authenticated |
| 201 | def get(self): | 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 | # respond to AJAX to get a JSON question | 118 | # respond to AJAX to get a JSON question |
| 211 | class QuestionHandler(BaseHandler): | 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 | @tornado.web.authenticated | 129 | @tornado.web.authenticated |
| 213 | def get(self): | 130 | def get(self): |
| 214 | self.redirect('/') | 131 | self.redirect('/') |
| 215 | 132 | ||
| 216 | @tornado.web.authenticated | 133 | @tornado.web.authenticated |
| 217 | def post(self): | 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 | user = self.current_user | 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 | else: | 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,5 +171,6 @@ def main(): | ||
| 276 | tornado.ioloop.IOLoop.current().stop() | 171 | tornado.ioloop.IOLoop.current().stop() |
| 277 | print('\n--- stop ---') | 172 | print('\n--- stop ---') |
| 278 | 173 | ||
| 174 | +# ---------------------------------------------------------------------------- | ||
| 279 | if __name__ == "__main__": | 175 | if __name__ == "__main__": |
| 280 | main() | 176 | main() |
| 281 | \ No newline at end of file | 177 | \ No newline at end of file |
templates/learn.html
| @@ -78,7 +78,7 @@ | @@ -78,7 +78,7 @@ | ||
| 78 | </div> | 78 | </div> |
| 79 | 79 | ||
| 80 | </form> | 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 | </div> <!-- container --> | 83 | </div> <!-- container --> |
| 84 | 84 | ||
| @@ -111,26 +111,27 @@ $.fn.extend({ | @@ -111,26 +111,27 @@ $.fn.extend({ | ||
| 111 | // } | 111 | // } |
| 112 | 112 | ||
| 113 | function updateQuestion(response){ | 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 | $('#question_div').animateCSS('pulse'); | 131 | $('#question_div').animateCSS('pulse'); |
| 132 | + } | ||
| 119 | else | 133 | else |
| 120 | $('#question_div').animateCSS('shake'); | 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 | function getQuestion() { | 137 | function getQuestion() { |
templates/login.html
| @@ -30,6 +30,7 @@ | @@ -30,6 +30,7 @@ | ||
| 30 | <div class="form-group"> | 30 | <div class="form-group"> |
| 31 | <input type="text" name="uid" class="form-control" placeholder="Número" required autofocus> | 31 | <input type="text" name="uid" class="form-control" placeholder="Número" required autofocus> |
| 32 | <input type="password" name="pw" class="form-control" placeholder="Password" required> | 32 | <input type="password" name="pw" class="form-control" placeholder="Password" required> |
| 33 | + <p> {{ error }} </p> | ||
| 33 | </div> | 34 | </div> |
| 34 | <button class="btn btn-primary" type="submit"> | 35 | <button class="btn btn-primary" type="submit"> |
| 35 | <i class="fa fa-sign-in" aria-hidden="true"></i> Entrar | 36 | <i class="fa fa-sign-in" aria-hidden="true"></i> Entrar |