#!/usr/bin/env python import sys, os, re, time, shutil mylib = os.path.join(os.environ['HOME'], 'lib', 'python') grlib = os.path.join(os.environ['HOME'], 'lib', 'grading') sys.path.extend([mylib, grlib]) from Util import * def usage(progname): usage_str = """ Usage: """+progname+""" --course [--hw ] [--student ] [--cleaning] [--testing] Computes the homework scores, and discounts the homework grades for lateness. Uses: - the submission log files to determine the submission time. - the file due-dates in the current directory to determine the submission deadline for each homework. - the gradefile /GRADE- or GRADE- in the students\' submission directory. It will produce, in each grade file: - a line \"Current grand total:\" showing the grade; - a line \"After lateness discount:\" showing the discounted grade after it. - a backup file with ending .~ . The backup files may have to be erased using the --cleaning option, before a new grade computation. - Unless --student is specified, it will also write a file in the grading directory,in the standard exam record format, with the name of the homework, listing the homework score of each student and commenting if there is no submission record. Options: --hw 05 : Only homework 05 will be processed. --student pepe : only student pepe is done. --cleaning : only the corresponding backup files are erased. --testing : the original grade file is unchanged, the new grade files get the name GRADE-.tmp. """ sys.stderr.write(usage_str) sys.exit(1) months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split() mon2num = {} for i in range(12): mon2num[months[i]] = i # Date example: Jan 13 15:33:21 1998 find_date_rx = r"(\w{3} {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4})" def parse_date(date): parse_date_rx = r"(\w{3}) {1,2}(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})" return re.search(parse_date_rx, date).groups() def date2seconds(date): mon, mday, hour, min, sec, year = parse_date(date) year, mday, hour, min, sec =\ map(lambda x: int(x), (year, mday, hour, min, sec)) mon = mon2num[mon] + 1 """Below, after sec comes the weekday, 0-6, the Julian day, 1-366, and the daylight savings flag, for which -1 means I don't know. It is OK to pass anything for weekdays and Julian day, and -1 for the daylight savings flag. """ return time.mktime((year, mon, mday, hour, min, sec, 0, 0, -1)) def get_due_times(due_dates_file): """ Get due times from the file due_dates_file in which lines have the form: 03 Jan 13 15:33:21 1998 Jan 20 20:00:00 1998 The second time is the late due time. Comment lines begin with # """ try: due_dates_h = open(due_dates_file) except IOError, detail: die("Cannot open "+due_dates_file, detail) due_times = {} due_dates_rx = (r"(\S+)\s+max:\s+(\d+)\s+due:\s+" + find_date_rx + r"\s+late:\s+" + find_date_rx) while 1: line = due_dates_h.readline() if not line: break if '#' == line[0]: continue hw, max, due_date, late_date = re.search(due_dates_rx, line).groups() due_times[hw] = map(date2seconds, (due_date, late_date)) due_dates_h.close() return due_times def get_subm_records(logfile, hw_list): """ Get submission records of a student from a logfile. Typical line: aborisov submitted 02/.forward at Wed Sep 30 22:42:09 1998 For e.g. 02, finds the latest submission record of any 02/something. RETURNS: the submission records as a dictionary indexed by the homework. ASSUMES: gsubmit reports always the full target pathname of the submitted file relative to the submission directory. """ try: log_h = open(logfile) except IOError, detail: die("Cannot open "+logfile, detail) log_start_rx = r"submitted ((("+os.sep+".*)|\.)"+os.sep+")?" subm_recs = {} while 1: line = log_h.readline() if not line: break for hw in hw_list: if re.search(log_start_rx + hw, line): subm_recs[hw] = line.rstrip() break log_h.close() return subm_recs def has_ignored_message(line): ignored_messages = [ "After lateness discount:", "Current grand total:", "No submission record.", "Late by", ] for message in ignored_messages: if re.search(message, line): return 1 return 0 def sum_scores(gf_h, newgf_h, date): """Sum scores in gf_h, write into newgf_h. """ sum = 0 last_line = "" while 1: line = gf_h.readline() if not line: break if has_ignored_message(line): continue last_line = line # Remember the last line beyond the loop. newgf_h.write(line) hit = re.search(r"\(-?\d+\):\s*(-?\d+)", line) score = 0 if hit: score = int(hit.groups()[0]) sum = sum + score hit = re.match(r"^Grand total:\s*(\d+)", line) if hit: old_total, = hit.groups() if "\n" != last_line: newgf_h.write("\n") newgf_h.write("Current grand total: %d (%s)\n" % (sum, date)) return sum def calc_grade(gradefi, ratio_hours, date, testing = 0): """Calculate the grades in gradefi. ARGS: ratio_hours = [, ] Add a line showing the discounted grade. Keep the original gradefile as gradefile.~. RETURNS: [, ]. """ try: gf_h = open(gradefi) newgf_h = open(gradefi+".tmp", "w") except IOError, detail: warn("Opening %s and >%s.tmp" % (gradefi, gradefi), detail) return (0,"Could not open file.") warn(gradefi) sum = sum_scores(gf_h, newgf_h, date) comment = "" ratio, late_hours = ratio_hours if 0 > ratio: comment = "No submission record." newgf_h.write(comment+"\n") ratio = 1 discounted = round(sum * ratio) print "sum = %d, discounted = %d" % (sum, discounted) newgf_h.write("After lateness discount: %d\n" % discounted) if sum != discounted: newgf_h.write("Late by %.1f hours\n" % late_hours) gf_h.close() newgf_h.close() if not testing: try: shutil.copy(gradefi, gradefi+".~") os.rename(gradefi+".tmp", gradefi) os.chmod(gradefi, 0660) except IOError, detail: warn("Updating "+gradefi, detail) return [discounted, comment] class Allhw: """ Collects many variables that would have to be global otherwise. Also includes functions making substantial use of these variables. Only one instance of this class will be used. """ def __init__(self, options): cwd = os.getcwd() # cwd == "/home/dawn/gacs/cs113/grading" cwdlist = cwd.split(os.sep) self.course = options.get("--course") or cwdlist[4] warn("Course: "+self.course) self.Hw = options.get("--hw") if self.Hw: warn("Homework: "+self.Hw) self.Logname = options.get("--student") if self.Logname: warn("Student: "+self.Logname) self.cleaning = options.has_key("--cleaning") if self.cleaning: warn("Cleaning ...") self.testing = options.has_key("--testing") if self.testing: warn("Testing ...") course_dir = os.path.join(os.sep, "cs", "course", self.course, "current", "homework") log = os.path.join(course_dir, "log") self.spool = os.path.join(course_dir, "spool") self.grading_dir = os.path.join(os.environ["HOME"], self.course, "grading") due_dates_file = os.path.join(self.grading_dir, "due-dates") if not self.cleaning: self.date = time.ctime(time.time()) self.due_times = get_due_times(due_dates_file) self.hw_list = self.due_times.keys() self.hw_list.sort() self.students = os.listdir(log) if self.Logname: if self.Logname in self.students: self.students = [self.Logname] else: die(self.Logname+" has no submission record.") self.subm_recsof = {} for student in self.students: logfile = os.path.join(log, student) self.subm_recsof[student] = get_subm_records(logfile, self.hw_list) def find_ratio(self, student = "", hw = ""): """ Find ratio of lateness to difference between due times, or -1. RETURNS: [, ]. """ if not self.subm_recsof.has_key(student): return [-1, 0] subm_rec = self.subm_recsof[student].get(hw) if not subm_rec: return [-1, 0] subm_date, = re.search(find_date_rx, subm_rec).groups() subm_time = date2seconds(subm_date) if None == subm_time: warn("submission time of "+hw+" undefined") return [-1, ""] if not self.due_times.has_key(hw): warn("due time of "+hw+" undefined") return [-1, 0] due_time, late_time = self.due_times[hw] max_diff = late_time - due_time if 0 >= max_diff: max_diff = 1 diff = subm_time - due_time if 0 > diff: diff = 0 if diff > max_diff: diff = max_diff late_hours = diff / 3600 return [(max_diff - diff)/max_diff, late_hours] def proc_files(self): """Sum and discount the scores discounts in all the student files. """ for hw in self.hw_list: if self.Hw and self.Hw != hw: continue warn("\n\n "+hw+":") hw_rec = os.path.join(self.grading_dir, hw) trouble_rec = os.path.join(self.grading_dir, hw+".trouble") if self.Logname: hw_rec = hw_rec + "." + self.Logname try: hw_rec_h = open(hw_rec, "w") trouble_h = open(trouble_rec, "w") except IOError, detail: die("Cannot open %s or %s for writing" % (hw_rec, trouble_rec), detail) for logname in self.students: warn(logname+" ...") hwdir = os.path.join(self.spool, logname, hw) gradefi = os.path.join(hwdir, "GRADE-"+hw) if not os.path.isfile(gradefi): gradefi = os.path.join(self.spool, logname, "GRADE-"+hw) if not os.path.isfile(gradefi): warn("no file "+gradefi) continue if self.cleaning: if os.path.isfile(gradefi+".~"): try: os.remove(gradefi+".~") except IOError, detail: warn("cannot remove"+gradefi+".~", detail) continue ratio_hours = self.find_ratio(student = logname, hw = hw) discounted, comment = calc_grade(gradefi, ratio_hours, self.date, self.testing) hw_rec_h.write("%-12s %4d %s\n" % (logname, discounted, comment)) if ("No submission record." == comment and os.path.exists(hwdir)): trouble_h.write(logname+"\n") "main" progname = os.path.basename(sys.argv[0]) long_optlist = ["course=", "hw=", "student=", "cleaning", "testing"] options, args = getopt_map("x", long_optlist) allhw = Allhw(options) allhw.proc_files()