Almost finished!

This commit is contained in:
jay-tux 2025-02-24 09:56:44 +01:00
parent c8b605353c
commit 054970bb79
Signed by: jay-tux
GPG Key ID: 84302006B056926E
8 changed files with 206 additions and 48 deletions

View File

@ -1,8 +1,10 @@
package com.jaytux.grader.data
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.id.CompositeIdTable
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
object Courses : UUIDTable("courses") {
val name = varchar("name", 50).uniqueIndex()
@ -53,12 +55,14 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
val editionId = reference("edition_id", Editions.id)
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")
}
object SoloAssignments : UUIDTable("soloAssgmts") {
val editionId = reference("edition_id", Editions.id)
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")
}
object GroupFeedbacks : CompositeIdTable("grpFdbks") {

View File

@ -14,6 +14,14 @@ object Database {
GroupAssignments, SoloAssignments,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
)
val addMissing = SchemaUtils.addMissingColumnsStatements(
Courses, Editions, Groups,
Students, GroupStudents, EditionStudents,
GroupAssignments, SoloAssignments,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
)
addMissing.forEach { exec(it) }
}
actual
}

View File

@ -59,6 +59,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var edition by Edition referencedOn GroupAssignments.editionId
var name by GroupAssignments.name
var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline
}
class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
@ -67,6 +68,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var edition by Edition referencedOn SoloAssignments.editionId
var name by SoloAssignments.name
var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline
}
class GroupFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {

View File

@ -4,7 +4,6 @@ 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.OutlinedTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -13,13 +12,20 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.jaytux.grader.viewmodel.GroupAssignmentState
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
import kotlinx.datetime.format.byUnicodePattern
@OptIn(ExperimentalMaterial3Api::class, FormatStringsInDatetimeFormats::class)
@Composable
fun GroupAssignmentView(state: GroupAssignmentState) {
val (course, edition) = state.editionCourse
val name by state.name
val task by state.task
val deadline by state.deadline
val allFeedback by state.feedback.entities
var idx by remember { mutableStateOf(0) }
@ -45,11 +51,34 @@ fun GroupAssignmentView(state: GroupAssignmentState) {
if(idx == 0) {
var updTask by remember { mutableStateOf(task) }
Row {
var showPicker by remember { mutableStateOf(false) }
val dateState = rememberDatePickerState()
Text("Deadline: ${deadline.format(LocalDateTime.Format { byUnicodePattern("dd/MM/yyyy - HH:mm") })}", Modifier.align(Alignment.CenterVertically))
Spacer(Modifier.width(10.dp))
Button({ showPicker = true }) { Text("Change") }
if(showPicker) DatePickerDialog(
{ showPicker = false },
{ Button({ showPicker = false; dateState.selectedDateMillis?.let { state.updateDeadline(it) } }) { Text("Set deadline") } },
Modifier,
{ Button({ showPicker = false }) { Text("Cancel") } },
shape = MaterialTheme.shapes.medium,
tonalElevation = 10.dp,
colors = DatePickerDefaults.colors(),
properties = DialogProperties()
) {
DatePicker(
dateState,
Modifier.fillMaxWidth().padding(10.dp),
)
}
}
OutlinedTextField(updTask, { updTask = it }, Modifier.fillMaxWidth().weight(1f), singleLine = false, minLines = 5, label = { Text("Task") })
CancelSaveRow(updTask != task, { updTask = task }, "Reset", "Update") { state.updateTask(updTask) }
}
else {
val (group, feedback, individual) = allFeedback[idx - 1].second
groupFeedback(state, allFeedback[idx - 1].second)
}
}
@ -108,7 +137,24 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
}
}
else {
//
val (student, details) = individual[idx - 1]
var sGrade by remember { mutableStateOf(details.second?.grade ?: "") }
var sMsg by remember { mutableStateOf(TextFieldValue(details.second?.feedback ?: "")) }
Row {
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(sGrade, { sGrade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button({ state.upsertIndividualFeedback(student, group, sMsg.text, sGrade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = sGrade.isNotBlank() || sMsg.text.isNotBlank()) {
Text("Save")
}
}
AutocompleteLineField(
sMsg, { sMsg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter ->
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
}
}
}
}

View File

@ -2,11 +2,11 @@ package com.jaytux.grader.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface

View File

@ -4,13 +4,8 @@ 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.Checkbox
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
@ -41,6 +36,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 available by state.availableStudents.entities
val toggle = { i: Int, p: Panel ->
idx = if(idx?.p == p && idx?.i == i) null else Current(p, i)
@ -76,7 +72,8 @@ fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
} else {
Box(Modifier.weight(0.5f)) {
StudentsWidget(
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) }
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) },
available, { state.addToCourse(it) }
) { name, note, contact, addToEdition ->
state.newStudent(name, contact, note, addToEdition)
}
@ -135,12 +132,13 @@ fun <T> EditionSideWidget(
@Composable
fun StudentsWidget(
course: Course, edition: Edition, students: List<Student>, selected: Int?, onSelect: (Int) -> Unit,
availableStudents: List<Student>, onImport: (List<Student>) -> Unit,
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
) = EditionSideWidget(
course, edition, "Student list", "students", "a student", students, selected, onSelect,
{ Text(it.name, Modifier.padding(5.dp)) }
) { onExit ->
StudentDialog(course, edition, onExit, onAdd)
StudentDialog(course, edition, onExit, availableStudents, onImport, onAdd)
}
@Composable
@ -148,29 +146,93 @@ fun StudentDialog(
course: Course,
edition: Edition,
onClose: () -> Unit,
availableStudents: List<Student>,
onImport: (List<Student>) -> Unit,
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
) = DialogWindow(
onCloseRequest = onClose,
state = rememberDialogState(size = DpSize(600.dp, 400.dp), position = WindowPosition(Alignment.Center))
) {
Surface(Modifier.fillMaxSize().padding(10.dp)) {
Box(Modifier.fillMaxSize()) {
var name by remember { mutableStateOf("") }
var contact by remember { mutableStateOf("") }
var note by remember { mutableStateOf("") }
var add by remember { mutableStateOf(true) }
Surface(Modifier.fillMaxSize()) {
Column(Modifier.padding(10.dp)) {
var isImport by remember { mutableStateOf(false) }
TabRow(if(isImport) 1 else 0) {
Tab(!isImport, { isImport = false }) { Text("Add new student") }
Tab(isImport, { isImport = true }) { Text("Add existing student") }
}
Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), singleLine = true, label = { Text("Student name") })
OutlinedTextField(contact, { contact = it }, Modifier.fillMaxWidth(), singleLine = true, label = { Text("Student contact") })
OutlinedTextField(note, { note = it }, Modifier.fillMaxWidth(), singleLine = false, minLines = 3, label = { Text("Note") })
Row {
Checkbox(add, { add = it })
Text("Add student to ${course.name} ${edition.name}?", Modifier.align(Alignment.CenterVertically))
if(isImport) {
if(availableStudents.isEmpty()) {
Box(Modifier.fillMaxSize()) {
Text("No students available to add to this course.", Modifier.align(Alignment.Center))
}
}
CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
onAdd(name, note, contact, add)
onClose()
else {
var selected by remember { mutableStateOf(setOf<Int>()) }
val onClick = { idx: Int ->
selected = if(idx in selected) selected - idx else selected + idx
}
Text("Select students to add to ${course.name} ${edition.name}")
LazyColumn {
itemsIndexed(availableStudents) { idx, student ->
Surface(
Modifier.fillMaxWidth().clickable { onClick(idx) },
tonalElevation = if (selected.contains(idx)) 5.dp else 0.dp
) {
Row {
Checkbox(selected.contains(idx), { onClick(idx) })
Text(student.name, Modifier.padding(5.dp))
}
}
}
}
CancelSaveRow(selected.isNotEmpty(), onClose) {
onImport(selected.map { idx -> availableStudents[idx] })
onClose()
}
}
}
else {
Box(Modifier.fillMaxSize()) {
var name by remember { mutableStateOf("") }
var contact by remember { mutableStateOf("") }
var note by remember { mutableStateOf("") }
var add by remember { mutableStateOf(true) }
Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(
name,
{ name = it },
Modifier.fillMaxWidth(),
singleLine = true,
label = { Text("Student name") })
OutlinedTextField(
contact,
{ contact = it },
Modifier.fillMaxWidth(),
singleLine = true,
label = { Text("Student contact") })
OutlinedTextField(
note,
{ note = it },
Modifier.fillMaxWidth(),
singleLine = false,
minLines = 3,
label = { Text("Note") })
Row {
Checkbox(add, { add = it })
Text(
"Add student to ${course.name} ${edition.name}?",
Modifier.align(Alignment.CenterVertically)
)
}
CancelSaveRow(name.isNotBlank() && contact.isNotBlank(), onClose) {
onAdd(name, note, contact, add)
onClose()
}
}
}
}
}

View File

@ -67,8 +67,8 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, onS
onCloseRequest = onClose,
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
) {
Surface(Modifier.fillMaxSize().padding(10.dp)) {
Box(Modifier.fillMaxSize()) {
Surface(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize().padding(10.dp)) {
var name by remember { mutableStateOf("") }
Column(Modifier.align(Alignment.Center)) {
androidx.compose.material.OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text(label) }, isError = name in taken)

View File

@ -4,11 +4,18 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import com.jaytux.grader.data.*
import com.jaytux.grader.data.EditionStudents.editionId
import com.jaytux.grader.data.EditionStudents.studentId
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
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()))
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
@ -23,7 +30,7 @@ class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
}
class CourseListState {
val courses = RawDbState { Course.all().toList() }
val courses = RawDbState { Course.all().sortAsc(Courses.name).toList() }
fun new(name: String) {
transaction { Course.new { this.name = name } }
@ -39,7 +46,7 @@ class CourseListState {
}
class EditionListState(val course: Course) {
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.toList() }
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.sortAsc(Editions.name).toList() }
fun new(name: String) {
transaction { Edition.new { this.name = name; this.course = this@EditionListState.course } }
@ -54,10 +61,16 @@ class EditionListState(val course: Course) {
class EditionState(val edition: Edition) {
val course = transaction { edition.course }
val students = RawDbState { edition.soloStudents.toList() }
val groups = RawDbState { edition.groups.toList() }
val solo = RawDbState { edition.soloAssignments.toList() }
val groupAs = RawDbState { edition.groupAssignments.toList() }
val students = RawDbState { edition.soloStudents.sortAsc(Students.name).toList() }
val groups = RawDbState { edition.groups.sortAsc(Groups.name).toList() }
val solo = RawDbState { edition.soloAssignments.sortAsc(SoloAssignments.name).toList() }
val groupAs = RawDbState { edition.groupAssignments.sortAsc(GroupAssignments.name).toList() }
val availableStudents = RawDbState {
Student.find {
(Students.id notInList edition.soloStudents.map { it.id })
}.toList()
}
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
transaction {
@ -69,6 +82,18 @@ class EditionState(val edition: Edition) {
}
if(addToEdition) students.refresh()
else availableStudents.refresh()
}
fun addToCourse(students: List<Student>) {
transaction {
EditionStudents.batchInsert(students) {
this[editionId] = edition.id
this[studentId] = it.id
}
}
availableStudents.refresh();
this.students.refresh()
}
fun newGroup(name: String) {
@ -94,8 +119,10 @@ class EditionState(val edition: Edition) {
class StudentState(val student: Student, edition: Edition) {
val editionCourse = transaction { edition.course to edition }
val groups = RawDbState { student.groups.map { it to (it.edition.course.name to it.edition.name) }.toList() }
val courseEditions = RawDbState { student.courses.map{ it to it.course }.toList() }
val groups = RawDbState { student.groups.sortAsc(Groups.name).map { it to (it.edition.course.name to it.edition.name) }.toList() }
val courseEditions = RawDbState { student.courses.map{ it to it.course }.sortedWith {
(e1, c1), (e2, c2) -> c1.name.compareTo(c2.name).let { if(it == 0) e1.name.compareTo(e2.name) else it }
}.toList() }
fun update(f: Student.() -> Unit) {
transaction {
@ -105,17 +132,17 @@ class StudentState(val student: Student, edition: Edition) {
}
class GroupState(val group: Group) {
val members = RawDbState { group.studentRoles.map{ it.student to it.role }.toList() }
val members = RawDbState { group.studentRoles.map{ it.student to it.role }.sortedBy { it.first.name }.toList() }
val availableStudents = RawDbState { Student.find {
// not yet in the group
(Students.id notInList group.students.map { it.id }) and
// but in the same course (edition)
(Students.id inList group.edition.soloStudents.map { it.id })
}.toList() }
}.sortAsc(Students.name).toList() }
val course = transaction { group.edition.course to group.edition }
val roles = RawDbState {
GroupStudents.select(GroupStudents.role).where{ GroupStudents.role.isNotNull() }
.withDistinct(true).map{ it[GroupStudents.role] ?: "" }.toList()
.withDistinct(true).sortAsc(GroupStudents.role).map{ it[GroupStudents.role] ?: "" }.toList()
}
fun addStudent(student: Student) {
@ -151,24 +178,25 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
data class LocalGFeedback(
val group: Group,
val feedback: LocalFeedback?,
val individuals: Map<Student, Pair<String?, LocalFeedback?>>
val individuals: List<Pair<Student, Pair<String?, LocalFeedback?>>>
)
val editionCourse = transaction { assignment.edition.course to assignment.edition }
private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
val feedback = RawDbState { loadFeedback() }
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val autofill = RawDbState {
val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
it[GroupFeedbacks.feedback].split('\n')
}.toList()
}
val forIndividuals = IndividualFeedbacks.selectAll().where { IndividualFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
it[IndividualFeedbacks.feedback].split('\n')
}.toList()
}
(forGroups + forIndividuals).distinct()
(forGroups + forIndividuals).distinct().sorted()
}
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
@ -186,8 +214,8 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val groups = Group.find {
(Groups.editionId eq assignment.edition.id)
}.map { group ->
val students = group.studentRoles.associate { sR ->
}.sortAsc(Groups.name).map { group ->
val students = group.studentRoles.sortedBy { it.student.name }.map { sR ->
val student = sR.student
val role = sR.role
val feedback = individuals[student.id]
@ -234,6 +262,14 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
}
_task.value = t
}
fun updateDeadline(instant: Long) {
val d = Instant.fromEpochMilliseconds(instant).toLocalDateTime(TimeZone.currentSystemDefault())
transaction {
assignment.deadline = d
}
_deadline.value = d
}
}