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 |