serve.py 10.7 KB
#!/usr/bin/env python3.6

# python standard library
from os import path
import sys
import json
import base64
import uuid
import concurrent.futures
import logging.config
import argparse

# user installed libraries
import markdown
import tornado.ioloop
import tornado.web
import tornado.httpserver
from tornado import template, gen

# this project
from learnapp import LearnApp
from tools import load_yaml, md


# ============================================================================
# WebApplication - Tornado Web Server
# ============================================================================
class WebApplication(tornado.web.Application):
    def __init__(self, learnapp, debug=False):
        handlers = [
            (r'/login',         LoginHandler),
            (r'/logout',        LogoutHandler),
            (r'/change_password', ChangePasswordHandler),
            (r'/question',      QuestionHandler),
            (r'/',              RootHandler),
            (r'/topic/(.+)',    TopicHandler),
            (r'/(.+)',          FileHandler),
        ]
        settings = {
            'template_path': path.join(path.dirname(__file__), 'templates'),
            'static_path':   path.join(path.dirname(__file__), 'static'),
            'static_url_prefix': '/static/',
            'xsrf_cookies': False, # FIXME see how to do it...
            'cookie_secret': base64.b64encode(uuid.uuid4().bytes),
            'login_url': '/login',
            'debug': debug,
        }
        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')
            return user
            # 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

# ----------------------------------------------------------------------------
# /auth/login  and  /auth/logout
# ----------------------------------------------------------------------------
class LoginHandler(BaseHandler):
    def get(self):
        self.render('login.html', error='')

    # @gen.coroutine
    def post(self):
        uid = self.get_body_argument('uid')
        pw = self.get_body_argument('pw')

        if self.learn.login(uid, pw):
            self.set_secure_cookie("user", str(uid), expires_days=30)
            self.redirect(self.get_argument("next", "/"))
        else:
            self.render("login.html", error='Número ou senha incorrectos')

# ----------------------------------------------------------------------------
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', '/'))

# ----------------------------------------------------------------------------
class ChangePasswordHandler(BaseHandler):
    @tornado.web.authenticated
    def post(self):
        uid = self.current_user
        pw = self.get_body_arguments('new_password')[0];

        if self.learn.change_password(uid, pw):
            notification = tornado.escape.to_unicode(self.render_string('notification.html', type='success', msg='A password foi alterada!'))
        else:
            notification = tornado.escape.to_unicode(self.render_string('notification.html', type='danger', msg='A password não foi alterada!'))
        self.write({'msg': notification})

# ----------------------------------------------------------------------------
# Main page:  /
# Shows a list of topics and proficiency (stars, locked).
# ----------------------------------------------------------------------------
class RootHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        uid = self.current_user
        self.render('maintopics.html',
            uid=uid,
            name=self.learn.get_student_name(uid),
            state=self.learn.get_student_state(uid)
            )

# ----------------------------------------------------------------------------
# Start a given topic:  /topic
# ----------------------------------------------------------------------------
class TopicHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, topic):
        uid = self.current_user
        # topic = self.get_query_argument('topic', default='')

        self.learn.start_topic(uid, topic)
        self.render('topic.html',
            uid=uid,
            name=self.learn.get_student_name(uid),
            title=self.learn.get_title()
            )


# ----------------------------------------------------------------------------
class FileHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, filename):
        uid = self.current_user
        public_dir = self.learn.get_current_public_dir(uid)
        filepath = path.expanduser(path.join(public_dir, filename))
        try:
            f = open(filepath, 'rb')
        except FileNotFoundError:
            raise tornado.web.HTTPError(404)
        else:
            self.write(f.read())
            f.close()

