#!/opt/local/bin/python3.6 # python standard library import os import json import random from contextlib import contextmanager # `with` statement in db sessions # installed libraries import bcrypt import markdown import tornado.ioloop import tornado.web from tornado import template, gen import concurrent.futures from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session # this project import questions from models import Student # DataBase, # markdown helper def md(text): return markdown.markdown(text, extensions=[ 'markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.def_list', 'markdown.extensions.sane_lists' ]) # A thread pool to be used for password hashing with bcrypt. executor = concurrent.futures.ThreadPoolExecutor(2) # ============================================================================ # LearnApp - application logic # ============================================================================ class LearnApp(object): def __init__(self): print('LearnApp.__init__') self.factory = questions.QuestionFactory() self.factory.load_files(['questions.yaml'], 'demo') # FIXME self.online = {} # connect to database and check registered students engine = create_engine('sqlite:///{}'.format('students.db'), echo=False) self.Session = scoped_session(sessionmaker(bind=engine)) try: with self.db_session() as s: n = s.query(Student).filter(Student.id != '0').count() except Exception as e: print('Database not usable.') raise e else: print('Database has {} students registered.'.format(n)) # ------------------------------------------------------------------------ def login_ok(self, uid, try_pw): print('LearnApp.login') with self.db_session() as s: student = s.query(Student).filter(Student.id == uid).one_or_none() if student is None or student in self.online: # student does not exist return False # hashedtry = yield executor.submit(bcrypt.hashpw, # try_pw.encode('utf-8'), student.password) hashedtry = bcrypt.hashpw(try_pw.encode('utf-8'), student.password) if hashedtry != student.password: # wrong password return False # success self.online[uid] = { 'name': student.name, 'number': student.id, 'current': None, } print(self.online) return True # ------------------------------------------------------------------------ # returns dictionary def next_question(self, uid): # print('next question') # q = self.factory.generate('math-expressions') questions = list(self.factory) q = self.factory.generate(random.choice(questions)) self.online[uid]['current'] = q return q # ------------------------------------------------------------------------ # helper to manage db sessions using the `with` statement, for example # with self.db_session() as s: s.query(...) @contextmanager def db_session(self): try: yield self.Session() finally: self.Session.remove() # ============================================================================ # WebApplication - Tornado Web Server # ============================================================================ class WebApplication(tornado.web.Application): def __init__(self): handlers = [ (r'/', MainHandler), (r'/login', LoginHandler), (r'/logout', LogoutHandler), (r'/learn', LearnHandler), (r'/question', QuestionHandler), ] settings = { 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 'static_path': os.path.join(os.path.dirname(__file__), 'static'), 'static_url_prefix': '/static/', # this is the default 'xsrf_cookies': False, # FIXME see how to do it... 'cookie_secret': '__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__', # FIXME 'login_url': '/login', 'debug': True, } super().__init__(handlers, **settings) self.learn = LearnApp() # ============================================================================ # Handlers # ============================================================================ # ---------------------------------------------------------------------------- # Base handler common to all handlers. class BaseHandler(tornado.web.RequestHandler): @property def learn(self): return self.application.learn def get_current_user(self): cookie = self.get_secure_cookie("user") if cookie: return cookie.decode('utf-8') # ---------------------------------------------------------------------------- class MainHandler(BaseHandler): @tornado.web.authenticated def get(self): self.redirect('/learn') # ---------------------------------------------------------------------------- # /auth/login and /auth/logout # ---------------------------------------------------------------------------- class LoginHandler(BaseHandler): def get(self): self.render('login.html') # @gen.coroutine def post(self): uid = self.get_body_argument('uid') pw = self.get_body_argument('pw') print(f'login.post: user={uid}, pw={pw}') x = self.learn.login_ok(uid, pw) print(x) if x: # hashedtry == student.password: print('login ok') self.set_secure_cookie("user", str(uid)) self.redirect(self.get_argument("next", "/")) else: print('login failed') self.render("login.html", error="incorrect password") # ---------------------------------------------------------------------------- class LogoutHandler(BaseHandler): @tornado.web.authenticated def get(self): self.clear_cookie('user') self.redirect(self.get_argument('next', '/')) # ---------------------------------------------------------------------------- # /learn # ---------------------------------------------------------------------------- class LearnHandler(BaseHandler): @tornado.web.authenticated def get(self): print('GET /learn') user = self.current_user # name = self.application.learn.online[user] print(' user = '+user) print(self.learn.online) self.render('learn.html', name='dhsjdhsj', uid=user) # FIXME # self.learn.online[user]['name'] # ---------------------------------------------------------------------------- # respond to AJAX to get a JSON question class QuestionHandler(BaseHandler): @tornado.web.authenticated def get(self): self.redirect('/') @tornado.web.authenticated def post(self): print('---------------> question.post') # experiment answering one question and correct it ref = self.get_body_arguments('question_ref') # print('Reference' + str(ref)) user = self.current_user userdata = self.learn.online[user] question = userdata['current'] # get current question print('=====================================') print(' ' + str(question)) print('-------------------------------------') if question is not None: answer = self.get_body_arguments('answer') print(' answer = ' + str(answer)) # question['answer'] = ans # insert answer grade = question.correct(answer) # correct answer print(' grade = ' + str(grade)) correct = grade > 0.99999 if correct: question = self.application.learn.next_question(user) else: correct = True # to animate correctly question = self.application.learn.next_question(user) templates = { 'checkbox': 'question-checkbox.html', 'radio': 'question-radio.html', 'text': 'question-text.html', 'text_regex': 'question-text.html', 'text_numeric': 'question-text.html', 'textarea': 'question-textarea.html', } html_out = self.render_string(templates[question['type']], question=question, # the dictionary with the question?? md=md, # passes function that renders markdown to html ) self.write({ 'html': tornado.escape.to_unicode(html_out), 'correct': correct, }) # ---------------------------------------------------------------------------- def main(): server = WebApplication() server.listen(8080) try: print('--- start ---') tornado.ioloop.IOLoop.current().start() except KeyboardInterrupt: tornado.ioloop.IOLoop.current().stop() print('\n--- stop ---') if __name__ == "__main__": main()