Compare commits
10 Commits
97fe7a8139
...
c88d0d2e58
Author | SHA1 | Date |
---|---|---|
|
c88d0d2e58 | |
|
a7aafccd19 | |
|
034b018e2d | |
|
4da4b0bb85 | |
|
f407a8c43e | |
|
49e3b8126f | |
|
63c4197cfc | |
|
b69b46afee | |
|
d0ddd54710 | |
|
0bfef36559 |
|
@ -18,3 +18,4 @@ captures
|
|||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
**/grader.db
|
||||
**/*.backup
|
|
@ -48,6 +48,7 @@ compose.desktop {
|
|||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "com.jaytux.grader"
|
||||
packageVersion = "1.0.0"
|
||||
includeAllModules = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package com.jaytux.grader
|
||||
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun String.maxN(n: Int): String {
|
||||
return if (this.length > n) {
|
||||
this.substring(0, n - 3) + "..."
|
||||
|
@ -7,3 +13,11 @@ fun String.maxN(n: Int): String {
|
|||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun RichTextState.toClipboard(clip: ClipboardManager) {
|
||||
clip.setText(AnnotatedString(this.toMarkdown()))
|
||||
}
|
||||
|
||||
fun RichTextState.loadClipboard(clip: ClipboardManager, scope: CoroutineScope) {
|
||||
scope.launch { setMarkdown(clip.getText()?.text ?: "") }
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.jaytux.grader.data
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import org.jetbrains.exposed.dao.id.CompositeIdTable
|
||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
@ -53,42 +52,92 @@ object EditionStudents : Table("editionStudents") {
|
|||
|
||||
object GroupAssignments : UUIDTable("grpAssgmts") {
|
||||
val editionId = reference("edition_id", Editions.id)
|
||||
val number = integer("number").nullable()
|
||||
val name = varchar("name", 50)
|
||||
val assignment = text("assignment")
|
||||
val deadline = datetime("deadline")
|
||||
}
|
||||
|
||||
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
|
||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
||||
val name = varchar("name", 50)
|
||||
val desc = text("description")
|
||||
}
|
||||
|
||||
object SoloAssignments : UUIDTable("soloAssgmts") {
|
||||
val editionId = reference("edition_id", Editions.id)
|
||||
val number = integer("number").nullable()
|
||||
val name = varchar("name", 50)
|
||||
val assignment = text("assignment")
|
||||
val deadline = datetime("deadline")
|
||||
}
|
||||
|
||||
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
|
||||
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
|
||||
val name = varchar("name", 50)
|
||||
val desc = text("description")
|
||||
}
|
||||
|
||||
object PeerEvaluations : UUIDTable("peerEvals") {
|
||||
val editionId = reference("edition_id", Editions.id)
|
||||
val number = integer("number").nullable()
|
||||
val name = varchar("name", 50)
|
||||
}
|
||||
|
||||
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
|
||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
||||
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable()
|
||||
val groupId = reference("group_id", Groups.id)
|
||||
val feedback = text("feedback")
|
||||
val grade = varchar("grade", 32)
|
||||
|
||||
override val primaryKey = PrimaryKey(groupAssignmentId, groupId)
|
||||
override val primaryKey = PrimaryKey(groupId, criterionId)
|
||||
}
|
||||
|
||||
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
||||
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
|
||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
||||
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable()
|
||||
val groupId = reference("group_id", Groups.id)
|
||||
val studentId = reference("student_id", Students.id)
|
||||
val feedback = text("feedback")
|
||||
val grade = varchar("grade", 32)
|
||||
|
||||
override val primaryKey = PrimaryKey(groupAssignmentId, studentId)
|
||||
override val primaryKey = PrimaryKey(studentId, criterionId)
|
||||
}
|
||||
|
||||
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
||||
val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id)
|
||||
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
|
||||
val criterionId = reference("criterion_id", SoloAssignmentCriteria.id).nullable()
|
||||
val studentId = reference("student_id", Students.id)
|
||||
val feedback = text("feedback")
|
||||
val grade = varchar("grade", 32)
|
||||
|
||||
override val primaryKey = PrimaryKey(soloAssignmentId, studentId)
|
||||
override val primaryKey = PrimaryKey(studentId, criterionId)
|
||||
}
|
||||
|
||||
object PeerEvaluationContents : CompositeIdTable("peerEvalCnts") {
|
||||
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
|
||||
val groupId = reference("group_id", Groups.id)
|
||||
val content = text("content")
|
||||
|
||||
override val primaryKey = PrimaryKey(peerEvaluationId, groupId)
|
||||
}
|
||||
|
||||
object StudentToGroupEvaluation : CompositeIdTable("stToGrEv") {
|
||||
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
|
||||
val studentId = reference("student_id", Students.id)
|
||||
val grade = varchar("grade", 32)
|
||||
val note = text("note")
|
||||
|
||||
override val primaryKey = PrimaryKey(peerEvaluationId, studentId)
|
||||
}
|
||||
|
||||
object StudentToStudentEvaluation : CompositeIdTable("stToStEv") {
|
||||
val peerEvaluationId = reference("peer_evaluation_id", PeerEvaluations.id)
|
||||
val studentIdFrom = reference("student_id_from", Students.id)
|
||||
val studentIdTo = reference("student_id_to", Students.id)
|
||||
val grade = varchar("grade", 32)
|
||||
val note = text("note")
|
||||
|
||||
override val primaryKey = PrimaryKey(peerEvaluationId, studentIdFrom, studentIdTo)
|
||||
}
|
|
@ -11,15 +11,19 @@ object Database {
|
|||
SchemaUtils.create(
|
||||
Courses, Editions, Groups,
|
||||
Students, GroupStudents, EditionStudents,
|
||||
GroupAssignments, SoloAssignments,
|
||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
|
||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
||||
StudentToGroupEvaluation
|
||||
)
|
||||
|
||||
val addMissing = SchemaUtils.addMissingColumnsStatements(
|
||||
Courses, Editions, Groups,
|
||||
Students, GroupStudents, EditionStudents,
|
||||
GroupAssignments, SoloAssignments,
|
||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
|
||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
||||
StudentToGroupEvaluation
|
||||
)
|
||||
addMissing.forEach { exec(it) }
|
||||
}
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
package com.jaytux.grader.data
|
||||
|
||||
import com.jaytux.grader.data.GroupAssignment.Companion.referrersOn
|
||||
import org.jetbrains.exposed.dao.Entity
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.id.CompositeID
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.util.UUID
|
||||
|
||||
class Course(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, Course>(Courses)
|
||||
|
||||
fun loadEditions() = transaction { Edition.find { Editions.courseId eq this@Course.id }.toList() }
|
||||
|
||||
var name by Courses.name
|
||||
val editions by Edition referrersOn Editions.courseId
|
||||
}
|
||||
|
||||
class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
|
@ -24,6 +22,7 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||
val soloStudents by Student via EditionStudents
|
||||
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
|
||||
val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
||||
val peerEvaluations by PeerEvaluation referrersOn PeerEvaluations.editionId
|
||||
}
|
||||
|
||||
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
|
@ -57,43 +56,82 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|||
companion object : EntityClass<UUID, GroupAssignment>(GroupAssignments)
|
||||
|
||||
var edition by Edition referencedOn GroupAssignments.editionId
|
||||
var number by GroupAssignments.number
|
||||
var name by GroupAssignments.name
|
||||
var assignment by GroupAssignments.assignment
|
||||
var deadline by GroupAssignments.deadline
|
||||
|
||||
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
|
||||
}
|
||||
|
||||
class GroupAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, GroupAssignmentCriterion>(GroupAssignmentCriteria)
|
||||
|
||||
var assignment by GroupAssignment referencedOn GroupAssignmentCriteria.assignmentId
|
||||
var name by GroupAssignmentCriteria.name
|
||||
var description by GroupAssignmentCriteria.desc
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as GroupAssignmentCriterion
|
||||
|
||||
if (name != other.name) return false
|
||||
if (description != other.description) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + description.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments)
|
||||
|
||||
var edition by Edition referencedOn SoloAssignments.editionId
|
||||
var number by SoloAssignments.number
|
||||
var name by SoloAssignments.name
|
||||
var assignment by SoloAssignments.assignment
|
||||
var deadline by SoloAssignments.deadline
|
||||
|
||||
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
|
||||
}
|
||||
|
||||
class GroupFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
|
||||
companion object : EntityClass<CompositeID, GroupFeedback>(GroupFeedbacks)
|
||||
class SoloAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, SoloAssignmentCriterion>(SoloAssignmentCriteria)
|
||||
|
||||
var group by Group referencedOn GroupFeedbacks.groupId
|
||||
var assignment by GroupAssignment referencedOn GroupFeedbacks.groupAssignmentId
|
||||
var feedback by GroupFeedbacks.feedback
|
||||
var grade by GroupFeedbacks.grade
|
||||
var assignment by SoloAssignment referencedOn SoloAssignmentCriteria.assignmentId
|
||||
var name by SoloAssignmentCriteria.name
|
||||
var description by SoloAssignmentCriteria.desc
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SoloAssignmentCriterion
|
||||
|
||||
if (name != other.name) return false
|
||||
if (description != other.description) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
class IndividualFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
|
||||
companion object : EntityClass<CompositeID, IndividualFeedback>(IndividualFeedbacks)
|
||||
|
||||
var student by Student referencedOn IndividualFeedbacks.studentId
|
||||
var assignment by SoloAssignment referencedOn IndividualFeedbacks.groupAssignmentId
|
||||
var feedback by IndividualFeedbacks.feedback
|
||||
var grade by IndividualFeedbacks.grade
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + description.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class SoloFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
|
||||
companion object : EntityClass<CompositeID, SoloFeedback>(SoloFeedbacks)
|
||||
class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||
companion object : EntityClass<UUID, PeerEvaluation>(PeerEvaluations)
|
||||
|
||||
var student by Student referencedOn SoloFeedbacks.studentId
|
||||
var assignment by SoloAssignment referencedOn SoloFeedbacks.soloAssignmentId
|
||||
var feedback by SoloFeedbacks.feedback
|
||||
var grade by SoloFeedbacks.grade
|
||||
var edition by Edition referencedOn PeerEvaluations.editionId
|
||||
var number by PeerEvaluations.number
|
||||
var name by PeerEvaluations.name
|
||||
}
|
|
@ -2,33 +2,41 @@ package com.jaytux.grader.ui
|
|||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jaytux.grader.data.GroupAssignment
|
||||
import com.jaytux.grader.data.GroupAssignmentCriterion
|
||||
import com.jaytux.grader.data.SoloAssignmentCriterion
|
||||
import com.jaytux.grader.data.Student
|
||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
||||
import com.jaytux.grader.viewmodel.SoloAssignmentState
|
||||
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||
val (course, edition) = state.editionCourse
|
||||
val name by state.name
|
||||
val task by state.task
|
||||
val deadline by state.deadline
|
||||
val allFeedback by state.feedback.entities
|
||||
val criteria by state.criteria.entities
|
||||
|
||||
var idx by remember(state) { mutableStateOf(0) }
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(name, "group assignment", course, edition)
|
||||
if(allFeedback.any { it.second.feedback == null }) {
|
||||
Text("Groups in bold have no feedback yet.", fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
@ -37,7 +45,7 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
|||
}
|
||||
|
||||
TabRow(idx) {
|
||||
Tab(idx == 0, { idx = 0 }) { Text("Assignment") }
|
||||
Tab(idx == 0, { idx = 0 }) { Text("Task and Criteria") }
|
||||
allFeedback.forEachIndexed { i, it ->
|
||||
val (group, feedback) = it
|
||||
Tab(idx == i + 1, { idx = i + 1 }) {
|
||||
|
@ -47,12 +55,75 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
|||
}
|
||||
|
||||
if(idx == 0) {
|
||||
val updTask = rememberRichTextState()
|
||||
groupTaskWidget(
|
||||
task, deadline, criteria,
|
||||
onSetTask = { state.updateTask(it) },
|
||||
onSetDeadline = { state.updateDeadline(it) },
|
||||
onAddCriterion = { state.addCriterion(it) },
|
||||
onModCriterion = { c, n, d -> state.updateCriterion(c, n, d) },
|
||||
onRmCriterion = { state.deleteCriterion(it) }
|
||||
)
|
||||
}
|
||||
else {
|
||||
groupFeedback(state, allFeedback[idx - 1].second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(task) { updTask.setMarkdown(task) }
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun groupTaskWidget(
|
||||
taskMD: String,
|
||||
deadline: LocalDateTime,
|
||||
criteria: List<GroupAssignmentCriterion>,
|
||||
onSetTask: (String) -> Unit,
|
||||
onSetDeadline: (LocalDateTime) -> Unit,
|
||||
onAddCriterion: (name: String) -> Unit,
|
||||
onModCriterion: (cr: GroupAssignmentCriterion, name: String, desc: String) -> Unit,
|
||||
onRmCriterion: (cr: GroupAssignmentCriterion) -> Unit
|
||||
) {
|
||||
var critIdx by remember { mutableStateOf(0) }
|
||||
var adding by remember { mutableStateOf(false) }
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
|
||||
Row {
|
||||
DateTimePicker(deadline, { state.updateDeadline(it) })
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { critIdx = 0 },
|
||||
tonalElevation = if (critIdx == 0) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(criteria) { i, crit ->
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { critIdx = i + 1 },
|
||||
tonalElevation = if (critIdx == i + 1) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text(crit.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
|
||||
Text("Add evaluation criterion")
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.75f).padding(10.dp)) {
|
||||
if (critIdx == 0) {
|
||||
val updTask = rememberRichTextState()
|
||||
|
||||
LaunchedEffect(taskMD) { updTask.setMarkdown(taskMD) }
|
||||
|
||||
Column {
|
||||
Row {
|
||||
DateTimePicker(deadline, onSetDeadline)
|
||||
}
|
||||
RichTextStyleRow(state = updTask)
|
||||
OutlinedRichTextEditor(
|
||||
|
@ -62,10 +133,53 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
|||
minLines = 5,
|
||||
label = { Text("Task") }
|
||||
)
|
||||
CancelSaveRow(true, { updTask.setMarkdown(task) }, "Reset", "Update") { state.updateTask(updTask.toMarkdown()) }
|
||||
CancelSaveRow(
|
||||
true,
|
||||
{ updTask.setMarkdown(taskMD) },
|
||||
"Reset",
|
||||
"Update"
|
||||
) { onSetTask(updTask.toMarkdown()) }
|
||||
}
|
||||
}
|
||||
else {
|
||||
groupFeedback(state, allFeedback[idx - 1].second)
|
||||
val crit = criteria[critIdx - 1]
|
||||
var name by remember(crit) { mutableStateOf(crit.name) }
|
||||
var desc by remember(crit) { mutableStateOf(crit.description) }
|
||||
|
||||
Column {
|
||||
Row {
|
||||
OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Button({ onModCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
|
||||
Text("Update")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
|
||||
label = { Text("Description") },
|
||||
singleLine = false,
|
||||
minLines = 5
|
||||
)
|
||||
Button({ confirming = true }, Modifier.fillMaxWidth()) {
|
||||
Text("Remove criterion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(adding) {
|
||||
AddStringDialog(
|
||||
"Evaluation criterion name", criteria.map{ it.name }, { adding = false }
|
||||
) { onAddCriterion(it) }
|
||||
}
|
||||
|
||||
if(confirming && critIdx != 0) {
|
||||
ConfirmDeleteDialog(
|
||||
"an evaluation criterion",
|
||||
{ confirming = false }, { onRmCriterion(criteria[critIdx - 1]); critIdx = 0 }
|
||||
) {
|
||||
Text(criteria[critIdx - 1].name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,9 +187,9 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
|
|||
@Composable
|
||||
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
||||
val (group, feedback, individual) = fdbk
|
||||
var grade by remember(fdbk) { mutableStateOf(feedback?.grade ?: "") }
|
||||
var msg by remember(fdbk) { mutableStateOf(TextFieldValue(feedback?.feedback ?: "")) }
|
||||
var idx by remember(fdbk) { mutableStateOf(0) }
|
||||
var critIdx by remember(fdbk) { mutableStateOf(0) }
|
||||
val criteria by state.criteria.entities
|
||||
val suggestions by state.autofill.entities
|
||||
|
||||
Row {
|
||||
|
@ -104,42 +218,453 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
|
|||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||
val updateGrade = { grade: String ->
|
||||
if(idx == 0) {
|
||||
Row {
|
||||
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||
Spacer(Modifier.weight(0.6f))
|
||||
Button({ state.upsertGroupFeedback(group, msg.text, grade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = grade.isNotBlank() || msg.text.isNotBlank()) {
|
||||
Text("Save")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
AutocompleteLineField(
|
||||
msg, { msg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
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 (student, details) = individual[idx - 1]
|
||||
var sGrade by remember { mutableStateOf(details.second?.grade ?: "") }
|
||||
var sMsg by remember { mutableStateOf(TextFieldValue(details.second?.feedback ?: "")) }
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun groupFeedbackPane(
|
||||
criteria: List<GroupAssignmentCriterion>,
|
||||
currentCriterion: Int,
|
||||
onSelectCriterion: (Int) -> Unit,
|
||||
globFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
|
||||
autofill: List<String>,
|
||||
onSetGrade: (String) -> Unit,
|
||||
onSetFeedback: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
key: Any? = null
|
||||
) {
|
||||
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
|
||||
var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(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()
|
||||
) {
|
||||
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))
|
||||
AutocompleteLineField(
|
||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SoloAssignmentView(state: SoloAssignmentState) {
|
||||
val task by state.task
|
||||
val deadline by state.deadline
|
||||
val suggestions by state.autofill.entities
|
||||
val grades by state.feedback.entities
|
||||
val criteria by state.criteria.entities
|
||||
|
||||
var tab by remember(state) { mutableStateOf(0) }
|
||||
var idx by remember(state, tab) { mutableStateOf(0) }
|
||||
var critIdx by remember(state, tab, idx) { mutableStateOf(0) }
|
||||
var adding by remember(state, tab) { mutableStateOf(false) }
|
||||
var confirming by remember(state, tab) { mutableStateOf(false) }
|
||||
|
||||
val updateGrade = { grade: String ->
|
||||
state.upsertFeedback(
|
||||
grades[idx].first,
|
||||
if(critIdx == 0) grades[idx].second.global?.feedback else grades[idx].second.byCriterion[critIdx - 1].second?.feedback,
|
||||
grade,
|
||||
if(critIdx == 0) null else criteria[critIdx - 1]
|
||||
)
|
||||
}
|
||||
|
||||
val updateFeedback = { feedback: String ->
|
||||
state.upsertFeedback(
|
||||
grades[idx].first,
|
||||
feedback,
|
||||
if(critIdx == 0) grades[idx].second.global?.grade else grades[idx].second.byCriterion[critIdx - 1].second?.grade,
|
||||
if(critIdx == 0) null else criteria[critIdx - 1]
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Row {
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
TabRow(tab) {
|
||||
Tab(tab == 0, { tab = 0 }) { Text("Task/Criteria") }
|
||||
Tab(tab == 1, { tab = 1 }) { Text("Students") }
|
||||
}
|
||||
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
if (tab == 0) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = 0 },
|
||||
tonalElevation = if (idx == 0) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("Assignment", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(criteria) { i, crit ->
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
||||
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text(crit.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(grades.toList()) { i, (student, _) ->
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { idx = i },
|
||||
tonalElevation = if (idx == i) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text(student.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tab == 0) {
|
||||
Button({ adding = true }, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) {
|
||||
Text("Add evaluation criterion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||
if(tab == 0) {
|
||||
if (idx == 0) {
|
||||
val updTask = rememberRichTextState()
|
||||
|
||||
LaunchedEffect(task) { updTask.setMarkdown(task) }
|
||||
|
||||
Row {
|
||||
DateTimePicker(deadline, { state.updateDeadline(it) })
|
||||
}
|
||||
RichTextStyleRow(state = updTask)
|
||||
OutlinedRichTextEditor(
|
||||
state = updTask,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
singleLine = false,
|
||||
minLines = 5,
|
||||
label = { Text("Task") }
|
||||
)
|
||||
CancelSaveRow(
|
||||
true,
|
||||
{ updTask.setMarkdown(task) },
|
||||
"Reset",
|
||||
"Update"
|
||||
) { state.updateTask(updTask.toMarkdown()) }
|
||||
} else {
|
||||
val crit = criteria[idx - 1]
|
||||
var name by remember(crit) { mutableStateOf(crit.name) }
|
||||
var desc by remember(crit) { mutableStateOf(crit.description) }
|
||||
|
||||
Column {
|
||||
Row {
|
||||
OutlinedTextField(name, { name = it }, Modifier.weight(0.8f))
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Button({ state.updateCriterion(crit, name, desc) }, Modifier.weight(0.1f)) {
|
||||
Text("Update")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
desc, { desc = it }, Modifier.fillMaxWidth().weight(1f),
|
||||
label = { Text("Description") },
|
||||
singleLine = false,
|
||||
minLines = 5
|
||||
)
|
||||
Button({ confirming = true }, Modifier.fillMaxWidth()) {
|
||||
Text("Remove criterion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
soloFeedbackPane(
|
||||
criteria, critIdx, { critIdx = it }, grades[idx].second.global,
|
||||
if(critIdx == 0) grades[idx].second.global else grades[idx].second.byCriterion[critIdx - 1].second,
|
||||
suggestions, updateGrade, updateFeedback,
|
||||
key = tab to idx
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(adding) {
|
||||
AddStringDialog(
|
||||
"Evaluation criterion name", criteria.map{ it.name }, { adding = false }
|
||||
) { state.addCriterion(it) }
|
||||
}
|
||||
|
||||
if(confirming && idx != 0) {
|
||||
ConfirmDeleteDialog(
|
||||
"an evaluation criterion",
|
||||
{ confirming = false }, { state.deleteCriterion(criteria[idx - 1]); idx = 0 }
|
||||
) {
|
||||
Text(criteria[idx - 1].name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun soloFeedbackPane(
|
||||
criteria: List<SoloAssignmentCriterion>,
|
||||
currentCriterion: Int,
|
||||
onSelectCriterion: (Int) -> Unit,
|
||||
globFeedback: SoloAssignmentState.LocalFeedback?,
|
||||
criterionFeedback: SoloAssignmentState.LocalFeedback?,
|
||||
autofill: List<String>,
|
||||
onSetGrade: (String) -> Unit,
|
||||
onSetFeedback: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
key: Any? = null
|
||||
) {
|
||||
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
|
||||
var feedback by remember(currentCriterion, criteria, key) { mutableStateOf(TextFieldValue(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()
|
||||
) {
|
||||
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))
|
||||
AutocompleteLineField(
|
||||
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PeerEvaluationView(state: PeerEvaluationState) {
|
||||
val contents by state.contents.entities
|
||||
var idx by remember(state) { mutableStateOf(0) }
|
||||
var editing by remember(state) { mutableStateOf<Triple<Student, Student?, PeerEvaluationState.Student2StudentEntry?>?>(null) }
|
||||
val measure = rememberTextMeasurer()
|
||||
|
||||
val isSelected = { from: Student, to: Student? ->
|
||||
editing?.let { (f, t, _) -> f == from && t == to } ?: false
|
||||
}
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
TabRow(idx) {
|
||||
contents.forEachIndexed { i, it ->
|
||||
Tab(idx == i, { idx = i; editing = null }) { Text(it.group.name) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row {
|
||||
val current = contents[idx]
|
||||
val horScroll = rememberLazyListState()
|
||||
val style = LocalTextStyle.current
|
||||
val textLenMeasured = remember(state, idx) {
|
||||
current.students.maxOf { (s, _) ->
|
||||
measure.measure(s.name, style).size.width
|
||||
} + 10
|
||||
}
|
||||
val cellSize = 75.dp
|
||||
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Row {
|
||||
Box { FromTo(textLenMeasured.dp) }
|
||||
LazyRow(Modifier.height(textLenMeasured.dp), state = horScroll) {
|
||||
item { VLine() }
|
||||
items(current.students) { (s, _) ->
|
||||
Box(
|
||||
Modifier.width(cellSize).height(textLenMeasured.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
var _h: Int = 0
|
||||
Text(s.name, Modifier.layout{ m, c ->
|
||||
val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
|
||||
_h = p.height
|
||||
layout(p.height, p.width) { p.place(0, 0) }
|
||||
}.graphicsLayer {
|
||||
rotationZ = -90f
|
||||
transformOrigin = TransformOrigin(0f, 0.5f)
|
||||
translationX = _h.toFloat() / 2f
|
||||
translationY = textLenMeasured.dp.value - 15f
|
||||
})
|
||||
}
|
||||
}
|
||||
item { VLine() }
|
||||
item {
|
||||
Box(
|
||||
Modifier.width(cellSize).height(textLenMeasured.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
var _h: Int = 0
|
||||
Text("Group Rating", Modifier.layout{ m, c ->
|
||||
val p = m.measure(c.copy(minWidth = c.maxWidth, maxWidth = Constraints.Infinity))
|
||||
_h = p.height
|
||||
layout(p.height, p.width) { p.place(0, 0) }
|
||||
}.graphicsLayer {
|
||||
rotationZ = -90f
|
||||
transformOrigin = TransformOrigin(0f, 0.5f)
|
||||
translationX = _h.toFloat() / 2f
|
||||
translationY = textLenMeasured.dp.value - 15f
|
||||
}, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
item { VLine() }
|
||||
}
|
||||
}
|
||||
MeasuredLazyColumn(key = idx) {
|
||||
measuredItem { HLine() }
|
||||
items(current.students) { (from, glob, map) ->
|
||||
Row(Modifier.height(cellSize)) {
|
||||
Text(from.name, Modifier.width(textLenMeasured.dp).align(Alignment.CenterVertically))
|
||||
LazyRow(state = horScroll) {
|
||||
item { VLine() }
|
||||
items(map) { (to, entry) ->
|
||||
PEGradeWidget(entry,
|
||||
{ editing = Triple(from, to, entry) }, { editing = null },
|
||||
isSelected(from, to), Modifier.size(cellSize, cellSize)
|
||||
)
|
||||
}
|
||||
item { VLine() }
|
||||
item {
|
||||
PEGradeWidget(glob,
|
||||
{ editing = Triple(from, null, glob) }, { editing = null },
|
||||
isSelected(from, null), Modifier.size(cellSize, cellSize))
|
||||
}
|
||||
item { VLine() }
|
||||
}
|
||||
}
|
||||
}
|
||||
measuredItem { HLine() }
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
var groupLevel by remember(state, idx) { mutableStateOf(contents[idx].content) }
|
||||
editing?.let {
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
val (from, to, data) = it
|
||||
|
||||
var sGrade by remember(editing) { mutableStateOf(data?.grade ?: "") }
|
||||
var sMsg by remember(editing) { mutableStateOf(data?.feedback ?: "") }
|
||||
|
||||
Box(Modifier.padding(5.dp)) {
|
||||
to?.let { s2 ->
|
||||
if(from == s2)
|
||||
Text("Self-evaluation by ${from.name}", fontWeight = FontWeight.Bold)
|
||||
else
|
||||
Text("Evaluation of ${s2.name} by ${from.name}", fontWeight = FontWeight.Bold)
|
||||
} ?: Text("Group-level evaluation by ${from.name}", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
Row {
|
||||
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
|
||||
Spacer(Modifier.weight(0.6f))
|
||||
Button({ state.upsertIndividualFeedback(student, group, sMsg.text, sGrade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()) {
|
||||
Button(
|
||||
{ state.upsertIndividualFeedback(from, to, sGrade, sMsg); editing = null },
|
||||
Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||
enabled = sGrade.isNotBlank() || sMsg.isNotBlank()
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
|
||||
AutocompleteLineField(
|
||||
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||
) { filter ->
|
||||
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||
OutlinedTextField(
|
||||
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f),
|
||||
label = { Text("Feedback") },
|
||||
singleLine = false,
|
||||
minLines = 5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Row {
|
||||
Text("Group-level notes", Modifier.weight(1f).align(Alignment.CenterVertically), fontWeight = FontWeight.Bold)
|
||||
Button(
|
||||
{ state.upsertGroupFeedback(current.group, groupLevel); editing = null },
|
||||
enabled = groupLevel != contents[idx].content
|
||||
) { Text("Update") }
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
groupLevel, { groupLevel = it }, Modifier.fillMaxWidth().weight(1f),
|
||||
label = { Text("Group-level notes") },
|
||||
singleLine = false,
|
||||
minLines = 5
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
|||
val data by state.courses.entities
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Box(Modifier.padding(15.dp)) {
|
||||
ListOrEmpty(
|
||||
data,
|
||||
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
||||
|
@ -36,6 +37,7 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
|||
) { _, it ->
|
||||
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
|
@ -16,153 +19,305 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.window.DialogWindow
|
||||
import androidx.compose.ui.window.WindowPosition
|
||||
import androidx.compose.ui.window.rememberDialogState
|
||||
import com.jaytux.grader.data.*
|
||||
import com.jaytux.grader.viewmodel.EditionState
|
||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||
import com.jaytux.grader.viewmodel.GroupState
|
||||
import com.jaytux.grader.viewmodel.StudentState
|
||||
import com.jaytux.grader.data.Course
|
||||
import com.jaytux.grader.data.Edition
|
||||
import com.jaytux.grader.data.Group
|
||||
import com.jaytux.grader.data.Student
|
||||
import com.jaytux.grader.viewmodel.*
|
||||
|
||||
enum class Panel { Student, Group, Solo, GroupAs }
|
||||
data class Current(val p: Panel, val i: Int)
|
||||
fun Current?.studentIdx() = this?.let { if(p == Panel.Student) i else null }
|
||||
fun Current?.groupIdx() = this?.let { if(p == Panel.Group) i else null }
|
||||
fun Current?.soloIdx() = this?.let { if(p == Panel.Solo) i else null }
|
||||
fun Current?.groupAsIdx() = this?.let { if(p == Panel.GroupAs) i else null }
|
||||
data class Navigators(
|
||||
val student: (Student) -> Unit,
|
||||
val group: (Group) -> Unit,
|
||||
val assignment: (Assignment) -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||
var isGroup by remember { mutableStateOf(false) }
|
||||
var idx by remember { mutableStateOf<Current?>(null) }
|
||||
|
||||
val course = state.course; val edition = state.edition
|
||||
val students by state.students.entities
|
||||
val availableStudents by state.availableStudents.entities
|
||||
val groups by state.groups.entities
|
||||
val solo by state.solo.entities
|
||||
val groupAs by state.groupAs.entities
|
||||
val available by state.availableStudents.entities
|
||||
|
||||
val toggle = { i: Int, p: Panel ->
|
||||
idx = if(idx?.p == p && idx?.i == i) null else Current(p, i)
|
||||
}
|
||||
val peers by state.peer.entities
|
||||
val mergedAssignments by remember(solo, groupAs, peers) { mutableStateOf(Assignment.merge(groupAs, solo, peers)) }
|
||||
val hist by state.history
|
||||
|
||||
val navs = Navigators(
|
||||
student = { state.navTo(OpenPanel.Student, students.indexOfFirst{ s -> s.id == it.id }) },
|
||||
group = { state.navTo(OpenPanel.Group, groups.indexOfFirst { g -> g.id == it.id }) },
|
||||
assignment = { state.navTo(OpenPanel.Assignment, mergedAssignments.indexOfFirst { a -> a.id() == it.id() }) }
|
||||
)
|
||||
|
||||
val (id, tab) = hist.last()
|
||||
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
||||
TabLayout(
|
||||
listOf("Students", "Groups"),
|
||||
if (isGroup) 1 else 0,
|
||||
{ isGroup = it == 1 },
|
||||
{ Text(it) }
|
||||
OpenPanel.entries,
|
||||
tab.ordinal,
|
||||
{ state.navTo(OpenPanel.entries[it]) },
|
||||
{ Text(it.tabName) }
|
||||
) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
if (isGroup) {
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
GroupsWidget(
|
||||
state.course,
|
||||
state.edition,
|
||||
groups,
|
||||
idx.groupIdx(),
|
||||
{ toggle(it, Panel.Group) },
|
||||
{ state.newGroup(it) }) { group, name ->
|
||||
state.setGroupName(group, name)
|
||||
when(tab) {
|
||||
OpenPanel.Student -> StudentPanel(
|
||||
course, edition, students, availableStudents, id,
|
||||
{ state.navTo(it) },
|
||||
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
||||
{ students -> state.addToCourse(students) },
|
||||
{ s, name -> state.setStudentName(s, name) }
|
||||
) { s -> state.delete(s) }
|
||||
|
||||
OpenPanel.Group -> GroupPanel(
|
||||
course, edition, groups, id,
|
||||
{ state.navTo(it) },
|
||||
{ name -> state.newGroup(name) },
|
||||
{ g, name -> state.setGroupName(g, name) }
|
||||
) { g -> state.delete(g) }
|
||||
|
||||
OpenPanel.Assignment -> AssignmentPanel(
|
||||
course, edition, mergedAssignments, id,
|
||||
{ state.navTo(it) },
|
||||
{ type, name -> state.newAssignment(type, name) },
|
||||
{ a, name -> state.setAssignmentTitle(a, name) },
|
||||
{ a1, a2 -> state.swapOrder(a1, a2) }
|
||||
) { a -> state.delete(a) }
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
GroupAssignmentsWidget(
|
||||
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) },
|
||||
{ state.newGroupAssignment(it) }) { assignment, title ->
|
||||
state.setGroupAssignmentTitle(
|
||||
assignment,
|
||||
title
|
||||
)
|
||||
}
|
||||
|
||||
Column(Modifier.weight(0.75f)) {
|
||||
Row {
|
||||
IconButton({ state.back() }, enabled = hist.size >= 2) {
|
||||
Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp()).align(Alignment.CenterVertically))
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
StudentsWidget(
|
||||
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) },
|
||||
available, { state.addToCourse(it) }
|
||||
) { name, note, contact, addToEdition ->
|
||||
state.newStudent(name, contact, note, addToEdition)
|
||||
when(tab) {
|
||||
OpenPanel.Student -> {
|
||||
if(id == -1) PaneHeader("Nothing selected", "students", course, edition)
|
||||
else PaneHeader(students[id].name, "student", course, edition)
|
||||
}
|
||||
OpenPanel.Group -> {
|
||||
if(id == -1) PaneHeader("Nothing selected", "groups", course, edition)
|
||||
else PaneHeader(groups[id].name, "group", course, edition)
|
||||
}
|
||||
Box(Modifier.weight(0.5f)) {
|
||||
AssignmentsWidget(
|
||||
state.course,
|
||||
state.edition,
|
||||
solo,
|
||||
idx.soloIdx(),
|
||||
{ toggle(it, Panel.Solo) },
|
||||
{ state.newSoloAssignment(it) }) { assignment, title ->
|
||||
state.setSoloAssignmentTitle(assignment, title)
|
||||
OpenPanel.Assignment -> {
|
||||
if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition)
|
||||
else {
|
||||
when(val a = mergedAssignments[id]) {
|
||||
is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
|
||||
is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition)
|
||||
is Assignment.PeerEval -> PaneHeader(a.name(), "peer evaluation", course, edition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
if (id != -1) {
|
||||
when (tab) {
|
||||
OpenPanel.Student -> StudentView(StudentState(students[id], edition), navs)
|
||||
OpenPanel.Group -> GroupView(GroupState(groups[id]), navs)
|
||||
OpenPanel.Assignment -> {
|
||||
when (val a = mergedAssignments[id]) {
|
||||
is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
|
||||
is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
|
||||
is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation))
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(0.75f)) {
|
||||
idx?.let { i ->
|
||||
when(i.p) {
|
||||
Panel.Student -> StudentView(StudentState(students[i.i], state.edition))
|
||||
Panel.Group -> GroupView(GroupState(groups[i.i]))
|
||||
Panel.GroupAs -> GroupAssignmentView(GroupAssignmentState(groupAs[i.i]))
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> EditionSideWidget(
|
||||
course: Course, edition: Edition, header: String, hasNoX: String, addX: String,
|
||||
data: List<T>, selected: Int?, onSelect: (Int) -> Unit,
|
||||
singleWidget: @Composable (T) -> Unit,
|
||||
editDialog: @Composable ((current: T, onExit: () -> Unit) -> Unit)? = null,
|
||||
dialog: @Composable (onExit: () -> Unit) -> Unit
|
||||
fun StudentPanel(
|
||||
course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
||||
selected: Int, onSelect: (Int) -> Unit,
|
||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
||||
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
Text(header, style = MaterialTheme.typography.headlineMedium)
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var current by remember { mutableStateOf<T?>(null) }
|
||||
var deleting by remember { mutableStateOf(-1) }
|
||||
var editing by remember { mutableStateOf(-1) }
|
||||
|
||||
Text("Student list (${students.size})", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
ListOrEmpty(
|
||||
data,
|
||||
{ Text("Course ${course.name} (edition ${edition.name})\nhas no $hasNoX yet.", Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center) },
|
||||
{ Text("Add $addX") },
|
||||
students,
|
||||
{ Text(
|
||||
"Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||
) },
|
||||
{ Text("Add a student") },
|
||||
{ showDialog = true }
|
||||
) { idx, it ->
|
||||
Surface(
|
||||
Modifier.fillMaxWidth().clickable { onSelect(idx) },
|
||||
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
SelectEditDeleteRow(
|
||||
selected == idx,
|
||||
{ onSelect(idx) }, { onSelect(-1) },
|
||||
{ editing = idx }, { deleting = idx }
|
||||
) {
|
||||
Row {
|
||||
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { singleWidget(it) }
|
||||
editDialog?.let { _ ->
|
||||
IconButton({ current = it }, Modifier.align(Alignment.CenterVertically)) {
|
||||
Icon(Icons.Default.Edit, "Edit")
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(it.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) dialog { showDialog = false }
|
||||
editDialog?.let { d ->
|
||||
current?.let { c ->
|
||||
d(c) { current = null }
|
||||
if(showDialog) {
|
||||
StudentDialog(course, edition, { showDialog = false }, available, onImport, onAdd)
|
||||
}
|
||||
else if(editing != -1) {
|
||||
AddStringDialog("Student name", students.map { it.name }, { editing = -1 }, students[editing].name) {
|
||||
onUpdate(students[editing], it)
|
||||
}
|
||||
}
|
||||
else if(deleting != -1) {
|
||||
ConfirmDeleteDialog(
|
||||
"a student",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(students[deleting]) }
|
||||
) { Text(students[deleting].name) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StudentsWidget(
|
||||
course: Course, edition: Edition, students: List<Student>, selected: Int?, onSelect: (Int) -> Unit,
|
||||
availableStudents: List<Student>, onImport: (List<Student>) -> Unit,
|
||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Student list (${students.size})", "students", "a student", students, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||
) { onExit ->
|
||||
StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd)
|
||||
fun GroupPanel(
|
||||
course: Course, edition: Edition, groups: List<Group>,
|
||||
selected: Int, onSelect: (Int) -> Unit,
|
||||
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(-1) }
|
||||
var editing by remember { mutableStateOf(-1) }
|
||||
|
||||
Text("Group list (${groups.size})", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
ListOrEmpty(
|
||||
groups,
|
||||
{ Text(
|
||||
"Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||
) },
|
||||
{ Text("Add a group") },
|
||||
{ showDialog = true }
|
||||
) { idx, it ->
|
||||
SelectEditDeleteRow(
|
||||
selected == idx,
|
||||
{ onSelect(idx) }, { onSelect(-1) },
|
||||
{ editing = idx }, { deleting = idx }
|
||||
) {
|
||||
Text(it.name, Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) {
|
||||
AddStringDialog("Group name", groups.map{ it.name }, { showDialog = false }) { onAdd(it) }
|
||||
}
|
||||
else if(editing != -1) {
|
||||
AddStringDialog("Group name", groups.map { it.name }, { editing = -1 }, groups[editing].name) {
|
||||
onUpdate(groups[editing], it)
|
||||
}
|
||||
}
|
||||
else if(deleting != -1) {
|
||||
ConfirmDeleteDialog(
|
||||
"a group",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(groups[deleting]) }
|
||||
) { Text(groups[deleting].name) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AssignmentPanel(
|
||||
course: Course, edition: Edition, assignments: List<Assignment>,
|
||||
selected: Int, onSelect: (Int) -> Unit,
|
||||
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
||||
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit
|
||||
) = Column(Modifier.padding(10.dp)) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(-1) }
|
||||
var editing by remember { mutableStateOf(-1) }
|
||||
|
||||
val dialog: @Composable (String, List<String>, () -> Unit, String, (AssignmentType, String) -> Unit) -> Unit =
|
||||
{ label, taken, onClose, current, onSave ->
|
||||
DialogWindow(
|
||||
onCloseRequest = onClose,
|
||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||
) {
|
||||
var name by remember(current) { mutableStateOf(current) }
|
||||
var tab by remember { mutableStateOf(AssignmentType.Solo) }
|
||||
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
TabLayout(
|
||||
AssignmentType.entries,
|
||||
tab.ordinal,
|
||||
{ tab = AssignmentType.entries[it] },
|
||||
{ Text(it.show) }
|
||||
) {
|
||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
OutlinedTextField(
|
||||
name,
|
||||
{ name = it },
|
||||
Modifier.fillMaxWidth(),
|
||||
label = { Text(label) },
|
||||
isError = name in taken
|
||||
)
|
||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||
onSave(tab, name)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Assignment list (${assignments.size})", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
ListOrEmpty(
|
||||
assignments,
|
||||
{ Text(
|
||||
"Course ${course.name} (edition ${edition.name})\nhas no assignments yet.",
|
||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||
) },
|
||||
{ Text("Add an assignment") },
|
||||
{ showDialog = true }
|
||||
) { idx, it ->
|
||||
Selectable(
|
||||
selected == idx,
|
||||
{ onSelect(idx) }, { onSelect(-1) }
|
||||
) {
|
||||
Row {
|
||||
Text(it.name(), Modifier.padding(5.dp).align(Alignment.CenterVertically).weight(1f))
|
||||
Column(Modifier.padding(2.dp)) {
|
||||
Icon(Icons.Default.ArrowUpward, "Move up", Modifier.clickable {
|
||||
if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1])
|
||||
})
|
||||
Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable {
|
||||
if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1])
|
||||
})
|
||||
}
|
||||
Column(Modifier.padding(2.dp)) {
|
||||
Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx })
|
||||
Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = idx })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(showDialog) {
|
||||
dialog("Assignment name", assignments.map{ it.name() }, { showDialog = false }, "", onAdd)
|
||||
}
|
||||
else if(editing != -1) {
|
||||
AddStringDialog("Assignment name", assignments.map { it.name() }, { editing = -1 }, assignments[editing].name()) {
|
||||
onUpdate(assignments[editing], it)
|
||||
}
|
||||
}
|
||||
else if(deleting != -1) {
|
||||
ConfirmDeleteDialog(
|
||||
"an assignment",
|
||||
{ deleting = -1 },
|
||||
{ onDelete(assignments[deleting]) }
|
||||
) { Text(assignments[deleting].name()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -262,39 +417,3 @@ fun StudentDialog(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupsWidget(
|
||||
course: Course, edition: Edition, groups: List<Group>, selected: Int?, onSelect: (Int) -> Unit,
|
||||
onAdd: (name: String) -> Unit, onUpdate: (Group, String) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Group list (${groups.size})", "groups", "a group", groups, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) },
|
||||
{ current, onExit -> AddStringDialog("Group name", groups.map { it.name }, onExit, current.name) { onUpdate(current, it) } }
|
||||
) { onExit ->
|
||||
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AssignmentsWidget(
|
||||
course: Course, edition: Edition, assignments: List<SoloAssignment>, selected: Int?,
|
||||
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit, onUpdate: (SoloAssignment, String) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Assignment list", "assignments", "an assignment", assignments, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) },
|
||||
{ current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } }
|
||||
) { onExit ->
|
||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupAssignmentsWidget(
|
||||
course: Course, edition: Edition, assignments: List<GroupAssignment>, selected: Int?,
|
||||
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit, onUpdate: (GroupAssignment, String) -> Unit
|
||||
) = EditionSideWidget(
|
||||
course, edition, "Group assignment list", "group assignments", "an assignment", assignments, selected, onSelect,
|
||||
{ Text(it.name, Modifier.padding(5.dp)) },
|
||||
{ current, onExit -> AddStringDialog("Assignment title", assignments.map { it.name }, onExit, current.name) { onUpdate(current, it) } }
|
||||
) { onExit ->
|
||||
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jaytux.grader.viewmodel.immutable
|
||||
|
||||
interface MeasuredLazyListScope : LazyListScope {
|
||||
fun measuredWidth(): State<Dp>
|
||||
|
||||
fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit)
|
||||
}
|
||||
|
||||
interface MeasuredLazyItemScope : LazyItemScope {
|
||||
fun measuredWidth(): State<Dp>
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MeasuredLazyColumn(modifier: Modifier = Modifier, key: Any? = null, content: MeasuredLazyListScope.() -> Unit) {
|
||||
val measuredWidth = remember(key) { mutableStateOf(0.dp) }
|
||||
LazyColumn(modifier.onGloballyPositioned {
|
||||
measuredWidth.value = it.size.width.dp
|
||||
}) {
|
||||
val lisToMlis = { lis: LazyItemScope ->
|
||||
object : MeasuredLazyItemScope, LazyItemScope by lis {
|
||||
override fun measuredWidth(): State<Dp> = measuredWidth.immutable()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val scope = object : MeasuredLazyListScope, LazyListScope by this {
|
||||
override fun measuredWidth(): State<Dp> = measuredWidth.immutable()
|
||||
|
||||
override fun measuredItem(content: @Composable MeasuredLazyItemScope.() -> Unit) {
|
||||
item {
|
||||
lisToMlis(this).content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.content()
|
||||
}
|
||||
}
|
|
@ -1,42 +1,46 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
|
||||
import com.jaytux.grader.loadClipboard
|
||||
import com.jaytux.grader.toClipboard
|
||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||
|
||||
@OptIn(ExperimentalRichTextApi::class)
|
||||
@Composable
|
||||
fun RichTextStyleRow(
|
||||
modifier: Modifier = Modifier,
|
||||
state: RichTextState,
|
||||
) {
|
||||
val clip = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(modifier.fillMaxWidth()) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
item {
|
||||
RichTextStyleButton(
|
||||
|
@ -186,6 +190,14 @@ fun RichTextStyleRow(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton({ state.toClipboard(clip) }) {
|
||||
Icon(Icons.Default.ContentCopy, contentDescription = "Copy markdown")
|
||||
}
|
||||
IconButton({ state.loadClipboard(clip, scope) }) {
|
||||
Icon(Icons.Default.ContentPaste, contentDescription = "Paste markdown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -22,14 +22,13 @@ import com.jaytux.grader.viewmodel.GroupState
|
|||
import com.jaytux.grader.viewmodel.StudentState
|
||||
|
||||
@Composable
|
||||
fun StudentView(state: StudentState) {
|
||||
fun StudentView(state: StudentState, nav: Navigators) {
|
||||
val groups by state.groups.entities
|
||||
val courses by state.courseEditions.entities
|
||||
val groupGrades by state.groupGrades.entities
|
||||
val soloGrades by state.soloGrades.entities
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(state.student.name, "student", state.editionCourse)
|
||||
Row {
|
||||
Column(Modifier.weight(0.45f)) {
|
||||
Column(Modifier.padding(10.dp).weight(0.35f)) {
|
||||
|
@ -48,9 +47,9 @@ fun StudentView(state: StudentState) {
|
|||
Column(Modifier.weight(0.45f)) {
|
||||
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
||||
Row {
|
||||
val (group, c) = it
|
||||
val (course, ed) = c
|
||||
Row(Modifier.clickable { nav.group(group) }) {
|
||||
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
Text(
|
||||
|
@ -144,22 +143,20 @@ fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun GroupView(state: GroupState) {
|
||||
fun GroupView(state: GroupState, nav: Navigators) {
|
||||
val members by state.members.entities
|
||||
val available by state.availableStudents.entities
|
||||
val allRoles by state.roles.entities
|
||||
val (course, edition) = state.course
|
||||
|
||||
var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
PaneHeader(state.group.name, "group", course, edition)
|
||||
Row {
|
||||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
||||
val (student, role) = it
|
||||
Row {
|
||||
Row(Modifier.clickable { nav.student(student) }) {
|
||||
Text(
|
||||
"${student.name} (${role ?: "no role"})",
|
||||
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
||||
|
@ -177,7 +174,7 @@ fun GroupView(state: GroupState) {
|
|||
Column(Modifier.weight(0.5f)) {
|
||||
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
||||
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
||||
Row(Modifier.padding(5.dp)) {
|
||||
Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
|
||||
IconButton({ state.addStudent(it) }) {
|
||||
Icon(ChevronLeft, "Add student")
|
||||
}
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
package com.jaytux.grader.ui
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.*
|
||||
import com.jaytux.grader.data.Course
|
||||
import com.jaytux.grader.data.Edition
|
||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format.DateTimeFormat
|
||||
import kotlinx.datetime.format.byUnicodePattern
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
|
@ -74,7 +79,7 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, cur
|
|||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||
var name by remember(current) { mutableStateOf(current) }
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||
onSave(name)
|
||||
onClose()
|
||||
|
@ -84,6 +89,60 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, cur
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConfirmDeleteDialog(
|
||||
deleteAWhat: String,
|
||||
onExit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
render: @Composable () -> Unit
|
||||
) = DialogWindow(
|
||||
onCloseRequest = onExit,
|
||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||
) {
|
||||
Surface(Modifier.width(400.dp).height(300.dp), tonalElevation = 5.dp) {
|
||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
Text("You are about to delete $deleteAWhat.", Modifier.padding(10.dp))
|
||||
render()
|
||||
CancelSaveRow(true, onExit, "Cancel", "Delete") {
|
||||
onDelete()
|
||||
onExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ListOrEmpty(
|
||||
data: List<T>,
|
||||
onEmpty: @Composable ColumnScope.() -> Unit,
|
||||
addOptions: @Composable ColumnScope.() -> Unit,
|
||||
addAfterLazy: Boolean = true,
|
||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||
) {
|
||||
if(data.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
onEmpty()
|
||||
addOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Column {
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
itemsIndexed(data) { idx, it ->
|
||||
item(idx, it)
|
||||
}
|
||||
|
||||
if(!addAfterLazy) item { addOptions() }
|
||||
}
|
||||
if(addAfterLazy) addOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ListOrEmpty(
|
||||
data: List<T>,
|
||||
|
@ -92,41 +151,12 @@ fun <T> ListOrEmpty(
|
|||
onAdd: () -> Unit,
|
||||
addAfterLazy: Boolean = true,
|
||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||
) {
|
||||
if(data.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
emptyText()
|
||||
Button(onAdd, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
addText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Column {
|
||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||
itemsIndexed(data) { idx, it ->
|
||||
item(idx, it)
|
||||
}
|
||||
|
||||
if(!addAfterLazy) {
|
||||
item {
|
||||
Button(onAdd, Modifier.fillMaxWidth()) {
|
||||
addText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(addAfterLazy) {
|
||||
Button(onAdd, Modifier.fillMaxWidth()) {
|
||||
addText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) = ListOrEmpty(
|
||||
data, emptyText,
|
||||
{ Button(onAdd, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) { addText() } },
|
||||
addAfterLazy,
|
||||
item
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun <T> ListOrEmpty(
|
||||
|
@ -349,3 +379,82 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{
|
|||
Text(italic, fontStyle = FontStyle.Italic)
|
||||
Text(normal)
|
||||
}
|
||||
|
||||
@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 SelectEditDeleteRow(
|
||||
isSelected: Boolean,
|
||||
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) = Selectable(isSelected, onSelect, onDeselect) {
|
||||
Row {
|
||||
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() }
|
||||
IconButton(onEdit, Modifier.align(Alignment.CenterVertically)) {
|
||||
Icon(Icons.Default.Edit, "Edit")
|
||||
}
|
||||
IconButton(onDelete, Modifier.align(Alignment.CenterVertically)) {
|
||||
Icon(Icons.Default.Delete, "Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FromTo(size: Dp) {
|
||||
var w by remember { mutableStateOf(0) }
|
||||
var h by remember { mutableStateOf(0) }
|
||||
Box(Modifier.width(size).height(size).onGloballyPositioned {
|
||||
w = it.size.width
|
||||
h = it.size.height
|
||||
}) {
|
||||
Box(Modifier.align(Alignment.BottomStart)) {
|
||||
Text("Evaluator", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
Box {
|
||||
Text("Evaluated", Modifier.graphicsLayer {
|
||||
rotationZ = -90f
|
||||
translationX = w - 15f
|
||||
translationY = h - 15f
|
||||
transformOrigin = TransformOrigin(0f, 0.5f)
|
||||
}, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PEGradeWidget(
|
||||
grade: PeerEvaluationState.Student2StudentEntry?,
|
||||
onSelect: () -> Unit, onDeselect: () -> Unit,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) = Box(modifier.padding(2.dp)) {
|
||||
Selectable(isSelected, onSelect, onDeselect) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(grade?.let { if(it.grade.isNotBlank()) it.grade else if(it.feedback.isNotBlank()) "(other)" else null } ?: "none")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VLine(width: Dp = 1.dp, color: Color = Color.Black) = Spacer(Modifier.fillMaxHeight().width(width).background(color))
|
||||
|
||||
@Composable
|
||||
fun MeasuredLazyItemScope.HLine(height: Dp = 1.dp, color: Color = Color.Black) {
|
||||
val width by measuredWidth()
|
||||
Spacer(Modifier.width(width).height(height).background(color))
|
||||
}
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import com.jaytux.grader.data.*
|
||||
import com.jaytux.grader.data.EditionStudents.editionId
|
||||
import com.jaytux.grader.data.EditionStudents.studentId
|
||||
import com.jaytux.grader.viewmodel.GroupAssignmentState.*
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.datetime.TimeZone
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
@ -13,10 +14,47 @@ import org.jetbrains.exposed.sql.*
|
|||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
fun <T> MutableState<T>.immutable(): State<T> = this
|
||||
fun <T> SizedIterable<T>.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray()))
|
||||
|
||||
enum class AssignmentType(val show: String) { Solo("Solo Assignment"), Group("Group Assignment"), Peer("Peer Evaluation") }
|
||||
sealed class Assignment {
|
||||
class GAssignment(val assignment: GroupAssignment) : Assignment() {
|
||||
override fun name(): String = assignment.name
|
||||
override fun id(): EntityID<UUID> = assignment.id
|
||||
override fun index(): Int? = assignment.number
|
||||
}
|
||||
class SAssignment(val assignment: SoloAssignment) : Assignment() {
|
||||
override fun name(): String = assignment.name
|
||||
override fun id(): EntityID<UUID> = assignment.id
|
||||
override fun index(): Int? = assignment.number
|
||||
}
|
||||
class PeerEval(val evaluation: com.jaytux.grader.data.PeerEvaluation) : Assignment() {
|
||||
override fun name(): String = evaluation.name
|
||||
override fun id(): EntityID<UUID> = evaluation.id
|
||||
override fun index(): Int? = evaluation.number
|
||||
}
|
||||
|
||||
abstract fun name(): String
|
||||
abstract fun id(): EntityID<UUID>
|
||||
abstract fun index(): Int?
|
||||
|
||||
companion object {
|
||||
fun from(assignment: GroupAssignment) = GAssignment(assignment)
|
||||
fun from(assignment: SoloAssignment) = SAssignment(assignment)
|
||||
fun from(pEval: PeerEvaluation) = PeerEval(pEval)
|
||||
|
||||
fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>, peers: List<PeerEvaluation>): List<Assignment> {
|
||||
val g = groups.map { from(it) }
|
||||
val s = solos.map { from(it) }
|
||||
val p = peers.map { from(it) }
|
||||
return (g + s + p).sortedWith(compareBy<Assignment> { it.index() }.thenBy { it.name() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
||||
|
||||
private val rawEntities by lazy {
|
||||
|
@ -59,12 +97,19 @@ class EditionListState(val course: Course) {
|
|||
}
|
||||
}
|
||||
|
||||
enum class OpenPanel(val tabName: String) {
|
||||
Student("Students"), Group("Groups"), Assignment("Assignments")
|
||||
}
|
||||
|
||||
class EditionState(val edition: Edition) {
|
||||
val course = transaction { edition.course }
|
||||
val students = RawDbState { edition.soloStudents.sortAsc(Students.name).toList() }
|
||||
val groups = RawDbState { edition.groups.sortAsc(Groups.name).toList() }
|
||||
val solo = RawDbState { edition.soloAssignments.sortAsc(SoloAssignments.name).toList() }
|
||||
val groupAs = RawDbState { edition.groupAssignments.sortAsc(GroupAssignments.name).toList() }
|
||||
val peer = RawDbState { edition.peerEvaluations.sortAsc(PeerEvaluations.name).toList() }
|
||||
private val _history = mutableStateOf(listOf(-1 to OpenPanel.Assignment))
|
||||
val history = _history.immutable()
|
||||
|
||||
val availableStudents = RawDbState {
|
||||
Student.find {
|
||||
|
@ -84,7 +129,12 @@ class EditionState(val edition: Edition) {
|
|||
if(addToEdition) students.refresh()
|
||||
else availableStudents.refresh()
|
||||
}
|
||||
|
||||
fun setStudentName(student: Student, name: String) {
|
||||
transaction {
|
||||
student.name = name
|
||||
}
|
||||
students.refresh()
|
||||
}
|
||||
fun addToCourse(students: List<Student>) {
|
||||
transaction {
|
||||
EditionStudents.batchInsert(students) {
|
||||
|
@ -92,7 +142,7 @@ class EditionState(val edition: Edition) {
|
|||
this[studentId] = it.id
|
||||
}
|
||||
}
|
||||
availableStudents.refresh();
|
||||
availableStudents.refresh()
|
||||
this.students.refresh()
|
||||
}
|
||||
|
||||
|
@ -114,9 +164,17 @@ class EditionState(val edition: Edition) {
|
|||
return instant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
}
|
||||
|
||||
private fun nextIdx(): Int = max(
|
||||
solo.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0,
|
||||
groupAs.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0
|
||||
) + 1
|
||||
|
||||
fun newSoloAssignment(name: String) {
|
||||
transaction {
|
||||
SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() }
|
||||
SoloAssignment.new {
|
||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||
this.number = nextIdx()
|
||||
}
|
||||
solo.refresh()
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +186,10 @@ class EditionState(val edition: Edition) {
|
|||
}
|
||||
fun newGroupAssignment(name: String) {
|
||||
transaction {
|
||||
GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() }
|
||||
GroupAssignment.new {
|
||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
||||
this.number = nextIdx()
|
||||
}
|
||||
groupAs.refresh()
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +199,161 @@ class EditionState(val edition: Edition) {
|
|||
}
|
||||
groupAs.refresh()
|
||||
}
|
||||
fun newPeerEvaluation(name: String) {
|
||||
transaction {
|
||||
PeerEvaluation.new {
|
||||
this.name = name; this.edition = this@EditionState.edition
|
||||
this.number = nextIdx()
|
||||
}
|
||||
peer.refresh()
|
||||
}
|
||||
}
|
||||
fun setPeerEvaluationTitle(assignment: PeerEvaluation, title: String) {
|
||||
transaction {
|
||||
assignment.name = title
|
||||
}
|
||||
peer.refresh()
|
||||
}
|
||||
|
||||
fun newAssignment(type: AssignmentType, name: String) = when(type) {
|
||||
AssignmentType.Solo -> newSoloAssignment(name)
|
||||
AssignmentType.Group -> newGroupAssignment(name)
|
||||
AssignmentType.Peer -> newPeerEvaluation(name)
|
||||
}
|
||||
fun setAssignmentTitle(assignment: Assignment, title: String) = when(assignment) {
|
||||
is Assignment.GAssignment -> setGroupAssignmentTitle(assignment.assignment, title)
|
||||
is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title)
|
||||
is Assignment.PeerEval -> setPeerEvaluationTitle(assignment.evaluation, title)
|
||||
}
|
||||
|
||||
fun swapOrder(a1: Assignment, a2: Assignment) {
|
||||
transaction {
|
||||
when(a1) {
|
||||
is Assignment.GAssignment -> {
|
||||
when(a2) {
|
||||
is Assignment.GAssignment -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = a2.assignment.number
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.SAssignment -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = nextIdx()
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.PeerEval -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = nextIdx()
|
||||
a2.evaluation.number = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
is Assignment.SAssignment -> {
|
||||
when(a2) {
|
||||
is Assignment.GAssignment -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = a2.assignment.number
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.SAssignment -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = a2.assignment.number
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.PeerEval -> {
|
||||
val temp = a1.assignment.number
|
||||
a1.assignment.number = nextIdx()
|
||||
a2.evaluation.number = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
is Assignment.PeerEval -> {
|
||||
when(a2) {
|
||||
is Assignment.GAssignment -> {
|
||||
val temp = a1.evaluation.number
|
||||
a1.evaluation.number = a2.assignment.number
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.SAssignment -> {
|
||||
val temp = a1.evaluation.number
|
||||
a1.evaluation.number = a2.assignment.number
|
||||
a2.assignment.number = temp
|
||||
}
|
||||
is Assignment.PeerEval -> {
|
||||
val temp = a1.evaluation.number
|
||||
a1.evaluation.number = a2.evaluation.number
|
||||
a2.evaluation.number = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
solo.refresh(); groupAs.refresh()
|
||||
}
|
||||
|
||||
fun delete(s: Student) {
|
||||
transaction {
|
||||
EditionStudents.deleteWhere { studentId eq s.id }
|
||||
GroupStudents.deleteWhere { studentId eq s.id }
|
||||
IndividualFeedbacks.deleteWhere { studentId eq s.id }
|
||||
}
|
||||
students.refresh(); availableStudents.refresh()
|
||||
}
|
||||
fun delete(g: Group) {
|
||||
transaction {
|
||||
GroupFeedbacks.deleteWhere { groupId eq g.id }
|
||||
IndividualFeedbacks.deleteWhere { groupId eq g.id }
|
||||
GroupStudents.deleteWhere { groupId eq g.id }
|
||||
g.delete()
|
||||
}
|
||||
groups.refresh(); groupAs.refresh()
|
||||
}
|
||||
fun delete(sa: SoloAssignment) {
|
||||
transaction {
|
||||
SoloAssignmentCriteria.selectAll().where { SoloAssignmentCriteria.assignmentId eq sa.id }.forEach { it ->
|
||||
val id = it[SoloAssignmentCriteria.assignmentId]
|
||||
SoloFeedbacks.deleteWhere { criterionId eq id }
|
||||
}
|
||||
SoloAssignmentCriteria.deleteWhere { assignmentId eq sa.id }
|
||||
sa.delete()
|
||||
}
|
||||
solo.refresh()
|
||||
}
|
||||
fun delete(ga: GroupAssignment) {
|
||||
transaction {
|
||||
GroupAssignmentCriteria.selectAll().where { GroupAssignmentCriteria.assignmentId eq ga.id }.forEach { it ->
|
||||
val id = it[GroupAssignmentCriteria.assignmentId]
|
||||
GroupFeedbacks.deleteWhere { criterionId eq id }
|
||||
IndividualFeedbacks.deleteWhere { criterionId eq id }
|
||||
}
|
||||
GroupAssignmentCriteria.deleteWhere { assignmentId eq ga.id }
|
||||
ga.delete()
|
||||
}
|
||||
groupAs.refresh()
|
||||
}
|
||||
fun delete(pe: PeerEvaluation) {
|
||||
transaction {
|
||||
PeerEvaluationContents.deleteWhere { peerEvaluationId eq pe.id }
|
||||
StudentToStudentEvaluation.deleteWhere { peerEvaluationId eq pe.id }
|
||||
pe.delete()
|
||||
}
|
||||
peer.refresh()
|
||||
}
|
||||
fun delete(assignment: Assignment) = when(assignment) {
|
||||
is Assignment.GAssignment -> delete(assignment.assignment)
|
||||
is Assignment.SAssignment -> delete(assignment.assignment)
|
||||
is Assignment.PeerEval -> delete(assignment.evaluation)
|
||||
}
|
||||
|
||||
fun navTo(panel: OpenPanel, id: Int = -1) {
|
||||
_history.value += (id to panel)
|
||||
}
|
||||
fun navTo(id: Int) = navTo(_history.value.last().second, id)
|
||||
fun back() {
|
||||
var temp = _history.value.dropLast(1)
|
||||
while(temp.last().first == -1 && temp.size >= 2) temp = temp.dropLast(1)
|
||||
_history.value = temp
|
||||
}
|
||||
}
|
||||
|
||||
class StudentState(val student: Student, edition: Edition) {
|
||||
|
@ -155,13 +371,15 @@ class StudentState(val student: Student, edition: Edition) {
|
|||
(Groups.editionId eq edition.id) and (Groups.id inList student.groups.map { it.id })
|
||||
}.associate { it.id to it.name }
|
||||
|
||||
val asGroup = (GroupAssignments innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||
GroupFeedbacks.groupId inList groupsForEdition.keys.toList()
|
||||
}.map { it[GroupFeedbacks.groupAssignmentId] to it }
|
||||
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
||||
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
|
||||
(GroupAssignmentCriteria.name eq "")
|
||||
}.map { it[GroupAssignments.id] to it }
|
||||
|
||||
val asIndividual = (GroupAssignments innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
||||
IndividualFeedbacks.studentId eq student.id
|
||||
}.map { it[IndividualFeedbacks.groupAssignmentId] to it }
|
||||
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
||||
(IndividualFeedbacks.studentId eq student.id) and
|
||||
(GroupAssignmentCriteria.name eq "")
|
||||
}.map { it[GroupAssignments.id] to it }
|
||||
|
||||
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
|
||||
asGroup.forEach {
|
||||
|
@ -183,8 +401,9 @@ class StudentState(val student: Student, edition: Edition) {
|
|||
}
|
||||
|
||||
val soloGrades = RawDbState {
|
||||
(SoloAssignments innerJoin SoloFeedbacks).selectAll().where {
|
||||
SoloFeedbacks.studentId eq student.id
|
||||
(SoloAssignments innerJoin SoloAssignmentCriteria innerJoin SoloFeedbacks).selectAll().where {
|
||||
(SoloFeedbacks.studentId eq student.id) and
|
||||
(SoloAssignmentCriteria.name eq "")
|
||||
}.map { LocalSoloGrade(it[SoloAssignments.name], it[SoloFeedbacks.grade]) }.toList()
|
||||
}
|
||||
|
||||
|
@ -238,25 +457,34 @@ class GroupState(val group: Group) {
|
|||
}
|
||||
|
||||
class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||
data class LocalFeedback(val feedback: String, val grade: String)
|
||||
data class FeedbackEntry(val feedback: String, val grade: String)
|
||||
data class LocalCriterionFeedback(
|
||||
val criterion: GroupAssignmentCriterion, val entry: FeedbackEntry?
|
||||
)
|
||||
data class LocalFeedback(
|
||||
val global: FeedbackEntry?, val byCriterion: List<LocalCriterionFeedback>
|
||||
)
|
||||
data class LocalGFeedback(
|
||||
val group: Group,
|
||||
val feedback: LocalFeedback?,
|
||||
val individuals: List<Pair<Student, Pair<String?, LocalFeedback?>>>
|
||||
val feedback: LocalFeedback,
|
||||
val individuals: List<Pair<Student, Pair<String?, LocalFeedback>>> // Student -> (Role, Feedback)
|
||||
)
|
||||
|
||||
val editionCourse = transaction { assignment.edition.course to assignment.edition }
|
||||
private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
|
||||
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||
val feedback = RawDbState { loadFeedback() }
|
||||
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
||||
val criteria = RawDbState {
|
||||
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList()
|
||||
}
|
||||
val feedback = RawDbState { loadFeedback() }
|
||||
|
||||
val autofill = RawDbState {
|
||||
val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||
val forGroups = (GroupFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap {
|
||||
it[GroupFeedbacks.feedback].split('\n')
|
||||
}
|
||||
|
||||
val forIndividuals = IndividualFeedbacks.selectAll().where { IndividualFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||
val forIndividuals = (IndividualFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap {
|
||||
it[IndividualFeedbacks.feedback].split('\n')
|
||||
}
|
||||
|
||||
|
@ -264,60 +492,88 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||
}
|
||||
|
||||
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
||||
val individuals = IndividualFeedbacks.selectAll().where {
|
||||
IndividualFeedbacks.groupAssignmentId eq assignment.id
|
||||
}.map {
|
||||
it[IndividualFeedbacks.studentId] to LocalFeedback(it[IndividualFeedbacks.feedback], it[IndividualFeedbacks.grade])
|
||||
}.associate { it }
|
||||
val allCrit = GroupAssignmentCriterion.find {
|
||||
GroupAssignmentCriteria.assignmentId eq assignment.id
|
||||
}
|
||||
|
||||
val groupFeedbacks = GroupFeedbacks.selectAll().where {
|
||||
GroupFeedbacks.groupAssignmentId eq assignment.id
|
||||
}.map {
|
||||
it[GroupFeedbacks.groupId] to (it[GroupFeedbacks.feedback] to it[GroupFeedbacks.grade])
|
||||
}.associate { it }
|
||||
|
||||
val groups = Group.find {
|
||||
return Group.find {
|
||||
(Groups.editionId eq assignment.edition.id)
|
||||
}.sortAsc(Groups.name).map { group ->
|
||||
val students = group.studentRoles.sortedBy { it.student.name }.map { sR ->
|
||||
val student = sR.student
|
||||
val role = sR.role
|
||||
val feedback = individuals[student.id]
|
||||
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 fdbk = row[GroupFeedbacks.feedback]
|
||||
val grade = row[GroupFeedbacks.grade]
|
||||
|
||||
student to (role to feedback)
|
||||
crit to FeedbackEntry(fdbk, grade)
|
||||
}
|
||||
|
||||
groupFeedbacks[group.id]?.let { (f, g) ->
|
||||
group to LocalGFeedback(group, LocalFeedback(f, g), students)
|
||||
} ?: (group to LocalGFeedback(group, null, students))
|
||||
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 byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||
}
|
||||
|
||||
return groups
|
||||
val byGroup = LocalFeedback(global, byCrit)
|
||||
|
||||
val indiv = group.studentRoles.map {
|
||||
val student = it.student
|
||||
val role = it.role
|
||||
|
||||
val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents)
|
||||
.selectAll().where {
|
||||
(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 fdbk = row[IndividualFeedbacks.feedback]
|
||||
val grade = row[IndividualFeedbacks.grade]
|
||||
|
||||
crit to FeedbackEntry(fdbk, grade)
|
||||
}
|
||||
|
||||
fun upsertGroupFeedback(group: Group, msg: String, grd: String) {
|
||||
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 byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
||||
}
|
||||
val byStudent = LocalFeedback(global, byCrit)
|
||||
|
||||
student to (role to byStudent)
|
||||
}
|
||||
|
||||
group to LocalGFeedback(group, byGroup, indiv)
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertGroupFeedback(group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
|
||||
transaction {
|
||||
GroupFeedbacks.upsert {
|
||||
it[groupAssignmentId] = assignment.id
|
||||
it[assignmentId] = assignment.id
|
||||
it[groupId] = group.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
it[criterionId] = criterion?.id
|
||||
}
|
||||
}
|
||||
feedback.refresh()
|
||||
feedback.refresh(); autofill.refresh()
|
||||
}
|
||||
|
||||
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) {
|
||||
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
|
||||
transaction {
|
||||
IndividualFeedbacks.upsert {
|
||||
it[groupAssignmentId] = assignment.id
|
||||
it[assignmentId] = assignment.id
|
||||
it[groupId] = group.id
|
||||
it[studentId] = student.id
|
||||
it[this.feedback] = msg
|
||||
it[this.grade] = grd
|
||||
it[criterionId] = criterion?.id
|
||||
}
|
||||
}
|
||||
feedback.refresh()
|
||||
feedback.refresh(); autofill.refresh()
|
||||
}
|
||||
|
||||
fun updateTask(t: String) {
|
||||
|
@ -333,6 +589,215 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||
}
|
||||
_deadline.value = d
|
||||
}
|
||||
|
||||
fun addCriterion(name: String) {
|
||||
transaction {
|
||||
GroupAssignmentCriterion.new {
|
||||
this.name = name;
|
||||
this.description = "";
|
||||
this.assignment = this@GroupAssignmentState.assignment
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCriterion(criterion: GroupAssignmentCriterion, name: String, desc: String) {
|
||||
transaction {
|
||||
criterion.name = name
|
||||
criterion.description = desc
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
|
||||
fun deleteCriterion(criterion: GroupAssignmentCriterion) {
|
||||
transaction {
|
||||
GroupFeedbacks.deleteWhere { criterionId eq criterion.id }
|
||||
IndividualFeedbacks.deleteWhere { criterionId eq criterion.id }
|
||||
criterion.delete()
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
class SoloAssignmentState(val assignment: SoloAssignment) {
|
||||
data class LocalFeedback(val feedback: String, val grade: String)
|
||||
data class FullFeedback(val global: LocalFeedback?, val byCriterion: List<Pair<SoloAssignmentCriterion, LocalFeedback?>>)
|
||||
|
||||
val editionCourse = transaction { assignment.edition.course to assignment.edition }
|
||||
private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
|
||||
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()
|
||||
}
|
||||
val feedback = RawDbState { loadFeedback() }
|
||||
|
||||
val autofill = RawDbState {
|
||||
SoloFeedbacks.selectAll().where { SoloFeedbacks.assignmentId eq assignment.id }.map {
|
||||
it[SoloFeedbacks.feedback].split('\n')
|
||||
}.flatten().distinct().sorted()
|
||||
}
|
||||
|
||||
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
|
||||
val allCrit = SoloAssignmentCriterion.find {
|
||||
SoloAssignmentCriteria.assignmentId eq assignment.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 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 byCrit = allCrit.map { c ->
|
||||
byCrit_[c.id] ?: Pair(c, null)
|
||||
}
|
||||
|
||||
student to FullFeedback(global, byCrit)
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertFeedback(student: Student, msg: String?, grd: String?, criterion: SoloAssignmentCriterion? = null) {
|
||||
transaction {
|
||||
SoloFeedbacks.upsert {
|
||||
it[assignmentId] = assignment.id
|
||||
it[studentId] = student.id
|
||||
it[this.feedback] = msg ?: ""
|
||||
it[this.grade] = grd ?: ""
|
||||
it[criterionId] = criterion?.id
|
||||
}
|
||||
}
|
||||
feedback.refresh(); autofill.refresh()
|
||||
}
|
||||
|
||||
fun updateTask(t: String) {
|
||||
transaction {
|
||||
assignment.assignment = t
|
||||
}
|
||||
_task.value = t
|
||||
}
|
||||
|
||||
fun updateDeadline(d: LocalDateTime) {
|
||||
transaction {
|
||||
assignment.deadline = d
|
||||
}
|
||||
_deadline.value = d
|
||||
}
|
||||
|
||||
fun addCriterion(name: String) {
|
||||
transaction {
|
||||
SoloAssignmentCriterion.new {
|
||||
this.name = name;
|
||||
this.description = "";
|
||||
this.assignment = this@SoloAssignmentState.assignment
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCriterion(criterion: SoloAssignmentCriterion, name: String, desc: String) {
|
||||
transaction {
|
||||
criterion.name = name
|
||||
criterion.description = desc
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
|
||||
fun deleteCriterion(criterion: SoloAssignmentCriterion) {
|
||||
transaction {
|
||||
SoloFeedbacks.deleteWhere { criterionId eq criterion.id }
|
||||
criterion.delete()
|
||||
}
|
||||
criteria.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
class PeerEvaluationState(val evaluation: PeerEvaluation) {
|
||||
data class Student2StudentEntry(val grade: String, val feedback: String)
|
||||
data class StudentEntry(val student: Student, val global: Student2StudentEntry?, val others: List<Pair<Student, Student2StudentEntry?>>)
|
||||
data class GroupEntry(val group: Group, val content: String, val students: List<StudentEntry>)
|
||||
val editionCourse = transaction { evaluation.edition.course to evaluation.edition }
|
||||
private val _name = mutableStateOf(evaluation.name); val name = _name.immutable()
|
||||
val contents = RawDbState { loadContents() }
|
||||
|
||||
private fun Transaction.loadContents(): List<GroupEntry> {
|
||||
val found = (Groups leftJoin PeerEvaluationContents).selectAll().where {
|
||||
Groups.editionId eq evaluation.edition.id
|
||||
}.associate { gc ->
|
||||
val group = Group[gc[Groups.id]]
|
||||
val content = gc[PeerEvaluationContents.content] ?: ""
|
||||
val students = group.students.map { student1 ->
|
||||
val others = group.students.map { student2 ->
|
||||
val eval = StudentToStudentEvaluation.selectAll().where {
|
||||
StudentToStudentEvaluation.peerEvaluationId eq evaluation.id and
|
||||
(StudentToStudentEvaluation.studentIdFrom eq student1.id) and
|
||||
(StudentToStudentEvaluation.studentIdTo eq student2.id)
|
||||
}.firstOrNull()
|
||||
student2 to eval?.let {
|
||||
Student2StudentEntry(
|
||||
it[StudentToStudentEvaluation.grade], it[StudentToStudentEvaluation.note]
|
||||
)
|
||||
}
|
||||
}.sortedBy { it.first.name }
|
||||
val global = StudentToGroupEvaluation.selectAll().where {
|
||||
StudentToGroupEvaluation.peerEvaluationId eq evaluation.id and
|
||||
(StudentToGroupEvaluation.studentId eq student1.id)
|
||||
}.firstOrNull()?.let {
|
||||
Student2StudentEntry(it[StudentToGroupEvaluation.grade], it[StudentToGroupEvaluation.note])
|
||||
}
|
||||
|
||||
StudentEntry(student1, global, others)
|
||||
}.sortedBy { it.student.name } // enforce synchronized order
|
||||
|
||||
group to GroupEntry(group, content, students)
|
||||
}
|
||||
|
||||
return editionCourse.second.groups.map {
|
||||
found[it] ?: GroupEntry(
|
||||
it, "",
|
||||
it.students.map { s1 -> StudentEntry(s1, null, it.students.map { s2 -> s2 to null }) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertGroupFeedback(group: Group, feedback: String) {
|
||||
transaction {
|
||||
PeerEvaluationContents.upsert {
|
||||
it[peerEvaluationId] = evaluation.id
|
||||
it[groupId] = group.id
|
||||
it[this.content] = feedback
|
||||
}
|
||||
}
|
||||
contents.refresh()
|
||||
}
|
||||
|
||||
fun upsertIndividualFeedback(from: Student, to: Student?, grade: String, feedback: String) {
|
||||
transaction {
|
||||
to?.let {
|
||||
StudentToStudentEvaluation.upsert {
|
||||
it[peerEvaluationId] = evaluation.id
|
||||
it[studentIdFrom] = from.id
|
||||
it[studentIdTo] = to.id
|
||||
it[this.grade] = grade
|
||||
it[this.note] = feedback
|
||||
}
|
||||
} ?: StudentToGroupEvaluation.upsert {
|
||||
it[peerEvaluationId] = evaluation.id
|
||||
it[studentId] = from.id
|
||||
it[this.grade] = grade
|
||||
it[this.note] = feedback
|
||||
}
|
||||
}
|
||||
contents.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -353,4 +818,3 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue