From 29d23f84007a34c678d410013947079191c996aa Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 10 Jun 2025 15:18:13 +0200 Subject: [PATCH 1/5] Fixed primary key-related (nullability) issues in DB schema --- .../kotlin/com/jaytux/grader/data/DSL.kt | 8 ++-- .../kotlin/com/jaytux/grader/data/Database.kt | 3 ++ .../kotlin/com/jaytux/grader/data/Entities.kt | 2 + .../com/jaytux/grader/viewmodel/DbState.kt | 47 +++++++++++-------- gradle/libs.versions.toml | 3 +- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt index f81fd01..6f68497 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/DSL.kt @@ -56,6 +56,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") { val name = varchar("name", 50) val assignment = text("assignment") val deadline = datetime("deadline") + val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id) } object GroupAssignmentCriteria : UUIDTable("grpAsCr") { @@ -70,6 +71,7 @@ object SoloAssignments : UUIDTable("soloAssgmts") { val name = varchar("name", 50) val assignment = text("assignment") val deadline = datetime("deadline") + val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id) } object SoloAssignmentCriteria : UUIDTable("soloAsCr") { @@ -86,7 +88,7 @@ object PeerEvaluations : UUIDTable("peerEvals") { object GroupFeedbacks : CompositeIdTable("grpFdbks") { val assignmentId = reference("group_assignment_id", GroupAssignments.id) - val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() + val criterionId = reference("criterion_id", GroupAssignmentCriteria.id) val groupId = reference("group_id", Groups.id) val feedback = text("feedback") val grade = varchar("grade", 32) @@ -96,7 +98,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") { object IndividualFeedbacks : CompositeIdTable("indivFdbks") { val assignmentId = reference("group_assignment_id", GroupAssignments.id) - val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable() + val criterionId = reference("criterion_id", GroupAssignmentCriteria.id) val groupId = reference("group_id", Groups.id) val studentId = reference("student_id", Students.id) val feedback = text("feedback") @@ -107,7 +109,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") { object SoloFeedbacks : CompositeIdTable("soloFdbks") { val assignmentId = reference("solo_assignment_id", SoloAssignments.id) - val criterionId = reference("criterion_id", SoloAssignmentCriteria.id).nullable() + val criterionId = reference("criterion_id", SoloAssignmentCriteria.id) val studentId = reference("student_id", Students.id) val feedback = text("feedback") val grade = varchar("grade", 32) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt index b3bbecc..f247bbe 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt @@ -3,7 +3,10 @@ package com.jaytux.grader.data import MigrationUtils import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update object Database { val db by lazy { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt index 80dccf9..b585d90 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Entities.kt @@ -60,6 +60,7 @@ class GroupAssignment(id: EntityID) : Entity(id) { var name by GroupAssignments.name var assignment by GroupAssignments.assignment var deadline by GroupAssignments.deadline + var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId } @@ -98,6 +99,7 @@ class SoloAssignment(id: EntityID) : Entity(id) { var name by SoloAssignments.name var assignment by SoloAssignments.assignment var deadline by SoloAssignments.deadline + var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index 0ad0483..a79a268 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -475,7 +475,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() val criteria = RawDbState { - assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList() + assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id } } val feedback = RawDbState { loadFeedback() } @@ -494,7 +494,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { private fun Transaction.loadFeedback(): List> { val allCrit = GroupAssignmentCriterion.find { GroupAssignmentCriteria.assignmentId eq assignment.id - } + }.filter { it.id != assignment.globalCriterion.id } return Group.find { (Groups.editionId eq assignment.edition.id) @@ -502,16 +502,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) { val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where { (GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id) }.map { row -> - val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] } + val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]] val fdbk = row[GroupFeedbacks.feedback] val grade = row[GroupFeedbacks.grade] crit to FeedbackEntry(fdbk, grade) } - val global = forGroup.firstOrNull { it.first == null }?.second - val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } - .filterNotNull().associateBy { it.criterion.id } + val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second + val byCrit_ = forGroup + .filter{ it.first.id != assignment.globalCriterion.id } + .map { LocalCriterionFeedback(it.first, it.second) } + .associateBy { it.criterion.id } + val byCrit = allCrit.map { c -> byCrit_[c.id] ?: LocalCriterionFeedback(c, null) } @@ -527,16 +530,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) { (IndividualFeedbacks.assignmentId eq assignment.id) and (GroupStudents.studentId eq student.id) and (Groups.id eq group.id) }.map { row -> - val crit = row[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] } + val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]] val fdbk = row[IndividualFeedbacks.feedback] val grade = row[IndividualFeedbacks.grade] crit to FeedbackEntry(fdbk, grade) } - val global = forSt.firstOrNull { it.first == null }?.second - val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } } - .filterNotNull().associateBy { it.criterion.id } + val global = forSt.firstOrNull { it.first == assignment.globalCriterion.id }?.second + val byCrit_ = forSt + .filter { it.first != assignment.globalCriterion.id } + .map { LocalCriterionFeedback(it.first, it.second) } + .associateBy { it.criterion.id } + val byCrit = allCrit.map { c -> byCrit_[c.id] ?: LocalCriterionFeedback(c, null) } @@ -556,7 +562,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { it[groupId] = group.id it[this.feedback] = msg it[this.grade] = grd - it[criterionId] = criterion?.id + it[criterionId] = criterion?.id ?: assignment.globalCriterion.id } } feedback.refresh(); autofill.refresh() @@ -570,7 +576,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { it[studentId] = student.id it[this.feedback] = msg it[this.grade] = grd - it[criterionId] = criterion?.id + it[criterionId] = criterion?.id ?: assignment.globalCriterion.id } } feedback.refresh(); autofill.refresh() @@ -628,7 +634,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) { private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable() private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable() val criteria = RawDbState { - assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).toList() + assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id } } val feedback = RawDbState { loadFeedback() } @@ -641,22 +647,25 @@ class SoloAssignmentState(val assignment: SoloAssignment) { private fun Transaction.loadFeedback(): List> { val allCrit = SoloAssignmentCriterion.find { SoloAssignmentCriteria.assignmentId eq assignment.id - } + }.filter { it.id != assignment.globalCriterion.id } return editionCourse.second.soloStudents.sortAsc(Students.name).map { student -> val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where { (IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id) }.map { row -> - val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] } + val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]] val fdbk = row[IndividualFeedbacks.feedback] val grade = row[IndividualFeedbacks.grade] crit to LocalFeedback(fdbk, grade) } - val global = forStudent.firstOrNull { it.first == null }?.second - val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } } - .filterNotNull().associateBy { it.first.id } + val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second + val byCrit_ = forStudent + .filter { it.first != assignment.globalCriterion.id } + .map { Pair(it.first, it.second) } + .associateBy { it.first.id } + val byCrit = allCrit.map { c -> byCrit_[c.id] ?: Pair(c, null) } @@ -672,7 +681,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) { it[studentId] = student.id it[this.feedback] = msg ?: "" it[this.grade] = grd ?: "" - it[criterionId] = criterion?.id + it[criterionId] = criterion?.id ?: assignment.globalCriterion.id } } feedback.refresh(); autofill.refresh() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46d35c3..0408ea0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" } exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" } exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" } +exposed-migration = { group = "org.jetbrains.exposed", name = "exposed-migration", version.ref = "exposed" } exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" } sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" } sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" } @@ -33,4 +34,4 @@ rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } From 6dc88285d0c59acd18d4fd875fd6596431379832 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 10 Jun 2025 15:30:23 +0200 Subject: [PATCH 2/5] Show grades in student overview --- .../kotlin/com/jaytux/grader/viewmodel/DbState.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index a79a268..037a119 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -373,12 +373,12 @@ class StudentState(val student: Student, edition: Edition) { val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where { (GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and - (GroupAssignmentCriteria.name eq "") + (GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion) }.map { it[GroupAssignments.id] to it } val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where { (IndividualFeedbacks.studentId eq student.id) and - (GroupAssignmentCriteria.name eq "") + (GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion) }.map { it[GroupAssignments.id] to it } val res = mutableMapOf, LocalGroupGrade>() @@ -396,7 +396,7 @@ class StudentState(val student: Student, edition: Edition) { val og = res[gAId] ?: LocalGroupGrade(iRow[Groups.name], iRow[GroupAssignments.name], null, null) res[gAId] = og.copy(indivGrade = iRow[IndividualFeedbacks.grade]) } - + res.values.toList() } @@ -644,7 +644,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) { }.flatten().distinct().sorted() } - private fun Transaction.loadFeedback(): List> { + private fun Transaction.loadFeedback(): List> {3 val allCrit = SoloAssignmentCriterion.find { SoloAssignmentCriteria.assignmentId eq assignment.id }.filter { it.id != assignment.globalCriterion.id } From f7b4f29e2e73883c68c3ee09f0b4ef795979e474 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 10 Jun 2025 15:50:43 +0200 Subject: [PATCH 3/5] Allow rich-text in feedback --- .../com/jaytux/grader/ui/Assignments.kt | 52 +++++++++---------- .../kotlin/com/jaytux/grader/ui/RichText.kt | 15 ++++++ .../kotlin/com/jaytux/grader/ui/Widgets.kt | 4 +- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt index 9132253..8d06552 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -126,14 +126,15 @@ fun groupTaskWidget( Row { DateTimePicker(deadline, onSetDeadline) } - RichTextStyleRow(state = updTask) - OutlinedRichTextEditor( - state = updTask, - modifier = Modifier.fillMaxWidth().weight(1f), - singleLine = false, - minLines = 5, - label = { Text("Task") } - ) + RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") } +// RichTextStyleRow(state = updTask) +// OutlinedRichTextEditor( +// state = updTask, +// modifier = Modifier.fillMaxWidth().weight(1f), +// singleLine = false, +// minLines = 5, +// label = { Text("Task") } +// ) CancelSaveRow( true, { updTask.setMarkdown(taskMD) }, @@ -276,16 +277,20 @@ fun groupFeedbackPane( key: Any? = null ) { var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } - var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } + val feedback = rememberRichTextState() + + LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) { + feedback.setMarkdown(criterionFeedback?.feedback ?: "") + } + Column(modifier) { Row { Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) Spacer(Modifier.weight(0.6f)) Button( - { onSetGrade(grade); onSetFeedback(feedback.text) }, - Modifier.weight(0.2f).align(Alignment.CenterVertically), - enabled = grade.isNotBlank() || feedback.text.isNotBlank() + { onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) }, + Modifier.weight(0.2f).align(Alignment.CenterVertically) ) { Text("Save") } @@ -297,11 +302,7 @@ fun groupFeedbackPane( } } Spacer(Modifier.height(5.dp)) - AutocompleteLineField( - feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } - ) { filter -> - autofill.filter { x -> x.trim().startsWith(filter.trim()) } - } + RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") } } } @@ -480,16 +481,19 @@ fun soloFeedbackPane( key: Any? = null ) { var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } - var feedback by remember(currentCriterion, criteria, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) } + val feedback = rememberRichTextState() + + LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) { + feedback.setMarkdown(criterionFeedback?.feedback ?: "") + } Column(modifier) { Row { Text("Overall grade: ", Modifier.align(Alignment.CenterVertically)) OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) Spacer(Modifier.weight(0.6f)) Button( - { onSetGrade(grade); onSetFeedback(feedback.text) }, - Modifier.weight(0.2f).align(Alignment.CenterVertically), - enabled = grade.isNotBlank() || feedback.text.isNotBlank() + { onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) }, + Modifier.weight(0.2f).align(Alignment.CenterVertically) ) { Text("Save") } @@ -501,11 +505,7 @@ fun soloFeedbackPane( } } Spacer(Modifier.height(5.dp)) - AutocompleteLineField( - feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") } - ) { filter -> - autofill.filter { x -> x.trim().startsWith(filter.trim()) } - } + RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") } } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt index 3be15cf..838fa87 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/RichText.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp import com.jaytux.grader.loadClipboard import com.jaytux.grader.toClipboard import com.mohamedrejeb.richeditor.model.RichTextState +import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor @Composable fun RichTextStyleRow( @@ -237,4 +238,18 @@ fun RichTextStyleButton( ) ) } +} + +@Composable +fun RichTextField( + state: RichTextState, + modifier: Modifier = Modifier, + buttonsModifier: Modifier = Modifier, + outerModifier: Modifier = Modifier, + label: @Composable (() -> Unit)? = null +) = Column(outerModifier) { + RichTextStyleRow(buttonsModifier, state) + OutlinedRichTextEditor( + state = state, modifier = modifier, singleLine = false, minLines = 5, label = label + ) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index fe51e68..475427a 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.window.* import com.jaytux.grader.data.Course import com.jaytux.grader.data.Edition import com.jaytux.grader.viewmodel.PeerEvaluationState +import com.mohamedrejeb.richeditor.model.RichTextState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.datetime.* @@ -211,7 +212,8 @@ fun PaneHeader(name: String, type: String, courseEdition: Pair) @OptIn(ExperimentalFoundationApi::class) @Composable -fun AutocompleteLineField( +fun AutocompleteLineField__( +// state: RichTextState, value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null, onFilter: (String) -> List From 0883d2332e33edeea3a94ebb3efe907749e30a5e Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 10 Jun 2025 16:41:02 +0200 Subject: [PATCH 4/5] Fix loading bug --- .../com/jaytux/grader/ui/Assignments.kt | 77 +++++++------------ .../com/jaytux/grader/viewmodel/DbState.kt | 21 +++-- 2 files changed, 41 insertions(+), 57 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt index 8d06552..6740048 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -12,11 +12,9 @@ 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.GroupAssignment import com.jaytux.grader.data.GroupAssignmentCriterion import com.jaytux.grader.data.SoloAssignmentCriterion import com.jaytux.grader.data.Student @@ -26,7 +24,6 @@ import com.jaytux.grader.viewmodel.SoloAssignmentState import com.mohamedrejeb.richeditor.model.rememberRichTextState import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor import kotlinx.datetime.LocalDateTime -import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction @Composable fun GroupAssignmentView(state: GroupAssignmentState) { @@ -189,7 +186,7 @@ fun groupTaskWidget( @Composable fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) { val (group, feedback, individual) = fdbk - var idx by remember(fdbk) { mutableStateOf(0) } + var studentIdx by remember(fdbk) { mutableStateOf(0) } var critIdx by remember(fdbk) { mutableStateOf(0) } val criteria by state.criteria.entities val suggestions by state.autofill.entities @@ -199,8 +196,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) { item { Surface( - Modifier.fillMaxWidth().clickable { idx = 0 }, - tonalElevation = if (idx == 0) 50.dp else 0.dp, + Modifier.fillMaxWidth().clickable { studentIdx = 0 }, + tonalElevation = if (studentIdx == 0) 50.dp else 0.dp, shape = MaterialTheme.shapes.medium ) { Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic) @@ -210,8 +207,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG itemsIndexed(individual.toList()) { i, (student, details) -> val (role, _) = details Surface( - Modifier.fillMaxWidth().clickable { idx = i + 1 }, - tonalElevation = if (idx == i + 1) 50.dp else 0.dp, + Modifier.fillMaxWidth().clickable { studentIdx = i + 1 }, + tonalElevation = if (studentIdx == i + 1) 50.dp else 0.dp, shape = MaterialTheme.shapes.medium ) { Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp)) @@ -220,45 +217,25 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG } } - val updateGrade = { grade: String -> - if(idx == 0) { - state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade) - } - else { - val ind = individual[idx - 1] - val glob = ind.second.second.global - state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade) - } - } - - val updateFeedback = { fdbk: String -> - if(idx == 0) { - if(critIdx == 0) { - state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null) - } - else { - val current = feedback.byCriterion[critIdx - 1] - state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion) - } - } - else { - val ind = individual[idx - 1] - if(critIdx == 0) { - val entry = ind.second.second - state.upsertIndividualFeedback(ind.first, group, fdbk, entry.global?.grade ?: "", null) - } - else { - val entry = ind.second.second.byCriterion[critIdx - 1] - state.upsertIndividualFeedback(ind.first, group, fdbk, entry.entry?.grade ?: "", entry.criterion) - } + val onSave = { grade: String, fdbk: String -> + when { + studentIdx == 0 && critIdx == 0 -> state.upsertGroupFeedback(group, fdbk, grade) + studentIdx == 0 && critIdx != 0 -> state.upsertGroupFeedback(group, fdbk, grade, criteria[critIdx - 1]) + studentIdx != 0 && critIdx == 0 -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade) + else -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade, criteria[critIdx - 1]) } } groupFeedbackPane( - criteria, critIdx, { critIdx = it }, feedback.global, - if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry, - suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp), - key = idx to critIdx + criteria, critIdx, { critIdx = it }, + when { + studentIdx == 0 && critIdx == 0 -> feedback.global + studentIdx == 0 && critIdx != 0 -> feedback.byCriterion[critIdx - 1].entry + studentIdx != 0 && critIdx == 0 -> individual[studentIdx - 1].second.second.global + else -> individual[studentIdx - 1].second.second.byCriterion[critIdx - 1].entry + }, + suggestions, onSave, Modifier.weight(0.75f).padding(10.dp), + key = studentIdx to critIdx ) } } @@ -268,19 +245,17 @@ fun groupFeedbackPane( criteria: List, currentCriterion: Int, onSelectCriterion: (Int) -> Unit, - globFeedback: GroupAssignmentState.FeedbackEntry?, - criterionFeedback: GroupAssignmentState.FeedbackEntry?, + rawFeedback: GroupAssignmentState.FeedbackEntry?, autofill: List, - onSetGrade: (String) -> Unit, - onSetFeedback: (String) -> Unit, + onSave: (String, String) -> Unit, modifier: Modifier = Modifier, key: Any? = null ) { - var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") } + var grade by remember(rawFeedback, key) { mutableStateOf(rawFeedback?.grade ?: "") } val feedback = rememberRichTextState() - LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) { - feedback.setMarkdown(criterionFeedback?.feedback ?: "") + LaunchedEffect(currentCriterion, criteria, rawFeedback, key) { + feedback.setMarkdown(rawFeedback?.feedback ?: "") } Column(modifier) { @@ -289,7 +264,7 @@ fun groupFeedbackPane( OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f)) Spacer(Modifier.weight(0.6f)) Button( - { onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) }, + { onSave(grade, feedback.toMarkdown()) }, Modifier.weight(0.2f).align(Alignment.CenterVertically) ) { Text("Save") diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt index 037a119..675e9a2 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/DbState.kt @@ -171,10 +171,14 @@ class EditionState(val edition: Edition) { fun newSoloAssignment(name: String) { transaction { - SoloAssignment.new { + val assign = SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() this.number = nextIdx() } + val global = SoloAssignmentCriterion.new { + this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign + } + assign.globalCriterion = global solo.refresh() } } @@ -186,10 +190,14 @@ class EditionState(val edition: Edition) { } fun newGroupAssignment(name: String) { transaction { - GroupAssignment.new { + val assign = GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() this.number = nextIdx() } + val global = GroupAssignmentCriterion.new { + this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign + } + assign.globalCriterion = global groupAs.refresh() } } @@ -494,7 +502,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { private fun Transaction.loadFeedback(): List> { val allCrit = GroupAssignmentCriterion.find { GroupAssignmentCriteria.assignmentId eq assignment.id - }.filter { it.id != assignment.globalCriterion.id } + }//.filter { it.id != assignment.globalCriterion.id } return Group.find { (Groups.editionId eq assignment.edition.id) @@ -525,11 +533,12 @@ class GroupAssignmentState(val assignment: GroupAssignment) { val student = it.student val role = it.role - val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents) + val forSt = (IndividualFeedbacks innerJoin Groups) .selectAll().where { (IndividualFeedbacks.assignmentId eq assignment.id) and - (GroupStudents.studentId eq student.id) and (Groups.id eq group.id) + (IndividualFeedbacks.studentId eq student.id) and (Groups.id eq group.id) }.map { row -> + val stdId = row[IndividualFeedbacks.studentId] val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]] val fdbk = row[IndividualFeedbacks.feedback] val grade = row[IndividualFeedbacks.grade] @@ -537,7 +546,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) { crit to FeedbackEntry(fdbk, grade) } - val global = forSt.firstOrNull { it.first == assignment.globalCriterion.id }?.second + val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second val byCrit_ = forSt .filter { it.first != assignment.globalCriterion.id } .map { LocalCriterionFeedback(it.first, it.second) } From 119ff4b6c500db752a87b0dee72358f9514a0dc7 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Tue, 10 Jun 2025 16:42:16 +0200 Subject: [PATCH 5/5] Remove needless commented code --- .../kotlin/com/jaytux/grader/ui/Assignments.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt index 6740048..96b556f 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Assignments.kt @@ -124,14 +124,6 @@ fun groupTaskWidget( DateTimePicker(deadline, onSetDeadline) } RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") } -// RichTextStyleRow(state = updTask) -// OutlinedRichTextEditor( -// state = updTask, -// modifier = Modifier.fillMaxWidth().weight(1f), -// singleLine = false, -// minLines = 5, -// label = { Text("Task") } -// ) CancelSaveRow( true, { updTask.setMarkdown(taskMD) },