#!/usr/bin/env python3.6 # python standard library from os import path import sys import base64 import uuid import logging.config import argparse # user installed libraries 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_to_html # ============================================================================ # 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), # each question (r'/topic/(.+)', TopicHandler), # page for doing a topic (r'/', RootHandler), # show list of topics # (r'/file/(.+)', FileHandler), # FIXME ] 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), title=self.learn.get_title() ) # ---------------------------------------------------------------------------- # Start a given topic: /topic # ---------------------------------------------------------------------------- class TopicHandler(BaseHandler): @tornado.web.authenticated def get(self, topic): uid = self.current_user self.learn.start_topic(uid, topic) self.render('topic.html', uid=uid, name=self.learn.get_student_name(uid), ) # ---------------------------------------------------------------------------- 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): question = self.learn.get_student_question(user) # Question template = self.templates[question['type']] question_html = self.render_string(template, question=question, md=md_to_html) return { 'method': 'new_question', 'params': { 'question': tornado.escape.to_unicode(question_html), 'progress': self.learn.get_student_progress(user), } } def wrong_answer(self, user): progress = self.learn.get_student_progress(user) # in the current topic return { 'method': 'shake', 'params': { 'progress': progress, } } def finished_topic(self, user): # FIXME user unused return { 'method': 'finished_topic', 'params': { # FIXME no html here please! 'question': f'trophy' } } @tornado.web.authenticated def get(self): logging.debug('QuestionHandler.get()') user = self.current_user question = self.learn.get_student_question(user) template = self.templates[question['type']] question_html = self.render_string(template, question=question, md=md_to_html) self.write({ 'method': 'new_question', 'params': { 'question': tornado.escape.to_unicode(question_html), 'progress': self.learn.get_student_progress(user) , } }) # handles answer posted @tornado.web.authenticated def post(self): logging.debug('QuestionHandler.post()') user = self.current_user # check answer and get next question (same, new or None) answer = self.get_body_arguments('answer') # list if not answer: answer = None else: # answers returned in a list. fix depending on question type qtype = self.learn.get_student_question_type(user) if qtype in ('success', 'information', 'info'): # FIXME danger... answer = None elif qtype != 'checkbox': # radio, text, textarea, ... answer = answer[0] grade = self.learn.check_answer(user, answer) question = self.learn.get_student_question(user) if question is None: self.write(self.finished_topic(user)) elif grade > 0.999: self.write(self.new_question(user)) else: self.write(self.wrong_answer(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()