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 | ... | ... |