More grading updates
This commit is contained in:
@@ -9,6 +9,8 @@ import com.jaytux.grader.data.v2.Courses
|
||||
import com.jaytux.grader.data.v2.NumericGrade
|
||||
import com.jaytux.grader.data.v2.v2Tables
|
||||
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.transactions.transaction
|
||||
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.batchInsert
|
||||
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 {
|
||||
val dataDir: String = ProjectDirectories.from("com", "jaytux", "grader").dataDir.also {
|
||||
@@ -43,6 +48,8 @@ object Database {
|
||||
it[CategoricGrades.id]
|
||||
}
|
||||
|
||||
var passId: EntityID<UUID>? = null
|
||||
var bId: EntityID<UUID>? = null
|
||||
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 }
|
||||
@@ -50,7 +57,15 @@ object Database {
|
||||
this[CategoricGradeOptions.option] = it.first
|
||||
this[CategoricGradeOptions.gradeId] = it.second
|
||||
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) {
|
||||
|
||||
@@ -142,6 +142,7 @@ object PeerEvaluationS2SEvaluations : UUIDTable("peerEvalS2SEvals") {
|
||||
|
||||
object CategoricGrades : UUIDTable("categoricGrades") {
|
||||
val name = varchar("name", 50).uniqueIndex()
|
||||
val defaultOption = reference("default_option_id", CategoricGradeOptions.id)
|
||||
}
|
||||
|
||||
object CategoricGradeOptions : UUIDTable("categoricGradeOpts") {
|
||||
|
||||
@@ -101,6 +101,7 @@ class CategoricGrade(id: EntityID<UUID>) : UUIDEntity(id) {
|
||||
companion object : EntityClass<UUID, CategoricGrade>(CategoricGrades)
|
||||
|
||||
var name by CategoricGrades.name
|
||||
var default by CategoricGradeOption referencedOn CategoricGrades.defaultOption
|
||||
|
||||
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.SoloGrading
|
||||
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.Navigator
|
||||
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 editingRubric by remember { mutableStateOf(-1) }
|
||||
var updatingGrade by remember { mutableStateOf(false) }
|
||||
var renaming by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
|
||||
val navToGrading = 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) {
|
||||
ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it ->
|
||||
ListOrEmpty(assignments, { Text("No assignments yet.") }) { idx, it ->
|
||||
QuickAssignment(idx, it, vm)
|
||||
}
|
||||
}
|
||||
@@ -73,8 +78,21 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil
|
||||
val peerEvalData by vm.asPeerEvaluation.entity
|
||||
var updatingPeerEvalGrade by remember { mutableStateOf(false) }
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
Row {
|
||||
Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically))
|
||||
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 {
|
||||
@@ -300,7 +327,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken:
|
||||
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 }
|
||||
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) {
|
||||
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))
|
||||
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 }
|
||||
GradeTypePicker(type, categories, numeric, vm::mkScale, vm::modScale, vm::mkNumericScale, Modifier.weight(1f)) { type = it }
|
||||
|
||||
CancelSaveRow(true, onClose) {
|
||||
onSave(type)
|
||||
@@ -349,7 +376,9 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose:
|
||||
@Composable
|
||||
fun GradeTypePicker(
|
||||
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,
|
||||
onUpdate: (UiGradeType) -> Unit
|
||||
) = Column(modifier) {
|
||||
@@ -394,19 +423,26 @@ fun GradeTypePicker(
|
||||
}
|
||||
}
|
||||
(type as? UiGradeType.Categoric)?.let {
|
||||
var updating by remember(type, categories) { mutableStateOf<UiGradeType.Categoric?>(null) }
|
||||
|
||||
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
|
||||
)
|
||||
Row(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(it.grade.name, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
"(${it.options.size} options; default ${it.default?.option ?: "none"})",
|
||||
Modifier.padding(start = 10.dp),
|
||||
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 {
|
||||
LazyColumn(Modifier.weight(1f)) {
|
||||
itemsIndexed(numeric) { idx, it ->
|
||||
@@ -445,8 +486,8 @@ fun GradeTypePicker(
|
||||
|
||||
if(adding) {
|
||||
when(type) {
|
||||
is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options ->
|
||||
mkCat(name, options)
|
||||
is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options, idx ->
|
||||
mkCat(name, options, idx)
|
||||
}
|
||||
is UiGradeType.Numeric -> AddNumScaleDialog(numeric.map { it.grade.name }, { adding = false }) { name, max ->
|
||||
mkNum(name, max)
|
||||
@@ -457,7 +498,7 @@ fun GradeTypePicker(
|
||||
}
|
||||
|
||||
@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,
|
||||
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 options by remember { mutableStateOf(listOf<String>()) }
|
||||
var adding by remember { mutableStateOf("") }
|
||||
var default by remember { mutableStateOf(0) }
|
||||
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.fillMaxSize().padding(10.dp)) {
|
||||
@@ -474,8 +516,15 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
|
||||
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 } }) {
|
||||
Column(Modifier.weight(1f).align(Alignment.CenterVertically)) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -489,8 +538,77 @@ fun AddCatScaleDialog(taken: List<String>, onClose: () -> Unit, onSave: (String,
|
||||
}
|
||||
}
|
||||
}
|
||||
CancelSaveRow(name.isNotBlank() && name !in taken, onClose) {
|
||||
onSave(name, options)
|
||||
CancelSaveRow(name.isNotBlank() && name !in taken && options.isNotEmpty() && default in options.indices, onClose) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.Student
|
||||
import com.jaytux.grader.startEmail
|
||||
import com.jaytux.grader.ui.EditableText
|
||||
import com.jaytux.grader.viewmodel.EditionVM
|
||||
import com.jaytux.grader.viewmodel.SnackVM
|
||||
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) }
|
||||
|
||||
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 snacks = viewModel<SnackVM> { SnackVM() }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||
ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it ->
|
||||
@@ -80,14 +84,21 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||
}
|
||||
else {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(group.group.name, style = MaterialTheme.typography.headlineMedium)
|
||||
EditableName(
|
||||
group.group.name, groupNames,
|
||||
{ vm.modGroup(group.group, it) },
|
||||
{ deleting = true },
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
) {
|
||||
if (group.members.any { it.first.contact.isNotBlank() }) {
|
||||
IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) {
|
||||
Icon(Mail, "Send email", Modifier.fillMaxHeight())
|
||||
IconButton(Mail, "Send email", Modifier.align(Alignment.CenterVertically)) {
|
||||
startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) {
|
||||
snacks.show(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(5.dp))
|
||||
Row(Modifier.padding(5.dp)) {
|
||||
var showTargetBorder by remember { mutableStateOf(false) }
|
||||
@@ -224,6 +235,15 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||
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 {
|
||||
|
||||
@@ -1430,3 +1430,84 @@ val Mail: ImageVector by lazy {
|
||||
}
|
||||
}.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -48,6 +49,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||
val students by vm.studentList.entities
|
||||
val focus by vm.focusIndex
|
||||
val snacks = viewModel<SnackVM> { SnackVM() }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) {
|
||||
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) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(students[focus].name, style = MaterialTheme.typography.headlineSmall)
|
||||
if(students[focus].contact.isNotBlank()) {
|
||||
IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) {
|
||||
Icon(Mail, "Send email", Modifier.fillMaxHeight())
|
||||
}
|
||||
EditableText(
|
||||
students[focus].name, style = MaterialTheme.typography.headlineSmall,
|
||||
canSave = { it.isNotBlank() && (it == students[focus].name || !students.any { x -> x.name == it }) }
|
||||
) {
|
||||
vm.modStudent(students[focus], it, null, null)
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
IconButton(Delete, "Delete student", Modifier.align(Alignment.CenterVertically)) {
|
||||
deleting = true
|
||||
}
|
||||
}
|
||||
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 })
|
||||
|
||||
EditableText(students[focus].contact, Modifier.align(Alignment.CenterVertically), displayAdapt = { it.ifBlank { "No contact info." } }) {
|
||||
vm.modStudent(students[focus], null, it, null)
|
||||
}
|
||||
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 })
|
||||
IconButton(Mail, "Send email") {
|
||||
startEmail(listOf(students[focus].contact)) { snacks.show(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +148,9 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) {
|
||||
}
|
||||
|
||||
items(grades ?: listOf()) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Divider()
|
||||
}
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Row {
|
||||
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
|
||||
|
||||
@@ -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.items
|
||||
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.runtime.*
|
||||
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.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.window.*
|
||||
import com.jaytux.grader.maxN
|
||||
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 {
|
||||
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 ->
|
||||
println("Loading grade summary for student ${st.name}")
|
||||
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 = asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfGroup.any { over -> over.student == st } }
|
||||
val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||
gr to asGroup?.asGroupFeedback app (solo != null)
|
||||
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 = run findSolo@{
|
||||
for(groupLevel in asg.globalCriterion.feedbacks) {
|
||||
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 -> {
|
||||
val eval = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st }
|
||||
?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||
println(" -> For solo assignment ${asg.name}: $eval")
|
||||
eval to null app false
|
||||
}
|
||||
AssignmentType.PEER_EVALUATION -> {
|
||||
val eval = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == st.id }
|
||||
?.let { Grade.fromAssignment(asg.globalCriterion, it) }
|
||||
println(" -> For peer evaluation assignment ${asg.name}: $eval")
|
||||
eval to null app false
|
||||
}
|
||||
}
|
||||
@@ -129,6 +140,43 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
|
||||
val selectedTab = _selectedTab.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) {
|
||||
_selectedTab.value = tab
|
||||
_focusIndex.value = -1
|
||||
@@ -397,20 +445,38 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
|
||||
assignmentList.refresh()
|
||||
}
|
||||
|
||||
fun mkScale(name: String, options: List<String>) {
|
||||
fun mkScale(name: String, options: List<String>, default: Int) {
|
||||
transaction {
|
||||
val grade = CategoricGrade.new { this.name = name }
|
||||
options.forEachIndexed { idx, opt ->
|
||||
CategoricGradeOption.new {
|
||||
val x = CategoricGradeOption.new {
|
||||
this.grade = grade
|
||||
this.option = opt
|
||||
this.index = idx
|
||||
}
|
||||
if(idx == default) grade.default = x
|
||||
}
|
||||
}
|
||||
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) {
|
||||
transaction {
|
||||
NumericGrade.new {
|
||||
@@ -441,8 +507,14 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() {
|
||||
|
||||
fun rmAssignment(assignment: BaseAssignment) {
|
||||
transaction {
|
||||
assignment.delete()
|
||||
assignment.criteria.forEach {
|
||||
it.feedbacks.forEach { f ->
|
||||
f.delete()
|
||||
}
|
||||
it.delete()
|
||||
}
|
||||
(assignment.asPeerEvaluation ?: assignment.asGroupAssignment ?: assignment.asSoloAssignment)?.delete()
|
||||
assignment.delete()
|
||||
}
|
||||
unfocus()
|
||||
assignmentList.refresh()
|
||||
|
||||
@@ -39,10 +39,15 @@ sealed class Grade {
|
||||
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.CATEGORIC -> {
|
||||
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.NONE -> FreeText(fdb.gradeFreeText!!)
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ 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()
|
||||
data class Categoric(val options: List<CategoricGradeOption>, val default: CategoricGradeOption?, val grade: CategoricGrade) : UiGradeType()
|
||||
|
||||
companion object {
|
||||
context(trns: Transaction)
|
||||
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.PERCENTAGE -> Percentage
|
||||
GradeType.NONE -> FreeText
|
||||
|
||||
Reference in New Issue
Block a user