#!/usr/bin/env python3 # create sms update files # (requires output of getsturec.pl to exist in cwd, called roll.txt) import sys, os, cgi, re, string, subprocess, fcntl import cgitb; cgitb.enable() ACC = "/usr/bin/acc" # config TUTORS_FILE = "tutors" ADMIN_FILE = "admins" SMSFIELD_FILE = "smsfield" ROLL_FILE = "roll.txt" UPDATE_FILE = "web_updater.upd" SIZE_WEEK_SELECT = 5 SORT_BY_DAY_OF_WEEK = True # if false, then sort alphanumerically (friday first) # convenience SCRIPT_URI = os.environ["REQUEST_URI"] SCRIPT_FILENAME = os.environ["SCRIPT_FILENAME"] REMOTE_USER = os.environ["REMOTE_USER"] GETDATA_CLASS = "class" ALL_CLASS = "all" GETDATA_WEEKS = "weeks" GETDATA_SUBMIT = "Select" POSTDATA_SUBMIT = "Update" SMS_UPDATE_FIELD = "sms_update" POSTDATA_CONFIRM = "Confirm" TABLE_START = '
\n' TABLE_END = '
\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
''' % (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%s
(%d)\n' % (field_title, int(field_max)) elif (field_type == 'mark'): field_max = db.field_max(field_name) field_limit = db.field_limit(field_name) if field_limit == "soft": limit = '+' else: limit = '' html = '\t\t%s
(%g%s)\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' % SCRIPT_URI # start table html += TABLE_START html += '\t\n' # print first table header row html += '\t\n' \ '\t\t%s\n' % (len(db.dsp_fields), DSP_HEADER) for week in weeks: if week == MOD_WEEK: if len(db.mod_fields) > 0: html += '\t\t%s\n' % (len(db.mod_fields), MOD_HEADER) else: if len(db.week_fields[week]) > 0: html += '\t\t%s\n' % (len(db.week_fields[week]), WEEK_HEADER % week) html += '\t\n' # print second table header row html += '\t\n' for field in db.dsp_fields: html += '\t\t%s\n' % db.field_title(field) for week in weeks: if week == MOD_WEEK: for field in db.mod_fields: html += getFieldHeader(db, field) else: if len(db.week_fields[week]) > 0: for field in db.week_fields[week]: html += getFieldHeader(db, field) html += '\t\n' html += '\t\n' # grab the lines from the update file (use a dictionary for speed) updates = {} if os.path.isfile(UPDATE_FILE): ##for line in file(UPDATE_FILE).readlines(): with open(UPDATE_FILE) as f: for line in f: line = line.strip() if line: student_id, field, value = line.split('|')[:3] updates['%s|%s' % (student_id, field)] = value # scan through roll, and print rows for each relevant student html += '\t\n' class_field = db.class_field() id_field = db.id_field() for student in roll: if student[class_field] in classes \ and clss == ALL_CLASS or clss == student[class_field]: colspan = 0 html += '\t\n' for field in db.dsp_fields: html += '\t\t%s\n' % student[field] colspan += 1 for week in weeks: if week == MOD_WEEK: for field in db.mod_fields: html += '\t\t%s %s\n' \ % (getUpdateInput(db, student[id_field], field), getValue(db, student, updates, field)) colspan += 1 else: for field in db.week_fields[week]: html += '\t\t%s %s\n' \ % (getUpdateInput(db, student[id_field], field), getValue(db, student, updates, field)) colspan += 1 html += '\t\n' html += '\t\n' # finish table html += TABLE_END # put in update button html += '
\n' \ % (POSTDATA_SUBMIT, POSTDATA_SUBMIT) # finish 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%sField' \ 'Required Type' \ 'Submitted Value\n' \ % (TABLE_START, db.field_title(db.id_field())) error += '%s%s' \ '
%d <= %s <= %d
' \ '
%s
\n' \ % (student_id, field_name, \ imin, field_type, imax, \ value) elif field_type == 'mark': try: fmin = float(db.field_min(field_name)) fmax = float(db.field_max(field_name)) limit = db.field_limit(field_name) f = float(value) if limit == 'hard': if f < fmin: raise ValueError("%g is less than %g" % (f, fmin)) if f > fmax: raise ValueError("%g is greater than %g" % (f, fmax)) except ValueError: if not error: error = '%s%sField' \ 'Required Type' \ 'Submitted Value\n' \ % (TABLE_START, db.field_title(db.id_field())) error += '%s%s' \ '
%g <= %s <= %g (%s limits)
' \ '
%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