Merge pull request 'Fix some assesment-related bugs' (#7) from fix/ids_constraints into main

Reviewed-on: jay-tux/grader#7
This commit is contained in:
2025-06-11 10:30:48 +02:00
8 changed files with 116 additions and 106 deletions

View File

@ -56,6 +56,7 @@ object GroupAssignments : UUIDTable("grpAssgmts") {
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")
val globalCriterion = reference("global_crit", GroupAssignmentCriteria.id)
}
object GroupAssignmentCriteria : UUIDTable("grpAsCr") {
@ -70,6 +71,7 @@ object SoloAssignments : UUIDTable("soloAssgmts") {
val name = varchar("name", 50)
val assignment = text("assignment")
val deadline = datetime("deadline")
val globalCriterion = reference("global_crit", SoloAssignmentCriteria.id)
}
object SoloAssignmentCriteria : UUIDTable("soloAsCr") {
@ -86,7 +88,7 @@ object PeerEvaluations : UUIDTable("peerEvals") {
object GroupFeedbacks : CompositeIdTable("grpFdbks") {
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable()
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
val groupId = reference("group_id", Groups.id)
val feedback = text("feedback")
val grade = varchar("grade", 32)
@ -96,7 +98,7 @@ object GroupFeedbacks : CompositeIdTable("grpFdbks") {
object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
val assignmentId = reference("group_assignment_id", GroupAssignments.id)
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id).nullable()
val criterionId = reference("criterion_id", GroupAssignmentCriteria.id)
val groupId = reference("group_id", Groups.id)
val studentId = reference("student_id", Students.id)
val feedback = text("feedback")
@ -107,7 +109,7 @@ object IndividualFeedbacks : CompositeIdTable("indivFdbks") {
object SoloFeedbacks : CompositeIdTable("soloFdbks") {
val assignmentId = reference("solo_assignment_id", SoloAssignments.id)
val criterionId = reference("criterion_id", SoloAssignmentCriteria.id).nullable()
val criterionId = reference("criterion_id", SoloAssignmentCriteria.id)
val studentId = reference("student_id", Students.id)
val feedback = text("feedback")
val grade = varchar("grade", 32)

View File

@ -3,7 +3,10 @@ package com.jaytux.grader.data
import MigrationUtils
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Database {
val db by lazy {

View File

@ -60,6 +60,7 @@ class GroupAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by GroupAssignments.name
var assignment by GroupAssignments.assignment
var deadline by GroupAssignments.deadline
var globalCriterion by GroupAssignmentCriterion referencedOn GroupAssignments.globalCriterion
val criteria by GroupAssignmentCriterion referrersOn GroupAssignmentCriteria.assignmentId
}
@ -98,6 +99,7 @@ class SoloAssignment(id: EntityID<UUID>) : Entity<UUID>(id) {
var name by SoloAssignments.name
var assignment by SoloAssignments.assignment
var deadline by SoloAssignments.deadline
var globalCriterion by SoloAssignmentCriterion referencedOn SoloAssignments.globalCriterion
val criteria by SoloAssignmentCriterion referrersOn SoloAssignmentCriteria.assignmentId
}

View File

@ -12,11 +12,9 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
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.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.jaytux.grader.data.GroupAssignment
import com.jaytux.grader.data.GroupAssignmentCriterion
import com.jaytux.grader.data.SoloAssignmentCriterion
import com.jaytux.grader.data.Student
@ -26,7 +24,6 @@ import com.jaytux.grader.viewmodel.SoloAssignmentState
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.OutlinedRichTextEditor
import kotlinx.datetime.LocalDateTime
import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction
@Composable
fun GroupAssignmentView(state: GroupAssignmentState) {
@ -126,14 +123,7 @@ fun groupTaskWidget(
Row {
DateTimePicker(deadline, onSetDeadline)
}
RichTextStyleRow(state = updTask)
OutlinedRichTextEditor(
state = updTask,
modifier = Modifier.fillMaxWidth().weight(1f),
singleLine = false,
minLines = 5,
label = { Text("Task") }
)
RichTextField(updTask, Modifier.fillMaxWidth().weight(1f)) { Text("Task") }
CancelSaveRow(
true,
{ updTask.setMarkdown(taskMD) },
@ -188,7 +178,7 @@ fun groupTaskWidget(
@Composable
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
val (group, feedback, individual) = fdbk
var idx by remember(fdbk) { mutableStateOf(0) }
var studentIdx by remember(fdbk) { mutableStateOf(0) }
var critIdx by remember(fdbk) { mutableStateOf(0) }
val criteria by state.criteria.entities
val suggestions by state.autofill.entities
@ -198,8 +188,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
item {
Surface(
Modifier.fillMaxWidth().clickable { idx = 0 },
tonalElevation = if (idx == 0) 50.dp else 0.dp,
Modifier.fillMaxWidth().clickable { studentIdx = 0 },
tonalElevation = if (studentIdx == 0) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) {
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
@ -209,8 +199,8 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
itemsIndexed(individual.toList()) { i, (student, details) ->
val (role, _) = details
Surface(
Modifier.fillMaxWidth().clickable { idx = i + 1 },
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
Modifier.fillMaxWidth().clickable { studentIdx = i + 1 },
tonalElevation = if (studentIdx == i + 1) 50.dp else 0.dp,
shape = MaterialTheme.shapes.medium
) {
Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
@ -219,45 +209,25 @@ fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalG
}
}
val updateGrade = { grade: String ->
if(idx == 0) {
state.upsertGroupFeedback(group, feedback.global?.feedback ?: "", grade)
}
else {
val ind = individual[idx - 1]
val glob = ind.second.second.global
state.upsertIndividualFeedback(ind.first, group, glob?.feedback ?: "", grade)
}
}
val updateFeedback = { fdbk: String ->
if(idx == 0) {
if(critIdx == 0) {
state.upsertGroupFeedback(group, fdbk, feedback.global?.grade ?: "", null)
}
else {
val current = feedback.byCriterion[critIdx - 1]
state.upsertGroupFeedback(group, fdbk, current.entry?.grade ?: "", current.criterion)
}
}
else {
val ind = individual[idx - 1]
if(critIdx == 0) {
val entry = ind.second.second
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.global?.grade ?: "", null)
}
else {
val entry = ind.second.second.byCriterion[critIdx - 1]
state.upsertIndividualFeedback(ind.first, group, fdbk, entry.entry?.grade ?: "", entry.criterion)
}
val onSave = { grade: String, fdbk: String ->
when {
studentIdx == 0 && critIdx == 0 -> state.upsertGroupFeedback(group, fdbk, grade)
studentIdx == 0 && critIdx != 0 -> state.upsertGroupFeedback(group, fdbk, grade, criteria[critIdx - 1])
studentIdx != 0 && critIdx == 0 -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade)
else -> state.upsertIndividualFeedback(individual[studentIdx - 1].first, group, fdbk, grade, criteria[critIdx - 1])
}
}
groupFeedbackPane(
criteria, critIdx, { critIdx = it }, feedback.global,
if(critIdx == 0) feedback.global else feedback.byCriterion[critIdx - 1].entry,
suggestions, updateGrade, updateFeedback, Modifier.weight(0.75f).padding(10.dp),
key = idx to critIdx
criteria, critIdx, { critIdx = it },
when {
studentIdx == 0 && critIdx == 0 -> feedback.global
studentIdx == 0 && critIdx != 0 -> feedback.byCriterion[critIdx - 1].entry
studentIdx != 0 && critIdx == 0 -> individual[studentIdx - 1].second.second.global
else -> individual[studentIdx - 1].second.second.byCriterion[critIdx - 1].entry
},
suggestions, onSave, Modifier.weight(0.75f).padding(10.dp),
key = studentIdx to critIdx
)
}
}
@ -267,25 +237,27 @@ fun groupFeedbackPane(
criteria: List<GroupAssignmentCriterion>,
currentCriterion: Int,
onSelectCriterion: (Int) -> Unit,
globFeedback: GroupAssignmentState.FeedbackEntry?,
criterionFeedback: GroupAssignmentState.FeedbackEntry?,
rawFeedback: GroupAssignmentState.FeedbackEntry?,
autofill: List<String>,
onSetGrade: (String) -> Unit,
onSetFeedback: (String) -> Unit,
onSave: (String, String) -> Unit,
modifier: Modifier = Modifier,
key: Any? = null
) {
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria, criterionFeedback, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
var grade by remember(rawFeedback, key) { mutableStateOf(rawFeedback?.grade ?: "") }
val feedback = rememberRichTextState()
LaunchedEffect(currentCriterion, criteria, rawFeedback, key) {
feedback.setMarkdown(rawFeedback?.feedback ?: "")
}
Column(modifier) {
Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button(
{ onSetGrade(grade); onSetFeedback(feedback.text) },
Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
{ onSave(grade, feedback.toMarkdown()) },
Modifier.weight(0.2f).align(Alignment.CenterVertically)
) {
Text("Save")
}
@ -297,11 +269,7 @@ fun groupFeedbackPane(
}
}
Spacer(Modifier.height(5.dp))
AutocompleteLineField(
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter ->
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
}
RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
}
}
@ -480,16 +448,19 @@ fun soloFeedbackPane(
key: Any? = null
) {
var grade by remember(globFeedback, key) { mutableStateOf(globFeedback?.grade ?: "") }
var feedback by remember(currentCriterion, criteria, key) { mutableStateOf(TextFieldValue(criterionFeedback?.feedback ?: "")) }
val feedback = rememberRichTextState()
LaunchedEffect(currentCriterion, criteria, criterionFeedback, key) {
feedback.setMarkdown(criterionFeedback?.feedback ?: "")
}
Column(modifier) {
Row {
Text("Overall grade: ", Modifier.align(Alignment.CenterVertically))
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
Spacer(Modifier.weight(0.6f))
Button(
{ onSetGrade(grade); onSetFeedback(feedback.text) },
Modifier.weight(0.2f).align(Alignment.CenterVertically),
enabled = grade.isNotBlank() || feedback.text.isNotBlank()
{ onSetGrade(grade); onSetFeedback(feedback.toMarkdown()) },
Modifier.weight(0.2f).align(Alignment.CenterVertically)
) {
Text("Save")
}
@ -501,11 +472,7 @@ fun soloFeedbackPane(
}
}
Spacer(Modifier.height(5.dp))
AutocompleteLineField(
feedback, { feedback = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
) { filter ->
autofill.filter { x -> x.trim().startsWith(filter.trim()) }
}
RichTextField(feedback, Modifier.fillMaxWidth().weight(1f)) { Text("Feedback") }
}
}

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
import com.jaytux.grader.loadClipboard
import com.jaytux.grader.toClipboard
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.ui.material.OutlinedRichTextEditor
@Composable
fun RichTextStyleRow(
@ -238,3 +239,17 @@ fun RichTextStyleButton(
)
}
}
@Composable
fun RichTextField(
state: RichTextState,
modifier: Modifier = Modifier,
buttonsModifier: Modifier = Modifier,
outerModifier: Modifier = Modifier,
label: @Composable (() -> Unit)? = null
) = Column(outerModifier) {
RichTextStyleRow(buttonsModifier, state)
OutlinedRichTextEditor(
state = state, modifier = modifier, singleLine = false, minLines = 5, label = label
)
}

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.window.*
import com.jaytux.grader.data.Course
import com.jaytux.grader.data.Edition
import com.jaytux.grader.viewmodel.PeerEvaluationState
import com.mohamedrejeb.richeditor.model.RichTextState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.*
@ -211,7 +212,8 @@ fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AutocompleteLineField(
fun AutocompleteLineField__(
// state: RichTextState,
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
onFilter: (String) -> List<String>

View File

@ -171,10 +171,14 @@ class EditionState(val edition: Edition) {
fun newSoloAssignment(name: String) {
transaction {
SoloAssignment.new {
val assign = SoloAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx()
}
val global = SoloAssignmentCriterion.new {
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
}
assign.globalCriterion = global
solo.refresh()
}
}
@ -186,10 +190,14 @@ class EditionState(val edition: Edition) {
}
fun newGroupAssignment(name: String) {
transaction {
GroupAssignment.new {
val assign = GroupAssignment.new {
this.name = name; this.edition = this@EditionState.edition; assignment = ""; deadline = now()
this.number = nextIdx()
}
val global = GroupAssignmentCriterion.new {
this.name = "_global"; this.description = "[Global] Meta-criterion for $name"; this.assignment = assign
}
assign.globalCriterion = global
groupAs.refresh()
}
}
@ -373,12 +381,12 @@ class StudentState(val student: Student, edition: Edition) {
val asGroup = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin GroupFeedbacks innerJoin Groups).selectAll().where {
(GroupFeedbacks.groupId inList groupsForEdition.keys.toList()) and
(GroupAssignmentCriteria.name eq "")
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
}.map { it[GroupAssignments.id] to it }
val asIndividual = (GroupAssignments innerJoin GroupAssignmentCriteria innerJoin IndividualFeedbacks innerJoin Groups).selectAll().where {
(IndividualFeedbacks.studentId eq student.id) and
(GroupAssignmentCriteria.name eq "")
(GroupAssignmentCriteria.id eq GroupAssignments.globalCriterion)
}.map { it[GroupAssignments.id] to it }
val res = mutableMapOf<EntityID<UUID>, LocalGroupGrade>()
@ -475,7 +483,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState {
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).toList()
assignment.criteria.orderBy(GroupAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
}
val feedback = RawDbState { loadFeedback() }
@ -494,7 +502,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
val allCrit = GroupAssignmentCriterion.find {
GroupAssignmentCriteria.assignmentId eq assignment.id
}
}//.filter { it.id != assignment.globalCriterion.id }
return Group.find {
(Groups.editionId eq assignment.edition.id)
@ -502,16 +510,19 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val forGroup = (GroupFeedbacks innerJoin Groups).selectAll().where {
(GroupFeedbacks.assignmentId eq assignment.id) and (Groups.id eq group.id)
}.map { row ->
val crit = row[GroupFeedbacks.criterionId]?.let { GroupAssignmentCriterion[it] }
val crit = GroupAssignmentCriterion[row[GroupFeedbacks.criterionId]]
val fdbk = row[GroupFeedbacks.feedback]
val grade = row[GroupFeedbacks.grade]
crit to FeedbackEntry(fdbk, grade)
}
val global = forGroup.firstOrNull { it.first == null }?.second
val byCrit_ = forGroup.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
.filterNotNull().associateBy { it.criterion.id }
val global = forGroup.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
val byCrit_ = forGroup
.filter{ it.first.id != assignment.globalCriterion.id }
.map { LocalCriterionFeedback(it.first, it.second) }
.associateBy { it.criterion.id }
val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
}
@ -522,21 +533,25 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
val student = it.student
val role = it.role
val forSt = (IndividualFeedbacks innerJoin Groups innerJoin GroupStudents)
val forSt = (IndividualFeedbacks innerJoin Groups)
.selectAll().where {
(IndividualFeedbacks.assignmentId eq assignment.id) and
(GroupStudents.studentId eq student.id) and (Groups.id eq group.id)
(IndividualFeedbacks.studentId eq student.id) and (Groups.id eq group.id)
}.map { row ->
val crit = row[IndividualFeedbacks.criterionId]?.let { id -> GroupAssignmentCriterion[id] }
val stdId = row[IndividualFeedbacks.studentId]
val crit = GroupAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
val fdbk = row[IndividualFeedbacks.feedback]
val grade = row[IndividualFeedbacks.grade]
crit to FeedbackEntry(fdbk, grade)
}
val global = forSt.firstOrNull { it.first == null }?.second
val byCrit_ = forSt.map { it.first?.let { k -> LocalCriterionFeedback(k, it.second) } }
.filterNotNull().associateBy { it.criterion.id }
val global = forSt.firstOrNull { it.first.id == assignment.globalCriterion.id }?.second
val byCrit_ = forSt
.filter { it.first != assignment.globalCriterion.id }
.map { LocalCriterionFeedback(it.first, it.second) }
.associateBy { it.criterion.id }
val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: LocalCriterionFeedback(c, null)
}
@ -556,7 +571,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[groupId] = group.id
it[this.feedback] = msg
it[this.grade] = grd
it[criterionId] = criterion?.id
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
}
}
feedback.refresh(); autofill.refresh()
@ -570,7 +585,7 @@ class GroupAssignmentState(val assignment: GroupAssignment) {
it[studentId] = student.id
it[this.feedback] = msg
it[this.grade] = grd
it[criterionId] = criterion?.id
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
}
}
feedback.refresh(); autofill.refresh()
@ -628,7 +643,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
private val _deadline = mutableStateOf(assignment.deadline); val deadline = _deadline.immutable()
val criteria = RawDbState {
assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).toList()
assignment.criteria.orderBy(SoloAssignmentCriteria.name to SortOrder.ASC).filter { it.id != assignment.globalCriterion.id }
}
val feedback = RawDbState { loadFeedback() }
@ -638,25 +653,28 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
}.flatten().distinct().sorted()
}
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {
private fun Transaction.loadFeedback(): List<Pair<Student, FullFeedback>> {3
val allCrit = SoloAssignmentCriterion.find {
SoloAssignmentCriteria.assignmentId eq assignment.id
}
}.filter { it.id != assignment.globalCriterion.id }
return editionCourse.second.soloStudents.sortAsc(Students.name).map { student ->
val forStudent = (IndividualFeedbacks innerJoin Students).selectAll().where {
(IndividualFeedbacks.assignmentId eq assignment.id) and (Students.id eq student.id)
}.map { row ->
val crit = row[IndividualFeedbacks.criterionId]?.let { SoloAssignmentCriterion[it] }
val crit = SoloAssignmentCriterion[row[IndividualFeedbacks.criterionId]]
val fdbk = row[IndividualFeedbacks.feedback]
val grade = row[IndividualFeedbacks.grade]
crit to LocalFeedback(fdbk, grade)
}
val global = forStudent.firstOrNull { it.first == null }?.second
val byCrit_ = forStudent.map { it.first?.let { k -> Pair(k, it.second) } }
.filterNotNull().associateBy { it.first.id }
val global = forStudent.firstOrNull { it.first == assignment.globalCriterion.id }?.second
val byCrit_ = forStudent
.filter { it.first != assignment.globalCriterion.id }
.map { Pair(it.first, it.second) }
.associateBy { it.first.id }
val byCrit = allCrit.map { c ->
byCrit_[c.id] ?: Pair(c, null)
}
@ -672,7 +690,7 @@ class SoloAssignmentState(val assignment: SoloAssignment) {
it[studentId] = student.id
it[this.feedback] = msg ?: ""
it[this.grade] = grd ?: ""
it[criterionId] = criterion?.id
it[criterionId] = criterion?.id ?: assignment.globalCriterion.id
}
}
feedback.refresh(); autofill.refresh()

View File

@ -20,6 +20,7 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
exposed-dao = { group = "org.jetbrains.exposed", name = "exposed-dao", version.ref = "exposed" }
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
exposed-migration = { group = "org.jetbrains.exposed", name = "exposed-migration", version.ref = "exposed" }
exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime", version.ref = "exposed" }
sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }