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

# base
from os import path
import sys
import argparse
import logging.config
import json
import base64
import uuid

# packages
import tornado.ioloop
import tornado.web
import tornado.httpserver
from tornado import template, gen

# project
from tools import load_yaml, md_to_html, md_to_html_review
from app import App, AppException


class WebApplication(tornado.web.Application):
    def __init__(self, testapp, debug=False):
        handlers = [
            (r'/login',         LoginHandler),
            (r'/logout',        LogoutHandler),
            (r'/test',          TestHandler),
            (r'/review',        ReviewHandler),
            (r'/admin',         AdminHandler),
            (r'/students_table', StudentsTable), # FIXME
            (r'/static/(.+)',   FileHandler), # FIXME
            (r'/',              RootHandler), # TODO multiple tests
        ]

        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 not needed on private network
            'cookie_secret': base64.b64encode(uuid.uuid4().bytes),
            'login_url': '/login',
            'debug': debug,
        }

        super().__init__(handlers, **settings)
        self.testapp = testapp


# -------------------------------------------------------------------------
# Base handler common to all handlers.
# -------------------------------------------------------------------------
class BaseHandler(tornado.web.RequestHandler):
    @property
    def testapp(self):
        return self.application.testapp

    def get_current_user(self):
        cookie = self.get_secure_cookie('user')
        if cookie:
            return cookie.decode('utf-8')

# -------------------------------------------------------------------------
# /login  and  /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.testapp.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ão autorizado ou número/senha inválido')

# -------------------------------------------------------------------------
class LogoutHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        self.testapp.logout(self.current_user)
        self.clear_cookie('user')
        self.redirect('/')

# -------------------------------------------------------------------------
# FIXME checkit
class FileHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, filename):
        uid = self.current_user
        public_dir = self.learn.get_current_public_dir(uid)   # FIXME!!!
        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()

# -------------------------------------------------------------------------
# FIXME images missing, needs testing
# -------------------------------------------------------------------------
class TestHandler(BaseHandler):
    templates = {
        'radio':        'question-radio.html',
        'checkbox':     'question-checkbox.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',
        'warning':      'question-warning.html',
        'warn':         'question-warning.html',
        'alert':        'question-alert.html',
        'success':      'question-success.html',
    }

    # GET
    @tornado.web.authenticated
    def get(self):
        uid = self.current_user
        t = self.testapp.get_test(uid) or self.testapp.generate_test(uid)
        self.render('test.html', t=t, md=md_to_html, templ=self.templates)


    # POST
    @tornado.web.authenticated
    def post(self):
        uid = self.current_user

        # self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']}
        # build dictionary ans={0: 'answer0', 1:, 'answer1', ...}
        # unanswered questions not included.
        t = self.testapp.get_test(uid)
        ans = {}
        for i, q in enumerate(t['questions']):
            qid = str(i)  # question id
            if 'answered-' + qid in self.request.arguments:
                ans[i] = self.get_body_arguments(qid, None)

                # remove list when it does not make sense...
                if q['type'] == 'radio':
                    if not ans[i]:
                        ans[i] = None
                    else:
                        ans[i] = ans[i][0]
                elif q['type'] in ('text', 'text-regex', 'textarea', 'numeric-interval'):
                    ans[i] = ans[i][0]

        self.testapp.correct_test(uid, ans)
        self.testapp.logout(uid)
        self.clear_cookie('user')

        self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid))


# --- REVIEW -------------------------------------------------------------
class ReviewHandler(BaseHandler):
    _templates = {
        'radio':        'review-question-radio.html',
        'checkbox':     'review-question-checkbox.html',
        'text':         'review-question-text.html',
        'text-regex':   'review-question-text.html',
        'numeric-interval': 'review-question-text.html',
        'textarea':     'review-question-text.html',
        # -- information panels --
        'information':  'question-information.html',
        'info':         'question-information.html',
        'warning':      'question-warning.html',
        'warn':         'question-warning.html',
        'alert':        'question-alert.html',
        'success':      'question-success.html',
    }

    @tornado.web.authenticated
    def get(self):
        uid = self.current_user
        if uid != '0':
            raise tornado.web.HTTPError(404)

        test_id = self.get_query_argument('test_id', None)
        try:
            fname = self.testapp.get_json_filename_of_test(test_id)
        except:
            raise tornado.web.HTTPError(404, 'Test ID not found.')

        try:
            f = open(path.expanduser(fname))
        except FileNotFoundError:  # FIXME EnvironmentError?
            logging.error(f'Cannot find "{fname}" for review.')
        except Exception as e:
            raise e
        else:
            with f:
                t = json.load(f)
                self.render('review.html', t=t, md=md_to_html_review, templ=self._templates)