# ----------------------------------------------------------------------------
# respond to AJAX to get a JSON question
# ----------------------------------------------------------------------------
class QuestionHandler(BaseHandler):
    templates = {
        'checkbox':     'question-checkbox.html',
        'radio':        'question-radio.html',
        'text':         'question-text.html',
        'text-regex':   'question-text.html',
        'numeric-interval': 'question-text.html',
        'textarea':     'question-textarea.html',
        # -- information panels --
        'information':  'question-information.html',
        'info':         'question-information.html',
        'success':      'question-success.html',
        # 'warning':    '', FIXME
        # 'warn':       '', FIXME
        # 'alert':      '', FIXME
    }

    def new_question(self, user):
        state = self.learn.get_student_state(user)  # all topics [('a', 0.1), ...]
        current_topic = self.learn.get_student_topic(user) # str
        progress = self.learn.get_student_progress(user) # float
        question = self.learn.get_student_question(user) # dict?

        question_html = self.render_string(self.templates[question['type']],question=question, md=md)
        topics_html = self.render_string('topics.html', state=state, current_topic=current_topic, gettopicname=self.learn.get_topic_name)

        return {
            'method': 'new_question',
            'params': {
                'question': tornado.escape.to_unicode(question_html),
                'state': tornado.escape.to_unicode(topics_html),
                'progress': progress,
                }
            }

    def shake(self, user):
        progress = self.learn.get_student_progress(user)  # in the current topic
        return {
            'method': 'shake',
            'params': {
                'progress': progress,
                }
            }

    def finished_topic(self, user):
        state = self.learn.get_student_state(user)        # all topics
        current_topic = self.learn.get_student_topic(uid)

        topics_html = self.render_string('topics.html',
            state=state,
            current_topic=current_topic,
            topicname=self.learn.get_topic_name,    # translate ref to names
            )
        return {
            'method': 'finished_topic',
            'params': {
                'state': tornado.escape.to_unicode(topics_html),
                }
            }

    @tornado.web.authenticated
    def get(self, topic=''):
        self.write(self.new_question(self.current_user))

    # handles answer posted
    @tornado.web.authenticated
    def post(self):
        user = self.current_user
        answer = self.get_body_arguments('answer')
        grade = self.learn.check_answer(user, answer)
        question = self.learn.get_student_question(user)  # same, new or None

        if question is None:
            self.write(self.finished_topic(user))
        elif grade > 0.999:
            self.write(self.new_question(user))
        else:
            self.write(self.shake(user))


# -------------------------------------------------------------------------
# Tornado web server
# ----------------------------------------------------------------------------
def main():
    # --- Commandline argument parsing
    argparser = argparse.ArgumentParser(description='Server for online learning. Enrolled students and topics have to be previously configured. Please read the documentation included with this software before running the server.')
    argparser.add_argument('conffile', type=str, nargs='+',
        help='Topics configuration file in YAML format.') # FIXME only one
    argparser.add_argument('--debug', action='store_true',
        help='Enable debug messages.')
    arg = argparser.parse_args()

    # --- Setup logging
    logger_file = 'logger-debug.yaml' if arg.debug else 'logger.yaml'
    SERVER_PATH = path.dirname(path.realpath(__file__))
    LOGGER_CONF = path.join(SERVER_PATH, f'config/{logger_file}')

    try:
        logging.config.dictConfig(load_yaml(LOGGER_CONF))
    except:
        print('An error ocurred while setting up the logging system.')
        sys.exit(1)
    logging.info('====================================================')

    # --- start application
    logging.info('Starting App.')
    learnapp = LearnApp(arg.conffile[0])

    # --- create web application
    logging.info('Starting Web App (tornado).')
    try:
        webapp = WebApplication(learnapp, debug=arg.debug)
    except Exception as e:
        logging.critical('Failed to start application.')
        raise e

    # --- create webserver
    http_server = tornado.httpserver.HTTPServer(webapp,
        ssl_options={
            "certfile": "certs/cert.pem",
            "keyfile": "certs/key.pem"
        })
    http_server.listen(8443)

    # --- run webserver
    logging.info('Webserver running...')

    try:
        tornado.ioloop.IOLoop.current().start()  # running...
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.current().stop()

    logging.critical('Webserver stopped.')

# ----------------------------------------------------------------------------
if __name__ == "__main__":
    main()