This commit is contained in:
2025-02-19 09:27:21 +01:00
commit bccff22866
20 changed files with 922 additions and 0 deletions

View File

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

View File

@ -0,0 +1,23 @@
package com.jaytux.grader
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.jaytux.grader.ui.CoursesView
import com.jaytux.grader.viewmodel.CourseListState
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
val courseList = CourseListState()
Surface(Modifier.fillMaxSize().padding(10.dp)) {
CoursesView(courseList)
}
}
}

View File

@ -0,0 +1,86 @@
package com.jaytux.grader.data
import org.jetbrains.exposed.dao.id.CompositeIdTable
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Table
object Courses : UUIDTable("courses") {
val name = varchar("name", 50).uniqueIndex()
}
object Editions : UUIDTable("editions") {
val courseId = reference("course_id", Courses.id)
val name = varchar("name", 50)
init {
uniqueIndex(courseId, name)
}
}
object Groups : UUIDTable("groups") {
val editionId = reference("edition_id", Editions.id)
val name = varchar("name", 50)
init {
uniqueIndex(editionId, name)
}
}
object Students : UUIDTable("students") {
val name = varchar("name", 50)
val note = text("note")
}
object GroupStudents : Table("grpStudents") {
val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id)
override val primaryKey = PrimaryKey(groupId, studentId)
}
object SoloStudents : Table("soloStudents") {
val editionId = reference("edition_id", Editions.id)
val studentId = reference("student_id", Students.id)
override val primaryKey = PrimaryKey(studentId)
}
object GroupAssignments : UUIDTable("grpAssgmts") {
val editionId = reference("edition_id_", Editions.id)
val name = varchar("name_", 50)
val assignment = text("assignment_")
}
object SoloAssignments : UUIDTable("soloAssgmts") {
val editionId = GroupAssignments.reference("edition_id", Editions.id)
val name = GroupAssignments.varchar("name", 50)
val assignment = GroupAssignments.text("assignment")
}
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
val groupId = reference("group_id", Groups.id)
val feedback = text("feedback")
val grade = varchar("grade", 32)
override val primaryKey = PrimaryKey(groupAssignmentId, groupId)
}
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
val groupAssignmentId = reference("group_assignment_id", GroupAssignments.id)
val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id)
val feedback = text("feedback")
val grade = varchar("grade", 32)
override val primaryKey = PrimaryKey(groupAssignmentId, studentId)
}
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val soloAssignmentId = reference("solo_assignment_id", SoloAssignments.id)
val studentId = reference("student_id", Students.id)
val feedback = text("feedback")
val grade = varchar("grade", 32)
override val primaryKey = PrimaryKey(soloAssignmentId, studentId)
}

View File

@ -0,0 +1,22 @@
package com.jaytux.grader.data
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object Database {
val db by lazy {
val actual = Database.connect("jdbc:sqlite:file:./grader.db", "org.sqlite.JDBC")
transaction {
SchemaUtils.create(
Courses, Editions, Groups,
Students, GroupStudents, SoloStudents,
GroupAssignments, SoloAssignments,
GroupFeedbacks, IndividualFeedbacks, SoloFeedbacks
)
}
actual
}
fun init() { db }
}

View File

@ -0,0 +1,85 @@
package com.jaytux.grader.data
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.transactions.transaction
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
}
class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, Edition>(Editions)
var course by Course referencedOn Editions.courseId
var name by Editions.name
val groups by Group referrersOn Groups.editionId
val soloStudents by Student via SoloStudents
}
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, Group>(Groups)
var edition by Edition referencedOn Groups.editionId
var name by Groups.name
val students by Student via GroupStudents
}
class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, Student>(Students)
var name by Students.name
var note by Students.note
val groups by Group via GroupStudents
val soloCourses by Edition via SoloStudents
}
class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, GroupAssignment>(GroupAssignments)
var edition by Edition referencedOn GroupAssignments.editionId
var name by GroupAssignments.name
var assignment by GroupAssignments.assignment
}
class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
companion object : EntityClass<UUID, SoloAssignment>(SoloAssignments)
var edition by Edition referencedOn SoloAssignments.editionId
var name by SoloAssignments.name
var assignment by SoloAssignments.assignment
}
class GroupFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
companion object : EntityClass<CompositeID, GroupFeedback>(GroupFeedbacks)
var group by Group referencedOn GroupFeedbacks.groupId
var assignment by GroupAssignment referencedOn GroupFeedbacks.groupAssignmentId
var feedback by GroupFeedbacks.feedback
var grade by GroupFeedbacks.grade
}
class IndividualFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
companion object : EntityClass<CompositeID, IndividualFeedback>(IndividualFeedbacks)
var student by Student referencedOn IndividualFeedbacks.studentId
var assignment by SoloAssignment referencedOn IndividualFeedbacks.groupAssignmentId
var feedback by IndividualFeedbacks.feedback
var grade by IndividualFeedbacks.grade
}
class SoloFeedback(id: EntityID<CompositeID>) : Entity<CompositeID>(id) {
companion object : EntityClass<CompositeID, SoloFeedback>(SoloFeedbacks)
var student by Student referencedOn SoloFeedbacks.studentId
var assignment by SoloAssignment referencedOn SoloFeedbacks.soloAssignmentId
var feedback by SoloFeedbacks.feedback
var grade by SoloFeedbacks.grade
}

