serve.py 8.15 KB
#!/usr/bin/env python3.4
# -*- coding: utf-8 -*-

# The program runs a web server where students login to answer a sequence of questions.
# Their grades are automatically calculated and stored in a sqlite3 database.

import cherrypy
from mako.lookup import TemplateLookup
import argparse

# my code
from myauth import AuthController, require
import test
import database

# ============================================================================
#   Classes that respond to HTTP
# ============================================================================
class Root(object):

    def __init__(self, testconf):
        self.testconf = testconf  # base test dict (not instance)
        self.database = database.Database(testconf['database'])
        self.auth = AuthController(database=testconf['database'])
        self.templates = TemplateLookup(directories=['templates'], input_encoding='utf-8')
        self.tags = {'online': set(), 'finished': set()}  # FIXME should be in application, not server

    # --- DEFAULT ------------------------------------------------------------
    # any path, e.g. /xpto/aargh is redirected to the test
    # (and possibly through the login first)
    @cherrypy.expose
    def default(self, *args):
        raise cherrypy.HTTPRedirect('/test')

    # --- LOGOUT ------------------------------------------------------------
    @cherrypy.expose
    @require()
    def logout(self):
        uid = cherrypy.session.get('userid')
        self.tags['online'].discard(uid)
        cherrypy.lib.sessions.expire()  # session coockie expires client side
        cherrypy.session['userid'] = cherrypy.request.login = None
        cherrypy.log.error('Student {0} logged out.'.format(uid), 'APPLICATION')
        raise cherrypy.HTTPRedirect('/')

    # --- STUDENTS -----------------------------------------------------------
    @cherrypy.expose
    @require()
    def students(self, **reset_pw):
        uid = cherrypy.session.get('userid')
        if uid != '0':
            raise cherrypy.HTTPRedirect('/')  #FIXME use authorization @require(admin)

        if reset_pw:
            self.database.student_reset_pw(reset_pw)
            for num in reset_pw:
                cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION')

        students = self.database.get_students()
        template = self.templates.get_template('students.html')
        return template.render(students=students, tags=self.tags)

    # --- RESULTS ------------------------------------------------------------
    @cherrypy.expose
    def results(self):
        if self.testconf.get('practice', False):
            r = self.database.test_grades(self.testconf['ref'])
            template = self.templates.get_template('results.html')
            return template.render(t=self.testconf, results=r)
        else:
            raise cherrypy.HTTPRedirect('/')

    # --- TEST ---------------------------------------------------------------
    @cherrypy.expose
    @require()
    def test(self):
        # Get student number and assigned questions from current session.
        # If it's the first time, create instance of the test and register the
        # time.
        uid = cherrypy.session.get('userid')
        name = cherrypy.session.get('name')
        t = cherrypy.session.get('test', None)
        if t is None:
            # create instance and add the name and number of the student
            cherrypy.session['test'] = t = test.Test(self.testconf)
            t['number'] = uid
            t['name'] = name
            self.tags['online'].add(uid)  # track logged in students

        # Generate question
        template = self.templates.get_template('test.html')
        return template.render(t=t, questions=t['questions'])

    # --- CORRECT ------------------------------------------------------------
    @cherrypy.expose
    @require()
    def correct(self, **kwargs):
        '''Receives a dictionary {'question_id': 'answer', ...} submited by
        the student, corrects and saves the answers, computes the final grade
        of the test and stores in the database.'''

        # shouldn't be here if there is no test
        t = cherrypy.session.get('test')
        if t is None:
            raise cherrypy.HTTPRedirect('/test')

        # each question that is marked to be classified must have an answer.
        # variable `ans` contains the answers to be corrected
        ans = {}
        for q in t['questions']:
            if 'answered-' + q['ref'] in kwargs:
                # HACK: checkboxes in html return None instead of an empty list
                # we have to manualy replace by []
                default_ans = [] if q['type'] == 'checkbox' else None
                ans[q['ref']] = kwargs.get(q['ref'], default_ans)

        # store the answers in the Test, correct it, save JSON and
        # store results in the database
        t.update_answers(ans)
        t.correct()

        if t['save_answers']:
            t.save_json(self.testconf['answers_dir'])
        self.database.save_test(t)

        if t['practice']:
            # ---- Repeat the test ----
            cherrypy.log.error('Student %s terminated with grade = %.2f points.' %
                               (t['number'], t['grade']), 'APPLICATION')
            raise cherrypy.HTTPRedirect('/test')

        else:
            # ---- Expire session ----
            self.tags['online'].discard(t['number'])
            self.tags['finished'].add(t['number'])
            cherrypy.lib.sessions.expire()  # session coockie expires client side
            cherrypy.session['userid'] = cherrypy.request.login = None
            cherrypy.log.error('Student %s terminated with grade = %.2f points.' %
                               (t['number'], t['grade']), 'APPLICATION')

            # ---- Show result to student ----
            grades = self.database.student_grades(t['number'])
            template = self.templates.get_template('grade.html')
            return template.render(t=t, allgrades=grades)

# ============================================================================
def parse_arguments():
    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('--server', default='conf/server.conf', type=str, help='server configuration file')
    argparser.add_argument('--debug', action='store_true',
        help='Show datastructures when rendering questions')
    argparser.add_argument('--show_ref', action='store_true',
        help='Show filename and ref field for each question')
    argparser.add_argument('--show_points', action='store_true',
        help='Show normalized points for each question')
    argparser.add_argument('--show_hints', action='store_true',
        help='Show hints in questions, if available')
    argparser.add_argument('--save_answers', action='store_true',
        help='Saves answers in JSON format')
    argparser.add_argument('--practice', action='store_true',
        help='Show correction results and allow repetitive resubmission of the test')
    argparser.add_argument('testfile', type=str, nargs='+', help='test in YAML format.')
    return argparser.parse_args()

# ============================================================================
if __name__ == '__main__':
    # --- parse command line arguments and build base test
    arg = parse_arguments()
    testconf = test.read_configuration(arg.testfile[0], debug=arg.debug, show_points=arg.show_points, show_hints=arg.show_hints, save_answers=arg.save_answers, practice=arg.practice, show_ref=arg.show_ref)

    print('=' * 79)
    print('- Title:      %s' % testconf['title'])
    print('- StudentsDB: %s' % testconf['database'])
    print('- Questions:\n     ', '\n      '.join(testconf['files']))
    print('      (%i questions read)' % len(testconf['questions']))
    print('-' * 79)
    print('- Starting server...')

    # --- controller
    root = Root(testconf)

    # --- Mount and run server.
    cherrypy.quickstart(root, '/', config=arg.server)
    cherrypy.log.error('Terminated OK ------------------------', 'APPLICATION')
    print('\n- Server terminated OK')
    print('=' * 79)