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)
packageName = "com.jaytux.grader"
packageVersion = "1.0.0"
includeAllModules = true
}
}
}

View File

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

View File

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

View File

@ -1,10 +1,9 @@
package com.jaytux.grader.data
import com.jaytux.grader.data.GroupAssignment.Companion.referrersOn
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.UUID
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 assignment by GroupAssignments.assignment
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) {
@ -71,6 +80,35 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by SoloAssignments.name
var assignment by SoloAssignments.assignment
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) {

View File

@ -1,7 +1,5 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
@ -9,8 +7,6 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.graphicsLayer
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.unit.Constraints
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.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 kotlinx.datetime.LocalDateTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GroupAssignmentView(state: GroupAssignmentState) {
val task by state.task
val deadline by state.deadline
val allFeedback by state.feedback.entities
val criteria by state.criteria.entities
var idx by remember(state) { mutableStateOf(0) }
@ -45,7 +45,7 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
}
TabRow(idx) {
Tab(idx == 0, { idx = 0 }) { Text("Assignment") }
Tab(idx == 0, { idx = 0 }) { Text("Task and Criteria") }
allFeedback.forEachIndexed { i, it ->
val (group, feedback) = it
Tab(idx == i + 1, { idx = i + 1 }) {
@ -55,12 +55,75 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
}
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 {
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)
OutlinedRichTextEditor(
@ -70,10 +133,53 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
minLines = 5,
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 {
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
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
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 critIdx by remember(fdbk) { mutableStateOf(0) }
val criteria by state.criteria.entities
val suggestions by state.autofill.entities
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) {
Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button({ state.upsertGroupFeedback(group, msg.text, grade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = grade.isNotBlank() || msg.text.isNotBlank()) {
Text("Save")
state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade)
}
else {
val ind = individual[idx - 1]
val glob = ind.second.second.global
state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade)
}
}
AutocompleteLineField(
msg, { msg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter ->
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
val updateFeedback = { fdbk: String ->
if(idx == 0) {
if(critIdx == 0) {
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 {
val (student, details) = individual[idx - 1]
var sGrade by remember { mutableStateOf(details.second?.grade ?: "") }
var sMsg by remember { mutableStateOf(TextFieldValue(details.second?.feedback ?: "")) }
Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button({ state.upsertIndividualFeedback(student, group, sMsg.text, sGrade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()) {
Text("Save")
val ind = individual[idx - 1]
if(critIdx == 0) {
val entry = ind.second.second
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.global?.grade ?: "", null)
}
else {
val entry = ind.second.second.byCriterion[critIdx - 1]
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.entry?.grade ?: "", entry.criterion)
}
}
}
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(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { 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 suggestions by state.autofill.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)) {
Row {
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 {
Surface(
Modifier.fillMaxWidth().clickable { idx = 0 },
@ -178,11 +356,21 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
}
}
itemsIndexed(grades.toList()) { i, (student, _) ->
itemsIndexed(criteria) { i, crit ->
Surface(
Modifier.fillMaxWidth().clickable { idx = i + 1 },
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
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))
}
@ -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)) {
if(tab == 0) {
if (idx == 0) {
val updTask = rememberRichTextState()
@ -214,29 +411,97 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
"Update"
) { state.updateTask(updTask.toMarkdown()) }
} else {
val (student, fg) = grades[idx - 1]
var sGrade by remember(idx) { mutableStateOf(fg?.grade ?: "") }
var sMsg by remember(idx) { mutableStateOf(TextFieldValue(fg?.feedback ?: "")) }
val crit = criteria[idx - 1]
var name by remember(crit) { mutableStateOf(crit.name) }
var desc by remember(crit) { mutableStateOf(crit.description) }
Column {
Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
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))
Button(
{ state.upsertFeedback(student, sMsg.text, sGrade) },
{ onSetGrade(grade); onSetFeedback(feedback.text) },
Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()
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(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { 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.EditionStudents.editionId
import com.jaytux.grader.data.EditionStudents.studentId
import com.jaytux.grader.viewmodel.GroupAssignmentState.*
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import org.jetbrains.exposed.dao.id.EntityID
@ -445,18 +446,27 @@ class GroupState(val group: Group) {
}
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(
val group: Group,
val feedback: LocalFeedback?,
val individuals: List<Pair<Student, Pair<String?, LocalFeedback?>>>
val feedback: LocalFeedback,
val individuals: List<Pair<Student, Pair<String?, LocalFeedback>>>
)
val editionCourse = transaction { assignment.edition.course to assignment.edition }
private val _name = mutableStateOf(assignment.name); val name = _name.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()
val criteria = RawDbState {
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList()
}
val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState {
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>> {
val individuals = IndividualFeedbacks.selectAll().where {
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 {
return Group.find {
(Groups.editionId eq assignment.edition.id)
}.sortAsc(Groups.name).map { group ->
val students = group.studentRoles.sortedBy { it.student.name }.map { sR ->
val student = sR.student
val role = sR.role
val feedback = individuals[student.id]
// step 1: group-level feedback, including criteria
val forGroup = GroupFeedbacks.selectAll().where {
(GroupFeedbacks.groupAssignmentId eq assignment.id) and
(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) ->
group to LocalGFeedback(group, LocalFeedback(f, g), students)
} ?: (group to LocalGFeedback(group, null, students))
}
return groups
}
fun upsertGroupFeedback(group: Group, msg: String, grd: String) {
fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
transaction {
GroupFeedbacks.upsert {
it[groupAssignmentId] = assignment.id
it[groupId] = group.id
it[this.feedback] = msg
it[this.grade] = grd
it[criterionId] = criterion?.id
}
}
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 {
IndividualFeedbacks.upsert {
it[groupAssignmentId] = assignment.id
@ -522,6 +545,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[studentId] = student.id
it[this.feedback] = msg
it[this.grade] = grd
it[criterionId] = criterion?.id
}
}
feedback.refresh(); autofill.refresh()
@ -540,16 +564,48 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
}
_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) {
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 }
private val _name = mutableStateOf(assignment.name); val name = _name.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()
val criteria = RawDbState {
assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).toList()
}
val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState {
SoloFeedbacks.selectAll().where { SoloFeedbacks.soloAssignmentId eq assignment.id }.map {
@ -557,24 +613,33 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
}.flatten().distinct().sorted()
}
private fun Transaction.loadFeedback(): List<Pair<Student, LocalFeedback?>> {
val students = editionCourse.second.soloStudents
val feedbacks = SoloFeedbacks.selectAll().where {
SoloFeedbacks.soloAssignmentId eq assignment.id
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
val each = SoloFeedbacks.selectAll().where {
(SoloFeedbacks.soloAssignmentId eq assignment.id) and
(SoloFeedbacks.studentId eq student.id)
}.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) {
fun upsertFeedback(student: Student, msg: String?, grd: String?, criterion: SoloAssignmentCriterion? = null) {
transaction {
SoloFeedbacks.upsert {
it[soloAssignmentId] = assignment.id
it[studentId] = student.id
it[this.feedback] = msg
it[this.grade] = grd
it[this.feedback] = msg ?: ""
it[this.grade] = grd ?: ""
it[criterionId] = criterion?.id
}
}
feedback.refresh(); autofill.refresh()
@ -593,6 +658,33 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
}
_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) {