View File

@ -0,0 +1,18 @@
package com.jaytux.grader
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.jaytux.grader.App
import com.jaytux.grader.data.Database
fun main(){
Database.init()
application {
Window(
onCloseRequest = ::exitApplication,
title = "Grader",
) {
App()
}
}
}

View File

@ -0,0 +1,95 @@
package com.jaytux.grader.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import com.jaytux.grader.data.Course
import com.jaytux.grader.viewmodel.CourseListState
@Composable
fun CoursesView(state: CourseListState) {
val data by state.courses.entities
var showDialog by remember { mutableStateOf(false) }
if(data.isEmpty()) {
Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.Center)) {
Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally))
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
Text("Add a course")
}
}
}
}
else {
LazyColumn(Modifier.fillMaxSize()) {
items(data) { CourseWidget(it) { state.delete(it) } }
item {
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
Text("Add course")
}
}
}
}
if(showDialog) AddCourseDialog(data.map { it.name }, { showDialog = false }) { state.new(it) }
}
@Composable
fun AddCourseDialog(taken: List<String>, onClose: () -> Unit, onSave: (String) -> Unit) = DialogWindow(
onCloseRequest = onClose,
state = rememberDialogState(size = DpSize(400.dp, 300.dp), position = WindowPosition(Alignment.Center))
) {
Surface(Modifier.fillMaxSize().padding(10.dp)) {
Box(Modifier.fillMaxSize()) {
var name by remember { mutableStateOf("") }
Column(Modifier.align(Alignment.Center)) {
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Course name") }, isError = name in taken)
Row {
Button({ onClose() }, Modifier.weight(0.45f)) { Text("Cancel") }
Spacer(Modifier.weight(0.1f))
Button({ onSave(name); onClose() }, Modifier.weight(0.45f)) { Text("Save") }
}
}
}
}
}
@Composable
fun CourseWidget(course: Course, onDelete: () -> Unit) {
val editions = remember(course) { course.loadEditions().size }
Surface(Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 10.dp), shape = MaterialTheme.shapes.medium, tonalElevation = 2.dp, shadowElevation = 2.dp) {
Row {
Column(Modifier.weight(1f)) {
Text(course.name, Modifier.padding(5.dp), style = MaterialTheme.typography.headlineMedium)
Row {
Spacer(Modifier.width(15.dp))
Text("$editions editions", fontStyle = FontStyle.Italic)
}
}
Column {
IconButton({ onDelete() }) { Icon(Icons.Default.Delete, "Remove") }
IconButton({ TODO() }) { Icon(Icons.Default.Edit, "Edit") }
}
}
}
}

View File

@ -0,0 +1,43 @@
package com.jaytux.grader.viewmodel
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import com.jaytux.grader.data.Course
import com.jaytux.grader.data.Edition
import com.jaytux.grader.data.Editions
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
class RawDbState<T: Entity<UUID>>(private val loader: (Transaction.() -> List<T>)) {
private fun <T> MutableState<T>.immutable(): State<T> = this@immutable
private val rawEntities by lazy {
mutableStateOf(transaction { loader() })
}
val entities = rawEntities.immutable()
fun refresh() {
rawEntities.value = transaction { loader() }
}
}
class CourseListState {
val courses = RawDbState { Course.all().toList() }
fun new(name: String) {
transaction { Course.new { this.name = name } }
courses.refresh()
}
fun delete(course: Course) {
transaction { course.delete() }
courses.refresh()
}
}
class EditionListState(private val course: Course) {
val editions = RawDbState { Edition.find { Editions.courseId eq course.id }.toList() }
}