Peer evaluation UI

This commit is contained in:
jay-tux 2025-03-20 15:15:49 +01:00
parent f407a8c43e
commit 4da4b0bb85
Signed by: jay-tux
GPG Key ID: 84302006B056926E
8 changed files with 476 additions and 40 deletions

View File

@ -67,6 +67,12 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
val deadline = datetime("deadline") val deadline = datetime("deadline")
} }
object PeerEvaluations : UUIDTable("peerEvals") {
val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable()
val name = varchar("name", 50)
}
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 groupId = reference("group_id", Groups.id) val groupId = reference("group_id", Groups.id)
@ -93,4 +99,31 @@ object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val grade = varchar("grade", 32) val grade = varchar("grade", 32)
override val primaryKey = PrimaryKey(soloAssignmentId, studentId) override val primaryKey = PrimaryKey(soloAssignmentId, studentId)
}
object PeerEvaluationContents : CompositeIdTable("peerEvalCnts") {
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
val groupId = reference("group_id", Groups.id)
val content = text("content")
override val primaryKey = PrimaryKey(peerEvaluationId, groupId)
}
object StudentToGroupEvaluation : CompositeIdTable("stToGrEv") {
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
val studentId = reference("student_id", Students.id)
val grade = varchar("grade", 32)
val note = text("note")
override val primaryKey = PrimaryKey(peerEvaluationId)
}
object StudentToStudentEvaluation : CompositeIdTable("stToStEv") {
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
val studentIdFrom = reference("student_id_from", Students.id)
val studentIdTo = reference("student_id_to", Students.id)
val grade = varchar("grade", 32)
val note = text("note")
override val primaryKey = PrimaryKey(peerEvaluationId, studentIdFrom, studentIdTo)
} }

View File

@ -12,14 +12,18 @@ object Database {
Courses, Editions, Groups, Courses, Editions, Groups,
Students, GroupStudents, EditionStudents, Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments, GroupAssignments, SoloAssignments,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
StudentToGroupEvaluation
) )
val addMissing = SchemaUtils.addMissingColumnsStatements( val addMissing = SchemaUtils.addMissingColumnsStatements(
Courses, Editions, Groups, Courses, Editions, Groups,
Students, GroupStudents, EditionStudents, Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments, GroupAssignments, SoloAssignments,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
StudentToGroupEvaluation
) )
addMissing.forEach { exec(it) } addMissing.forEach { exec(it) }
} }

View File

@ -23,6 +23,7 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
val soloStudents by Student via EditionStudents val soloStudents by Student via EditionStudents
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
val peerEvaluations by PeerEvaluation referrersOn PeerEvaluations.editionId
} }
class Group(id: EntityID<UUID>) : Entity<UUID>(id) { class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -72,29 +73,10 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var deadline by SoloAssignments.deadline var deadline by SoloAssignments.deadline
} }
class GroupFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) { class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<CompositeID, GroupFeedback>(GroupFeedbacks) companion object : EntityClass<UUID, PeerEvaluation>(PeerEvaluations)
var group by Group referencedOn GroupFeedbacks.groupId var edition by Edition referencedOn PeerEvaluations.editionId
var assignment by GroupAssignment referencedOn GroupFeedbacks.groupAssignmentId var number by PeerEvaluations.number
var feedback by GroupFeedbacks.feedback var name by PeerEvaluations.name
var grade by GroupFeedbacks.grade
}
class IndividualFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
companion object : EntityClass<CompositeID, IndividualFeedback>(IndividualFeedbacks)
var student by Student referencedOn IndividualFeedbacks.studentId
var assignment by SoloAssignment referencedOn IndividualFeedbacks.groupAssignmentId
var feedback by IndividualFeedbacks.feedback
var grade by IndividualFeedbacks.grade
}
class SoloFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
companion object : EntityClass<CompositeID, SoloFeedback>(SoloFeedbacks)
var student by Student referencedOn SoloFeedbacks.studentId
var assignment by SoloAssignment referencedOn SoloFeedbacks.soloAssignmentId
var feedback by SoloFeedbacks.feedback
var grade by SoloFeedbacks.grade
} }

