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 4744a26..c27d459 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt @@ -67,6 +67,12 @@ object SoloAssignments : UUIDTable("soloAssgmts") { 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") { val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id) val groupId = reference("group_id", Groups.id) @@ -93,4 +99,31 @@ object SoloFeedbacks : CompositeIdTable("soloFdbks") { val grade = varchar("grade", 32) 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) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt index ae42ef4..7fac47a 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt @@ -12,14 +12,18 @@ object Database { Courses, Editions, Groups, Students, GroupStudents, EditionStudents, GroupAssignments, SoloAssignments, - GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks + GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks, + PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation, + StudentToGroupEvaluation ) val addMissing = SchemaUtils.addMissingColumnsStatements( Courses, Editions, Groups, Students, GroupStudents, EditionStudents, GroupAssignments, SoloAssignments, - GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks + GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks, + PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation, + StudentToGroupEvaluation ) addMissing.forEach { exec(it) } } 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 4af829e..9ecbd8b 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt @@ -23,6 +23,7 @@ class Edition(id: EntityID) : Entity(id) { val soloStudents by Student via EditionStudents val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId + val peerEvaluations by PeerEvaluation referrersOn PeerEvaluations.editionId } class Group(id: EntityID) : Entity(id) { @@ -72,29 +73,10 @@ class SoloAssignment(id: EntityID) : Entity(id) { var deadline by SoloAssignments.deadline } -class GroupFeedback(id: EntityID) : Entity(id) { - companion object : EntityClass(GroupFeedbacks) +class PeerEvaluation(id: EntityID) : Entity(id) { + companion object : EntityClass(PeerEvaluations) - var group by Group referencedOn GroupFeedbacks.groupId - var assignment by GroupAssignment referencedOn GroupFeedbacks.groupAssignmentId - var feedback by GroupFeedbacks.feedback - var grade by GroupFeedbacks.grade -} - -class IndividualFeedback(id: EntityID) : Entity(id) { - companion object : EntityClass(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) : Entity(id) { - companion object : EntityClass(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 + var edition by Edition referencedOn PeerEvaluations.editionId + var number by PeerEvaluations.number + var name by PeerEvaluations.name } \ No newline at end of file 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 413353c..15b3c77 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -1,18 +1,28 @@ 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.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.* 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 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.rememberTextMeasurer +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp +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 @@ -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?>(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 + ) + } + } + } + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt index 93fb077..d8d9f35 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Editions.kt @@ -39,7 +39,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { val groups by state.groups.entities val solo by state.solo.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 navs = Navigators( @@ -103,6 +104,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) { when(val a = mergedAssignments[id]) { is Assignment.SAssignment -> PaneHeader(a.name(), "individual 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]) { is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment)) is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment)) + is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation)) } } } @@ -245,7 +248,7 @@ fun AssignmentPanel( AssignmentType.entries, tab.ordinal, { tab = AssignmentType.entries[it] }, - { Text(it.name) } + { Text(it.show) } ) { Box(Modifier.fillMaxSize().padding(10.dp)) { Column(Modifier.align(Alignment.Center)) { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/MeasuredLazyColumn.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/MeasuredLazyColumn.kt new file mode 100644 index 0000000..d6699c1 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/MeasuredLazyColumn.kt @@ -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 + + fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit) +} + +interface MeasuredLazyItemScope : LazyItemScope { + fun measuredWidth(): State +} + +@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 = measuredWidth.immutable() + } + } + + + val scope = object : MeasuredLazyListScope, LazyListScope by this { + override fun measuredWidth(): State = measuredWidth.immutable() + + override fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit) { + item { + lisToMlis(this).content() + } + } + } + + scope.content() + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index 18940be..9ebe998 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -1,12 +1,10 @@ package com.jaytux.grader.ui import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete @@ -15,20 +13,27 @@ 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.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.key 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.capitalize 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.intl.Locale -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import androidx.compose.ui.window.* import com.jaytux.grader.data.Course import com.jaytux.grader.data.Edition +import com.jaytux.grader.viewmodel.PeerEvaluationState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.datetime.* @@ -379,11 +384,12 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{ fun Selectable( isSelected: Boolean, onSelect: () -> Unit, onDeselect: () -> Unit, + unselectedElevation: Dp = 0.dp, selectedElevation: Dp = 50.dp, content: @Composable () -> Unit ) { Surface( 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 ) { content() @@ -405,4 +411,47 @@ fun SelectEditDeleteRow( 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)) } \ No newline at end of file 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 e1fb2a6..aa0a258 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -18,7 +18,7 @@ import kotlin.math.max fun MutableState.immutable(): State = this fun SizedIterable.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 { class GAssignment(val assignment: GroupAssignment) : Assignment() { override fun name(): String = assignment.name @@ -30,6 +30,11 @@ sealed class Assignment { override fun id(): EntityID = assignment.id 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 = evaluation.id + override fun index(): Int? = evaluation.number + } abstract fun name(): String abstract fun id(): EntityID @@ -38,11 +43,13 @@ sealed class Assignment { companion object { fun from(assignment: GroupAssignment) = GAssignment(assignment) fun from(assignment: SoloAssignment) = SAssignment(assignment) + fun from(pEval: PeerEvaluation) = PeerEval(pEval) - fun merge(groups: List, solos: List): List { + fun merge(groups: List, solos: List, peers: List): List { val g = groups.map { from(it) } val s = solos.map { from(it) } - return (g + s).sortedWith(compareBy { it.index() }.thenBy { it.name() }) + val p = peers.map { from(it) } + return (g + s + p).sortedWith(compareBy { it.index() }.thenBy { it.name() }) } } } @@ -99,6 +106,7 @@ class EditionState(val edition: Edition) { val groups = RawDbState { edition.groups.sortAsc(Groups.name).toList() } val solo = RawDbState { edition.soloAssignments.sortAsc(SoloAssignments.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)) val history = _history.immutable() @@ -190,14 +198,31 @@ class EditionState(val edition: Edition) { } 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) { AssignmentType.Solo -> newSoloAssignment(name) AssignmentType.Group -> newGroupAssignment(name) + AssignmentType.Peer -> newPeerEvaluation(name) } fun setAssignmentTitle(assignment: Assignment, title: String) = when(assignment) { is Assignment.GAssignment -> setGroupAssignmentTitle(assignment.assignment, title) is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title) + is Assignment.PeerEval -> setPeerEvaluationTitle(assignment.evaluation, title) } fun swapOrder(a1: Assignment, a2: Assignment) { @@ -215,6 +240,11 @@ class EditionState(val edition: Edition) { a1.assignment.number = nextIdx() a2.assignment.number = temp } + is Assignment.PeerEval -> { + val temp = a1.assignment.number + a1.assignment.number = nextIdx() + a2.evaluation.number = temp + } } } is Assignment.SAssignment -> { @@ -229,6 +259,30 @@ class EditionState(val edition: Edition) { a1.assignment.number = a2.assignment.number 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() } + 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) { is Assignment.GAssignment -> delete(assignment.assignment) is Assignment.SAssignment -> delete(assignment.assignment) + is Assignment.PeerEval -> delete(assignment.evaluation) } 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>) + data class GroupEntry(val group: Group, val content: String, val students: List) + 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 { + 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() + } +}