Peer evaluation UI
This commit is contained in:
parent
f407a8c43e
commit
4da4b0bb85
|
@ -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)
|
||||
|
@ -94,3 +100,30 @@ object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
|||
|
||||
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)
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(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<UUID>) : Entity<UUID>(id) {
|
||||
|
@ -72,29 +73,10 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||
var deadline by SoloAssignments.deadline
|
||||
}
|
||||
|
||||
class GroupFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
|
||||
companion object : EntityClass<CompositeID, GroupFeedback>(GroupFeedbacks)
|
||||
class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, PeerEvaluation>(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<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
|
||||
var edition by Edition referencedOn PeerEvaluations.editionId
|
||||
var number by PeerEvaluations.number
|
||||
var name by PeerEvaluations.name
|
||||
}
|
|
@ -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
|
||||
|
@ -230,3 +240,165 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
@ -406,3 +412,46 @@ fun SelectEditDeleteRow(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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))
|
||||
}
|
|
@ -18,7 +18,7 @@ import kotlin.math.max
|
|||
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()))
|
||||
|
||||
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<UUID> = 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<UUID> = evaluation.id
|
||||
override fun index(): Int? = evaluation.number
|
||||
}
|
||||
|
||||
abstract fun name(): String
|
||||
abstract fun id(): EntityID<UUID>
|
||||
|
@ -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<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 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 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<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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue