\n'
OLD_VALUE_COLOUR = 'blue'
UPDATE_EXPLANATION = '' \
% (OLD_VALUE_COLOUR, OLD_VALUE_COLOUR)
# field manipulation
DSP_HEADER = ""
MOD_WEEK = -1
MOD_HEADER = "Other"
WEEKS = range(1,11)
WEEK_HEADER = "Week %d"
class smsdb:
(MIN, MAX, PRECISION, LIMIT) = range(4)
ID_FIELD_NAME = "StudentID"
def __init__(self, smsfile, weeks):
self.smsfile = smsfile
self.weeks = weeks
self.field_titles = []
self.dsp_fields = []
self.mod_fields = []
self.week_fields = [[] for w in range(0, max(weeks)+1)]
self.field_names = []
self.field_types = {}
self.field_attrs = {}
def add(self):
if os.path.isfile(self.smsfile):
weekpatt = re.compile('WEEK')
commentpatt = re.compile('^#')
fieldindex = 0
##for line in map(str.strip, file(self.smsfile).readlines()):
with open(self.smsfile) as f:
for line in f:
line = line.strip()
if line and not commentpatt.match(line):
fieldtitle, displaymode, smsfield = line.split('\t')
self.field_titles.append(fieldtitle)
if displaymode == "SHOW":
self.dsp_fields.append(fieldindex)
elif displaymode == "UPDATE":
self.mod_fields.append(fieldindex)
elif weekpatt.match(displaymode):
week = int(weekpatt.sub("", displaymode))
if (week in self.weeks):
self.week_fields[week].append(fieldindex)
smsname, smstype = smsfield.split(' ')[:2]
smsattr = smsfield.split(' ')[2:]
self.field_names.append(smsname)
self.field_types[smsname] = smstype
self.field_attrs[smsname] = smsattr
fieldindex += 1
def id_field(self):
index = 0
for name in self.field_names:
if name == self.ID_FIELD_NAME:
break
index += 1
return index
def class_field(self):
index = 0
for name in self.field_titles:
if name == "Class":
break
index += 1
return index
def show_class(self):
self.dsp_fields.append(self.class_field())
def field_title(self, index):
return self.field_titles[index]
def field_name(self, index):
return self.field_names[index]
def field_type(self, name):
return self.field_types[name]
def field_attr(self, name):
return self.field_attrs[name]
def field_max(self, name):
attr = self.field_attrs[name]
return attr[self.MAX]
def field_min(self, name):
attr = self.field_attrs[name]
return attr[self.MIN]
def field_precision(self, name):
attr = self.field_attrs[name]
return attr[self.PRECISION]
def field_limit(self, name):
attr = self.field_attrs[name]
return attr[self.LIMIT]
# return the standard header
def getHeader(name, desc, desc2 = '', jscript = ''):
return '''Content-Type: text/html
%s
%s %s
''' % (name, jscript, desc, desc2)
# return the standard footer
def getFooter():
return '
\n\n'
##def clssSort(clss_1, clss_2):
## day_list = ["mon", "tue", "wed", "thu", "fri"]
##
## if clss_1[:3] in day_list and clss_2[:3] in day_list:
## # sort by day if different
## if clss_1[:3] != clss_2[:3]:
## return cmp(day_list.index(clss_1[:3]), day_list.index(clss_2[:3]))
## # else sort by time if different (and then by lab)
## else:
## return cmp(clss_1[3:], clss_2[3:])
## else:
## return cmp(clss_1, clss_2)
def clssSort(clss):
days= ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
clss_day = clss[:3]
if clss_day in days:
return "%s%s" % (days.index(clss_day), clss[3:])
else:
return clss
# return a list of classes and fields from the given roll file contents
# use a dictionary because hashing is fast
def getClasses(db, roll):
class_field = db.class_field()
classes_dictionary = {}
for entry in roll:
classes_dictionary[entry[class_field]] = 1
##classes = classes_dictionary.keys()
classes = list(classes_dictionary.keys())
if SORT_BY_DAY_OF_WEEK:
##classes.sort(clssSort)
classes.sort(key=clssSort)
else:
classes.sort()
return classes
# return selection boxes for class (single) and sms field (multi)
def getClassFieldSelection(classes, clss=None, weeks=[]):
if not clss:
selected = " selected"
else:
selected = ""
html = '''\
''' % GETDATA_SUBMIT
return html
# check if there's valid get data
def parseGetData(data):
##if data.has_key(GETDATA_CLASS):
if GETDATA_CLASS in data:
#clss = data.getfirst(GETDATA_CLASS).lower()
clss = data.getfirst(GETDATA_CLASS)
else:
clss = None
##if data.has_key(GETDATA_WEEKS):
if GETDATA_WEEKS in data:
##weeks = map(int, data.getlist(GETDATA_WEEKS))
weeks = [ int(i) for i in data.getlist(GETDATA_WEEKS) ]
else:
weeks = []
return clss, weeks
# generate an form input from the given student id and field
def getUpdateInput(db, id, field):
field_name = db.field_name(field)
field_type = db.field_type(field_name)
field_attr = db.field_attr(field_name)
if field_type == 'enum':
s = ''
return s
else:
if field_type == 'int' or field_type == 'mark':
width = 2
else:
width = 12
return '' % (int(id), field_name, width)
# return the value from the student dictionary, unless there's a corresponding
# entry in the updates, in which case return that
def getValue(db, student, updates, field):
# check updates for value
# otherwise return the value from the roll
id_field = db.id_field()
key = '%s|%s' % (student[id_field], db.field_name(field))
if key in updates:
value = updates[key]
else:
value = student[field]
return '%s' % (OLD_VALUE_COLOUR, value)
# when no weeks are selected, and select is clicked, then let them know
def noWeeksMistake():
return '
Whoops
\n' \
'You clicked on the %s button ' \
'without selecting any weeks to update.\n' \
'
Please try again.\n' \
% GETDATA_SUBMIT
# generate the field table header
def getFieldHeader(db, field):
field_title = db.field_title(field)
field_name = db.field_name(field)
field_type = db.field_type(field_name)
if (field_type == 'int'):
field_max = db.field_max(field_name)
html = '\t\t
\n' % (field_title, float(field_max), limit)
else:
html = '\t\t
%s
\n' % (field_title)
return html
# generate the field update form
def getFieldUpdateForm(db, classes, roll, clss, weeks):
html = ''
if clss != ALL_CLASS:
html += '
%s
' % clss
else:
db.show_class()
# if no weeks selected, then print error
if not weeks:
return noWeeksMistake()
# show explanation
html += UPDATE_EXPLANATION
# create form
html += '\n'
return html
# check the environment to find out who is logged in and which classes they
# can play with
def getUserAndClasses(classes):
# if the user is in the admin group, then return all classes
##remote_user = string.lower(REMOTE_USER)
remote_user = REMOTE_USER.lower()
remote_user = re.sub('[^a-z0-9]', '', remote_user)
##usernames = string.strip(os.popen("%s %s format='$user|$aliases'" % (ACC,remote_user)).read()).split('|')
usernames = os.popen("%s %s format='$user|$aliases'" % (ACC,remote_user)).read().strip().split('|')
##admins = [ admin.strip() for admin in file(ADMIN_FILE).readlines() ]
with open(ADMIN_FILE) as f:
admins = [ line.strip() for line in f ]
for username in usernames:
if username in admins:
return remote_user, classes
# if the user is in the tutor group, then return their classes
##allocations = map(string.split, file(TUTORS_FILE).readlines())
with open(TUTORS_FILE) as f:
allocations = [ line.split() for line in f ]
tutor_classes = []
for alloc in allocations:
if alloc[1] in usernames:
tutor_classes.append(alloc[0])
return remote_user, tutor_classes
# check if user calls for an update
def updateSubmitted(data):
##return data.has_key(POSTDATA_SUBMIT) and len(data.keys()) > 1
return POSTDATA_SUBMIT in data and len(data.keys()) > 1
# check if the data is valid
def checkDataSubmitted(db, data):
error = ''
for field in data:
value = data[field].value
# skip submit 'field' and '.' and '?'
if field == POSTDATA_SUBMIT \
or value == '.' \
or value == '?':
continue
student_id, field_name = field.split('|')
field_type = db.field_type(field_name)
if field_type == 'int':
try:
imin = int(db.field_min(field_name))
imax = int(db.field_max(field_name))
i = int(value)
if i < imin:
raise ValueError("%d is less than %d" % (i, imin))
if i > imax:
raise ValueError("%d is greater than %d" % (i, imax))
except ValueError:
if not error:
error = '%s
\n' \
% (student_id, field_name, \
fmin, field_type, fmax, limit, \
value)
if error:
error += '\n'
return error
# show a nice message with the error
def dataSubmissionMistake(data_error):
return '
Whoops
\n' \
'Some of the data you entered was invalid:\n' \
'%s\n' \
'Click "Back" in your browser to try again \n' \
'or choose a class above.\n' \
% data_error
# show a nice message explaining how to use
def getUpdateMistake():
return '
Whoops
\n' \
'You clicked on the %s button without modifying any marks.\n' \
'
Click "Back" in your browser to try again ' \
'or choose a class above.\n' \
% POSTDATA_SUBMIT
# return a string with containing the relevant formatted field value
def formatField(db, data, field):
value = data[field].value
# return '.' and '?' without formatting
if value == '.' or value == '?':
return value
field_name = field.split('|')[1]
field_type = db.field_type(field_name)
if field_type == 'int':
return '%d' % int(value)
if field_type == 'mark':
return '%g' % float(value)
return value
# create the update file in a suitable location
def generateUpdateFile(db, data):
update = file(UPDATE_FILE, 'a')
fcntl.flock(update, fcntl.LOCK_EX)
update_data = ""
for entry in data:
if entry != POSTDATA_SUBMIT:
update_data += "%s|%s|\n" % (entry, formatField(db, data, entry))
# we need the extra newline because html eats the last one
update.write("%s\n" % update_data)
update.flush()
fcntl.flock(update, fcntl.LOCK_UN)
update.close()
# show a nice message confirming creation of update file
def getConfirmedMessage():
return '
Confirmed!
\n' \
'Updates are cached and will be put into sms overnight\n'
# main function
def main():
# print the header
print(getHeader("SMS", "SMS Updater", ""))
# read in the field attributes
db = smsdb(SMSFIELD_FILE, WEEKS)
db.add()
# read in the student records from the roll
##roll = [ line.split('|')[:-1] for line in map(string.strip, file(ROLL_FILE).readlines())]
with open(ROLL_FILE) as f:
roll = [ line.strip().split('|')[:-1] for line in f ]
# grab the list of classes from the roll
classes = getClasses(db, roll)
# check which user it is, and filter out the classes that aren't theirs
user, classes = getUserAndClasses(classes)
if len(roll) <= 0:
print("There are no student records.")
elif len(user) <=0:
print("Your are not a valid user.")
elif len(classes) <= 0:
print("You don't have any classes.")
else:
print('
%s
\n' % user)
# read in any get or post data
data = cgi.FieldStorage()
# if there is post data submitted
if updateSubmitted(data):
data_error = checkDataSubmitted(db, data)
if data_error:
# show error
print('%s\n\n%s\n' % (getClassFieldSelection(classes),dataSubmissionMistake(data_error)))
else:
generateUpdateFile(db, data)
print('%s\n\n%s\n\n' % (getClassFieldSelection(classes), getConfirmedMessage()))
# otherwise, show class/field selection menu
else:
# scan get data
clss, weeks = parseGetData(data)
print('%s\n\n' % getClassFieldSelection(classes, clss, weeks))
# if user clicked on update without updating a field, print a warning
##if data.has_key(POSTDATA_SUBMIT):
if POSTDATA_SUBMIT in data:
print('%s\n\n' % getUpdateMistake())
# if there is get data, then show field update part
if clss:
print('%s\n\n' % getFieldUpdateForm(db, classes, roll, clss, weeks))
print(getFooter())
# if being invoked directly, then run main
if __name__ == '__main__':
main()
# vim:ft=python tw=80