Skip to content

Commit

Permalink
Merge pull request #235 from code-freak/feature/csv-export
Browse files Browse the repository at this point in the history
Add CSV export
  • Loading branch information
erikhofer committed Oct 21, 2019
2 parents 36bd3a0 + b4d9bd4 commit 4af670d
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 3 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {
implementation 'org.webjars:font-awesome:5.8.1'
implementation 'com.spotify:docker-client:8.15.1'
implementation 'org.apache.commons:commons-compress:1.19'
implementation 'org.apache.commons:commons-csv:1.7'
implementation 'io.sentry:sentry-logback:1.7.16'
implementation 'com.github.hsingh:java-shortuuid:2a4d72f'
implementation 'com.github.Open-MBEE:junit-xml-parser:1.0.0'
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/de/code_freak/codefreak/entity/Evaluation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.time.Instant
import javax.persistence.Entity
import javax.persistence.ManyToOne
import javax.persistence.OneToMany
import javax.persistence.OrderBy

@Entity
class Evaluation(
Expand All @@ -19,6 +20,7 @@ class Evaluation(
var filesDigest: ByteArray,

@OneToMany
@OrderBy("position ASC")
var results: List<EvaluationResult>
) : BaseEntity() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import de.code_freak.codefreak.service.GitImportService
import de.code_freak.codefreak.service.evaluation.EvaluationService
import de.code_freak.codefreak.util.TarUtil
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.security.access.annotation.Secured
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
Expand All @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
import org.springframework.web.servlet.mvc.support.RedirectAttributes
import java.io.ByteArrayOutputStream
import java.util.UUID
Expand Down Expand Up @@ -127,4 +129,12 @@ class AssignmentController : BaseController() {
model.addAttribute("evaluationViewModels", evaluationViewModels)
return "submissions"
}

@Secured(Authority.ROLE_TEACHER)
@GetMapping("/assignments/{id}/submissions.csv")
fun getSubmissionsCsv(@PathVariable("id") assignmentId: UUID): ResponseEntity<StreamingResponseBody> {
val assignment = assignmentService.findAssignment(assignmentId)
val csv = submissionService.generateSubmissionCsv(assignment)
return download("${assignment.title}-submissions.csv", csv.byteInputStream())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.code_freak.codefreak.service

import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import org.springframework.stereotype.Service

@Service
class SpreadsheetService : BaseService() {

fun generateCsv(headers: List<String>, rows: Iterable<Iterable<String>>): String {
val output = StringBuilder()
val outWriter = CSVPrinter(output, CSVFormat.EXCEL)
outWriter.printRecord(headers)
rows.forEach(outWriter::printRecord)
return output.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import de.code_freak.codefreak.entity.Assignment
import de.code_freak.codefreak.entity.Submission
import de.code_freak.codefreak.entity.User
import de.code_freak.codefreak.repository.SubmissionRepository
import de.code_freak.codefreak.service.evaluation.EvaluationService
import de.code_freak.codefreak.service.file.FileService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
Expand All @@ -20,6 +21,15 @@ class SubmissionService : BaseService() {
@Autowired
lateinit var fileService: FileService

@Autowired
private lateinit var taskService: TaskService

@Autowired
private lateinit var evaluationService: EvaluationService

@Autowired
private lateinit var spreadsheetService: SpreadsheetService

@Transactional
fun findSubmission(id: UUID): Submission = submissionRepository.findById(id)
.orElseThrow { EntityNotFoundException("Submission not found") }
Expand All @@ -39,4 +49,44 @@ class SubmissionService : BaseService() {
val submission = Submission(assignment = assignment, user = user)
return submissionRepository.save(submission)
}

fun generateSubmissionCsv(assignment: Assignment): String {
// store a list of (task -> evaluation steps) that represents each column in our table
val columnDefinitions = assignment.tasks.flatMap { task ->
taskService.getTaskDefinition(task.id).evaluation.mapIndexed { index, evaluationDefinition ->
Triple(task, index, evaluationDefinition)
}
}
// generate the header columns. In CSV files we have no option to join columns so we have to create a flat
// list of task-evaluation combinations
// [EMPTY] | Task #1 Eval #1 | Task #1 Eval #2 | Task #2 Eval #1 | ...
val resultTitles = columnDefinitions.map { (task, _, evaluationDefinition) ->
"${task.title} (${evaluationDefinition.step})"
}
val titleCols = mutableListOf("User").apply {
addAll(resultTitles)
}
val submissions = findSubmissionsOfAssignment(assignment.id)

// generate the actual data rows for each submission
val rows = submissions.map { submission ->
val resultCols = columnDefinitions.map { (task, index, _) ->
val answer = submission.getAnswer(task.id)
val evaluation = answer?.id?.let { evaluationService.getLatestEvaluation(it).orElse(null) }
val result = evaluation?.results?.get(index)?.let { evaluationService.getSummary(it).toString() }
when {
answer == null -> "[no answer]"
evaluation == null -> "[no evaluation]"
else -> result ?: "[no result]"
}
}

// all columns combined starting with the username
mutableListOf(submission.user.username).apply {
addAll(resultCols)
}
}

return spreadsheetService.generateCsv(titleCols, rows)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.code_freak.codefreak.service.evaluation
import de.code_freak.codefreak.entity.Answer
import de.code_freak.codefreak.entity.Assignment
import de.code_freak.codefreak.entity.Evaluation
import de.code_freak.codefreak.entity.EvaluationResult
import de.code_freak.codefreak.repository.EvaluationRepository
import de.code_freak.codefreak.service.BaseService
import de.code_freak.codefreak.service.ContainerService
Expand Down Expand Up @@ -67,6 +68,14 @@ class EvaluationService : BaseService() {

fun isEvaluationRunningOrQueued(answerId: UUID) = evaluationQueue.isQueued(answerId) || evaluationQueue.isRunning(answerId)

fun getSummary(evaluationResult: EvaluationResult): Any {
return getEvaluationRunner(evaluationResult.runnerName).let {
it.getSummary(
it.parseResultContent(evaluationResult.content)
)
}
}

fun isEvaluationUpToDate(answerId: UUID): Boolean = getLatestEvaluation(answerId).map {
it.filesDigest.contentEquals(fileService.getCollectionMd5Digest(answerId))
}.orElse(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class CommandLineRunner : EvaluationRunner {
private constructor() : this ("", ExecResult("", 0))
}

private data class Summary(val total: Int, val passed: Int)
private data class Summary(val total: Int, val passed: Int) {
override fun toString() = "$passed/$total"
}

@Autowired
private lateinit var containerService: ContainerService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ class JUnitRunner : CommandLineRunner() {
private constructor() : this(emptyList(), emptyList())
}

private data class Summary(val error: Boolean, val total: Int, val passed: Int)
private data class Summary(val error: Boolean, val total: Int, val passed: Int) {
override fun toString() = if (error) "error" else "$passed/$total"
}

private val mapper = ObjectMapper()

Expand Down
12 changes: 11 additions & 1 deletion src/main/resources/templates/submissions.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
There are no submissions yet.
</div>

<form th:action="${@urls.get(assignment) + '/evaluations'}" method="post" th:unless="${submissions.isEmpty()}">
<form th:action="${@urls.get(assignment) + '/evaluations'}" method="post" th:unless="${submissions.isEmpty()}" class="d-inline-block">
<button th:unless="${upToDate}"
class="btn btn-primary"><i class="fas fa-play"></i> Evaluate all submissions</button>
<button th:if="${upToDate}"
Expand All @@ -24,6 +24,16 @@
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>

<div class="d-inline-block dropdown" th:unless="${submissions.isEmpty()}">
<button class="btn btn-light dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fas fa-download"></i> Download submissions…
</button>
<div class="dropdown-menu">
<a class="dropdown-item"
th:href="${@urls.get(assignment) + '/submissions.csv'}"><i class="fas fa-file-csv"></i> CSV Spreadsheet</a>
</div>
</div>

<div class="alert alert-info my-4 pending-evaluations"
th:data-assignment-id="${assignment.id}"
th:if="${#lists.size(runningEvaluations)} > 0">
Expand Down

0 comments on commit 4af670d

Please sign in to comment.