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