View File

@ -1,18 +1,28 @@
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.LazyColumn import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.* 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.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue 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 androidx.compose.ui.unit.dp
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.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
@ -229,4 +239,166 @@ fun SoloAssignmentView(state: SoloAssignmentState) {
} }
} }
} }
}
@Composable
fun PeerEvaluationView(state: PeerEvaluationState) {
val contents by state.contents.entities
var idx by remember(state) { mutableStateOf(0) }
var editing by remember(state) { mutableStateOf<Triple<Student, Student?, PeerEvaluationState.Student2StudentEntry?>?>(null) }
val measure = rememberTextMeasurer()
val isSelected = { from: Student, to: Student? ->
editing?.let { (f, t, _) -> f == from && t == to } ?: false
}
Column(Modifier.padding(10.dp)) {
TabRow(idx) {
contents.forEachIndexed { i, it ->
Tab(idx == i, { idx = i }) { Text(it.group.name) }
}
}
Spacer(Modifier.height(10.dp))
Row {
val current = contents[idx]
val horScroll = rememberLazyListState()
val style = LocalTextStyle.current
val textLenMeasured = remember(state, idx) {
current.students.maxOf { (s, _) ->
measure.measure(s.name, style).size.width
} + 10
}
val cellSize = 75.dp
Column(Modifier.weight(0.5f)) {
Row {
Box { FromTo(textLenMeasured.dp) }
LazyRow(Modifier.height(textLenMeasured.dp), state = horScroll) {
item { VLine() }
items(current.students) { (s, _) ->
Box(
Modifier.width(cellSize).height(textLenMeasured.dp),
contentAlignment = Alignment.TopCenter
) {
var _h: Int = 0
Text(s.name, Modifier.layout{ m, c ->
val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
_h = p.height
layout(p.height, p.width) { p.place(0, 0) }
}.graphicsLayer {
rotationZ = -90f
transformOrigin = TransformOrigin(0f, 0.5f)
translationX = _h.toFloat() / 2f
translationY = textLenMeasured.dp.value - 15f
})
}
}
item { VLine() }
item {
Box(
Modifier.width(cellSize).height(textLenMeasured.dp),
contentAlignment = Alignment.TopCenter
) {
var _h: Int = 0
Text("Group Rating", Modifier.layout{ m, c ->
val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
_h = p.height
layout(p.height, p.width) { p.place(0, 0) }
}.graphicsLayer {
rotationZ = -90f
transformOrigin = TransformOrigin(0f, 0.5f)
translationX = _h.toFloat() / 2f
translationY = textLenMeasured.dp.value - 15f
}, fontWeight = FontWeight.Bold)
}
}
item { VLine() }
}
}
MeasuredLazyColumn {
measuredItem { HLine() }
items(current.students) { (from, glob, map) ->
Row(Modifier.height(cellSize)) {
Text(from.name, Modifier.width(textLenMeasured.dp).align(Alignment.CenterVertically))
LazyRow(state = horScroll) {
item { VLine() }
items(map) { (to, entry) ->
PEGradeWidget(entry,
{ editing = Triple(from, to, entry) }, { editing = null },
isSelected(from, to), Modifier.size(cellSize, cellSize)
)
}
item { VLine() }
item {
PEGradeWidget(glob,
{ editing = Triple(from, null, glob) }, { editing = null },
isSelected(from, null), Modifier.size(cellSize, cellSize))
}
item { VLine() }
}
}
}
measuredItem { HLine() }
}
}
Column(Modifier.weight(0.5f)) {
var groupLevel by remember(state, idx) { mutableStateOf(contents[idx].content) }
editing?.let {
Column(Modifier.weight(0.5f)) {
val (from, to, data) = it
var sGrade by remember(idx) { mutableStateOf(data?.grade ?: "") }
var sMsg by remember(idx) { mutableStateOf(data?.feedback ?: "") }
Box(Modifier.padding(5.dp)) {
to?.let { s2 ->
if(from == s2)
Text("Self-evaluation by ${from.name}", fontWeight = FontWeight.Bold)
else
Text("Evaluation of ${s2.name} by ${from.name}", fontWeight = FontWeight.Bold)
} ?: Text("Group-level evaluation by ${from.name}", fontWeight = FontWeight.Bold)
}
Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button(
{ state.upsertIndividualFeedback(from, to, sMsg, sGrade) },
Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = sGrade.isNotBlank() || sMsg.isNotBlank()
) {
Text("Save")
}
}
OutlinedTextField(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f),
label = { Text("Feedback") },
singleLine = false,
minLines = 5
)
}
}
Column(Modifier.weight(0.5f)) {
Row {
Text("Group-level notes", Modifier.weight(1f).align(Alignment.CenterVertically), fontWeight = FontWeight.Bold)
Button(
{ state.upsertGroupFeedback(current.group, groupLevel) },
enabled = groupLevel != contents[idx].content
) { Text("Update") }
}
OutlinedTextField(
groupLevel, { groupLevel = it }, Modifier.fillMaxWidth().weight(1f),
label = { Text("Group-level notes") },
singleLine = false,
minLines = 5
)
}
}
}
}
} }

