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

3
.gitignore vendored
View File

@ -17,4 +17,5 @@ captures
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
**/grader.db
**/grader.db
**/*.backup

View File

@ -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") {

View File

@ -70,6 +70,24 @@ class GroupAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(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<UUID>) : Entity<UUID>(id) {
@ -104,8 +122,7 @@ class SoloAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(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
}

View File

@ -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<String>,
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))

View File

@ -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<EntityID<UUID>, 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<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 }
@ -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<Pair<Group, LocalGFeedback>> {
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<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 {
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 ?: ""