Re-orderable assignments
This commit is contained in:
parent
63c4197cfc
commit
49e3b8126f
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -375,16 +375,27 @@ fun ItalicAndNormal(italic: String, normal: String) = Row{
|
||||||
Text(normal)
|
Text(normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Selectable(
|
||||||
|
isSelected: Boolean,
|
||||||
|
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
|
@Composable
|
||||||
fun SelectEditDeleteRow(
|
fun SelectEditDeleteRow(
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
|
onSelect: () -> Unit, onDeselect: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit,
|
||||||
content: @Composable BoxScope.() -> Unit
|
content: @Composable BoxScope.() -> Unit
|
||||||
) = Surface(
|
) = Selectable(isSelected, onSelect, onDeselect) {
|
||||||
Modifier.fillMaxWidth().clickable { if(isSelected) onDeselect() else onSelect() },
|
|
||||||
tonalElevation = if (isSelected) 50.dp else 0.dp,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
|
||||||
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)) {
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
Loading…
Reference in New Issue