#!/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'/change_password', ChangePasswordHandler), (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': self.redirect('/') # FIXME 404 not found? test_id = self.get_query_argument('test_id', None) # FIXME if test_id does not exist... fname = self.testapp.get_json_filename_of_test(test_id) 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) # @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 AdminHandler(BaseHandler): @tornado.web.authenticated def get(self): if self.current_user != '0': self.redirect('/') 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) # --- start 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()