-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6190339
commit 3006446
Showing
18 changed files
with
1,908 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
name <<= submitProjectName(pname => "progfun-"+ pname) | ||
|
||
version := "1.0.0" | ||
|
||
scalaVersion := "2.10.2" | ||
|
||
scalacOptions ++= Seq("-deprecation", "-feature") | ||
|
||
libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1" % "test" | ||
|
||
libraryDependencies += "junit" % "junit" % "4.10" % "test" | ||
|
||
// This setting defines the project to which a solution is submitted. When creating a | ||
// handout, the 'createHandout' task will make sure that its value is correct. | ||
submitProjectName := "recfun" | ||
|
||
// See documentation in ProgFunBuild.scala | ||
projectDetailsMap := { | ||
val currentCourseId = "progfun-003" | ||
Map( | ||
"example" -> ProjectDetails( | ||
packageName = "example", | ||
assignmentPartId = "fTzFogNl", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"recfun" -> ProjectDetails( | ||
packageName = "recfun", | ||
assignmentPartId = "3Rarn9Ki", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"funsets" -> ProjectDetails( | ||
packageName = "funsets", | ||
assignmentPartId = "fBXOL6Qd", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"objsets" -> ProjectDetails( | ||
packageName = "objsets", | ||
assignmentPartId = "95dMMEz7", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"patmat" -> ProjectDetails( | ||
packageName = "patmat", | ||
assignmentPartId = "3gPmpcif", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"forcomp" -> ProjectDetails( | ||
packageName = "forcomp", | ||
assignmentPartId = "fG1oZGIO", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId), | ||
"streams" -> ProjectDetails( | ||
packageName = "streams", | ||
assignmentPartId = "CWKgCFCi", | ||
maxScore = 10d, | ||
styleScoreRatio = 0.2, | ||
courseId=currentCourseId)//, | ||
// "simulations" -> ProjectDetails( | ||
// packageName = "simulations", | ||
// assignmentPartId = "iYs4GARk", | ||
// maxScore = 10d, | ||
// styleScoreRatio = 0.2, | ||
// courseId="progfun2-001"), | ||
// "interpreter" -> ProjectDetails( | ||
// packageName = "interpreter", | ||
// assignmentPartId = "1SZhe1Ut", | ||
// maxScore = 10d, | ||
// styleScoreRatio = 0.2, | ||
// courseId="progfun2-001") | ||
) | ||
} | ||
|
||
// Files that we hand out to the students | ||
handoutFiles <<= (baseDirectory, projectDetailsMap, commonSourcePackages) map { (basedir, detailsMap, commonSrcs) => | ||
(projectName: String) => { | ||
val details = detailsMap.getOrElse(projectName, sys.error("Unknown project name: "+ projectName)) | ||
val commonFiles = (PathFinder.empty /: commonSrcs)((files, pkg) => | ||
files +++ (basedir / "src" / "main" / "scala" / pkg ** "*.scala") | ||
) | ||
(basedir / "src" / "main" / "scala" / details.packageName ** "*.scala") +++ | ||
commonFiles +++ | ||
(basedir / "src" / "main" / "resources" / details.packageName ** "*") +++ | ||
(basedir / "src" / "test" / "scala" / details.packageName ** "*.scala") +++ | ||
(basedir / "build.sbt") +++ | ||
(basedir / "project" / "build.properties") +++ | ||
(basedir / "project" ** ("*.scala" || "*.sbt")) +++ | ||
(basedir / "project" / "scalastyle_config.xml") +++ | ||
(basedir / "lib_managed" ** "*.jar") +++ | ||
(basedir * (".classpath" || ".project")) +++ | ||
(basedir / ".settings" / "org.scala-ide.sdt.core.prefs") | ||
} | ||
} | ||
|
||
// This setting allows to restrict the source files that are compiled and tested | ||
// to one specific project. It should be either the empty string, in which case all | ||
// projects are included, or one of the project names from the projectDetailsMap. | ||
currentProject := "" | ||
|
||
// Packages in src/main/scala that are used in every project. Included in every | ||
// handout, submission. | ||
commonSourcePackages += "common" | ||
|
||
// Packages in src/test/scala that are used for grading projects. Always included | ||
// compiling tests, grading a project. | ||
gradingTestPackages += "grading" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
import dispatch.{Request, Http, NoLogging, StatusCode, url} | ||
import cc.spray.json.{JsNull, JsonParser, DefaultJsonProtocol, JsValue} | ||
import RichJsValue._ | ||
import org.apache.commons.codec.digest.DigestUtils | ||
import org.apache.commons.codec.binary.{Hex, Base64} | ||
import java.io.{IOException, File, FileInputStream} | ||
import scalaz.Scalaz.{mkIdentity, ValidationNEL} | ||
|
||
import Settings._ | ||
|
||
case class JsonSubmission(api_state: String, user_info: JsValue, submission_metadata: JsValue, solutions: JsValue, submission_encoding: String, submission: String) | ||
//case class JsonQueueResult(submission: JsonSubmission) | ||
object SubmitJsonProtocol extends DefaultJsonProtocol { | ||
implicit val jsonSubmissionFormat = jsonFormat6(JsonSubmission) | ||
// implicit val jsonQueueResultFormat = jsonFormat1(JsonQueueResult) | ||
} | ||
|
||
object CourseraHttp { | ||
private lazy val http = new Http with NoLogging | ||
|
||
private def executeRequest[T](req: Request)(parse: String => ValidationNEL[String, T]): ValidationNEL[String, T] = { | ||
try { | ||
http(req >- { res => | ||
parse(res) | ||
}) | ||
} catch { | ||
case ex: IOException => | ||
("Connection failed\n"+ ex.toString()).failNel | ||
case StatusCode(code, message) => | ||
("HTTP failed with status "+ code +"\n"+ message).failNel | ||
} | ||
} | ||
|
||
|
||
/****************************** | ||
* SUBMITTING | ||
*/ | ||
|
||
def getChallenge(email: String, submitProject: ProjectDetails): ValidationNEL[String, Challenge] = { | ||
val baseReq = url(challengeUrl(submitProject.courseId)) | ||
val withArgs = baseReq << Map("email_address" -> email, | ||
"assignment_part_sid" -> submitProject.assignmentPartId, | ||
"response_encoding" -> "delim") | ||
|
||
executeRequest(withArgs) { res => | ||
// example result. there might be an `aux_data` value at the end. | ||
// |email_address|[email protected]|challenge_key|XXYYXXYYXXYY|state|XXYYXXYYXXYY|challenge_aux_data| | ||
val parts = res.split('|').filterNot(_.isEmpty) | ||
if (parts.length < 7) | ||
("Unexpected challenge format: \n"+ res).failNel | ||
else | ||
Challenge(parts(1), parts(3), parts(5)).successNel | ||
} | ||
} | ||
|
||
def submitSolution(sourcesJar: File, submitProject: ProjectDetails, challenge: Challenge, chResponse: String): ValidationNEL[String, String] = { | ||
val fileLength = sourcesJar.length() | ||
if (!sourcesJar.exists()) { | ||
("Sources jar archive does not exist\n"+ sourcesJar.getAbsolutePath).failNel | ||
} else if (fileLength == 0l) { | ||
("Sources jar archive is empty\n"+ sourcesJar.getAbsolutePath).failNel | ||
} else if (fileLength > maxSubmitFileSize) { | ||
("Sources jar archive is too big. Allowed size: "+ | ||
maxSubmitFileSize +" bytes, found "+ fileLength +" bytes.\n"+ | ||
sourcesJar.getAbsolutePath).failNel | ||
} else { | ||
val bytes = new Array[Byte](fileLength.toInt) | ||
val sizeRead = try { | ||
val is = new FileInputStream(sourcesJar) | ||
val read = is.read(bytes) | ||
is.close() | ||
read | ||
} catch { | ||
case ex: IOException => | ||
("Failed to read sources jar archive\n"+ ex.toString()).failNel | ||
} | ||
if (sizeRead != bytes.length) { | ||
("Failed to read the sources jar archive, size read: "+ sizeRead).failNel | ||
} else { | ||
val fileData = encodeBase64(bytes) | ||
val baseReq = url(submitUrl(submitProject.courseId)) | ||
val withArgs = baseReq << Map("assignment_part_sid" -> submitProject.assignmentPartId, | ||
"email_address" -> challenge.email, | ||
"submission" -> fileData, | ||
"submission_aux" -> "", | ||
"challenge_response" -> chResponse, | ||
"state" -> challenge.state) | ||
executeRequest(withArgs) { res => | ||
// the API returns HTTP 200 even if there are failures, how impolite... | ||
if (res.contains("Your submission has been accepted")) | ||
res.successNel | ||
else | ||
res.failNel | ||
} | ||
} | ||
} | ||
} | ||
|
||
def challengeResponse(challenge: Challenge, otPassword: String): String = | ||
shaHexDigest(challenge.challengeKey + otPassword) | ||
|
||
|
||
/******************************** | ||
* DOWNLOADING SUBMISSIONS | ||
*/ | ||
|
||
// def downloadFromQueue(queue: String, targetJar: File, apiKey: String): ValidationNEL[String, QueueResult] = { | ||
// val baseReq = url(Settings.submitQueueUrl) | ||
// val withArgsAndHeader = baseReq << Map("queue" -> queue) <:< Map("X-api-key" -> apiKey) | ||
|
||
// executeRequest(withArgsAndHeader) { res => | ||
// extractJson(res, targetJar) | ||
// } | ||
// } | ||
|
||
def readJsonFile(jsonFile: File, targetJar: File): ValidationNEL[String, QueueResult] = { | ||
extractJson(sbt.IO.read(jsonFile), targetJar) | ||
} | ||
|
||
def extractJson(jsonData: String, targetJar: File): ValidationNEL[String, QueueResult] = { | ||
import SubmitJsonProtocol._ | ||
for { | ||
jsonSubmission <- { | ||
try { | ||
val parsed = JsonParser(jsonData) | ||
val submission = parsed \ "submission" | ||
if (submission == JsNull) { | ||
("Nothing to grade, queue is empty.").failNel | ||
} else { | ||
submission.convertTo[JsonSubmission].successNel | ||
} | ||
} catch { | ||
case e: Exception => | ||
("Could not parse submission\n"+ jsonData +"\n"+ fullExceptionString(e)).failNel | ||
} | ||
} | ||
queueResult <- { | ||
val encodedFile = jsonSubmission.submission | ||
val jarContent = decodeBase64(encodedFile) | ||
try { | ||
sbt.IO.write(targetJar, jarContent) | ||
QueueResult(jsonSubmission.api_state).successNel | ||
} catch { | ||
case e: IOException => | ||
("Failed to write jar file to "+ targetJar.getAbsolutePath +"\n"+ e.toString).failNel | ||
} | ||
} | ||
} yield queueResult | ||
} | ||
|
||
def unpackJar(file: File, targetDirectory: File): ValidationNEL[String, Unit] = { | ||
try { | ||
val files = sbt.IO.unzip(file, targetDirectory) | ||
if (files.isEmpty) | ||
("No files found when unpacking jar file "+ file.getAbsolutePath).failNel | ||
else | ||
().successNel | ||
} catch { | ||
case e: IOException => | ||
val msg = "Error while unpacking the jar file "+ file.getAbsolutePath +" to "+ targetDirectory.getAbsolutePath +"\n"+ e.toString | ||
if (Settings.offlineMode) { | ||
println("[offline mode] "+ msg) | ||
().successNel | ||
} else { | ||
msg.failNel | ||
} | ||
} | ||
} | ||
|
||
|
||
/******************************** | ||
* SUBMITTING GRADES | ||
*/ | ||
|
||
def submitGrade(feedback: String, score: String, apiState: String, apiKey: String, gradeProject: ProjectDetails): ValidationNEL[String, Unit] = { | ||
import DefaultJsonProtocol._ | ||
val baseReq = url(Settings.uploadFeedbackUrl(gradeProject.courseId)) | ||
val withArgs = baseReq << Map("api_state" -> apiState, "score" -> score, "feedback" -> feedback) <:< Map("X-api-key" -> apiKey) | ||
executeRequest(withArgs) { res => | ||
try { | ||
val js = JsonParser(res) | ||
val status = (js \ "status").convertTo[String] | ||
if (status == "202") | ||
().successNel | ||
else | ||
("Unexpected result from submit request: "+ status).failNel | ||
} catch { | ||
case e: Exception => | ||
("Failed to parse response while submitting grade\n"+ res +"\n"+ fullExceptionString(e)).failNel | ||
} | ||
} | ||
} | ||
|
||
|
||
/********************************* | ||
* TOOLS AND STUFF | ||
*/ | ||
|
||
def shaHexDigest(s: String): String = { | ||
val chars = Hex.encodeHex(DigestUtils.sha(s)) | ||
new String(chars) | ||
} | ||
|
||
|
||
def fullExceptionString(e: Throwable) = | ||
e.toString +"\n"+ e.getStackTrace.map(_.toString).mkString("\n") | ||
|
||
|
||
/* Base 64 tools */ | ||
|
||
def encodeBase64(bytes: Array[Byte]): String = | ||
new String(Base64.encodeBase64(bytes)) | ||
|
||
def decodeBase64(str: String): Array[Byte] = { | ||
// codecs 1.4 has a version accepting a string, but not 1.2; jar hell. | ||
Base64.decodeBase64(str.getBytes) | ||
} | ||
} | ||
|
||
case class Challenge(email: String, challengeKey: String, state: String) | ||
|
||
case class QueueResult(apiState: String) | ||
|
Oops, something went wrong.