Finished UI overhaul

This commit is contained in:
2026-03-26 14:03:56 +01:00
parent 52ff467d9c
commit bdc56748dd
19 changed files with 772 additions and 4972 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,8 @@ import com.jaytux.grader.data.Database
import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.awt.Desktop
import java.net.URI
import java.time.Clock
import java.time.LocalDateTime
import java.util.prefs.Preferences
@@ -42,3 +44,13 @@ object Preferences {
}
infix fun <T1, T2, T3> Pair<T1, T2>.app(x: T3) = Triple(first, second, x)
fun startEmail(recipients: List<String>, onError: (String) -> Unit) {
if(Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.MAIL)) {
val mailTo = "mailto:${recipients.joinToString(",")}"
Desktop.getDesktop().mail(URI(mailTo))
}
else {
onError("Email client is not supported on this platform.")
}
}

View File

@@ -108,20 +108,14 @@ object SoloFeedbacks : CompositeIdTable("soloFdbks") {
object PeerEvaluations : UUIDTable("peerEvals") {
val baseAssignmentId = reference("base_assignment_id", BaseAssignments.id).uniqueIndex()
val studentCriterion = reference("student_crit", Criteria.id)
}
object PeerEvaluationFeedbacks : CompositeIdTable("peerEvalFdbks") {
val groupId = reference("group_id", Groups.id)
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
override val primaryKey = PrimaryKey(groupId, feedbackId)
}
object PeerEvaluationStudentOverrideFeedbacks : UUIDTable("peerEvalStudOvrFdbks") {
val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id)
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
val overrides = reference("overrides", BaseFeedbacks.id)
override val primaryKey = PrimaryKey(studentId, feedbackId)
}
object PeerEvaluationS2GEvaluations : UUIDTable("peerEvalS2GEvals") {
@@ -176,6 +170,6 @@ enum class AssignmentType(val display: String) {
val v2Tables = arrayOf(
Courses, Editions, Groups, Students, GroupStudents, EditionStudents, BaseAssignments, Criteria, GroupAssignments,
SoloAssignments, BaseFeedbacks, GroupFeedbacks, StudentOverrideFeedbacks, SoloFeedbacks, PeerEvaluations,
PeerEvaluationFeedbacks, PeerEvaluationStudentOverrideFeedbacks, PeerEvaluationS2GEvaluations,
PeerEvaluationS2SEvaluations, CategoricGrades, CategoricGradeOptions, NumericGrades
PeerEvaluationFeedbacks, PeerEvaluationS2GEvaluations, PeerEvaluationS2SEvaluations, CategoricGrades,
CategoricGradeOptions, NumericGrades
)

View File

@@ -94,6 +94,7 @@ class PeerEvaluation(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : EntityClass<UUID, PeerEvaluation>(PeerEvaluations)
var base by BaseAssignment referencedOn PeerEvaluations.baseAssignmentId
var studentCriterion by Criterion referencedOn PeerEvaluations.studentCriterion
}
class CategoricGrade(id: EntityID<UUID>) : UUIDEntity(id) {
@@ -143,14 +144,13 @@ class BaseFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
private val _forStudentIfSolo by Student via SoloFeedbacks
private val _forGroupIfGroup by Group via GroupFeedbacks
private val _forGroupIfPeer by Group via PeerEvaluationFeedbacks
private val _forStudentIfPeer by Student via PeerEvaluationFeedbacks
val asSoloFeedback get() = _forStudentIfSolo.singleOrNull()
val asGroupFeedback get() = _forGroupIfGroup.singleOrNull()
val asPeerEvaluationFeedback get() = _forGroupIfPeer.singleOrNull()
val asPeerEvaluationFeedback get() = _forStudentIfPeer.singleOrNull()
val forStudentsOverrideIfGroup by StudentOverrideFeedback referrersOn StudentOverrideFeedbacks.overrides
val forStudentsOverrideIfPeer by PeerEvaluationStudentOverrideFeedback referrersOn PeerEvaluationStudentOverrideFeedbacks.overrides
}
class StudentOverrideFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
@@ -162,15 +162,6 @@ class StudentOverrideFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
var overrides by BaseFeedback referencedOn StudentOverrideFeedbacks.overrides
}
class PeerEvaluationStudentOverrideFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : EntityClass<UUID, PeerEvaluationStudentOverrideFeedback>(PeerEvaluationStudentOverrideFeedbacks)
var group by Group referencedOn PeerEvaluationStudentOverrideFeedbacks.groupId
var student by Student referencedOn PeerEvaluationStudentOverrideFeedbacks.studentId
var feedback by BaseFeedback referencedOn PeerEvaluationStudentOverrideFeedbacks.feedbackId
var overrides by BaseFeedback referencedOn PeerEvaluationStudentOverrideFeedbacks.overrides
}
class PeerEvaluationS2G(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : EntityClass<UUID, PeerEvaluationS2G>(PeerEvaluationS2GEvaluations)

View File

@@ -70,6 +70,9 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
}
} else {
Column(Modifier.padding(10.dp)) {
val peerEvalData by vm.asPeerEvaluation.entity
var updatingPeerEvalGrade by remember { mutableStateOf(false) }
Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium)
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic)
Row {
@@ -85,6 +88,30 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
}
}
}
peerEvalData?.let { pe ->
Row {
Text("Students are reviewing each other using ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) {
Box(Modifier.clickable { updatingPeerEvalGrade = true }.padding(3.dp)) {
Text(
when (val t = pe.second) {
is UiGradeType.Categoric -> t.grade.name
UiGradeType.FreeText -> "by free-form grades"
is UiGradeType.Numeric -> t.grade.name
UiGradeType.Percentage -> "by percentages"
}
)
}
}
}
if(updatingPeerEvalGrade) {
SetGradingDialog("${assignment.assignment.name} (peer review grade)", pe.second, vm, { updatingPeerEvalGrade = false }) { type ->
vm.setPEGrade(pe.first, type)
}
}
}
Row {
Column(Modifier.weight(0.75f)) {
Row {

View File

@@ -141,25 +141,9 @@ fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.Gr
}
}
private fun gradeState(crit: GroupsGradingVM.CritData, current: Grade?): Grade = transaction {
if(current == null) Grade.default(crit.criterion.gradeType, crit.cat, crit.num)
when(crit.criterion.gradeType) {
GradeType.CATEGORIC ->
if(current is Grade.Categoric && current.grade.id == crit.criterion.categoricGrade?.id) current
else Grade.default(GradeType.CATEGORIC, crit.cat, crit.num)
GradeType.NUMERIC ->
if(current is Grade.Numeric && current.grade.id == crit.criterion.numericGrade?.id) current
else Grade.default(GradeType.NUMERIC, crit.cat, crit.num)
GradeType.PERCENTAGE ->
current as? Grade.Percentage ?: Grade.default(GradeType.PERCENTAGE, crit.cat, crit.num)
GradeType.NONE ->
current as? Grade.FreeText ?: Grade.default(GradeType.NONE, crit.cat, crit.num)
}
}
@Composable
fun GFWidget(
crit: GroupsGradingVM.CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any,
crit: CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any,
isOpen: Boolean, showDesc: Boolean = false, overrideName: String? = null, markOverridden: Set<UUID> = setOf(),
onToggle: () -> Unit
) = Surface(Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, shadowElevation = 3.dp) {
@@ -199,7 +183,7 @@ fun GFWidget(
Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp))
Button({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }) {
Button({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save grade and feedback")
}
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -43,9 +44,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.startEmail
import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.SnackVM
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
@@ -60,6 +64,7 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val group = remember(groups, focus) { if(focus != -1) groups[focus] else null }
val grades by vm.groupGrades.entities
val snacks = viewModel<SnackVM> { SnackVM() }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
@@ -75,7 +80,14 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
}
else {
Column(Modifier.padding(10.dp)) {
Text(group.group.name, style = MaterialTheme.typography.headlineMedium)
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(group.group.name, style = MaterialTheme.typography.headlineMedium)
if (group.members.any { it.first.contact.isNotBlank() }) {
IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) {
Icon(Mail, "Send email", Modifier.fillMaxHeight())
}
}
}
Spacer(Modifier.height(5.dp))
Row(Modifier.padding(5.dp)) {
var showTargetBorder by remember { mutableStateOf(false) }

View File

@@ -20,7 +20,7 @@ fun HomeTitle() = Text("Grader")
@Composable
fun HomeView(token: Navigator.NavToken) {
val vm = viewModel<HomeVM>()
val vm = viewModel<HomeVM> { HomeVM() }
val courses by vm.courses.entities
var addingCourse by remember { mutableStateOf(false) }

View File

@@ -1388,4 +1388,45 @@ val DoubleForward: ImageVector by lazy {
close()
}
}.build()
}
val Mail: ImageVector by lazy {
ImageVector.Builder(
name = "mail",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(22f, 7f)
lineToRelative(-8.991f, 5.727f)
arcToRelative(2f, 2f, 0f, false, true, -2.009f, 0f)
lineTo(2f, 7f)
}
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round
) {
moveTo(4f, 4f)
horizontalLineTo(20f)
arcTo(2f, 2f, 0f, false, true, 22f, 6f)
verticalLineTo(18f)
arcTo(2f, 2f, 0f, false, true, 20f, 20f)
horizontalLineTo(4f)
arcTo(2f, 2f, 0f, false, true, 2f, 18f)
verticalLineTo(6f)
arcTo(2f, 2f, 0f, false, true, 4f, 4f)
close()
}
}.build()
}

