Criteria for assignment grading

This commit is contained in:
jay-tux 2025-04-25 09:47:18 +02:00
parent a7aafccd19
commit c88d0d2e58
Signed by: jay-tux
GPG Key ID: 84302006B056926E
5 changed files with 132 additions and 79 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ captures
!*.xcworkspace/contents.xcworkspacedata !*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings **/xcshareddata/WorkspaceSettings.xcsettings
**/grader.db **/grader.db
**/*.backup

View File

@ -85,34 +85,34 @@ object PeerEvaluations : UUIDTable("peerEvals") {
} }
object GroupFeedbacks : CompositeIdTable("grpFdbks") { object GroupFeedbacks : CompositeIdTable("grpFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignments.id).nullable() val criterionId = reference("criterion_id", GroupAssignmentCriteria.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)
override val primaryKey = PrimaryKey(groupAssignmentId, groupId) override val primaryKey = PrimaryKey(groupId, criterionId)
} }
object IndividualFeedbacks : CompositeIdTable("indivFdbks") { object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignments.id).nullable() val criterionId = reference("criterion_id", GroupAssignmentCriteria.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")
val grade = varchar("grade", 32) val grade = varchar("grade", 32)
override val primaryKey = PrimaryKey(groupAssignmentId, studentId) override val primaryKey = PrimaryKey(studentId, criterionId)
} }
object SoloFeedbacks : CompositeIdTable("soloFdbks") { object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id) val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
val criterionId = reference("criterion_id", SoloAssignments.id).nullable() val criterionId = reference("criterion_id", SoloAssignmentCriteria.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)
override val primaryKey = PrimaryKey(soloAssignmentId, studentId) override val primaryKey = PrimaryKey(studentId, criterionId)
} }
object PeerEvaluationContents : CompositeIdTable("peerEvalCnts") { object PeerEvaluationContents : CompositeIdTable("peerEvalCnts") {

View File

@ -70,6 +70,24 @@ class GroupAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
var assignment by GroupAssignment referencedOn GroupAssignmentCriteria.assignmentId var assignment by GroupAssignment referencedOn GroupAssignmentCriteria.assignmentId
var name by GroupAssignmentCriteria.name var name by GroupAssignmentCriteria.name
var description by GroupAssignmentCriteria.desc 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<UUID>) : Entity<UUID>(id) { class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -104,8 +122,7 @@ class SoloAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = assignment.hashCode() var result = name.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode() result = 31 * result + description.hashCode()
return result return result
} }

View File

@ -255,7 +255,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
groupFeedbackPane( groupFeedbackPane(
criteria, critIdx, { critIdx = it }, feedback.global, criteria, critIdx, { critIdx = it }, feedback.global,
if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry, 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<String>, autofill: List<String>,
onSetGrade: (String) -> Unit, onSetGrade: (String) -> Unit,
onSetFeedback: (String) -> Unit, onSetFeedback: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
key: Any? = null
) { ) {
var grade by remember(globFeedback) { mutableStateOf(globFeedback?.grade ?: "") } var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
Column(modifier) { Column(modifier) {
Row { Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))

View File

@ -310,15 +310,23 @@ class EditionState(val edition: Edition) {
} }
fun delete(sa: SoloAssignment) { fun delete(sa: SoloAssignment) {
transaction { 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() sa.delete()
} }
solo.refresh() solo.refresh()
} }
fun delete(ga: GroupAssignment) { fun delete(ga: GroupAssignment) {
transaction { transaction {
GroupFeedbacks.deleteWhere { groupAssignmentId eq ga.id } GroupAssignmentCriteria.selectAll().where { GroupAssignmentCriteria.assignmentId eq ga.id }.forEach { it ->
IndividualFeedbacks.deleteWhere { groupAssignmentId eq ga.id } val id = it[GroupAssignmentCriteria.assignmentId]
GroupFeedbacks.deleteWhere { criterionId eq id }
IndividualFeedbacks.deleteWhere { criterionId eq id }
}
GroupAssignmentCriteria.deleteWhere { assignmentId eq ga.id }
ga.delete() ga.delete()
} }
groupAs.refresh() 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 }) (Groups.editionId eq edition.id) and (Groups.id inList student.groups.map { it.id })
}.associate { it.id to it.name } }.associate { it.id to it.name }
val asGroup = (GroupAssignments innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
GroupFeedbacks.groupId inList groupsForEdition.keys.toList() (GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
}.map { it[GroupFeedbacks.groupAssignmentId] to it } (GroupAssignmentCriteria.name eq "")
}.map { it[GroupAssignments.id] to it }
val asIndividual = (GroupAssignments innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
IndividualFeedbacks.studentId eq student.id (IndividualFeedbacks.studentId eq student.id) and
}.map { it[IndividualFeedbacks.groupAssignmentId] to it } (GroupAssignmentCriteria.name eq "")
}.map { it[GroupAssignments.id] to it }
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>() val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
asGroup.forEach { asGroup.forEach {
@ -391,8 +401,9 @@ class StudentState(val student: Student, edition: Edition) {
} }
val soloGrades = RawDbState { val soloGrades = RawDbState {
(SoloAssignments innerJoin SoloFeedbacks).selectAll().where { (SoloAssignments innerJoin SoloAssignmentCriteria innerJoin SoloFeedbacks).selectAll().where {
SoloFeedbacks.studentId eq student.id (SoloFeedbacks.studentId eq student.id) and
(SoloAssignmentCriteria.name eq "")
}.map { LocalSoloGrade(it[SoloAssignments.name], it[SoloFeedbacks.grade]) }.toList() }.map { LocalSoloGrade(it[SoloAssignments.name], it[SoloFeedbacks.grade]) }.toList()
} }
@ -456,7 +467,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
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>>> // Student -> (Role, Feedback)
) )
val editionCourse = transaction { assignment.edition.course to assignment.edition } val editionCourse = transaction { assignment.edition.course to assignment.edition }
@ -469,11 +480,11 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val feedback = RawDbState { loadFeedback() } val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState { 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') 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') it[IndividualFeedbacks.feedback].split('\n')
} }
@ -481,53 +492,67 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
} }
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> { private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
val allCrit = GroupAssignmentCriterion.find {
GroupAssignmentCriteria.assignmentId eq assignment.id
}
return Group.find { return Group.find {
(Groups.editionId eq assignment.edition.id) (Groups.editionId eq assignment.edition.id)
}.sortAsc(Groups.name).map { group -> }.sortAsc(Groups.name).map { group ->
// step 1: group-level feedback, including criteria val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
val forGroup = GroupFeedbacks.selectAll().where { (GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
(GroupFeedbacks.groupAssignmentId eq assignment.id) and }.map { row ->
(GroupFeedbacks.groupId eq group.id) val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] }
}.associate { val fdbk = row[GroupFeedbacks.feedback]
val criterion = it[GroupFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] } val grade = row[GroupFeedbacks.grade]
val fe = FeedbackEntry(it[GroupFeedbacks.feedback], it[GroupFeedbacks.grade])
criterion to fe 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 global = forGroup.firstOrNull { it.first == null }?.second
val individuals = group.studentRoles.map { sr -> val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
val student = sr.student .filterNotNull().associateBy { it.criterion.id }
val role = sr.role 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 studentFeedback = LocalFeedback(
global = forStudent[null],
byCriterion = criteria.entities.value.map { c -> LocalCriterionFeedback(c, forStudent[c]) }
)
student to (role to studentFeedback) val byGroup = LocalFeedback(global, byCrit)
}.sortedBy { it.first.name }
group to LocalGFeedback(group, feedback, individuals) 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 byStudent = LocalFeedback(global, byCrit)
student to (role to byStudent)
}
group to LocalGFeedback(group, byGroup, indiv)
} }
} }
fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) { fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
transaction { transaction {
GroupFeedbacks.upsert { GroupFeedbacks.upsert {
it[groupAssignmentId] = assignment.id it[assignmentId] = 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
@ -540,7 +565,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) { fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
transaction { transaction {
IndividualFeedbacks.upsert { IndividualFeedbacks.upsert {
it[groupAssignmentId] = assignment.id it[assignmentId] = assignment.id
it[groupId] = group.id it[groupId] = group.id
it[studentId] = student.id it[studentId] = student.id
it[this.feedback] = msg it[this.feedback] = msg
@ -608,34 +633,42 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
val feedback = RawDbState { loadFeedback() } val feedback = RawDbState { loadFeedback() }
val autofill = RawDbState { 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') it[SoloFeedbacks.feedback].split('\n')
}.flatten().distinct().sorted() }.flatten().distinct().sorted()
} }
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> { private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student -> val allCrit = SoloAssignmentCriterion.find {
val each = SoloFeedbacks.selectAll().where { SoloAssignmentCriteria.assignmentId eq assignment.id
(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] }
)
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) { fun upsertFeedback(student: Student, msg: String?, grd: String?, criterion: SoloAssignmentCriterion? = null) {
transaction { transaction {
SoloFeedbacks.upsert { SoloFeedbacks.upsert {
it[soloAssignmentId] = assignment.id it[assignmentId] = 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 ?: ""