Added evaluation criteria

This commit is contained in:
jay-tux 2025-03-27 18:18:15 +01:00
parent 034b018e2d
commit a7aafccd19
Signed by: jay-tux
GPG Key ID: 84302006B056926E
6 changed files with 566 additions and 156 deletions

View File

@ -48,6 +48,7 @@ compose.desktop {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.jaytux.grader" packageName = "com.jaytux.grader"
packageVersion = "1.0.0" packageVersion = "1.0.0"
includeAllModules = true
} }
} }
} }

View File

@ -1,6 +1,5 @@
package com.jaytux.grader.data package com.jaytux.grader.data
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.id.CompositeIdTable import org.jetbrains.exposed.dao.id.CompositeIdTable
import org.jetbrains.exposed.dao.id.UUIDTable import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
@ -59,6 +58,12 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
val deadline = datetime("deadline") val deadline = datetime("deadline")
} }
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val name = varchar("name", 50)
val desc = text("description")
}
object SoloAssignments : UUIDTable("soloAssgmts") { object SoloAssignments : UUIDTable("soloAssgmts") {
val editionId = reference("edition_id", Editions.id) val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable() val number = integer("number").nullable()
@ -67,6 +72,12 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
val deadline = datetime("deadline") val deadline = datetime("deadline")
} }
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
val name = varchar("name", 50)
val desc = text("description")
}
object PeerEvaluations : UUIDTable("peerEvals") { object PeerEvaluations : UUIDTable("peerEvals") {
val editionId = reference("edition_id", Editions.id) val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable() val number = integer("number").nullable()
@ -75,6 +86,7 @@ object PeerEvaluations : UUIDTable("peerEvals") {
object GroupFeedbacks : CompositeIdTable("grpFdbks") { object GroupFeedbacks : CompositeIdTable("grpFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignments.id).nullable()
val groupId = reference("group_id", Groups.id) val groupId = reference("group_id", Groups.id)
val feedback = text("feedback") val feedback = text("feedback")
val grade = varchar("grade", 32) val grade = varchar("grade", 32)
@ -84,6 +96,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") {
object IndividualFeedbacks : CompositeIdTable("indivFdbks") { object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignments.id).nullable()
val groupId = reference("group_id", Groups.id) val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id) val studentId = reference("student_id", Students.id)
val feedback = text("feedback") val feedback = text("feedback")
@ -94,6 +107,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
object SoloFeedbacks : CompositeIdTable("soloFdbks") { object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id) val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id)
val criterionId = reference("criterion_id", SoloAssignments.id).nullable()
val studentId = reference("student_id", Students.id) val studentId = reference("student_id", Students.id)
val feedback = text("feedback") val feedback = text("feedback")
val grade = varchar("grade", 32) val grade = varchar("grade", 32)

View File

@ -11,7 +11,7 @@ object Database {
SchemaUtils.create( SchemaUtils.create(
Courses, Editions, Groups, Courses, Editions, Groups,
Students, GroupStudents, EditionStudents, Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments, GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks, GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation, PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
StudentToGroupEvaluation StudentToGroupEvaluation
@ -20,7 +20,7 @@ object Database {
val addMissing = SchemaUtils.addMissingColumnsStatements( val addMissing = SchemaUtils.addMissingColumnsStatements(
Courses, Editions, Groups, Courses, Editions, Groups,
Students, GroupStudents, EditionStudents, Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments, GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks, GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation, PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
StudentToGroupEvaluation StudentToGroupEvaluation

View File

@ -1,10 +1,9 @@
package com.jaytux.grader.data package com.jaytux.grader.data
import com.jaytux.grader.data.GroupAssignment.Companion.referrersOn
import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.UUID import java.util.UUID
class Course(id: EntityID<UUID>) : Entity<UUID>(id) { class Course(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -61,6 +60,16 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by GroupAssignments.name var name by GroupAssignments.name
var assignment by GroupAssignments.assignment var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline var deadline by GroupAssignments.deadline
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
}
class GroupAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, GroupAssignmentCriterion>(GroupAssignmentCriteria)
var assignment by GroupAssignment referencedOn GroupAssignmentCriteria.assignmentId
var name by GroupAssignmentCriteria.name
var description by GroupAssignmentCriteria.desc
} }
class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) { class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -71,6 +80,35 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by SoloAssignments.name var name by SoloAssignments.name
var assignment by SoloAssignments.assignment var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline var deadline by SoloAssignments.deadline
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
}
class SoloAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, SoloAssignmentCriterion>(SoloAssignmentCriteria)
var assignment by SoloAssignment referencedOn SoloAssignmentCriteria.assignmentId
var name by SoloAssignmentCriteria.name
var description by SoloAssignmentCriteria.desc
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SoloAssignmentCriterion
if (name != other.name) return false
if (description != other.description) return false
return true
}
override fun hashCode(): Int {
var result = assignment.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode()
return result
}
} }
class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) { class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) {

View File

@ -1,7 +1,5 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
@ -9,8 +7,6 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
@ -20,19 +16,23 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jaytux.grader.data.GroupAssignment
import com.jaytux.grader.data.GroupAssignmentCriterion
import com.jaytux.grader.data.SoloAssignmentCriterion
import com.jaytux.grader.data.Student import com.jaytux.grader.data.Student
import com.jaytux.grader.viewmodel.GroupAssignmentState import com.jaytux.grader.viewmodel.GroupAssignmentState
import com.jaytux.grader.viewmodel.PeerEvaluationState import com.jaytux.grader.viewmodel.PeerEvaluationState
import com.jaytux.grader.viewmodel.SoloAssignmentState import com.jaytux.grader.viewmodel.SoloAssignmentState
import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
import kotlinx.datetime.LocalDateTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GroupAssignmentView(state: GroupAssignmentState) { fun GroupAssignmentView(state: GroupAssignmentState) {
val task by state.task val task by state.task
val deadline by state.deadline val deadline by state.deadline
val allFeedback by state.feedback.entities val allFeedback by state.feedback.entities
val criteria by state.criteria.entities
var idx by remember(state) { mutableStateOf(0) } var idx by remember(state) { mutableStateOf(0) }
@ -45,7 +45,7 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
} }
TabRow(idx) { TabRow(idx) {
Tab(idx == 0, { idx = 0 }) { Text("Assignment") } Tab(idx == 0, { idx = 0 }) { Text("Task and Criteria") }
allFeedback.forEachIndexed { i, it -> allFeedback.forEachIndexed { i, it ->
val (group, feedback) = it val (group, feedback) = it
Tab(idx == i + 1, { idx = i + 1 }) { Tab(idx == i + 1, { idx = i + 1 }) {
@ -55,12 +55,75 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
} }
if(idx == 0) { if(idx == 0) {
val updTask = rememberRichTextState() groupTaskWidget(
task, deadline, criteria,
onSetTask = { state.updateTask(it) },
onSetDeadline = { state.updateDeadline(it) },
onAddCriterion = { state.addCriterion(it) },
onModCriterion = { c, n, d -> state.updateCriterion(c, n, d) },
onRmCriterion = { state.deleteCriterion(it) }
)
}
else {
groupFeedback(state, allFeedback[idx - 1].second)
}
}
}
LaunchedEffect(task) { updTask.setMarkdown(task) } @OptIn(ExperimentalMaterial3Api::class)
@Composable
fun groupTaskWidget(
taskMD: String,
deadline: LocalDateTime,
criteria: List<GroupAssignmentCriterion>,
onSetTask: (String) -> Unit,
onSetDeadline: (LocalDateTime) -> Unit,
onAddCriterion: (name: String) -> Unit,
onModCriterion: (cr: GroupAssignmentCriterion, name: String, desc: String) -> Unit,
onRmCriterion: (cr: GroupAssignmentCriterion) -> Unit
) {
var critIdx by remember { mutableStateOf(0) }
var adding by remember { mutableStateOf(false) }
var confirming by remember { mutableStateOf(false) }
Row { Row {
DateTimePicker(deadline, { state.updateDeadline(it) }) Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
Column(Modifier.padding(10.dp)) {
LazyColumn(Modifier.weight(1f)) {
item {
Surface(
Modifier.fillMaxWidth().clickable { critIdx = 0 },
tonalElevation = if (critIdx == 0) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) {
Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
}
}
itemsIndexed(criteria) { i, crit ->
Surface(
Modifier.fillMaxWidth().clickable { critIdx = i + 1 },
tonalElevation = if (critIdx == i + 1) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) {
Text(crit.name, Modifier.padding(5.dp))
}
}
}
Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
Text("Add evaluation criterion")
}
}
}
Box(Modifier.weight(0.75f).padding(10.dp)) {
if (critIdx == 0) {
val updTask = rememberRichTextState()
LaunchedEffect(taskMD) { updTask.setMarkdown(taskMD) }
Column {
Row {
DateTimePicker(deadline, onSetDeadline)
} }
RichTextStyleRow(state = updTask) RichTextStyleRow(state = updTask)
OutlinedRichTextEditor( OutlinedRichTextEditor(
@ -70,10 +133,53 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
minLines = 5, minLines = 5,
label = { Text("Task") } label = { Text("Task") }
) )
CancelSaveRow(true, { updTask.setMarkdown(task) }, "Reset", "Update") { state.updateTask(updTask.toMarkdown()) } CancelSaveRow(
true,
{ updTask.setMarkdown(taskMD) },
"Reset",
"Update"
) { onSetTask(updTask.toMarkdown()) }
}
} }
else { else {
groupFeedback(state, allFeedback[idx - 1].second) val crit = criteria[critIdx - 1]
var name by remember(crit) { mutableStateOf(crit.name) }
var desc by remember(crit) { mutableStateOf(crit.description) }
Column {
Row {
OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
Spacer(Modifier.weight(0.1f))
Button({ onModCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
Text("Update")
}
}
OutlinedTextField(
desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
label = { Text("Description") },
singleLine = false,
minLines = 5
)
Button({ confirming = true }, Modifier.fillMaxWidth()) {
Text("Remove criterion")
}
}
}
}
}
if(adding) {
AddStringDialog(
"Evaluation criterion name", criteria.map{ it.name }, { adding = false }
) { onAddCriterion(it) }
}
if(confirming && critIdx != 0) {
ConfirmDeleteDialog(
"an evaluation criterion",
{ confirming = false }, { onRmCriterion(criteria[critIdx - 1]); critIdx = 0 }
) {
Text(criteria[critIdx - 1].name)
} }
} }
} }
@ -81,9 +187,9 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
@Composable @Composable
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) { fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
val (group, feedback, individual) = fdbk val (group, feedback, individual) = fdbk
var grade by remember(fdbk) { mutableStateOf(feedback?.grade ?: "") }
var msg by remember(fdbk) { mutableStateOf(TextFieldValue(feedback?.feedback ?: "")) }
var idx by remember(fdbk) { mutableStateOf(0) } var idx by remember(fdbk) { mutableStateOf(0) }
var critIdx by remember(fdbk) { mutableStateOf(0) }
val criteria by state.criteria.entities
val suggestions by state.autofill.entities val suggestions by state.autofill.entities
Row { Row {
@ -112,44 +218,86 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
} }
} }
Column(Modifier.weight(0.75f).padding(10.dp)) { val updateGrade = { grade: String ->
if(idx == 0) { if(idx == 0) {
Row { state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade)
Text("Grade: ", Modifier.align(Alignment.CenterVertically)) }
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) else {
Spacer(Modifier.weight(0.6f)) val ind = individual[idx - 1]
Button({ state.upsertGroupFeedback(group, msg.text, grade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically), val glob = ind.second.second.global
enabled = grade.isNotBlank() || msg.text.isNotBlank()) { state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade)
Text("Save")
} }
} }
AutocompleteLineField( val updateFeedback = { fdbk: String ->
msg, { msg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } if(idx == 0) {
) { filter -> if(critIdx == 0) {
suggestions.filter { x -> x.trim().startsWith(filter.trim()) } state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null)
}
else {
val current = feedback.byCriterion[critIdx - 1]
state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion)
} }
} }
else { else {
val (student, details) = individual[idx - 1] val ind = individual[idx - 1]
var sGrade by remember { mutableStateOf(details.second?.grade ?: "") } if(critIdx == 0) {
var sMsg by remember { mutableStateOf(TextFieldValue(details.second?.feedback ?: "")) } val entry = ind.second.second
Row { state.upsertIndividualFeedback(ind.first, group, fdbk, entry.global?.grade ?: "", null)
Text("Grade: ", Modifier.align(Alignment.CenterVertically)) }
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f)) else {
Spacer(Modifier.weight(0.6f)) val entry = ind.second.second.byCriterion[critIdx - 1]
Button({ state.upsertIndividualFeedback(student, group, sMsg.text, sGrade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically), state.upsertIndividualFeedback(ind.first, group, fdbk, entry.entry?.grade ?: "", entry.criterion)
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()) { }
Text("Save")
} }
} }
groupFeedbackPane(
criteria, critIdx, { critIdx = it }, feedback.global,
if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry,
suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp)
)
}
}
@Composable
fun groupFeedbackPane(
criteria: List<GroupAssignmentCriterion>,
currentCriterion: Int,
onSelectCriterion: (Int) -> Unit,
globFeedback: GroupAssignmentState.FeedbackEntry?,
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
autofill: List<String>,
onSetGrade: (String) -> Unit,
onSetFeedback: (String) -> Unit,
modifier: Modifier = Modifier
) {
var grade by remember(globFeedback) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
Column(modifier) {
Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button(
{ onSetGrade(grade); onSetFeedback(feedback.text) },
Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
) {
Text("Save")
}
}
TabRow(currentCriterion) {
Tab(currentCriterion == 0, { onSelectCriterion(0) }) { Text("General feedback", fontStyle = FontStyle.Italic) }
criteria.forEachIndexed { i, c ->
Tab(currentCriterion == i + 1, { onSelectCriterion(i + 1) }) { Text(c.name) }
}
}
Spacer(Modifier.height(5.dp))
AutocompleteLineField( AutocompleteLineField(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter -> ) { filter ->
suggestions.filter { x -> x.trim().startsWith(filter.trim()) } autofill.filter { x -> x.trim().startsWith(filter.trim()) }
}
}
} }
} }
} }
@ -161,13 +309,43 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
val deadline by state.deadline val deadline by state.deadline
val suggestions by state.autofill.entities val suggestions by state.autofill.entities
val grades by state.feedback.entities val grades by state.feedback.entities
val criteria by state.criteria.entities
var idx by remember(state) { mutableStateOf(0) } var tab by remember(state) { mutableStateOf(0) }
var idx by remember(state, tab) { mutableStateOf(0) }
var critIdx by remember(state, tab, idx) { mutableStateOf(0) }
var adding by remember(state, tab) { mutableStateOf(false) }
var confirming by remember(state, tab) { mutableStateOf(false) }
val updateGrade = { grade: String ->
state.upsertFeedback(
grades[idx].first,
if(critIdx == 0) grades[idx].second.global?.feedback else grades[idx].second.byCriterion[critIdx - 1].second?.feedback,
grade,
if(critIdx == 0) null else criteria[critIdx - 1]
)
}
val updateFeedback = { feedback: String ->
state.upsertFeedback(
grades[idx].first,
feedback,
if(critIdx == 0) grades[idx].second.global?.grade else grades[idx].second.byCriterion[critIdx - 1].second?.grade,
if(critIdx == 0) null else criteria[critIdx - 1]
)
}
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row { Row {
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) { Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) { Column(Modifier.padding(10.dp)) {
TabRow(tab) {
Tab(tab == 0, { tab = 0 }) { Text("Task/Criteria") }
Tab(tab == 1, { tab = 1 }) { Text("Students") }
}
LazyColumn(Modifier.weight(1f)) {
if (tab == 0) {
item { item {
Surface( Surface(
Modifier.fillMaxWidth().clickable { idx = 0 }, Modifier.fillMaxWidth().clickable { idx = 0 },
@ -178,11 +356,21 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
} }
} }
itemsIndexed(grades.toList()) { i, (student, _) -> itemsIndexed(criteria) { i, crit ->
Surface( Surface(
Modifier.fillMaxWidth().clickable { idx = i + 1 }, Modifier.fillMaxWidth().clickable { idx = i + 1 },
tonalElevation = if (idx == i + 1) 50.dp else 0.dp, tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) {
Text(crit.name, Modifier.padding(5.dp))
}
}
} else {
itemsIndexed(grades.toList()) { i, (student, _) ->
Surface(
Modifier.fillMaxWidth().clickable { idx = i },
tonalElevation = if (idx == i) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) { ) {
Text(student.name, Modifier.padding(5.dp)) Text(student.name, Modifier.padding(5.dp))
} }
@ -190,7 +378,16 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
} }
} }
if (tab == 0) {
Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
Text("Add evaluation criterion")
}
}
}
}
Column(Modifier.weight(0.75f).padding(10.dp)) { Column(Modifier.weight(0.75f).padding(10.dp)) {
if(tab == 0) {
if (idx == 0) { if (idx == 0) {
val updTask = rememberRichTextState() val updTask = rememberRichTextState()
@ -214,29 +411,97 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
"Update" "Update"
) { state.updateTask(updTask.toMarkdown()) } ) { state.updateTask(updTask.toMarkdown()) }
} else { } else {
val (student, fg) = grades[idx - 1] val crit = criteria[idx - 1]
var sGrade by remember(idx) { mutableStateOf(fg?.grade ?: "") } var name by remember(crit) { mutableStateOf(crit.name) }
var sMsg by remember(idx) { mutableStateOf(TextFieldValue(fg?.feedback ?: "")) } var desc by remember(crit) { mutableStateOf(crit.description) }
Column {
Row { Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically)) OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f)) Spacer(Modifier.weight(0.1f))
Button({ state.updateCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
Text("Update")
}
}
OutlinedTextField(
desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
label = { Text("Description") },
singleLine = false,
minLines = 5
)
Button({ confirming = true }, Modifier.fillMaxWidth()) {
Text("Remove criterion")
}
}
}
}
else {
soloFeedbackPane(
criteria, critIdx, { critIdx = it }, grades[idx].second.global,
if(critIdx == 0) grades[idx].second.global else grades[idx].second.byCriterion[critIdx - 1].second,
suggestions, updateGrade, updateFeedback,
key = tab to idx
)
}
}
}
}
if(adding) {
AddStringDialog(
"Evaluation criterion name", criteria.map{ it.name }, { adding = false }
) { state.addCriterion(it) }
}
if(confirming && idx != 0) {
ConfirmDeleteDialog(
"an evaluation criterion",
{ confirming = false }, { state.deleteCriterion(criteria[idx - 1]); idx = 0 }
) {
Text(criteria[idx - 1].name)
}
}
}
@Composable
fun soloFeedbackPane(
criteria: List<SoloAssignmentCriterion>,
currentCriterion: Int,
onSelectCriterion: (Int) -> Unit,
globFeedback: SoloAssignmentState.LocalFeedback?,
criterionFeedback: SoloAssignmentState.LocalFeedback?,
autofill: List<String>,
onSetGrade: (String) -> Unit,
onSetFeedback: (String) -> Unit,
modifier: Modifier = Modifier,
key: Any? = null
) {
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
Column(modifier) {
Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f)) Spacer(Modifier.weight(0.6f))
Button( Button(
{ state.upsertFeedback(student, sMsg.text, sGrade) }, { onSetGrade(grade); onSetFeedback(feedback.text) },
Modifier.weight(0.2f).align(Alignment.CenterVertically), Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank() enabled = grade.isNotBlank() || feedback.text.isNotBlank()
) { ) {
Text("Save") Text("Save")
} }
} }
TabRow(currentCriterion) {
Tab(currentCriterion == 0, { onSelectCriterion(0) }) { Text("General feedback", fontStyle = FontStyle.Italic) }
criteria.forEachIndexed { i, c ->
Tab(currentCriterion == i + 1, { onSelectCriterion(i + 1) }) { Text(c.name) }
}
}
Spacer(Modifier.height(5.dp))
AutocompleteLineField( AutocompleteLineField(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter -> ) { filter ->
suggestions.filter { x -> x.trim().startsWith(filter.trim()) } autofill.filter { x -> x.trim().startsWith(filter.trim()) }
}
}
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf
import com.jaytux.grader.data.* import com.jaytux.grader.data.*
import com.jaytux.grader.data.EditionStudents.editionId import com.jaytux.grader.data.EditionStudents.editionId
import com.jaytux.grader.data.EditionStudents.studentId import com.jaytux.grader.data.EditionStudents.studentId
import com.jaytux.grader.viewmodel.GroupAssignmentState.*
import kotlinx.datetime.* import kotlinx.datetime.*
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@ -445,18 +446,27 @@ class GroupState(val group: Group) {
} }
class GroupAssignmentState(val assignment: GroupAssignment) { class GroupAssignmentState(val assignment: GroupAssignment) {
data class LocalFeedback(val feedback: String, val grade: String) data class FeedbackEntry(val feedback: String, val grade: String)
data class LocalCriterionFeedback(
val criterion: GroupAssignmentCriterion, val entry: FeedbackEntry?
)
data class LocalFeedback(
val global: FeedbackEntry?, val byCriterion: List<LocalCriterionFeedback>
)
data class LocalGFeedback( data class LocalGFeedback(
val group: Group, val group: Group,
val feedback: LocalFeedback?, val feedback: LocalFeedback,
val individuals: List<Pair<Student, Pair<String?, LocalFeedback?>>> val individuals: List<Pair<Student, Pair<String?, LocalFeedback>>>
) )
val editionCourse = transaction { assignment.edition.course to assignment.edition } val editionCourse = transaction { assignment.edition.course to assignment.edition }
private val _name = mutableStateOf(assignment.name); val name = _name.immutable() private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
val feedback = RawDbState { loadFeedback() }
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState {
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList()
}
val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState { val autofill = RawDbState {
val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap { val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
@ -471,50 +481,63 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
} }
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> { private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
val individuals = IndividualFeedbacks.selectAll().where { return Group.find {
IndividualFeedbacks.groupAssignmentId eq assignment.id
}.map {
it[IndividualFeedbacks.studentId] to LocalFeedback(it[IndividualFeedbacks.feedback], it[IndividualFeedbacks.grade])
}.associate { it }
val groupFeedbacks = GroupFeedbacks.selectAll().where {
GroupFeedbacks.groupAssignmentId eq assignment.id
}.map {
it[GroupFeedbacks.groupId] to (it[GroupFeedbacks.feedback] to it[GroupFeedbacks.grade])
}.associate { it }
val groups = Group.find {
(Groups.editionId eq assignment.edition.id) (Groups.editionId eq assignment.edition.id)
}.sortAsc(Groups.name).map { group -> }.sortAsc(Groups.name).map { group ->
val students = group.studentRoles.sortedBy { it.student.name }.map { sR -> // step 1: group-level feedback, including criteria
val student = sR.student val forGroup = GroupFeedbacks.selectAll().where {
val role = sR.role (GroupFeedbacks.groupAssignmentId eq assignment.id) and
val feedback = individuals[student.id] (GroupFeedbacks.groupId eq group.id)
}.associate {
val criterion = it[GroupFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] }
val fe = FeedbackEntry(it[GroupFeedbacks.feedback], it[GroupFeedbacks.grade])
criterion to fe
}
val feedback = LocalFeedback(
global = forGroup[null],
byCriterion = criteria.entities.value.map { c -> LocalCriterionFeedback(c, forGroup[c]) }
)
student to (role to feedback) // step 2: individual feedback
val individuals = group.studentRoles.map { sr ->
val student = sr.student
val role = sr.role
val forStudent = IndividualFeedbacks.selectAll().where {
(IndividualFeedbacks.groupAssignmentId eq assignment.id) and
(IndividualFeedbacks.groupId eq group.id) and
(IndividualFeedbacks.studentId eq student.id)
}.associate {
val criterion = it[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] }
val fe = FeedbackEntry(it[IndividualFeedbacks.feedback], it[IndividualFeedbacks.grade])
criterion to fe
}
val studentFeedback = LocalFeedback(
global = forStudent[null],
byCriterion = criteria.entities.value.map { c -> LocalCriterionFeedback(c, forStudent[c]) }
)
student to (role to studentFeedback)
}.sortedBy { it.first.name }
group to LocalGFeedback(group, feedback, individuals)
}
} }
groupFeedbacks[group.id]?.let { (f, g) -> fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
group to LocalGFeedback(group, LocalFeedback(f, g), students)
} ?: (group to LocalGFeedback(group, null, students))
}
return groups
}
fun upsertGroupFeedback(group: Group, msg: String, grd: String) {
transaction { transaction {
GroupFeedbacks.upsert { GroupFeedbacks.upsert {
it[groupAssignmentId] = assignment.id it[groupAssignmentId] = assignment.id
it[groupId] = group.id it[groupId] = group.id
it[this.feedback] = msg it[this.feedback] = msg
it[this.grade] = grd it[this.grade] = grd
it[criterionId] = criterion?.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()
} }
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) { fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
transaction { transaction {
IndividualFeedbacks.upsert { IndividualFeedbacks.upsert {
it[groupAssignmentId] = assignment.id it[groupAssignmentId] = assignment.id
@ -522,6 +545,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[studentId] = student.id it[studentId] = student.id
it[this.feedback] = msg it[this.feedback] = msg
it[this.grade] = grd it[this.grade] = grd
it[criterionId] = criterion?.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()
@ -540,16 +564,48 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
} }
_deadline.value = d _deadline.value = d
} }
fun addCriterion(name: String) {
transaction {
GroupAssignmentCriterion.new {
this.name = name;
this.description = "";
this.assignment = this@GroupAssignmentState.assignment
}
criteria.refresh()
}
}
fun updateCriterion(criterion: GroupAssignmentCriterion, name: String, desc: String) {
transaction {
criterion.name = name
criterion.description = desc
}
criteria.refresh()
}
fun deleteCriterion(criterion: GroupAssignmentCriterion) {
transaction {
GroupFeedbacks.deleteWhere { criterionId eq criterion.id }
IndividualFeedbacks.deleteWhere { criterionId eq criterion.id }
criterion.delete()
}
criteria.refresh()
}
} }
class SoloAssignmentState(val assignment: SoloAssignment) { class SoloAssignmentState(val assignment: SoloAssignment) {
data class LocalFeedback(val feedback: String, val grade: String) data class LocalFeedback(val feedback: String, val grade: String)
data class FullFeedback(val global: LocalFeedback?, val byCriterion: List<Pair<SoloAssignmentCriterion, LocalFeedback?>>)
val editionCourse = transaction { assignment.edition.course to assignment.edition } val editionCourse = transaction { assignment.edition.course to assignment.edition }
private val _name = mutableStateOf(assignment.name); val name = _name.immutable() private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
val feedback = RawDbState { loadFeedback() }
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState {
assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).toList()
}
val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState { val autofill = RawDbState {
SoloFeedbacks.selectAll().where { SoloFeedbacks.soloAssignmentId eq assignment.id }.map { SoloFeedbacks.selectAll().where { SoloFeedbacks.soloAssignmentId eq assignment.id }.map {
@ -557,24 +613,33 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
}.flatten().distinct().sorted() }.flatten().distinct().sorted()
} }
private fun Transaction.loadFeedback(): List<Pair<Student, LocalFeedback?>> { private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
val students = editionCourse.second.soloStudents return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
val feedbacks = SoloFeedbacks.selectAll().where { val each = SoloFeedbacks.selectAll().where {
SoloFeedbacks.soloAssignmentId eq assignment.id (SoloFeedbacks.soloAssignmentId eq assignment.id) and
(SoloFeedbacks.studentId eq student.id)
}.associate { }.associate {
it[SoloFeedbacks.studentId] to LocalFeedback(it[SoloFeedbacks.feedback], it[SoloFeedbacks.grade]) val criterion = it[SoloFeedbacks.criterionId]?.let { id -> SoloAssignmentCriterion[id] }
val fe = LocalFeedback(it[SoloFeedbacks.feedback], it[SoloFeedbacks.grade])
criterion to fe
}
val feedback = FullFeedback(
global = each[null],
byCriterion = criteria.entities.value.map { c -> c to each[c] }
)
student to feedback
}
} }
return students.map { s -> s to feedbacks[s.id] } fun upsertFeedback(student: Student, msg: String?, grd: String?, criterion: SoloAssignmentCriterion? = null) {
}
fun upsertFeedback(student: Student, msg: String, grd: String) {
transaction { transaction {
SoloFeedbacks.upsert { SoloFeedbacks.upsert {
it[soloAssignmentId] = assignment.id it[soloAssignmentId] = assignment.id
it[studentId] = student.id it[studentId] = student.id
it[this.feedback] = msg it[this.feedback] = msg ?: ""
it[this.grade] = grd it[this.grade] = grd ?: ""
it[criterionId] = criterion?.id
} }
} }
feedback.refresh(); autofill.refresh() feedback.refresh(); autofill.refresh()
@ -593,6 +658,33 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
} }
_deadline.value = d _deadline.value = d
} }
fun addCriterion(name: String) {
transaction {
SoloAssignmentCriterion.new {
this.name = name;
this.description = "";
this.assignment = this@SoloAssignmentState.assignment
}
criteria.refresh()
}
}
fun updateCriterion(criterion: SoloAssignmentCriterion, name: String, desc: String) {
transaction {
criterion.name = name
criterion.description = desc
}
criteria.refresh()
}
fun deleteCriterion(criterion: SoloAssignmentCriterion) {
transaction {
SoloFeedbacks.deleteWhere { criterionId eq criterion.id }
criterion.delete()
}
criteria.refresh()
}
} }
class PeerEvaluationState(val evaluation: PeerEvaluation) { class PeerEvaluationState(val evaluation: PeerEvaluation) {