View File

@ -39,7 +39,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
val groups by state.groups.entities val groups by state.groups.entities
val solo by state.solo.entities val solo by state.solo.entities
val groupAs by state.groupAs.entities val groupAs by state.groupAs.entities
val mergedAssignments by remember(solo, groupAs) { mutableStateOf(Assignment.merge(groupAs, solo)) } val peers by state.peer.entities
val mergedAssignments by remember(solo, groupAs, peers) { mutableStateOf(Assignment.merge(groupAs, solo, peers)) }
val hist by state.history val hist by state.history
val navs = Navigators( val navs = Navigators(
@ -103,6 +104,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
when(val a = mergedAssignments[id]) { when(val a = mergedAssignments[id]) {
is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition) is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition) is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition)
is Assignment.PeerEval -> PaneHeader(a.name(), "peer evaluation", course, edition)
} }
} }
} }
@ -117,6 +119,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
when (val a = mergedAssignments[id]) { when (val a = mergedAssignments[id]) {
is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment)) is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment)) is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation))
} }
} }
} }
@ -245,7 +248,7 @@ fun AssignmentPanel(
AssignmentType.entries, AssignmentType.entries,
tab.ordinal, tab.ordinal,
{ tab = AssignmentType.entries[it] }, { tab = AssignmentType.entries[it] },
{ Text(it.name) } { Text(it.show) }
) { ) {
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) { Column(Modifier.align(Alignment.Center)) {

View File

@ -0,0 +1,51 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.jaytux.grader.viewmodel.immutable
interface MeasuredLazyListScope : LazyListScope {
fun measuredWidth(): State<Dp>
fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit)
}
interface MeasuredLazyItemScope : LazyItemScope {
fun measuredWidth(): State<Dp>
}
@Composable
fun MeasuredLazyColumn(modifier: Modifier = Modifier, content: MeasuredLazyListScope.() -> Unit) {
val measuredWidth = remember { mutableStateOf(0.dp) }
LazyColumn(modifier.onGloballyPositioned {
measuredWidth.value = it.size.width.dp
}) {
val lisToMlis = { lis: LazyItemScope ->
object : MeasuredLazyItemScope, LazyItemScope by lis {
override fun measuredWidth(): State<Dp> = measuredWidth.immutable()
}
}
val scope = object : MeasuredLazyListScope, LazyListScope by this {
override fun measuredWidth(): State<Dp> = measuredWidth.immutable()
override fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit) {
item {
lisToMlis(this).content()
}
}
}
scope.content()
}
}

View File