# --- FILE -------------------------------------------------------------
# class      FIXME
#     @cherrypy.expose
#     @require(name_is('0'))
#     def absfile(self, name):
#         filename = path.abspath(path.join(self.app.get_questions_path(), name))
#         return cherrypy.lib.static.serve_file(filename)


# -------------------------------------------------------------------------
# FIXME this should be a post in the test with command giveup instead of correct...
class GiveupHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        uid = self.current_user
        t = self.testapp.giveup_test(uid)
        self.testapp.logout(uid)

        # --- Show result to student
        self.render('grade.html', t=t, allgrades=self.testapp.get_student_grades_from_all_tests(uid))


# -------------------------------------------------------------------------
# FIXME list available tests
class RootHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        if self.current_user == '0':
            self.redirect('/admin')
        else:
            self.redirect('/test')


# ============================================================================
#   Admin  FIXME
# ============================================================================
class StudentsTable(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        print('StudentsTable')
        if self.current_user != '0':
            raise tornado.web.HTTPError(404)

        data = {'data': self.testapp.get_students_state()}
        self.write(json.dumps(data, default=str))


class AdminHandler(BaseHandler):

    @tornado.web.authenticated
    def get(self):
        if self.current_user != '0':
            raise tornado.web.HTTPError(404)
        self.render('admin.html')


    @tornado.web.authenticated
    def post(self):
        if self.current_user != '0':
            self.redirect('/')

        cmd = self.get_body_argument('cmd', None)
        value = self.get_body_argument('value', None)

        if cmd == 'get_students':
            data = {
                'students': self.testapp.get_students_state(),
                'test': self.testapp.testfactory
            }
            self.write(json.dumps(data, default=str))

        elif cmd == 'allow':
            self.write(self.testapp.allow_student(value))

        elif cmd == 'deny':
            self.write(self.testapp.deny_student(value))

        elif cmd == 'reset_password':
            self.write(self.testapp.reset_password(value))

        elif cmd == 'insert_student':
            value = json.loads(value)
            self.write(self.testapp.insert_new_student(uid=value['number'], name=value['name']))

        else:
            logging.error(f'Unknown command in post: "{cmd}"')



# # ============================================================================
# #   Student webservice
# # ============================================================================
# class StudentWebService(object):

#     @cherrypy.tools.accept(media='application/json') # FIXME
#     def POST(self, **args):
#         # uid = cherrypy.session.get(SESSION_KEY)
#         if args['cmd'] == 'focus':
#             v = json.loads(args['value'])
#             self.app.set_student_focus(uid=args['number'], value=v)



# -------------------------------------------------------------------------
# Tornado web server
# -------------------------------------------------------------------------
def main():
    SERVER_PATH = path.dirname(path.realpath(__file__))
    LOGGER_CONF = path.join(SERVER_PATH, 'config/logger.yaml')

    # --- Commandline argument parsing
    argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.')
    argparser.add_argument('--debug', action='store_true',
        help='Show datastructures when rendering questions')
    argparser.add_argument('--allow-all', action='store_true', help='Students are initially allowed to login (can be denied later)')
    argparser.add_argument('testfile', type=str, nargs='+', help='test/exam in YAML format.') # FIXME only one exam supported at the moment
    arg = argparser.parse_args()

    # --- Setup logging
    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
    filename = path.abspath(path.expanduser(arg.testfile[0]))

    try:
        app = App(filename, vars(arg))
    except AppException:
        logging.critical('Failed to start application.')
        sys.exit(1)

    # --- create web application
    try:
        webapp = WebApplication(app, debug=arg.debug)
    except Exception as e:
        logging.critical('Can\'t start application.')
        raise e

    # --- create webserver
    http_server = tornado.httpserver.HTTPServer(webapp,
        ssl_options={
            "certfile": "certs/cert.crt",
            "keyfile": "certs/cert.key"
        })
    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()