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") { object GroupAssignments : UUIDTable("grpAssgmts") {
val editionId = reference("edition_id", Editions.id) val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable()
val name = varchar("name", 50) val name = varchar("name", 50)
val assignment = text("assignment") val assignment = text("assignment")
val deadline = datetime("deadline") val deadline = datetime("deadline")
@ -60,6 +61,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
object SoloAssignments : UUIDTable("soloAssgmts") { object SoloAssignments : UUIDTable("soloAssgmts") {
val editionId = reference("edition_id", Editions.id) val editionId = reference("edition_id", Editions.id)
val number = integer("number").nullable()
val name = varchar("name", 50) val name = varchar("name", 50)
val assignment = text("assignment") val assignment = text("assignment")
val deadline = datetime("deadline") val deadline = datetime("deadline")

View File

@ -10,9 +10,8 @@ import java.util.UUID
class Course(id: EntityID<UUID>) : Entity<UUID>(id) { class Course(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, Course>(Courses) companion object : EntityClass<UUID, Course>(Courses)
fun loadEditions() = transaction { Edition.find { Editions.courseId eq this@Course.id }.toList() }
var name by Courses.name var name by Courses.name
val editions by Edition referrersOn Editions.courseId
} }
class Edition(id: EntityID<UUID>) : Entity<UUID>(id) { 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) companion object : EntityClass<UUID, GroupAssignment>(GroupAssignments)
var edition by Edition referencedOn GroupAssignments.editionId var edition by Edition referencedOn GroupAssignments.editionId
var number by GroupAssignments.number
var name by GroupAssignments.name var name by GroupAssignments.name
var assignment by GroupAssignments.assignment var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline var deadline by GroupAssignments.deadline
@ -66,6 +66,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments) companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments)
var edition by Edition referencedOn SoloAssignments.editionId var edition by Edition referencedOn SoloAssignments.editionId
var number by SoloAssignments.number
var name by SoloAssignments.name var name by SoloAssignments.name
var assignment by SoloAssignments.assignment var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment 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.text.style.TextAlign
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.window.* import androidx.compose.ui.window.DialogWindow
import com.jaytux.grader.data.* 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.* import com.jaytux.grader.viewmodel.*
data class Navigators( data class Navigators(
@ -29,9 +39,7 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
val groups by state.groups.entities val groups by state.groups.entities
val solo by state.solo.entities val solo by state.solo.entities
val groupAs by state.groupAs.entities val groupAs by state.groupAs.entities
val mergedAssignments by remember(solo, groupAs) { val mergedAssignments by remember(solo, groupAs) { mutableStateOf(Assignment.merge(groupAs, solo)) }
mutableStateOf(Assignment.merge(groupAs, solo))
}
val hist by state.history val hist by state.history
val navs = Navigators( val navs = Navigators(
@ -68,7 +76,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
course, edition, mergedAssignments, id, course, edition, mergedAssignments, id,
{ state.navTo(it) }, { state.navTo(it) },
{ type, name -> state.newAssignment(type, name) }, { 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) } ) { a -> state.delete(a) }
} }
} }
@ -90,7 +99,12 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
} }
OpenPanel.Assignment -> { OpenPanel.Assignment -> {
if(id == -1) PaneHeader("Nothing selected", "assignments", course, edition) 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>, course: Course, edition: Edition, assignments: List<Assignment>,
selected: Int, onSelect: (Int) -> Unit, selected: Int, onSelect: (Int) -> Unit,
onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit, onAdd: (AssignmentType, String) -> Unit, onUpdate: (Assignment, String) -> Unit,
onDelete: (Assignment) -> Unit onSwapOrder: (Assignment, Assignment) -> Unit, onDelete: (Assignment) -> Unit
) = Column(Modifier.padding(10.dp)) { ) = Column(Modifier.padding(10.dp)) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(-1) } var deleting by remember { mutableStateOf(-1) }
@ -264,12 +278,25 @@ fun AssignmentPanel(
{ Text("Add an assignment") }, { Text("Add an assignment") },
{ showDialog = true } { showDialog = true }
) { idx, it -> ) { idx, it ->
SelectEditDeleteRow( Selectable(
selected == idx, selected == idx,
{ onSelect(idx) }, { onSelect(-1) }, { onSelect(idx) }, { onSelect(-1) }
{ editing = idx }, { deleting = idx }
) { ) {
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 @Composable
fun SelectEditDeleteRow( fun Selectable(
isSelected: Boolean, isSelected: Boolean,
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit, onSelect: () -> Unit, onDeselect: () -> Unit,
content: @Composable BoxScope.() -> Unit content: @Composable () -> Unit
) = Surface( ) {
Surface(
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() }, Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
tonalElevation = if (isSelected) 50.dp else 0.dp, tonalElevation = if (isSelected) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium 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 { Row {
Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() } Box(Modifier.weight(1f).align(Alignment.CenterVertically)) { content() }
IconButton(onEdit, Modifier.align(Alignment.CenterVertically)) { 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.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.util.* import java.util.*
import kotlin.math.max
fun <T> MutableState<T>.immutable(): State<T> = this fun <T> MutableState<T>.immutable(): State<T> = this
fun <T> SizedIterable<T>.sortAsc(vararg columns: Expression<*>) = this.orderBy(*(columns.map { it to SortOrder.ASC }.toTypedArray())) 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() { class GAssignment(val assignment: GroupAssignment) : Assignment() {
override fun name(): String = assignment.name override fun name(): String = assignment.name
override fun id(): EntityID<UUID> = assignment.id override fun id(): EntityID<UUID> = assignment.id
override fun index(): Int? = assignment.number
} }
class SAssignment(val assignment: SoloAssignment) : Assignment() { class SAssignment(val assignment: SoloAssignment) : Assignment() {
override fun name(): String = assignment.name override fun name(): String = assignment.name
override fun id(): EntityID<UUID> = assignment.id override fun id(): EntityID<UUID> = assignment.id
override fun index(): Int? = assignment.number
} }
abstract fun name(): String abstract fun name(): String
abstract fun id(): EntityID<UUID> abstract fun id(): EntityID<UUID>
abstract fun index(): Int?
companion object { companion object {
fun from(assignment: GroupAssignment) = GAssignment(assignment) fun from(assignment: GroupAssignment) = GAssignment(assignment)
@ -38,9 +42,7 @@ sealed class Assignment {
fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>): List<Assignment> { fun merge(groups: List<GroupAssignment>, solos: List<SoloAssignment>): List<Assignment> {
val g = groups.map { from(it) } val g = groups.map { from(it) }
val s = solos.map { from(it) } val s = solos.map { from(it) }
return (g + s).sortedBy { return (g + s).sortedWith(compareBy<Assignment> { it.index() }.thenBy { it.name() })
(it as? GAssignment)?.assignment?.name ?: (it as SAssignment).assignment.name
}
} }
} }
} }
@ -153,9 +155,17 @@ class EditionState(val edition: Edition) {
return instant.toLocalDateTime(TimeZone.currentSystemDefault()) 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) { fun newSoloAssignment(name: String) {
transaction { 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() solo.refresh()
} }
} }
@ -167,7 +177,10 @@ class EditionState(val edition: Edition) {
} }
fun newGroupAssignment(name: String) { fun newGroupAssignment(name: String) {
transaction { 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() groupAs.refresh()
} }
} }
@ -187,6 +200,42 @@ class EditionState(val edition: Edition) {
is Assignment.SAssignment -> setSoloAssignmentTitle(assignment.assignment, title) 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) { fun delete(s: Student) {
transaction { transaction {
EditionStudents.deleteWhere { studentId eq s.id } EditionStudents.deleteWhere { studentId eq s.id }