@ -1,12 +1,10 @@
package com.jaytux.grader.ui package com.jaytux.grader.ui
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.LazyColumn import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@ -15,20 +13,27 @@ 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.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.jaytux.grader.data.Course import com.jaytux.grader.data.Course
import com.jaytux.grader.data.Edition import com.jaytux.grader.data.Edition
import com.jaytux.grader.viewmodel.PeerEvaluationState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.* import kotlinx.datetime.*
@ -379,11 +384,12 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{
fun Selectable( fun Selectable(
isSelected: Boolean, isSelected: Boolean,
onSelect: () -> Unit, onDeselect: () -> Unit, onSelect: () -> Unit, onDeselect: () -> Unit,
unselectedElevation: Dp = 0.dp, selectedElevation: Dp = 50.dp,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
Surface( Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) 50.dp else 0.dp, tonalElevation = if (isSelected) selectedElevation else unselectedElevation,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
content() content()
@ -405,4 +411,47 @@ fun SelectEditDeleteRow(
Icon(Icons.Default.Delete, "Delete") Icon(Icons.Default.Delete, "Delete")
} }
} }
}
@Composable
fun FromTo(size: Dp) {
Box(Modifier.width(size).height(size)) {
Box(Modifier.align(Alignment.BottomStart)) {
Text("Evaluator", fontWeight = FontWeight.Bold)
}
Box(
Modifier.align(Alignment.TopEnd)
) {
Text("Evaluated", Modifier.graphicsLayer {
rotationZ = -90f
transformOrigin = TransformOrigin(0f, 0.5f)
translationX = size.value / 2f - 15f
translationY = size.value - 15f
}, fontWeight = FontWeight.Bold)
}
}
}
@Composable
fun PEGradeWidget(
grade: PeerEvaluationState.Student2StudentEntry?,
onSelect: () -> Unit, onDeselect: () -> Unit,
isSelected: Boolean,
modifier: Modifier = Modifier
) = Box(modifier.padding(2.dp)) {
Selectable(isSelected, onSelect, onDeselect) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(grade?.let { if(it.grade.isNotBlank()) it.grade else if(it.feedback.isNotBlank()) "n/a" else null } ?: "none")
}
}
}
@Composable
fun VLine(width: Dp = 1.dp, color: Color = Color.Black) = Spacer(Modifier.fillMaxHeight().width(width).background(color))
@Composable
fun MeasuredLazyItemScope.HLine(height: Dp = 1.dp, color: Color = Color.Black) {
val width by measuredWidth()
Spacer(Modifier.width(width).height(height).background(color))
} }

View File

