I am writing a simple quiz app using Tornado and MongoDB. The user authentication is done using Facebook and each user is uniquely identified by their Facebook ID. When a user first creates an account, his score will be set to zero and he can access only questions whose ID is less than or equal to his current score. And question IF start from zero.
The questions are accessed with URL and answers also submitted using URL only. Following is the URL scheme for valid for accessing questions:
myapp.com/q_id
myapp.com/1
myapp.com/1/
The following scheme is valid for submitting answers:
myapp.com/q_id/answer
myapp.com/1/hi
myapp.com/1/hi/
So, in summary:
User logs in via Facebook, his Facebook ID will be stored in cookie
self.set_secure_cookie('user_id', str(user['id']))
He will presented with the homepage where his current score is displayed and link to next question by:
myapp.com/score
user_id = self.get_secure_cookie('user_id') if user_id: users = self.application.db.users search_user = users.find_one({'user_id': user_id}) if not search_user: #i.e. new user score = 0 name = self._get_fb_name(user_id) users.save({'user_id': user_id, 'score': score, 'name' : name }) else: score = search_user.get('score') name = search_user.get('name')
The user can access only questions whose ID equal to his current score (i.e. his next challenge) or less than current score (i.e. previously correctly answered questions)
current_score = int(self.get_secure_cookie('score')) if int(q_id) > current_score: self.write('You cannot access this question as your score is less') return
User submits answers as explained in above URL schemes. If answer is correct, then his score is updated in DB.
question_data = questions.find_one({'q_id': q_id}) if not question_data: self.write('Huh, this question no exist') return if not answer: # user is requesting for next question self.render("quest-template.html", qdata = question_data) return else: if answer in question_data['q_ans']: current_score += 1 # update score in db and in cookie self.write('Correct answer, move to next question') return else: self.write('Wrong answer buddy')
Here is the complete Tornado app code:
#!/usr/bin/env python
import os.path
import os
import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
import pymongo
from pymongo import MongoClient
import requests
import views
from settings import *
define("port", default=8080, help="run on the given port", type=int)
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/', MainHandler),
(r'/auth/login', LoginHandler),
(r'/auth/logout', LogoutHandler),
(r'/(\d+)(/?\w*)', Q0Handler)
]
settings = application_handler_setttings
conn = MongoClient()
self.db = conn["mydb"]
tornado.web.Application.__init__(self, handlers, **settings)
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
if self.get_secure_cookie('user_name'):
return True
return None
class MainHandler(tornado.web.RequestHandler):
def get(self):
user_id = self.get_secure_cookie('user_id')
if user_id:
users = self.application.db.users
search_user = users.find_one({'user_id': user_id})
if not search_user:
score = 0
name = self._get_fb_name(user_id)
users.save({'user_id': user_id,
'score': score,
'name' : name
})
else:
score = search_user.get('score')
name = search_user.get('name')
self.set_secure_cookie('score', str(score))
self.render("index.html", user_name = name, score = score)
else:
self.render("login.html")
def _get_fb_name(self, user_id):
fb_graph_api_url = 'https://graph.facebook.com/%s' % user_id
r = requests.get(fb_graph_api_url)
return dict(r.json()).get('name')
class Q0Handler(BaseHandler):
@tornado.web.authenticated
def get(self, q_id, answer=None):
answer = answer[1:]
questions = self.application.db.questions
users = self.application.db.users
user_id = self.get_secure_cookie('user_id')
current_score = int(self.get_secure_cookie('score'))
if int(q_id) > current_score:
self.write('You cannot access this question as your score is less')
return
question_data = questions.find_one({'q_id': q_id})
if not question_data:
self.write('Huh, this question no exist')
return
if not answer:
self.render("quest-template.html", qdata = question_data)
return
else:
if answer in question_data['q_ans']:
current_score += 1
self.set_secure_cookie('score', str(current_score))
users.update({'user_id': user_id}, {'$set': {'score': current_score}})
self.write('Correct answer, move to next question')
return
else:
self.write('Wrong answer buddy')
class LoginHandler(tornado.web.RequestHandler, tornado.auth.FacebookGraphMixin):
@tornado.web.asynchronous
def get(self):
user_id = self.get_secure_cookie('user_id')
if self.get_argument('code', None):
self.get_authenticated_user(
redirect_uri=redirect_url,
client_id=self.settings['facebook_api_key'],
client_secret=self.settings['facebook_secret'],
code=self.get_argument('code'),
callback=self.async_callback(self._on_facebook_login))
return
elif self.get_secure_cookie('access_token'):
self.redirect('/')
self.authorize_redirect(
redirect_uri=redirect_url,
client_id=self.settings['facebook_api_key'],
extra_params={'scope': 'user_photos, publish_stream'}
)
def _on_facebook_login(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, 'Facebook authentication failed')
self.set_secure_cookie('user_id', str(user['id']))
self.set_secure_cookie('user_name', str(user['name']))
self.set_secure_cookie('access_token', str(user['access_token']))
self.redirect('/')
class LogoutHandler(tornado.web.RequestHandler):
def get(self):
self.clear_all_cookies()
self.render('logout.html')
def main():
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
- How do I improve my regex and is there any to avoid
answer = answer[1:]
? Currently my regex is so that the second parameteranswer
also contains leading slash. To remove it I am using slicing. - At every correct answer, I am updating the database and also the cookie. However, when I need to check his score, I am only seeing the cookie. Is that bad?
- Earlier, I was not updating the database at every current answer, instead I was saving it in just cookie. But when user logs out, I save the scores to the database. Is this the right/suggested way?