# script to semi-automate homework grading # IMPORTANT DIRECTIONS: # 1. All student files must be named in the format studentemail (where # the e-mail address is written minus the "@ucdavis.edu"). Have all of # them in the current directory, with no other .txt files present. # 2. The format of a student file is required to be as follows: # The file alternates between one line of problem number and a SINGLE # line of answer. The problem number line of problem n must be #n, with # a suffix of a, b etc. for parts, if any. The answer line for a # numeric problem ("type N" below) must be executable R code. # For example, Say Problem 1 has the answer 3 times (2/17), Problem 2(a) # and 2(b) have the answers 8 and 88, and Problem 3 consists of filling # in two blanks in code, with answers "x*y" and "if". Say Problem 4 is # a multiple-choice problem, with answer (b). The student answers file # would then consist of # #1 # 3 * (2/17) # #2a # 8 # #2b # 88 # #3a # x*y # #3b # if # #4 # (b) # Note that there are no blank lines. # 3. There are three problem types-- N for numeric, C for code, S # otherwise (e.g. answers to multiple choice questions). In the N case, # the student must write executable R code. (Currently C is treated as # S.) # 4. The Answers file, in this same directory, also alternates problem # number lines with answer lines. In the former, the problem number is # followed by the problem type (N for numeric, C for code, S otherwise) # and the point total for the problem. # The Answers file for the above example would look like this: # #1 N 10 # 3 * (2/17) # #2a N 20 # 8 # #2b N 20 # 88 # #3a C 15 # x*y # #3b C 15 # if # #4 S # (b) # The student writes 00 if he did not answer on the paper # version, and writes Nothing in a Code answer if he believes code # should be filled in to that blank. # 5. Run the script from the same directory. For each student file, # the function will print the e-mail address and grades (scores on each # problem, and total, plus a 10-point bonus) to stdout. # usage: # # call grader() to get the scores # call calcltrgrades() to get the lettergrades # call emailresults() to mail out the results # # important note: after each of the calls to grader() and # calcltrgrades(), the R variable output is saved to the file outfile # (in R object format); the latter can be copied to another machine # before calling emailresults() # if the student file has format errors, the program opens the file in # vim, and the user may attempt to fix it; if fixed, the user will # specify a smaller bonus value than 10; if the user is not able to # easily fix the file, he assigns a bonus of 0, which is a signal to not # grade this file via the script # globals # # quiznum: quiz number (Quiz 1, Quiz 2 etc.) # answerslist: all data derived from the official Answers file # studentlines: contents of the student answers file # output: quiz results minus letter grades gettrueans <- function() { answerslist <<- list() answersfile <- paste("Answers",quiznum,sep="") lines <- scan(file=answersfile,what="",sep="\n",quiet=T) nproblems <- length(lines) / 2 answerslist$nproblems <<- nproblems answerslist$probnum <<- vector(mode="character",length=nproblems) answerslist$ans <<- vector(mode="character",length=nproblems) answerslist$probtype <<- vector(mode="character",length=nproblems) answerslist$points <<- vector(mode="integer",length=nproblems) for (i in 1:answerslist$nproblems) { l <- lines[2*i-1] answerslist$ans[i] <<- lines[2*i] answerslist$probnum[i] <<- strsplit(l," ")[[1]][1] answerslist$probtype[i] <<- strsplit(l," ")[[1]][2] answerslist$points[i] <<- strsplit(l," ")[[1]][3] } } readstudentfile <- function(sfl) { studentlines <<- scan(file=sfl,what="",sep="\n",quiet=T) } # grade the i-th problem (counting subparts separately), using lines # from the student answer file gradestudentans <- function(i) { studentans <- studentlines[2*i] studentanscopy <- studentans realans <- answerslist$ans[i] fullpts <- answerslist$points[i] if (answerslist$probtype[i] == "N") { studentans <- evalstring(studentans) realans <- evalstring(realans) } probnum <- answerslist$probnum[i] cat(probnum," student original:",studentanscopy,"\n") cat(probnum," student evaluated:",studentans,"\n") cat(probnum," true evaluated:",realans,"\n") fullpts <- answerslist$points[i] resp <- readline(paste("give ",fullpts, "? ")) if (resp == "") pts <- fullpts else pts <- resp } # check studentlines lines for format errors, returning 1 if OK, 0 else studentfileok <- function() { # correct number of lines? nslines <- length(studentlines) if (nslines != 2 * answerslist$nproblems) { print("wrong line count") return(0) } # do problem numbers and answers alternate, one each at a time? for (i in 1:nslines) { tmp <- strsplit(studentlines[i]," ")[[1]][1] firstchar <- substr(tmp,1,1) if (i %% 2 == 1 && firstchar != "#") { print("number, answer lines don't alternate correctly") return(0) } } # in an "N" problem, does the R parse correctly? for (i in 1:answerslist$nproblems) { if (answerslist$probtype[i] == "N") { tmp <- try(evalstring(studentlines[2*i])) if (class(tmp) == "try-error") { print("R parse error") return(0) } } } return(1) } tryfix <- function(sfl) { # open student file for possible fix xcmd <- paste("xterm -e vi ",sfl," &") system(xcmd) while (T) { resp <- readline("re-read student file? ") if (resp == "y") { readstudentfile(sfl) if (studentfileok()) break } else break } return(as.integer(readline("bonus amount: "))) } grader <- function() { # is this Quiz 1, Quiz 2, or what? quiznum <<- readline("enter quiz number: ") gettrueans() # sets up R list trueans output <<- vector(mode="character") for (sfl in list.files(pattern="*.txt")) { emailaddr <- substr(sfl,1,(nchar(sfl)-4)) cat("\n\n"," now grading",emailaddr,"\n") total <- 0 outputline <- emailaddr readstudentfile(sfl) sflok <- studentfileok() if (sflok == 0) { bonus <- tryfix(sfl) if (bonus == 0) next } else bonus <- 10 # display entire student file, e.g. to see if answers in a code # problem work correctly together even though approach is different # from mine # for (sln in studentlines) print(sln) total <- 0 for (i in 1:answerslist$nproblems) { score <- gradestudentans(i) outputline <- paste(outputline," ", score,"/",answerslist$points[i],sep="") total <- total + as.integer(score) } outputline <- paste(outputline," bonus = ",bonus," ",sep="") total <- total + bonus outputline <- paste(outputline,"total =",total) print(outputline) output <<- c(output,outputline) } if (readline("need to edit? ") == "y") output <<- edit(output) cat("\n","results:","\n") save(output,file="outfile") for (i in 1:length(output)) { cat(output[i],"\n") } } calcltrgrades <- function() { load("outfile") tmp <- readline("enter cutoffs, e.g. 95 A+ 85 A 70 B...0: ") tmp <- strsplit(tmp," ")[[1]] inds <- 1:length(tmp) evens <- inds[inds %% 2 == 0] odds <- inds[inds %% 2 == 1] ltrgrades <- tmp[evens] cutoffs <- as.integer(tmp[odds]) totcol <- length(strsplit(output[1]," ")[[1]]) for (i in 1:length(output)) { total <- strsplit(output[i]," ")[[1]][totcol] total <- as.integer(total) ltrgrd <- num2ltr(total,cutoffs,ltrgrades) output[i] <- paste(output[i],ltrgrd) } # save to file for records save(output,file="outfile") write(output,file=paste("Quiz",quiznum,"Grades",sep="")) cat("\n"," letter grade results:","\n") for (i in 1:length(output)) { cat(output[i],"\n") } print("if not grading in office, upload outfile and QuiznGrades") } emailresults <- function() { load("outfile") for (l in output) { tmp <- strsplit(l," ")[[1]] emailaddr <- tmp[1] emailaddr <- paste(emailaddr,"@ucdavis.edu",sep="") cat(l,file="onestudent") tosend <- paste("mutt",emailaddr,"-s 'quiz results' < onestudent") system(tosend) system("/bin/rm onestudent") } } num2ltr <- function(tot,cuts,lgs) { for (i in 1:length(cuts)) { if (tot >= cuts[i]) return(lgs[i]) } return("F") } evalstring <- function(toexec) { cat(toexec,file="tmpexec") sourceresult <- try(source("tmpexec")) if (class(sourceresult) == "try-error" || !is.numeric(sourceresult$value)) { print("R parse error") return(NA) } else return(sourceresult$value) }