Commit 894bdd0526be2fad60ac19702559c98980020848

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

- moved all application logic to app.py

- login error message on wrong user/password
- general cleanup
app.py 0 → 100644
@@ -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()
@@ -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