Compare commits
10 Commits
6b80457da5
...
main
Author | SHA1 | Date | |
---|---|---|---|
eca161b251
|
|||
0d6f361a45
|
|||
59d97f8ce5
|
|||
497af308fe | |||
119ff4b6c5
|
|||
0883d2332e
|
|||
f7b4f29e2e
|
|||
6dc88285d0
|
|||
29d23f8400
|
|||
3a47154969
|
@ -31,10 +31,15 @@ kotlin {
|
|||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
implementation(libs.exposed.dao)
|
implementation(libs.exposed.dao)
|
||||||
|
implementation(libs.exposed.migration)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.material3.desktop)
|
implementation(libs.material3.desktop)
|
||||||
implementation(libs.rtfield)
|
implementation(libs.rtfield)
|
||||||
|
implementation(libs.filekit.core)
|
||||||
|
implementation(libs.filekit.dialogs)
|
||||||
|
implementation(libs.filekit.dialogs.compose)
|
||||||
|
implementation(libs.filekit.coil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,6 +54,10 @@ compose.desktop {
|
|||||||
packageName = "com.jaytux.grader"
|
packageName = "com.jaytux.grader"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.0"
|
||||||
includeAllModules = true
|
includeAllModules = true
|
||||||
|
|
||||||
|
linux {
|
||||||
|
modules("jdk.security.auth")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
|
|||||||
val name = varchar("name", 50)
|
val name = varchar("name", 50)
|
||||||
val assignment = text("assignment")
|
val assignment = text("assignment")
|
||||||
val deadline = datetime("deadline")
|
val deadline = datetime("deadline")
|
||||||
|
val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
|
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
|
||||||
@ -70,6 +71,7 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
|
|||||||
val name = varchar("name", 50)
|
val name = varchar("name", 50)
|
||||||
val assignment = text("assignment")
|
val assignment = text("assignment")
|
||||||
val deadline = datetime("deadline")
|
val deadline = datetime("deadline")
|
||||||
|
val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
|
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
|
||||||
@ -86,7 +88,7 @@ object PeerEvaluations : UUIDTable("peerEvals") {
|
|||||||
|
|
||||||
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
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 groupId = reference("group_id", Groups.id)
|
||||||
val feedback = text("feedback")
|
val feedback = text("feedback")
|
||||||
val grade = varchar("grade", 32)
|
val grade = varchar("grade", 32)
|
||||||
@ -96,7 +98,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
|||||||
|
|
||||||
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
||||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
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 groupId = reference("group_id", Groups.id)
|
||||||
val studentId = reference("student_id", Students.id)
|
val studentId = reference("student_id", Students.id)
|
||||||
val feedback = text("feedback")
|
val feedback = text("feedback")
|
||||||
@ -107,7 +109,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
|||||||
|
|
||||||
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
||||||
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
|
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 studentId = reference("student_id", Students.id)
|
||||||
val feedback = text("feedback")
|
val feedback = text("feedback")
|
||||||
val grade = varchar("grade", 32)
|
val grade = varchar("grade", 32)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package com.jaytux.grader.data
|
package com.jaytux.grader.data
|
||||||
|
|
||||||
|
import MigrationUtils
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
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.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
object Database {
|
object Database {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
@ -17,15 +21,19 @@ object Database {
|
|||||||
StudentToGroupEvaluation
|
StudentToGroupEvaluation
|
||||||
)
|
)
|
||||||
|
|
||||||
val addMissing = SchemaUtils.addMissingColumnsStatements(
|
val migrate = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||||
Courses, Editions, Groups,
|
Courses, Editions, Groups,
|
||||||
Students, GroupStudents, EditionStudents,
|
Students, GroupStudents, EditionStudents,
|
||||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
||||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
||||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
||||||
StudentToGroupEvaluation
|
StudentToGroupEvaluation,
|
||||||
|
withLogs = true
|
||||||
)
|
)
|
||||||
addMissing.forEach { exec(it) }
|
|
||||||
|
println(" --- Migration --- ")
|
||||||
|
migrate.forEach { println(it); exec(it) }
|
||||||
|
println(" --- End migration --- ")
|
||||||
}
|
}
|
||||||
actual
|
actual
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||||||
var name by GroupAssignments.name
|
var name by GroupAssignments.name
|
||||||
var assignment by GroupAssignments.assignment
|
var assignment by GroupAssignments.assignment
|
||||||
var deadline by GroupAssignments.deadline
|
var deadline by GroupAssignments.deadline
|
||||||
|
var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion
|
||||||
|
|
||||||
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
|
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
|
||||||
}
|
}
|
||||||
@ -98,6 +99,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||||||
var name by SoloAssignments.name
|
var name by SoloAssignments.name
|
||||||
var assignment by SoloAssignments.assignment
|
var assignment by SoloAssignments.assignment
|
||||||
var deadline by SoloAssignments.deadline
|
var deadline by SoloAssignments.deadline
|
||||||
|
var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion
|
||||||
|
|
||||||
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
|
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.jaytux.grader.data
|
||||||
|
|
||||||
|
import com.jaytux.grader.viewmodel.Assignment
|
||||||
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
|
import io.github.vinceglb.filekit.PlatformFile
|
||||||
|
|
||||||
|
class MdBuilder {
|
||||||
|
private val content = StringBuilder()
|
||||||
|
|
||||||
|
fun appendHeader(text: String, level: Int = 1) {
|
||||||
|
require(level in 1..6) { "Header level must be between 1 and 6" }
|
||||||
|
content.appendLine()
|
||||||
|
content.appendLine("#".repeat(level) + " $text")
|
||||||
|
content.appendLine()
|
||||||
|
}
|
||||||
|
fun appendMd(text: String) { content.appendLine(text) }
|
||||||
|
fun appendParagraph(text: String, bold: Boolean = false, italic: Boolean = false) {
|
||||||
|
val formattedText = buildString {
|
||||||
|
if (bold) append("**")
|
||||||
|
if (italic) append("_")
|
||||||
|
append(text)
|
||||||
|
if (italic) append("_")
|
||||||
|
if (bold) append("**")
|
||||||
|
}
|
||||||
|
content.appendLine(formattedText)
|
||||||
|
content.appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): String = content.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GroupAssignmentState.LocalGFeedback.exportTo(path: PlatformFile, assignment: GroupAssignment) {
|
||||||
|
val builder = MdBuilder()
|
||||||
|
builder.appendHeader("${assignment.name} Feedback for ${group.name}")
|
||||||
|
if(feedback.global != null && feedback.global.grade.isNotBlank()) {
|
||||||
|
val global = feedback.global.grade
|
||||||
|
builder.appendParagraph("Overall grade: ${feedback.global.grade}", true, true)
|
||||||
|
|
||||||
|
individuals.forEach { (student, it) ->
|
||||||
|
val (_, data) = it
|
||||||
|
if(data.global != null && data.global.grade.isNotBlank() && data.global.grade != global) {
|
||||||
|
builder.appendParagraph("${student.name} grade: ${data.global.grade}", true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendFeedback(heading: String, group: GroupAssignmentState.FeedbackEntry?, byStudent: List<Pair<Student, GroupAssignmentState.FeedbackEntry>>) {
|
||||||
|
if(group != null || byStudent.isNotEmpty()) {
|
||||||
|
builder.appendHeader(heading, 2)
|
||||||
|
if(group != null) {
|
||||||
|
if(group.grade.isNotBlank()) {
|
||||||
|
builder.appendParagraph("Group grade: ${group.grade}", true, true)
|
||||||
|
}
|
||||||
|
if(group.feedback.isNotBlank()) {
|
||||||
|
builder.appendMd(group.feedback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byStudent.forEach { (student, it) ->
|
||||||
|
if(it.grade.isNotBlank() || it.feedback.isNotBlank()) builder.appendHeader(student.name, 3)
|
||||||
|
if(it.grade.isNotBlank()) {
|
||||||
|
builder.appendParagraph("Grade: ${it.grade}", true, true)
|
||||||
|
}
|
||||||
|
if(it.feedback.isNotBlank()) {
|
||||||
|
builder.appendMd(it.feedback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFeedback("Overall Feedback", feedback.global,
|
||||||
|
individuals.mapNotNull { it.second.second.global?.let { g -> it.first to g } }
|
||||||
|
)
|
||||||
|
|
||||||
|
val criteria = (feedback.byCriterion.map { (c, _) -> c } +
|
||||||
|
individuals.flatMap { (_, it) -> it.second.byCriterion.map { (c, _) -> c } }).distinctBy { it.id.value }
|
||||||
|
|
||||||
|
criteria.forEach { c ->
|
||||||
|
appendFeedback(
|
||||||
|
c.name,
|
||||||
|
feedback.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry,
|
||||||
|
individuals.mapNotNull { (student, it) ->
|
||||||
|
val entry = it.second.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry
|
||||||
|
entry?.let { student to it }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
path.file.writeText(builder.build())
|
||||||
|
}
|
@ -4,10 +4,13 @@ import androidx.compose.ui.window.Window
|
|||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import com.jaytux.grader.App
|
import com.jaytux.grader.App
|
||||||
import com.jaytux.grader.data.Database
|
import com.jaytux.grader.data.Database
|
||||||
|
import io.github.vinceglb.filekit.FileKit
|
||||||
|
|
||||||
fun main(){
|
fun main(){
|
||||||
Database.init()
|
Database.init()
|
||||||
application {
|
application {
|
||||||
|
FileKit.init(appId = "com.jaytux.grader")
|
||||||
|
|
||||||
Window(
|
Window(
|
||||||
onCloseRequest = ::exitApplication,
|
onCloseRequest = ::exitApplication,
|
||||||
title = "Grader",
|
title = "Grader",
|
||||||
|
@ -12,21 +12,22 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.layout.layout
|
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.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.jaytux.grader.data.GroupAssignment
|
|
||||||
import com.jaytux.grader.data.GroupAssignmentCriterion
|
import com.jaytux.grader.data.GroupAssignmentCriterion
|
||||||
import com.jaytux.grader.data.SoloAssignmentCriterion
|
import com.jaytux.grader.data.SoloAssignmentCriterion
|
||||||
import com.jaytux.grader.data.Student
|
import com.jaytux.grader.data.Student
|
||||||
|
import com.jaytux.grader.data.exportTo
|
||||||
|
import com.jaytux.grader.maxN
|
||||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
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
|
||||||
|
import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupAssignmentView(state: GroupAssignmentState) {
|
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||||
@ -126,14 +127,7 @@ fun groupTaskWidget(
|
|||||||
Row {
|
Row {
|
||||||
DateTimePicker(deadline, onSetDeadline)
|
DateTimePicker(deadline, onSetDeadline)
|
||||||
}
|
}
|
||||||
RichTextStyleRow(state = updTask)
|
RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") }
|
||||||
OutlinedRichTextEditor(
|
|
||||||
state = updTask,
|
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
|
||||||
singleLine = false,
|
|
||||||
minLines = 5,
|
|
||||||
label = { Text("Task") }
|
|
||||||
)
|
|
||||||
CancelSaveRow(
|
CancelSaveRow(
|
||||||
true,
|
true,
|
||||||
{ updTask.setMarkdown(taskMD) },
|
{ updTask.setMarkdown(taskMD) },
|
||||||
@ -188,77 +182,106 @@ fun groupTaskWidget(
|
|||||||
@Composable
|
@Composable
|
||||||
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
||||||
val (group, feedback, individual) = fdbk
|
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) }
|
var critIdx by remember(fdbk) { mutableStateOf(0) }
|
||||||
val criteria by state.criteria.entities
|
val criteria by state.criteria.entities
|
||||||
val suggestions by state.autofill.entities
|
val suggestions by state.autofill.entities
|
||||||
|
val exporting by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val exporter = rememberFileSaverLauncher { file ->
|
||||||
|
file?.let {
|
||||||
|
scope.launch { fdbk.exportTo(it, state.assignment) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val critGrade: (Int) -> String? = { crit: Int ->
|
||||||
|
when {
|
||||||
|
studentIdx == 0 && crit == 0 -> feedback.global?.grade?.ifBlank { null }
|
||||||
|
studentIdx == 0 && crit != 0 -> feedback.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
|
||||||
|
studentIdx != 0 && crit == 0 -> individual[studentIdx - 1].second.second.global?.grade?.ifBlank { null }
|
||||||
|
else -> individual[studentIdx - 1].second.second.byCriterion[crit - 1].entry?.grade?.ifBlank { null }
|
||||||
|
}.also { println("Mapping criterion #${crit} to grade ${it}") }
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||||
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
|
Column(Modifier.padding(10.dp)) {
|
||||||
item {
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
Surface(
|
item {
|
||||||
Modifier.fillMaxWidth().clickable { idx = 0 },
|
Surface(
|
||||||
tonalElevation = if (idx == 0) 50.dp else 0.dp,
|
Modifier.fillMaxWidth().clickable { studentIdx = 0 },
|
||||||
shape = MaterialTheme.shapes.medium
|
tonalElevation = if (studentIdx == 0) 50.dp else 0.dp,
|
||||||
) {
|
shape = MaterialTheme.shapes.medium
|
||||||
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
) {
|
||||||
|
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(individual.toList()) { i, (student, details) ->
|
||||||
|
val (role, _) = details
|
||||||
|
Surface(
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(individual.toList()) { i, (student, details) ->
|
Button(
|
||||||
val (role, _) = details
|
{ exporter.launch("${state.assignment.name} (${fdbk.group.name})", "md") },
|
||||||
Surface(
|
Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
|
||||||
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
) {
|
||||||
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
|
Text("Export group feedback")
|
||||||
shape = MaterialTheme.shapes.medium
|
}
|
||||||
) {
|
}
|
||||||
Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||||
|
TabRow(critIdx) {
|
||||||
|
Tab(critIdx == 0, { critIdx = 0 }) {
|
||||||
|
Text(
|
||||||
|
"General feedback",
|
||||||
|
Modifier.padding(5.dp),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
criteria.forEachIndexed { i, c ->
|
||||||
|
Tab(critIdx == i + 1, { critIdx = i + 1 }) {
|
||||||
|
Text(
|
||||||
|
c.name,
|
||||||
|
Modifier.padding(5.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val updateGrade = { grade: String ->
|
Spacer(Modifier.height(5.dp))
|
||||||
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 ->
|
groupFeedbackPane(
|
||||||
if(idx == 0) {
|
criteria, critIdx, { critIdx = it },
|
||||||
if(critIdx == 0) {
|
when {
|
||||||
state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null)
|
studentIdx == 0 && critIdx == 0 -> feedback.global
|
||||||
}
|
studentIdx == 0 && critIdx != 0 -> feedback.byCriterion[critIdx - 1].entry
|
||||||
else {
|
studentIdx != 0 && critIdx == 0 -> individual[studentIdx - 1].second.second.global
|
||||||
val current = feedback.byCriterion[critIdx - 1]
|
else -> individual[studentIdx - 1].second.second.byCriterion[critIdx - 1].entry
|
||||||
state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion)
|
},
|
||||||
}
|
suggestions, onSave,
|
||||||
}
|
if(critIdx == 0 && criteria.isNotEmpty()) criteria.mapIndexed { idx, it -> it.name to critGrade(idx + 1) } else null,
|
||||||
else {
|
key = studentIdx to critIdx
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,40 +290,54 @@ fun groupFeedbackPane(
|
|||||||
criteria: List<GroupAssignmentCriterion>,
|
criteria: List<GroupAssignmentCriterion>,
|
||||||
currentCriterion: Int,
|
currentCriterion: Int,
|
||||||
onSelectCriterion: (Int) -> Unit,
|
onSelectCriterion: (Int) -> Unit,
|
||||||
globFeedback: GroupAssignmentState.FeedbackEntry?,
|
rawFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||||
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
|
|
||||||
autofill: List<String>,
|
autofill: List<String>,
|
||||||
onSetGrade: (String) -> Unit,
|
onSave: (String, String) -> Unit,
|
||||||
onSetFeedback: (String) -> Unit,
|
critGrades: List<Pair<String, String?>>? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
key: Any? = null
|
key: Any? = null
|
||||||
) {
|
) {
|
||||||
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
|
var grade by remember(rawFeedback, key) { mutableStateOf(rawFeedback?.grade ?: "") }
|
||||||
var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
|
val feedback = rememberRichTextState()
|
||||||
|
|
||||||
|
LaunchedEffect(currentCriterion, criteria, rawFeedback, key) {
|
||||||
|
feedback.setMarkdown(rawFeedback?.feedback ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
Row {
|
Row {
|
||||||
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
|
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||||
Spacer(Modifier.weight(0.6f))
|
Spacer(Modifier.weight(0.6f))
|
||||||
Button(
|
Button(
|
||||||
{ onSetGrade(grade); onSetFeedback(feedback.text) },
|
{ onSave(grade, feedback.toMarkdown()) },
|
||||||
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
Modifier.weight(0.2f).align(Alignment.CenterVertically)
|
||||||
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
|
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TabRow(currentCriterion) {
|
|
||||||
Tab(currentCriterion == 0, { onSelectCriterion(0) }) { Text("General feedback", fontStyle = FontStyle.Italic) }
|
|
||||||
criteria.forEachIndexed { i, c ->
|
|
||||||
Tab(currentCriterion == i + 1, { onSelectCriterion(i + 1) }) { Text(c.name) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
AutocompleteLineField(
|
Row {
|
||||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
RichTextField(feedback, outerModifier = Modifier.weight(0.7f).fillMaxHeight()) { Text("Feedback") }
|
||||||
) { filter ->
|
critGrades?.let { grades ->
|
||||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
Spacer(Modifier.width(10.dp))
|
||||||
|
LazyColumn(Modifier.weight(0.3f)) {
|
||||||
|
item {
|
||||||
|
Text("Criteria grades", Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
|
||||||
|
}
|
||||||
|
items(grades) { (crit, grade) ->
|
||||||
|
Column {
|
||||||
|
Text(crit, Modifier.padding(5.dp), fontWeight = FontWeight.Bold)
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
if(grade == null) Text("(no grade yet)", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||||
|
else Text(grade, Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,16 +517,19 @@ fun soloFeedbackPane(
|
|||||||
key: Any? = null
|
key: Any? = null
|
||||||
) {
|
) {
|
||||||
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
|
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) {
|
Column(modifier) {
|
||||||
Row {
|
Row {
|
||||||
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
|
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
|
||||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||||
Spacer(Modifier.weight(0.6f))
|
Spacer(Modifier.weight(0.6f))
|
||||||
Button(
|
Button(
|
||||||
{ onSetGrade(grade); onSetFeedback(feedback.text) },
|
{ onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) },
|
||||||
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
Modifier.weight(0.2f).align(Alignment.CenterVertically)
|
||||||
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
|
|
||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
@ -501,11 +541,7 @@ fun soloFeedbackPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
AutocompleteLineField(
|
RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
|
||||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
|
||||||
) { filter ->
|
|
||||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,14 +64,14 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
|||||||
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
||||||
{ students -> state.addToCourse(students) },
|
{ students -> state.addToCourse(students) },
|
||||||
{ s, name -> state.setStudentName(s, name) }
|
{ s, name -> state.setStudentName(s, name) }
|
||||||
) { s -> state.delete(s) }
|
) { s, idx -> state.delete(s); if(id == idx) state.clearHistoryIndex() }
|
||||||
|
|
||||||
OpenPanel.Group -> GroupPanel(
|
OpenPanel.Group -> GroupPanel(
|
||||||
course, edition, groups, id,
|
course, edition, groups, id,
|
||||||
{ state.navTo(it) },
|
{ state.navTo(it) },
|
||||||
{ name -> state.newGroup(name) },
|
{ name -> state.newGroup(name) },
|
||||||
{ g, name -> state.setGroupName(g, name) }
|
{ g, name -> state.setGroupName(g, name) }
|
||||||
) { g -> state.delete(g) }
|
) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
|
||||||
|
|
||||||
OpenPanel.Assignment -> AssignmentPanel(
|
OpenPanel.Assignment -> AssignmentPanel(
|
||||||
course, edition, mergedAssignments, id,
|
course, edition, mergedAssignments, id,
|
||||||
@ -79,7 +79,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
|||||||
{ type, name -> state.newAssignment(type, name) },
|
{ type, name -> state.newAssignment(type, name) },
|
||||||
{ a, name -> state.setAssignmentTitle(a, name) },
|
{ a, name -> state.setAssignmentTitle(a, name) },
|
||||||
{ a1, a2 -> state.swapOrder(a1, a2) }
|
{ a1, a2 -> state.swapOrder(a1, a2) }
|
||||||
) { a -> state.delete(a) }
|
) { a, idx -> state.delete(a); if(id == idx) state.clearHistoryIndex() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ fun StudentPanel(
|
|||||||
course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
||||||
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student) -> Unit
|
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student, Int) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
@ -171,7 +171,7 @@ fun StudentPanel(
|
|||||||
ConfirmDeleteDialog(
|
ConfirmDeleteDialog(
|
||||||
"a student",
|
"a student",
|
||||||
{ deleting = -1 },
|
{ deleting = -1 },
|
||||||
{ onDelete(students[deleting]) }
|
{ onDelete(students[deleting], deleting) }
|
||||||
) { Text(students[deleting].name) }
|
) { Text(students[deleting].name) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ fun StudentPanel(
|
|||||||
fun GroupPanel(
|
fun GroupPanel(
|
||||||
course: Course, edition: Edition, groups: List<Group>,
|
course: Course, edition: Edition, groups: List<Group>,
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group) -> Unit
|
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group, Int) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
@ -218,7 +218,7 @@ fun GroupPanel(
|
|||||||
ConfirmDeleteDialog(
|
ConfirmDeleteDialog(
|
||||||
"a group",
|
"a group",
|
||||||
{ deleting = -1 },
|
{ deleting = -1 },
|
||||||
{ onDelete(groups[deleting]) }
|
{ onDelete(groups[deleting], deleting) }
|
||||||
) { Text(groups[deleting].name) }
|
) { Text(groups[deleting].name) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +228,7 @@ fun AssignmentPanel(
|
|||||||
course: Course, edition: Edition, assignments: List<Assignment>,
|
course: Course, edition: Edition, assignments: List<Assignment>,
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
selected: Int, onSelect: (Int) -> Unit,
|
||||||
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
||||||
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit
|
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment, Int) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
var deleting by remember { mutableStateOf(-1) }
|
||||||
@ -315,8 +315,8 @@ fun AssignmentPanel(
|
|||||||
ConfirmDeleteDialog(
|
ConfirmDeleteDialog(
|
||||||
"an assignment",
|
"an assignment",
|
||||||
{ deleting = -1 },
|
{ deleting = -1 },
|
||||||
{ onDelete(assignments[deleting]) }
|
{ onDelete(assignments[deleting], deleting) }
|
||||||
) { Text(assignments[deleting].name()) }
|
) { if(deleting != -1) Text(assignments[deleting].name()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.jaytux.grader.loadClipboard
|
import com.jaytux.grader.loadClipboard
|
||||||
import com.jaytux.grader.toClipboard
|
import com.jaytux.grader.toClipboard
|
||||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
|
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextStyleRow(
|
fun RichTextStyleRow(
|
||||||
@ -238,3 +239,17 @@ fun RichTextStyleButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RichTextField(
|
||||||
|
state: RichTextState,
|
||||||
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
buttonsModifier: Modifier = Modifier.fillMaxWidth(),
|
||||||
|
outerModifier: Modifier = Modifier,
|
||||||
|
label: @Composable (() -> Unit)? = null
|
||||||
|
) = Column(outerModifier) {
|
||||||
|
RichTextStyleRow(buttonsModifier, state)
|
||||||
|
OutlinedRichTextEditor(
|
||||||
|
state = state, modifier = modifier, singleLine = false, minLines = 5, label = label
|
||||||
|
)
|
||||||
|
}
|
@ -34,6 +34,7 @@ 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 com.jaytux.grader.viewmodel.PeerEvaluationState
|
||||||
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
@ -211,7 +212,8 @@ fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>)
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AutocompleteLineField(
|
fun AutocompleteLineField__(
|
||||||
|
// state: RichTextState,
|
||||||
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
||||||
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
||||||
onFilter: (String) -> List<String>
|
onFilter: (String) -> List<String>
|
||||||
|
@ -171,10 +171,14 @@ class EditionState(val edition: Edition) {
|
|||||||
|
|
||||||
fun newSoloAssignment(name: String) {
|
fun newSoloAssignment(name: String) {
|
||||||
transaction {
|
transaction {
|
||||||
SoloAssignment.new {
|
val assign = SoloAssignment.new {
|
||||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||||
this.number = nextIdx()
|
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()
|
solo.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,10 +190,14 @@ class EditionState(val edition: Edition) {
|
|||||||
}
|
}
|
||||||
fun newGroupAssignment(name: String) {
|
fun newGroupAssignment(name: String) {
|
||||||
transaction {
|
transaction {
|
||||||
GroupAssignment.new {
|
val assign = GroupAssignment.new {
|
||||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||||
this.number = nextIdx()
|
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()
|
groupAs.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,6 +362,10 @@ class EditionState(val edition: Edition) {
|
|||||||
while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1)
|
while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1)
|
||||||
_history.value = temp
|
_history.value = temp
|
||||||
}
|
}
|
||||||
|
fun clearHistoryIndex() {
|
||||||
|
val last = _history.value.lastOrNull() ?: return
|
||||||
|
_history.value = _history.value.filter { (i, panel) -> panel != last.second || i != last.first } + (-1 to last.second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StudentState(val student: Student, edition: Edition) {
|
class StudentState(val student: Student, edition: Edition) {
|
||||||
@ -373,12 +385,12 @@ class StudentState(val student: Student, edition: Edition) {
|
|||||||
|
|
||||||
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||||
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
|
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
|
||||||
(GroupAssignmentCriteria.name eq "")
|
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
||||||
}.map { it[GroupAssignments.id] to it }
|
}.map { it[GroupAssignments.id] to it }
|
||||||
|
|
||||||
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
||||||
(IndividualFeedbacks.studentId eq student.id) and
|
(IndividualFeedbacks.studentId eq student.id) and
|
||||||
(GroupAssignmentCriteria.name eq "")
|
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
||||||
}.map { it[GroupAssignments.id] to it }
|
}.map { it[GroupAssignments.id] to it }
|
||||||
|
|
||||||
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
|
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
|
||||||
@ -475,7 +487,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||||
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||||
val criteria = RawDbState {
|
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() }
|
val feedback = RawDbState { loadFeedback() }
|
||||||
|
|
||||||
@ -494,7 +506,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
||||||
val allCrit = GroupAssignmentCriterion.find {
|
val allCrit = GroupAssignmentCriterion.find {
|
||||||
GroupAssignmentCriteria.assignmentId eq assignment.id
|
GroupAssignmentCriteria.assignmentId eq assignment.id
|
||||||
}
|
}.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
||||||
|
|
||||||
return Group.find {
|
return Group.find {
|
||||||
(Groups.editionId eq assignment.edition.id)
|
(Groups.editionId eq assignment.edition.id)
|
||||||
@ -502,16 +514,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
|
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||||
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
|
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
|
||||||
}.map { row ->
|
}.map { row ->
|
||||||
val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] }
|
val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]]
|
||||||
val fdbk = row[GroupFeedbacks.feedback]
|
val fdbk = row[GroupFeedbacks.feedback]
|
||||||
val grade = row[GroupFeedbacks.grade]
|
val grade = row[GroupFeedbacks.grade]
|
||||||
|
|
||||||
crit to FeedbackEntry(fdbk, grade)
|
crit to FeedbackEntry(fdbk, grade)
|
||||||
}
|
}
|
||||||
|
|
||||||
val global = forGroup.firstOrNull { it.first == null }?.second
|
val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
||||||
val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
|
val byCrit_ = forGroup
|
||||||
.filterNotNull().associateBy { it.criterion.id }
|
.filter{ it.first.id != assignment.globalCriterion.id }
|
||||||
|
.map { LocalCriterionFeedback(it.first, it.second) }
|
||||||
|
.associateBy { it.criterion.id }
|
||||||
|
|
||||||
val byCrit = allCrit.map { c ->
|
val byCrit = allCrit.map { c ->
|
||||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||||
}
|
}
|
||||||
@ -522,21 +537,25 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
val student = it.student
|
val student = it.student
|
||||||
val role = it.role
|
val role = it.role
|
||||||
|
|
||||||
val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents)
|
val forSt = (IndividualFeedbacks innerJoin Groups)
|
||||||
.selectAll().where {
|
.selectAll().where {
|
||||||
(IndividualFeedbacks.assignmentId eq assignment.id) and
|
(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 ->
|
}.map { row ->
|
||||||
val crit = row[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] }
|
val stdId = row[IndividualFeedbacks.studentId]
|
||||||
|
val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
|
||||||
val fdbk = row[IndividualFeedbacks.feedback]
|
val fdbk = row[IndividualFeedbacks.feedback]
|
||||||
val grade = row[IndividualFeedbacks.grade]
|
val grade = row[IndividualFeedbacks.grade]
|
||||||
|
|
||||||
crit to FeedbackEntry(fdbk, grade)
|
crit to FeedbackEntry(fdbk, grade)
|
||||||
}
|
}
|
||||||
|
|
||||||
val global = forSt.firstOrNull { it.first == null }?.second
|
val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
||||||
val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
|
val byCrit_ = forSt
|
||||||
.filterNotNull().associateBy { it.criterion.id }
|
.filter { it.first != assignment.globalCriterion.id }
|
||||||
|
.map { LocalCriterionFeedback(it.first, it.second) }
|
||||||
|
.associateBy { it.criterion.id }
|
||||||
|
|
||||||
val byCrit = allCrit.map { c ->
|
val byCrit = allCrit.map { c ->
|
||||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||||
}
|
}
|
||||||
@ -556,7 +575,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
it[groupId] = group.id
|
it[groupId] = group.id
|
||||||
it[this.feedback] = msg
|
it[this.feedback] = msg
|
||||||
it[this.grade] = grd
|
it[this.grade] = grd
|
||||||
it[criterionId] = criterion?.id
|
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedback.refresh(); autofill.refresh()
|
feedback.refresh(); autofill.refresh()
|
||||||
@ -570,7 +589,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||||||
it[studentId] = student.id
|
it[studentId] = student.id
|
||||||
it[this.feedback] = msg
|
it[this.feedback] = msg
|
||||||
it[this.grade] = grd
|
it[this.grade] = grd
|
||||||
it[criterionId] = criterion?.id
|
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedback.refresh(); autofill.refresh()
|
feedback.refresh(); autofill.refresh()
|
||||||
@ -628,7 +647,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
|
|||||||
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||||
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||||
val criteria = RawDbState {
|
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() }
|
val feedback = RawDbState { loadFeedback() }
|
||||||
|
|
||||||
@ -638,25 +657,28 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
|
|||||||
}.flatten().distinct().sorted()
|
}.flatten().distinct().sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
|
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {3
|
||||||
val allCrit = SoloAssignmentCriterion.find {
|
val allCrit = SoloAssignmentCriterion.find {
|
||||||
SoloAssignmentCriteria.assignmentId eq assignment.id
|
SoloAssignmentCriteria.assignmentId eq assignment.id
|
||||||
}
|
}.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
||||||
|
|
||||||
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
|
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
|
||||||
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
|
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
|
||||||
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
|
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
|
||||||
}.map { row ->
|
}.map { row ->
|
||||||
val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] }
|
val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
|
||||||
val fdbk = row[IndividualFeedbacks.feedback]
|
val fdbk = row[IndividualFeedbacks.feedback]
|
||||||
val grade = row[IndividualFeedbacks.grade]
|
val grade = row[IndividualFeedbacks.grade]
|
||||||
|
|
||||||
crit to LocalFeedback(fdbk, grade)
|
crit to LocalFeedback(fdbk, grade)
|
||||||
}
|
}
|
||||||
|
|
||||||
val global = forStudent.firstOrNull { it.first == null }?.second
|
val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second
|
||||||
val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } }
|
val byCrit_ = forStudent
|
||||||
.filterNotNull().associateBy { it.first.id }
|
.filter { it.first != assignment.globalCriterion.id }
|
||||||
|
.map { Pair(it.first, it.second) }
|
||||||
|
.associateBy { it.first.id }
|
||||||
|
|
||||||
val byCrit = allCrit.map { c ->
|
val byCrit = allCrit.map { c ->
|
||||||
byCrit_[c.id] ?: Pair(c, null)
|
byCrit_[c.id] ?: Pair(c, null)
|
||||||
}
|
}
|
||||||
@ -672,7 +694,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
|
|||||||
it[studentId] = student.id
|
it[studentId] = student.id
|
||||||
it[this.feedback] = msg ?: ""
|
it[this.feedback] = msg ?: ""
|
||||||
it[this.grade] = grd ?: ""
|
it[this.grade] = grd ?: ""
|
||||||
it[criterionId] = criterion?.id
|
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedback.refresh(); autofill.refresh()
|
feedback.refresh(); autofill.refresh()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
androidx-lifecycle = "2.8.4"
|
androidx-lifecycle = "2.8.4"
|
||||||
compose-multiplatform = "1.7.0"
|
compose-multiplatform = "1.8.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.1.0"
|
kotlin = "2.1.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.1"
|
||||||
@ -9,6 +9,7 @@ material3 = "1.7.3"
|
|||||||
ui-android = "1.7.8"
|
ui-android = "1.7.8"
|
||||||
foundation-layout-android = "1.7.8"
|
foundation-layout-android = "1.7.8"
|
||||||
rtf = "1.0.0-rc11"
|
rtf = "1.0.0-rc11"
|
||||||
|
filekit = "0.10.0-beta04"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
@ -20,6 +21,7 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
|
|||||||
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
|
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-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" }
|
||||||
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", 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" }
|
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" }
|
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
|
||||||
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
||||||
@ -29,6 +31,10 @@ material-icons = { group = "org.jetbrains.compose.material", name = "material-ic
|
|||||||
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
||||||
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
||||||
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
|
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
|
||||||
|
filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
|
||||||
|
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
||||||
|
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
||||||
|
filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
|
Reference in New Issue
Block a user