#!/usr/bin/env python # # Display student marks stored in a CSV file on the course account. # # Constraints: # MARK_FILE must exist and be CSV-formatted as follows: # first line: assignment names # second line: "out-of values" # remaining lines: student user-id (<=8 characters) followed by their marks on each assignment # Author: Morgan Grainger # Version: 1.0 2008-April-24 # 1.1 2008-May-26: modify CSV file format. # 1.2 ???????????: provide easier customization via variables, added ROOT_USERS, and...? (Jonathan Templin? Ian Goldberg? Others?) # 1.3 2010-Mar-04: Check for shtml *or* php standard includes, and also autodetection of course name and root users read in using Classlist.pm to reduce start-of-term overhead. (Terry Vaskor) # 1.3.2 2010-Aug-27: CGI debugging output, bug fixing: improved handling of marks file format (leading empty fields + various newline conventions), long userid handling. (Terry Vaskor, with cgitb usage inherited from anonymous modifications to CS 241 script) # 1.3.3 2010-Sep-07: ldapsearch command change; incompatible with Solaris systems, but allows operation on Linux systems. (Terry Vaskor, Ian Goldberg) # 1.3.4 2010-Sep-07: Bug fix for search in 1.3.3 # # import os import urllib2 import cgi import re import pwd import subprocess import cgitb cgitb.enable() # for troubleshooting # COURSE OFFERING-SPECIFIC INFORMATION # This must be modified by course staff for the script to behave properly. # TODO: # Replace I_clicker flag with a root directory for marks files, and then # an explicit list of files to find in that directory. #if I-clicker is part of the course, set to True, or else set to False I_clicker = False # The rest will be detected automatically CL_INFO_PATH = "/u/isg/bin/get_classlist_info"; FORMAT_USER_PATH = "/u/isg/bin/format_userid"; def getName( userId ): try: # userId has already had the above RE matched to it, so # including it in a command line is safe data = subprocess.Popen(['ldapsearch', '-LLLx', '-h', 'uwldap.uwaterloo.ca', '-b', 'dc=uwaterloo,dc=ca', 'uid=' + userId, 'cn'], stdout=subprocess.PIPE ).stdout.readlines() for line in data: tag, val = line.split(": ", 1) if tag == "cn": return val except: return None # Course Name: Course = pwd.getpwuid(os.getuid())[0] # Can look up anyone's marks by specifying ?user= at the end of the URL # This will be read in from .coursestaff.$term; explicitly strip the trailing newlines. ROOT_USERS = [user.strip("\n") for user in subprocess.Popen([CL_INFO_PATH, '-r', 'type', '(instructor|isc|tutor)', 'userid'], stdout=subprocess.PIPE).stdout.readlines()] #ISC id ISCid = subprocess.Popen([CL_INFO_PATH, 'type', 'isc', 'userid'],stdout=subprocess.PIPE).stdout.readlines()[0].strip("\n") # ISC Name ISC = getName(ISCid) ERROR_TEXT = "

If you are enrolled in the course, please contact course personnel and we'll get things straightened out.

"%Course MARK_FILE = "/u/%s/marks/public-marks.txt"%Course PARTICIPATION_MARK_FILE = "/u/%s/marks/public-marks-participation.txt"%Course form = cgi.FieldStorage() def normalize(userId): return subprocess.Popen([FORMAT_USER_PATH, userId], stdout=subprocess.PIPE).stdout.readlines()[0].strip("\n") # Do hardcoded normalization that *should* catch most cases... # Launching external userid-processing utility appears to be *VERRRRY* slow, so # don't do it when it's clearly good not to. # Ideally the external launch would just be quicker... def normalize_quick(userId): return userId[:8].lower() def currentUserId(): try: user = os.environ['REMOTE_USER'] if user in ROOT_USERS and form.has_key('user'): user = form['user'].value if re.match("^[a-zA-Z][a-zA-Z0-9_-]*$", user) == None: return None return normalize(user) except: return None def getMarksHtml(userId): html = "" # Open the file in a mode that will accept newline standards for any OS. # Then, strip leading newlines from every line to accomodate variations # in spreadsheet styling. fObj = file(MARK_FILE,"rU") lines = [line.lstrip(",") for line in fObj.readlines()] fObj.close() # TODO: # A first line will be inserted containing the heading for the table. # The second line will actually be the meanings (implying number) of # special lines (here, hardcoded as assigns and totals). # Then, the following N-1 lines will be the hardcoded vales for those # table columns; the final column will be student-specific data. # PROBLEM: Clicker data will actually be messy here, because there will # be *multiple* student-supplied fields. # Make it K+M headings, where K are hard-coded, and M are student-specific, # separated by a sub-delimiter? # ... Oy. :S assert( len(lines) > 0 ) assigns = lines[0].split(",") totals = lines[1].split(",") foundStudent = False if I_clicker: fObjP = file(PARTICIPATION_MARK_FILE) linesP = fObjP.readlines() fObjP.close() assert( len(linesP) > 0) for line in linesP[2:]: if normalize_quick(line.split(":")[0]) == userId: marks = line.split(":")[1:] html += "

Clicker marks

\n\n\t\n\t\t\n\t\t\n\t\t\n\t\n" html += "\t\n\t\t\n\t\t\n\t\t\n\t\n" % (marks[1], marks[2], marks[3]) html += "
CorrectAnsweredUnanswered
%s%s%s
\n" html += "

Other marks

\n" foundStudent = True break for line in lines[2:]: if normalize_quick(line.split(",")[0]) == userId: marks = line.split(",")[1:] html += "\n\t\n\t\t\n\t\t\n\t\t\n\t\n" for i in range( len(marks) ): if not(totals[i]==''): html += "\t\n\t\t\n\t\t\n\t\t\n\t\n" % (assigns[i], totals[i], marks[i]) html += "
ComponentOut ofYour Grade
%s%s%s
\n" foundStudent = True break if not foundStudent: html = "" html += "

Oops! We don't have any marks on record for you.

" html += ERROR_TEXT return html def getSpecificContents(url): try: fObj = urllib2.urlopen(url) data = "".join(fObj.readlines()) fObj.close() return data except: return "" def getPageContents(baseurl): return getSpecificContents(baseurl + ".php") or getSpecificContents(baseurl + ".shtml"); def getTopHtml(): html = "" html += "Status: 200 OK\n" html += "Content-Type: text/html\n\n" html += """ %s: View Marks """%Course html += getPageContents("http://www.student.cs.uwaterloo.ca/~%s/standard_style"%Course) html += """ """ html += getPageContents("http://www.student.cs.uwaterloo.ca/~%s/standard_init"%Course) html += "

Marks

" return html def getBottomHtml(): html = "" html += getPageContents("http://www.student.cs.uwaterloo.ca/~%s/standard_fini"%Course) html += """ """ return html print getTopHtml() if currentUserId() is None or len(currentUserId()) == 0: print "

Your username wasn't recognized.

" print ERROR_TEXT else: fullName = getName(currentUserId()) if fullName is None: fullName = currentUserId() else: fullName = "%s (%s)" % (fullName, currentUserId()) print "

Here are the marks we have recorded for you so far.

" print "

Marks for %s

" % fullName print getMarksHtml(currentUserId()) print "

A few notes

" print "" print getBottomHtml()