@ -18,7 +18,7 @@ import kotlin.math.max
fun <T> MutableState<T>.immutable(): State<T> = this fun <T> MutableState<T>.immutable(): State<T> = this
fun <T> SizedIterable<T>.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray())) fun <T> SizedIterable<T>.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray()))
enum class AssignmentType { Solo, Group } enum class AssignmentType(val show: String) { Solo("Solo Assignment"), Group("Group Assignment"), Peer("Peer Evaluation") }
sealed class Assignment { sealed class Assignment {
class GAssignment(val assignment: GroupAssignment) : Assignment() { class GAssignment(val assignment: GroupAssignment) : Assignment() {
override fun name(): String = assignment.name override fun name(): String = assignment.name
@ -30,6 +30,11 @@ sealed class Assignment {
override fun id(): EntityID<UUID> = assignment.id override fun id(): EntityID<UUID> = assignment.id
override fun index(): Int? = assignment.number override fun index(): Int? = assignment.number
} }
class PeerEval(val evaluation: com.jaytux.grader.data.PeerEvaluation) : Assignment() {
override fun name(): String = evaluation.name
override fun id(): EntityID<UUID> = evaluation.id
override fun index(): Int? = evaluation.number
}
abstract fun name(): String abstract fun name(): String
abstract fun id(): EntityID<UUID> abstract fun id(): EntityID<UUID>
@ -38,11 +43,13 @@ sealed class Assignment {
companion object { companion object {
fun from(assignment: GroupAssignment) = GAssignment(assignment) fun from(assignment: GroupAssignment) = GAssignment(assignment)
fun from(assignment: SoloAssignment) = SAssignment(assignment) fun from(assignment: SoloAssignment) = SAssignment(assignment)
fun from(pEval: PeerEvaluation) = PeerEval(pEval)
fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>): List<Assignment> { fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>, peers: List<PeerEvaluation>): List<Assignment> {
val g = groups.map { from(it) } val g = groups.map { from(it) }
val s = solos.map { from(it) } val s = solos.map { from(it) }
return (g + s).sortedWith(compareBy<Assignment> { it.index() }.thenBy { it.name() }) val p = peers.map { from(it) }
return (g + s + p).sortedWith(compareBy<Assignment> { it.index() }.thenBy { it.name() })
} }
} }
} }
@ -99,6 +106,7 @@ class EditionState(val edition: Edition) {
val groups = RawDbState { edition.groups.sortAsc(Groups.name).toList() } val groups = RawDbState { edition.groups.sortAsc(Groups.name).toList() }
val solo = RawDbState { edition.soloAssignments.sortAsc(SoloAssignments.name).toList() } val solo = RawDbState { edition.soloAssignments.sortAsc(SoloAssignments.name).toList() }
val groupAs = RawDbState { edition.groupAssignments.sortAsc(GroupAssignments.name).toList() } val groupAs = RawDbState { edition.groupAssignments.sortAsc(GroupAssignments.name).toList() }
val peer = RawDbState { edition.peerEvaluations.sortAsc(PeerEvaluations.name).toList() }
private val _history = mutableStateOf(listOf(-1 to OpenPanel.Assignment)) private val _history = mutableStateOf(listOf(-1 to OpenPanel.Assignment))
val history = _history.immutable() val history = _history.immutable()
@ -190,14 +198,31 @@ class EditionState(val edition: Edition) {
} }
groupAs.refresh() groupAs.refresh()
} }
fun newPeerEvaluation(name: String) {
transaction {
PeerEvaluation.new {
this.name = name; this.edition = this@EditionState.edition
this.number = nextIdx()
}
peer.refresh()
}
}
fun setPeerEvaluationTitle(assignment: PeerEvaluation, title: String) {
transaction {
assignment.name = title
}
peer.refresh()
}
fun newAssignment(type: AssignmentType, name: String) = when(type) { fun newAssignment(type: AssignmentType, name: String) = when(type) {
AssignmentType.Solo -> newSoloAssignment(name) AssignmentType.Solo -> newSoloAssignment(name)
AssignmentType.Group -> newGroupAssignment(name) AssignmentType.Group -> newGroupAssignment(name)
AssignmentType.Peer -> newPeerEvaluation(name)
} }
fun setAssignmentTitle(assignment: Assignment, title: String) = when(assignment) { fun setAssignmentTitle(assignment: Assignment, title: String) = when(assignment) {
is Assignment.GAssignment -> setGroupAssignmentTitle(assignment.assignment, title) is Assignment.GAssignment -> setGroupAssignmentTitle(assignment.assignment, title)
is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title) is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title)
is Assignment.PeerEval -> setPeerEvaluationTitle(assignment.evaluation, title)
} }
fun swapOrder(a1: Assignment, a2: Assignment) { fun swapOrder(a1: Assignment, a2: Assignment) {
@ -215,6 +240,11 @@ class EditionState(val edition: Edition) {
a1.assignment.number = nextIdx() a1.assignment.number = nextIdx()
a2.assignment.number = temp a2.assignment.number = temp
} }
is Assignment.PeerEval -> {
val temp = a1.assignment.number
a1.assignment.number = nextIdx()
a2.evaluation.number = temp
}
} }
} }
is Assignment.SAssignment -> { is Assignment.SAssignment -> {
@ -229,6 +259,30 @@ class EditionState(val edition: Edition) {
a1.assignment.number = a2.assignment.number a1.assignment.number = a2.assignment.number
a2.assignment.number = temp a2.assignment.number = temp
} }
is Assignment.PeerEval -> {
val temp = a1.assignment.number
a1.assignment.number = nextIdx()
a2.evaluation.number = temp
}
}
}
is Assignment.PeerEval -> {
when(a2) {
is Assignment.GAssignment -> {
val temp = a1.evaluation.number
a1.evaluation.number = a2.assignment.number
a2.assignment.number = temp
}
is Assignment.SAssignment -> {
val temp = a1.evaluation.number
a1.evaluation.number = a2.assignment.number
a2.assignment.number = temp
}
is Assignment.PeerEval -> {
val temp = a1.evaluation.number
a1.evaluation.number = a2.evaluation.number
a2.evaluation.number = temp
}
} }
} }
} }
@ -268,9 +322,18 @@ class EditionState(val edition: Edition) {
} }
groupAs.refresh() groupAs.refresh()
} }
fun delete(pe: PeerEvaluation) {
transaction {
PeerEvaluationContents.deleteWhere { peerEvaluationId eq pe.id }
StudentToStudentEvaluation.deleteWhere { peerEvaluationId eq pe.id }
pe.delete()
}
peer.refresh()
}
fun delete(assignment: Assignment) = when(assignment) { fun delete(assignment: Assignment) = when(assignment) {
is Assignment.GAssignment -> delete(assignment.assignment) is Assignment.GAssignment -> delete(assignment.assignment)
is Assignment.SAssignment -> delete(assignment.assignment) is Assignment.SAssignment -> delete(assignment.assignment)
is Assignment.PeerEval -> delete(assignment.evaluation)
} }
fun navTo(panel: OpenPanel, id: Int = -1) { fun navTo(panel: OpenPanel, id: Int = -1) {
@ -532,6 +595,85 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
} }
} }
class PeerEvaluationState(val evaluation: PeerEvaluation) {
data class Student2StudentEntry(val grade: String, val feedback: String)
data class StudentEntry(val student: Student, val global: Student2StudentEntry?, val others: List<Pair<Student, Student2StudentEntry?>>)
data class GroupEntry(val group: Group, val content: String, val students: List<StudentEntry>)
val editionCourse = transaction { evaluation.edition.course to evaluation.edition }
private val _name = mutableStateOf(evaluation.name); val name = _name.immutable()
val contents = RawDbState { loadContents() }
private fun Transaction.loadContents(): List<GroupEntry> {
val found = PeerEvaluationContents.selectAll().where {
PeerEvaluationContents.peerEvaluationId eq evaluation.id
}.associate { gc ->
val group = Group[gc[PeerEvaluationContents.groupId]]
val content = gc[PeerEvaluationContents.content]
val students = group.students.map { student1 ->
val others = group.students.map { student2 ->
val eval = StudentToStudentEvaluation.selectAll().where {
StudentToStudentEvaluation.peerEvaluationId eq evaluation.id and
(StudentToStudentEvaluation.studentIdTo eq student1.id) and
(StudentToStudentEvaluation.studentIdFrom eq student2.id)
}.firstOrNull()
student2 to eval?.let {
Student2StudentEntry(
it[StudentToStudentEvaluation.grade], it[StudentToStudentEvaluation.note]
)
}
}.sortedBy { it.first.name }
val global = StudentToGroupEvaluation.selectAll().where {
StudentToGroupEvaluation.peerEvaluationId eq evaluation.id and
(StudentToGroupEvaluation.studentId eq student1.id)
}.firstOrNull()?.let {
Student2StudentEntry(it[StudentToGroupEvaluation.grade], it[StudentToGroupEvaluation.note])
}
StudentEntry(student1, global, others)
}.sortedBy { it.student.name } // enforce synchronized order
group to GroupEntry(group, content, students)
}
return editionCourse.second.groups.map {
found[it] ?: GroupEntry(
it, "",
it.students.map { s1 -> StudentEntry(s1, null, it.students.map { s2 -> s2 to null }) }
)
}
}
fun upsertGroupFeedback(group: Group, feedback: String) {
transaction {
PeerEvaluationContents.upsert {
it[peerEvaluationId] = evaluation.id
it[groupId] = group.id
it[this.content] = feedback
}
}
contents.refresh()
}
fun upsertIndividualFeedback(from: Student, to: Student?, grade: String, feedback: String) {
transaction {
to?.let {
StudentToStudentEvaluation.upsert {
it[peerEvaluationId] = evaluation.id
it[studentIdFrom] = from.id
it[studentIdTo] = to.id
it[this.grade] = grade
it[this.note] = feedback
}
} ?: StudentToGroupEvaluation.upsert {
it[peerEvaluationId] = evaluation.id
it[studentId] = from.id
it[this.grade] = grade
it[this.note] = feedback
}
}
contents.refresh()
}
}