#!/usr/bin/python # Process a marking file that I've written. # Adam Sampson import sys, os, re, csv, optparse, m25, random from cStringIO import StringIO from safefiles import read_file, write_file def load_attendance(fn): student_info = {} key = None for row in csv.reader(open(fn, 'rb'), dialect='excel-tab'): if row[0] == "Number": key = {} for i in range(len(row)): key[i] = row[i].replace(" ", "_") elif row[0] == "" or key is None: continue elif row[0][0] in "0123456789": info = {} for i in range(len(row)): info[key[i]] = row[i] student = info["Forenames"].split()[0] + " " + info["Surname"] student_info[student] = info return student_info def main(): parser = optparse.OptionParser(usage="usage: %prog [options] INPUTFILE") parser.add_option("-a", dest="attendance", help="TSV file with attendance information") parser.add_option("-d", dest="outputdir", default="asst", help="output directory") parser.add_option("-t", dest="template", default="template", help="m25 template file for email") parser.add_option("-g", dest="generate", action="store_true", help="generate shuffled list of students; args are sections") (options, args) = parser.parse_args() if options.attendance: student_info = load_attendance(options.attendance) else: student_info = {} if options.generate: students = student_info.keys() random.shuffle(students) for name in students: sys.stdout.write("# %s\n\n" % name) for heading in args: sys.stdout.write("## %s (0)\n\n" % heading) return feedback = {} sections = [] scores = {} section_feedback = {} computed = [] exprs = {} if len(args) != 1: parser.error("must specify one input file") f = StringIO() m25.expand_file(args[0], f) f.seek(0) line = 0 def warn(*s): print >>sys.stderr, "%d: %s" % (line, "".join(map(str, s))) def die(*s): warn(*s) sys.exit(1) student = None section = None while True: line += 1 l = f.readline() if l == "": break l = l.rstrip() m = re.match(r'^# (.*)$', l) if m is not None: student = m.group(1) if student in feedback: die("Seen student already: ", student) feedback[student] = [] section = None m = re.match(r'^#(=|\$) ([^:]+):(.*)$', l) if m is not None: name = m.group(2).strip() expr = m.group(3).strip() if m.group(1) == "=": computed.append(name) exprs[name] = expr continue if student is None: continue m = re.match(r'^## (.*) \(([^)]+)\)$', l) if m is not None: section = m.group(1) score = m.group(2) if section not in sections: sections.append(section) t = scores.setdefault(section, {}) if student in t: die("Seen section for student already: ", student, ", ", section) t[student] = score t = section_feedback.setdefault(section, {}) t[student] = [] feedback[student].append(l) if section is not None and not l.startswith("#"): section_feedback[section][student].append(l) f.close() def student_key(student): info = student_info.get(student) if info is None: # No info for this student -- sort on the full name. return student else: # Sort by surname first. return info["Surname"] + " " + info["Forenames"] students = sorted(feedback.keys(), key=student_key) for student in students: if len(student_info) != 0 and student not in student_info: warn("No student info: ", student) for compute in computed: e = exprs[compute] def get(m): var = m.group(1) if var in scores and student in scores[var]: v = scores[var][student] if v in exprs: v = exprs[v] elif student in student_info and var in student_info[student]: v = student_info[student][var] else: warn("Student ", student, " missing variable ", var, "; using 0") v = "0" return str(float(v)) s = re.sub(r'\$([A-Za-z_]+)', get, e) t = scores.setdefault(compute, {}) t[student] = str(int(eval(s) + 0.5)) sections += computed stuw = max([len(s) for s in students]) + 2 colw = max([len(s) for s in sections]) + 1 def pad(s, width): return (s + (" " * width))[:width] try: os.makedirs(options.outputdir + "/Feedback") except OSError: pass nl = "\r\n" o = open(options.outputdir + "/Marks.csv", "w") o.write(pad("Student", stuw) + ",") o.write(",".join([pad(s, colw) for s in sections])) o.write(nl) for student in students: o.write(pad(student, stuw)) for s in sections: c = scores[s].get(student, "?") o.write("," + pad(c, colw)) o.write(nl) o.write("Total %d students%s" % (len(students), nl)) o.close() template = read_file(options.template) for student in students: ctx = m25.Context() info = student_info.get(student) if info is not None: ctx.define_literal("StudentNumber", info["Number"].split("/")[0]) ctx.define_literal("Feedback", "\n".join(feedback[student])) for k, v in scores.items(): ctx.define_literal(k, v[student]) text = m25.expand_string(template, ctx) write_file(options.outputdir + "/Feedback/" + student + ".txt", text.replace("\n", nl)) def quote(s): return s o = open(options.outputdir + "/Feedback.tsv", "w") o.write("Student") for s in sections: o.write("\t" + quote(s)) if s in section_feedback: o.write("\t" + quote(s + " feedback")) o.write(nl) for student in students: o.write(quote(student)) for s in sections: if student in scores[s]: score = str(scores[s][student]) else: score = "?" o.write("\t" + score) if s in section_feedback: if student in section_feedback[s]: feedback = section_feedback[s][student] else: feedback = [] fs = " ".join(feedback) fs = re.sub(r'\s+', ' ', fs).strip() o.write("\t" + quote(fs)) o.write(nl) o.close() if __name__ == "__main__": main()