#!/usr/bin/env python3 # -*- 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 from os import path # path where this file is located SERVER_PATH = path.dirname(path.realpath(__file__)) TEMPLATES_DIR = path.join(SERVER_PATH, 'templates') # 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_DIR], 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 @require() def results(self): if self.testconf.get('practice', False): uid = cherrypy.session.get('userid') name = cherrypy.session.get('name') r = self.database.test_grades(self.testconf['ref']) template = self.templates.get_template('/results.html') return template.render(t=self.testconf, results=r, name=name, uid=uid) 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.') serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) argparser.add_argument('--server', default=serverconf_file, 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/exam in YAML format.') # FIXME only one exam supported at the moment 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']) # FIXME problems with UnicodeEncodeError print('- Database: %s' % testconf['database']) print('- Loaded %i questions from:' % len(testconf['questions'])) print(' path: %s' % testconf['questions_dir']) print(' files: %s' % ', '.join(testconf['files'])) print('-' * 79) print('- Starting server...') # --- controller root = Root(testconf) # --- Mount and run server cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) cherrypy.quickstart(root, '/', config=arg.server) cherrypy.log('Terminated OK ------------------------', 'APPLICATION') print('\n- Server terminated OK') print('=' * 79)