View File

@@ -1,13 +1,60 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.GroupGrading
import com.jaytux.grader.PeerEvalGrading
import com.jaytux.grader.app
import com.jaytux.grader.data.v2.CategoricGrade
import com.jaytux.grader.data.v2.GradeType
import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.viewmodel.Grade
import com.jaytux.grader.viewmodel.GroupsGradingVM
import com.jaytux.grader.viewmodel.Navigator
import com.jaytux.grader.viewmodel.PeerEvalsGradingVM
import sun.tools.jconsole.LabeledComponent.layout
@Composable
fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading")
@@ -17,4 +64,224 @@ fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
val vm = viewModel<PeerEvalsGradingVM>(key = data.assignment.id.toString()) {
PeerEvalsGradingVM(data.course, data.edition, data.assignment)
}
val groups by vm.groupList.entities
val focus by vm.focus
val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) }
val students by vm.students.entities
val matrix by vm.evaluationMatrix.entities
val studentGrades by vm.studentGrades.entities
var selectedStudent by remember(selectedGroup, studentGrades) {
mutableStateOf(0)
}
Column(Modifier.padding(10.dp)) {
Text("Grading ${vm.base.name}", style = MaterialTheme.typography.headlineMedium)
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
Spacer(Modifier.height(5.dp))
Row(Modifier.fillMaxSize()) {
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
}
}
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
if (focus == -1 || selectedGroup == null) {
Box(Modifier.weight(0.75f).fillMaxHeight()) {
Text("Select a group to start grading.", Modifier.align(Alignment.Center))
}
} else {
Column(Modifier.weight(0.75f).padding(15.dp)) {
Row {
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
Icon(DoubleBack, "Previous group")
}
Spacer(Modifier.width(10.dp))
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.weight(1f))
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
Icon(DoubleForward, "Next group")
}
}
Spacer(Modifier.height(10.dp))
matrix?.let { mat ->
students?.let { stu ->
Box(Modifier.weight(0.66f)) {
GradeTable(mat, stu, selectedGroup.group, vm.studentCriterion, vm::setEvaluation)
}
}
} ?: Box(Modifier.weight(0.66f).fillMaxWidth()) {
Text("Error: could not load evaluations for this group.", Modifier.align(Alignment.Center), color = MaterialTheme.colorScheme.error)
}
Column(Modifier.weight(0.33f)) {
studentGrades?.let { sgs ->
val currentStudent = sgs[selectedStudent]
PrimaryScrollableTabRow(selectedStudent, Modifier.fillMaxWidth()) {
sgs.forEachIndexed { idx, st ->
Tab(idx == selectedStudent, { selectedStudent = idx }) {
Row {
Icon(UserIcon, "")
Spacer(Modifier.width(5.dp))
Text(st.first.name, Modifier.align(Alignment.CenterVertically))
}
}
}
}
SingleStudentGrade(currentStudent.first.name, currentStudent.second, vm.global) { grade, feedback ->
vm.setStudentGrade(currentStudent.first, grade, feedback)
}
}
}
}
}
}
}
}
}
@Composable
fun GradeTable(
matrix: List<PeerEvalsGradingVM.Evaluation>, students: List<Student>, group: Group,
egData: CritData, onSet: (evaluator: Student, evaluatee: Student?, group: Group, grade: Grade, feedback: String) -> Unit
) {
Row {
val horScroll = rememberLazyListState()
val style = LocalTextStyle.current
val measure = rememberTextMeasurer()
val textLenMeasured = remember(matrix, students) {
students.maxOf { s ->
measure.measure(s.name, style).size.width
} + 10
}
val cellSize = 75.dp
var idx by remember(matrix, students) { mutableStateOf(0) }
var editing by remember(matrix, students) { mutableStateOf<Triple<Student, Student?, FeedbackItem?>?>(null) }
val isSelected = { from: Student, to: Student? ->
editing?.let { (f, t, _) -> f == from && t == to } ?: false
}
Column(Modifier.weight(0.66f).padding(10.dp)) {
Row {
Box { FromTo(textLenMeasured.dp) }
LazyRow(Modifier.height(textLenMeasured.dp), state = horScroll) {
item { VLine() }
items(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(key = idx) {
measuredItem { HLine() }
items(matrix) { (evaluator, groupLevel, s2s) ->
Row(Modifier.height(cellSize)) {
Column(Modifier.width(textLenMeasured.dp).align(Alignment.CenterVertically)) {
Text(evaluator.name, Modifier.width(textLenMeasured.dp))
}
LazyRow(state = horScroll) {
item { VLine() }
items(s2s) { (evaluatee, entry) ->
PEGradeWidget(
entry,
{ editing = evaluator to evaluatee app entry }, { editing = null },
isSelected(evaluator, evaluatee), Modifier.size(cellSize, cellSize)
)
}
item { VLine() }
item {
PEGradeWidget(
groupLevel,
{ editing = evaluator to null app groupLevel }, { editing = null },
isSelected(evaluator, null), Modifier.size(cellSize, cellSize)
)
}
item { VLine() }
}
}
}
measuredItem { HLine() }
}
}
editing?.let {
Surface(Modifier.weight(0.33f), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) {
val (evaluator, evaluatee, data) = it
EditS2SOrS2G(evaluator.name, evaluatee?.name ?: group.name, data, egData) { grade, feedback ->
onSet(evaluator, evaluatee, group, grade, feedback)
}
}
} ?: Box(Modifier.weight(0.33f)) {}
}
}
@Composable
fun EditS2SOrS2G(evaluator: String, evaluatee: String, current: FeedbackItem?, critData: CritData, onUpdate: (Grade, String) -> Unit) =
Column(Modifier.padding(10.dp).fillMaxHeight()) {
println("Recomposing editor for $evaluator -> $evaluatee with current ${current?.grade}")
var grade by remember(evaluator, evaluatee, current) { mutableStateOf(gradeState(critData, current?.grade)) }
var text by remember(evaluator, evaluatee, current) { mutableStateOf(current?.feedback ?: "") }
Text(evaluatee, style = MaterialTheme.typography.headlineSmall)
Text("Evaluated by $evaluator", style = MaterialTheme.typography.bodyMedium, fontStyle = FontStyle.Italic)
Spacer(Modifier.height(10.dp))
GradePicker(grade, key = evaluator to evaluatee to current) { grade = it }
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 10, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(10.dp))
Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save")
}
}
@Composable
fun SingleStudentGrade(name: String, current: FeedbackItem?, critData: CritData, onUpdate: (Grade, String) -> Unit) = Column {
var grade by remember(name, critData) { mutableStateOf(gradeState(critData, current?.grade)) }
var text by remember(name) { mutableStateOf(current?.feedback ?: "") }
GradePicker(grade, key = critData to current app name) { grade = it }
Spacer(Modifier.height(5.dp))
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
Spacer(Modifier.height(5.dp))
Button({ onUpdate(grade, text) }, Modifier.padding(horizontal = 20.dp).fillMaxWidth()) {
Text("Save grade and feedback")
}
}

View File

@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -35,14 +36,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.data.v2.Edition
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.startEmail
import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.SnackVM
@Composable
fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val students by vm.studentList.entities
val focus by vm.focusIndex
val snacks = viewModel<SnackVM> { SnackVM() }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(students, { Text("No students yet.") }) { idx, it ->
@@ -63,19 +68,30 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Column(Modifier.weight(0.75f).padding(15.dp)) {
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) {
Column(Modifier.padding(10.dp)) {
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall)
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall)
if(students[focus].contact.isNotBlank()) {
IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) {
Icon(Mail, "Send email", Modifier.fillMaxHeight())
}
}
}
Row {
var editing by remember { mutableStateOf(false) }
Text("Contact: ", Modifier.align(Alignment.CenterVertically).padding(start = 15.dp))
if(!editing) {
if (students[focus].contact.isBlank()) Text(
"No contact info.",
Modifier.padding(start = 5.dp),
fontStyle = FontStyle.Italic,
color = LocalTextStyle.current.color.copy(alpha = 0.5f)
)
else Text(students[focus].contact, Modifier.padding(start = 5.dp))
if (students[focus].contact.isBlank()) {
Text(
"No contact info.",
Modifier.padding(start = 5.dp),
fontStyle = FontStyle.Italic,
color = LocalTextStyle.current.color.copy(alpha = 0.5f)
)
}
else {
Text(students[focus].contact, Modifier.padding(start = 5.dp))
}
Spacer(Modifier.width(5.dp))
Icon(Edit, "Edit contact info", Modifier.clickable { editing = true })
}

View File

@@ -4,6 +4,72 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import com.jaytux.grader.data.v2.BaseFeedback
import com.jaytux.grader.data.v2.CategoricGrade
import com.jaytux.grader.data.v2.Criterion
import com.jaytux.grader.data.v2.GradeType
import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.viewmodel.Grade
import org.jetbrains.exposed.v1.core.Transaction
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
@Composable
fun TextUnit.toDp(): Dp = with(LocalDensity.current) { value.toDp() }
fun TextUnit.toDp(): Dp = with(LocalDensity.current) { value.toDp() }
data class CritData(val criterion: Criterion, val cat: CategoricGrade?, val num: NumericGrade?) {
companion object {
context(trns: Transaction)
fun fromDb(c: Criterion) = CritData(c, c.categoricGrade, c.numericGrade)
}
}
data class FeedbackItem(val base: BaseFeedback, val grade: Grade, val feedback: String) {
companion object {
context(trns: Transaction)
fun fromDb(f: BaseFeedback): FeedbackItem = when(f.criterion.gradeType) {
GradeType.CATEGORIC -> {
val categoric = f.criterion.categoricGrade!!
val options = categoric.options.toList()
Grade.Categoric(f.gradeCategoric ?: options.first(), options, categoric)
}
GradeType.NUMERIC -> Grade.Numeric(f.gradeNumeric ?: 0.0, f.criterion.numericGrade!!)
GradeType.PERCENTAGE -> Grade.Percentage(f.gradeNumeric ?: 0.0)
GradeType.NONE -> Grade.FreeText(f.gradeFreeText ?: "")
}.let { FeedbackItem(f, it, f.feedback) }
}
}
fun gradeState(type: GradeType, categoric: CategoricGrade?, numeric: NumericGrade?, current: Grade?): Grade = transaction {
if(current == null) {
println("gradeState: current is null, defaulting")
Grade.default(type, categoric, numeric)
}
else {
when(type) {
GradeType.CATEGORIC ->
if(current is Grade.Categoric && current.grade.id == categoric?.id) {
println("gradeState: current categoric grade is valid, keeping")
current
}
else {
println("gradeState: current categoric grade is invalid, defaulting [${current is Grade.Categoric} (${current::class.java.simpleName}), ${(current as? Grade.Categoric)?.grade?.name} == ${categoric?.name}]")
Grade.default(GradeType.CATEGORIC, categoric, numeric)
}
GradeType.NUMERIC ->
if(current is Grade.Numeric && current.grade.id == numeric?.id) {
println("gradeState: current numeric grade is valid, keeping")
current
}
else {
println("gradeState: current numeric grade is invalid, defaulting [${current is Grade.Numeric}, ${(current as? Grade.Numeric)?.grade?.id == numeric?.id}]")
Grade.default(GradeType.NUMERIC, categoric, numeric)
}
GradeType.PERCENTAGE ->
current as? Grade.Percentage ?: Grade.default(GradeType.PERCENTAGE, categoric, numeric)
GradeType.NONE ->
current as? Grade.FreeText ?: Grade.default(GradeType.NONE, categoric, numeric)
}
}
}
fun gradeState(crit: CritData, current: Grade?): Grade = gradeState(crit.criterion.gradeType, crit.cat, crit.num, current)

View File

@@ -126,7 +126,7 @@ fun FromTo(size: Dp) {
}
Box {
Text("Evaluated", Modifier.graphicsLayer {
Text("Evaluatee", Modifier.graphicsLayer {
rotationZ = -90f
translationX = w - 15f
translationY = h - 15f
@@ -136,19 +136,37 @@ fun FromTo(size: Dp) {
}
}
//@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) {
@Composable
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) selectedElevation else unselectedElevation,
shape = MaterialTheme.shapes.medium
) {
content()
}
}
@Composable
fun PEGradeWidget(
feedback: FeedbackItem?,
onSelect: () -> Unit, onDeselect: () -> Unit,
isSelected: Boolean,
modifier: Modifier = Modifier
) = Box(modifier.padding(2.dp)) {
Selectable(isSelected, onSelect, onDeselect) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
feedback?.grade?.render() ?: Text("(none)", fontStyle = FontStyle.Italic)
// Text(grade?.let { if(it.grade.isNotBlank()) it.grade else if(it.feedback.isNotBlank()) "(other)" else null } ?: "none")
// }
// }
//}
}
}
}
@Composable
fun VLine(width: Dp = 1.dp, color: Color = Color.Black) = Spacer(Modifier.fillMaxHeight().width(width).background(color))

