serve.py 9.8 KB
#!/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
import tornado.httpserver
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

    # ------------------------------------------------------------------------
    # logout
    def logout(self, uid):
        del self.online[uid]  # FIXME save current question?

    # ------------------------------------------------------------------------
    # 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'/',          LearnHandler),
            (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:
            user = cookie.decode('utf-8')
            # FIXME if the cookie exists but user is not in learn.online, this will force new login and store new (duplicate?) cookie. is this correct??
            if user in self.learn.online:
                return user

# # ----------------------------------------------------------------------------
# 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.learn.logout(self.current_user)
        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]['name']
        print('  user = '+user)
        print(self.learn.online[user]['name'])
        self.render('learn.html', name=name, 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():
    webapp = WebApplication()
    http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={
        "certfile": "certs/cert.pem",
        "keyfile": "certs/key.pem"
        })
    http_server.listen(8443)

    try:
        print('--- start ---')
        tornado.ioloop.IOLoop.current().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.current().stop()
        print('\n--- stop ---')

if __name__ == "__main__":
    main()