From c88d0d2e584de9627cb38a620cf4266cc85777b3 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Fri, 25 Apr 2025 09:47:18 +0200 Subject: [PATCH] Criteria for assignment grading --- .gitignore | 3 +- .../kotlin/com/jaytux/grader/data/DSL.kt | 18 +- .../kotlin/com/jaytux/grader/data/Entities.kt | 21 ++- .../com/jaytux/grader/ui/Assignments.kt | 10 +- .../com/jaytux/grader/viewmodel/DbState.kt | 159 +++++++++++------- 5 files changed, 132 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index a5ee101..f8113f6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings -**/grader.db \ No newline at end of file +**/grader.db +**/*.backup \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt index 2570352..f81fd01 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt @@ -85,34 +85,34 @@ 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 assignmentId = reference("group_assignment_id", GroupAssignments.id) + val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() val groupId = reference("group_id", Groups.id) val feedback = text("feedback") val grade = varchar("grade", 32) - override val primaryKey = PrimaryKey(groupAssignmentId, groupId) + override val primaryKey = PrimaryKey(groupId, criterionId) } object IndividualFeedbacks : CompositeIdTable("indivFdbks") { - val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) - val criterionId = reference("criterion_id", GroupAssignments.id).nullable() + val assignmentId = reference("group_assignment_id", GroupAssignments.id) + val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() val groupId = reference("group_id", Groups.id) val studentId = reference("student_id", Students.id) val feedback = text("feedback") val grade = varchar("grade", 32) - override val primaryKey = PrimaryKey(groupAssignmentId, studentId) + override val primaryKey = PrimaryKey(studentId, criterionId) } object SoloFeedbacks : CompositeIdTable("soloFdbks") { - val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id) - val criterionId = reference("criterion_id", SoloAssignments.id).nullable() + val assignmentId = reference("solo_assignment_id", SoloAssignments.id) + val criterionId = reference("criterion_id", SoloAssignmentCriteria.id).nullable() val studentId = reference("student_id", Students.id) val feedback = text("feedback") val grade = varchar("grade", 32) - override val primaryKey = PrimaryKey(soloAssignmentId, studentId) + override val primaryKey = PrimaryKey(studentId, criterionId) } object PeerEvaluationContents : CompositeIdTable("peerEvalCnts") { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt index b822c85..80dccf9 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt @@ -70,6 +70,24 @@ class GroupAssignmentCriterion(id: EntityID) : Entity(id) { var assignment by GroupAssignment referencedOn GroupAssignmentCriteria.assignmentId var name by GroupAssignmentCriteria.name var description by GroupAssignmentCriteria.desc + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GroupAssignmentCriterion + + if (name != other.name) return false + if (description != other.description) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + description.hashCode() + return result + } } class SoloAssignment(id: EntityID) : Entity(id) { @@ -104,8 +122,7 @@ class SoloAssignmentCriterion(id: EntityID) : Entity(id) { } override fun hashCode(): Int { - var result = assignment.hashCode() - result = 31 * result + name.hashCode() + var result = name.hashCode() result = 31 * result + description.hashCode() return result } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt index 82ee529..3010eed 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -255,7 +255,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG 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) + suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp), + key = idx to critIdx ) } } @@ -270,10 +271,11 @@ fun groupFeedbackPane( autofill: List, onSetGrade: (String) -> Unit, onSetFeedback: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + key: Any? = null ) { - var grade by remember(globFeedback) { mutableStateOf(globFeedback?.grade ?: "") } - var feedback by remember(currentCriterion, criteria) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } + var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } + var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } Column(modifier) { Row { Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index e4ef136..9998e53 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -310,15 +310,23 @@ class EditionState(val edition: Edition) { } fun delete(sa: SoloAssignment) { transaction { - SoloFeedbacks.deleteWhere { soloAssignmentId eq sa.id } + SoloAssignmentCriteria.selectAll().where { SoloAssignmentCriteria.assignmentId eq sa.id }.forEach { it -> + val id = it[SoloAssignmentCriteria.assignmentId] + SoloFeedbacks.deleteWhere { criterionId eq id } + } + SoloAssignmentCriteria.deleteWhere { assignmentId eq sa.id } sa.delete() } solo.refresh() } fun delete(ga: GroupAssignment) { transaction { - GroupFeedbacks.deleteWhere { groupAssignmentId eq ga.id } - IndividualFeedbacks.deleteWhere { groupAssignmentId eq ga.id } + GroupAssignmentCriteria.selectAll().where { GroupAssignmentCriteria.assignmentId eq ga.id }.forEach { it -> + val id = it[GroupAssignmentCriteria.assignmentId] + GroupFeedbacks.deleteWhere { criterionId eq id } + IndividualFeedbacks.deleteWhere { criterionId eq id } + } + GroupAssignmentCriteria.deleteWhere { assignmentId eq ga.id } ga.delete() } groupAs.refresh() @@ -363,13 +371,15 @@ class StudentState(val student: Student, edition: Edition) { (Groups.editionId eq edition.id) and (Groups.id inList student.groups.map { it.id }) }.associate { it.id to it.name } - val asGroup = (GroupAssignments innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { - GroupFeedbacks.groupId inList groupsForEdition.keys.toList() - }.map { it[GroupFeedbacks.groupAssignmentId] to it } + val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { + (GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and + (GroupAssignmentCriteria.name eq "") + }.map { it[GroupAssignments.id] to it } - val asIndividual = (GroupAssignments innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { - IndividualFeedbacks.studentId eq student.id - }.map { it[IndividualFeedbacks.groupAssignmentId] to it } + val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { + (IndividualFeedbacks.studentId eq student.id) and + (GroupAssignmentCriteria.name eq "") + }.map { it[GroupAssignments.id] to it } val res = mutableMapOf, LocalGroupGrade>() asGroup.forEach { @@ -391,8 +401,9 @@ class StudentState(val student: Student, edition: Edition) { } val soloGrades = RawDbState { - (SoloAssignments innerJoin SoloFeedbacks).selectAll().where { - SoloFeedbacks.studentId eq student.id + (SoloAssignments innerJoin SoloAssignmentCriteria innerJoin SoloFeedbacks).selectAll().where { + (SoloFeedbacks.studentId eq student.id) and + (SoloAssignmentCriteria.name eq "") }.map { LocalSoloGrade(it[SoloAssignments.name], it[SoloFeedbacks.grade]) }.toList() } @@ -456,7 +467,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { data class LocalGFeedback( val group: Group, val feedback: LocalFeedback, - val individuals: List>> + val individuals: List>> // Student -> (Role, Feedback) ) val editionCourse = transaction { assignment.edition.course to assignment.edition } @@ -469,11 +480,11 @@ class GroupAssignmentState(val assignment: GroupAssignment) { val feedback = RawDbState { loadFeedback() } val autofill = RawDbState { - val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap { + val forGroups = (GroupFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap { it[GroupFeedbacks.feedback].split('\n') } - val forIndividuals = IndividualFeedbacks.selectAll().where { IndividualFeedbacks.groupAssignmentId eq assignment.id }.flatMap { + val forIndividuals = (IndividualFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap { it[IndividualFeedbacks.feedback].split('\n') } @@ -481,53 +492,67 @@ class GroupAssignmentState(val assignment: GroupAssignment) { } private fun Transaction.loadFeedback(): List> { + val allCrit = GroupAssignmentCriterion.find { + GroupAssignmentCriteria.assignmentId eq assignment.id + } + return Group.find { (Groups.editionId eq assignment.edition.id) }.sortAsc(Groups.name).map { group -> - // 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 forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where { + (GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id) + }.map { row -> + val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] } + val fdbk = row[GroupFeedbacks.feedback] + val grade = row[GroupFeedbacks.grade] + + crit to FeedbackEntry(fdbk, grade) } - val feedback = LocalFeedback( - global = forGroup[null], - byCriterion = criteria.entities.value.map { c -> LocalCriterionFeedback(c, forGroup[c]) } - ) - // step 2: individual feedback - val individuals = group.studentRoles.map { sr -> - val student = sr.student - val role = sr.role + val global = forGroup.firstOrNull { it.first == null }?.second + val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } + .filterNotNull().associateBy { it.criterion.id } + val byCrit = allCrit.map { c -> + byCrit_[c.id] ?: LocalCriterionFeedback(c, null) + } - 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 byGroup = LocalFeedback(global, byCrit) + + val indiv = group.studentRoles.map { + val student = it.student + val role = it.role + + val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents) + .selectAll().where { + (IndividualFeedbacks.assignmentId eq assignment.id) and + (GroupStudents.studentId eq student.id) and (Groups.id eq group.id) + }.map { row -> + val crit = row[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] } + val fdbk = row[IndividualFeedbacks.feedback] + val grade = row[IndividualFeedbacks.grade] + + crit to FeedbackEntry(fdbk, grade) + } + + val global = forSt.firstOrNull { it.first == null }?.second + val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } + .filterNotNull().associateBy { it.criterion.id } + val byCrit = allCrit.map { c -> + byCrit_[c.id] ?: LocalCriterionFeedback(c, null) } - val studentFeedback = LocalFeedback( - global = forStudent[null], - byCriterion = criteria.entities.value.map { c -> LocalCriterionFeedback(c, forStudent[c]) } - ) + val byStudent = LocalFeedback(global, byCrit) - student to (role to studentFeedback) - }.sortedBy { it.first.name } + student to (role to byStudent) + } - group to LocalGFeedback(group, feedback, individuals) + group to LocalGFeedback(group, byGroup, indiv) } } fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) { transaction { GroupFeedbacks.upsert { - it[groupAssignmentId] = assignment.id + it[assignmentId] = assignment.id it[groupId] = group.id it[this.feedback] = msg it[this.grade] = grd @@ -540,7 +565,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) { transaction { IndividualFeedbacks.upsert { - it[groupAssignmentId] = assignment.id + it[assignmentId] = assignment.id it[groupId] = group.id it[studentId] = student.id it[this.feedback] = msg @@ -608,34 +633,42 @@ class SoloAssignmentState(val assignment: SoloAssignment) { val feedback = RawDbState { loadFeedback() } val autofill = RawDbState { - SoloFeedbacks.selectAll().where { SoloFeedbacks.soloAssignmentId eq assignment.id }.map { + SoloFeedbacks.selectAll().where { SoloFeedbacks.assignmentId eq assignment.id }.map { it[SoloFeedbacks.feedback].split('\n') }.flatten().distinct().sorted() } private fun Transaction.loadFeedback(): List> { - 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 { - 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] } - ) + val allCrit = SoloAssignmentCriterion.find { + SoloAssignmentCriteria.assignmentId eq assignment.id + } - student to feedback + return editionCourse.second.soloStudents.sortAsc(Students.name).map { student -> + val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where { + (IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id) + }.map { row -> + val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] } + val fdbk = row[IndividualFeedbacks.feedback] + val grade = row[IndividualFeedbacks.grade] + + crit to LocalFeedback(fdbk, grade) + } + + val global = forStudent.firstOrNull { it.first == null }?.second + val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } } + .filterNotNull().associateBy { it.first.id } + val byCrit = allCrit.map { c -> + byCrit_[c.id] ?: Pair(c, null) + } + + student to FullFeedback(global, byCrit) } } fun upsertFeedback(student: Student, msg: String?, grd: String?, criterion: SoloAssignmentCriterion? = null) { transaction { SoloFeedbacks.upsert { - it[soloAssignmentId] = assignment.id + it[assignmentId] = assignment.id it[studentId] = student.id it[this.feedback] = msg ?: "" it[this.grade] = grd ?: ""