Re-orderable assignments

This commit is contained in:
jay-tux 2025-03-04 17:40:32 +01:00
parent 63c4197cfc
commit 49e3b8126f
Signed by: jay-tux
GPG Key ID: 84302006B056926E
5 changed files with 114 additions and 24 deletions

View File

@ -53,6 +53,7 @@ object EditionStudents : Table("editionStudents") {
object GroupAssignments : UUIDTable("grpAssgmts") {
val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable()
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")
@ -60,6 +61,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
object SoloAssignments : UUIDTable("soloAssgmts") {
val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable()
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")

View File

@ -10,9 +10,8 @@ import java.util.UUID
class Course(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, Course>(Courses)
fun loadEditions() = transaction { Edition.find { Editions.courseId eq this@Course.id }.toList() }
var name by Courses.name
val editions by Edition referrersOn Editions.courseId
}
class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -57,6 +56,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, GroupAssignment>(GroupAssignments)
var edition by Edition referencedOn GroupAssignments.editionId
var number by GroupAssignments.number
var name by GroupAssignments.name
var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline
@ -66,6 +66,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments)
var edition by Edition referencedOn SoloAssignments.editionId
var number by SoloAssignments.number
var name by SoloAssignments.name
var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline

View File

@ -4,6 +4,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -11,8 +16,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import com.jaytux.grader.data.*
import androidx.compose.ui.window.DialogWindow
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberDialogState
import com.jaytux.grader.data.Course
import com.jaytux.grader.data.Edition
import com.jaytux.grader.data.Group
import com.jaytux.grader.data.Student
import com.jaytux.grader.viewmodel.*
data class Navigators(
@ -29,9 +39,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
val groups by state.groups.entities
val solo by state.solo.entities
val groupAs by state.groupAs.entities
val mergedAssignments by remember(solo, groupAs) {
mutableStateOf(Assignment.merge(groupAs, solo))
}
val mergedAssignments by remember(solo, groupAs) { mutableStateOf(Assignment.merge(groupAs, solo)) }
val hist by state.history
val navs = Navigators(
@ -68,7 +76,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
course, edition, mergedAssignments, id,
{ state.navTo(it) },
{ type, name -> state.newAssignment(type, name) },
{ a, name -> state.setAssignmentTitle(a, name) }
{ a, name -> state.setAssignmentTitle(a, name) },
{ a1, a2 -> state.swapOrder(a1, a2) }
) { a -> state.delete(a) }
}
}
@ -90,7 +99,12 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
}
OpenPanel.Assignment -> {
if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition)
else PaneHeader(mergedAssignments[id].name(), "assignment", course, edition)
else {
when(val a = mergedAssignments[id]) {
is Assignment.SAssignment -> PaneHeader(a.name(), "individual assignment", course, edition)
is Assignment.GAssignment -> PaneHeader(a.name(), "group assignment", course, edition)
}
}
}
}
}
@ -211,7 +225,7 @@ fun AssignmentPanel(
course: Course, edition: Edition, assignments: List<Assignment>,
selected: Int, onSelect: (Int) -> Unit,
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
onDelete: (Assignment) -> Unit
onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit
) = Column(Modifier.padding(10.dp)) {
var showDialog by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(-1) }
@ -264,12 +278,25 @@ fun AssignmentPanel(
{ Text("Add an assignment") },
{ showDialog = true }
) { idx, it ->
SelectEditDeleteRow(
Selectable(
selected == idx,
{ onSelect(idx) }, { onSelect(-1) },
{ editing = idx }, { deleting = idx }
{ onSelect(idx) }, { onSelect(-1) }
) {
Text(it.name(), Modifier.padding(5.dp))
Row {
Text(it.name(), Modifier.padding(5.dp).align(Alignment.CenterVertically).weight(1f))
Column(Modifier.padding(2.dp)) {
Icon(Icons.Default.ArrowUpward, "Move up", Modifier.clickable {
if(idx > 0) onSwapOrder(assignments[idx], assignments[idx - 1])
})
Icon(Icons.Default.ArrowDownward, "Move down", Modifier.clickable {
if(idx < assignments.size - 1) onSwapOrder(assignments[idx], assignments[idx + 1])
})
}
Column(Modifier.padding(2.dp)) {
Icon(Icons.Default.Edit, "Edit", Modifier.clickable { editing = idx })
Icon(Icons.Default.Delete, "Delete", Modifier.clickable { deleting = idx })
}
}
}
}

View File

@ -376,15 +376,26 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{
}
@Composable
fun SelectEditDeleteRow(
fun Selectable(
isSelected: Boolean,
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
content: @Composable BoxScope.() -> Unit
) = Surface(
onSelect: () -> Unit, onDeselect: () -> Unit,
content: @Composable () -> Unit
) {
Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) {
content()
}
}
@Composable
fun SelectEditDeleteRow(
isSelected: Boolean,
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
content: @Composable BoxScope.() -> Unit
) = Selectable(isSelected, onSelect, onDeselect) {
Row {
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() }
IconButton(onEdit, Modifier.align(Alignment.CenterVertically)) {

View File

@ -13,6 +13,7 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
import kotlin.math.max
fun <T> MutableState<T>.immutable(): State<T> = this
fun <T> SizedIterable<T>.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray()))
@ -22,14 +23,17 @@ sealed class Assignment {
class GAssignment(val assignment: GroupAssignment) : Assignment() {
override fun name(): String = assignment.name
override fun id(): EntityID<UUID> = assignment.id
override fun index(): Int? = assignment.number
}
class SAssignment(val assignment: SoloAssignment) : Assignment() {
override fun name(): String = assignment.name
override fun id(): EntityID<UUID> = assignment.id
override fun index(): Int? = assignment.number
}
abstract fun name(): String
abstract fun id(): EntityID<UUID>
abstract fun index(): Int?
companion object {
fun from(assignment: GroupAssignment) = GAssignment(assignment)
@ -38,9 +42,7 @@ sealed class Assignment {
fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>): List<Assignment> {
val g = groups.map { from(it) }
val s = solos.map { from(it) }
return (g + s).sortedBy {
(it as? GAssignment)?.assignment?.name ?: (it as SAssignment).assignment.name
}
return (g + s).sortedWith(compareBy<Assignment> { it.index() }.thenBy { it.name() })
}
}
}
@ -153,9 +155,17 @@ class EditionState(val edition: Edition) {
return instant.toLocalDateTime(TimeZone.currentSystemDefault())
}
private fun nextIdx(): Int = max(
solo.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0,
groupAs.entities.value.maxOfOrNull { it.number ?: 0 } ?: 0
) + 1
fun newSoloAssignment(name: String) {
transaction {
SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() }
SoloAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx()
}
solo.refresh()
}
}
@ -167,7 +177,10 @@ class EditionState(val edition: Edition) {
}
fun newGroupAssignment(name: String) {
transaction {
GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now() }
GroupAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx()
}
groupAs.refresh()
}
}
@ -187,6 +200,42 @@ class EditionState(val edition: Edition) {
is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title)
}
fun swapOrder(a1: Assignment, a2: Assignment) {
transaction {
when(a1) {
is Assignment.GAssignment -> {
when(a2) {
is Assignment.GAssignment -> {
val temp = a1.assignment.number
a1.assignment.number = a2.assignment.number
a2.assignment.number = temp
}
is Assignment.SAssignment -> {
val temp = a1.assignment.number
a1.assignment.number = nextIdx()
a2.assignment.number = temp
}
}
}
is Assignment.SAssignment -> {
when(a2) {
is Assignment.GAssignment -> {
val temp = a1.assignment.number
a1.assignment.number = a2.assignment.number
a2.assignment.number = temp
}
is Assignment.SAssignment -> {
val temp = a1.assignment.number
a1.assignment.number = a2.assignment.number
a2.assignment.number = temp
}
}
}
}
}
solo.refresh(); groupAs.refresh()
}
fun delete(s: Student) {
transaction {
EditionStudents.deleteWhere { studentId eq s.id }