Peer evaluation UI
This commit is contained in:
parent
f407a8c43e
commit
4da4b0bb85
|
@ -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)
|
||||||
}
|
}
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
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))
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue