Restructure DB & UI, pt.1
This commit is contained in:
@@ -7,12 +7,17 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm("desktop")
|
compilerOptions {
|
||||||
|
freeCompilerArgs.add("-Xcontext-parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
jvm("desktop") {}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val desktopMain by getting
|
val desktopMain by getting
|
||||||
|
|
||||||
commonMain.dependencies {
|
desktopMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material)
|
implementation(compose.material)
|
||||||
@@ -22,16 +27,13 @@ kotlin {
|
|||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.material3.core)
|
implementation(libs.material3.core)
|
||||||
implementation(libs.material.icons)
|
|
||||||
implementation(libs.sl4j)
|
implementation(libs.sl4j)
|
||||||
}
|
|
||||||
desktopMain.dependencies {
|
|
||||||
implementation(compose.desktop.currentOs)
|
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
implementation(libs.exposed.dao)
|
implementation(libs.exposed.dao)
|
||||||
implementation(libs.exposed.migration)
|
implementation(libs.exposed.migration)
|
||||||
|
implementation(libs.exposed.migration.jdbc)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.material3.desktop)
|
implementation(libs.material3.desktop)
|
||||||
@@ -40,6 +42,9 @@ kotlin {
|
|||||||
implementation(libs.filekit.dialogs)
|
implementation(libs.filekit.dialogs)
|
||||||
implementation(libs.filekit.dialogs.compose)
|
implementation(libs.filekit.dialogs.compose)
|
||||||
implementation(libs.filekit.coil)
|
implementation(libs.filekit.coil)
|
||||||
|
implementation(libs.directories)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
implementation(libs.compose.backhandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +57,7 @@ compose.desktop {
|
|||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "com.jaytux.grader"
|
packageName = "com.jaytux.grader"
|
||||||
|
mainClass = "com.jaytux.grader.MainKt"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.0"
|
||||||
includeAllModules = true
|
includeAllModules = true
|
||||||
|
|
||||||
|
|||||||
4866
composeApp/hs_err_pid2264499.log
Normal file
4866
composeApp/hs_err_pid2264499.log
Normal file
File diff suppressed because one or more lines are too long
@@ -1,43 +1,37 @@
|
|||||||
package com.jaytux.grader
|
package com.jaytux.grader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
import androidx.compose.ui.Modifier
|
import com.jaytux.grader.data.v2.Course
|
||||||
import androidx.compose.ui.unit.dp
|
import com.jaytux.grader.data.v2.Edition
|
||||||
import com.jaytux.grader.ui.ChevronLeft
|
import com.jaytux.grader.ui.EditionTitle
|
||||||
import com.jaytux.grader.ui.CoursesView
|
import com.jaytux.grader.ui.EditionView
|
||||||
import com.jaytux.grader.ui.toDp
|
import com.jaytux.grader.ui.GroupsGradingTitle
|
||||||
import com.jaytux.grader.viewmodel.CourseListState
|
import com.jaytux.grader.ui.GroupsGradingView
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import com.jaytux.grader.ui.HomeTitle
|
||||||
|
import com.jaytux.grader.ui.HomeView
|
||||||
|
import com.jaytux.grader.ui.PeerEvalsGradingTitle
|
||||||
|
import com.jaytux.grader.ui.PeerEvalsGradingView
|
||||||
|
import com.jaytux.grader.ui.SolosGradingTitle
|
||||||
|
import com.jaytux.grader.ui.SolosGradingView
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
|
||||||
data class UiRoute(val heading: String, val content: @Composable (push: (UiRoute) -> Unit) -> Unit)
|
object Home : Navigator.IDestination
|
||||||
|
data class EditionDetail(val ed: Edition, val course: Course) : Navigator.IDestination
|
||||||
|
data class GroupGrading(val course: Course, val edition: Edition, val assignment: BaseAssignment) : Navigator.IDestination
|
||||||
|
data class SoloGrading(val course: Course, val edition: Edition, val assignment: BaseAssignment) : Navigator.IDestination
|
||||||
|
data class PeerEvalGrading(val course: Course, val edition: Edition, val assignment: BaseAssignment) : Navigator.IDestination
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
|
||||||
fun App() {
|
fun App() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val courseList = CourseListState()
|
Navigator.NavHost(Home) {
|
||||||
var stack by remember {
|
composable<Home>({ HomeTitle() }) { _, token -> HomeView(token) }
|
||||||
val start = UiRoute("Courses Overview") { CoursesView(courseList, it) }
|
composable<EditionDetail>({ EditionTitle(it) }) { data, token -> EditionView(data, token) }
|
||||||
mutableStateOf(listOf(start))
|
composable<GroupGrading>({ GroupsGradingTitle(it) }) { data, token -> GroupsGradingView(data, token) }
|
||||||
}
|
composable<SoloGrading>({ SolosGradingTitle(it) }) { data, token -> SolosGradingView(data, token) }
|
||||||
|
composable<PeerEvalGrading>({ PeerEvalsGradingTitle(it) }) { data, token -> PeerEvalsGradingView(data, token) }
|
||||||
Column {
|
|
||||||
Surface(Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary, tonalElevation = 10.dp, shadowElevation = 10.dp) {
|
|
||||||
Row(Modifier.padding(10.dp)) {
|
|
||||||
IconButton({ stack = stack.toMutableList().also { it.removeLast() } }, enabled = stack.size >= 2) {
|
|
||||||
Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineLarge.fontSize.toDp()))
|
|
||||||
}
|
|
||||||
Text(stack.last().heading, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineLarge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Surface(Modifier.fillMaxSize()) {
|
|
||||||
Box {
|
|
||||||
stack.last().content { stack += (it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.jaytux.grader
|
||||||
|
|
||||||
|
sealed class Either<out E , out V> {
|
||||||
|
class Error<E>(val error: E) : Either<E, Nothing>() {
|
||||||
|
override fun toString(): String = "Error($error)"
|
||||||
|
}
|
||||||
|
class Value<V>(val value: V) : Either<Nothing, V>() {
|
||||||
|
override fun toString(): String = "Value($value)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isError() = this is Error<E>
|
||||||
|
fun isValue() = this is Value<V>
|
||||||
|
|
||||||
|
fun <R> map(f: (V) -> R) = when(this) {
|
||||||
|
is Error -> this
|
||||||
|
is Value -> Value(f(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> mapSuspend(f: suspend (V) -> R) = when(this) {
|
||||||
|
is Error -> this
|
||||||
|
is Value -> Value(f(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <R> mapError(f: (E) -> R) = when(this) {
|
||||||
|
is Error -> Error(f(error))
|
||||||
|
is Value -> this
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> mapErrorSuspend(f: suspend (E) -> R) = when(this) {
|
||||||
|
is Error -> Error(f(error))
|
||||||
|
is Value -> this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <R> fold(fError: (E) -> R, fValue: (V) -> R): R = when(this) {
|
||||||
|
is Error -> fError(error)
|
||||||
|
is Value -> fValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> foldSuspend(fError: suspend (E) -> R, fValue: suspend (V) -> R): R = when(this) {
|
||||||
|
is Error -> fError(error)
|
||||||
|
is Value -> fValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <E> E.error(): Either<E, Nothing> = Error(this)
|
||||||
|
fun <V> V.value(): Either<Nothing, V> = Value(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
package com.jaytux.grader
|
package com.jaytux.grader
|
||||||
|
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.Clipboard
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import com.jaytux.grader.data.Database
|
||||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.prefs.Preferences
|
||||||
|
|
||||||
fun String.maxN(n: Int): String {
|
fun String.maxN(n: Int): String {
|
||||||
return if (this.length > n) {
|
return if (this.length > n) {
|
||||||
@@ -14,10 +20,25 @@ fun String.maxN(n: Int): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RichTextState.toClipboard(clip: ClipboardManager) {
|
suspend fun RichTextState.toClipboard(clip: ClipboardManager) {
|
||||||
clip.setText(AnnotatedString(this.toMarkdown()))
|
clip.setText(AnnotatedString(this.toMarkdown()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RichTextState.loadClipboard(clip: ClipboardManager, scope: CoroutineScope) {
|
suspend fun RichTextState.loadClipboard(clip: ClipboardManager, scope: CoroutineScope) {
|
||||||
scope.launch { setMarkdown(clip.getText()?.text ?: "") }
|
scope.launch { setMarkdown(clip.getText()?.text ?: "") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Preferences {
|
||||||
|
private val _p = Preferences.userNodeForPackage(this::class.java)
|
||||||
|
|
||||||
|
operator fun get(key: String): String? = _p.get(key, null)
|
||||||
|
operator fun set(key: String, value: String) {
|
||||||
|
_p.put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exportPath
|
||||||
|
get() = get("exportPath") ?: System.getProperty("user.home") + "/grader_export"
|
||||||
|
set(value) { set("exportPath", value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun <T1, T2, T3> Pair<T1, T2>.app(x: T3) = Triple(first, second, x)
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.jaytux.grader.data
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.CompositeIdTable
|
|
||||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
|
||||||
import org.jetbrains.exposed.sql.Table
|
|
||||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
|
||||||
|
|
||||||
object Courses : UUIDTable("courses") {
|
|
||||||
val name = varchar("name", 50).uniqueIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
object Editions : UUIDTable("editions") {
|
|
||||||
val courseId = reference("course_id", Courses.id)
|
|
||||||
val name = varchar("name", 50)
|
|
||||||
|
|
||||||
init {
|
|
||||||
uniqueIndex(courseId, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Groups : UUIDTable("groups") {
|
|
||||||
val editionId = reference("edition_id", Editions.id)
|
|
||||||
val name = varchar("name", 50)
|
|
||||||
|
|
||||||
init {
|
|
||||||
uniqueIndex(editionId, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Students : UUIDTable("students") {
|
|
||||||
val name = varchar("name", 50)
|
|
||||||
val contact = varchar("contact", 50)
|
|
||||||
val note = text("note")
|
|
||||||
}
|
|
||||||
|
|
||||||
object GroupStudents : UUIDTable("grpStudents") {
|
|
||||||
val groupId = reference("group_id", Groups.id)
|
|
||||||
val studentId = reference("student_id", Students.id)
|
|
||||||
val role = varchar("role", 50).nullable()
|
|
||||||
|
|
||||||
init {
|
|
||||||
uniqueIndex(groupId, studentId) // can't figure out how to make this a composite key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object EditionStudents : Table("editionStudents") {
|
|
||||||
val editionId = reference("edition_id", Editions.id)
|
|
||||||
val studentId = reference("student_id", Students.id)
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(studentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
|
||||||
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
|
|
||||||
val groupId = reference("group_id", Groups.id)
|
|
||||||
val feedback = text("feedback")
|
|
||||||
val grade = varchar("grade", 32)
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(groupId, criterionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
|
|
||||||
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
|
|
||||||
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
|
|
||||||
val groupId = reference("group_id", Groups.id)
|
|
||||||
val studentId = reference("student_id", Students.id)
|
|
||||||
val feedback = text("feedback")
|
|
||||||
val grade = varchar("grade", 32)
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(studentId, criterionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
|
||||||
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
|
|
||||||
val criterionId = reference("criterion_id", SoloAssignmentCriteria.id)
|
|
||||||
val studentId = reference("student_id", Students.id)
|
|
||||||
val feedback = text("feedback")
|
|
||||||
val grade = varchar("grade", 32)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,64 @@
|
|||||||
package com.jaytux.grader.data
|
package com.jaytux.grader.data
|
||||||
|
|
||||||
import MigrationUtils
|
import com.jaytux.grader.app
|
||||||
import org.jetbrains.exposed.sql.Database
|
import com.jaytux.grader.data.v2.CategoricGrade
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import com.jaytux.grader.data.v2.CategoricGradeOption
|
||||||
import org.jetbrains.exposed.sql.and
|
import com.jaytux.grader.data.v2.CategoricGradeOptions
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import com.jaytux.grader.data.v2.CategoricGrades
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import com.jaytux.grader.data.v2.Courses
|
||||||
import org.jetbrains.exposed.sql.update
|
import com.jaytux.grader.data.v2.NumericGrade
|
||||||
|
import com.jaytux.grader.data.v2.v2Tables
|
||||||
|
import dev.dirs.ProjectDirectories
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import kotlin.getValue
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
|
|
||||||
object Database {
|
object Database {
|
||||||
val db by lazy {
|
val dataDir: String = ProjectDirectories.from("com", "jaytux", "grader").dataDir.also {
|
||||||
val actual = Database.connect("jdbc:sqlite:file:./grader.db", "org.sqlite.JDBC")
|
val path = Path(it)
|
||||||
transaction {
|
if(!path.exists()) path.createDirectories()
|
||||||
SchemaUtils.create(
|
|
||||||
Courses, Editions, Groups,
|
|
||||||
Students, GroupStudents, EditionStudents,
|
|
||||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
|
||||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
|
||||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
|
||||||
StudentToGroupEvaluation
|
|
||||||
)
|
|
||||||
|
|
||||||
val migrate = MigrationUtils.statementsRequiredForDatabaseMigration(
|
|
||||||
Courses, Editions, Groups,
|
|
||||||
Students, GroupStudents, EditionStudents,
|
|
||||||
GroupAssignments, SoloAssignments, GroupAssignmentCriteria, SoloAssignmentCriteria,
|
|
||||||
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks,
|
|
||||||
PeerEvaluations, PeerEvaluationContents, StudentToStudentEvaluation,
|
|
||||||
StudentToGroupEvaluation,
|
|
||||||
withLogs = true
|
|
||||||
)
|
|
||||||
|
|
||||||
println(" --- Migration --- ")
|
|
||||||
migrate.forEach { println(it); exec(it) }
|
|
||||||
println(" --- End migration --- ")
|
|
||||||
}
|
}
|
||||||
|
val db by lazy {
|
||||||
|
val actual = Database.connect("jdbc:sqlite:file:${dataDir}/grader.db", "org.sqlite.JDBC")
|
||||||
|
transaction(actual) {
|
||||||
|
SchemaUtils.create(*v2Tables)
|
||||||
|
}
|
||||||
|
|
||||||
actual
|
actual
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init() { db }
|
fun init() {
|
||||||
|
TransactionManager.defaultDatabase = db
|
||||||
|
transaction {
|
||||||
|
if(CategoricGrade.count() == 0L) {
|
||||||
|
val (pf, af) = CategoricGrades.batchInsert(listOf("Pass/Fail", "Default A-F"), shouldReturnGeneratedValues = true) {
|
||||||
|
this[CategoricGrades.name] = it
|
||||||
|
}.map {
|
||||||
|
it[CategoricGrades.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoricGradeOptions.batchInsert(
|
||||||
|
listOf("Pass", "Fail").mapIndexed { idx, it -> it to pf app idx } +
|
||||||
|
listOf("A (Excellent)", "B (Good)", "C (Satisfactory)", "D (Poor)", "F (Fail)").mapIndexed { idx, it -> it to af app idx }
|
||||||
|
) {
|
||||||
|
this[CategoricGradeOptions.option] = it.first
|
||||||
|
this[CategoricGradeOptions.gradeId] = it.second
|
||||||
|
this[CategoricGradeOptions.index] = it.third
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(NumericGrade.count() == 0L) {
|
||||||
|
NumericGrade.new {
|
||||||
|
name = "Max-20"
|
||||||
|
max = 20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
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.EntityID
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class Course(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, Course>(Courses)
|
|
||||||
|
|
||||||
var name by Courses.name
|
|
||||||
val editions by Edition referrersOn Editions.courseId
|
|
||||||
}
|
|
||||||
|
|
||||||
class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, Edition>(Editions)
|
|
||||||
|
|
||||||
var course by Course referencedOn Editions.courseId
|
|
||||||
var name by Editions.name
|
|
||||||
val groups by Group referrersOn Groups.editionId
|
|
||||||
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) {
|
|
||||||
companion object : EntityClass<UUID, Group>(Groups)
|
|
||||||
|
|
||||||
var edition by Edition referencedOn Groups.editionId
|
|
||||||
var name by Groups.name
|
|
||||||
val students by Student via GroupStudents
|
|
||||||
val studentRoles by GroupMember referrersOn GroupStudents.groupId
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupMember(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, GroupMember>(GroupStudents)
|
|
||||||
|
|
||||||
var student by Student referencedOn GroupStudents.studentId
|
|
||||||
var group by Group referencedOn GroupStudents.groupId
|
|
||||||
var role by GroupStudents.role
|
|
||||||
}
|
|
||||||
|
|
||||||
class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, Student>(Students)
|
|
||||||
|
|
||||||
var name by Students.name
|
|
||||||
var note by Students.note
|
|
||||||
var contact by Students.contact
|
|
||||||
val groups by Group via GroupStudents
|
|
||||||
val courses by Edition via EditionStudents
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion
|
|
||||||
|
|
||||||
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
|
|
||||||
var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion
|
|
||||||
|
|
||||||
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
|
|
||||||
}
|
|
||||||
|
|
||||||
class SoloAssignmentCriterion(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, SoloAssignmentCriterion>(SoloAssignmentCriteria)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = name.hashCode()
|
|
||||||
result = 31 * result + description.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PeerEvaluation(id: EntityID<UUID>) : Entity<UUID>(id) {
|
|
||||||
companion object : EntityClass<UUID, PeerEvaluation>(PeerEvaluations)
|
|
||||||
|
|
||||||
var edition by Edition referencedOn PeerEvaluations.editionId
|
|
||||||
var number by PeerEvaluations.number
|
|
||||||
var name by PeerEvaluations.name
|
|
||||||
}
|
|
||||||
@@ -1,90 +1,118 @@
|
|||||||
package com.jaytux.grader.data
|
package com.jaytux.grader.data
|
||||||
|
|
||||||
import com.jaytux.grader.viewmodel.Assignment
|
//class MdBuilder {
|
||||||
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
// private val content = StringBuilder()
|
||||||
import io.github.vinceglb.filekit.PlatformFile
|
//
|
||||||
|
// fun appendHeader(text: String, level: Int = 1) {
|
||||||
class MdBuilder {
|
// require(level in 1..6) { "Header level must be between 1 and 6" }
|
||||||
private val content = StringBuilder()
|
// content.appendLine()
|
||||||
|
// content.appendLine("#".repeat(level) + " $text")
|
||||||
fun appendHeader(text: String, level: Int = 1) {
|
// content.appendLine()
|
||||||
require(level in 1..6) { "Header level must be between 1 and 6" }
|
// }
|
||||||
content.appendLine()
|
// fun appendMd(text: String) { content.appendLine(text) }
|
||||||
content.appendLine("#".repeat(level) + " $text")
|
// fun appendParagraph(text: String, bold: Boolean = false, italic: Boolean = false) {
|
||||||
content.appendLine()
|
// val formattedText = buildString {
|
||||||
}
|
// if (bold) append("**")
|
||||||
fun appendMd(text: String) { content.appendLine(text) }
|
// if (italic) append("_")
|
||||||
fun appendParagraph(text: String, bold: Boolean = false, italic: Boolean = false) {
|
// append(text)
|
||||||
val formattedText = buildString {
|
// if (italic) append("_")
|
||||||
if (bold) append("**")
|
// if (bold) append("**")
|
||||||
if (italic) append("_")
|
// }
|
||||||
append(text)
|
// content.appendLine(formattedText)
|
||||||
if (italic) append("_")
|
// content.appendLine()
|
||||||
if (bold) append("**")
|
// }
|
||||||
}
|
//
|
||||||
content.appendLine(formattedText)
|
// fun build(title: String, scheme: String = "dark"): String = "${prologue(title, scheme)}\n\n${content.toString()}"
|
||||||
content.appendLine()
|
// private fun prologue(title: String, scheme: String = "dark") = """
|
||||||
}
|
// ---
|
||||||
|
// title: $title
|
||||||
fun build(): String = content.toString()
|
// date: ${Clock.System.now()}
|
||||||
}
|
// header-includes:
|
||||||
|
// - '<link rel="stylesheet" href="https://classless.de/classless-tiny.css" media="(prefers-color-scheme: $scheme)" />'
|
||||||
fun GroupAssignmentState.LocalGFeedback.exportTo(path: PlatformFile, assignment: GroupAssignment) {
|
// - '<link rel="stylesheet" href="https://classless.de/addons/themes.css" media="(prefers-color-scheme: light)" />'
|
||||||
val builder = MdBuilder()
|
// ---
|
||||||
builder.appendHeader("${assignment.name} Feedback for ${group.name}")
|
// """.trimIndent()
|
||||||
if(feedback.global != null && feedback.global.grade.isNotBlank()) {
|
//}
|
||||||
val global = feedback.global.grade
|
//
|
||||||
builder.appendParagraph("Overall grade: ${feedback.global.grade}", true, true)
|
//object Exporter {
|
||||||
|
// private fun MdBuilder.appendGroupFeedback(assignment: GroupAssignment, it: GroupAssignmentState.LocalGFeedback) {
|
||||||
individuals.forEach { (student, it) ->
|
// appendHeader("${assignment.name} (group: ${it.group.name})", 1)
|
||||||
val (_, data) = it
|
// if (it.feedback.global != null && it.feedback.global.grade.isNotBlank()) {
|
||||||
if(data.global != null && data.global.grade.isNotBlank() && data.global.grade != global) {
|
// val global = it.feedback.global.grade
|
||||||
builder.appendParagraph("${student.name} grade: ${data.global.grade}", true, true)
|
// appendParagraph("Overall grade: ${it.feedback.global.grade}", true, true)
|
||||||
}
|
//
|
||||||
}
|
// it.individuals.forEach { (student, it) ->
|
||||||
}
|
// val (_, data) = it
|
||||||
|
// if (data.global != null && data.global.grade.isNotBlank() && data.global.grade != global) {
|
||||||
fun appendFeedback(heading: String, group: GroupAssignmentState.FeedbackEntry?, byStudent: List<Pair<Student, GroupAssignmentState.FeedbackEntry>>) {
|
// appendParagraph("${student.name} grade: ${data.global.grade}", true, true)
|
||||||
if(group != null || byStudent.isNotEmpty()) {
|
// }
|
||||||
builder.appendHeader(heading, 2)
|
// }
|
||||||
if(group != null) {
|
// }
|
||||||
if(group.grade.isNotBlank()) {
|
//
|
||||||
builder.appendParagraph("Group grade: ${group.grade}", true, true)
|
// fun appendFeedback(
|
||||||
}
|
// heading: String,
|
||||||
if(group.feedback.isNotBlank()) {
|
// group: GroupAssignmentState.FeedbackEntry?,
|
||||||
builder.appendMd(group.feedback)
|
// byStudent: List<Pair<Student, GroupAssignmentState.FeedbackEntry>>
|
||||||
}
|
// ) {
|
||||||
}
|
// if (group != null || byStudent.isNotEmpty()) {
|
||||||
|
// appendHeader(heading, 2)
|
||||||
byStudent.forEach { (student, it) ->
|
// if (group != null) {
|
||||||
if(it.grade.isNotBlank() || it.feedback.isNotBlank()) builder.appendHeader(student.name, 3)
|
// if (group.grade.isNotBlank()) {
|
||||||
if(it.grade.isNotBlank()) {
|
// appendParagraph("Group grade: ${group.grade}", true, true)
|
||||||
builder.appendParagraph("Grade: ${it.grade}", true, true)
|
// }
|
||||||
}
|
// if (group.feedback.isNotBlank()) {
|
||||||
if(it.feedback.isNotBlank()) {
|
// appendMd(group.feedback)
|
||||||
builder.appendMd(it.feedback)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
}
|
// byStudent.forEach { (student, it) ->
|
||||||
}
|
// if (it.grade.isNotBlank() || it.feedback.isNotBlank()) appendHeader(student.name, 3)
|
||||||
|
// if (it.grade.isNotBlank()) {
|
||||||
appendFeedback("Overall Feedback", feedback.global,
|
// appendParagraph("Grade: ${it.grade}", true, true)
|
||||||
individuals.mapNotNull { it.second.second.global?.let { g -> it.first to g } }
|
// }
|
||||||
)
|
// if (it.feedback.isNotBlank()) {
|
||||||
|
// appendMd(it.feedback)
|
||||||
val criteria = (feedback.byCriterion.map { (c, _) -> c } +
|
// }
|
||||||
individuals.flatMap { (_, it) -> it.second.byCriterion.map { (c, _) -> c } }).distinctBy { it.id.value }
|
// }
|
||||||
|
// }
|
||||||
criteria.forEach { c ->
|
// }
|
||||||
appendFeedback(
|
//
|
||||||
c.name,
|
// appendFeedback(
|
||||||
feedback.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry,
|
// "Overall Feedback", it.feedback.global,
|
||||||
individuals.mapNotNull { (student, it) ->
|
// it.individuals.mapNotNull { ind -> ind.second.second.global?.let { g -> ind.first to g } }
|
||||||
val entry = it.second.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry
|
// )
|
||||||
entry?.let { student to it }
|
//
|
||||||
}
|
// val criteria = (it.feedback.byCriterion.map { (c, _) -> c } +
|
||||||
)
|
// it.individuals.flatMap { (_, x) -> x.second.byCriterion.map { (c, _) -> c } }).distinctBy { x -> x.id.value }
|
||||||
}
|
//
|
||||||
|
// criteria.forEach { c ->
|
||||||
path.file.writeText(builder.build())
|
// appendFeedback(
|
||||||
}
|
// c.name,
|
||||||
|
// it.feedback.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry,
|
||||||
|
// it.individuals.mapNotNull { (student, s) ->
|
||||||
|
// val entry = s.second.byCriterion.firstOrNull { it.criterion.id == c.id }?.entry
|
||||||
|
// entry?.let { student to it }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun MdBuilder.outputTo(path: Path, title: String) {
|
||||||
|
// val contents = build(title)
|
||||||
|
// val buffer = Buffer()
|
||||||
|
// buffer.write(contents.toByteArray())
|
||||||
|
// SystemFileSystem.sink(path, false).write(buffer, buffer.size)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun GroupAssignmentState.LocalGFeedback.exportTo(path: Path, assignment: GroupAssignment) {
|
||||||
|
// val builder = MdBuilder()
|
||||||
|
// builder.appendGroupFeedback(assignment, this)
|
||||||
|
// builder.outputTo(path, "${assignment.name} (for group ${group.name})")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun GroupAssignmentState.batchExport(dirPath: Path) {
|
||||||
|
// feedback.entities.value.forEach { (_, it) ->
|
||||||
|
// it.exportTo(dirPath / "${it.group.name} (${assignment.name}).md", assignment)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.jaytux.grader.data.v2
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable
|
||||||
|
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
||||||
|
import org.jetbrains.exposed.v1.datetime.datetime
|
||||||
|
|
||||||
|
object Courses : UUIDTable("courses") {
|
||||||
|
val name = varchar("name", 50).uniqueIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
object Editions : UUIDTable("editions") {
|
||||||
|
val courseId = reference("course_id", Courses.id)
|
||||||
|
val name = varchar("name", 50)
|
||||||
|
val archived = bool("archived").default(false)
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(courseId, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Groups : UUIDTable("groups") {
|
||||||
|
val editionId = reference("edition_id", Editions.id)
|
||||||
|
val name = varchar("name", 50)
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(editionId, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Students : UUIDTable("students") {
|
||||||
|
val name = varchar("name", 50)
|
||||||
|
val contact = varchar("contact", 50)
|
||||||
|
val note = text("note")
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupStudents : UUIDTable("grpStudents") {
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val role = varchar("role", 50).nullable()
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(groupId, studentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object EditionStudents : CompositeIdTable("editionStudents") {
|
||||||
|
val editionId = reference("edition_id", Editions.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(editionId, studentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object BaseAssignments : UUIDTable("baseAssgmts") {
|
||||||
|
val editionId = reference("edition_id", Editions.id)
|
||||||
|
val name = varchar("name", 50)
|
||||||
|
val assignment = text("assignment")
|
||||||
|
val globalCriterion = reference("global_crit", Criteria.id)
|
||||||
|
val deadline = datetime("deadline")
|
||||||
|
val number = integer("number").nullable()
|
||||||
|
val type = enumerationByName("type", 20, AssignmentType::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Criteria : UUIDTable("criteria") {
|
||||||
|
val assignmentId = reference("assignment_id", BaseAssignments.id)
|
||||||
|
val name = varchar("name", 50)
|
||||||
|
val desc = text("desc")
|
||||||
|
val gradeType = enumerationByName("grade_type", 20, GradeType::class)
|
||||||
|
val categoricGrade = reference("categoric_grade_id", CategoricGrades.id).nullable()
|
||||||
|
val numericGrade = reference("numeric_grade_id", NumericGrades.id).nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupAssignments : UUIDTable("grpAssgmts") {
|
||||||
|
val baseAssignmentId = reference("base_assignment_id", BaseAssignments.id).uniqueIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
object SoloAssignments : UUIDTable("soloAssgmts") {
|
||||||
|
val baseAssignmentId = reference("base_assignment_id", BaseAssignments.id).uniqueIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
object BaseFeedbacks : UUIDTable("baseFeedbacks") {
|
||||||
|
val criterionId = reference("criterion_id", Criteria.id)
|
||||||
|
val feedback = text("feedback")
|
||||||
|
val gradeFreeText = varchar("grade_text", 32).nullable()
|
||||||
|
val gradeCategoric = reference("grade_categoric", CategoricGradeOptions.id).nullable()
|
||||||
|
val gradeNumeric = double("grade_numeric").nullable()
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(groupId, feedbackId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object StudentOverrideFeedbacks : UUIDTable("studOvrFdbks") {
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
|
||||||
|
val overrides = reference("overrides", GroupFeedbacks.feedbackId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(studentId, feedbackId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PeerEvaluations : UUIDTable("peerEvals") {
|
||||||
|
val baseAssignmentId = reference("base_assignment_id", BaseAssignments.id).uniqueIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
object PeerEvaluationFeedbacks : CompositeIdTable("peerEvalFdbks") {
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(groupId, feedbackId)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PeerEvaluationStudentOverrideFeedbacks : UUIDTable("peerEvalStudOvrFdbks") {
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val feedbackId = reference("feedback_id", BaseFeedbacks.id)
|
||||||
|
val overrides = reference("overrides", BaseFeedbacks.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
object PeerEvaluationS2GEvaluations : UUIDTable("peerEvalS2GEvals") {
|
||||||
|
val peerEvalId = reference("peer_eval_id", PeerEvaluations.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val groupId = reference("group_id", Groups.id)
|
||||||
|
val evaluationId = reference("evaluation_id", BaseFeedbacks.id)
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(peerEvalId, groupId, studentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PeerEvaluationS2SEvaluations : UUIDTable("peerEvalS2SEvals") {
|
||||||
|
val peerEvalId = reference("peer_eval_id", PeerEvaluations.id)
|
||||||
|
val studentId = reference("student_id", Students.id)
|
||||||
|
val evaluatedStudentId = reference("evaluated_student_id", Students.id)
|
||||||
|
val evaluationId = reference("evaluation_id", BaseFeedbacks.id)
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(peerEvalId, studentId, evaluatedStudentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CategoricGrades : UUIDTable("categoricGrades") {
|
||||||
|
val name = varchar("name", 50).uniqueIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
object CategoricGradeOptions : UUIDTable("categoricGradeOpts") {
|
||||||
|
val gradeId = reference("grade_id", CategoricGrades.id)
|
||||||
|
val option = varchar("option", 50)
|
||||||
|
val index = integer("index")
|
||||||
|
|
||||||
|
init {
|
||||||
|
uniqueIndex(gradeId, option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object NumericGrades : UUIDTable("numericGrades") {
|
||||||
|
val name = varchar("name", 50).uniqueIndex()
|
||||||
|
val max = double("max")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GradeType {
|
||||||
|
CATEGORIC, NUMERIC, PERCENTAGE, NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AssignmentType(val display: String) {
|
||||||
|
GROUP("Group Assignment"), SOLO("Individual Assignment"), PEER_EVALUATION("Peer Evaluation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val v2Tables = arrayOf(
|
||||||
|
Courses, Editions, Groups, Students, GroupStudents, EditionStudents, BaseAssignments, Criteria, GroupAssignments,
|
||||||
|
SoloAssignments, BaseFeedbacks, GroupFeedbacks, StudentOverrideFeedbacks, SoloFeedbacks, PeerEvaluations,
|
||||||
|
PeerEvaluationFeedbacks, PeerEvaluationStudentOverrideFeedbacks, PeerEvaluationS2GEvaluations,
|
||||||
|
PeerEvaluationS2SEvaluations, CategoricGrades, CategoricGradeOptions, NumericGrades
|
||||||
|
)
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.jaytux.grader.data.v2
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.dao.Entity
|
||||||
|
import org.jetbrains.exposed.v1.dao.EntityClass
|
||||||
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.v1.dao.java.UUIDEntity
|
||||||
|
import org.jetbrains.exposed.v1.dao.java.UUIDEntityClass
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class Course(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : UUIDEntityClass<Course>(Courses)
|
||||||
|
|
||||||
|
var name by Courses.name
|
||||||
|
|
||||||
|
val editions by Edition referrersOn Editions.courseId orderBy Editions.name
|
||||||
|
}
|
||||||
|
|
||||||
|
class Edition(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, Edition>(Editions)
|
||||||
|
|
||||||
|
var course by Course referencedOn Editions.courseId
|
||||||
|
var name by Editions.name
|
||||||
|
var archived by Editions.archived
|
||||||
|
|
||||||
|
val students by Student via EditionStudents orderBy Students.name
|
||||||
|
val groups by Group referrersOn Groups.editionId orderBy Groups.name
|
||||||
|
val assignments by BaseAssignment referrersOn BaseAssignments.editionId orderBy BaseAssignments.number
|
||||||
|
}
|
||||||
|
|
||||||
|
class Group(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, Group>(Groups)
|
||||||
|
|
||||||
|
var edition by Edition referencedOn Groups.editionId
|
||||||
|
var name by Groups.name
|
||||||
|
|
||||||
|
val students by GroupStudent referrersOn GroupStudents.groupId
|
||||||
|
val feedbacks by BaseFeedback via GroupFeedbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
class Student(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, Student>(Students)
|
||||||
|
|
||||||
|
var name by Students.name
|
||||||
|
var note by Students.note
|
||||||
|
var contact by Students.contact
|
||||||
|
|
||||||
|
val editions by Edition via EditionStudents orderBy Editions.name
|
||||||
|
val groups by GroupStudent referrersOn GroupStudents.studentId
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupStudent(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, GroupStudent>(GroupStudents)
|
||||||
|
|
||||||
|
var student by Student referencedOn GroupStudents.studentId
|
||||||
|
var group by Group referencedOn GroupStudents.groupId
|
||||||
|
var role by GroupStudents.role
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseAssignment(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, BaseAssignment>(BaseAssignments)
|
||||||
|
|
||||||
|
var name by BaseAssignments.name
|
||||||
|
var assignment by BaseAssignments.assignment
|
||||||
|
var globalCriterion by Criterion referencedOn BaseAssignments.globalCriterion
|
||||||
|
var deadline by BaseAssignments.deadline
|
||||||
|
var number by BaseAssignments.number
|
||||||
|
var edition by Edition referencedOn BaseAssignments.editionId
|
||||||
|
var type by BaseAssignments.type
|
||||||
|
|
||||||
|
private val _asGroupAssignment by GroupAssignment referrersOn GroupAssignments.baseAssignmentId
|
||||||
|
private val _asSoloAssignment by SoloAssignment referrersOn SoloAssignments.baseAssignmentId
|
||||||
|
private val _asPeerEvaluation by PeerEvaluation referrersOn PeerEvaluations.baseAssignmentId
|
||||||
|
val asGroupAssignment get() = _asGroupAssignment.singleOrNull()
|
||||||
|
val asSoloAssignment get() = _asSoloAssignment.singleOrNull()
|
||||||
|
val asPeerEvaluation get() = _asPeerEvaluation.singleOrNull()
|
||||||
|
|
||||||
|
val criteria by Criterion referrersOn Criteria.assignmentId orderBy Criteria.name
|
||||||
|
val nonBaseCriteria get() = criteria.filterNot { it.id.value == globalCriterion.id.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupAssignment(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, GroupAssignment>(GroupAssignments)
|
||||||
|
|
||||||
|
var base by BaseAssignment referencedOn GroupAssignments.baseAssignmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoloAssignment(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments)
|
||||||
|
|
||||||
|
var base by BaseAssignment referencedOn SoloAssignments.baseAssignmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerEvaluation(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, PeerEvaluation>(PeerEvaluations)
|
||||||
|
|
||||||
|
var base by BaseAssignment referencedOn PeerEvaluations.baseAssignmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoricGrade(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, CategoricGrade>(CategoricGrades)
|
||||||
|
|
||||||
|
var name by CategoricGrades.name
|
||||||
|
|
||||||
|
val options by CategoricGradeOption referrersOn CategoricGradeOptions.gradeId orderBy CategoricGradeOptions.index
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoricGradeOption(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, CategoricGradeOption>(CategoricGradeOptions)
|
||||||
|
|
||||||
|
var grade by CategoricGrade referencedOn CategoricGradeOptions.gradeId
|
||||||
|
var option by CategoricGradeOptions.option
|
||||||
|
var index by CategoricGradeOptions.index
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumericGrade(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, NumericGrade>(NumericGrades)
|
||||||
|
|
||||||
|
var name by NumericGrades.name
|
||||||
|
var max by NumericGrades.max
|
||||||
|
}
|
||||||
|
|
||||||
|
class Criterion(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, Criterion>(Criteria)
|
||||||
|
|
||||||
|
var assignment by BaseAssignment referencedOn Criteria.assignmentId
|
||||||
|
var name by Criteria.name
|
||||||
|
var desc by Criteria.desc
|
||||||
|
var gradeType by Criteria.gradeType
|
||||||
|
var categoricGrade by CategoricGrade optionalReferencedOn Criteria.categoricGrade
|
||||||
|
var numericGrade by NumericGrade optionalReferencedOn Criteria.numericGrade
|
||||||
|
|
||||||
|
val feedbacks by BaseFeedback referrersOn BaseFeedbacks.criterionId
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, BaseFeedback>(BaseFeedbacks)
|
||||||
|
|
||||||
|
var criterion by Criterion referencedOn BaseFeedbacks.criterionId
|
||||||
|
var feedback by BaseFeedbacks.feedback
|
||||||
|
var gradeFreeText by BaseFeedbacks.gradeFreeText
|
||||||
|
var gradeCategoric by CategoricGradeOption optionalReferencedOn BaseFeedbacks.gradeCategoric
|
||||||
|
var gradeNumeric by BaseFeedbacks.gradeNumeric
|
||||||
|
|
||||||
|
private val _forStudentIfSolo by Student via SoloFeedbacks
|
||||||
|
private val _forGroupIfGroup by Group via GroupFeedbacks
|
||||||
|
private val _forGroupIfPeer by Group via PeerEvaluationFeedbacks
|
||||||
|
|
||||||
|
val asSoloFeedback get() = _forStudentIfSolo.singleOrNull()
|
||||||
|
val asGroupFeedback get() = _forGroupIfGroup.singleOrNull()
|
||||||
|
val asPeerEvaluationFeedback get() = _forGroupIfPeer.singleOrNull()
|
||||||
|
|
||||||
|
val forStudentsOverrideIfGroup by StudentOverrideFeedback referrersOn StudentOverrideFeedbacks.overrides
|
||||||
|
val forStudentsOverrideIfPeer by PeerEvaluationStudentOverrideFeedback referrersOn PeerEvaluationStudentOverrideFeedbacks.overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
class StudentOverrideFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, StudentOverrideFeedback>(StudentOverrideFeedbacks)
|
||||||
|
|
||||||
|
var group by Group referencedOn StudentOverrideFeedbacks.groupId
|
||||||
|
var student by Student referencedOn StudentOverrideFeedbacks.studentId
|
||||||
|
var feedback by BaseFeedback referencedOn StudentOverrideFeedbacks.feedbackId
|
||||||
|
var overrides by BaseFeedback referencedOn StudentOverrideFeedbacks.overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerEvaluationStudentOverrideFeedback(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, PeerEvaluationStudentOverrideFeedback>(PeerEvaluationStudentOverrideFeedbacks)
|
||||||
|
|
||||||
|
var group by Group referencedOn PeerEvaluationStudentOverrideFeedbacks.groupId
|
||||||
|
var student by Student referencedOn PeerEvaluationStudentOverrideFeedbacks.studentId
|
||||||
|
var feedback by BaseFeedback referencedOn PeerEvaluationStudentOverrideFeedbacks.feedbackId
|
||||||
|
var overrides by BaseFeedback referencedOn PeerEvaluationStudentOverrideFeedbacks.overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerEvaluationS2G(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, PeerEvaluationS2G>(PeerEvaluationS2GEvaluations)
|
||||||
|
|
||||||
|
var peerEvaluation by PeerEvaluation referencedOn PeerEvaluationS2GEvaluations.peerEvalId
|
||||||
|
var student by Student referencedOn PeerEvaluationS2GEvaluations.studentId
|
||||||
|
var group by Group referencedOn PeerEvaluationS2GEvaluations.groupId
|
||||||
|
var evaluation by BaseFeedback referencedOn PeerEvaluationS2GEvaluations.evaluationId
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerEvaluationS2S(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||||
|
companion object : EntityClass<UUID, PeerEvaluationS2S>(PeerEvaluationS2SEvaluations)
|
||||||
|
|
||||||
|
var peerEvaluation by PeerEvaluation referencedOn PeerEvaluationS2SEvaluations.peerEvalId
|
||||||
|
var student by Student referencedOn PeerEvaluationS2SEvaluations.studentId
|
||||||
|
var evaluatedStudent by Student referencedOn PeerEvaluationS2SEvaluations.evaluatedStudentId
|
||||||
|
var evaluation by BaseFeedback referencedOn PeerEvaluationS2SEvaluations.evaluationId
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
COURSES {
|
||||||
|
uuid id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
|
||||||
|
EDITIONS {
|
||||||
|
uuid id PK
|
||||||
|
uuid courseId FK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
COURSES ||--o{ EDITIONS : has
|
||||||
|
|
||||||
|
GROUPS {
|
||||||
|
uuid id PK
|
||||||
|
uuid editionId FK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
EDITIONS ||--o{ GROUPS : has
|
||||||
|
|
||||||
|
STUDENTS {
|
||||||
|
uuid id PK
|
||||||
|
string name
|
||||||
|
string contact
|
||||||
|
string note
|
||||||
|
}
|
||||||
|
EDITIONS }o--o{ STUDENTS : "has (through edition_students)"
|
||||||
|
|
||||||
|
GROUP_STUDENTS {
|
||||||
|
uuid id PK
|
||||||
|
uuid groupId FK
|
||||||
|
uuid studentId FK
|
||||||
|
maybe(string) role
|
||||||
|
}
|
||||||
|
STUDENTS ||--o{ GROUP_STUDENTS : belongs_to
|
||||||
|
GROUPS ||--o{ GROUP_STUDENTS : has
|
||||||
|
|
||||||
|
BASE_ASSIGNMENTS {
|
||||||
|
uuid id PK
|
||||||
|
uuid editionId FK
|
||||||
|
string name
|
||||||
|
string assignment
|
||||||
|
uuid globalCriterion FK
|
||||||
|
datetime deadline
|
||||||
|
maybe(int) number
|
||||||
|
}
|
||||||
|
EDITIONS ||--o{ BASE_ASSIGNMENTS : has
|
||||||
|
BASE_ASSIGNMENTS ||--|| CRITERIA : "global/main criterion"
|
||||||
|
|
||||||
|
CRITERIA {
|
||||||
|
uuid id PK
|
||||||
|
uuid assignmentId FK
|
||||||
|
string name
|
||||||
|
string desc
|
||||||
|
GradeType gradeType
|
||||||
|
maybe(uuid) categoricGrade FK
|
||||||
|
maybe(uuid) numericGrade FK
|
||||||
|
}
|
||||||
|
CRITERIA ||--o{ BASE_ASSIGNMENTS : belongs_to
|
||||||
|
CRITERIA o|--o{ CATEGORIC_GRADES : "if categoric"
|
||||||
|
CRITERIA o|--o{ NUMERIC_GRADES : "if numeric"
|
||||||
|
|
||||||
|
GROUP_ASSIGNMENTS {
|
||||||
|
uuid id PK
|
||||||
|
uuid baseAssignmentId FK
|
||||||
|
}
|
||||||
|
BASE_ASSIGNMENTS ||--o{ GROUP_ASSIGNMENTS : is
|
||||||
|
|
||||||
|
SOLO_ASSIGNMENTS {
|
||||||
|
uuid id PK
|
||||||
|
uuid baseAssignmentId FK
|
||||||
|
}
|
||||||
|
BASE_ASSIGNMENTS ||--o{ SOLO_ASSIGNMENTS : is
|
||||||
|
|
||||||
|
BASE_FEEDBACKS {
|
||||||
|
uuid id PK
|
||||||
|
uuid criterionId FK
|
||||||
|
string feedback
|
||||||
|
maybe(string) gradeFreeText
|
||||||
|
maybe(uuid) gradeCategoric FK
|
||||||
|
maybe(double) gradeNumeric
|
||||||
|
}
|
||||||
|
CRITERIA ||--o{ BASE_FEEDBACKS : has
|
||||||
|
GROUPS }o--o{ BASE_FEEDBACKS : "has (through group_feedbacks)"
|
||||||
|
STUDENTS }o--o{ BASE_FEEDBACKS : "has (through solo_feedbacks)"
|
||||||
|
CATEGORIC_GRADES |o--o{ BASE_FEEDBACKS : "if categoric"
|
||||||
|
|
||||||
|
STUDENT_OVERRIDE_FEEDBACKS {
|
||||||
|
uuid id PK
|
||||||
|
uuid groupId FK
|
||||||
|
uuid studentId FK
|
||||||
|
uuid feedbackId FK
|
||||||
|
}
|
||||||
|
GROUPS }o--|| STUDENT_OVERRIDE_FEEDBACKS : "original feedback"
|
||||||
|
STUDENTS }o--|| STUDENT_OVERRIDE_FEEDBACKS : "overridden for"
|
||||||
|
BASE_FEEDBACKS ||--o{ STUDENT_OVERRIDE_FEEDBACKS : has
|
||||||
|
|
||||||
|
PEER_EVALUATIONS {
|
||||||
|
uuid id PK
|
||||||
|
uuid baseAssignmentId FK
|
||||||
|
}
|
||||||
|
BASE_ASSIGNMENTS ||--o{ PEER_EVALUATIONS : has
|
||||||
|
GROUPS }o--o{ PEER_EVALUATIONS : "has (through peer_evaluation_groups)"
|
||||||
|
|
||||||
|
PEER_EVALUATION_STUDENT_OVERRIDE_FEEDBACKS {
|
||||||
|
uuid id PK
|
||||||
|
uuid groupId FK
|
||||||
|
uuid studentId FK
|
||||||
|
uuid feedbackId FK
|
||||||
|
}
|
||||||
|
GROUPS }o--|| PEER_EVALUATION_STUDENT_OVERRIDE_FEEDBACKS : "original feedback"
|
||||||
|
STUDENTS }o--|| PEER_EVALUATION_STUDENT_OVERRIDE_FEEDBACKS : "overridden for"
|
||||||
|
BASE_FEEDBACKS ||--o{ PEER_EVALUATION_STUDENT_OVERRIDE_FEEDBACKS : has
|
||||||
|
|
||||||
|
PEER_EVALUATION_S2G_EVALUATIONS {
|
||||||
|
uuid id PK
|
||||||
|
uuid peerEvalId FK
|
||||||
|
uuid studentId FK
|
||||||
|
uuid groupId FK
|
||||||
|
uuid evaluationId FK
|
||||||
|
}
|
||||||
|
PEER_EVALUATIONS }o--|| PEER_EVALUATION_S2G_EVALUATIONS : has
|
||||||
|
STUDENTS }o--|| PEER_EVALUATION_S2G_EVALUATIONS : "evaluates"
|
||||||
|
GROUPS }o--|| PEER_EVALUATION_S2G_EVALUATIONS : "is evaluated"
|
||||||
|
BASE_FEEDBACKS ||--o{ PEER_EVALUATION_S2G_EVALUATIONS : "evaluation"
|
||||||
|
|
||||||
|
PEER_EVALUATION_S2S_EVALUATIONS {
|
||||||
|
uuid id PK
|
||||||
|
uuid peerEvalId FK
|
||||||
|
uuid studentId FK
|
||||||
|
uuid evaluatedStudentId FK
|
||||||
|
uuid evaluationId FK
|
||||||
|
}
|
||||||
|
PEER_EVALUATIONS }o--|| PEER_EVALUATION_S2S_EVALUATIONS : has
|
||||||
|
STUDENTS }o--|| PEER_EVALUATION_S2S_EVALUATIONS : "evaluates"
|
||||||
|
STUDENTS }o--|| PEER_EVALUATION_S2S_EVALUATIONS : "is evaluated"
|
||||||
|
BASE_FEEDBACKS ||--o{ PEER_EVALUATION_S2S_EVALUATIONS : "evaluation"
|
||||||
|
|
||||||
|
CATEGORIC_GRADES {
|
||||||
|
uuid id PK
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORIC_GRADE_OPTIONS {
|
||||||
|
uuid id PK
|
||||||
|
uuid gradeId FK
|
||||||
|
string option
|
||||||
|
}
|
||||||
|
CATEGORIC_GRADES ||--o{ CATEGORIC_GRADE_OPTIONS : has
|
||||||
|
|
||||||
|
NUMERIC_GRADES {
|
||||||
|
uuid id PK
|
||||||
|
string name
|
||||||
|
double max
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,500 @@
|
|||||||
|
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.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.*
|
||||||
|
import com.jaytux.grader.GroupGrading
|
||||||
|
import com.jaytux.grader.PeerEvalGrading
|
||||||
|
import com.jaytux.grader.SoloGrading
|
||||||
|
import com.jaytux.grader.data.v2.AssignmentType
|
||||||
|
import com.jaytux.grader.viewmodel.EditionVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
import com.jaytux.grader.viewmodel.UiGradeType
|
||||||
|
import com.mohamedrejeb.richeditor.model.rememberRichTextState
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import kotlinx.datetime.format.MonthNames
|
||||||
|
import kotlinx.datetime.format.char
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fillMaxSize()) {
|
||||||
|
val assignments by vm.assignmentList.entities
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val descRtf = rememberRichTextState()
|
||||||
|
val assignment = remember(assignments, focus) {
|
||||||
|
assignments.getOrNull(focus)?.also {
|
||||||
|
scope.launch { descRtf.setMarkdown(it.global.criterion.desc) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatingDeadline by remember { mutableStateOf(false) }
|
||||||
|
var addingRubric by remember { mutableStateOf(false) }
|
||||||
|
var editingRubric by remember { mutableStateOf(-1) }
|
||||||
|
var updatingGrade by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val navToGrading = lambda@{
|
||||||
|
if(assignment == null) return@lambda
|
||||||
|
when(assignment.assignment.type) {
|
||||||
|
AssignmentType.GROUP -> token.navTo(GroupGrading(vm.course, vm.edition, assignment.assignment))
|
||||||
|
AssignmentType.SOLO -> token.navTo(SoloGrading(vm.course, vm.edition, assignment.assignment))
|
||||||
|
AssignmentType.PEER_EVALUATION -> token.navTo(PeerEvalGrading(vm.course, vm.edition, assignment.assignment))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||||
|
ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it ->
|
||||||
|
QuickAssignment(idx, it, vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
|
||||||
|
if (assignment == null) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Text("Select an assignment to see details.", Modifier.padding(10.dp).align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic)
|
||||||
|
Row {
|
||||||
|
Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically))
|
||||||
|
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) {
|
||||||
|
Box(Modifier.clickable { updatingGrade = true }.padding(3.dp)) {
|
||||||
|
Text(when(val t = assignment.global.gradeType){
|
||||||
|
is UiGradeType.Categoric -> t.grade.name
|
||||||
|
UiGradeType.FreeText -> "by free-form grades"
|
||||||
|
is UiGradeType.Numeric -> t.grade.name
|
||||||
|
UiGradeType.Percentage -> "by percentages"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Column(Modifier.weight(0.75f)) {
|
||||||
|
Row {
|
||||||
|
Text("Description:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp).weight(1f))
|
||||||
|
Button({ vm.setDesc(assignment, descRtf.toMarkdown()) }) {
|
||||||
|
Text("Update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
RichTextField(descRtf)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Surface(Modifier.weight(0.25f), color = Color.White) {
|
||||||
|
Column(Modifier.padding(15.dp)) {
|
||||||
|
Row {
|
||||||
|
Text("Grading Rubrics", Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
|
||||||
|
IconButton({ addingRubric = true }) {
|
||||||
|
Icon(CirclePlus, "Add grading rubric")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
itemsIndexed(assignment.criteria) { idx, it ->
|
||||||
|
Row(Modifier.padding(5.dp)) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(it.criterion.name)
|
||||||
|
Text(it.criterion.desc, Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
IconButton({ editingRubric = idx }, Modifier.align(Alignment.Top)) {
|
||||||
|
Icon(Edit, "Edit grading rubric")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Button({ navToGrading() }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Go to grading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(updatingDeadline) {
|
||||||
|
if(assignment == null) updatingDeadline = false
|
||||||
|
else {
|
||||||
|
DeadlinePicker(assignment.assignment.deadline, { updatingDeadline = false }) {
|
||||||
|
vm.modAssignment(assignment.assignment, null, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addingRubric) {
|
||||||
|
if(assignment == null) addingRubric = false
|
||||||
|
else {
|
||||||
|
AddCriterionDialog(null, vm, assignment.criteria.map { it.criterion.name }, { addingRubric = false }) { name, desc, type ->
|
||||||
|
vm.mkCriterion(assignment.assignment, name, desc, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(editingRubric != -1) {
|
||||||
|
if(assignment == null) editingRubric = -1
|
||||||
|
else {
|
||||||
|
AddCriterionDialog(assignment.criteria[editingRubric], vm, assignment.criteria.map { it.criterion.name }, { editingRubric = -1 }) { name, desc, type ->
|
||||||
|
vm.modCriterion(assignment.criteria[editingRubric].criterion, name, desc, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(updatingGrade) {
|
||||||
|
if(assignment == null) updatingGrade = false
|
||||||
|
else {
|
||||||
|
SetGradingDialog(assignment.assignment.name, assignment.global.gradeType, vm, { updatingGrade = false }) { type ->
|
||||||
|
vm.modCriterion(assignment.global.criterion, null, null, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fmt = LocalDateTime.Format {
|
||||||
|
date(LocalDate.Format {
|
||||||
|
day(); char(' '); monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); year()
|
||||||
|
})
|
||||||
|
char(' ')
|
||||||
|
time(LocalTime.Format {
|
||||||
|
amPmHour(); char(':'); minute(); char(' '); amPmMarker("AM", "PM")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun QuickAssignment(idx: Int, assignment: EditionVM.AssignmentData, vm: EditionVM) {
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
|
||||||
|
Text(assignment.assignment.name, fontWeight = FontWeight.Bold)
|
||||||
|
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddAssignmentDialog(label: String, taken: List<String>, onClose: () -> Unit, current: String = "", onSave: (String, AssignmentType) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(750.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
var type by remember { mutableStateOf(AssignmentType.SOLO) }
|
||||||
|
var name by remember(current) { mutableStateOf(current) }
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||||
|
AssignmentType.entries.forEachIndexed { idx, it ->
|
||||||
|
SegmentedButton(
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(idx, AssignmentType.entries.size),
|
||||||
|
selected = type == it,
|
||||||
|
onClick = { type = it }
|
||||||
|
) { Text(it.display) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text(label) }, isError = name in taken)
|
||||||
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
|
onSave(name, type)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { focus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DeadlinePicker(deadline: LocalDateTime, onDismiss: () -> Unit, onSave: (LocalDateTime) -> Unit) {
|
||||||
|
val state = rememberDatePickerState(deadline.date.toJavaLocalDate())
|
||||||
|
val (h, m) = deadline.time.let { it.hour to it.minute }
|
||||||
|
val time = rememberTimePickerState(h, m)
|
||||||
|
|
||||||
|
val reconstruct = {
|
||||||
|
val inst = Instant.fromEpochMilliseconds(state.selectedDateMillis!!)
|
||||||
|
val date = inst.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
LocalDateTime(date.date, LocalTime(time.hour, time.minute))
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismiss, DialogProperties()) {
|
||||||
|
Surface(tonalElevation = 5.dp, shape = MaterialTheme.shapes.extraLarge) {
|
||||||
|
Column(Modifier.padding(15.dp)) {
|
||||||
|
DatePicker(state, Modifier.fillMaxWidth())
|
||||||
|
TimeInput(time, Modifier.fillMaxWidth())
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Button(onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Button({ onSave(reconstruct()); onDismiss() }) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken: List<String>, onClose: () -> Unit, onSave: (name: String, desc: String, type: UiGradeType) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
var type by remember(current) { mutableStateOf(current?.gradeType ?: UiGradeType.FreeText) }
|
||||||
|
var name by remember(current) { mutableStateOf(current?.criterion?.name ?: "") }
|
||||||
|
var desc by remember(current) { mutableStateOf(current?.criterion?.desc ?: "") }
|
||||||
|
val categories by vm.categoricGrades.entities
|
||||||
|
val numeric by vm.numericGrades.entities
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Criterion Name") }, isError = name in taken, singleLine = true)
|
||||||
|
OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true)
|
||||||
|
Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
|
||||||
|
Column {
|
||||||
|
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
|
||||||
|
|
||||||
|
CancelSaveRow(name.isNotBlank() && (name !in taken || name == current?.criterion?.name), onClose) {
|
||||||
|
onSave(name, desc, type)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(current) { focus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose: () -> Unit, onSave: (type: UiGradeType) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
val categories by vm.categoricGrades.entities
|
||||||
|
val numeric by vm.numericGrades.entities
|
||||||
|
var type by remember(current) { mutableStateOf(current) }
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
Text("Select a grading scale for $name", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 10.dp))
|
||||||
|
Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
|
||||||
|
Column {
|
||||||
|
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it }
|
||||||
|
|
||||||
|
CancelSaveRow(true, onClose) {
|
||||||
|
onSave(type)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(current) { focus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GradeTypePicker(
|
||||||
|
type: UiGradeType, categories: List<UiGradeType.Categoric>, numeric: List<UiGradeType.Numeric>,
|
||||||
|
mkCat: (String, List<String>) -> Unit, mkNum: (String, Double) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onUpdate: (UiGradeType) -> Unit
|
||||||
|
) = Column(modifier) {
|
||||||
|
var selectedCategory by remember(categories) {
|
||||||
|
mutableStateOf(
|
||||||
|
if(type is UiGradeType.Categoric) categories.indexOfFirst { it.grade.id == type.grade.id }
|
||||||
|
else -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var selectedNumeric by remember(numeric) {
|
||||||
|
mutableStateOf(
|
||||||
|
if(type is UiGradeType.Numeric) numeric.indexOfFirst { it.grade.id == type.grade.id }
|
||||||
|
else -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var adding by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||||
|
SegmentedButton(
|
||||||
|
type is UiGradeType.FreeText, onClick = { onUpdate(UiGradeType.FreeText) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(0, 4)
|
||||||
|
) {
|
||||||
|
Text("Free-form grade")
|
||||||
|
}
|
||||||
|
SegmentedButton(
|
||||||
|
type is UiGradeType.Percentage, onClick = { onUpdate(UiGradeType.Percentage) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(1, 4)
|
||||||
|
) {
|
||||||
|
Text("Percentage")
|
||||||
|
}
|
||||||
|
SegmentedButton(
|
||||||
|
type is UiGradeType.Categoric, onClick = { onUpdate(categories[maxOf(selectedCategory, 0)]) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(2, 4)
|
||||||
|
) {
|
||||||
|
Text("Grading System")
|
||||||
|
}
|
||||||
|
SegmentedButton(
|
||||||
|
type is UiGradeType.Numeric, onClick = { onUpdate(numeric[maxOf(selectedNumeric, 0)]) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(3, 4)
|
||||||
|
) {
|
||||||
|
Text("Numeric Grade")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(type as? UiGradeType.Categoric)?.let {
|
||||||
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
itemsIndexed(categories) { idx, it ->
|
||||||
|
Surface(
|
||||||
|
tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
|
||||||
|
Text(it.grade.name, fontWeight = FontWeight.Bold)
|
||||||
|
Text(
|
||||||
|
"(${it.options.size} options)",
|
||||||
|
Modifier.padding(start = 10.dp),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Button({ adding = true }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Add grading system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: (type as? UiGradeType.Numeric)?.let {
|
||||||
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
itemsIndexed(numeric) { idx, it ->
|
||||||
|
Surface(
|
||||||
|
tonalElevation = if (selectedNumeric == idx) 15.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { selectedNumeric = idx; onUpdate(it) }.padding(10.dp)) {
|
||||||
|
Text(it.grade.name, fontWeight = FontWeight.Bold)
|
||||||
|
Text(
|
||||||
|
"(graded as X/${it.grade.max})",
|
||||||
|
Modifier.padding(start = 10.dp),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Button({ adding = true }, Modifier.fillMaxWidth()) {
|
||||||
|
Text("Add numeric system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
if(adding) {
|
||||||
|
when(type) {
|
||||||
|
is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options ->
|
||||||
|
mkCat(name, options)
|
||||||
|
}
|
||||||
|
is UiGradeType.Numeric -> AddNumScaleDialog(numeric.map { it.grade.name }, { adding = false }) { name, max ->
|
||||||
|
mkNum(name, max)
|
||||||
|
}
|
||||||
|
else -> adding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String, List<String>) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var options by remember { mutableStateOf(listOf<String>()) }
|
||||||
|
var adding by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Grading system name") }, isError = name in taken, singleLine = true)
|
||||||
|
Text("Grade options:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp))
|
||||||
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
itemsIndexed(options) { idx, it ->
|
||||||
|
Row(Modifier.fillMaxWidth().padding(5.dp)) {
|
||||||
|
Text(it, Modifier.weight(1f))
|
||||||
|
IconButton({ options = options.filterNot { o -> o == it } }) {
|
||||||
|
Icon(Delete, "Delete grading option")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Row {
|
||||||
|
OutlinedTextField(adding, { adding = it }, Modifier.weight(1f).align(Alignment.CenterVertically).padding(5.dp), label = { Text("New option") }, isError = adding in options, singleLine = true)
|
||||||
|
Button({ options = options + adding; adding = "" }, Modifier.align(Alignment.CenterVertically).padding(5.dp), enabled = adding.isNotBlank() && adding !in options) {
|
||||||
|
Text("Add")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
|
onSave(name, options)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { focus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddNumScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String, Double) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(750.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var maxStr by remember { mutableStateOf("0") }
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text("Grading system name") }, isError = name in taken, singleLine = true)
|
||||||
|
OutlinedTextField(maxStr, { maxStr = it.toDoubleOrNull()?.let { _ -> it } ?: "0" }, Modifier.fillMaxWidth(), label = { Text("Maximum grade") }, singleLine = true)
|
||||||
|
|
||||||
|
CancelSaveRow(name.isNotBlank() && name !in taken && (maxStr.toDoubleOrNull() ?: 0.0) > 0.0, onClose) {
|
||||||
|
onSave(name, maxStr.toDoubleOrNull()!!)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { focus.requestFocus() }
|
||||||
|
}
|
||||||
@@ -1,109 +1,87 @@
|
|||||||
package com.jaytux.grader.ui
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
//@Composable
|
||||||
import androidx.compose.foundation.layout.*
|
//fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
||||||
import androidx.compose.material.icons.Icons
|
// val data by state.courses.entities
|
||||||
import androidx.compose.material.icons.filled.Delete
|
// var showDialog by remember { mutableStateOf(false) }
|
||||||
import androidx.compose.material.icons.filled.Edit
|
//
|
||||||
import androidx.compose.material3.Icon
|
// Box(Modifier.padding(15.dp)) {
|
||||||
import androidx.compose.material3.IconButton
|
// ListOrEmpty(
|
||||||
import androidx.compose.material3.Button
|
// data,
|
||||||
import androidx.compose.material3.MaterialTheme
|
// { Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
||||||
import androidx.compose.material3.Surface
|
// { Text("Add a course") },
|
||||||
import androidx.compose.material3.Text
|
// { showDialog = true },
|
||||||
import androidx.compose.runtime.*
|
// addAfterLazy = false
|
||||||
import androidx.compose.ui.Alignment
|
// ) { _, it ->
|
||||||
import androidx.compose.ui.Modifier
|
// CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
// }
|
||||||
import androidx.compose.ui.unit.dp
|
// }
|
||||||
import com.jaytux.grader.UiRoute
|
//
|
||||||
import com.jaytux.grader.data.Edition
|
// if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
import com.jaytux.grader.viewmodel.CourseListState
|
//}
|
||||||
import com.jaytux.grader.viewmodel.EditionListState
|
//
|
||||||
import com.jaytux.grader.viewmodel.EditionState
|
//@Composable
|
||||||
|
//fun CourseWidget(state: EditionListState, onDelete: () -> Unit, push: (UiRoute) -> Unit) {
|
||||||
@Composable
|
// val editions by state.editions.entities
|
||||||
fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
// var isOpened by remember { mutableStateOf(false) }
|
||||||
val data by state.courses.entities
|
// var showDialog by remember { mutableStateOf(false) }
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
//
|
||||||
|
// val callback = { it: Edition ->
|
||||||
Box(Modifier.padding(15.dp)) {
|
// val s = EditionState(it)
|
||||||
ListOrEmpty(
|
// val route = UiRoute("${state.course.name}: ${it.name}") {
|
||||||
data,
|
// EditionView(s)
|
||||||
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
// }
|
||||||
{ Text("Add a course") },
|
// push(route)
|
||||||
{ showDialog = true },
|
// }
|
||||||
addAfterLazy = false
|
//
|
||||||
) { _, it ->
|
// Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { isOpened = !isOpened }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
||||||
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
// Row {
|
||||||
}
|
// Column(Modifier.weight(1f).padding(5.dp)) {
|
||||||
}
|
// Row {
|
||||||
|
// Icon(
|
||||||
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
// if (isOpened) ChevronDown else ChevronRight, "Toggle editions",
|
||||||
}
|
// Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp())
|
||||||
|
// .align(Alignment.CenterVertically)
|
||||||
@Composable
|
// )
|
||||||
fun CourseWidget(state: EditionListState, onDelete: () -> Unit, push: (UiRoute) -> Unit) {
|
// Column {
|
||||||
val editions by state.editions.entities
|
// Text(state.course.name, style = MaterialTheme.typography.headlineMedium)
|
||||||
var isOpened by remember { mutableStateOf(false) }
|
// }
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
// }
|
||||||
|
// Row {
|
||||||
val callback = { it: Edition ->
|
// Spacer(Modifier.width(25.dp))
|
||||||
val s = EditionState(it)
|
// Text(
|
||||||
val route = UiRoute("${state.course.name}: ${it.name}") {
|
// "${editions.size} edition(s)",
|
||||||
EditionView(s)
|
// fontStyle = FontStyle.Italic,
|
||||||
}
|
// style = MaterialTheme.typography.bodySmall
|
||||||
push(route)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { isOpened = !isOpened }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
// if(isOpened) {
|
||||||
Row {
|
// Row {
|
||||||
Column(Modifier.weight(1f).padding(5.dp)) {
|
// Spacer(Modifier.width(25.dp))
|
||||||
Row {
|
// Column {
|
||||||
Icon(
|
// editions.forEach { EditionWidget(it, { callback(it) }) { state.delete(it) } }
|
||||||
if (isOpened) ChevronDown else ChevronRight, "Toggle editions",
|
// Button({ showDialog = true }, Modifier.fillMaxWidth()) { Text("Add edition") }
|
||||||
Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp())
|
// }
|
||||||
.align(Alignment.CenterVertically)
|
// }
|
||||||
)
|
// }
|
||||||
Column {
|
// }
|
||||||
Text(state.course.name, style = MaterialTheme.typography.headlineMedium)
|
// Column {
|
||||||
}
|
// IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
||||||
}
|
// IconButton({ TODO() }, enabled = false) { Icon(Icons.Default.Edit, "Edit") }
|
||||||
Row {
|
// }
|
||||||
Spacer(Modifier.width(25.dp))
|
// }
|
||||||
Text(
|
// }
|
||||||
"${editions.size} edition(s)",
|
//
|
||||||
fontStyle = FontStyle.Italic,
|
// if(showDialog) AddStringDialog("Edition name", editions.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
style = MaterialTheme.typography.bodySmall
|
//}
|
||||||
)
|
//
|
||||||
}
|
//@Composable
|
||||||
|
//fun EditionWidget(edition: Edition, onOpen: () -> Unit, onDelete: () -> Unit) {
|
||||||
if(isOpened) {
|
// Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { onOpen() }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
||||||
Row {
|
// Row(Modifier.padding(5.dp)) {
|
||||||
Spacer(Modifier.width(25.dp))
|
// Text(edition.name, Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
|
||||||
Column {
|
// IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
||||||
editions.forEach { EditionWidget(it, { callback(it) }) { state.delete(it) } }
|
// }
|
||||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) { Text("Add edition") }
|
// }
|
||||||
}
|
//}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
|
||||||
IconButton({ TODO() }, enabled = false) { Icon(Icons.Default.Edit, "Edit") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(showDialog) AddStringDialog("Edition name", editions.map { it.name }, { showDialog = false }) { state.new(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EditionWidget(edition: Edition, onOpen: () -> Unit, onDelete: () -> Unit) {
|
|
||||||
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp).clickable { onOpen() }, shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
|
|
||||||
Row(Modifier.padding(5.dp)) {
|
|
||||||
Text(edition.name, Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall)
|
|
||||||
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.EditionDetail
|
||||||
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import com.jaytux.grader.viewmodel.EditionVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditionTitle(data: EditionDetail) = Text("Courses / ${data.course.name} / ${data.ed.name}")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditionView(data: EditionDetail, token: Navigator.NavToken) {
|
||||||
|
val vm = viewModel<EditionVM>(key = data.ed.id.toString()) { EditionVM(data.ed, data.course) }
|
||||||
|
val tab by vm.selectedTab
|
||||||
|
var adding by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val groups by vm.groupList.entities
|
||||||
|
val assignments by vm.assignmentList.entities
|
||||||
|
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Row {
|
||||||
|
Text("${vm.course.name} - ${vm.edition.name}", Modifier.weight(1f), style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Button({ adding = true }) {
|
||||||
|
Icon(CirclePlus, "Add ${tab.addText}")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Add ${tab.addText}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
PrimaryScrollableTabRow(tab.ordinal, edgePadding = 10.dp) {
|
||||||
|
EditionVM.Tab.entries.forEach {
|
||||||
|
Tab(tab == it, onClick = { vm.switchTo(it) }, modifier = Modifier.padding(horizontal = 5.dp)) { it.renderTab() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(1f)) {
|
||||||
|
when (tab) {
|
||||||
|
EditionVM.Tab.STUDENTS -> StudentsView(vm)
|
||||||
|
EditionVM.Tab.GROUPS -> GroupsView(vm)
|
||||||
|
EditionVM.Tab.ASSIGNMENTS -> AssignmentsView(vm, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(adding) {
|
||||||
|
when(tab) {
|
||||||
|
EditionVM.Tab.STUDENTS ->
|
||||||
|
AddStringDialog("Student Name", listOf(), { adding = false }, "") { vm.mkStudent(it, "", "") }
|
||||||
|
EditionVM.Tab.GROUPS ->
|
||||||
|
AddStringDialog("Group Name", groups.map { it.group.name }, { adding = false }, "") { vm.mkGroup(it) }
|
||||||
|
EditionVM.Tab.ASSIGNMENTS ->
|
||||||
|
AddAssignmentDialog("Assignment Name", assignments.map { it.assignment.name }, { adding = false }, "") { name, type -> vm.mkAssignment(name, type) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
|
||||||
|
Icon(UserIcon, "Students")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Students")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
|
||||||
|
Icon(UserGroupIcon, "Groups")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Groups")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AssignmentsTabHeader() = Row(Modifier.padding(all = 5.dp)) {
|
||||||
|
Icon(AssignmentIcon, "Assignments")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Assignments")
|
||||||
|
}
|
||||||
@@ -1,419 +1,409 @@
|
|||||||
package com.jaytux.grader.ui
|
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.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.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
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.Course
|
|
||||||
import com.jaytux.grader.data.Edition
|
|
||||||
import com.jaytux.grader.data.Group
|
|
||||||
import com.jaytux.grader.data.Student
|
|
||||||
import com.jaytux.grader.viewmodel.*
|
|
||||||
|
|
||||||
data class Navigators(
|
//data class Navigators(
|
||||||
val student: (Student) -> Unit,
|
// val student: (Student) -> Unit,
|
||||||
val group: (Group) -> Unit,
|
// val group: (Group) -> Unit,
|
||||||
val assignment: (Assignment) -> Unit
|
// val assignment: (Assignment) -> Unit
|
||||||
)
|
//)
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
//fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||||
val course = state.course; val edition = state.edition
|
// val course = state.course; val edition = state.edition
|
||||||
val students by state.students.entities
|
// val students by state.students.entities
|
||||||
val availableStudents by state.availableStudents.entities
|
// val availableStudents by state.availableStudents.entities
|
||||||
val groups by state.groups.entities
|
// val groups by state.groups.entities
|
||||||
val solo by state.solo.entities
|
// val solo by state.solo.entities
|
||||||
val groupAs by state.groupAs.entities
|
// val groupAs by state.groupAs.entities
|
||||||
val peers by state.peer.entities
|
// val peers by state.peer.entities
|
||||||
val mergedAssignments by remember(solo, groupAs, peers) { mutableStateOf(Assignment.merge(groupAs, solo, peers)) }
|
// val mergedAssignments by remember(solo, groupAs, peers) { mutableStateOf(Assignment.merge(groupAs, solo, peers)) }
|
||||||
val hist by state.history
|
// val hist by state.history
|
||||||
|
//
|
||||||
val navs = Navigators(
|
// val scope = rememberCoroutineScope()
|
||||||
student = { state.navTo(OpenPanel.Student, students.indexOfFirst{ s -> s.id == it.id }) },
|
//
|
||||||
group = { state.navTo(OpenPanel.Group, groups.indexOfFirst { g -> g.id == it.id }) },
|
// var groupExporting by remember { mutableStateOf<GroupAssignmentState?>(null) }
|
||||||
assignment = { state.navTo(OpenPanel.Assignment, mergedAssignments.indexOfFirst { a -> a.id() == it.id() }) }
|
// val groupPopup = rememberDirectoryPickerLauncher(directory = PlatformFile(Preferences.exportPath)) { path ->
|
||||||
)
|
// if(path != null) {
|
||||||
|
// groupExporting?.let {
|
||||||
val (id, tab) = hist.last()
|
// Preferences.exportPath = path.toKotlinxIoPath().toString()
|
||||||
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
// scope.launch { it.batchExport(path.toKotlinxIoPath()) }
|
||||||
TabLayout(
|
// }
|
||||||
OpenPanel.entries,
|
// }
|
||||||
tab.ordinal,
|
// }
|
||||||
{ state.navTo(OpenPanel.entries[it]) },
|
//
|
||||||
{ Text(it.tabName) }
|
// val navs = Navigators(
|
||||||
) {
|
// student = { state.navTo(OpenPanel.Student, students.indexOfFirst{ s -> s.id == it.id }) },
|
||||||
when(tab) {
|
// group = { state.navTo(OpenPanel.Group, groups.indexOfFirst { g -> g.id == it.id }) },
|
||||||
OpenPanel.Student -> StudentPanel(
|
// assignment = { state.navTo(OpenPanel.Assignment, mergedAssignments.indexOfFirst { a -> a.id() == it.id() }) }
|
||||||
course, edition, students, availableStudents, id,
|
// )
|
||||||
{ state.navTo(it) },
|
//
|
||||||
{ name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
// val (id, tab) = hist.last()
|
||||||
{ students -> state.addToCourse(students) },
|
// Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
||||||
{ s, name -> state.setStudentName(s, name) }
|
// TabLayout(
|
||||||
) { s, idx -> state.delete(s); if(id == idx) state.clearHistoryIndex() }
|
// OpenPanel.entries,
|
||||||
|
// tab.ordinal,
|
||||||
OpenPanel.Group -> GroupPanel(
|
// { state.navTo(OpenPanel.entries[it]) },
|
||||||
course, edition, groups, id,
|
// { Text(it.tabName) }
|
||||||
{ state.navTo(it) },
|
// ) {
|
||||||
{ name -> state.newGroup(name) },
|
// when(tab) {
|
||||||
{ g, name -> state.setGroupName(g, name) }
|
// OpenPanel.Student -> StudentPanel(
|
||||||
) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
|
// course, edition, students, availableStudents, id,
|
||||||
|
// { state.navTo(it) },
|
||||||
OpenPanel.Assignment -> AssignmentPanel(
|
// { name, note, contact, add -> state.newStudent(name, contact, note, add) },
|
||||||
course, edition, mergedAssignments, id,
|
// { students -> state.addToCourse(students) },
|
||||||
{ state.navTo(it) },
|
// { s, name -> state.setStudentName(s, name) }
|
||||||
{ type, name -> state.newAssignment(type, name) },
|
// ) { s, idx -> state.delete(s); if(id == idx) state.clearHistoryIndex() }
|
||||||
{ a, name -> state.setAssignmentTitle(a, name) },
|
//
|
||||||
{ a1, a2 -> state.swapOrder(a1, a2) }
|
// OpenPanel.Group -> GroupPanel(
|
||||||
) { a, idx -> state.delete(a); if(id == idx) state.clearHistoryIndex() }
|
// course, edition, groups, id,
|
||||||
}
|
// { state.navTo(it) },
|
||||||
}
|
// { name -> state.newGroup(name) },
|
||||||
}
|
// { g, name -> state.setGroupName(g, name) }
|
||||||
|
// ) { g, idx -> state.delete(g); if(id == idx) state.clearHistoryIndex() }
|
||||||
Column(Modifier.weight(0.75f)) {
|
//
|
||||||
Row {
|
// OpenPanel.Assignment -> AssignmentPanel(
|
||||||
IconButton({ state.back() }, enabled = hist.size >= 2) {
|
// course, edition, mergedAssignments, id,
|
||||||
Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp()).align(Alignment.CenterVertically))
|
// { state.navTo(it) },
|
||||||
}
|
// { type, name -> state.newAssignment(type, name) },
|
||||||
when(tab) {
|
// { a, name -> state.setAssignmentTitle(a, name) },
|
||||||
OpenPanel.Student -> {
|
// { a1, a2 -> state.swapOrder(a1, a2) }
|
||||||
if(id == -1) PaneHeader("Nothing selected", "students", course, edition)
|
// ) { a, idx -> state.delete(a); if(id == idx) state.clearHistoryIndex() }
|
||||||
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)
|
// Column(Modifier.weight(0.75f)) {
|
||||||
}
|
// Row {
|
||||||
OpenPanel.Assignment -> {
|
// IconButton({ state.back() }, enabled = hist.size >= 2) {
|
||||||
if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition)
|
// Icon(ChevronLeft, "Back", Modifier.size(MaterialTheme.typography.headlineMedium.fontSize.toDp()).align(Alignment.CenterVertically))
|
||||||
else {
|
// }
|
||||||
when(val a = mergedAssignments[id]) {
|
// when(tab) {
|
||||||
is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
|
// OpenPanel.Student -> {
|
||||||
is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition)
|
// if(id == -1) PaneHeader("Nothing selected", "students", course, edition)
|
||||||
is Assignment.PeerEval -> PaneHeader(a.name(), "peer evaluation", 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(1f)) {
|
// OpenPanel.Assignment -> {
|
||||||
if (id != -1) {
|
// if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition)
|
||||||
when (tab) {
|
// else {
|
||||||
OpenPanel.Student -> StudentView(StudentState(students[id], edition), navs)
|
// when(val a = mergedAssignments[id]) {
|
||||||
OpenPanel.Group -> GroupView(GroupState(groups[id]), navs)
|
// is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
|
||||||
OpenPanel.Assignment -> {
|
// is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition) {
|
||||||
when (val a = mergedAssignments[id]) {
|
// groupExporting = GroupAssignmentState(a.assignment); groupPopup.launch()
|
||||||
is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
|
// }
|
||||||
is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
|
// is Assignment.PeerEval -> PaneHeader(a.name(), "peer evaluation", course, edition)
|
||||||
is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation))
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// Box(Modifier.weight(1f)) {
|
||||||
}
|
// if (id != -1) {
|
||||||
}
|
// when (tab) {
|
||||||
|
// OpenPanel.Student -> StudentView(StudentState(students[id], edition), navs)
|
||||||
@Composable
|
// OpenPanel.Group -> GroupView(GroupState(groups[id]), navs)
|
||||||
fun StudentPanel(
|
// OpenPanel.Assignment -> {
|
||||||
course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
// when (val a = mergedAssignments[id]) {
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
// is Assignment.SAssignment -> SoloAssignmentView(SoloAssignmentState(a.assignment))
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
// is Assignment.GAssignment -> GroupAssignmentView(GroupAssignmentState(a.assignment))
|
||||||
onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student, Int) -> Unit
|
// is Assignment.PeerEval -> PeerEvaluationView(PeerEvaluationState(a.evaluation))
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
// }
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
// }
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
// }
|
||||||
var editing by remember { mutableStateOf(-1) }
|
// }
|
||||||
|
// }
|
||||||
Text("Student list (${students.size})", style = MaterialTheme.typography.headlineMedium)
|
// }
|
||||||
|
//}
|
||||||
ListOrEmpty(
|
//
|
||||||
students,
|
//@Composable
|
||||||
{ Text(
|
//fun StudentPanel(
|
||||||
"Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
// course: Course, edition: Edition, students: List<Student>, available: List<Student>,
|
||||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
// selected: Int, onSelect: (Int) -> Unit,
|
||||||
) },
|
// onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit,
|
||||||
{ Text("Add a student") },
|
// onImport: (List<Student>) -> Unit, onUpdate: (Student, String) -> Unit, onDelete: (Student, Int) -> Unit
|
||||||
{ showDialog = true }
|
//) = Column(Modifier.padding(10.dp)) {
|
||||||
) { idx, it ->
|
// var showDialog by remember { mutableStateOf(false) }
|
||||||
SelectEditDeleteRow(
|
// var deleting by remember { mutableStateOf(-1) }
|
||||||
selected == idx,
|
// var editing by remember { mutableStateOf(-1) }
|
||||||
{ onSelect(idx) }, { onSelect(-1) },
|
//
|
||||||
{ editing = idx }, { deleting = idx }
|
// Text("Student list (${students.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
) {
|
//
|
||||||
Text(it.name, Modifier.padding(5.dp))
|
// ListOrEmpty(
|
||||||
}
|
// students,
|
||||||
}
|
// { Text(
|
||||||
|
// "Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
||||||
if(showDialog) {
|
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
StudentDialog(course, edition, { showDialog = false }, available, onImport, onAdd)
|
// ) },
|
||||||
}
|
// { Text("Add a student") },
|
||||||
else if(editing != -1) {
|
// { showDialog = true }
|
||||||
AddStringDialog("Student name", students.map { it.name }, { editing = -1 }, students[editing].name) {
|
// ) { idx, it ->
|
||||||
onUpdate(students[editing], it)
|
// SelectEditDeleteRow(
|
||||||
}
|
// selected == idx,
|
||||||
}
|
// { onSelect(idx) }, { onSelect(-1) },
|
||||||
else if(deleting != -1) {
|
// { editing = idx }, { deleting = idx }
|
||||||
ConfirmDeleteDialog(
|
// ) {
|
||||||
"a student",
|
// Text(it.name, Modifier.padding(5.dp))
|
||||||
{ deleting = -1 },
|
// }
|
||||||
{ onDelete(students[deleting], deleting) }
|
// }
|
||||||
) { Text(students[deleting].name) }
|
//
|
||||||
}
|
// if(showDialog) {
|
||||||
}
|
// StudentDialog(course, edition, { showDialog = false }, available, onImport, onAdd)
|
||||||
|
// }
|
||||||
@Composable
|
// else if(editing != -1) {
|
||||||
fun GroupPanel(
|
// AddStringDialog("Student name", students.map { it.name }, { editing = -1 }, students[editing].name) {
|
||||||
course: Course, edition: Edition, groups: List<Group>,
|
// onUpdate(students[editing], it)
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
// }
|
||||||
onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group, Int) -> Unit
|
// }
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
// else if(deleting != -1) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
// ConfirmDeleteDialog(
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
// "a student",
|
||||||
var editing by remember { mutableStateOf(-1) }
|
// { deleting = -1 },
|
||||||
|
// { onDelete(students[deleting], deleting) }
|
||||||
Text("Group list (${groups.size})", style = MaterialTheme.typography.headlineMedium)
|
// ) { Text(students[deleting].name) }
|
||||||
|
// }
|
||||||
ListOrEmpty(
|
//}
|
||||||
groups,
|
//
|
||||||
{ Text(
|
//@Composable
|
||||||
"Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
//fun GroupPanel(
|
||||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
// course: Course, edition: Edition, groups: List<Group>,
|
||||||
) },
|
// selected: Int, onSelect: (Int) -> Unit,
|
||||||
{ Text("Add a group") },
|
// onAdd: (String) -> Unit, onUpdate: (Group, String) -> Unit, onDelete: (Group, Int) -> Unit
|
||||||
{ showDialog = true }
|
//) = Column(Modifier.padding(10.dp)) {
|
||||||
) { idx, it ->
|
// var showDialog by remember { mutableStateOf(false) }
|
||||||
SelectEditDeleteRow(
|
// var deleting by remember { mutableStateOf(-1) }
|
||||||
selected == idx,
|
// var editing by remember { mutableStateOf(-1) }
|
||||||
{ onSelect(idx) }, { onSelect(-1) },
|
//
|
||||||
{ editing = idx }, { deleting = idx }
|
// Text("Group list (${groups.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
) {
|
//
|
||||||
Text(it.name, Modifier.padding(5.dp))
|
// ListOrEmpty(
|
||||||
}
|
// groups,
|
||||||
}
|
// { Text(
|
||||||
|
// "Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
||||||
if(showDialog) {
|
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
AddStringDialog("Group name", groups.map{ it.name }, { showDialog = false }) { onAdd(it) }
|
// ) },
|
||||||
}
|
// { Text("Add a group") },
|
||||||
else if(editing != -1) {
|
// { showDialog = true }
|
||||||
AddStringDialog("Group name", groups.map { it.name }, { editing = -1 }, groups[editing].name) {
|
// ) { idx, it ->
|
||||||
onUpdate(groups[editing], it)
|
// SelectEditDeleteRow(
|
||||||
}
|
// selected == idx,
|
||||||
}
|
// { onSelect(idx) }, { onSelect(-1) },
|
||||||
else if(deleting != -1) {
|
// { editing = idx }, { deleting = idx }
|
||||||
ConfirmDeleteDialog(
|
// ) {
|
||||||
"a group",
|
// Text(it.name, Modifier.padding(5.dp))
|
||||||
{ deleting = -1 },
|
// }
|
||||||
{ onDelete(groups[deleting], deleting) }
|
// }
|
||||||
) { Text(groups[deleting].name) }
|
//
|
||||||
}
|
// if(showDialog) {
|
||||||
}
|
// AddStringDialog("Group name", groups.map{ it.name }, { showDialog = false }) { onAdd(it) }
|
||||||
|
// }
|
||||||
@Composable
|
// else if(editing != -1) {
|
||||||
fun AssignmentPanel(
|
// AddStringDialog("Group name", groups.map { it.name }, { editing = -1 }, groups[editing].name) {
|
||||||
course: Course, edition: Edition, assignments: List<Assignment>,
|
// onUpdate(groups[editing], it)
|
||||||
selected: Int, onSelect: (Int) -> Unit,
|
// }
|
||||||
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
// }
|
||||||
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment, Int) -> Unit
|
// else if(deleting != -1) {
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
// ConfirmDeleteDialog(
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
// "a group",
|
||||||
var deleting by remember { mutableStateOf(-1) }
|
// { deleting = -1 },
|
||||||
var editing by remember { mutableStateOf(-1) }
|
// { onDelete(groups[deleting], deleting) }
|
||||||
|
// ) { Text(groups[deleting].name) }
|
||||||
val dialog: @Composable (String, List<String>, () -> Unit, String, (AssignmentType, String) -> Unit) -> Unit =
|
// }
|
||||||
{ label, taken, onClose, current, onSave ->
|
//}
|
||||||
DialogWindow(
|
//
|
||||||
onCloseRequest = onClose,
|
//@Composable
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
//fun AssignmentPanel(
|
||||||
) {
|
// course: Course, edition: Edition, assignments: List<Assignment>,
|
||||||
var name by remember(current) { mutableStateOf(current) }
|
// selected: Int, onSelect: (Int) -> Unit,
|
||||||
var tab by remember { mutableStateOf(AssignmentType.Solo) }
|
// onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
|
||||||
|
// onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment, Int) -> Unit
|
||||||
Surface(Modifier.fillMaxSize()) {
|
//) = Column(Modifier.padding(10.dp)) {
|
||||||
TabLayout(
|
// var showDialog by remember { mutableStateOf(false) }
|
||||||
AssignmentType.entries,
|
// var deleting by remember { mutableStateOf(-1) }
|
||||||
tab.ordinal,
|
// var editing by remember { mutableStateOf(-1) }
|
||||||
{ tab = AssignmentType.entries[it] },
|
//
|
||||||
{ Text(it.show) }
|
// val dialog: @Composable (String, List<String>, () -> Unit, String, (AssignmentType, String) -> Unit) -> Unit =
|
||||||
) {
|
// { label, taken, onClose, current, onSave ->
|
||||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
// DialogWindow(
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
// onCloseRequest = onClose,
|
||||||
OutlinedTextField(
|
// state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
name,
|
// ) {
|
||||||
{ name = it },
|
// var name by remember(current) { mutableStateOf(current) }
|
||||||
Modifier.fillMaxWidth(),
|
// var tab by remember { mutableStateOf(AssignmentType.Solo) }
|
||||||
label = { Text(label) },
|
//
|
||||||
isError = name in taken
|
// Surface(Modifier.fillMaxSize()) {
|
||||||
)
|
// TabLayout(
|
||||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
// AssignmentType.entries,
|
||||||
onSave(tab, name)
|
// tab.ordinal,
|
||||||
onClose()
|
// { tab = AssignmentType.entries[it] },
|
||||||
}
|
// { Text(it.show) }
|
||||||
}
|
// ) {
|
||||||
}
|
// Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
}
|
// Column(Modifier.align(Alignment.Center)) {
|
||||||
}
|
// OutlinedTextField(
|
||||||
}
|
// name,
|
||||||
}
|
// { name = it },
|
||||||
|
// Modifier.fillMaxWidth(),
|
||||||
Text("Assignment list (${assignments.size})", style = MaterialTheme.typography.headlineMedium)
|
// label = { Text(label) },
|
||||||
|
// isError = name in taken
|
||||||
ListOrEmpty(
|
// )
|
||||||
assignments,
|
// CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
{ Text(
|
// onSave(tab, name)
|
||||||
"Course ${course.name} (edition ${edition.name})\nhas no assignments yet.",
|
// onClose()
|
||||||
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
// }
|
||||||
) },
|
// }
|
||||||
{ Text("Add an assignment") },
|
// }
|
||||||
{ showDialog = true }
|
// }
|
||||||
) { idx, it ->
|
// }
|
||||||
Selectable(
|
// }
|
||||||
selected == idx,
|
// }
|
||||||
{ onSelect(idx) }, { onSelect(-1) }
|
//
|
||||||
) {
|
// Text("Assignment list (${assignments.size})", style = MaterialTheme.typography.headlineMedium)
|
||||||
Row {
|
//
|
||||||
Text(it.name(), Modifier.padding(5.dp).align(Alignment.CenterVertically).weight(1f))
|
// ListOrEmpty(
|
||||||
Column(Modifier.padding(2.dp)) {
|
// assignments,
|
||||||
Icon(Icons.Default.ArrowUpward, "Move up", Modifier.clickable {
|
// { Text(
|
||||||
if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1])
|
// "Course ${course.name} (edition ${edition.name})\nhas no assignments yet.",
|
||||||
})
|
// Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center
|
||||||
Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable {
|
// ) },
|
||||||
if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1])
|
// { Text("Add an assignment") },
|
||||||
})
|
// { showDialog = true }
|
||||||
}
|
// ) { idx, it ->
|
||||||
Column(Modifier.padding(2.dp)) {
|
// Selectable(
|
||||||
Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx })
|
// selected == idx,
|
||||||
Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = 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(showDialog) {
|
// if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1])
|
||||||
dialog("Assignment name", assignments.map{ it.name() }, { showDialog = false }, "", onAdd)
|
// })
|
||||||
}
|
// Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable {
|
||||||
else if(editing != -1) {
|
// if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1])
|
||||||
AddStringDialog("Assignment name", assignments.map { it.name() }, { editing = -1 }, assignments[editing].name()) {
|
// })
|
||||||
onUpdate(assignments[editing], it)
|
// }
|
||||||
}
|
// Column(Modifier.padding(2.dp)) {
|
||||||
}
|
// Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx })
|
||||||
else if(deleting != -1) {
|
// Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = idx })
|
||||||
ConfirmDeleteDialog(
|
// }
|
||||||
"an assignment",
|
// }
|
||||||
{ deleting = -1 },
|
// }
|
||||||
{ onDelete(assignments[deleting], deleting) }
|
// }
|
||||||
) { if(deleting != -1) Text(assignments[deleting].name()) }
|
//
|
||||||
}
|
// if(showDialog) {
|
||||||
}
|
// dialog("Assignment name", assignments.map{ it.name() }, { showDialog = false }, "", onAdd)
|
||||||
|
// }
|
||||||
@Composable
|
// else if(editing != -1) {
|
||||||
fun StudentDialog(
|
// AddStringDialog("Assignment name", assignments.map { it.name() }, { editing = -1 }, assignments[editing].name()) {
|
||||||
course: Course,
|
// onUpdate(assignments[editing], it)
|
||||||
edition: Edition,
|
// }
|
||||||
onClose: () -> Unit,
|
// }
|
||||||
availableStudents: List<Student>,
|
// else if(deleting != -1) {
|
||||||
onImport: (List<Student>) -> Unit,
|
// ConfirmDeleteDialog(
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
// "an assignment",
|
||||||
) = DialogWindow(
|
// { deleting = -1 },
|
||||||
onCloseRequest = onClose,
|
// { onDelete(assignments[deleting], deleting) }
|
||||||
state = rememberDialogState(size = DpSize(600.dp, 400.dp), position = WindowPosition(Alignment.Center))
|
// ) { if(deleting != -1) Text(assignments[deleting].name()) }
|
||||||
) {
|
// }
|
||||||
Surface(Modifier.fillMaxSize()) {
|
//}
|
||||||
Column(Modifier.padding(10.dp)) {
|
//
|
||||||
var isImport by remember { mutableStateOf(false) }
|
//@Composable
|
||||||
TabRow(if(isImport) 1 else 0) {
|
//fun StudentDialog(
|
||||||
Tab(!isImport, { isImport = false }) { Text("Add new student") }
|
// course: Course,
|
||||||
Tab(isImport, { isImport = true }) { Text("Add existing student") }
|
// edition: Edition,
|
||||||
}
|
// onClose: () -> Unit,
|
||||||
|
// availableStudents: List<Student>,
|
||||||
if(isImport) {
|
// onImport: (List<Student>) -> Unit,
|
||||||
if(availableStudents.isEmpty()) {
|
// onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||||
Box(Modifier.fillMaxSize()) {
|
//) = DialogWindow(
|
||||||
Text("No students available to add to this course.", Modifier.align(Alignment.Center))
|
// onCloseRequest = onClose,
|
||||||
}
|
// state = rememberDialogState(size = DpSize(600.dp, 400.dp), position = WindowPosition(Alignment.Center))
|
||||||
}
|
//) {
|
||||||
else {
|
// Surface(Modifier.fillMaxSize()) {
|
||||||
var selected by remember { mutableStateOf(setOf<Int>()) }
|
// Column(Modifier.padding(10.dp)) {
|
||||||
|
// var isImport by remember { mutableStateOf(false) }
|
||||||
val onClick = { idx: Int ->
|
// TabRow(if(isImport) 1 else 0) {
|
||||||
selected = if(idx in selected) selected - idx else selected + idx
|
// Tab(!isImport, { isImport = false }) { Text("Add new student") }
|
||||||
}
|
// Tab(isImport, { isImport = true }) { Text("Add existing student") }
|
||||||
|
// }
|
||||||
Text("Select students to add to ${course.name} ${edition.name}")
|
//
|
||||||
LazyColumn {
|
// if(isImport) {
|
||||||
itemsIndexed(availableStudents) { idx, student ->
|
// if(availableStudents.isEmpty()) {
|
||||||
Surface(
|
// Box(Modifier.fillMaxSize()) {
|
||||||
Modifier.fillMaxWidth().clickable { onClick(idx) },
|
// Text("No students available to add to this course.", Modifier.align(Alignment.Center))
|
||||||
tonalElevation = if (selected.contains(idx)) 5.dp else 0.dp
|
// }
|
||||||
) {
|
// }
|
||||||
Row {
|
// else {
|
||||||
Checkbox(selected.contains(idx), { onClick(idx) })
|
// var selected by remember { mutableStateOf(setOf<Int>()) }
|
||||||
Text(student.name, Modifier.padding(5.dp))
|
//
|
||||||
}
|
// val onClick = { idx: Int ->
|
||||||
}
|
// selected = if(idx in selected) selected - idx else selected + idx
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
CancelSaveRow(selected.isNotEmpty(), onClose) {
|
// Text("Select students to add to ${course.name} ${edition.name}")
|
||||||
onImport(selected.map { idx -> availableStudents[idx] })
|
// LazyColumn {
|
||||||
onClose()
|
// itemsIndexed(availableStudents) { idx, student ->
|
||||||
}
|
// Surface(
|
||||||
}
|
// Modifier.fillMaxWidth().clickable { onClick(idx) },
|
||||||
}
|
// tonalElevation = if (selected.contains(idx)) 5.dp else 0.dp
|
||||||
else {
|
// ) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
// Row {
|
||||||
var name by remember { mutableStateOf("") }
|
// Checkbox(selected.contains(idx), { onClick(idx) })
|
||||||
var contact by remember { mutableStateOf("") }
|
// Text(student.name, Modifier.padding(5.dp))
|
||||||
var note by remember { mutableStateOf("") }
|
// }
|
||||||
var add by remember { mutableStateOf(true) }
|
// }
|
||||||
|
// }
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
// }
|
||||||
OutlinedTextField(
|
// CancelSaveRow(selected.isNotEmpty(), onClose) {
|
||||||
name,
|
// onImport(selected.map { idx -> availableStudents[idx] })
|
||||||
{ name = it },
|
// onClose()
|
||||||
Modifier.fillMaxWidth(),
|
// }
|
||||||
singleLine = true,
|
// }
|
||||||
label = { Text("Student name") })
|
// }
|
||||||
OutlinedTextField(
|
// else {
|
||||||
contact,
|
// Box(Modifier.fillMaxSize()) {
|
||||||
{ contact = it },
|
// var name by remember { mutableStateOf("") }
|
||||||
Modifier.fillMaxWidth(),
|
// var contact by remember { mutableStateOf("") }
|
||||||
singleLine = true,
|
// var note by remember { mutableStateOf("") }
|
||||||
label = { Text("Student contact") })
|
// var add by remember { mutableStateOf(true) }
|
||||||
OutlinedTextField(
|
//
|
||||||
note,
|
// Column(Modifier.align(Alignment.Center)) {
|
||||||
{ note = it },
|
// OutlinedTextField(
|
||||||
Modifier.fillMaxWidth(),
|
// name,
|
||||||
singleLine = false,
|
// { name = it },
|
||||||
minLines = 3,
|
// Modifier.fillMaxWidth(),
|
||||||
label = { Text("Note") })
|
// singleLine = true,
|
||||||
Row {
|
// label = { Text("Student name") })
|
||||||
Checkbox(add, { add = it })
|
// OutlinedTextField(
|
||||||
Text(
|
// contact,
|
||||||
"Add student to ${course.name} ${edition.name}?",
|
// { contact = it },
|
||||||
Modifier.align(Alignment.CenterVertically)
|
// Modifier.fillMaxWidth(),
|
||||||
)
|
// singleLine = true,
|
||||||
}
|
// label = { Text("Student contact") })
|
||||||
CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
|
// OutlinedTextField(
|
||||||
onAdd(name, note, contact, add)
|
// note,
|
||||||
onClose()
|
// { note = it },
|
||||||
}
|
// Modifier.fillMaxWidth(),
|
||||||
}
|
// singleLine = false,
|
||||||
}
|
// minLines = 3,
|
||||||
}
|
// label = { Text("Note") })
|
||||||
}
|
// Row {
|
||||||
}
|
// Checkbox(add, { add = it })
|
||||||
}
|
// Text(
|
||||||
|
// "Add student to ${course.name} ${edition.name}?",
|
||||||
|
// Modifier.align(Alignment.CenterVertically)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
|
||||||
|
// onAdd(name, note, contact, add)
|
||||||
|
// onClose()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.GroupGrading
|
||||||
|
import com.jaytux.grader.app
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGrade
|
||||||
|
import com.jaytux.grader.data.v2.Criterion
|
||||||
|
import com.jaytux.grader.data.v2.GradeType
|
||||||
|
import com.jaytux.grader.data.v2.Group
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import com.jaytux.grader.viewmodel.Grade
|
||||||
|
import com.jaytux.grader.viewmodel.GroupsGradingVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupsGradingTitle(data: GroupGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Group Assignments / ${data.assignment.name} / Grading")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupsGradingView(data: GroupGrading, token: Navigator.NavToken) {
|
||||||
|
val vm = viewModel<GroupsGradingVM>(key = data.assignment.id.toString()) {
|
||||||
|
GroupsGradingVM(data.course, data.edition, data.assignment)
|
||||||
|
}
|
||||||
|
val groups by vm.groupList.entities
|
||||||
|
val focus by vm.focus
|
||||||
|
|
||||||
|
val selectedGroup = remember(focus, groups) { groups.getOrNull(focus) }
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Text("Grading ${vm.base.name}", Modifier.weight(1f), style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("Group assignment in ${vm.course.name} - ${vm.edition.name}")
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
Row(Modifier.fillMaxSize()) {
|
||||||
|
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||||
|
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
|
||||||
|
QuickAGroup(idx == focus, { vm.focusGroup(idx) }, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
|
||||||
|
if (focus == -1 || selectedGroup == null) {
|
||||||
|
Box(Modifier.weight(0.75f).fillMaxHeight()) {
|
||||||
|
Text("Select a group to start grading.", Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(Modifier.weight(0.75f).padding(15.dp)) {
|
||||||
|
Row {
|
||||||
|
IconButton({ vm.focusPrev() }, Modifier.align(Alignment.CenterVertically), enabled = focus > 0) {
|
||||||
|
Icon(DoubleBack, "Previous group")
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Text(selectedGroup.group.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
IconButton({ vm.focusNext() }, Modifier.align(Alignment.CenterVertically), enabled = focus < groups.size - 1) {
|
||||||
|
Icon(DoubleForward, "Next group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
|
val global by vm.globalGrade.entity
|
||||||
|
val byCriteria by vm.gradeList.entities
|
||||||
|
|
||||||
|
Surface(Modifier.fillMaxSize(), color = Color.White, shape = MaterialTheme.shapes.medium) {
|
||||||
|
LazyColumn {
|
||||||
|
items(byCriteria ?: listOf()) { (crit, fdbk) ->
|
||||||
|
var isOpen by remember(selectedGroup) { mutableStateOf(false) }
|
||||||
|
Column(Modifier.padding(5.dp)) {
|
||||||
|
GFWidget(crit, selectedGroup.group, fdbk, vm, global to byCriteria, isOpen) { isOpen = !isOpen }
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global?.let { fdbk ->
|
||||||
|
item {
|
||||||
|
Box(Modifier.padding(5.dp)) {
|
||||||
|
GFWidget(
|
||||||
|
vm.global, selectedGroup.group, fdbk, vm, global to byCriteria, true,
|
||||||
|
(byCriteria ?: listOf()).flatMap { (_, it) ->
|
||||||
|
it.overrides.mapNotNull { o ->
|
||||||
|
o.second?.let { _ -> o.first.id.value }
|
||||||
|
}
|
||||||
|
}.toSet()
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun QuickAGroup(isFocus: Boolean, onFocus: () -> Unit, group: GroupsGradingVM.GroupData) {
|
||||||
|
Surface(tonalElevation = if(isFocus) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { onFocus() }.padding(10.dp)) {
|
||||||
|
Text(group.group.name, fontWeight = FontWeight.Bold)
|
||||||
|
Text("${group.students.size} student(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gradeState(crit: GroupsGradingVM.CritData, current: Grade?): Grade = transaction {
|
||||||
|
if(current == null) Grade.default(crit.criterion.gradeType, crit.cat, crit.num)
|
||||||
|
when(crit.criterion.gradeType) {
|
||||||
|
GradeType.CATEGORIC ->
|
||||||
|
if(current is Grade.Categoric && current.grade.id == crit.criterion.categoricGrade?.id) current
|
||||||
|
else Grade.default(GradeType.CATEGORIC, crit.cat, crit.num)
|
||||||
|
GradeType.NUMERIC ->
|
||||||
|
if(current is Grade.Numeric && current.grade.id == crit.criterion.numericGrade?.id) current
|
||||||
|
else Grade.default(GradeType.NUMERIC, crit.cat, crit.num)
|
||||||
|
GradeType.PERCENTAGE ->
|
||||||
|
current as? Grade.Percentage ?: Grade.default(GradeType.PERCENTAGE, crit.cat, crit.num)
|
||||||
|
GradeType.NONE ->
|
||||||
|
current as? Grade.FreeText ?: Grade.default(GradeType.NONE, crit.cat, crit.num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GFWidget(crit: GroupsGradingVM.CritData, gr: Group, feedback: GroupsGradingVM.FeedbackData, vm: GroupsGradingVM, key: Any, isOpen: Boolean, markOverridden: Set<UUID> = setOf(), onToggle: () -> Unit) = Surface(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
shadowElevation = 3.dp
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Surface(tonalElevation = 5.dp) {
|
||||||
|
Row(Modifier.fillMaxWidth().clickable { onToggle() }.padding(10.dp)) {
|
||||||
|
Icon(if(isOpen) ChevronDown else ChevronRight, "Toggle criterion detail grading", Modifier.align(Alignment.CenterVertically))
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text(crit.criterion.name, Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
feedback.groupLevel?.grade?.let {
|
||||||
|
Row(Modifier.align(Alignment.Bottom)) {
|
||||||
|
ProvideTextStyle(MaterialTheme.typography.bodySmall) {
|
||||||
|
Text("(Grade: ")
|
||||||
|
it.render()
|
||||||
|
Text(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isOpen) {
|
||||||
|
Row(Modifier.padding(10.dp)) {
|
||||||
|
var grade by remember(key, feedback) { mutableStateOf(gradeState(crit, feedback.groupLevel?.grade)) }
|
||||||
|
var text by remember(key, feedback) { mutableStateOf(feedback.groupLevel?.feedback ?: "") }
|
||||||
|
Column(Modifier.weight(0.5f).height(IntrinsicSize.Min)) {
|
||||||
|
|
||||||
|
GradePicker(grade, key = crit to gr) { grade = it }
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
OutlinedTextField(text, { text = it }, label = { Text("Feedback") }, singleLine = false, minLines = 5, modifier = Modifier.fillMaxWidth().weight(1f))
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
Button({ vm.modGroupFeedback(crit.criterion, gr, grade, text) }) {
|
||||||
|
Text("Save grade and feedback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback.groupLevel?.let { groupLevel ->
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.5f).height(IntrinsicSize.Min), tonalElevation = 10.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Text("Individual overrides", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
feedback.overrides.forEach { (student, it) ->
|
||||||
|
var enable by remember(key, it) { mutableStateOf(false) }
|
||||||
|
var maybeRemoving by remember(key, it) { mutableStateOf(false) }
|
||||||
|
var sGrade by remember(key, it) { mutableStateOf(gradeState(crit, it?.grade ?: grade)) }
|
||||||
|
var sText by remember(key, it) { mutableStateOf(it?.feedback ?: "") }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Checkbox(enable, { if(it) { enable = true } else { maybeRemoving = true } })
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text(student.name, Modifier.align(Alignment.CenterVertically))
|
||||||
|
if(student.id.value in markOverridden) {
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("(Overridden)", Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.bodySmall, fontStyle = FontStyle.Italic, color = Color.Red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(enable) Row {
|
||||||
|
Spacer(Modifier.width(15.dp))
|
||||||
|
Surface(color = Color.White, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
GradePicker(sGrade, key = crit to gr app student) { sGrade = it }
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
OutlinedTextField(sText, { sText = it }, label = { Text("Feedback") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
Button({ vm.modOverrideFeedback(crit.criterion, gr, student, groupLevel, sGrade, sText) }) {
|
||||||
|
Text("Save override")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(maybeRemoving) {
|
||||||
|
ConfirmDeleteDialog("the individual grade for ${student.name}", { maybeRemoving = false }, {
|
||||||
|
maybeRemoving = false
|
||||||
|
enable = false
|
||||||
|
vm.rmOverrideFeedback(crit.criterion, gr, student)
|
||||||
|
}) {
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Text("Grade:")
|
||||||
|
sGrade.render()
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Text("Feedback:")
|
||||||
|
if(sText.isBlank()) Text("No feedback", fontStyle = FontStyle.Italic)
|
||||||
|
else Text(sText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.draganddrop.dragAndDropSource
|
||||||
|
import androidx.compose.foundation.draganddrop.dragAndDropTarget
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||||
|
import androidx.compose.ui.draganddrop.DragAndDropTarget
|
||||||
|
import androidx.compose.ui.draganddrop.DragAndDropTransferAction
|
||||||
|
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||||
|
import androidx.compose.ui.draganddrop.DragAndDropTransferable
|
||||||
|
import androidx.compose.ui.draganddrop.awtTransferable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jaytux.grader.data.v2.Group
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import com.jaytux.grader.viewmodel.EditionVM
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.awt.datatransfer.Transferable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||||
|
val groups by vm.groupList.entities
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
var swappingRole by remember { mutableStateOf(-1) }
|
||||||
|
|
||||||
|
val group = remember(groups, focus) { if(focus != -1) groups[focus] else null }
|
||||||
|
val grades by vm.groupGrades.entities
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||||
|
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
|
||||||
|
QuickGroup(idx, it, vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
|
||||||
|
if(group == null) {
|
||||||
|
Box(Modifier.weight(0.75f).fillMaxHeight()) {
|
||||||
|
Text("Select a group to view details.", Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Text(group.group.name, style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
Row(Modifier.padding(5.dp)) {
|
||||||
|
var showTargetBorder by remember { mutableStateOf(false) }
|
||||||
|
val ddTarget = remember {
|
||||||
|
DDTarget({ showTargetBorder = true }, { showTargetBorder = false }, { DDTarget.extractStudent(it) }) {
|
||||||
|
vm.addStudentToGroup(it, group.group, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.75f)) {
|
||||||
|
Surface(
|
||||||
|
Modifier.weight(0.5f).then(if(showTargetBorder) Modifier.border(BorderStroke(3.dp, Color.Black)) else Modifier)
|
||||||
|
.dragAndDropTarget({ true }, target = ddTarget),
|
||||||
|
shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
Surface(tonalElevation = 15.dp) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(10.dp)) {
|
||||||
|
Text("Members", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(group.members) { idx, (student, role) ->
|
||||||
|
Row(Modifier.clickable { vm.focus(student) }.padding(10.dp)) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(student.name, fontWeight = FontWeight.Bold)
|
||||||
|
if(student.contact.isEmpty())
|
||||||
|
Text("No contact info.", fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
else Text(student.contact)
|
||||||
|
}
|
||||||
|
if(role != null) {
|
||||||
|
Surface(Modifier.align(Alignment.CenterVertically), tonalElevation = 5.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Box(Modifier.clickable { swappingRole = -1 }.clickable { swappingRole = idx }) {
|
||||||
|
Text(role, Modifier.padding(horizontal = 5.dp, vertical = 2.dp), style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text("No role", Modifier.align(Alignment.CenterVertically).clickable { swappingRole = idx }, fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
IconButton({ vm.rmStudentFromGroup(student, group.group) }, Modifier.align(Alignment.CenterVertically)) {
|
||||||
|
Icon(PersonMinus, "Remove ${student.name} from group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(group.members.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(Modifier.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||||
|
Text("No members yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.5f)) {
|
||||||
|
Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
Surface(tonalElevation = 15.dp) {
|
||||||
|
Row(Modifier.padding(10.dp)) {
|
||||||
|
Text("Assignment", Modifier.weight(0.66f))
|
||||||
|
Text("Grade", Modifier.weight(0.33f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(grades ?: listOf()) {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Row {
|
||||||
|
Text(it.first.name, Modifier.weight(0.66f))
|
||||||
|
it.second?.render(Modifier.weight(0.33f))
|
||||||
|
?: Text("---", Modifier.weight(0.33f), color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if((grades ?: listOf()).isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(Modifier.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||||
|
Text("No grades yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
|
||||||
|
val available by vm.groupAvailableStudents.entities
|
||||||
|
Surface(Modifier.weight(0.25f), shape = MaterialTheme.shapes.medium, color = Color.White, shadowElevation = 1.dp) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
Surface(tonalElevation = 15.dp) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(10.dp)) {
|
||||||
|
Text("Available Students", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(available ?: listOf()) { student ->
|
||||||
|
AvailableStudent(student, group.group, vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if((available ?: listOf()).isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(Modifier.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||||
|
Text("No available students.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(swappingRole != -1) {
|
||||||
|
if(group != null) {
|
||||||
|
val roles by vm.usedRoles.entities
|
||||||
|
RolePicker(roles, group.members[swappingRole].second, { swappingRole = -1 }) { upd ->
|
||||||
|
vm.setStudentRole(group.members[swappingRole].first, group.group, upd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
swappingRole = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DDTarget<T>(val onStart: () -> Unit, val onEnd: () -> Unit, val validator: (Transferable) -> T?, val handle: (T) -> Unit) : DragAndDropTarget {
|
||||||
|
override fun onStarted(event: DragAndDropEvent) {
|
||||||
|
onStart()
|
||||||
|
super.onStarted(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnded(event: DragAndDropEvent) {
|
||||||
|
onEnd()
|
||||||
|
super.onEnded(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
override fun onDrop(event: DragAndDropEvent): Boolean {
|
||||||
|
println("Action at the target: ${event.action}")
|
||||||
|
|
||||||
|
return validator(event.awtTransferable)?.let {
|
||||||
|
handle(it)
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun mkStudentTransferable(student: Student) = DragAndDropTransferable(StringSelection("com.jaytux.grader:student:${student.id.value}"))
|
||||||
|
|
||||||
|
fun extractStudent(transf: Transferable): Student? {
|
||||||
|
if(transf.isDataFlavorSupported(DataFlavor.stringFlavor)) {
|
||||||
|
val raw = transf.getTransferData(DataFlavor.stringFlavor) as String
|
||||||
|
val prefix = "com.jaytux.grader:student:"
|
||||||
|
if(raw.startsWith(prefix)) {
|
||||||
|
val id = UUID.fromString(raw.removePrefix(prefix))
|
||||||
|
return transaction { Student.findById(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun QuickGroup(idx: Int, group: EditionVM.GroupData, vm: EditionVM) {
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
|
||||||
|
Text(group.group.name, fontWeight = FontWeight.Bold)
|
||||||
|
Text("${group.members.size} member(s)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AvailableStudent(student: Student, group: Group, vm: EditionVM) {
|
||||||
|
Row(Modifier.padding(10.dp).dragAndDropSource(
|
||||||
|
drawDragDecoration = {},
|
||||||
|
) {
|
||||||
|
DragAndDropTransferData(
|
||||||
|
transferable = DDTarget.mkStudentTransferable(student),
|
||||||
|
supportedActions = listOf(DragAndDropTransferAction.Move),
|
||||||
|
dragDecorationOffset = it,
|
||||||
|
onTransferCompleted = { act -> println("Source action: $act") }
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Text(student.name, Modifier.align(Alignment.CenterVertically).weight(1f), fontWeight = FontWeight.Bold)
|
||||||
|
IconButton({ vm.addStudentToGroup(student, group, null) }) {
|
||||||
|
Icon(CirclePlus, "Add ${student.name} to group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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.items
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.EditionDetail
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
import com.jaytux.grader.viewmodel.HomeVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeTitle() = Text("Grader")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeView(token: Navigator.NavToken) {
|
||||||
|
val vm = viewModel<HomeVM>()
|
||||||
|
val courses by vm.courses.entities
|
||||||
|
var addingCourse by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LazyColumn(Modifier.padding(15.dp)) {
|
||||||
|
item {
|
||||||
|
Row {
|
||||||
|
Text("Courses Overview", Modifier.weight(0.8f), style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Button({ addingCourse = true }) {
|
||||||
|
Icon(CirclePlus, "Add course")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Add course")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(courses) {
|
||||||
|
CourseCard(it, vm) { e -> token.navTo(EditionDetail(e, it.course)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addingCourse) {
|
||||||
|
AddStringDialog("Course Name", courses.map { it.course.name }, { addingCourse = false }, "") {
|
||||||
|
vm.mkCourse(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CourseCard(course: HomeVM.CourseData, vm: HomeVM, onOpenEdition: (Edition) -> Unit) {
|
||||||
|
var addingEdition by remember { mutableStateOf(false) }
|
||||||
|
var deleting by remember { mutableStateOf(false) }
|
||||||
|
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.fillMaxWidth().padding(10.dp)) {
|
||||||
|
Column(Modifier.padding(8.dp)) {
|
||||||
|
Row {
|
||||||
|
Text(course.course.name, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f))
|
||||||
|
IconButton({ deleting = true }) { Icon(Delete, "Delete course") }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text("Editions", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f))
|
||||||
|
Button({ addingEdition = true }) {
|
||||||
|
Icon(CirclePlus, "Add edition")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Add edition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
course.editions.forEach { EditionCard(course.course.name, it, vm, onOpenEdition) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if(course.archived.isNotEmpty()) {
|
||||||
|
Text("Archived editions", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
course.archived.forEach { EditionCard(course.course.name, it, vm, onOpenEdition) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addingEdition) {
|
||||||
|
AddStringDialog("Edition Name (in ${course.course.name})", course.editions.map { it.edition.name }, { addingEdition = false }, "") {
|
||||||
|
vm.mkEdition(course.course, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(deleting) {
|
||||||
|
ConfirmDeleteDialog("a course", { deleting = false }, { vm.rmCourse(course.course) }) {
|
||||||
|
Text(course.course.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditionCard(courseName: String, edition: HomeVM.EditionData, vm: HomeVM, onOpen: (Edition) -> Unit) {
|
||||||
|
val type = if(edition.edition.archived) "Archived" else "Active"
|
||||||
|
var deleting by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Surface(shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 5.dp, modifier = Modifier.padding(10.dp).clickable { onOpen(edition.edition) }) {
|
||||||
|
Column(Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
|
||||||
|
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||||
|
Text(edition.edition.name, style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Text(
|
||||||
|
"$type\n${edition.students.size} student(s) • ${edition.groups.size} group(s) • ${edition.assignments.size} assignment(s)",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
Row {
|
||||||
|
if(edition.edition.archived) {
|
||||||
|
Button({ vm.unarchiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
|
||||||
|
Icon(Unarchive, "Unarchive edition")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Unarchive edition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Button({ vm.archiveEdition(edition.edition) }, Modifier.weight(0.5f)) {
|
||||||
|
Icon(Archive, "Archive edition")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Archive edition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Button({ deleting = true }, Modifier.weight(0.5f)) {
|
||||||
|
Icon(Delete, "Archive edition")
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("Delete edition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(deleting) {
|
||||||
|
ConfirmDeleteDialog("an edition", { deleting = false }, { vm.rmEdition(edition.edition) }) {
|
||||||
|
Column {
|
||||||
|
Text(edition.edition.name, Modifier.align(Alignment.CenterHorizontally))
|
||||||
|
Text("Edition in course $courseName", Modifier.align(Alignment.CenterHorizontally))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.GroupGrading
|
||||||
|
import com.jaytux.grader.PeerEvalGrading
|
||||||
|
import com.jaytux.grader.viewmodel.GroupsGradingVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
import com.jaytux.grader.viewmodel.PeerEvalsGradingVM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeerEvalsGradingTitle(data: PeerEvalGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Peer Evaluations / ${data.assignment.name} / Grading")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeerEvalsGradingView(data: PeerEvalGrading, token: Navigator.NavToken) {
|
||||||
|
val vm = viewModel<PeerEvalsGradingVM>(key = data.assignment.id.toString()) {
|
||||||
|
PeerEvalsGradingVM(data.course, data.edition, data.assignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,6 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -18,6 +12,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
@@ -29,13 +24,14 @@ import com.jaytux.grader.loadClipboard
|
|||||||
import com.jaytux.grader.toClipboard
|
import com.jaytux.grader.toClipboard
|
||||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
import com.mohamedrejeb.richeditor.model.RichTextState
|
||||||
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
|
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextStyleRow(
|
fun RichTextStyleRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: RichTextState,
|
state: RichTextState,
|
||||||
) {
|
) {
|
||||||
val clip = LocalClipboardManager.current
|
val clip = LocalClipboardManager.current // I know this is deprecated, but I won't figure out the Clipboard API now
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Row(modifier.fillMaxWidth()) {
|
Row(modifier.fillMaxWidth()) {
|
||||||
@@ -53,7 +49,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
|
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
|
||||||
icon = Icons.Outlined.FormatBold
|
icon = FormatBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +63,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic,
|
isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic,
|
||||||
icon = Icons.Outlined.FormatItalic
|
icon = FormatItalic
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true,
|
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true,
|
||||||
icon = Icons.Outlined.FormatUnderlined
|
icon = FormatUnderline
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +91,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true,
|
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true,
|
||||||
icon = Icons.Outlined.FormatStrikethrough
|
icon = FormatStrikethrough
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +105,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.fontSize == 28.sp,
|
isSelected = state.currentSpanStyle.fontSize == 28.sp,
|
||||||
icon = Icons.Outlined.FormatSize
|
icon = FormatSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +119,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.color == Color.Red,
|
isSelected = state.currentSpanStyle.color == Color.Red,
|
||||||
icon = Icons.Filled.Circle,
|
icon = CircleFilled,
|
||||||
tint = Color.Red
|
tint = Color.Red
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,7 +134,7 @@ fun RichTextStyleRow(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
isSelected = state.currentSpanStyle.background == Color.Yellow,
|
isSelected = state.currentSpanStyle.background == Color.Yellow,
|
||||||
icon = Icons.Outlined.Circle,
|
icon = CircleOutline,
|
||||||
tint = Color.Yellow
|
tint = Color.Yellow
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -158,7 +154,7 @@ fun RichTextStyleRow(
|
|||||||
state.toggleUnorderedList()
|
state.toggleUnorderedList()
|
||||||
},
|
},
|
||||||
isSelected = state.isUnorderedList,
|
isSelected = state.isUnorderedList,
|
||||||
icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
|
icon = FormatListBullet,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +164,7 @@ fun RichTextStyleRow(
|
|||||||
state.toggleOrderedList()
|
state.toggleOrderedList()
|
||||||
},
|
},
|
||||||
isSelected = state.isOrderedList,
|
isSelected = state.isOrderedList,
|
||||||
icon = Icons.Outlined.FormatListNumbered,
|
icon = FormatListNumber,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,16 +183,16 @@ fun RichTextStyleRow(
|
|||||||
state.toggleCodeSpan()
|
state.toggleCodeSpan()
|
||||||
},
|
},
|
||||||
isSelected = state.isCodeSpan,
|
isSelected = state.isCodeSpan,
|
||||||
icon = Icons.Outlined.Code,
|
icon = FormatCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton({ state.toClipboard(clip) }) {
|
IconButton({ scope.launch { state.toClipboard(clip) } }) {
|
||||||
Icon(Icons.Default.ContentCopy, contentDescription = "Copy markdown")
|
Icon(ContentCopy, contentDescription = "Copy markdown")
|
||||||
}
|
}
|
||||||
IconButton({ state.loadClipboard(clip, scope) }) {
|
IconButton({ scope.launch { state.loadClipboard(clip, scope) } }) {
|
||||||
Icon(Icons.Default.ContentPaste, contentDescription = "Paste markdown")
|
Icon(ContentPaste, contentDescription = "Paste markdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.GroupGrading
|
||||||
|
import com.jaytux.grader.SoloGrading
|
||||||
|
import com.jaytux.grader.viewmodel.GroupsGradingVM
|
||||||
|
import com.jaytux.grader.viewmodel.Navigator
|
||||||
|
import com.jaytux.grader.viewmodel.SolosGradingVM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SolosGradingTitle(data: SoloGrading) = Text("Courses / ${data.course.name} / ${data.edition.name} / Individual Assignments / ${data.assignment.name} / Grading")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SolosGradingView(data: SoloGrading, token: Navigator.NavToken) {
|
||||||
|
val vm = viewModel<SolosGradingVM>(key = data.assignment.id.toString()) {
|
||||||
|
SolosGradingVM(data.course, data.edition, data.assignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import com.jaytux.grader.viewmodel.EditionVM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||||
|
val students by vm.studentList.entities
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||||
|
ListOrEmpty(students, { Text("No students yet.") }) { idx, it ->
|
||||||
|
QuickStudent(idx, it, vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(Modifier.weight(0.75f).fillMaxHeight(), tonalElevation = 1.dp) {
|
||||||
|
if(focus == -1) {
|
||||||
|
Box(Modifier.weight(0.75f).fillMaxHeight()) {
|
||||||
|
Text("Select a student to view details.", Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val groups by vm.studentGroups.entities
|
||||||
|
val grades by vm.studentGrades.entities
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.75f).padding(15.dp)) {
|
||||||
|
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Row {
|
||||||
|
var editing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Text("Contact: ", Modifier.align(Alignment.CenterVertically).padding(start = 15.dp))
|
||||||
|
if(!editing) {
|
||||||
|
if (students[focus].contact.isBlank()) Text(
|
||||||
|
"No contact info.",
|
||||||
|
Modifier.padding(start = 5.dp),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalTextStyle.current.color.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
else Text(students[focus].contact, Modifier.padding(start = 5.dp))
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Icon(Edit, "Edit contact info", Modifier.clickable { editing = true })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) }
|
||||||
|
OutlinedTextField(mod, { mod = it })
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Icon(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable {
|
||||||
|
vm.modStudent(students[focus], null, mod, null)
|
||||||
|
editing = false
|
||||||
|
})
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Icon(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text("Groups:", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
groups?.let { gList ->
|
||||||
|
if(gList.isEmpty()) null
|
||||||
|
else {
|
||||||
|
FlowRow(Modifier.padding(start = 10.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
gList.forEach { group ->
|
||||||
|
Surface(tonalElevation = 15.dp, shadowElevation = 1.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Box(Modifier.padding(5.dp).clickable { vm.focus(group.first) }) {
|
||||||
|
Text("${group.first.name} (${group.second ?: "no role"})", Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: Text("Not a member of any group.", Modifier.padding(start = 15.dp), fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Column(Modifier.weight(0.33f)) {
|
||||||
|
var mod by remember(focus, students[focus].note, students[focus].id.value) { mutableStateOf(students[focus].note) }
|
||||||
|
|
||||||
|
Text("Internal Note:")
|
||||||
|
OutlinedTextField(
|
||||||
|
mod,
|
||||||
|
{ mod = it },
|
||||||
|
singleLine = false,
|
||||||
|
minLines = 5,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
if(mod != students[focus].note) {
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Button({ vm.modStudent(students[focus], null, null, mod) }) {
|
||||||
|
Text("Update note")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Column(Modifier.weight(0.66f)) {
|
||||||
|
Text("Grade Summary: ", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Surface(shape = MaterialTheme.shapes.medium, color = Color.White) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
Surface(tonalElevation = 15.dp) {
|
||||||
|
Row(Modifier.padding(10.dp)) {
|
||||||
|
Text("Assignment", Modifier.weight(0.66f))
|
||||||
|
Text("Grade", Modifier.weight(0.33f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(grades ?: listOf()) {
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
Row {
|
||||||
|
Text(it.assignment.name, Modifier.weight(0.66f))
|
||||||
|
it.grade?.render(Modifier.weight(0.33f))
|
||||||
|
?: Text("---", Modifier.weight(0.33f), color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
|
||||||
|
it.asMember?.let { g ->
|
||||||
|
Row(Modifier.padding(start = 10.dp)) {
|
||||||
|
Text("As member of ${g.name}", fontStyle = FontStyle.Italic)
|
||||||
|
if (it.overridden) Text(" (overridden)", fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if((grades ?: listOf()).isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(Modifier.fillMaxWidth().padding(vertical = 5.dp)) {
|
||||||
|
Text("No grades yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun QuickStudent(idx: Int, student: Student, vm: EditionVM) {
|
||||||
|
val focus by vm.focusIndex
|
||||||
|
Surface(tonalElevation = if(focus == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small) {
|
||||||
|
Column(Modifier.fillMaxWidth().clickable { vm.focus(idx) }.padding(10.dp)) {
|
||||||
|
Text(student.name, fontWeight = FontWeight.Bold)
|
||||||
|
if(student.contact.isBlank())
|
||||||
|
Text("No contact info.", fontStyle = FontStyle.Italic, color = LocalTextStyle.current.color.copy(alpha = 0.5f))
|
||||||
|
else Text(student.contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,220 +1,200 @@
|
|||||||
package com.jaytux.grader.ui
|
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.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
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.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
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.maxN
|
|
||||||
import com.jaytux.grader.viewmodel.GroupState
|
|
||||||
import com.jaytux.grader.viewmodel.StudentState
|
|
||||||
|
|
||||||
@Composable
|
//@Composable
|
||||||
fun StudentView(state: StudentState, nav: Navigators) {
|
//fun StudentView(state: StudentState, nav: Navigators) {
|
||||||
val groups by state.groups.entities
|
// val groups by state.groups.entities
|
||||||
val courses by state.courseEditions.entities
|
// val courses by state.courseEditions.entities
|
||||||
val groupGrades by state.groupGrades.entities
|
// val groupGrades by state.groupGrades.entities
|
||||||
val soloGrades by state.soloGrades.entities
|
// val soloGrades by state.soloGrades.entities
|
||||||
|
//
|
||||||
Column(Modifier.padding(10.dp)) {
|
// Column(Modifier.padding(10.dp)) {
|
||||||
Row {
|
// Row {
|
||||||
Column(Modifier.weight(0.45f)) {
|
// Column(Modifier.weight(0.45f)) {
|
||||||
Column(Modifier.padding(10.dp).weight(0.35f)) {
|
// Column(Modifier.padding(10.dp).weight(0.35f)) {
|
||||||
Spacer(Modifier.height(10.dp))
|
// Spacer(Modifier.height(10.dp))
|
||||||
InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
|
// InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
|
||||||
InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
|
// InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
|
||||||
InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
|
// InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
|
||||||
}
|
// }
|
||||||
Column(Modifier.weight(0.20f)) {
|
// Column(Modifier.weight(0.20f)) {
|
||||||
Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
|
// ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
|
||||||
val (ed, course) = it
|
// val (ed, course) = it
|
||||||
Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
|
// Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Column(Modifier.weight(0.45f)) {
|
// Column(Modifier.weight(0.45f)) {
|
||||||
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
// Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
// ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
||||||
val (group, c) = it
|
// val (group, c) = it
|
||||||
val (course, ed) = c
|
// val (course, ed) = c
|
||||||
Row(Modifier.clickable { nav.group(group) }) {
|
// Row(Modifier.clickable { nav.group(group) }) {
|
||||||
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
// Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||||
Spacer(Modifier.width(5.dp))
|
// Spacer(Modifier.width(5.dp))
|
||||||
Text(
|
// Text(
|
||||||
"(in course $course ($ed))",
|
// "(in course $course ($ed))",
|
||||||
Modifier.align(Alignment.Bottom),
|
// Modifier.align(Alignment.Bottom),
|
||||||
style = MaterialTheme.typography.bodySmall
|
// style = MaterialTheme.typography.bodySmall
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Column(Modifier.weight(0.55f)) {
|
// Column(Modifier.weight(0.55f)) {
|
||||||
Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
||||||
LazyColumn {
|
// LazyColumn {
|
||||||
item {
|
// item {
|
||||||
Text("As group member", fontWeight = FontWeight.Bold)
|
// Text("As group member", fontWeight = FontWeight.Bold)
|
||||||
}
|
// }
|
||||||
items(groupGrades) {
|
// items(groupGrades) {
|
||||||
groupGradeWidget(it)
|
// groupGradeWidget(it)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
item {
|
// item {
|
||||||
Text("Solo assignments", fontWeight = FontWeight.Bold)
|
// Text("Solo assignments", fontWeight = FontWeight.Bold)
|
||||||
}
|
// }
|
||||||
items(soloGrades) {
|
// items(soloGrades) {
|
||||||
soloGradeWidget(it)
|
// soloGradeWidget(it)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun groupGradeWidget(gg: StudentState.LocalGroupGrade) {
|
//fun groupGradeWidget(gg: StudentState.LocalGroupGrade) {
|
||||||
val (group, assignment, gGrade, iGrade) = gg
|
// val (group, assignment, gGrade, iGrade) = gg
|
||||||
var expanded by remember { mutableStateOf(false) }
|
// var expanded by remember { mutableStateOf(false) }
|
||||||
Row(Modifier.padding(5.dp)) {
|
// Row(Modifier.padding(5.dp)) {
|
||||||
Spacer(Modifier.width(10.dp))
|
// Spacer(Modifier.width(10.dp))
|
||||||
Surface(
|
// Surface(
|
||||||
Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
|
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
|
||||||
tonalElevation = 5.dp,
|
// tonalElevation = 5.dp,
|
||||||
shape = MaterialTheme.shapes.medium
|
// shape = MaterialTheme.shapes.medium
|
||||||
) {
|
// ) {
|
||||||
Column(Modifier.padding(5.dp)) {
|
// Column(Modifier.padding(5.dp)) {
|
||||||
Text("${assignment.maxN(25)} (${iGrade ?: gGrade ?: "no grade yet"})")
|
// Text("${assignment.maxN(25)} (${iGrade ?: gGrade ?: "no grade yet"})")
|
||||||
|
//
|
||||||
if (expanded) {
|
// if (expanded) {
|
||||||
Row {
|
// Row {
|
||||||
Spacer(Modifier.width(10.dp))
|
// Spacer(Modifier.width(10.dp))
|
||||||
Column {
|
// Column {
|
||||||
ItalicAndNormal("Assignment: ", assignment)
|
// ItalicAndNormal("Assignment: ", assignment)
|
||||||
ItalicAndNormal("Group name: ", group)
|
// ItalicAndNormal("Group name: ", group)
|
||||||
ItalicAndNormal("Group grade: ", gGrade ?: "no grade yet")
|
// ItalicAndNormal("Group grade: ", gGrade ?: "no grade yet")
|
||||||
ItalicAndNormal("Individual grade: ", iGrade ?: "no individual grade")
|
// ItalicAndNormal("Individual grade: ", iGrade ?: "no individual grade")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
|
//fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
|
||||||
val (assignment, grade) = sg
|
// val (assignment, grade) = sg
|
||||||
var expanded by remember { mutableStateOf(false) }
|
// var expanded by remember { mutableStateOf(false) }
|
||||||
Row(Modifier.padding(5.dp)) {
|
// Row(Modifier.padding(5.dp)) {
|
||||||
Spacer(Modifier.width(10.dp))
|
// Spacer(Modifier.width(10.dp))
|
||||||
Surface(
|
// Surface(
|
||||||
Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
|
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
|
||||||
tonalElevation = 5.dp,
|
// tonalElevation = 5.dp,
|
||||||
shape = MaterialTheme.shapes.medium
|
// shape = MaterialTheme.shapes.medium
|
||||||
) {
|
// ) {
|
||||||
Column(Modifier.padding(5.dp)) {
|
// Column(Modifier.padding(5.dp)) {
|
||||||
Text("${assignment.maxN(25)} (${grade ?: "no grade yet"})")
|
// Text("${assignment.maxN(25)} (${grade ?: "no grade yet"})")
|
||||||
|
//
|
||||||
if (expanded) {
|
// if (expanded) {
|
||||||
Row {
|
// Row {
|
||||||
Spacer(Modifier.width(10.dp))
|
// Spacer(Modifier.width(10.dp))
|
||||||
Column {
|
// Column {
|
||||||
ItalicAndNormal("Assignment: ", assignment)
|
// ItalicAndNormal("Assignment: ", assignment)
|
||||||
ItalicAndNormal("Individual grade: ", grade ?: "no grade yet")
|
// ItalicAndNormal("Individual grade: ", grade ?: "no grade yet")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun GroupView(state: GroupState, nav: Navigators) {
|
//fun GroupView(state: GroupState, nav: Navigators) {
|
||||||
val members by state.members.entities
|
// val members by state.members.entities
|
||||||
val available by state.availableStudents.entities
|
// val available by state.availableStudents.entities
|
||||||
val allRoles by state.roles.entities
|
// val allRoles by state.roles.entities
|
||||||
|
//
|
||||||
var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
|
// var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
|
||||||
|
//
|
||||||
Column(Modifier.padding(10.dp)) {
|
// Column(Modifier.padding(10.dp)) {
|
||||||
Row {
|
// Row {
|
||||||
Column(Modifier.weight(0.5f)) {
|
// Column(Modifier.weight(0.5f)) {
|
||||||
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
// Text("Students", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
// ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
||||||
val (student, role) = it
|
// val (student, role) = it
|
||||||
Row(Modifier.clickable { nav.student(student) }) {
|
// Row(Modifier.clickable { nav.student(student) }) {
|
||||||
Text(
|
// Text(
|
||||||
"${student.name} (${role ?: "no role"})",
|
// "${student.name} (${role ?: "no role"})",
|
||||||
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
// Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
// style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
// )
|
||||||
IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
|
// IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
|
||||||
Icon(Icons.Default.Edit, "Change role")
|
// Icon(Icons.Default.Edit, "Change role")
|
||||||
}
|
// }
|
||||||
IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
|
// IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
|
||||||
Icon(Icons.Default.Delete, "Remove student")
|
// Icon(Icons.Default.Delete, "Remove student")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
Column(Modifier.weight(0.5f)) {
|
// Column(Modifier.weight(0.5f)) {
|
||||||
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
// Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
||||||
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
// ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
||||||
Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
|
// Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
|
||||||
IconButton({ state.addStudent(it) }) {
|
// IconButton({ state.addStudent(it) }) {
|
||||||
Icon(ChevronLeft, "Add student")
|
// Icon(ChevronLeft, "Add student")
|
||||||
}
|
// }
|
||||||
Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
|
// Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
pickRole?.let {
|
// pickRole?.let {
|
||||||
val (curr, onPick) = it
|
// val (curr, onPick) = it
|
||||||
RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
|
// RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
|
//fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
|
||||||
onCloseRequest = onClose,
|
// onCloseRequest = onClose,
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
|
// state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
|
||||||
) {
|
//) {
|
||||||
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
// Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
// Box(Modifier.fillMaxSize()) {
|
||||||
var role by remember { mutableStateOf(curr ?: "") }
|
// var role by remember { mutableStateOf(curr ?: "") }
|
||||||
Column {
|
// Column {
|
||||||
Text("Used roles:")
|
// Text("Used roles:")
|
||||||
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
|
// LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
|
||||||
items(used) {
|
// items(used) {
|
||||||
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
|
// Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
|
||||||
Text(it, Modifier.padding(5.dp))
|
// Text(it, Modifier.padding(5.dp))
|
||||||
}
|
// }
|
||||||
Spacer(Modifier.height(5.dp))
|
// Spacer(Modifier.height(5.dp))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
|
// OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
|
||||||
CancelSaveRow(true, onClose) {
|
// CancelSaveRow(true, onClose) {
|
||||||
onSave(role.ifBlank { null })
|
// onSave(role.ifBlank { null })
|
||||||
onClose()
|
// onClose()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
@@ -1,45 +1,34 @@
|
|||||||
package com.jaytux.grader.ui
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
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.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.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import com.jaytux.grader.data.Course
|
import com.jaytux.grader.maxN
|
||||||
import com.jaytux.grader.data.Edition
|
import com.jaytux.grader.viewmodel.Grade
|
||||||
import com.jaytux.grader.viewmodel.PeerEvaluationState
|
|
||||||
import com.mohamedrejeb.richeditor.model.RichTextState
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
||||||
@@ -50,37 +39,18 @@ fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> TabLayout(
|
|
||||||
options: List<T>,
|
|
||||||
currentIndex: Int,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
optionContent: @Composable (T) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) = Column(modifier) {
|
|
||||||
TabRow(currentIndex) {
|
|
||||||
options.forEachIndexed { idx, it ->
|
|
||||||
Tab(
|
|
||||||
selected = idx == currentIndex,
|
|
||||||
onClick = { onSelect(idx) },
|
|
||||||
text = { optionContent(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, current: String = "", onSave: (String) -> Unit) = DialogWindow(
|
fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, current: String = "", onSave: (String) -> Unit) = DialogWindow(
|
||||||
onCloseRequest = onClose,
|
onCloseRequest = onClose,
|
||||||
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
|
||||||
) {
|
) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
var name by remember(current) { mutableStateOf(current) }
|
var name by remember(current) { mutableStateOf(current) }
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth().focusRequester(focus), label = { Text(label) }, isError = name in taken)
|
||||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||||
onSave(name)
|
onSave(name)
|
||||||
onClose()
|
onClose()
|
||||||
@@ -88,6 +58,8 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, cur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { focus.requestFocus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -118,299 +90,25 @@ fun ConfirmDeleteDialog(
|
|||||||
fun <T> ListOrEmpty(
|
fun <T> ListOrEmpty(
|
||||||
data: List<T>,
|
data: List<T>,
|
||||||
onEmpty: @Composable ColumnScope.() -> Unit,
|
onEmpty: @Composable ColumnScope.() -> Unit,
|
||||||
addOptions: @Composable ColumnScope.() -> Unit,
|
modifier: Modifier = Modifier,
|
||||||
addAfterLazy: Boolean = true,
|
|
||||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||||
) {
|
) {
|
||||||
if(data.isEmpty()) {
|
if(data.isEmpty()) {
|
||||||
|
Box(modifier) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
onEmpty()
|
onEmpty()
|
||||||
addOptions()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Column {
|
Column(modifier) {
|
||||||
LazyColumn(Modifier.weight(1f)) {
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
itemsIndexed(data) { idx, it ->
|
itemsIndexed(data) { idx, it ->
|
||||||
item(idx, it)
|
item(idx, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!addAfterLazy) item { addOptions() }
|
|
||||||
}
|
}
|
||||||
if(addAfterLazy) addOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> ListOrEmpty(
|
|
||||||
data: List<T>,
|
|
||||||
emptyText: @Composable ColumnScope.() -> Unit,
|
|
||||||
addText: @Composable RowScope.() -> Unit,
|
|
||||||
onAdd: () -> Unit,
|
|
||||||
addAfterLazy: Boolean = true,
|
|
||||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
|
||||||
) = ListOrEmpty(
|
|
||||||
data, emptyText,
|
|
||||||
{ Button(onAdd, Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()) { addText() } },
|
|
||||||
addAfterLazy,
|
|
||||||
item
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> ListOrEmpty(
|
|
||||||
data: List<T>,
|
|
||||||
emptyText: @Composable ColumnScope.() -> Unit,
|
|
||||||
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
|
||||||
) {
|
|
||||||
if(data.isEmpty()) {
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
|
||||||
emptyText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Column {
|
|
||||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
|
||||||
itemsIndexed(data) { idx, it ->
|
|
||||||
item(idx, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InteractToEdit(
|
|
||||||
content: String, onSave: (String) -> Unit, pre: String, modifier: Modifier = Modifier,
|
|
||||||
w1: Float = 0.75f, w2: Float = 0.25f,
|
|
||||||
singleLine: Boolean = true
|
|
||||||
) {
|
|
||||||
var text by remember(content) { mutableStateOf(content) }
|
|
||||||
|
|
||||||
Row(modifier.padding(5.dp)) {
|
|
||||||
val base = if(singleLine) Modifier.align(Alignment.CenterVertically) else Modifier
|
|
||||||
OutlinedTextField(
|
|
||||||
text, { text = it }, base.weight(w1), label = { Text(pre) },
|
|
||||||
singleLine = singleLine, minLines = if(singleLine) 1 else 5
|
|
||||||
)
|
|
||||||
IconButton({ onSave(text) }, base.weight(w2)) { Icon(Icons.Default.Check, "Save") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PaneHeader(name: String, type: String, course: Course, edition: Edition) = Column {
|
|
||||||
Text(name, style = MaterialTheme.typography.headlineMedium)
|
|
||||||
Text("${type.capitalize(Locale.current)} in ${course.name} (${edition.name})", fontStyle = FontStyle.Italic)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>) = PaneHeader(name, type, courseEdition.first, courseEdition.second)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun AutocompleteLineField__(
|
|
||||||
// state: RichTextState,
|
|
||||||
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
|
||||||
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
|
||||||
onFilter: (String) -> List<String>
|
|
||||||
) = Column(modifier) {
|
|
||||||
var suggestions by remember { mutableStateOf(listOf<String>()) }
|
|
||||||
var selected by remember { mutableStateOf(0) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val scrollState = rememberLazyListState()
|
|
||||||
|
|
||||||
val posToLine = { pos: Int ->
|
|
||||||
(value.text.take(pos).count { it == '\n' }) to (value.text.take(pos).lastIndexOf('\n'))
|
|
||||||
}
|
|
||||||
|
|
||||||
val autoComplete = { str: String ->
|
|
||||||
val pos = value.selection.start
|
|
||||||
val lines = value.text.split("\n").toMutableList()
|
|
||||||
val (lineno, lineStart) = posToLine(pos)
|
|
||||||
|
|
||||||
lines[lineno] = str
|
|
||||||
onValueChange(value.copy(text = lines.joinToString("\n"), selection = TextRange(lineStart + str.length + 1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentLine = {
|
|
||||||
value.text.split('\n')[posToLine(value.selection.start).first]
|
|
||||||
}
|
|
||||||
|
|
||||||
val gotoOption = { idx: Int ->
|
|
||||||
selected = if(suggestions.isEmpty()) 0 else ((idx + suggestions.size) % suggestions.size)
|
|
||||||
scope.launch {
|
|
||||||
scrollState.animateScrollToItem(if(suggestions.isNotEmpty()) (selected + 1) else 0)
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
val onKey = { kev: KeyEvent ->
|
|
||||||
var res = true
|
|
||||||
if(suggestions.isNotEmpty()) {
|
|
||||||
when (kev.key) {
|
|
||||||
Key.Tab -> autoComplete(suggestions[selected])
|
|
||||||
Key.DirectionUp -> gotoOption(selected - 1)
|
|
||||||
Key.DirectionDown -> gotoOption(selected + 1)
|
|
||||||
Key.Escape -> suggestions = listOf()
|
|
||||||
else -> res = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else res = false
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(value.text) {
|
|
||||||
delay(300)
|
|
||||||
suggestions = onFilter(currentLine())
|
|
||||||
gotoOption(if(suggestions.isEmpty()) 0 else selected % suggestions.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value, onValueChange,
|
|
||||||
Modifier.fillMaxWidth().weight(0.75f).onKeyEvent(onKey), label = label, singleLine = false, minLines = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
if(suggestions.isNotEmpty()) {
|
|
||||||
LazyColumn(Modifier.weight(0.25f), state = scrollState) {
|
|
||||||
stickyHeader {
|
|
||||||
Surface(tonalElevation = 5.dp) {
|
|
||||||
Text("Suggestions", Modifier.padding(5.dp).fillMaxWidth(), fontStyle = FontStyle.Italic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemsIndexed(suggestions) { idx, it ->
|
|
||||||
Surface(Modifier.padding(5.dp).fillMaxWidth(), tonalElevation = if(selected == idx) 50.dp else 0.dp) {
|
|
||||||
Text(it, Modifier.clickable { autoComplete(it) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun DateTimePicker(
|
|
||||||
value: LocalDateTime,
|
|
||||||
onPick: (LocalDateTime) -> Unit,
|
|
||||||
formatter: (LocalDateTime) -> String = { java.text.DateFormat.getDateTimeInstance().format(Date.from(it.toInstant(TimeZone.currentSystemDefault()).toJavaInstant())) },
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var showPicker by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(modifier) {
|
|
||||||
Text(
|
|
||||||
formatter(value),
|
|
||||||
Modifier.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(10.dp))
|
|
||||||
Button({ showPicker = true }) { Text("Change") }
|
|
||||||
|
|
||||||
if (showPicker) {
|
|
||||||
val dateState = rememberDatePickerState(value.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds())
|
|
||||||
val timeState = rememberTimePickerState(value.hour, value.minute)
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
{ showPicker = false },
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.extraLarge, tonalElevation = 6.dp,
|
|
||||||
modifier = Modifier.width(800.dp).height(600.dp)
|
|
||||||
) {
|
|
||||||
val colors = TimePickerDefaults.colors(
|
|
||||||
selectorColor = MaterialTheme.colorScheme.primary,
|
|
||||||
timeSelectorSelectedContainerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
timeSelectorSelectedContentColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
clockDialSelectedContentColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
) // the colors are fucked, and I don't get why :(
|
|
||||||
|
|
||||||
Column(Modifier.padding(10.dp)) {
|
|
||||||
Row {
|
|
||||||
DatePicker(
|
|
||||||
dateState,
|
|
||||||
Modifier.padding(10.dp).weight(0.5f),
|
|
||||||
)
|
|
||||||
TimePicker(
|
|
||||||
timeState,
|
|
||||||
Modifier.weight(0.5f).align(Alignment.CenterVertically),
|
|
||||||
layoutType = TimePickerLayoutType.Vertical,
|
|
||||||
colors = colors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CancelSaveRow(true, { showPicker = false }) {
|
|
||||||
val date = (dateState.selectedDateMillis?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault()) } ?: value).date
|
|
||||||
val time = LocalTime(timeState.hour, timeState.minute)
|
|
||||||
|
|
||||||
onPick(LocalDateTime(date, time))
|
|
||||||
showPicker = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// DatePickerDialog(
|
|
||||||
// { showPicker = false },
|
|
||||||
// {
|
|
||||||
// Button({
|
|
||||||
// showPicker = false; dateState.selectedDateMillis?.let { state.updateDeadline(it) }
|
|
||||||
// }) { Text("Set deadline") }
|
|
||||||
// },
|
|
||||||
// Modifier,
|
|
||||||
// { Button({ showPicker = false }) { Text("Cancel") } },
|
|
||||||
// shape = MaterialTheme.shapes.medium,
|
|
||||||
// tonalElevation = 10.dp,
|
|
||||||
// colors = DatePickerDefaults.colors(),
|
|
||||||
// properties = DialogProperties()
|
|
||||||
// ) {
|
|
||||||
// DatePicker(
|
|
||||||
// dateState,
|
|
||||||
// Modifier.fillMaxWidth().padding(10.dp),
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,19 +136,19 @@ fun FromTo(size: Dp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
//@Composable
|
||||||
fun PEGradeWidget(
|
//fun PEGradeWidget(
|
||||||
grade: PeerEvaluationState.Student2StudentEntry?,
|
// grade: PeerEvaluationState.Student2StudentEntry?,
|
||||||
onSelect: () -> Unit, onDeselect: () -> Unit,
|
// onSelect: () -> Unit, onDeselect: () -> Unit,
|
||||||
isSelected: Boolean,
|
// isSelected: Boolean,
|
||||||
modifier: Modifier = Modifier
|
// modifier: Modifier = Modifier
|
||||||
) = Box(modifier.padding(2.dp)) {
|
//) = Box(modifier.padding(2.dp)) {
|
||||||
Selectable(isSelected, onSelect, onDeselect) {
|
// Selectable(isSelected, onSelect, onDeselect) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
// Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(grade?.let { if(it.grade.isNotBlank()) it.grade else if(it.feedback.isNotBlank()) "(other)" else null } ?: "none")
|
// Text(grade?.let { if(it.grade.isNotBlank()) it.grade else if(it.feedback.isNotBlank()) "(other)" else null } ?: "none")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VLine(width: Dp = 1.dp, color: Color = Color.Black) = Spacer(Modifier.fillMaxHeight().width(width).background(color))
|
fun VLine(width: Dp = 1.dp, color: Color = Color.Black) = Spacer(Modifier.fillMaxHeight().width(width).background(color))
|
||||||
@@ -460,3 +158,102 @@ fun MeasuredLazyItemScope.HLine(height: Dp = 1.dp, color: Color = Color.Black) {
|
|||||||
val width by measuredWidth()
|
val width by measuredWidth()
|
||||||
Spacer(Modifier.width(width).height(height).background(color))
|
Spacer(Modifier.width(width).height(height).background(color))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
var role by remember { mutableStateOf(curr ?: "") }
|
||||||
|
Column {
|
||||||
|
Text("Used roles:")
|
||||||
|
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
|
||||||
|
items(used) {
|
||||||
|
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
|
||||||
|
Text(it, Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
|
||||||
|
CancelSaveRow(true, onClose) {
|
||||||
|
onSave(role.ifBlank { null })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, onUpdate: (Grade) -> Unit) = Row(modifier) { // TODO: fix UI to remove save-buttons (instead wait fo end of editing)
|
||||||
|
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||||
|
|
||||||
|
when(grade) {
|
||||||
|
is Grade.Categoric -> {
|
||||||
|
if(grade.options.size <= 5) {
|
||||||
|
Column {
|
||||||
|
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||||
|
grade.options.forEachIndexed { idx, opt ->
|
||||||
|
println("Rendering opt ${opt.option} (index $idx) ~ current value: ${grade.value.option})")
|
||||||
|
SegmentedButton(
|
||||||
|
grade.value.option == opt.option, { onUpdate(Grade.Categoric(opt, grade.options, grade.grade)) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(idx, grade.options.size)
|
||||||
|
) { Text(opt.option.maxN(15)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Text(grade.value.option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var slider by remember(grade, key) { mutableStateOf(maxOf(0, grade.options.indexOfFirst { it.option == grade.value.option })) }
|
||||||
|
Row {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Slider(
|
||||||
|
slider.toFloat(),
|
||||||
|
onValueChange = { onUpdate(grade.copy(value = grade.options[slider])) },
|
||||||
|
steps = grade.options.size,
|
||||||
|
valueRange = 0f..(grade.options.size - 1).toFloat()
|
||||||
|
)
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Text(grade.options[slider].option, fontStyle = FontStyle.Italic, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Grade.FreeText -> {
|
||||||
|
var text by remember(grade, key) { mutableStateOf(grade.text) }
|
||||||
|
|
||||||
|
OutlinedTextField(grade.text, { onUpdate(grade.copy(text = it)) }, Modifier.weight(1f), singleLine = true)
|
||||||
|
Button({ onUpdate(Grade.FreeText(text)) }, enabled = text != grade.text) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Grade.Numeric -> {
|
||||||
|
var num by remember(grade, key) { mutableStateOf(grade.value.toString()) }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
num, { num = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } },
|
||||||
|
Modifier.weight(1f), singleLine = true, isError = (num.toDoubleOrNull() ?: 0.0) > grade.grade.max
|
||||||
|
)
|
||||||
|
Button({ onUpdate(Grade.Numeric(num.toDoubleOrNull() ?: 0.0, grade.grade)) }, enabled = (num.toDoubleOrNull() ?: 0.0) <= grade.grade.max) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Grade.Percentage -> {
|
||||||
|
var perc by remember(grade, key) { mutableStateOf(grade.percentage.toString()) }
|
||||||
|
|
||||||
|
OutlinedTextField("$perc%", { perc = it.filter { c -> c.isDigit() || c == '.' || c == ',' }.ifEmpty { "0" } }, Modifier.weight(1f), singleLine = true)
|
||||||
|
Button({ onUpdate(Grade.Percentage(perc.toDoubleOrNull() ?: 0.0)) }) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,821 +3,64 @@ package com.jaytux.grader.viewmodel
|
|||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import com.jaytux.grader.data.*
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
import com.jaytux.grader.data.EditionStudents.editionId
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
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
|
|
||||||
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> 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>)) {
|
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
||||||
|
|
||||||
private val rawEntities by lazy {
|
private val rawEntities by lazy {
|
||||||
mutableStateOf(transaction { loader() })
|
mutableStateOf(transaction { loader() })
|
||||||
}
|
}
|
||||||
val entities = rawEntities.immutable()
|
|
||||||
|
|
||||||
|
val entities = rawEntities.immutable()
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
rawEntities.value = transaction { loader() }
|
rawEntities.value = transaction { loader() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CourseListState {
|
class RawDbFocusableSingleState<TIn, TOut: Any>(private val loader: (Transaction.(TIn) -> TOut?)) {
|
||||||
val courses = RawDbState { Course.all().sortAsc(Courses.name).toList() }
|
private var _input: TIn? = null
|
||||||
|
private val rawEntity by lazy {
|
||||||
fun new(name: String) {
|
mutableStateOf<TOut?>(null)
|
||||||
transaction { Course.new { this.name = name } }
|
|
||||||
courses.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(course: Course) {
|
val entity: State<TOut?> = rawEntity.immutable()
|
||||||
transaction { course.delete() }
|
|
||||||
courses.refresh()
|
fun focus(input: TIn) {
|
||||||
|
_input = input
|
||||||
|
rawEntity.value = transaction { loader(input) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEditions(course: Course) = EditionListState(course)
|
fun unfocus() {
|
||||||
}
|
_input = null
|
||||||
|
rawEntity.value = null
|
||||||
class EditionListState(val course: Course) {
|
|
||||||
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.sortAsc(Editions.name).toList() }
|
|
||||||
|
|
||||||
fun new(name: String) {
|
|
||||||
transaction { Edition.new { this.name = name; this.course = this@EditionListState.course } }
|
|
||||||
editions.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(edition: Edition) {
|
fun refresh() {
|
||||||
transaction { edition.delete() }
|
rawEntity.value = transaction { _input?.let { loader(it) } }
|
||||||
editions.refresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class OpenPanel(val tabName: String) {
|
class RawDbFocusableState<TIn, TOut: Any>(private val loader: (Transaction.(TIn) -> List<TOut>)) {
|
||||||
Student("Students"), Group("Groups"), Assignment("Assignments")
|
private var _input: TIn? = null
|
||||||
}
|
private val rawEntities by lazy {
|
||||||
|
mutableStateOf<List<TOut>?>(null)
|
||||||
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 {
|
|
||||||
(Students.id notInList edition.soloStudents.map { it.id })
|
|
||||||
}.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
|
val entities: State<List<TOut>?> = rawEntities.immutable()
|
||||||
transaction {
|
|
||||||
val student = Student.new { this.name = name; this.contact = contact; this.note = note }
|
fun focus(input: TIn) {
|
||||||
if(addToEdition) EditionStudents.insert {
|
_input = input
|
||||||
it[editionId] = edition.id
|
rawEntities.value = transaction { loader(input) }
|
||||||
it[studentId] = student.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(addToEdition) students.refresh()
|
fun unfocus() {
|
||||||
else availableStudents.refresh()
|
_input = null
|
||||||
}
|
rawEntities.value = null
|
||||||
fun setStudentName(student: Student, name: String) {
|
|
||||||
transaction {
|
|
||||||
student.name = name
|
|
||||||
}
|
|
||||||
students.refresh()
|
|
||||||
}
|
|
||||||
fun addToCourse(students: List<Student>) {
|
|
||||||
transaction {
|
|
||||||
EditionStudents.batchInsert(students) {
|
|
||||||
this[editionId] = edition.id
|
|
||||||
this[studentId] = it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
availableStudents.refresh()
|
|
||||||
this.students.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newGroup(name: String) {
|
fun refresh() {
|
||||||
transaction {
|
rawEntities.value = transaction { _input?.let { loader(it) } }
|
||||||
Group.new { this.name = name; this.edition = this@EditionState.edition }
|
|
||||||
groups.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun setGroupName(group: Group, name: String) {
|
|
||||||
transaction {
|
|
||||||
group.name = name
|
|
||||||
}
|
|
||||||
groups.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun now(): LocalDateTime {
|
|
||||||
val instant = Instant.fromEpochMilliseconds(System.currentTimeMillis())
|
|
||||||
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 {
|
|
||||||
val assign = SoloAssignment.new {
|
|
||||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
|
||||||
this.number = nextIdx()
|
|
||||||
}
|
|
||||||
val global = SoloAssignmentCriterion.new {
|
|
||||||
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
|
|
||||||
}
|
|
||||||
assign.globalCriterion = global
|
|
||||||
solo.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun setSoloAssignmentTitle(assignment: SoloAssignment, title: String) {
|
|
||||||
transaction {
|
|
||||||
assignment.name = title
|
|
||||||
}
|
|
||||||
solo.refresh()
|
|
||||||
}
|
|
||||||
fun newGroupAssignment(name: String) {
|
|
||||||
transaction {
|
|
||||||
val assign = GroupAssignment.new {
|
|
||||||
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
|
|
||||||
this.number = nextIdx()
|
|
||||||
}
|
|
||||||
val global = GroupAssignmentCriterion.new {
|
|
||||||
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
|
|
||||||
}
|
|
||||||
assign.globalCriterion = global
|
|
||||||
groupAs.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun setGroupAssignmentTitle(assignment: GroupAssignment, title: String) {
|
|
||||||
transaction {
|
|
||||||
assignment.name = title
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
fun clearHistoryIndex() {
|
|
||||||
val last = _history.value.lastOrNull() ?: return
|
|
||||||
_history.value = _history.value.filter { (i, panel) -> panel != last.second || i != last.first } + (-1 to last.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StudentState(val student: Student, edition: Edition) {
|
|
||||||
data class LocalGroupGrade(val groupName: String, val assignmentName: String, val groupGrade: String?, val indivGrade: String?)
|
|
||||||
data class LocalSoloGrade(val assignmentName: String, val grade: String)
|
|
||||||
|
|
||||||
val editionCourse = transaction { edition.course to edition }
|
|
||||||
val groups = RawDbState { student.groups.sortAsc(Groups.name).map { it to (it.edition.course.name to it.edition.name) }.toList() }
|
|
||||||
val courseEditions = RawDbState { student.courses.map{ it to it.course }.sortedWith {
|
|
||||||
(e1, c1), (e2, c2) -> c1.name.compareTo(c2.name).let { if(it == 0) e1.name.compareTo(e2.name) else it }
|
|
||||||
}.toList() }
|
|
||||||
|
|
||||||
val groupGrades = RawDbState {
|
|
||||||
val groupsForEdition = Group.find {
|
|
||||||
(Groups.editionId eq edition.id) and (Groups.id inList student.groups.map { it.id })
|
|
||||||
}.associate { it.id to it.name }
|
|
||||||
|
|
||||||
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
|
|
||||||
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
|
|
||||||
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
|
||||||
}.map { it[GroupAssignments.id] to it }
|
|
||||||
|
|
||||||
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
|
|
||||||
(IndividualFeedbacks.studentId eq student.id) and
|
|
||||||
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
|
|
||||||
}.map { it[GroupAssignments.id] to it }
|
|
||||||
|
|
||||||
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
|
|
||||||
asGroup.forEach {
|
|
||||||
val (gAId, gRow) = it
|
|
||||||
|
|
||||||
res[gAId] = LocalGroupGrade(
|
|
||||||
gRow[Groups.name], gRow[GroupAssignments.name], gRow[GroupFeedbacks.grade], null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
asIndividual.forEach {
|
|
||||||
val (gAId, iRow) = it
|
|
||||||
|
|
||||||
val og = res[gAId] ?: LocalGroupGrade(iRow[Groups.name], iRow[GroupAssignments.name], null, null)
|
|
||||||
res[gAId] = og.copy(indivGrade = iRow[IndividualFeedbacks.grade])
|
|
||||||
}
|
|
||||||
|
|
||||||
res.values.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val soloGrades = RawDbState {
|
|
||||||
(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update(f: Student.() -> Unit) {
|
|
||||||
transaction {
|
|
||||||
student.f()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupState(val group: Group) {
|
|
||||||
val members = RawDbState { group.studentRoles.map{ it.student to it.role }.sortedBy { it.first.name }.toList() }
|
|
||||||
val availableStudents = RawDbState { Student.find {
|
|
||||||
// not yet in the group
|
|
||||||
(Students.id notInList group.students.map { it.id }) and
|
|
||||||
// but in the same course (edition)
|
|
||||||
(Students.id inList group.edition.soloStudents.map { it.id })
|
|
||||||
}.sortAsc(Students.name).toList() }
|
|
||||||
val course = transaction { group.edition.course to group.edition }
|
|
||||||
val roles = RawDbState {
|
|
||||||
GroupStudents.select(GroupStudents.role).where{ GroupStudents.role.isNotNull() }
|
|
||||||
.withDistinct(true).sortAsc(GroupStudents.role).map{ it[GroupStudents.role] ?: "" }.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addStudent(student: Student) {
|
|
||||||
transaction {
|
|
||||||
GroupStudents.insert {
|
|
||||||
it[groupId] = group.id
|
|
||||||
it[studentId] = student.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
members.refresh(); availableStudents.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeStudent(student: Student) {
|
|
||||||
transaction {
|
|
||||||
GroupStudents.deleteWhere { groupId eq group.id and (studentId eq student.id) }
|
|
||||||
}
|
|
||||||
members.refresh(); availableStudents.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateRole(student: Student, role: String?) {
|
|
||||||
transaction {
|
|
||||||
GroupStudents.update({ GroupStudents.groupId eq group.id and (GroupStudents.studentId eq student.id) }) {
|
|
||||||
it[this.role] = role
|
|
||||||
}
|
|
||||||
members.refresh()
|
|
||||||
roles.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupAssignmentState(val assignment: GroupAssignment) {
|
|
||||||
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>>> // 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()
|
|
||||||
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
|
|
||||||
val criteria = RawDbState {
|
|
||||||
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
|
||||||
}
|
|
||||||
val feedback = RawDbState { loadFeedback() }
|
|
||||||
|
|
||||||
val autofill = RawDbState {
|
|
||||||
val forGroups = (GroupFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap {
|
|
||||||
it[GroupFeedbacks.feedback].split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
val forIndividuals = (IndividualFeedbacks innerJoin GroupAssignmentCriteria).selectAll().where { GroupAssignmentCriteria.assignmentId eq assignment.id }.flatMap {
|
|
||||||
it[IndividualFeedbacks.feedback].split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
(forGroups + forIndividuals).distinct().sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
|
||||||
val allCrit = GroupAssignmentCriterion.find {
|
|
||||||
GroupAssignmentCriteria.assignmentId eq assignment.id
|
|
||||||
}.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
|
||||||
|
|
||||||
return Group.find {
|
|
||||||
(Groups.editionId eq assignment.edition.id)
|
|
||||||
}.sortAsc(Groups.name).map { group ->
|
|
||||||
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
|
|
||||||
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
|
|
||||||
}.map { row ->
|
|
||||||
val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]]
|
|
||||||
val fdbk = row[GroupFeedbacks.feedback]
|
|
||||||
val grade = row[GroupFeedbacks.grade]
|
|
||||||
|
|
||||||
crit to FeedbackEntry(fdbk, grade)
|
|
||||||
}
|
|
||||||
|
|
||||||
val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
|
||||||
val byCrit_ = forGroup
|
|
||||||
.filter{ it.first.id != assignment.globalCriterion.id }
|
|
||||||
.map { LocalCriterionFeedback(it.first, it.second) }
|
|
||||||
.associateBy { it.criterion.id }
|
|
||||||
|
|
||||||
val byCrit = allCrit.map { c ->
|
|
||||||
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val byGroup = LocalFeedback(global, byCrit)
|
|
||||||
|
|
||||||
val indiv = group.studentRoles.map {
|
|
||||||
val student = it.student
|
|
||||||
val role = it.role
|
|
||||||
|
|
||||||
val forSt = (IndividualFeedbacks innerJoin Groups)
|
|
||||||
.selectAll().where {
|
|
||||||
(IndividualFeedbacks.assignmentId eq assignment.id) and
|
|
||||||
(IndividualFeedbacks.studentId eq student.id) and (Groups.id eq group.id)
|
|
||||||
}.map { row ->
|
|
||||||
val stdId = row[IndividualFeedbacks.studentId]
|
|
||||||
val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
|
|
||||||
val fdbk = row[IndividualFeedbacks.feedback]
|
|
||||||
val grade = row[IndividualFeedbacks.grade]
|
|
||||||
|
|
||||||
crit to FeedbackEntry(fdbk, grade)
|
|
||||||
}
|
|
||||||
|
|
||||||
val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
|
|
||||||
val byCrit_ = forSt
|
|
||||||
.filter { it.first != assignment.globalCriterion.id }
|
|
||||||
.map { LocalCriterionFeedback(it.first, it.second) }
|
|
||||||
.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[assignmentId] = assignment.id
|
|
||||||
it[groupId] = group.id
|
|
||||||
it[this.feedback] = msg
|
|
||||||
it[this.grade] = grd
|
|
||||||
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
feedback.refresh(); autofill.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String, criterion: GroupAssignmentCriterion? = null) {
|
|
||||||
transaction {
|
|
||||||
IndividualFeedbacks.upsert {
|
|
||||||
it[assignmentId] = assignment.id
|
|
||||||
it[groupId] = group.id
|
|
||||||
it[studentId] = student.id
|
|
||||||
it[this.feedback] = msg
|
|
||||||
it[this.grade] = grd
|
|
||||||
it[criterionId] = criterion?.id ?: assignment.globalCriterion.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 {
|
|
||||||
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).filter { it.id != assignment.globalCriterion.id }
|
|
||||||
}
|
|
||||||
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>> {3
|
|
||||||
val allCrit = SoloAssignmentCriterion.find {
|
|
||||||
SoloAssignmentCriteria.assignmentId eq assignment.id
|
|
||||||
}.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
|
|
||||||
|
|
||||||
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
|
|
||||||
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
|
|
||||||
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
|
|
||||||
}.map { row ->
|
|
||||||
val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
|
|
||||||
val fdbk = row[IndividualFeedbacks.feedback]
|
|
||||||
val grade = row[IndividualFeedbacks.grade]
|
|
||||||
|
|
||||||
crit to LocalFeedback(fdbk, grade)
|
|
||||||
}
|
|
||||||
|
|
||||||
val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second
|
|
||||||
val byCrit_ = forStudent
|
|
||||||
.filter { it.first != assignment.globalCriterion.id }
|
|
||||||
.map { Pair(it.first, it.second) }
|
|
||||||
.associateBy { it.first.id }
|
|
||||||
|
|
||||||
val byCrit = allCrit.map { c ->
|
|
||||||
byCrit_[c.id] ?: Pair(c, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ?: assignment.globalCriterion.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 role: String?, 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> {
|
|
||||||
return evaluation.edition.groups.map { group ->
|
|
||||||
val globalNotes = PeerEvaluationContents.selectAll()
|
|
||||||
.where {
|
|
||||||
(PeerEvaluationContents.groupId eq group.id) and
|
|
||||||
(PeerEvaluationContents.peerEvaluationId eq evaluation.id)
|
|
||||||
}.firstOrNull()?.let {
|
|
||||||
it[PeerEvaluationContents.content]
|
|
||||||
}
|
|
||||||
|
|
||||||
val students = group.students.map { from ->
|
|
||||||
val role = GroupStudents.selectAll().where { (GroupStudents.studentId eq from.id) and (GroupStudents.groupId eq group.id) }.firstOrNull()?.let {
|
|
||||||
it[GroupStudents.role]
|
|
||||||
}
|
|
||||||
val s2g = StudentToGroupEvaluation.selectAll().where {
|
|
||||||
(StudentToGroupEvaluation.peerEvaluationId eq evaluation.id) and
|
|
||||||
(StudentToGroupEvaluation.studentId eq from.id)
|
|
||||||
}.firstOrNull()?.let {
|
|
||||||
Student2StudentEntry(it[StudentToGroupEvaluation.grade], it[StudentToGroupEvaluation.note])
|
|
||||||
}
|
|
||||||
|
|
||||||
val others = group.students.map { other ->
|
|
||||||
val eval = StudentToStudentEvaluation.selectAll().where {
|
|
||||||
(StudentToStudentEvaluation.peerEvaluationId eq evaluation.id) and
|
|
||||||
(StudentToStudentEvaluation.studentIdFrom eq from.id) and
|
|
||||||
(StudentToStudentEvaluation.studentIdTo eq other.id)
|
|
||||||
}.firstOrNull()?.let {
|
|
||||||
Student2StudentEntry(it[StudentToStudentEvaluation.grade], it[StudentToStudentEvaluation.note])
|
|
||||||
}
|
|
||||||
|
|
||||||
other to eval
|
|
||||||
}
|
|
||||||
|
|
||||||
StudentEntry(from, role, s2g, others)
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupEntry(group, globalNotes ?: "", students)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.jaytux.grader.app
|
||||||
|
import com.jaytux.grader.data.v2.*
|
||||||
|
import com.jaytux.grader.ui.AssignmentsTabHeader
|
||||||
|
import com.jaytux.grader.ui.GroupsTabHeader
|
||||||
|
import com.jaytux.grader.ui.StudentsTabHeader
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import org.jetbrains.exposed.v1.core.Expression
|
||||||
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.dao.with
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.select
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
|
class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
|
||||||
|
data class GroupData(val group: Group, val members: List<Pair<Student, String?>>)
|
||||||
|
data class CriterionData(val criterion: Criterion, val gradeType: UiGradeType) {
|
||||||
|
companion object {
|
||||||
|
context(trns: Transaction)
|
||||||
|
fun from(c: Criterion) = CriterionData(c, when(c.gradeType) {
|
||||||
|
GradeType.CATEGORIC -> UiGradeType.Categoric(c.categoricGrade!!.options.toList(), c.categoricGrade!!)
|
||||||
|
GradeType.NUMERIC -> UiGradeType.Numeric(c.numericGrade!!)
|
||||||
|
GradeType.PERCENTAGE -> UiGradeType.Percentage
|
||||||
|
GradeType.NONE -> UiGradeType.FreeText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class AssignmentData(val assignment: BaseAssignment, val global: CriterionData, val criteria: List<CriterionData>)
|
||||||
|
data class GradeSummary(val assignment: BaseAssignment, val asMember: Group?, val overridden: Boolean, val grade: Grade?)
|
||||||
|
enum class Tab(val renderTab: @Composable () -> Unit, val addText: String) {
|
||||||
|
STUDENTS(::StudentsTabHeader, "Student"),
|
||||||
|
GROUPS(::GroupsTabHeader, "Group"),
|
||||||
|
ASSIGNMENTS(::AssignmentsTabHeader, "Assignment")
|
||||||
|
}
|
||||||
|
|
||||||
|
val studentList = RawDbState { edition.students.sortedBy { it.name }.toList() }
|
||||||
|
val groupList = RawDbState {
|
||||||
|
edition.groups.with(Group::students, GroupStudent::student).sortedBy { it.name }.map {
|
||||||
|
GroupData(it, it.students.map { gs -> gs.student to gs.role }.sortedBy { it.first.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val assignmentList = RawDbState {
|
||||||
|
edition.assignments.sortedBy { it.number }.map {
|
||||||
|
AssignmentData(it, CriterionData.from(it.globalCriterion), it.nonBaseCriteria.map { c ->
|
||||||
|
CriterionData.from(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val usedRoles = RawDbState {
|
||||||
|
GroupStudents.select(GroupStudents.role).mapNotNull { it[GroupStudents.role] }.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
val categoricGrades = RawDbState {
|
||||||
|
CategoricGrade.all().map {
|
||||||
|
UiGradeType.Categoric(it.options.toList(), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val numericGrades = RawDbState {
|
||||||
|
NumericGrade.all().map { UiGradeType.Numeric(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val studentGrades = RawDbFocusableState { st: Student ->
|
||||||
|
val groupIds = st.groups.map { it.group.id }.toSet()
|
||||||
|
|
||||||
|
edition.assignments.map { asg ->
|
||||||
|
val (grade, memberOf, override) = when(asg.type) {
|
||||||
|
AssignmentType.GROUP -> {
|
||||||
|
val asGroup = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id in groupIds }
|
||||||
|
val solo = null // asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfGroup.any { over -> over.student == st } } // TODO
|
||||||
|
val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||||
|
gr to asGroup?.asGroupFeedback app (solo != null)
|
||||||
|
}
|
||||||
|
AssignmentType.SOLO -> {
|
||||||
|
val gr = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st }
|
||||||
|
?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||||
|
gr to null app false
|
||||||
|
}
|
||||||
|
AssignmentType.PEER_EVALUATION -> {
|
||||||
|
val asGroup = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id in groupIds }
|
||||||
|
val solo = asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfPeer.any { over -> over.student == st } }
|
||||||
|
val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||||
|
gr to asGroup?.asPeerEvaluationFeedback app (solo != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GradeSummary(asg, memberOf, override, grade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val studentGroups = RawDbFocusableState { st: Student ->
|
||||||
|
st.groups.map { it.group to it.role }
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupAvailableStudents = RawDbFocusableState { grp: Group ->
|
||||||
|
val exclude = grp.students.map { it.student.id }.toSet()
|
||||||
|
edition.students.filterNot { it.id in exclude }
|
||||||
|
}
|
||||||
|
val groupGrades = RawDbFocusableState { g: Group ->
|
||||||
|
edition.assignments.filter{ it.type != AssignmentType.SOLO }.map { asg ->
|
||||||
|
val grade = when(asg.type) {
|
||||||
|
AssignmentType.GROUP -> {
|
||||||
|
val asGroup = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id == g.id }
|
||||||
|
asGroup?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||||
|
}
|
||||||
|
AssignmentType.PEER_EVALUATION -> {
|
||||||
|
val asGroup = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == g.id }
|
||||||
|
asGroup?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
asg to grade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _selectedTab = mutableStateOf(Tab.STUDENTS)
|
||||||
|
private val _focusIndex = mutableStateOf(-1)
|
||||||
|
val selectedTab = _selectedTab.immutable()
|
||||||
|
val focusIndex = _focusIndex.immutable()
|
||||||
|
|
||||||
|
fun switchTo(tab: Tab) {
|
||||||
|
_selectedTab.value = tab
|
||||||
|
_focusIndex.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focus(idx: Int) {
|
||||||
|
_focusIndex.value = idx
|
||||||
|
|
||||||
|
when(_selectedTab.value) {
|
||||||
|
Tab.STUDENTS -> {
|
||||||
|
val st = studentList.entities.value[idx]
|
||||||
|
studentGrades.focus(st)
|
||||||
|
studentGroups.focus(st)
|
||||||
|
}
|
||||||
|
Tab.GROUPS -> {
|
||||||
|
val grp = groupList.entities.value[idx].group
|
||||||
|
groupAvailableStudents.focus(grp)
|
||||||
|
groupGrades.focus(grp)
|
||||||
|
}
|
||||||
|
Tab.ASSIGNMENTS -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focus(group: Group) {
|
||||||
|
val idx = groupList.entities.value.indexOfFirst { it.group.id == group.id }
|
||||||
|
if(idx != -1) {
|
||||||
|
switchTo(Tab.GROUPS)
|
||||||
|
focus(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focus(student: Student) {
|
||||||
|
val idx = studentList.entities.value.indexOfFirst { it.id == student.id }
|
||||||
|
if(idx != -1) {
|
||||||
|
switchTo(Tab.STUDENTS)
|
||||||
|
focus(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unfocus() {
|
||||||
|
_focusIndex.value = -1
|
||||||
|
|
||||||
|
studentGrades.unfocus()
|
||||||
|
studentGroups.unfocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkStudent(name: String, contact: String, note: String) {
|
||||||
|
transaction {
|
||||||
|
val s = Student.new {
|
||||||
|
this.name = name
|
||||||
|
this.contact = contact
|
||||||
|
this.note = note
|
||||||
|
}
|
||||||
|
EditionStudents.insert {
|
||||||
|
it[EditionStudents.editionId] = edition.id
|
||||||
|
it[EditionStudents.studentId] = s.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unfocus()
|
||||||
|
studentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modStudent(student: Student, name: String?, contact: String?, note: String?) {
|
||||||
|
transaction {
|
||||||
|
student.name = name ?: student.name
|
||||||
|
student.contact = contact ?: student.contact
|
||||||
|
student.note = note ?: student.note
|
||||||
|
}
|
||||||
|
studentList.refresh()
|
||||||
|
studentGroups.refresh()
|
||||||
|
studentGrades.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmStudent(student: Student) {
|
||||||
|
transaction {
|
||||||
|
student.delete()
|
||||||
|
}
|
||||||
|
unfocus()
|
||||||
|
studentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkGroup(name: String) {
|
||||||
|
transaction {
|
||||||
|
Group.new {
|
||||||
|
this.name = name
|
||||||
|
this.edition = this@EditionVM.edition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unfocus()
|
||||||
|
groupList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modGroup(group: Group, name: String?) {
|
||||||
|
transaction {
|
||||||
|
group.name = name ?: group.name
|
||||||
|
}
|
||||||
|
groupList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addStudentToGroup(student: Student, group: Group, role: String?) {
|
||||||
|
transaction {
|
||||||
|
GroupStudent.new {
|
||||||
|
this.student = student
|
||||||
|
this.group = group
|
||||||
|
this.role = role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupList.refresh()
|
||||||
|
studentGroups.refresh()
|
||||||
|
groupAvailableStudents.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStudentRole(student: Student, group: Group, role: String?) {
|
||||||
|
transaction {
|
||||||
|
GroupStudent.find { (GroupStudents.studentId eq student.id) and (GroupStudents.groupId eq group.id) }.firstOrNull()?.let {
|
||||||
|
it.role = role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupList.refresh()
|
||||||
|
groupAvailableStudents.refresh()
|
||||||
|
usedRoles.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmStudentFromGroup(student: Student, group: Group) {
|
||||||
|
transaction {
|
||||||
|
GroupStudent.find { (GroupStudents.studentId eq student.id) and (GroupStudents.groupId eq group.id) }.firstOrNull()?.delete()
|
||||||
|
}
|
||||||
|
groupList.refresh()
|
||||||
|
groupAvailableStudents.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmGroup(group: Group) {
|
||||||
|
transaction {
|
||||||
|
group.delete()
|
||||||
|
}
|
||||||
|
unfocus()
|
||||||
|
groupList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Transaction.mkBaseAssignment(name: String, type: AssignmentType): BaseAssignment {
|
||||||
|
val asg = BaseAssignment.new {
|
||||||
|
this.name = name
|
||||||
|
this.assignment = ""
|
||||||
|
this.deadline = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
this.number = assignmentList.entities.value.size
|
||||||
|
this.edition = this@EditionVM.edition
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
val crit = Criterion.new {
|
||||||
|
this.assignment = asg
|
||||||
|
this.name = "(Default Criterion)"
|
||||||
|
this.desc = "Default criterion for assignment ${asg.name}"
|
||||||
|
this.gradeType = GradeType.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
asg.globalCriterion = crit
|
||||||
|
return asg
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postCreateAsg() {
|
||||||
|
focus(assignmentList.entities.value.size)
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkGroupAssignment(name: String) {
|
||||||
|
transaction {
|
||||||
|
val asg = mkBaseAssignment(name, AssignmentType.GROUP)
|
||||||
|
GroupAssignment.new { this.base = asg }
|
||||||
|
}
|
||||||
|
postCreateAsg()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkSoloAssignment(name: String) {
|
||||||
|
transaction {
|
||||||
|
val asg = mkBaseAssignment(name, AssignmentType.SOLO)
|
||||||
|
SoloAssignment.new { this.base = asg }
|
||||||
|
}
|
||||||
|
postCreateAsg()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkPeerEvaluation(name: String) {
|
||||||
|
transaction {
|
||||||
|
val asg = mkBaseAssignment(name, AssignmentType.PEER_EVALUATION)
|
||||||
|
PeerEvaluation.new { this.base = asg }
|
||||||
|
}
|
||||||
|
postCreateAsg()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkAssignment(name: String, type: AssignmentType) {
|
||||||
|
when(type) {
|
||||||
|
AssignmentType.GROUP -> mkGroupAssignment(name)
|
||||||
|
AssignmentType.SOLO -> mkSoloAssignment(name)
|
||||||
|
AssignmentType.PEER_EVALUATION -> mkPeerEvaluation(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modAssignment(assignment: BaseAssignment, name: String?, deadline: LocalDateTime?) {
|
||||||
|
transaction {
|
||||||
|
assignment.name = name ?: assignment.name
|
||||||
|
assignment.deadline = deadline ?: assignment.deadline
|
||||||
|
}
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDesc(assignment: AssignmentData, desc: String) {
|
||||||
|
transaction {
|
||||||
|
assignment.global.criterion.desc = desc
|
||||||
|
}
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkCriterion(assignment: BaseAssignment, name: String, desc: String, gradeType: UiGradeType) {
|
||||||
|
transaction {
|
||||||
|
val crit = Criterion.new {
|
||||||
|
this.assignment = assignment
|
||||||
|
this.name = name
|
||||||
|
this.desc = desc
|
||||||
|
this.gradeType = when(gradeType) {
|
||||||
|
is UiGradeType.Categoric -> GradeType.CATEGORIC
|
||||||
|
is UiGradeType.Numeric -> GradeType.NUMERIC
|
||||||
|
UiGradeType.Percentage -> GradeType.PERCENTAGE
|
||||||
|
UiGradeType.FreeText -> GradeType.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when(gradeType) {
|
||||||
|
is UiGradeType.Categoric -> crit.categoricGrade = gradeType.grade
|
||||||
|
is UiGradeType.Numeric -> crit.numericGrade = gradeType.grade
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modCriterion(crit: Criterion, name: String?, desc: String?, gradeType: UiGradeType?) {
|
||||||
|
transaction {
|
||||||
|
crit.name = name ?: crit.name
|
||||||
|
crit.desc = desc ?: crit.desc
|
||||||
|
crit.gradeType = when(gradeType) {
|
||||||
|
null -> crit.gradeType
|
||||||
|
is UiGradeType.Categoric -> GradeType.CATEGORIC
|
||||||
|
is UiGradeType.Numeric -> GradeType.NUMERIC
|
||||||
|
UiGradeType.Percentage -> GradeType.PERCENTAGE
|
||||||
|
UiGradeType.FreeText -> GradeType.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
when(gradeType) {
|
||||||
|
is UiGradeType.Categoric -> crit.categoricGrade = gradeType.grade
|
||||||
|
is UiGradeType.Numeric -> crit.numericGrade = gradeType.grade
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkScale(name: String, options: List<String>) {
|
||||||
|
transaction {
|
||||||
|
val grade = CategoricGrade.new { this.name = name }
|
||||||
|
options.forEachIndexed { idx, opt ->
|
||||||
|
CategoricGradeOption.new {
|
||||||
|
this.grade = grade
|
||||||
|
this.option = opt
|
||||||
|
this.index = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categoricGrades.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkNumericScale(name: String, max: Double) {
|
||||||
|
transaction {
|
||||||
|
NumericGrade.new {
|
||||||
|
this.name = name
|
||||||
|
this.max = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numericGrades.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmAssignment(assignment: BaseAssignment) {
|
||||||
|
transaction {
|
||||||
|
assignment.delete()
|
||||||
|
(assignment.asPeerEvaluation ?: assignment.asGroupAssignment ?: assignment.asSoloAssignment)?.delete()
|
||||||
|
}
|
||||||
|
unfocus()
|
||||||
|
assignmentList.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.jaytux.grader.data.v2.BaseFeedback
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGrade
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGradeOption
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGradeOptions
|
||||||
|
import com.jaytux.grader.data.v2.Criterion
|
||||||
|
import com.jaytux.grader.data.v2.GradeType
|
||||||
|
import com.jaytux.grader.data.v2.NumericGrade
|
||||||
|
import com.jaytux.grader.maxN
|
||||||
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
|
sealed class Grade {
|
||||||
|
data class FreeText(val text: String) : Grade()
|
||||||
|
data class Percentage(val percentage: Double) : Grade()
|
||||||
|
data class Numeric(val value: Double, val grade: NumericGrade) : Grade()
|
||||||
|
data class Categoric(val value: CategoricGradeOption, val options: List<CategoricGradeOption>, val grade: CategoricGrade) : Grade()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun render(modifier: Modifier = Modifier) = when(this) {
|
||||||
|
is FreeText -> Text(text.maxN(15), modifier)
|
||||||
|
is Categoric -> Text(value.option, modifier)
|
||||||
|
is Numeric -> Text("$value / ${grade.max}", modifier)
|
||||||
|
is Percentage -> Text("$percentage%", modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
context(trns: Transaction)
|
||||||
|
fun fromAssignment(asg: Criterion, fdb: BaseFeedback): Grade = when(asg.gradeType) {
|
||||||
|
GradeType.CATEGORIC ->
|
||||||
|
Categoric(fdb.gradeCategoric!!, asg.categoricGrade!!.options.toList(), asg.categoricGrade!!)
|
||||||
|
|
||||||
|
GradeType.NUMERIC -> Numeric(fdb.gradeNumeric!!, asg.numericGrade!!)
|
||||||
|
GradeType.PERCENTAGE -> Percentage(fdb.gradeNumeric!!)
|
||||||
|
GradeType.NONE -> FreeText(fdb.gradeFreeText!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultFreeText() = FreeText("")
|
||||||
|
fun defaultPercentage() = Percentage(0.0)
|
||||||
|
fun defaultNumeric(grade: NumericGrade) = Numeric(0.0, grade)
|
||||||
|
fun defaultCategoric(grade: CategoricGrade, options: List<CategoricGradeOption>) = Categoric(options.first(), options, grade)
|
||||||
|
|
||||||
|
fun default(type: GradeType, cat: CategoricGrade?, num: NumericGrade?) = when(type) {
|
||||||
|
GradeType.CATEGORIC -> transaction {
|
||||||
|
cat!!
|
||||||
|
defaultCategoric(cat, cat.options.toList())
|
||||||
|
}
|
||||||
|
GradeType.NUMERIC -> defaultNumeric(num!!)
|
||||||
|
GradeType.PERCENTAGE -> defaultPercentage()
|
||||||
|
GradeType.NONE -> defaultFreeText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
|
import com.jaytux.grader.data.v2.BaseFeedback
|
||||||
|
import com.jaytux.grader.data.v2.BaseFeedbacks
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGrade
|
||||||
|
import com.jaytux.grader.data.v2.Course
|
||||||
|
import com.jaytux.grader.data.v2.Criterion
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
import com.jaytux.grader.data.v2.GradeType
|
||||||
|
import com.jaytux.grader.data.v2.Group
|
||||||
|
import com.jaytux.grader.data.v2.GroupAssignment
|
||||||
|
import com.jaytux.grader.data.v2.GroupFeedbacks
|
||||||
|
import com.jaytux.grader.data.v2.GroupStudent
|
||||||
|
import com.jaytux.grader.data.v2.NumericGrade
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import com.jaytux.grader.data.v2.StudentOverrideFeedback
|
||||||
|
import com.jaytux.grader.data.v2.StudentOverrideFeedbacks
|
||||||
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.dao.with
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.upsertReturning
|
||||||
|
|
||||||
|
class GroupsGradingVM(val course: Course, val edition: Edition, val base: BaseAssignment) : ViewModel() {
|
||||||
|
data class GroupData(val group: Group, val students: List<Pair<Student, String?>>)
|
||||||
|
data class FeedbackItem(val base: BaseFeedback, val grade: Grade, val feedback: String) {
|
||||||
|
companion object {
|
||||||
|
context(trns: Transaction)
|
||||||
|
fun fromDb(f: BaseFeedback): FeedbackItem = when(f.criterion.gradeType) {
|
||||||
|
GradeType.CATEGORIC -> {
|
||||||
|
val categoric = f.criterion.categoricGrade!!
|
||||||
|
val options = categoric.options.toList()
|
||||||
|
Grade.Categoric(f.gradeCategoric ?: options.first(), options, categoric)
|
||||||
|
}
|
||||||
|
GradeType.NUMERIC -> Grade.Numeric(f.gradeNumeric ?: 0.0, f.criterion.numericGrade!!)
|
||||||
|
GradeType.PERCENTAGE -> Grade.Percentage(f.gradeNumeric ?: 0.0)
|
||||||
|
GradeType.NONE -> Grade.FreeText(f.gradeFreeText ?: "")
|
||||||
|
}.let { FeedbackItem(f, it, f.feedback) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class FeedbackData(val groupLevel: FeedbackItem?, val overrides: List<Pair<Student, FeedbackItem?>>)
|
||||||
|
data class CritData(val criterion: Criterion, val cat: CategoricGrade?, val num: NumericGrade?) {
|
||||||
|
companion object {
|
||||||
|
context(trns: Transaction)
|
||||||
|
fun fromDb(c: Criterion) = CritData(c, c.categoricGrade, c.numericGrade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _focus = mutableStateOf(-1)
|
||||||
|
val focus = _focus.immutable()
|
||||||
|
|
||||||
|
val asGroup = transaction { base.asGroupAssignment!! }
|
||||||
|
val global = transaction { CritData.fromDb(base.globalCriterion) }
|
||||||
|
val groupList = RawDbState {
|
||||||
|
edition.groups.with(Group::students, GroupStudent::student).map { group ->
|
||||||
|
GroupData(group, group.students.map { Pair(it.student, it.role) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val globalGrade = RawDbFocusableSingleState { group: Group ->
|
||||||
|
val g = base.globalCriterion.feedbacks.find { it.asGroupFeedback?.id == group.id }?.let { FeedbackItem.fromDb(it) }
|
||||||
|
val overrides = g?.let { gl -> getOverrides(group, gl.base) } ?: group.students.map { it.student to null }
|
||||||
|
FeedbackData(g, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
val gradeList = RawDbFocusableState { group: Group ->
|
||||||
|
base.nonBaseCriteria.map { crit ->
|
||||||
|
val groupLevel = crit.feedbacks.find { it.asGroupFeedback?.id == group.id }?.let { FeedbackItem.fromDb(it) }
|
||||||
|
val overrides = groupLevel?.let { gl -> getOverrides(group, gl.base) } ?: group.students.map { it.student to null }
|
||||||
|
|
||||||
|
CritData.fromDb(crit) to FeedbackData(groupLevel, overrides)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Transaction.getOverrides(group: Group, fd: BaseFeedback): List<Pair<Student, FeedbackItem?>> {
|
||||||
|
// val feedbacks = fd.forStudentsOverrideIfGroup.filter { it.group.id == group.id }.associateBy { it.student.id }
|
||||||
|
//
|
||||||
|
// return group.students.map {
|
||||||
|
// it.student to feedbacks[it.student.id]?.let { sof -> FeedbackItem.fromDb(sof.feedback) }
|
||||||
|
// }
|
||||||
|
return listOf() // TODO!!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focusGroup(idx: Int) {
|
||||||
|
_focus.value = idx
|
||||||
|
val group = groupList.entities.value[idx].group
|
||||||
|
globalGrade.focus(group)
|
||||||
|
gradeList.focus(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focusPrev() {
|
||||||
|
if (focus.value > 0) {
|
||||||
|
focusGroup(focus.value - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun focusNext() {
|
||||||
|
if (focus.value < groupList.entities.value.size - 1) {
|
||||||
|
focusGroup(focus.value + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(trns: Transaction)
|
||||||
|
private fun BaseFeedback.set(grade: Grade, feedback: String) {
|
||||||
|
this.feedback = feedback
|
||||||
|
when(grade) {
|
||||||
|
is Grade.Categoric -> this.gradeCategoric = grade.value
|
||||||
|
is Grade.FreeText -> this.gradeFreeText = grade.text
|
||||||
|
is Grade.Numeric -> this.gradeNumeric = grade.value
|
||||||
|
is Grade.Percentage -> this.gradeNumeric = grade.percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modGroupFeedback(crit: Criterion, group: Group, grade: Grade, feedback: String) {
|
||||||
|
transaction {
|
||||||
|
val existing = group.feedbacks.find { f -> f.criterion.id == crit.id }
|
||||||
|
if(existing != null) {
|
||||||
|
existing.set(grade, feedback)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val fdb = BaseFeedback.new {
|
||||||
|
criterion = crit
|
||||||
|
set(grade, feedback)
|
||||||
|
}
|
||||||
|
GroupFeedbacks.insert {
|
||||||
|
it[GroupFeedbacks.feedbackId] = fdb.id
|
||||||
|
it[GroupFeedbacks.groupId] = group.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalGrade.refresh()
|
||||||
|
gradeList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modOverrideFeedback(crit: Criterion, group: Group, student: Student, groupLevel: FeedbackItem, grade: Grade, feedback: String) {
|
||||||
|
transaction {
|
||||||
|
val existing = groupLevel.base.forStudentsOverrideIfGroup.find { it.student.id == student.id }
|
||||||
|
if(existing != null) {
|
||||||
|
existing.feedback.set(grade, feedback)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val fdb = BaseFeedback.new {
|
||||||
|
criterion = crit
|
||||||
|
set(grade, feedback)
|
||||||
|
}
|
||||||
|
StudentOverrideFeedback.new {
|
||||||
|
this.group = group
|
||||||
|
this.student = student
|
||||||
|
this.feedback = fdb
|
||||||
|
this.overrides = groupLevel.base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalGrade.refresh()
|
||||||
|
gradeList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmOverrideFeedback(crit: Criterion, group: Group, student: Student) {
|
||||||
|
transaction {
|
||||||
|
crit.feedbacks.find {
|
||||||
|
it.asGroupFeedback!!.id == group.id // find relevant group-level feedback
|
||||||
|
}?.forStudentsOverrideIfGroup?.find {
|
||||||
|
it.student.id == student.id // find override for the student
|
||||||
|
}?.delete()
|
||||||
|
}
|
||||||
|
globalGrade.refresh()
|
||||||
|
gradeList.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
|
import com.jaytux.grader.data.v2.Course
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
import com.jaytux.grader.data.v2.Group
|
||||||
|
import com.jaytux.grader.data.v2.Student
|
||||||
|
import org.jetbrains.exposed.v1.dao.with
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
|
class HomeVM : ViewModel() {
|
||||||
|
data class EditionData(val edition: Edition, val students: List<Student>, val groups: List<Group>, val assignments: List<BaseAssignment>)
|
||||||
|
data class CourseData(val course: Course, val editions: List<EditionData>, val archived: List<EditionData>)
|
||||||
|
|
||||||
|
val courses = RawDbState {
|
||||||
|
Course.all().with(Course::editions, Edition::students, Edition::groups, Edition::assignments).map {
|
||||||
|
val mkEditionData = { e: Edition ->
|
||||||
|
EditionData(e, e.students.toList(), e.groups.toList(), e.assignments.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
CourseData(it, it.editions.filter { e -> !e.archived }.map(mkEditionData), it.editions.filter { e -> e.archived }.map(mkEditionData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkCourse(name: String) {
|
||||||
|
transaction {
|
||||||
|
Course.new { this.name = name }
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmCourse(course: Course) {
|
||||||
|
transaction {
|
||||||
|
course.delete()
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkEdition(course: Course, name: String) {
|
||||||
|
transaction {
|
||||||
|
Edition.new {
|
||||||
|
this.course = course
|
||||||
|
this.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmEdition(edition: Edition) {
|
||||||
|
transaction {
|
||||||
|
edition.delete()
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun archiveEdition(edition: Edition) {
|
||||||
|
transaction {
|
||||||
|
edition.archived = true
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unarchiveEdition(edition: Edition) {
|
||||||
|
transaction {
|
||||||
|
edition.archived = false
|
||||||
|
}
|
||||||
|
courses.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.backhandler.BackHandler
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.jaytux.grader.ui.ChevronLeft
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class Navigator private constructor(
|
||||||
|
private var _start: IDestination,
|
||||||
|
private val _typeMap: Map<KClass<out IDestination>, RenderData>
|
||||||
|
) : ViewModel() {
|
||||||
|
interface IDestination
|
||||||
|
|
||||||
|
private data class Entry<T : IDestination>(val dest: T, val token: NavToken)
|
||||||
|
inner class NavToken {
|
||||||
|
fun navTo(target: IDestination) { this@Navigator.navTo(target) }
|
||||||
|
fun back() { this@Navigator.back() }
|
||||||
|
inline fun <reified T : IDestination> backTo() { this@Navigator.backTo<T>() }
|
||||||
|
fun rewriteHistory(t: IDestination) { this@Navigator.rewriteHistory(t) }
|
||||||
|
}
|
||||||
|
internal data class RenderData(
|
||||||
|
val header: @Composable (IDestination) -> Unit,
|
||||||
|
val renderer: @Composable (IDestination, NavToken) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _stack = mutableStateOf(listOf<Entry<*>>(Entry(_start, NavToken())))
|
||||||
|
|
||||||
|
fun navTo(target: IDestination) {
|
||||||
|
_stack.value += Entry(target, NavToken())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun back() {
|
||||||
|
if(_stack.value.size > 1) _stack.value = _stack.value.dropLast(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : IDestination> backTo(cls: KClass<T>) {
|
||||||
|
val idx = _stack.value.indexOfLast { entry -> cls.isInstance(entry.dest) }
|
||||||
|
if(idx != -1 && idx != _stack.value.lastIndex) {
|
||||||
|
_stack.value = _stack.value.take(idx + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rewriteHistory(t: IDestination) {
|
||||||
|
_stack.value = listOf(Entry(t, NavToken()))
|
||||||
|
_start = t
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : IDestination> backTo() = backTo(T::class)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DisplayScaffold() {
|
||||||
|
val state = remember { SnackbarHostState() }
|
||||||
|
val stack by _stack
|
||||||
|
val (top, render) = remember(stack) {
|
||||||
|
val top = stack.last()
|
||||||
|
val render = _typeMap[top.dest::class]
|
||||||
|
?: throw IllegalStateException("No renderer for destination of type ${top.dest::class.simpleName}")
|
||||||
|
top to render
|
||||||
|
}
|
||||||
|
val snackVM = viewModel<SnackVM>()
|
||||||
|
snackVM.Launcher(state)
|
||||||
|
|
||||||
|
BackHandler { back() }
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
title = { render.header(top.dest) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton({ back() }, enabled = top != _start) {
|
||||||
|
Icon(ChevronLeft, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(state)
|
||||||
|
}
|
||||||
|
) { insets ->
|
||||||
|
Surface(Modifier.padding(insets), color = MaterialTheme.colorScheme.surface) {
|
||||||
|
render.renderer(top.dest, top.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
annotation class NavigatorDslMarker
|
||||||
|
|
||||||
|
@NavigatorDslMarker
|
||||||
|
class Builder internal constructor(
|
||||||
|
private val _onBuild: (IDestination, Map<KClass<out IDestination>, RenderData>) -> Navigator
|
||||||
|
) {
|
||||||
|
private val _typeMap = mutableMapOf<KClass<out IDestination>, RenderData>()
|
||||||
|
private lateinit var _start: IDestination
|
||||||
|
|
||||||
|
fun <T : IDestination> composable(cls: KClass<T>, title: @Composable (T) -> Unit, renderer: @Composable (T, NavToken) -> Unit) {
|
||||||
|
_typeMap[cls]?.let {
|
||||||
|
throw IllegalArgumentException("Destination of type ${cls.simpleName} is already registered.")
|
||||||
|
} ?: run {
|
||||||
|
_typeMap[cls] = RenderData({
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
title(it as T)
|
||||||
|
}) { d, t ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
renderer(d as T, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : IDestination> composable(noinline title: @Composable (T) -> Unit, noinline renderer: @Composable (T, NavToken) -> Unit) {
|
||||||
|
composable(T::class, title, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(start: IDestination) {
|
||||||
|
if(this::_start.isInitialized) throw IllegalStateException("Start destination is already set.")
|
||||||
|
_start = start
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun build(): Navigator {
|
||||||
|
if(!this::_start.isInitialized) throw IllegalStateException("Start destination is not set.")
|
||||||
|
return _onBuild(_start, _typeMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun build(block: Builder.() -> Unit): Navigator =
|
||||||
|
Builder { start, map -> Navigator(start, map) }
|
||||||
|
.apply { block() }.build()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavHost(initial: IDestination, block: Builder.() -> Unit) {
|
||||||
|
val vm = viewModel {
|
||||||
|
build {
|
||||||
|
block()
|
||||||
|
start(initial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.DisplayScaffold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
|
import com.jaytux.grader.data.v2.Course
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
|
||||||
|
class PeerEvalsGradingVM(val course: Course, val edition: Edition, val base: BaseAssignment) : ViewModel() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SnackVM : ViewModel() {
|
||||||
|
private val _snacks = Channel<String>()
|
||||||
|
val snacks = _snacks.receiveAsFlow()
|
||||||
|
|
||||||
|
fun show(msg: String): Unit {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_snacks.send(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Launcher(state: SnackbarHostState) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snacks.collect { snack -> state.showSnackbar(snack) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.jaytux.grader.data.v2.BaseAssignment
|
||||||
|
import com.jaytux.grader.data.v2.Course
|
||||||
|
import com.jaytux.grader.data.v2.Edition
|
||||||
|
|
||||||
|
class SolosGradingVM(val course: Course, val edition: Edition, val base: BaseAssignment) : ViewModel() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.jaytux.grader.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.jaytux.grader.data.v2.BaseFeedback
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGrade
|
||||||
|
import com.jaytux.grader.data.v2.CategoricGradeOption
|
||||||
|
import com.jaytux.grader.data.v2.Criterion
|
||||||
|
import com.jaytux.grader.data.v2.GradeType
|
||||||
|
import com.jaytux.grader.data.v2.NumericGrade
|
||||||
|
import com.jaytux.grader.maxN
|
||||||
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
|
|
||||||
|
sealed class UiGradeType {
|
||||||
|
object FreeText : UiGradeType()
|
||||||
|
object Percentage : UiGradeType()
|
||||||
|
data class Numeric(val grade: NumericGrade) : UiGradeType()
|
||||||
|
data class Categoric(val options: List<CategoricGradeOption>, val grade: CategoricGrade) : UiGradeType()
|
||||||
|
}
|
||||||
@@ -1,33 +1,38 @@
|
|||||||
[versions]
|
[versions]
|
||||||
androidx-lifecycle = "2.8.4"
|
androidx-lifecycle = "2.9.6"
|
||||||
compose-multiplatform = "1.8.1"
|
compose-multiplatform = "1.9.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlin = "2.1.0"
|
kotlin = "2.3.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.1"
|
||||||
exposed = "0.59.0"
|
exposed = "1.1.1"
|
||||||
material3 = "1.7.3"
|
material3 = "1.9.0"
|
||||||
ui-android = "1.7.8"
|
ui-android = "1.7.8"
|
||||||
foundation-layout-android = "1.7.8"
|
foundation-layout-android = "1.7.8"
|
||||||
rtf = "1.0.0-rc11"
|
rtf = "1.0.0-rc11"
|
||||||
filekit = "0.10.0-beta04"
|
filekit = "0.10.0-beta04"
|
||||||
|
directories = "26"
|
||||||
|
androidx-activity-compose = "1.12.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
|
||||||
|
compose-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" }
|
||||||
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
|
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
|
||||||
exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" }
|
exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" }
|
||||||
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-migration = { group = "org.jetbrains.exposed", name = "exposed-migration", version.ref = "exposed" }
|
exposed-migration = { group = "org.jetbrains.exposed", name = "exposed-migration-core", version.ref = "exposed" }
|
||||||
|
exposed-migration-jdbc = { group = "org.jetbrains.exposed", name = "exposed-migration-jdbc", version.ref = "exposed" }
|
||||||
exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" }
|
exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
|
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
|
||||||
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
||||||
material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" }
|
material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" }
|
||||||
material3-desktop = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" }
|
material3-desktop = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" }
|
||||||
material-icons = { group = "org.jetbrains.compose.material", name = "material-icons-extended", version.ref = "material3" }
|
|
||||||
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
||||||
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
||||||
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
|
rtfield = { group = "com.mohamedrejeb.richeditor", name = "richeditor-compose", version.ref = "rtf" }
|
||||||
@@ -35,6 +40,7 @@ filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.re
|
|||||||
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" }
|
||||||
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" }
|
||||||
filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" }
|
filekit-coil = { group = "io.github.vinceglb", name = "filekit-coil", version.ref = "filekit" }
|
||||||
|
directories = { group = "dev.dirs", name = "directories", version.ref = "directories" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
12
gradlew
vendored
12
gradlew
vendored
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -173,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -206,15 +203,14 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
3
gradlew.bat
vendored
3
gradlew.bat
vendored
@@ -70,11 +70,10 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
Reference in New Issue
Block a user