More grading updates

This commit is contained in:
2026-06-02 17:29:13 +02:00
parent bdc56748dd
commit a48ca56156
12 changed files with 446 additions and 268 deletions
@@ -9,6 +9,8 @@ import com.jaytux.grader.data.v2.Courses
import com.jaytux.grader.data.v2.NumericGrade import com.jaytux.grader.data.v2.NumericGrade
import com.jaytux.grader.data.v2.v2Tables import com.jaytux.grader.data.v2.v2Tables
import dev.dirs.ProjectDirectories import dev.dirs.ProjectDirectories
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import kotlin.getValue import kotlin.getValue
@@ -18,6 +20,9 @@ import kotlin.io.path.exists
import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.batchInsert import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import org.jetbrains.exposed.v1.jdbc.update
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
import java.util.UUID
object Database { object Database {
val dataDir: String = ProjectDirectories.from("com", "jaytux", "grader").dataDir.also { val dataDir: String = ProjectDirectories.from("com", "jaytux", "grader").dataDir.also {
@@ -43,6 +48,8 @@ object Database {
it[CategoricGrades.id] it[CategoricGrades.id]
} }
var passId: EntityID<UUID>? = null
var bId: EntityID<UUID>? = null
CategoricGradeOptions.batchInsert( CategoricGradeOptions.batchInsert(
listOf("Pass", "Fail").mapIndexed { idx, it -> it to pf app idx } + 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 } listOf("A (Excellent)", "B (Good)", "C (Satisfactory)", "D (Poor)", "F (Fail)").mapIndexed { idx, it -> it to af app idx }
@@ -50,9 +57,17 @@ object Database {
this[CategoricGradeOptions.option] = it.first this[CategoricGradeOptions.option] = it.first
this[CategoricGradeOptions.gradeId] = it.second this[CategoricGradeOptions.gradeId] = it.second
this[CategoricGradeOptions.index] = it.third this[CategoricGradeOptions.index] = it.third
}.forEach {
when(it[CategoricGradeOptions.option]) {
"Pass" -> passId = it[CategoricGradeOptions.id]
"B (Good)" -> bId = it[CategoricGradeOptions.id]
} }
} }
CategoricGrades.update(where = { CategoricGrades.id eq pf }) { it[CategoricGrades.defaultOption] = passId!! }
CategoricGrades.update(where = { CategoricGrades.id eq af }) { it[CategoricGrades.defaultOption] = bId!! }
}
if(NumericGrade.count() == 0L) { if(NumericGrade.count() == 0L) {
NumericGrade.new { NumericGrade.new {
name = "Max-20" name = "Max-20"
@@ -142,6 +142,7 @@ object PeerEvaluationS2SEvaluations : UUIDTable("peerEvalS2SEvals") {
object CategoricGrades : UUIDTable("categoricGrades") { object CategoricGrades : UUIDTable("categoricGrades") {
val name = varchar("name", 50).uniqueIndex() val name = varchar("name", 50).uniqueIndex()
val defaultOption = reference("default_option_id", CategoricGradeOptions.id)
} }
object CategoricGradeOptions : UUIDTable("categoricGradeOpts") { object CategoricGradeOptions : UUIDTable("categoricGradeOpts") {
@@ -101,6 +101,7 @@ class CategoricGrade(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : EntityClass<UUID, CategoricGrade>(CategoricGrades) companion object : EntityClass<UUID, CategoricGrade>(CategoricGrades)
var name by CategoricGrades.name var name by CategoricGrades.name
var default by CategoricGradeOption referencedOn CategoricGrades.defaultOption
val options by CategoricGradeOption referrersOn CategoricGradeOptions.gradeId orderBy CategoricGradeOptions.index val options by CategoricGradeOption referrersOn CategoricGradeOptions.gradeId orderBy CategoricGradeOptions.index
} }
@@ -20,6 +20,9 @@ import com.jaytux.grader.GroupGrading
import com.jaytux.grader.PeerEvalGrading import com.jaytux.grader.PeerEvalGrading
import com.jaytux.grader.SoloGrading import com.jaytux.grader.SoloGrading
import com.jaytux.grader.data.v2.AssignmentType import com.jaytux.grader.data.v2.AssignmentType
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.viewmodel.EditionVM import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.Navigator
import com.jaytux.grader.viewmodel.UiGradeType import com.jaytux.grader.viewmodel.UiGradeType
@@ -47,6 +50,8 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
var addingRubric by remember { mutableStateOf(false) } var addingRubric by remember { mutableStateOf(false) }
var editingRubric by remember { mutableStateOf(-1) } var editingRubric by remember { mutableStateOf(-1) }
var updatingGrade by remember { mutableStateOf(false) } var updatingGrade by remember { mutableStateOf(false) }
var renaming by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(false) }
val navToGrading = lambda@{ val navToGrading = lambda@{
if(assignment == null) return@lambda if(assignment == null) return@lambda
@@ -58,7 +63,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
} }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(assignments, { Text("No assignments yet.") }) { idx, it ->
QuickAssignment(idx, it, vm) QuickAssignment(idx, it, vm)
} }
} }
@@ -73,8 +78,21 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
val peerEvalData by vm.asPeerEvaluation.entity val peerEvalData by vm.asPeerEvaluation.entity
var updatingPeerEvalGrade by remember { mutableStateOf(false) } var updatingPeerEvalGrade by remember { mutableStateOf(false) }
Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium) Column {
Row(Modifier.height(IntrinsicSize.Min)) {
EditableText(
assignment.assignment.name, style = MaterialTheme.typography.headlineMedium,
canSave = { it.isNotBlank() && (it == assignment.assignment.name || !assignments.any { x -> x.assignment.name == it }) }
) {
vm.modAssignment(assignment.assignment, it, null)
}
Spacer(Modifier.width(10.dp))
IconButton(Delete, "Delete assignment", Modifier.align(Alignment.CenterVertically)) {
deleting = true
}
}
Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic) Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic)
}
Row { Row {
Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically)) Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically))
Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) {
@@ -192,6 +210,15 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
} }
} }
} }
if(deleting) {
if(assignment == null) deleting = false
else {
ConfirmDeleteDialog("an assignment", { deleting = false }, { vm.rmAssignment(assignment.assignment) }) {
Text("${assignment.assignment.type.display} \"${assignment.assignment.name}\"")
}
}
}
} }
val fmt = LocalDateTime.Format { val fmt = LocalDateTime.Format {
@@ -300,7 +327,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken:
OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, 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)) { Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, vm::mkScale, vm::modScale, vm::mkNumericScale, Modifier.weight(1f)) { type = it }
CancelSaveRow(name.isNotBlank() && (name !in taken || name == current?.criterion?.name), onClose) { CancelSaveRow(name.isNotBlank() && (name !in taken || name == current?.criterion?.name), onClose) {
onSave(name, desc, type) onSave(name, desc, type)
@@ -331,7 +358,7 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
Text("Select a grading scale for $name", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 10.dp)) 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)) { Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) {
Column { Column {
GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } GradeTypePicker(type, categories, numeric, vm::mkScale, vm::modScale, vm::mkNumericScale, Modifier.weight(1f)) { type = it }
CancelSaveRow(true, onClose) { CancelSaveRow(true, onClose) {
onSave(type) onSave(type)
@@ -349,7 +376,9 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
@Composable @Composable
fun GradeTypePicker( fun GradeTypePicker(
type: UiGradeType, categories: List<UiGradeType.Categoric>, numeric: List<UiGradeType.Numeric>, type: UiGradeType, categories: List<UiGradeType.Categoric>, numeric: List<UiGradeType.Numeric>,
mkCat: (String, List<String>) -> Unit, mkNum: (String, Double) -> Unit, mkCat: (String, List<String>, Int) -> Unit,
modCat: (cat: CategoricGrade, add: List<String>, default: Int) -> Unit,
mkNum: (String, Double) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onUpdate: (UiGradeType) -> Unit onUpdate: (UiGradeType) -> Unit
) = Column(modifier) { ) = Column(modifier) {
@@ -394,20 +423,27 @@ fun GradeTypePicker(
} }
} }
(type as? UiGradeType.Categoric)?.let { (type as? UiGradeType.Categoric)?.let {
var updating by remember(type, categories) { mutableStateOf<UiGradeType.Categoric?>(null) }
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(categories) { idx, it -> itemsIndexed(categories) { idx, it ->
Surface( Surface(
tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp, tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp,
shape = MaterialTheme.shapes.small shape = MaterialTheme.shapes.small
) { ) {
Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) { Row(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
Column(Modifier.weight(1f)) {
Text(it.grade.name, fontWeight = FontWeight.Bold) Text(it.grade.name, fontWeight = FontWeight.Bold)
Text( Text(
"(${it.options.size} options)", "(${it.options.size} options; default ${it.default?.option ?: "none"})",
Modifier.padding(start = 10.dp), Modifier.padding(start = 10.dp),
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
) )
} }
IconButton(Edit, modifier = Modifier.align(Alignment.CenterVertically)) {
updating = it
}
}
} }
} }
@@ -417,6 +453,11 @@ fun GradeTypePicker(
} }
} }
} }
if(updating != null) ModCatScaleDialog(updating!!, { updating = null }) { categoric, add, i ->
modCat(categoric.grade, add, i)
updating = null
}
} ?: (type as? UiGradeType.Numeric)?.let { } ?: (type as? UiGradeType.Numeric)?.let {
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(numeric) { idx, it -> itemsIndexed(numeric) { idx, it ->
@@ -445,8 +486,8 @@ fun GradeTypePicker(
if(adding) { if(adding) {
when(type) { when(type) {
is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options -> is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options, idx ->
mkCat(name, options) mkCat(name, options, idx)
} }
is UiGradeType.Numeric -> AddNumScaleDialog(numeric.map { it.grade.name }, { adding = false }) { name, max -> is UiGradeType.Numeric -> AddNumScaleDialog(numeric.map { it.grade.name }, { adding = false }) { name, max ->
mkNum(name, max) mkNum(name, max)
@@ -457,7 +498,7 @@ fun GradeTypePicker(
} }
@Composable @Composable
fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String, List<String>) -> Unit) = DialogWindow( fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String, List<String>, Int) -> Unit) = DialogWindow(
onCloseRequest = onClose, onCloseRequest = onClose,
state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center)) state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center))
) { ) {
@@ -465,6 +506,7 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var options by remember { mutableStateOf(listOf<String>()) } var options by remember { mutableStateOf(listOf<String>()) }
var adding by remember { mutableStateOf("") } var adding by remember { mutableStateOf("") }
var default by remember { mutableStateOf(0) }
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize().padding(10.dp)) { Box(Modifier.fillMaxSize().padding(10.dp)) {
@@ -474,8 +516,15 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
LazyColumn(Modifier.weight(1f)) { LazyColumn(Modifier.weight(1f)) {
itemsIndexed(options) { idx, it -> itemsIndexed(options) { idx, it ->
Row(Modifier.fillMaxWidth().padding(5.dp)) { Row(Modifier.fillMaxWidth().padding(5.dp)) {
Text(it, Modifier.weight(1f)) Column(Modifier.weight(1f).align(Alignment.CenterVertically)) {
IconButton({ options = options.filterNot { o -> o == it } }) { Text(it)
if(idx == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
}
IconButton({ default = idx }, Modifier.align(Alignment.CenterVertically)) {
if(default == idx) Icon(CheckboxChecked, "Default option")
else Icon(CheckboxUnchecked, "Set as default")
}
IconButton({ options = options.filterNot { o -> o == it } }, Modifier.align(Alignment.CenterVertically)) {
Icon(Delete, "Delete grading option") Icon(Delete, "Delete grading option")
} }
} }
@@ -489,8 +538,77 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
} }
} }
} }
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) { CancelSaveRow(name.isNotBlank() && name !in taken && options.isNotEmpty() && default in options.indices, onClose) {
onSave(name, options) onSave(name, options, default)
onClose()
}
}
}
}
LaunchedEffect(Unit) { focus.requestFocus() }
}
@Composable
fun ModCatScaleDialog(
current: UiGradeType.Categoric, onClose: () -> Unit,
onSave: (UiGradeType.Categoric, List<String>, Int) -> Unit
) = DialogWindow(
onCloseRequest = onClose,
state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center))
) {
val focus = remember { FocusRequester() }
val name = current.grade.name
var default by remember(current) {
mutableStateOf(maxOf(current.options.indexOfFirst { it.id.value == current.default?.id?.value }, 0))
}
var options by remember(current) { mutableStateOf(current.options) }
var added by remember(current) { mutableStateOf(listOf<String>()) }
var adding by remember(current) { mutableStateOf("") }
Surface(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize().padding(10.dp)) {
Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, {}, Modifier.fillMaxWidth(), label = { Text("Grading system name") }, singleLine = true, enabled = false)
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)) {
Column(Modifier.weight(1f).align(Alignment.CenterVertically)) {
Text(it.option)
if(idx == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
}
IconButton({ default = idx }, Modifier.align(Alignment.CenterVertically)) {
if(default == idx) Icon(CheckboxChecked, "Default option")
else Icon(CheckboxUnchecked, "Set as default")
}
}
}
itemsIndexed(added) { idx, it ->
Row(Modifier.fillMaxWidth().padding(5.dp)) {
Column(Modifier.weight(1f).align(Alignment.CenterVertically)) {
Text(it)
if(idx + options.size == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic)
}
IconButton({ default = idx + options.size }, Modifier.align(Alignment.CenterVertically)) {
if(default == idx) Icon(CheckboxChecked, "Default option")
else Icon(CheckboxUnchecked, "Set as default")
}
}
}
item {
Row {
OutlinedTextField(adding, { adding = it }, Modifier.weight(1f).align(Alignment.CenterVertically).padding(5.dp), label = { Text("New option") }, isError = adding in options.map { it.option } || adding in added, singleLine = true)
Button({ added = added + adding; adding = "" }, Modifier.align(Alignment.CenterVertically).padding(5.dp), enabled = adding.isNotBlank() && adding !in options.map { it.option } && adding !in added) {
Text("Add")
}
}
}
}
CancelSaveRow(true, onClose) {
onSave(current, added, default)
onClose() onClose()
} }
} }
@@ -23,6 +23,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -48,6 +49,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.jaytux.grader.data.v2.Group import com.jaytux.grader.data.v2.Group
import com.jaytux.grader.data.v2.Student import com.jaytux.grader.data.v2.Student
import com.jaytux.grader.startEmail import com.jaytux.grader.startEmail
import com.jaytux.grader.ui.EditableText
import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.EditionVM
import com.jaytux.grader.viewmodel.SnackVM import com.jaytux.grader.viewmodel.SnackVM
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
@@ -63,8 +65,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
var swappingRole by remember { mutableStateOf(-1) } var swappingRole by remember { mutableStateOf(-1) }
val group = remember(groups, focus) { if(focus != -1) groups[focus] else null } val group = remember(groups, focus) { if(focus != -1) groups[focus] else null }
val groupNames = remember(groups) { groups.map { it.group.name } }
val grades by vm.groupGrades.entities val grades by vm.groupGrades.entities
val snacks = viewModel<SnackVM> { SnackVM() } val snacks = viewModel<SnackVM> { SnackVM() }
var deleting by remember { mutableStateOf(false) }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
@@ -80,14 +84,21 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
} }
else { else {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { EditableName(
Text(group.group.name, style = MaterialTheme.typography.headlineMedium) group.group.name, groupNames,
{ vm.modGroup(group.group, it) },
{ deleting = true },
style = MaterialTheme.typography.headlineMedium
) {
if (group.members.any { it.first.contact.isNotBlank() }) { if (group.members.any { it.first.contact.isNotBlank() }) {
IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) { IconButton(Mail, "Send email", Modifier.align(Alignment.CenterVertically)) {
Icon(Mail, "Send email", Modifier.fillMaxHeight()) startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) {
snacks.show(it)
} }
} }
} }
}
Spacer(Modifier.height(5.dp)) Spacer(Modifier.height(5.dp))
Row(Modifier.padding(5.dp)) { Row(Modifier.padding(5.dp)) {
var showTargetBorder by remember { mutableStateOf(false) } var showTargetBorder by remember { mutableStateOf(false) }
@@ -224,6 +235,15 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
swappingRole = -1 swappingRole = -1
} }
} }
if(deleting) {
if(group == null) deleting = false
else {
ConfirmDeleteDialog("a group", { deleting = false }, { vm.rmGroup(group.group) }) {
Text(group.group.name)
}
}
}
} }
private class DDTarget<T>(val onStart: () -> Unit, val onEnd: () -> Unit, val validator: (Transferable) -> T?, val handle: (T) -> Unit) : DragAndDropTarget { private class DDTarget<T>(val onStart: () -> Unit, val onEnd: () -> Unit, val validator: (Transferable) -> T?, val handle: (T) -> Unit) : DragAndDropTarget {
@@ -1430,3 +1430,84 @@ val Mail: ImageVector by lazy {
} }
}.build() }.build()
} }
val CheckboxUnchecked: ImageVector by lazy {
ImageVector.Builder(
name = "checkbox-unchecked",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(
fill = SolidColor(Color.Black)
) {
moveTo(3f, 6.25f)
curveTo(3f, 4.45507f, 4.45507f, 3f, 6.25f, 3f)
horizontalLineTo(17.75f)
curveTo(19.5449f, 3f, 21f, 4.45507f, 21f, 6.25f)
verticalLineTo(17.75f)
curveTo(21f, 19.5449f, 19.5449f, 21f, 17.75f, 21f)
horizontalLineTo(6.25f)
curveTo(4.45507f, 21f, 3f, 19.5449f, 3f, 17.75f)
verticalLineTo(6.25f)
close()
moveTo(6.25f, 4.5f)
curveTo(5.2835f, 4.5f, 4.5f, 5.2835f, 4.5f, 6.25f)
verticalLineTo(17.75f)
curveTo(4.5f, 18.7165f, 5.2835f, 19.5f, 6.25f, 19.5f)
horizontalLineTo(17.75f)
curveTo(18.7165f, 19.5f, 19.5f, 18.7165f, 19.5f, 17.75f)
verticalLineTo(6.25f)
curveTo(19.5f, 5.2835f, 18.7165f, 4.5f, 17.75f, 4.5f)
horizontalLineTo(6.25f)
close()
}
}.build()
}
val CheckboxChecked: ImageVector by lazy {
ImageVector.Builder(
name = "checkbox-checked",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(
fill = SolidColor(Color.Black)
) {
moveTo(6.25f, 3f)
curveTo(4.45507f, 3f, 3f, 4.45507f, 3f, 6.25f)
verticalLineTo(17.75f)
curveTo(3f, 19.5449f, 4.45507f, 21f, 6.25f, 21f)
horizontalLineTo(17.75f)
curveTo(19.5449f, 21f, 21f, 19.5449f, 21f, 17.75f)
verticalLineTo(6.25f)
curveTo(21f, 4.45507f, 19.5449f, 3f, 17.75f, 3f)
horizontalLineTo(6.25f)
close()
moveTo(4.5f, 6.25f)
curveTo(4.5f, 5.2835f, 5.2835f, 4.5f, 6.25f, 4.5f)
horizontalLineTo(17.75f)
curveTo(18.7165f, 4.5f, 19.5f, 5.2835f, 19.5f, 6.25f)
verticalLineTo(17.75f)
curveTo(19.5f, 18.7165f, 18.7165f, 19.5f, 17.75f, 19.5f)
horizontalLineTo(6.25f)
curveTo(5.2835f, 19.5f, 4.5f, 18.7165f, 4.5f, 17.75f)
verticalLineTo(6.25f)
close()
moveTo(17.28f, 9.28064f)
curveTo(17.5731f, 8.98791f, 17.5734f, 8.51304f, 17.2806f, 8.21998f)
curveTo(16.9879f, 7.92691f, 16.513f, 7.92664f, 16.22f, 8.21936f)
lineTo(9.99658f, 14.4356f)
lineTo(7.78084f, 12.2197f)
curveTo(7.48795f, 11.9268f, 7.01308f, 11.9268f, 6.72018f, 12.2196f)
curveTo(6.42727f, 12.5125f, 6.42726f, 12.9874f, 6.72014f, 13.2803f)
lineTo(9.46591f, 16.0262f)
curveTo(9.75868f, 16.319f, 10.2333f, 16.3192f, 10.5263f, 16.0266f)
lineTo(17.28f, 9.28064f)
close()
}
}.build()
}
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.Divider
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -48,6 +49,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
val students by vm.studentList.entities val students by vm.studentList.entities
val focus by vm.focusIndex val focus by vm.focusIndex
val snacks = viewModel<SnackVM> { SnackVM() } val snacks = viewModel<SnackVM> { SnackVM() }
var deleting by remember { mutableStateOf(false) }
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
ListOrEmpty(students, { Text("No students yet.") }) { idx, it -> ListOrEmpty(students, { Text("No students yet.") }) { idx, it ->
@@ -69,42 +71,25 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall) EditableText(
if(students[focus].contact.isNotBlank()) { students[focus].name, style = MaterialTheme.typography.headlineSmall,
IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) { canSave = { it.isNotBlank() && (it == students[focus].name || !students.any { x -> x.name == it }) }
Icon(Mail, "Send email", Modifier.fillMaxHeight()) ) {
vm.modStudent(students[focus], it, null, null)
} }
Spacer(Modifier.width(10.dp))
IconButton(Delete, "Delete student", Modifier.align(Alignment.CenterVertically)) {
deleting = true
} }
} }
Row { Row {
var editing by remember { mutableStateOf(false) }
Text("Contact: ", Modifier.align(Alignment.CenterVertically).padding(start = 15.dp)) Text("Contact: ", Modifier.align(Alignment.CenterVertically).padding(start = 15.dp))
if(!editing) {
if (students[focus].contact.isBlank()) { EditableText(students[focus].contact, Modifier.align(Alignment.CenterVertically), displayAdapt = { it.ifBlank { "No contact info." } }) {
Text( vm.modStudent(students[focus], null, it, null)
"No contact info.",
Modifier.padding(start = 5.dp),
fontStyle = FontStyle.Italic,
color = LocalTextStyle.current.color.copy(alpha = 0.5f)
)
} }
else { IconButton(Mail, "Send email") {
Text(students[focus].contact, Modifier.padding(start = 5.dp)) startEmail(listOf(students[focus].contact)) { snacks.show(it) }
}
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 })
} }
} }
@@ -163,6 +148,9 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
} }
items(grades ?: listOf()) { items(grades ?: listOf()) {
Column(Modifier.padding(10.dp)) {
Divider()
}
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Row { Row {
Text(it.assignment.name, Modifier.weight(0.66f)) Text(it.assignment.name, Modifier.weight(0.66f))
@@ -193,6 +181,15 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
} }
} }
} }
if(deleting) {
if(focus == -1) deleting = false
else {
ConfirmDeleteDialog("a student", { deleting = false }, { vm.rmStudent(students[focus]) }) {
Text(students[focus].name)
}
}
}
} }
@Composable @Composable
@@ -1,200 +0,0 @@
package com.jaytux.grader.ui
//@Composable
//fun StudentView(state: StudentState, nav: Navigators) {
// val groups by state.groups.entities
// val courses by state.courseEditions.entities
// val groupGrades by state.groupGrades.entities
// val soloGrades by state.soloGrades.entities
//
// Column(Modifier.padding(10.dp)) {
// Row {
// Column(Modifier.weight(0.45f)) {
// Column(Modifier.padding(10.dp).weight(0.35f)) {
// Spacer(Modifier.height(10.dp))
// InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
// InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
// InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
// }
// Column(Modifier.weight(0.20f)) {
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
// val (ed, course) = it
// Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
// }
// }
// Column(Modifier.weight(0.45f)) {
// Text("Groups", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
// val (group, c) = it
// val (course, ed) = c
// Row(Modifier.clickable { nav.group(group) }) {
// Text(group.name, style = MaterialTheme.typography.bodyMedium)
// Spacer(Modifier.width(5.dp))
// Text(
// "(in course $course ($ed))",
// Modifier.align(Alignment.Bottom),
// style = MaterialTheme.typography.bodySmall
// )
// }
//
// }
// }
// }
// Column(Modifier.weight(0.55f)) {
// Text("Courses", style = MaterialTheme.typography.headlineSmall)
// LazyColumn {
// item {
// Text("As group member", fontWeight = FontWeight.Bold)
// }
// items(groupGrades) {
// groupGradeWidget(it)
// }
//
// item {
// Text("Solo assignments", fontWeight = FontWeight.Bold)
// }
// items(soloGrades) {
// soloGradeWidget(it)
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun groupGradeWidget(gg: StudentState.LocalGroupGrade) {
// val (group, assignment, gGrade, iGrade) = gg
// var expanded by remember { mutableStateOf(false) }
// Row(Modifier.padding(5.dp)) {
// Spacer(Modifier.width(10.dp))
// Surface(
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
// tonalElevation = 5.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Column(Modifier.padding(5.dp)) {
// Text("${assignment.maxN(25)} (${iGrade ?: gGrade ?: "no grade yet"})")
//
// if (expanded) {
// Row {
// Spacer(Modifier.width(10.dp))
// Column {
// ItalicAndNormal("Assignment: ", assignment)
// ItalicAndNormal("Group name: ", group)
// ItalicAndNormal("Group grade: ", gGrade ?: "no grade yet")
// ItalicAndNormal("Individual grade: ", iGrade ?: "no individual grade")
// }
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun soloGradeWidget(sg: StudentState.LocalSoloGrade) {
// val (assignment, grade) = sg
// var expanded by remember { mutableStateOf(false) }
// Row(Modifier.padding(5.dp)) {
// Spacer(Modifier.width(10.dp))
// Surface(
// Modifier.clickable { expanded = !expanded }.fillMaxWidth(),
// tonalElevation = 5.dp,
// shape = MaterialTheme.shapes.medium
// ) {
// Column(Modifier.padding(5.dp)) {
// Text("${assignment.maxN(25)} (${grade ?: "no grade yet"})")
//
// if (expanded) {
// Row {
// Spacer(Modifier.width(10.dp))
// Column {
// ItalicAndNormal("Assignment: ", assignment)
// ItalicAndNormal("Individual grade: ", grade ?: "no grade yet")
// }
// }
// }
// }
// }
// }
//}
//
//@Composable
//fun GroupView(state: GroupState, nav: Navigators) {
// val members by state.members.entities
// val available by state.availableStudents.entities
// val allRoles by state.roles.entities
//
// var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
//
// Column(Modifier.padding(10.dp)) {
// Row {
// Column(Modifier.weight(0.5f)) {
// Text("Students", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
// val (student, role) = it
// Row(Modifier.clickable { nav.student(student) }) {
// Text(
// "${student.name} (${role ?: "no role"})",
// Modifier.weight(0.75f).align(Alignment.CenterVertically),
// style = MaterialTheme.typography.bodyMedium
// )
// IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
// Icon(Icons.Default.Edit, "Change role")
// }
// IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
// Icon(Icons.Default.Delete, "Remove student")
// }
// }
// }
// }
// Column(Modifier.weight(0.5f)) {
// Text("Available students", style = MaterialTheme.typography.headlineSmall)
// ListOrEmpty(available, { Text("No students available") }) { _, it ->
// Row(Modifier.padding(5.dp).clickable { nav.student(it) }) {
// IconButton({ state.addStudent(it) }) {
// Icon(ChevronLeft, "Add student")
// }
// Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
// }
// }
// }
// }
// }
//
// pickRole?.let {
// val (curr, onPick) = it
// RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
// }
//}
//
//@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()
// }
// }
// }
// }
//}
@@ -7,6 +7,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.ProvideTextStyle
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
@@ -16,12 +19,15 @@ 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.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextStyle
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.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.jaytux.grader.maxN import com.jaytux.grader.maxN
import com.jaytux.grader.viewmodel.Grade import com.jaytux.grader.viewmodel.Grade
@@ -275,3 +281,65 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on
} }
} }
} }
@Composable
fun EditableText(
text: String, modifier: Modifier = Modifier, key: Any = Unit, style: TextStyle = LocalTextStyle.current,
label: (@Composable () -> Unit)? = null, displayAdapt: ((String) -> String)? = null,
canSave: (String) -> Boolean = { true },
onUpdate: (String) -> Unit
) {
var editing by remember(text, key) { mutableStateOf(false) }
if(editing) {
var current by remember(text, key) { mutableStateOf(text) }
val enableSave = canSave(current)
Row(modifier) {
OutlinedTextField(current, { current = it }, Modifier.align(Alignment.CenterVertically), label = label, textStyle = style, isError = !enableSave)
Spacer(Modifier.width(5.dp))
IconButton(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically), enableSave, iconHeight = style.fontSize.toDp()) {
onUpdate(current)
editing = false
}
Spacer(Modifier.width(5.dp))
IconButton(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically), iconHeight = style.fontSize.toDp()) {
editing = false
}
}
}
else {
Row(modifier) {
Text(displayAdapt?.let { it(text) } ?: text, Modifier.align(Alignment.CenterVertically), style = style)
Spacer(Modifier.width(5.dp))
IconButton(Edit, "Edit", iconHeight = style.fontSize.toDp()) {
editing = true
}
}
}
}
@Composable
fun IconButton(icon: ImageVector, contentDescription: String? = null, modifier: Modifier = Modifier, enabled: Boolean = true, iconHeight: Dp = Dp.Unspecified, onClick: () -> Unit) =
IconButton(onClick, modifier, enabled) {
if(iconHeight.isUnspecified) Icon(icon, contentDescription, modifier = Modifier.height(LocalTextStyle.current.fontSize.toDp()))
else Icon(icon, contentDescription, modifier = Modifier.height(iconHeight))
}
@Composable
fun EditableName(
name: String, taken: List<String>, onUpdate: (String) -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current, displayAdapt: ((String) -> String)? = null,
addBeforeDelete: (@Composable RowScope.() -> Unit)? = null
) = Row(modifier) {
ProvideTextStyle(style) {
EditableText(
name, style = style, canSave = { it.isNotBlank() && (it == name || it !in taken) }, onUpdate = onUpdate,
displayAdapt = displayAdapt
)
addBeforeDelete?.invoke(this@Row)
IconButton(Delete, "Delete", Modifier.align(Alignment.CenterVertically)) {
onDelete()
}
}
}
@@ -57,7 +57,7 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
val categoricGrades = RawDbState { val categoricGrades = RawDbState {
CategoricGrade.all().map { CategoricGrade.all().map {
UiGradeType.Categoric(it.options.toList(), it) UiGradeType.Categoric(it.options.toList(), it.default, it)
} }
} }
@@ -66,24 +66,35 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
} }
val studentGrades = RawDbFocusableState { st: Student -> val studentGrades = RawDbFocusableState { st: Student ->
println("Loading grade summary for student ${st.name}")
val groupIds = st.groups.map { it.group.id }.toSet() val groupIds = st.groups.map { it.group.id }.toSet()
edition.assignments.map { asg -> edition.assignments.map { asg ->
val (grade, memberOf, override) = when(asg.type) { val (grade, memberOf, override) = when(asg.type) {
AssignmentType.GROUP -> { AssignmentType.GROUP -> {
val asGroup = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id in groupIds } val (asGroup, raw) = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id in groupIds }?.let { Grade.fromAssignment(asg.globalCriterion, it) to it } ?: (null to null)
val solo = asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfGroup.any { over -> over.student == st } } val solo = run findSolo@{
val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) } for(groupLevel in asg.globalCriterion.feedbacks) {
gr to asGroup?.asGroupFeedback app (solo != null) for(override in groupLevel.forStudentsOverrideIfGroup) {
if(override.student.id == st.id) return@findSolo Grade.fromAssignment(asg.globalCriterion, override.feedback)
}
}
null
}
val gr = (solo ?: asGroup)//?.let { Grade.fromAssignment(asg.globalCriterion, it) }
println(" -> For group assignment ${asg.name}: $gr (solo override: $solo, group feedback: $asGroup)")
gr to raw?.asGroupFeedback app (solo != null)
} }
AssignmentType.SOLO -> { AssignmentType.SOLO -> {
val eval = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st } val eval = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st }
?.let { Grade.fromAssignment(asg.globalCriterion, it) } ?.let { Grade.fromAssignment(asg.globalCriterion, it) }
println(" -> For solo assignment ${asg.name}: $eval")
eval to null app false eval to null app false
} }
AssignmentType.PEER_EVALUATION -> { AssignmentType.PEER_EVALUATION -> {
val eval = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == st.id } val eval = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == st.id }
?.let { Grade.fromAssignment(asg.globalCriterion, it) } ?.let { Grade.fromAssignment(asg.globalCriterion, it) }
println(" -> For peer evaluation assignment ${asg.name}: $eval")
eval to null app false eval to null app false
} }
} }
@@ -129,6 +140,43 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
val selectedTab = _selectedTab.immutable() val selectedTab = _selectedTab.immutable()
val focusIndex = _focusIndex.immutable() val focusIndex = _focusIndex.immutable()
init {
transaction {
var count0 = 0
StudentOverrideFeedback.all().forEach {
val group = it.group.name
val student = it.student.name
val assignment = it.feedback.criterion
val assName = assignment.assignment.name
val ogGrade = Grade.fromAssignment(assignment, it.overrides)
val updGrade = Grade.fromAssignment(assignment, it.feedback)
println("OVERRIDE: '$student' in '$group' for '$assName' ('${assignment.name}'): $ogGrade -> $updGrade")
count0++
}
println(" --> Direct lookup: $count0 overrides")
var count1 = 0
GroupAssignment.all().forEach { asg ->
val assignment = asg.base
val baseCrit = assignment.globalCriterion
val overrides = baseCrit.feedbacks.flatMap { it.forStudentsOverrideIfGroup }
if(overrides.isNotEmpty()) {
println("Assignment '${assignment.name}' has ${overrides.size} overrides:")
overrides.forEach {
val group = it.group.name
val student = it.student.name
val assName = assignment.name
val ogGrade = Grade.fromAssignment(baseCrit, it.overrides)
val updGrade = Grade.fromAssignment(baseCrit, it.feedback)
println(" - OVERRIDE: '$student' in '$group' for '$assName': $ogGrade -> $updGrade")
count1++
}
}
}
println(" --> GroupAssignment lookup: $count1 overrides")
}
}
fun switchTo(tab: Tab) { fun switchTo(tab: Tab) {
_selectedTab.value = tab _selectedTab.value = tab
_focusIndex.value = -1 _focusIndex.value = -1
@@ -397,20 +445,38 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
assignmentList.refresh() assignmentList.refresh()
} }
fun mkScale(name: String, options: List<String>) { fun mkScale(name: String, options: List<String>, default: Int) {
transaction { transaction {
val grade = CategoricGrade.new { this.name = name } val grade = CategoricGrade.new { this.name = name }
options.forEachIndexed { idx, opt -> options.forEachIndexed { idx, opt ->
CategoricGradeOption.new { val x = CategoricGradeOption.new {
this.grade = grade this.grade = grade
this.option = opt this.option = opt
this.index = idx this.index = idx
} }
if(idx == default) grade.default = x
} }
} }
categoricGrades.refresh() categoricGrades.refresh()
} }
fun modScale(grade: CategoricGrade, add: List<String>, default: Int) {
transaction {
val currMax = grade.options.maxOfOrNull { it.index } ?: 0
add.forEachIndexed { idx, opt ->
CategoricGradeOption.new {
this.grade = grade
this.option = opt
this.index = idx + currMax
}
}
val default = grade.options.first { it.index == default }
grade.default = default
}
categoricGrades.refresh()
}
fun mkNumericScale(name: String, max: Double) { fun mkNumericScale(name: String, max: Double) {
transaction { transaction {
NumericGrade.new { NumericGrade.new {
@@ -441,8 +507,14 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
fun rmAssignment(assignment: BaseAssignment) { fun rmAssignment(assignment: BaseAssignment) {
transaction { transaction {
assignment.delete() assignment.criteria.forEach {
it.feedbacks.forEach { f ->
f.delete()
}
it.delete()
}
(assignment.asPeerEvaluation ?: assignment.asGroupAssignment ?: assignment.asSoloAssignment)?.delete() (assignment.asPeerEvaluation ?: assignment.asGroupAssignment ?: assignment.asSoloAssignment)?.delete()
assignment.delete()
} }
unfocus() unfocus()
assignmentList.refresh() assignmentList.refresh()
@@ -39,10 +39,15 @@ sealed class Grade {
companion object { companion object {
context(trns: Transaction) context(trns: Transaction)
fun fromAssignment(asg: Criterion, fdb: BaseFeedback): Grade = when(asg.gradeType) { fun fromAssignment(asg: Criterion, fdb: BaseFeedback): Grade = when(asg.gradeType) {
GradeType.CATEGORIC -> GradeType.CATEGORIC -> {
Categoric(fdb.gradeCategoric!!, asg.categoricGrade!!.options.toList(), asg.categoricGrade!!) val option = fdb.gradeCategoric ?: asg.categoricGrade!!.default
Categoric(option, asg.categoricGrade!!.options.toList(), asg.categoricGrade!!)
}
GradeType.NUMERIC -> Numeric(fdb.gradeNumeric!!, asg.numericGrade!!) GradeType.NUMERIC -> {
val grade = fdb.gradeNumeric ?: -1.0
Numeric(grade, asg.numericGrade!!)
}
GradeType.PERCENTAGE -> Percentage(fdb.gradeNumeric!!) GradeType.PERCENTAGE -> Percentage(fdb.gradeNumeric!!)
GradeType.NONE -> FreeText(fdb.gradeFreeText!!) GradeType.NONE -> FreeText(fdb.gradeFreeText!!)
} }
@@ -16,12 +16,12 @@ sealed class UiGradeType {
object FreeText : UiGradeType() object FreeText : UiGradeType()
object Percentage : UiGradeType() object Percentage : UiGradeType()
data class Numeric(val grade: NumericGrade) : UiGradeType() data class Numeric(val grade: NumericGrade) : UiGradeType()
data class Categoric(val options: List<CategoricGradeOption>, val grade: CategoricGrade) : UiGradeType() data class Categoric(val options: List<CategoricGradeOption>, val default: CategoricGradeOption?, val grade: CategoricGrade) : UiGradeType()
companion object { companion object {
context(trns: Transaction) context(trns: Transaction)
fun from(type: GradeType, categoric: CategoricGrade?, numeric: NumericGrade?) = when(type) { fun from(type: GradeType, categoric: CategoricGrade?, numeric: NumericGrade?) = when(type) {
GradeType.CATEGORIC -> Categoric(categoric!!.options.toList(), categoric) GradeType.CATEGORIC -> Categoric(categoric!!.options.toList(), categoric.default, categoric)
GradeType.NUMERIC -> Numeric(numeric!!) GradeType.NUMERIC -> Numeric(numeric!!)
GradeType.PERCENTAGE -> Percentage GradeType.PERCENTAGE -> Percentage
GradeType.NONE -> FreeText GradeType.NONE -> FreeText