serve.py
8.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/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()} # 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):
r = self.database.test_grades(self.testconf['ref'])
template = self.templates.get_template('results.html')
return template.render(t=self.testconf, results=r)
# --- 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)