View File

@@ -27,12 +27,7 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
data class CriterionData(val criterion: Criterion, val gradeType: UiGradeType) {
companion object {
context(trns: Transaction)
fun from(c: Criterion) = CriterionData(c, when(c.gradeType) {
GradeType.CATEGORIC -> UiGradeType.Categoric(c.categoricGrade!!.options.toList(), c.categoricGrade!!)
GradeType.NUMERIC -> UiGradeType.Numeric(c.numericGrade!!)
GradeType.PERCENTAGE -> UiGradeType.Percentage
GradeType.NONE -> UiGradeType.FreeText
})
fun from(c: Criterion) = CriterionData(c, UiGradeType.from(c.gradeType, c.categoricGrade, c.numericGrade))
}
}
data class AssignmentData(val assignment: BaseAssignment, val global: CriterionData, val criteria: List<CriterionData>)
@@ -82,15 +77,14 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
gr to asGroup?.asGroupFeedback app (solo != null)
}
AssignmentType.SOLO -> {
val gr = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st }
val eval = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st }
?.let { Grade.fromAssignment(asg.globalCriterion, it) }
gr to null app false
eval to null app false
}
AssignmentType.PEER_EVALUATION -> {
val asGroup = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id in groupIds }
val solo = asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfPeer.any { over -> over.student == st } }
val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) }
gr to asGroup?.asPeerEvaluationFeedback app (solo != null)
val eval = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == st.id }
?.let { Grade.fromAssignment(asg.globalCriterion, it) }
eval to null app false
}
}
@@ -123,6 +117,13 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
}
}
val asPeerEvaluation = RawDbFocusableSingleState { asg: BaseAssignment ->
asg.asPeerEvaluation?.let { peer ->
val stuCrit = peer.studentCriterion
peer to UiGradeType.from(stuCrit.gradeType, stuCrit.categoricGrade, stuCrit.numericGrade)
}
}
private val _selectedTab = mutableStateOf(Tab.STUDENTS)
private val _focusIndex = mutableStateOf(-1)
val selectedTab = _selectedTab.immutable()
@@ -147,7 +148,10 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
groupAvailableStudents.focus(grp)
groupGrades.focus(grp)
}
Tab.ASSIGNMENTS -> {}
Tab.ASSIGNMENTS -> {
val asg = assignmentList.entities.value[idx].assignment
asPeerEvaluation.focus(asg)
}
}
}
@@ -289,8 +293,8 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
}
private fun postCreateAsg() {
focus(assignmentList.entities.value.size)
assignmentList.refresh()
focus(assignmentList.entities.value.size - 1)
}
fun mkGroupAssignment(name: String) {
@@ -312,7 +316,16 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
fun mkPeerEvaluation(name: String) {
transaction {
val asg = mkBaseAssignment(name, AssignmentType.PEER_EVALUATION)
PeerEvaluation.new { this.base = asg }
val stCrit = Criterion.new {
this.assignment = asg
this.name = "@__internal"
this.desc = "INTERNAL ONLY: Criterion to store the grade type for peer evaluation assignments"
this.gradeType = GradeType.NONE
}
PeerEvaluation.new {
this.base = asg
this.studentCriterion = stCrit
}
}
postCreateAsg()
}
@@ -408,6 +421,24 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
numericGrades.refresh()
}
fun setPEGrade(pe: PeerEvaluation, gradeType: UiGradeType) {
transaction {
pe.studentCriterion.gradeType = when (gradeType) {
is UiGradeType.Categoric -> GradeType.CATEGORIC
is UiGradeType.Numeric -> GradeType.NUMERIC
UiGradeType.Percentage -> GradeType.PERCENTAGE
UiGradeType.FreeText -> GradeType.NONE
}
when (gradeType) {
is UiGradeType.Categoric -> pe.studentCriterion.categoricGrade = gradeType.grade
is UiGradeType.Numeric -> pe.studentCriterion.numericGrade = gradeType.grade
else -> {}
}
}
asPeerEvaluation.refresh()
}
fun rmAssignment(assignment: BaseAssignment) {
transaction {
assignment.delete()

View File

@@ -15,10 +15,18 @@ import org.jetbrains.exposed.v1.core.Transaction
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
sealed class Grade {
data class FreeText(val text: String) : Grade()
data class Percentage(val percentage: Double) : Grade()
data class Numeric(val value: Double, val grade: NumericGrade) : Grade()
data class Categoric(val value: CategoricGradeOption, val options: List<CategoricGradeOption>, val grade: CategoricGrade) : Grade()
data class FreeText(val text: String) : Grade() {
override fun toString(): String = "FreeText($text)"
}
data class Percentage(val percentage: Double) : Grade() {
override fun toString(): String = "Perc($percentage%)"
}
data class Numeric(val value: Double, val grade: NumericGrade) : Grade() {
override fun toString(): String = "Numeric($value / ${grade.max})"
}
data class Categoric(val value: CategoricGradeOption, val options: List<CategoricGradeOption>, val grade: CategoricGrade) : Grade() {
override fun toString(): String = "Categoric(${value.option})"
}
@Composable
fun render(modifier: Modifier = Modifier) = when(this) {

View File

@@ -18,6 +18,8 @@ import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.data.v2.StudentOverrideFeedback
import com.jaytux.grader.data.v2.StudentOverrideFeedbacks
import com.jaytux.grader.ui.CritData
import com.jaytux.grader.ui.FeedbackItem
import org.jetbrains.exposed.v1.core.Transaction
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
@@ -28,28 +30,7 @@ import org.jetbrains.exposed.v1.jdbc.upsertReturning
class GroupsGradingVM(val course: Course, val edition: Edition, val base: BaseAssignment) : ViewModel() {
data class GroupData(val group: Group, val students: List<Pair<Student, String?>>)
data class FeedbackItem(val base: BaseFeedback, val grade: Grade, val feedback: String) {
companion object {
context(trns: Transaction)
fun fromDb(f: BaseFeedback): FeedbackItem = when(f.criterion.gradeType) {
GradeType.CATEGORIC -> {
val categoric = f.criterion.categoricGrade!!
val options = categoric.options.toList()
Grade.Categoric(f.gradeCategoric ?: options.first(), options, categoric)
}
GradeType.NUMERIC -> Grade.Numeric(f.gradeNumeric ?: 0.0, f.criterion.numericGrade!!)
GradeType.PERCENTAGE -> Grade.Percentage(f.gradeNumeric ?: 0.0)
GradeType.NONE -> Grade.FreeText(f.gradeFreeText ?: "")
}.let { FeedbackItem(f, it, f.feedback) }
}
}
data class FeedbackData(val groupLevel: FeedbackItem?, val overrides: List<Pair<Student, FeedbackItem?>>)
data class CritData(val criterion: Criterion, val cat: CategoricGrade?, val num: NumericGrade?) {
companion object {
context(trns: Transaction)
fun fromDb(c: Criterion) = CritData(c, c.categoricGrade, c.numericGrade)
}
}
private val _focus = mutableStateOf(-1)
val focus = _focus.immutable()

View File

@@ -67,7 +67,7 @@ class Navigator private constructor(
?: throw IllegalStateException("No renderer for destination of type ${top.dest::class.simpleName}")
top to render
}
val snackVM = viewModel<SnackVM>()
val snackVM = viewModel<SnackVM> { SnackVM() }
snackVM.Launcher(state)
BackHandler { back() }

View File

@@ -1,9 +1,217 @@
package com.jaytux.grader.viewmodel
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.jaytux.grader.app
import com.jaytux.grader.data.v2.BaseAssignment
import com.jaytux.grader.data.v2.BaseFeedback
import com.jaytux.grader.data.v2.BaseFeedbacks
import com.jaytux.grader.data.v2.Course
import com.jaytux.grader.data.v2.Edition
import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.GroupStudent
import com.jaytux.grader.data.v2.PeerEvaluationFeedbacks
import com.jaytux.grader.data.v2.PeerEvaluationS2G
import com.jaytux.grader.data.v2.PeerEvaluationS2GEvaluations
import com.jaytux.grader.data.v2.PeerEvaluationS2S
import com.jaytux.grader.data.v2.PeerEvaluationS2SEvaluations
import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.ui.CritData
import com.jaytux.grader.ui.FeedbackItem
import com.jaytux.grader.viewmodel.GroupsGradingVM.GroupData
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.dao.with
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
class PeerEvalsGradingVM(val course: Course, val edition: Edition, val base: BaseAssignment) : ViewModel() {
data class S2S(val evaluatee: Student, val data: FeedbackItem?)
data class Evaluation(val evaluator: Student, val groupLevel: FeedbackItem?, val s2s: List<S2S>)
private val _focus = mutableStateOf(-1)
val focus = _focus.immutable()
val asPeer = transaction { base.asPeerEvaluation!! }
val global = transaction { CritData.fromDb(base.globalCriterion) }
val studentCriterion = transaction { CritData.fromDb(asPeer.studentCriterion) }
val groupList = RawDbState {
edition.groups.with(Group::students, GroupStudent::student).map { group ->
GroupData(group, group.students.map { Pair(it.student, it.role) })
}
}
val evaluationMatrix = RawDbFocusableState { group: Group ->
val studentIds = group.students.map { it.student.id.value }
val s2gs = PeerEvaluationS2G.find {
(PeerEvaluationS2GEvaluations.peerEvalId eq asPeer.id) and
(PeerEvaluationS2GEvaluations.studentId inList studentIds)
}.also {
println("S2G for group ${group.name}:")
it.forEach { println(" ${it.student.name} -> ${it.evaluation.gradeCategoric ?: it.evaluation.gradeNumeric ?: it.evaluation.gradeFreeText}") }
}
val s2ss = PeerEvaluationS2S.find {
(PeerEvaluationS2SEvaluations.peerEvalId eq asPeer.id) and
(PeerEvaluationS2SEvaluations.studentId inList studentIds)
}
group.students.map { evaluator ->
val s2s = group.students.map { evaluatee ->
val item = s2ss.find { it.student.id == evaluator.student.id && it.evaluatedStudent.id == evaluatee.student.id }?.let {
FeedbackItem.fromDb(it.evaluation)
}
S2S(evaluatee.student, item)
}
val s2g = s2gs.find { it.student.id == evaluator.student.id }?.let { FeedbackItem.fromDb(it.evaluation) }
Evaluation(evaluator.student, s2g, s2s)
}
}
val studentGrades = RawDbFocusableState { group: Group ->
val studentIds = group.students.map { it.student.id.value }.toSet()
val mapping = global.criterion.feedbacks.mapNotNull {
it.asPeerEvaluationFeedback?.let { x ->
if(x.id.value in studentIds) x.id to FeedbackItem.fromDb(it) else null
}
}.toMap()
group.students.map { student -> student.student to mapping[student.student.id] }
}
val students = RawDbFocusableState { group: Group ->
group.students.map { it.student }
}
fun focusGroup(idx: Int) {
_focus.value = idx
val current = groupList.entities.value[idx].group
evaluationMatrix.focus(current)
students.focus(current)
studentGrades.focus(current)
}
fun focusPrev() {
if (focus.value > 0) focusGroup(focus.value - 1)
}
fun focusNext() {
if (focus.value < groupList.entities.value.size - 1) focusGroup(focus.value + 1)
}
private fun setStudentEvaluation(evaluator: Student, evaluatee: Student, grade: Grade, feedback: String) = transaction {
val existing = PeerEvaluationS2S.find {
(PeerEvaluationS2SEvaluations.peerEvalId eq asPeer.id) and
(PeerEvaluationS2SEvaluations.studentId eq evaluator.id) and
(PeerEvaluationS2SEvaluations.evaluatedStudentId eq evaluatee.id)
}.firstOrNull()
if(existing != null) {
existing.evaluation.feedback = feedback
when(grade) {
is Grade.Categoric -> existing.evaluation.gradeCategoric = grade.value
is Grade.Numeric -> existing.evaluation.gradeNumeric = grade.value
is Grade.Percentage -> existing.evaluation.gradeNumeric = grade.percentage
is Grade.FreeText -> existing.evaluation.gradeFreeText = grade.text
}
}
else {
val base = BaseFeedback.new {
criterion = studentCriterion.criterion
this.feedback = feedback
when(grade) {
is Grade.Categoric -> this.gradeCategoric = grade.value
is Grade.Numeric -> this.gradeNumeric = grade.value
is Grade.Percentage -> this.gradeNumeric = grade.percentage
is Grade.FreeText -> this.gradeFreeText = grade.text
}
}
PeerEvaluationS2S.new {
evaluation = base
peerEvaluation = asPeer
student = evaluator
evaluatedStudent = evaluatee
}
}
}
private fun setStudentGroupEvaluation(evaluator: Student, group: Group, grade: Grade, feedback: String) = transaction {
val existing = PeerEvaluationS2G.find {
(PeerEvaluationS2GEvaluations.peerEvalId eq asPeer.id) and
(PeerEvaluationS2GEvaluations.studentId eq evaluator.id) and
(PeerEvaluationS2GEvaluations.groupId eq group.id)
}.firstOrNull()
if(existing != null) {
existing.evaluation.feedback = feedback
when(grade) {
is Grade.Categoric -> existing.evaluation.gradeCategoric = grade.value
is Grade.Numeric -> existing.evaluation.gradeNumeric = grade.value
is Grade.Percentage -> existing.evaluation.gradeNumeric = grade.percentage
is Grade.FreeText -> existing.evaluation.gradeFreeText = grade.text
}
}
else {
val base = BaseFeedback.new {
criterion = studentCriterion.criterion
this.feedback = feedback
when(grade) {
is Grade.Categoric -> this.gradeCategoric = grade.value
is Grade.Numeric -> this.gradeNumeric = grade.value
is Grade.Percentage -> this.gradeNumeric = grade.percentage
is Grade.FreeText -> this.gradeFreeText = grade.text
}
}
PeerEvaluationS2G.new {
evaluation = base
peerEvaluation = asPeer
student = evaluator
this.group = group
}
}
}
fun setEvaluation(evaluator: Student, evaluatee: Student?, group: Group, grade: Grade, feedback: String) {
println("Setting: evaluator=${evaluator.name}, evaluatee=${evaluatee?.name}, group=${group.name}, grade=$grade, feedback=$feedback")
evaluatee?.let { setStudentEvaluation(evaluator, it, grade, feedback) } ?: setStudentGroupEvaluation(evaluator, group, grade, feedback)
evaluationMatrix.refresh()
}
fun setStudentGrade(student: Student, grade: Grade, feedback: String) = transaction {
val existing = BaseFeedback.find { BaseFeedbacks.criterionId eq global.criterion.id }
.find { it.asPeerEvaluationFeedback?.id?.value == student.id.value }
if(existing != null) {
existing.feedback = feedback
when(grade) {
is Grade.Categoric -> existing.gradeCategoric = grade.value
is Grade.Numeric -> existing.gradeNumeric = grade.value
is Grade.Percentage -> existing.gradeNumeric = grade.percentage
is Grade.FreeText -> existing.gradeFreeText = grade.text
}
}
else {
val base = BaseFeedback.new {
criterion = global.criterion
this.feedback = feedback
when(grade) {
is Grade.Categoric -> this.gradeCategoric = grade.value
is Grade.Numeric -> this.gradeNumeric = grade.value
is Grade.Percentage -> this.gradeNumeric = grade.percentage
is Grade.FreeText -> this.gradeFreeText = grade.text
}
}
PeerEvaluationFeedbacks.insert {
it[PeerEvaluationFeedbacks.feedbackId] = base.id
it[PeerEvaluationFeedbacks.studentId] = student.id
}
}
studentGrades.refresh()
}
}

View File

@@ -17,4 +17,14 @@ sealed class UiGradeType {
object Percentage : UiGradeType()
data class Numeric(val grade: NumericGrade) : UiGradeType()
data class Categoric(val options: List<CategoricGradeOption>, val grade: CategoricGrade) : UiGradeType()
companion object {
context(trns: Transaction)
fun from(type: GradeType, categoric: CategoricGrade?, numeric: NumericGrade?) = when(type) {
GradeType.CATEGORIC -> Categoric(categoric!!.options.toList(), categoric)
GradeType.NUMERIC -> Numeric(numeric!!)
GradeType.PERCENTAGE -> Percentage
GradeType.NONE -> FreeText
}
}
}