Small UI rework, show criteria grades when writing global feedback, export evaluation per group per assignment to MD
This commit is contained in:
@ -36,6 +36,10 @@ kotlin {
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.material3.desktop)
|
||||
implementation(libs.rtfield)
|
||||
implementation(libs.filekit.core)
|
||||
implementation(libs.filekit.dialogs)
|
||||
implementation(libs.filekit.dialogs.compose)
|
||||
implementation(libs.filekit.coil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,6 +54,10 @@ compose.desktop {
|
||||
packageName = "com.jaytux.grader"
|
||||
packageVersion = "1.0.0"
|
||||
includeAllModules = true
|
||||
|
||||
linux {
|
||||
modules("jdk.security.auth")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
package com.jaytux.grader.data
|
||||
|
||||
import com.jaytux.grader.viewmodel.Assignment
|
||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||
import io.github.vinceglb.filekit.PlatformFile
|
||||
|
||||
class MdBuilder {
|
||||
private val content = StringBuilder()
|
||||
|
||||
fun appendHeader(text: String, level: Int = 1) {
|
||||
require(level in 1..6) { "Header level must be between 1 and 6" }
|
||||
content.appendLine()
|
||||
content.appendLine("#".repeat(level) + " $text")
|
||||
content.appendLine()
|
||||
}
|
||||
fun appendMd(text: String) { content.appendLine(text) }
|
||||
fun appendParagraph(text: String, bold: Boolean = false, italic: Boolean = false) {
|
||||
val formattedText = buildString {
|
||||
if (bold) append("**")
|
||||
if (italic) append("_")
|
||||
append(text)
|
||||
if (italic) append("_")
|
||||
if (bold) append("**")
|
||||
}
|
||||
content.appendLine(formattedText)
|
||||
content.appendLine()
|
||||
}
|
||||
|
||||
fun build(): String = content.toString()
|
||||
}
|
||||
|
||||
fun GroupAssignmentState.LocalGFeedback.exportTo(path: PlatformFile, assignment: GroupAssignment) {
|
||||
val builder = MdBuilder()
|
||||
builder.appendHeader("${assignment.name} Feedback for ${group.name}")
|
||||
if(feedback.global != null && feedback.global.grade.isNotBlank()) {
|
||||
val global = feedback.global.grade
|
||||
builder.appendParagraph("Overall grade: ${feedback.global.grade}", true, true)
|
||||
|
||||
individuals.forEach { (student, it) ->
|
||||
val (_, data) = it
|
||||
if(data.global != null && data.global.grade.isNotBlank() && data.global.grade != global) {
|
||||
builder.appendParagraph("${student.name} grade: ${data.global.grade}", true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun appendFeedback(heading: String, group: GroupAssignmentState.FeedbackEntry?, byStudent: List<Pair<Student, GroupAssignmentState.FeedbackEntry>>) {
|
||||
if(group != null || byStudent.isNotEmpty()) {
|
||||
builder.appendHeader(heading, 2)
|
||||
if(group != null) {
|
||||
if(group.grade.isNotBlank()) {
|
||||
builder.appendParagraph("Group grade: ${group.grade}", true, true)
|
||||
}
|
||||
if(group.feedback.isNotBlank()) {
|
||||
builder.appendMd(group.feedback)
|
||||
}
|
||||
}
|
||||
|
||||
byStudent.forEach { (student, it) ->
|
||||
if(it.grade.isNotBlank() || it.feedback.isNotBlank()) builder.appendHeader(student.name, 3)
|
||||
if(it.grade.isNotBlank()) {
|
||||
builder.appendParagraph("Grade: ${it.grade}", true, true)
|
||||
}
|
||||
if(it.feedback.isNotBlank()) {
|
||||
builder.appendMd(it.feedback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendFeedback("Overall Feedback", feedback.global,
|
||||
individuals.mapNotNull { it.second.second.global?.let { g -> it.first to g } }
|
||||
)
|
||||
|
||||
val criteria = (feedback.byCriterion.map { (c, _) -> c } +
|
||||
individuals.flatMap { (_, it) -> it.second.byCriterion.map { (c, _) -> c } }).distinctBy { it.id.value }
|
||||
|
||||
criteria.forEach { c ->
|
||||
appendFeedback(
|
||||
c.name,
|
||||
feedback.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry,
|
||||
individuals.mapNotNull { (student, it) ->
|
||||
val entry = it.second.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry
|
||||
entry?.let { student to it }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
path.file.writeText(builder.build())
|
||||
}
|
@ -4,10 +4,13 @@ import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import com.jaytux.grader.App
|
||||
import com.jaytux.grader.data.Database
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
|
||||
fun main(){
|
||||
Database.init()
|
||||
application {
|
||||
FileKit.init(appId = "com.jaytux.grader")
|
||||
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Grader",
|
||||
|
@ -18,11 +18,15 @@ import androidx.compose.ui.unit.dp
|
||||
import com.jaytux.grader.data.GroupAssignmentCriterion
|
||||
import com.jaytux.grader.data.SoloAssignmentCriterion
|
||||
import com.jaytux.grader.data.Student
|
||||
import com.jaytux.grader.data.exportTo
|
||||
import com.jaytux.grader.maxN
|
||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
||||
import com.jaytux.grader.viewmodel.SoloAssignmentState
|
||||
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@Composable
|
||||
@ -182,10 +186,37 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
|
||||
var critIdx by remember(fdbk) { mutableStateOf(0) }
|
||||
val criteria by state.criteria.entities
|
||||
val suggestions by state.autofill.entities
|
||||
val exporting by remember { mutableStateOf(false) }
|
||||
|
||||
val onSave = { grade: String, fdbk: String ->
|
||||
when {
|
||||
studentIdx == 0 && critIdx == 0 -> state.upsertGroupFeedback(group, fdbk, grade)
|
||||
studentIdx == 0 && critIdx != 0 -> state.upsertGroupFeedback(group, fdbk, grade, criteria[critIdx - 1])
|
||||
studentIdx != 0 && critIdx == 0 -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade)
|
||||
else -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade, criteria[critIdx - 1])
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val exporter = rememberFileSaverLauncher { file ->
|
||||
file?.let {
|
||||
scope.launch { fdbk.exportTo(it, state.assignment) }
|
||||
}
|
||||
}
|
||||
|
||||
val critGrade: (Int) -> String? = { crit: Int ->
|
||||
when {
|
||||
studentIdx == 0 && crit == 0 -> feedback.global?.grade?.ifBlank { null }
|
||||
studentIdx == 0 && crit != 0 -> feedback.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
|
||||
studentIdx != 0 && crit == 0 -> individual[studentIdx - 1].second.second.global?.grade?.ifBlank { null }
|
||||
else -> individual[studentIdx - 1].second.second.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
|
||||
}.also { println("Mapping criterion #${crit} to grade ${it}") }
|
||||
}
|
||||
|
||||
Row {
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { studentIdx = 0 },
|
||||
@ -207,16 +238,36 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
{ exporter.launch("${state.assignment.name} (${fdbk.group.name})", "md") },
|
||||
Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
|
||||
) {
|
||||
Text("Export group feedback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onSave = { grade: String, fdbk: String ->
|
||||
when {
|
||||
studentIdx == 0 && critIdx == 0 -> state.upsertGroupFeedback(group, fdbk, grade)
|
||||
studentIdx == 0 && critIdx != 0 -> state.upsertGroupFeedback(group, fdbk, grade, criteria[critIdx - 1])
|
||||
studentIdx != 0 && critIdx == 0 -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade)
|
||||
else -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade, criteria[critIdx - 1])
|
||||
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||
TabRow(critIdx) {
|
||||
Tab(critIdx == 0, { critIdx = 0 }) {
|
||||
Text(
|
||||
"General feedback",
|
||||
Modifier.padding(5.dp),
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
criteria.forEachIndexed { i, c ->
|
||||
Tab(critIdx == i + 1, { critIdx = i + 1 }) {
|
||||
Text(
|
||||
c.name,
|
||||
Modifier.padding(5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(5.dp))
|
||||
|
||||
groupFeedbackPane(
|
||||
criteria, critIdx, { critIdx = it },
|
||||
@ -226,10 +277,12 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
|
||||
studentIdx != 0 && critIdx == 0 -> individual[studentIdx - 1].second.second.global
|
||||
else -> individual[studentIdx - 1].second.second.byCriterion[critIdx - 1].entry
|
||||
},
|
||||
suggestions, onSave, Modifier.weight(0.75f).padding(10.dp),
|
||||
suggestions, onSave,
|
||||
if(critIdx == 0 && criteria.isNotEmpty()) criteria.mapIndexed { idx, it -> it.name to critGrade(idx + 1) } else null,
|
||||
key = studentIdx to critIdx
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -240,6 +293,7 @@ fun groupFeedbackPane(
|
||||
rawFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
autofill: List<String>,
|
||||
onSave: (String, String) -> Unit,
|
||||
critGrades: List<Pair<String, String?>>? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
key: Any? = null
|
||||
) {
|
||||
@ -252,7 +306,7 @@ fun groupFeedbackPane(
|
||||
|
||||
Column(modifier) {
|
||||
Row {
|
||||
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||
Spacer(Modifier.weight(0.6f))
|
||||
Button(
|
||||
@ -262,14 +316,29 @@ fun groupFeedbackPane(
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
TabRow(currentCriterion) {
|
||||
Tab(currentCriterion == 0, { onSelectCriterion(0) }) { Text("General feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic) }
|
||||
criteria.forEachIndexed { i, c ->
|
||||
Tab(currentCriterion == i + 1, { onSelectCriterion(i + 1) }) { Text(c.name, Modifier.padding(5.dp)) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
|
||||
Row {
|
||||
RichTextField(feedback, outerModifier = Modifier.weight(0.7f).fillMaxHeight()) { Text("Feedback") }
|
||||
critGrades?.let { grades ->
|
||||
Spacer(Modifier.width(10.dp))
|
||||
LazyColumn(Modifier.weight(0.3f)) {
|
||||
item {
|
||||
Text("Criteria grades", Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
|
||||
}
|
||||
items(grades) { (crit, grade) ->
|
||||
Column {
|
||||
Text(crit, Modifier.padding(5.dp), fontWeight = FontWeight.Bold)
|
||||
Row {
|
||||
Spacer(Modifier.width(5.dp))
|
||||
if(grade == null) Text("(no grade yet)", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||
else Text(grade, Modifier.padding(5.dp))
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,8 +243,8 @@ fun RichTextStyleButton(
|
||||
@Composable
|
||||
fun RichTextField(
|
||||
state: RichTextState,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonsModifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
buttonsModifier: Modifier = Modifier.fillMaxWidth(),
|
||||
outerModifier: Modifier = Modifier,
|
||||
label: @Composable (() -> Unit)? = null
|
||||
) = Column(outerModifier) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
androidx-lifecycle = "2.8.4"
|
||||
compose-multiplatform = "1.7.0"
|
||||
compose-multiplatform = "1.8.1"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.1.0"
|
||||
kotlinx-coroutines = "1.10.1"
|
||||
@ -9,6 +9,7 @@ material3 = "1.7.3"
|
||||
ui-android = "1.7.8"
|
||||
foundation-layout-android = "1.7.8"
|
||||
rtf = "1.0.0-rc11"
|
||||
filekit = "0.10.0-beta04"
|
||||
|
||||
[libraries]
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
@ -30,6 +31,10 @@ material-icons = { group = "org.jetbrains.compose.material", name = "material-ic
|
||||
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
||||
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
||||
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
|
||||
filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
|
||||
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
||||
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
||||
filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" }
|
||||
|
||||
[plugins]
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
|
Reference in New Issue
Block a user