diff options
| author | Jérémy Zurcher <jeremy@asynk.ch> | 2013-05-11 22:30:17 +0200 | 
|---|---|---|
| committer | Jérémy Zurcher <jeremy@asynk.ch> | 2016-11-10 18:03:24 +0100 | 
| commit | f693c7f267e2668be5626413d4eee8600630b6be (patch) | |
| tree | 10ede8ee23560a228175074a59bab72f9e52a143 /Scala/forcomp/project/ProgFunBuild.scala | |
| parent | 5c5087a4a7f8342946ffe0ccbf1a244d7d4790dc (diff) | |
| download | coursera-f693c7f267e2668be5626413d4eee8600630b6be.zip coursera-f693c7f267e2668be5626413d4eee8600630b6be.tar.gz  | |
Scala : Scala : add forcomp assignment
Diffstat (limited to 'Scala/forcomp/project/ProgFunBuild.scala')
| -rw-r--r-- | Scala/forcomp/project/ProgFunBuild.scala | 646 | 
1 files changed, 646 insertions, 0 deletions
diff --git a/Scala/forcomp/project/ProgFunBuild.scala b/Scala/forcomp/project/ProgFunBuild.scala new file mode 100644 index 0000000..93d4b9d --- /dev/null +++ b/Scala/forcomp/project/ProgFunBuild.scala @@ -0,0 +1,646 @@ +import sbt._ +import Keys._ + +import scalaz.Scalaz.mkIdentity +import scalaz.{Success, Failure} +import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys + +/** + * See README.md for high-level overview + * + * Libraries Doc Links + * + * Coursera API + *  - http://support.coursera.org/customer/portal/articles/573466-programming-assignments + *  - the python script 'submit.py' that can be downloaded from the above site + * + * SBT + *  - https://github.com/harrah/xsbt/wiki/Getting-Started-Full-Def + *  - https://github.com/harrah/xsbt/wiki/Getting-Started-Custom-Settings + *  - https://github.com/harrah/xsbt/wiki/Getting-Started-More-About-Settings + *  - https://github.com/harrah/xsbt/wiki/Input-Tasks + *  - https://github.com/harrah/xsbt/wiki/Tasks + *  - http://harrah.github.com/xsbt/latest/api/index.html + *  - https://groups.google.com/forum/?fromgroups#!forum/simple-build-tool + * + * Dispatch + *  - http://dispatch-classic.databinder.net/Response+Bodies.html + *  - http://www.flotsam.nl/dispatch-periodic-table.html + *  - http://databinder.net/dispatch-doc/ + * + * Scalaz + *  - http://www.lunatech-research.com/archives/2012/03/02/validation-scala + *  - http://scalaz.github.com/scalaz/scalaz-2.9.1-6.0.4/doc/index.html#scalaz.Validation + * + * Apache Commons Codec 1.4 + *  - http://www.jarvana.com/jarvana/view/commons-codec/commons-codec/1.4/commons-codec-1.4-javadoc.jar!/index.html + * + * Scalatest + *  - http://doc.scalatest.org/1.9.1/index.html#org.scalatest.package + */ +object ProgFunBuild extends Build { + +  /*********************************************************** +   * MAIN PROJECT DEFINITION +   */ + +  lazy val assignmentProject = Project(id = "assignment", base = file(".")) settings( +    // 'submit' depends on 'packageSrc', so needs to be a project-level setting: on build-level, 'packageSrc' is not defined +    submitSetting, +    createHandoutSetting, +    // put all libs in the lib_managed directory, that way we can distribute eclipse project files +    retrieveManaged := true, +    EclipseKeys.relativizeLibs := true, +    // Avoid generating eclipse source entries for the java directories +    (unmanagedSourceDirectories in Compile) <<= (scalaSource in Compile)(Seq(_)), +    (unmanagedSourceDirectories in Test) <<= (scalaSource in Test)(Seq(_)), +    commonSourcePackages := Seq(), // see build.sbt +    gradingTestPackages := Seq(),  // see build.sbt +    selectMainSources, +    selectTestSources, +    scalaTestSetting, +    styleCheckSetting, +    setTestPropertiesSetting, +    setTestPropertiesHook +  ) settings (packageSubmissionFiles: _*) + + +  /*********************************************************** +   * SETTINGS AND TASKS +   */ + +  /** The 'submit' task uses this project name (defined in the build.sbt file) to know where to submit the solution */ +  val submitProjectName = SettingKey[String]("submitProjectName") + +  /** Project-specific settings, see main build.sbt */ +  val projectDetailsMap = SettingKey[Map[String, ProjectDetails]]("projectDetailsMap") + +  /** +   * The files that are handed out to students. Accepts a string denoting the project name for +   * which a handout will be generated. +   */ +  val handoutFiles = TaskKey[String => PathFinder]("handoutFiles") + +  /** +   * 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. +   */ +  val currentProject = SettingKey[String]("currentProject") + +  /** Package names of source packages common for all projects, see comment in build.sbt */ +  val commonSourcePackages = SettingKey[Seq[String]]("commonSourcePackages") + +  /** Package names of test sources for grading, see comment in build.sbt */ +  val gradingTestPackages = SettingKey[Seq[String]]("gradingTestPackages") + +  /************************************************************ +   * SUBMITTING A SOLUTION TO COURSERA +   */ + +  val packageSubmission = TaskKey[File]("packageSubmission") + +  val packageSubmissionFiles = { +    // the packageSrc task uses Defaults.packageSrcMappings, which is defined as concatMappings(resourceMappings, sourceMappings) +    // in the packageSubmisson task we only use the sources, not the resources. +    inConfig(Compile)(Defaults.packageTaskSettings(packageSubmission, Defaults.sourceMappings)) +  } + +  /** Task to submit a solution to coursera */ +  val submit = InputKey[Unit]("submit") + +  lazy val submitSetting = submit <<= inputTask { argTask => +    (argTask, compile in Compile, currentProject, (packageSubmission in Compile), submitProjectName, projectDetailsMap, streams) map { (args, _, currentProject, sourcesJar, projectName, detailsMap, s) => +      if (currentProject != "") { +        val msg = +          """The 'currentProject' setting is not empty: '%s' +            | +            |This error only appears if there are mistakes in the build scripts. Please re-download the assignment +            |from the coursera webiste. Make sure that you did not perform any changes to the build files in the +            |`project/` directory. If this error persits, ask for help on the course forums.""".format(currentProject).stripMargin +"\n " +        s.log.error(msg) +        failSubmit() +      } else { +        lazy val wrongNameMsg = +          """Unknown project name: %s +            | +            |This error only appears if there are mistakes in the build scripts. Please re-download the assignment +            |from the coursera webiste. Make sure that you did not perform any changes to the build files in the +            |`project/` directory. If this error persits, ask for help on the course forums.""".format(projectName).stripMargin +"\n " +        // log strips empty lines at the ond of `msg`. to have one we add "\n " +        val details = detailsMap.getOrElse(projectName, {s.log.error(wrongNameMsg); failSubmit()}) +        args match { +          case email :: otPassword :: Nil => +            submitSources(sourcesJar, details, email, otPassword, s.log) +          case _ => +            val msg = +              """No e-mail address and / or submission password provided. The required syntax for `submit` is +                |  submit <e-mail> <submissionPassword> +                | +                |The submission password, which is NOT YOUR LOGIN PASSWORD, can be obtained from the assignment page +                |  https://class.coursera.org/%s/assignment/index""".format(details.courseId).stripMargin +"\n " +            s.log.error(msg) +            failSubmit() +        } +      } +    } +  } + + +  def submitSources(sourcesJar: File, submitProject: ProjectDetails, email: String, otPassword: String, logger: Logger) { +    import CourseraHttp._ +    logger.info("Connecting to coursera. Obtaining challenge...") +    val res = for { +      challenge  <- getChallenge(email, submitProject) +      chResponse <- { +        logger.info("Computing challenge response...") +        challengeResponse(challenge, otPassword).successNel[String] +      } +      response   <- { +        logger.info("Submitting solution...") +        submitSolution(sourcesJar, submitProject, challenge, chResponse) +      } +    } yield response + +    res match { +      case Failure(msgs) => +        for (msg <- msgs.list) logger.error(msg) +        failSubmit() +      case Success(response) => +        logger.success("Your code was successfully submitted: "+ response) +    } +  } + + +  def failSubmit(): Nothing = { +    sys.error("Submission failed") +  } + + + +  /*********************************************************** +   * CREATE THE HANDOUT ZIP FILE +   */ + +  val createHandout = InputKey[File]("createHandout") + +  // depends on "compile in Test" to make sure everything compiles. also makes sure that +  // all dependencies are downloaded, because we pack the .jar files into the handout. +  lazy val createHandoutSetting = createHandout <<= inputTask { argTask => +    (argTask, currentProject, baseDirectory, handoutFiles, submitProjectName, target, projectDetailsMap, compile in Test) map { (args, currentProject, basedir, filesFinder, submitProject, targetDir, detailsMap, _) => +      if (currentProject != "") +        sys.error("\nthe 'currentProject' setting in build.sbt needs to be \"\" in order to create a handout") +      else args match { +        case handoutProjectName :: eclipseDone :: Nil if eclipseDone == "eclipseWasCalled" => +          if (handoutProjectName != submitProject) +            sys.error("\nThe `submitProjectName` setting in `build.sbt` must match the project name for which a handout is generated\n ") +          val files = filesFinder(handoutProjectName).get +          val filesWithRelativeNames = files.x_!(relativeTo(basedir)) map { +            case (file, name) => (file, handoutProjectName+"/"+name) +          } +          val targetZip = targetDir / (handoutProjectName +".zip") +          IO.zip(filesWithRelativeNames, targetZip) +          targetZip +        case _ => +          val msg =""" +            | +            |Failed to create handout. Syntax: `createHandout <projectName> <eclipseWasCalled>` +            | +            |Valid project names are: %s +            | +            |The argument <eclipseWasCalled> needs to be the string "eclipseWasCalled". This is to remind +            |you that you **need** to manually run the `eclipse` command before running `createHandout`. +            | """.stripMargin.format(detailsMap.keys.mkString(", ")) +          sys.error(msg) +      } +    } +  } + + +  /************************************************************ +   * LIMITING SOURCES TO CURRENT PROJECT +   */ + +  def filter(basedir: File, packages: Seq[String]) = new FileFilter { +    def accept(file: File) = { +      basedir.equals(file) || { +        IO.relativize(basedir, file) match { +          case Some(str) => +            packages exists { pkg => +              str.startsWith(pkg) +            } +          case _ => +            sys.error("unexpected test file: "+ file +"\nbase dir: "+ basedir) +        } +      } +    } +  } + +  def projectFiles(allFiles: Seq[File], basedir: File, projectName: String, globalPackages: Seq[String], detailsMap: Map[String, ProjectDetails]) = { +    if (projectName == "") allFiles +    else detailsMap.get(projectName) match { +      case Some(project) => +        val finder = allFiles ** filter(basedir, globalPackages :+ project.packageName) +        finder.get +      case None => +        sys.error("currentProject is set to an invalid name: "+ projectName) +    } +  } + +  /** +   * Only include source files of 'currentProject', helpful when preparign a specific assignment. +   * Also keeps the source packages in 'commonSourcePackages'. +   */ +  val selectMainSources = { +    (unmanagedSources in Compile) <<= (unmanagedSources in Compile, scalaSource in Compile, projectDetailsMap, currentProject, commonSourcePackages) map { (sources, srcMainScalaDir, detailsMap, projectName, commonSrcs) => +      projectFiles(sources, srcMainScalaDir, projectName, commonSrcs, detailsMap) +    } +  } + +  /** +   * Only include the test files which are defined in the package of the current project. +   * Also keeps test sources in packages listed in 'gradingTestPackages'. +   */ +  val selectTestSources = { +    (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in Test, projectDetailsMap, currentProject, gradingTestPackages) map { (sources, srcTestScalaDir, detailsMap, projectName, gradingSrcs) => +      projectFiles(sources, srcTestScalaDir, projectName, gradingSrcs, detailsMap) +    } +  } + + +  /************************************************************ +   * PARAMETERS FOR RUNNING THE TESTS +   * +   * Setting some system properties that are parameters for the GradingSuite test +   * suite mixin. This is for running the `test` task in SBT's JVM. When running +   * the `scalaTest` task, the ScalaTestRunner creates a new JVM and passes the +   * same properties. +   */ + +  val setTestProperties = TaskKey[Unit]("setTestProperties") +  val setTestPropertiesSetting = setTestProperties := { +    import scala.util.Properties._ +    import Settings._ +    setProp(scalaTestIndividualTestTimeoutProperty, individualTestTimeout.toString) +    setProp(scalaTestDefaultWeigthProperty, scalaTestDefaultWeigth.toString) +  } + +  val setTestPropertiesHook = (test in Test) <<= (test in Test).dependsOn(setTestProperties) + + +  /************************************************************ +   * RUNNING WEIGHTED SCALATEST & STYLE CHECKER ON DEVELOPMENT SOURCES +   */ + +  def copiedResourceFiles(copied: collection.Seq[(java.io.File, java.io.File)]): List[File] = { +    copied collect { +      case (from, to) if to.isFile => to +    } toList +  } + +  val scalaTest = TaskKey[Unit]("scalaTest") +  val scalaTestSetting = scalaTest <<= +    (compile in Compile, +      compile in Test, +      fullClasspath in Test, +      copyResources in Compile, +      classDirectory in Test, +      baseDirectory, +      streams) map { (_, _, classpath, resources, testClasses, basedir, s) => +    // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed` +      val logger = s.log +      val outfile = basedir / Settings.testResultsFileName +      val policyFile = basedir / Settings.policyFileName +      val (score, maxScore, feedback, runLog) = ScalaTestRunner.runScalaTest(classpath, testClasses, outfile, policyFile, copiedResourceFiles(resources), logger.error(_)) +      logger.info(feedback) +      logger.info("Test Score: "+ score +" out of "+ maxScore) +      if (!runLog.isEmpty) { +        logger.info("Console output of ScalaTest process") +        logger.info(runLog) +      } +    } + +  val styleCheck = TaskKey[Unit]("styleCheck") + +  /** +   * depend on compile to make sure the sources pass the compiler +   */ +  val styleCheckSetting = styleCheck <<= (compile in Compile, sources in Compile, streams) map { (_, sourceFiles, s) => +    val logger = s.log +    val (feedback, score) = StyleChecker.assess(sourceFiles) +    logger.info(feedback) +    logger.info("Style Score: "+ score +" out of "+ StyleChecker.maxResult) +  } + + +  /************************************************************ +   * PROJECT DEFINITION FOR GRADING +   */ + +  lazy val submissionProject = Project(id = "submission", base = file(Settings.submissionDirName)) settings( +    /** settings we take over from the assignment project */ +    version <<= (version in assignmentProject), +    name <<= (name in assignmentProject), +    scalaVersion <<= (scalaVersion in assignmentProject), +    scalacOptions <<= (scalacOptions in assignmentProject), +    libraryDependencies <<= (libraryDependencies in assignmentProject), + +    /** settings specific to the grading project */ +    initGradingSetting, +    // default value, don't change. see comment on `val partIdOfGradingProject` +    partIdOfGradingProject := "", +    gradeProjectDetailsSetting, +    setMaxScoreSetting, +    setMaxScoreHook, +    // default value, don't change. see comment on `val apiKey` +    apiKey := "", +    getSubmissionSetting, +    getSubmissionHook, +    submissionLoggerSetting, +    readCompileLog, +    readTestCompileLog, +    setTestPropertiesSetting, +    setTestPropertiesHook, +    resourcesFromAssignment, +    selectResourcesForProject, +    testSourcesFromAssignment, +    selectTestsForProject, +    scalaTestSubmissionSetting, +    styleCheckSubmissionSetting, +    gradeSetting, +    EclipseKeys.skipProject := true +  ) + +  /** +   * The assignment part id of the project to be graded. Don't hard code this setting in .sbt or .scala, this +   * setting should remain a (command-line) parameter of the `submission/grade` task, defined when invoking sbt. +   * See also feedback string in "val gradeProjectDetailsSetting". +   */ +  val partIdOfGradingProject = SettingKey[String]("partIdOfGradingProject") + +  /** +   * The api key to access non-public api parts on coursera. This key is secret! It's defined in +   * 'submission/settings.sbt', which is not part of the handout. +   * +   * Default value 'apiKey' to make the handout sbt project work +   *  - In the handout, apiKey needs to be defined, otherwise the build doesn't compile +   *  - When correcting, we define 'apiKey' in the 'submission/sectrets.sbt' file +   *  - The value in the .sbt file will take precedence when correcting (settings in .sbt take +   *    precedence over those in .scala) +   */ +  val apiKey = SettingKey[String]("apiKey") + + +  /************************************************************ +   * GRADING INITIALIZATION +   */ + +  val initGrading = TaskKey[Unit]("initGrading") +  lazy val initGradingSetting = initGrading <<= (clean, sourceDirectory, baseDirectory) map { (_, submissionSrcDir, basedir) => +    deleteFiles(submissionSrcDir, basedir) +    GradingFeedback.initialize() +    RecordingLogger.clear() +  } + +  def deleteFiles(submissionSrcDir: File, basedir: File) { +    // don't delete anything in offline mode, useful for us when hacking testing / stylechecking +    if (!Settings.offlineMode){ +      IO.delete(submissionSrcDir) +      IO.delete(basedir / Settings.submissionJarFileName) +      IO.delete(basedir / Settings.testResultsFileName) +    } +  } + +  /** ProjectDetails of the project that we are grading */ +  val gradeProjectDetails = TaskKey[ProjectDetails]("gradeProjectDetails") + +  // here we depend on `initialize` because we already use the GradingFeedback +  lazy val gradeProjectDetailsSetting = gradeProjectDetails <<= (initGrading, partIdOfGradingProject, projectDetailsMap in assignmentProject) map { (_, partId, detailsMap) => +    detailsMap.find(_._2.assignmentPartId == partId) match { +      case Some((_, details)) => +        details +      case None => +        val validIds = detailsMap.map(_._2.assignmentPartId) +        val msgRaw = +          """Unknown assignment part id: %s +            |Valid part ids are: %s +            | +            |In order to grade a project, the `partIdOfGradingProject` setting has to be defined. If you are running +            |interactively in the sbt console, type `set (partIdOfGradingProject in submissionProject) := "idString"`. +            |When running the grading task from the command line, add the above `set` command, e.g. execute +            | +            |  sbt 'set (partIdOfGradingProject in submissionProject) := "idString"' submission/grade""" +        val msg = msgRaw.stripMargin.format(partId, validIds.mkString(", ")) + "\n " +        GradingFeedback.downloadUnpackFailed(msg) +        sys.error(msg) +    } +  } + +  val setMaxScore = TaskKey[Unit]("setMaxScore") +  val setMaxScoreSetting = setMaxScore <<= (gradeProjectDetails) map { project => +    GradingFeedback.setMaxScore(project.maxScore, project.styleScoreRatio) +  } + +  // set the maximal score before running compile / test / ... +  val setMaxScoreHook = (compile in Compile) <<= (compile in Compile).dependsOn(setMaxScore) + + +  /************************************************************ +   * DOWNLOADING AND EXTRACTING SUBMISSION +   */ + +  val getSubmission = TaskKey[Unit]("getSubmission") +  val getSubmissionSetting = getSubmission <<= (baseDirectory, scalaSource in Compile) map { (baseDir, scalaSrcDir) => +    readAndUnpackSubmission(baseDir, scalaSrcDir) +  } + +  def readAndUnpackSubmission(baseDir: File, targetSourceDir: File) { +    try { +      val jsonFile = baseDir / Settings.submissionJsonFileName +      val targetJar = baseDir / Settings.submissionJarFileName +      val res = for { +        queueResult <- { +          if (Settings.offlineMode) { +            println("[not unpacking from json file]") +            QueueResult("").successNel +          } else { +            CourseraHttp.readJsonFile(jsonFile, targetJar) +          } +        } +        _ <- { +          GradingFeedback.apiState = queueResult.apiState +          CourseraHttp.unpackJar(targetJar, targetSourceDir) +        } +      } yield () + +      res match { +        case Failure(msgs) => +          GradingFeedback.downloadUnpackFailed(msgs.list.mkString("\n")) +        case _ => +          () +      } +    } catch { +      case e: Throwable => +        // generate some useful feedback in case something fails +        GradingFeedback.downloadUnpackFailed(CourseraHttp.fullExceptionString(e)) +        throw e +    } +    if (GradingFeedback.isFailed) failDownloadUnpack() +  } + +  // dependsOn makes sure that `getSubmission` is executed *before* `unmanagedSources` +  val getSubmissionHook = (unmanagedSources in Compile) <<= (unmanagedSources in Compile).dependsOn(getSubmission) + +  def failDownloadUnpack(): Nothing = { +    sys.error("Download or Unpack failed") +  } + +  /************************************************************ +   * READING COMPILATION AND TEST COMPILATION LOGS +   */ + + +  // extraLoggers need to be defined globally. (extraLoggers in Compile) does not work - sbt only +  // looks at the global extraLoggers when creating the LogManager. +  val submissionLoggerSetting = extraLoggers ~= { currentFunction => +    (key: ScopedKey[_]) => { +      new FullLogger(RecordingLogger) +: currentFunction(key) +    } +  } + +  val readCompileLog = (compile in Compile) <<= (compile in Compile) mapR handleFailure(compileFailed) +  val readTestCompileLog = (compile in Test) <<= (compile in Test) mapR handleFailure(compileTestFailed) + +  def handleFailure[R](handler: (Incomplete, String) => Unit) = (res: Result[R]) => res match { +    case Inc(inc) => +      // Only call the handler of the task that actually failed. See comment in GradingFeedback.failed +      if (!GradingFeedback.isFailed) +        handler(inc, RecordingLogger.readAndClear()) +      throw inc +    case Value(v) => v +  } + +  def compileFailed(inc: Incomplete, log: String) { +    GradingFeedback.compileFailed(log) +  } + +  def compileTestFailed(inc: Incomplete, log: String) { +    GradingFeedback.testCompileFailed(log) +  } + + +  /************************************************************ +   * RUNNING SCALATEST +   */ + +  /** The submission project takes resource files from the main (assignment) project */ +  val resourcesFromAssignment = { +    (resourceDirectory in Compile) <<= (resourceDirectory in (assignmentProject, Compile)) +  } + +  /** +   * Only include the resource files which are defined in the package of the current project. +   */ +  val selectResourcesForProject = { +    (resources in Compile) <<= (resources in Compile, resourceDirectory in (assignmentProject, Compile), gradeProjectDetails) map { (resources, resourceDir, project) => +      val finder = resources ** filter(resourceDir, List(project.packageName)) +      finder.get +    } +  } + +  /** The submission project takes test files from the main (assignment) project */ +  val testSourcesFromAssignment = { +    (sourceDirectory in Test) <<= (sourceDirectory in (assignmentProject, Test)) +  } + +  /** +   * Only include the test files which are defined in the package of the current project. +   * Also keeps test sources in packages listed in 'gradingTestPackages' +   */ +  val selectTestsForProject = { +    (unmanagedSources in Test) <<= (unmanagedSources in Test, scalaSource in (assignmentProject, Test), gradingTestPackages in assignmentProject, gradeProjectDetails) map { (sources, testSrcScalaDir, gradingSrcs, project) => +      val finder = sources ** filter(testSrcScalaDir, gradingSrcs :+ project.packageName) +      finder.get +    } +  } + +  val scalaTestSubmission = TaskKey[Unit]("scalaTestSubmission") +  val scalaTestSubmissionSetting = scalaTestSubmission <<= +    (compile in Compile, +     compile in Test, +     fullClasspath in Test, +     copyResources in Compile, +     classDirectory in Test, +     baseDirectory) map { (_, _, classpath, resources, testClasses, basedir) => +      // we use `map`, so this is only executed if all dependencies succeed. no need to check `GradingFeedback.isFailed` +      val outfile = basedir / Settings.testResultsFileName +      val policyFile = basedir / ".." / Settings.policyFileName +      ScalaTestRunner.scalaTestGrade(classpath, testClasses, outfile, policyFile, copiedResourceFiles(resources)) +  } + + + +  /************************************************************ +   * STYLE CHECKING +   */ + +  val styleCheckSubmission = TaskKey[Unit]("styleCheckSubmission") + +  /** +   * - depend on scalaTestSubmission so that test get executed before style checking. the transitive +   *   dependencies also ensures that the "sources in Compile" don't have compilation errors +   * - using `map` makes this task execute only if all its dependencies succeeded. +   */ +  val styleCheckSubmissionSetting = styleCheckSubmission <<= (sources in Compile, scalaTestSubmission) map { (sourceFiles, _) => +    val (feedback, score) = StyleChecker.assess(sourceFiles) +    if (score == StyleChecker.maxResult) { +      GradingFeedback.perfectStyle() +    } else { +      val gradeScore = GradingFeedback.maxStyleScore * score / StyleChecker.maxResult +      GradingFeedback.styleProblems(feedback, gradeScore) +    } +  } + + + +  /************************************************************ +   * SUBMITTING GRADES TO COURSERA +   */ + +  val grade = TaskKey[Unit]("grade") + +  // mapR: submit the grade / feedback in any case, also on failure +  val gradeSetting = grade <<= (scalaTestSubmission, styleCheckSubmission, apiKey, gradeProjectDetails, streams) mapR { (_, _, apiKeyR, projectDetailsR, s) => +    val logOpt = s match { +      case Value(v) => Some(v.log) +      case _ => None +    } +    logOpt.foreach(_.info(GradingFeedback.feedbackString(html = false))) +    apiKeyR match { +      case Value(apiKey) if (!apiKey.isEmpty) => +        // if build failed early, we did not even get the api key from the submission queue +        if (!GradingFeedback.apiState.isEmpty && !Settings.offlineMode) { +          val scoreString = "%.2f".format(GradingFeedback.totalScore) +          val Value(projectDetails) = projectDetailsR +          CourseraHttp.submitGrade(GradingFeedback.feedbackString(), scoreString, GradingFeedback.apiState, apiKey, projectDetails) match { +            case Failure(msgs) => +              sys.error(msgs.list.mkString("\n")) +            case _ => +              () +          } +        } else if(Settings.offlineMode) { +          logOpt.foreach(_.info(" \nSettings.offlineMode enabled, not uploading the feedback")) +        } else { +          sys.error("Could not submit feedback - apiState not initialized") +        } +      case _ => +        sys.error("Could not submit feedback - apiKey not defined: "+ apiKeyR) +    } +  } +} + +case class ProjectDetails(packageName: String, +                          assignmentPartId: String, +                          maxScore: Double, +                          styleScoreRatio: Double, +                          courseId: String)  | 
