diff --git a/portal.toml b/portal.toml index b19f50c..d765b43 100644 --- a/portal.toml +++ b/portal.toml @@ -9,6 +9,10 @@ reference = "2019-06-12" offset = 7200000 wall_start = 1 +[reso] +day = 3 +time = 900 + [general] wiki_url = "https://wiki.kif.rocks/w/index.php?title=KIF470:Arbeitskreise&action=raw" diff --git a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt index a6d7cab..f7722fb 100644 --- a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt +++ b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt @@ -1,6 +1,7 @@ package de.kif.common import de.kif.common.model.ConstraintType +import de.kif.common.model.Room import de.kif.common.model.Schedule import kotlinx.serialization.Serializable @@ -16,10 +17,15 @@ data class ConstraintMap( fun checkConstraints( check: List, - against: List + against: List, + rooms: List, + resoDay: Int, + resoTime: Int ): ConstraintMap { val map = mutableMapOf>() + val roomMap = rooms.associateBy { it.id } + for (schedule in check) { if (schedule.id == null) continue val errors = mutableListOf() @@ -46,7 +52,7 @@ fun checkConstraints( schedule.time, schedule.time + schedule.workGroup.length ) - }.any {it} + }.any { it } if (blocked) { errors += ConstraintError("The room ${schedule.room.name} is blocked!") } @@ -54,10 +60,20 @@ fun checkConstraints( val start = schedule.getAbsoluteStartTime() val end = schedule.getAbsoluteEndTime() + + if (schedule.workGroup.resolution) { + val resoDeadline = resoDay * 24 * 60 + resoTime + + if (end > resoDeadline) { + errors += ConstraintError("The work group is ${end - resoDeadline} minutes after resolution deadline") + } + } + for (leader in schedule.workGroup.leader) { for (s in against) { if ( schedule != s && + schedule.day == s.day && leader in s.workGroup.leader && start < s.getAbsoluteEndTime() && s.getAbsoluteStartTime() < end @@ -70,6 +86,7 @@ fun checkConstraints( for (s in against) { if ( schedule != s && + schedule.day == s.day && schedule.room.id == s.room.id && start < s.getAbsoluteEndTime() && s.getAbsoluteStartTime() < end @@ -146,6 +163,7 @@ fun checkConstraints( for (s in against) { if ( s.workGroup.id == constraint.workGroup && + schedule.day == s.day && start <= s.getAbsoluteEndTime() && s.getAbsoluteStartTime() <= end ) { @@ -159,14 +177,25 @@ fun checkConstraints( for (constraint in constraints) { for (s in against) { if ( - s.workGroup.id == constraint.workGroup && - s.getAbsoluteEndTime() > start + s.workGroup.id == constraint.workGroup && ( + s.day > schedule.day || + s.day == schedule.day && s.getAbsoluteEndTime() > start + ) ) { errors += ConstraintError("Work group requires after ${s.workGroup.name}!") } } } } + ConstraintType.Room -> { + val roomBools = constraints.map { it.room == schedule.room.id } + if (roomBools.none { it }) { + val roomList = constraints.mapNotNull { it.room }.distinct().sorted().map { + "${(roomMap[it]?.name ?: "")}($it)" + } + errors += ConstraintError("Work group requires rooms $roomList, but is in room ${schedule.room.name}(${schedule.room.id})") + } + } } } diff --git a/src/commonMain/kotlin/de/kif/common/model/WorkGroupConstraint.kt b/src/commonMain/kotlin/de/kif/common/model/WorkGroupConstraint.kt index 79531b7..0186d8a 100644 --- a/src/commonMain/kotlin/de/kif/common/model/WorkGroupConstraint.kt +++ b/src/commonMain/kotlin/de/kif/common/model/WorkGroupConstraint.kt @@ -7,43 +7,49 @@ data class WorkGroupConstraint( val type: ConstraintType, val day: Int? = null, val time: Int? = null, - val workGroup: Long? = null + val workGroup: Long? = null, + val room: Long? = null ) enum class ConstraintType { /** - * Requires day, permits time and workGroup. + * Requires day, permits time, workGroup and room. */ OnlyOnDay, /** - * Requires day, permits time and workGroup. + * Requires day, permits time, workGroup and room. */ NotOnDay, /** - * Requires time, optionally allows day, permits workGroup. + * Requires time, optionally allows day, permits workGroup and room. */ OnlyAfterTime, /** - * Requires time, optionally allows day, permits workGroup. + * Requires time, optionally allows day, permits workGroup and room. */ OnlyBeforeTime, /** - * Requires time, optionally allows day, permits workGroup + * Requires time, optionally allows day, permits workGroup and room. */ ExactTime, /** - * Requires workGroup, permits day and time. + * Requires workGroup, permits day, time and room. */ NotAtSameTime, /** - * Requires workGroup, permits day and time. + * Requires workGroup, permits day, time and room. */ - OnlyAfterWorkGroup + OnlyAfterWorkGroup, + + /** + * Requires room, permits day, time and workGroup + */ + Room } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt index 579cf7e..1641bd1 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt @@ -1,6 +1,7 @@ package de.kif.frontend.views import de.kif.frontend.launch +import de.kif.frontend.repository.RoomRepository import de.kif.frontend.repository.WorkGroupRepository import de.westermann.kobserve.event.EventListener import de.westermann.kwebview.View @@ -10,6 +11,7 @@ import de.westermann.kwebview.createHtmlView import de.westermann.kwebview.iterator import org.w3c.dom.* import kotlin.browser.document +import kotlin.dom.clear fun initWorkGroupConstraints() { var index = 10000 @@ -149,10 +151,15 @@ fun initWorkGroupConstraints() { launch { val all = WorkGroupRepository.all() + val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value + for (wg in all) { val option = createHtmlView() option.value = wg.id.toString() option.textContent = wg.name + if (option.value == id) { + option.selected = true + } select.appendChild(option) } } @@ -177,10 +184,49 @@ fun initWorkGroupConstraints() { launch { val all = WorkGroupRepository.all() + val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value + for (wg in all) { val option = createHtmlView() option.value = wg.id.toString() option.textContent = wg.name + if (option.value == id) { + option.selected = true + } + select.appendChild(option) + } + } + + html.appendChild(select) + }.html) + } + } + addList.textView("In Raum x") { + onClick { + constraints.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("Raum").apply { + classList += "form-btn" + onClick { this@wrap.html.remove() } + }.html) + + val select = createHtmlView() + select.classList.add("form-control") + select.name = "constraint-room-${index++}" + + val id = (select.options[select.selectedIndex] as? HTMLOptionElement)?.value + + launch { + val all = RoomRepository.all() + select.clear() + + for (room in all) { + val option = createHtmlView() + option.value = room.id.toString() + option.textContent = room.name + if (option.value == id) { + option.selected = true + } select.appendChild(option) } } @@ -195,7 +241,6 @@ fun initWorkGroupConstraints() { val span = child.firstElementChild as HTMLElement span.addEventListener("click", org.w3c.dom.events.EventListener { - println("click") child.remove() }) } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt index f523b81..2b7f57f 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/TableLayout.kt @@ -1,5 +1,7 @@ package de.kif.frontend.views.table +import de.kif.frontend.launch +import de.kif.frontend.repository.TrackRepository import de.westermann.kwebview.components.InputView import de.westermann.kwebview.iterator import org.w3c.dom.HTMLFormElement @@ -14,21 +16,25 @@ fun initTableLayout() { val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement - val list = table.getElementsByTagName("tr").iterator().asSequence().filter { - it.dataset["search"] != null - }.map { - when (it.dataset["edit"]) { - "workgroup" -> WorkGroupTableLine(it) - "room" -> RoomTableLine(it) - else -> TableLine(it) - } - }.toList() + launch { + val tracks = TrackRepository.all() - val input = form.getElementsByTagName("input")[0] as HTMLInputElement - val search = InputView.wrap(input) - search.valueProperty.onChange { - for (row in list) { - row.search(search.value) + val list = table.getElementsByTagName("tr").iterator().asSequence().filter { + it.dataset["search"] != null + }.map { + when (it.dataset["edit"]) { + "workgroup" -> WorkGroupTableLine(it, tracks) + "room" -> RoomTableLine(it) + else -> TableLine(it) + } + }.toList() + + val input = form.getElementsByTagName("input")[0] as HTMLInputElement + val search = InputView.wrap(input) + search.valueProperty.onChange { + for (row in list) { + row.search(search.value) + } } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt index 80aa599..8e82fe9 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt @@ -13,7 +13,7 @@ import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLSpanElement import org.w3c.dom.get -class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { +class WorkGroupTableLine(view: HTMLElement, tracks: List) : TableLine(view) { private var lineId = dataset["id"]?.toLongOrNull() ?: -1 @@ -24,9 +24,7 @@ class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { private val spanWorkGroupLength: TextView private val spanWorkGroupInterested: TextView private val spanWorkGroupTrack: TextView - private val spanWorkGroupProjector: TextView private val spanWorkGroupResolution: TextView - private val spanWorkGroupLanguage: TextView override var searchElement: SearchElement = super.searchElement @@ -41,12 +39,8 @@ class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-interested" } as HTMLSpanElement) spanWorkGroupTrack = TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-track" } as HTMLSpanElement) - spanWorkGroupProjector = - TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-projector" } as HTMLSpanElement) spanWorkGroupResolution = TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-resolution" } as HTMLSpanElement) - spanWorkGroupLanguage = - TextView.wrap(spans.first { it.dataset["editType"] == "workgroup-language" } as HTMLSpanElement) setupEditable(spanWorkGroupName) { launch { @@ -77,13 +71,6 @@ class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { } } - setupBoolean(spanWorkGroupProjector) { - launch { - val wg = workGroup.get() - WorkGroupRepository.update(wg.copy(projector = !wg.projector)) - } - } - setupBoolean(spanWorkGroupResolution) { launch { val wg = workGroup.get() @@ -91,19 +78,10 @@ class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { } } - setupList(spanWorkGroupLanguage, Language.values().sortedBy { it.localeName }, { it.localeName }) { - if (it == null) return@setupList - launch { - val wg = workGroup.get() - if (wg.language == it) return@launch - WorkGroupRepository.update(wg.copy(language = it)) - } - } - launch { - val tracks = listOf(null) + TrackRepository.all() + val list = listOf(null) + tracks - setupList(spanWorkGroupTrack, tracks, { it.name }) { + setupList(spanWorkGroupTrack, list, { it.name }) { launch x@{ val wg = workGroup.get() if (wg.track == it) return@x @@ -124,9 +102,7 @@ class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { spanWorkGroupLength.text = wg.length.toString() spanWorkGroupInterested.text = wg.interested.toString() spanWorkGroupTrack.text = wg.track?.name ?: "" - spanWorkGroupProjector.text = wg.projector.toString() spanWorkGroupResolution.text = wg.resolution.toString() - spanWorkGroupLanguage.text = wg.language.localeName } } } diff --git a/src/jsMain/resources/style/components/_board.scss b/src/jsMain/resources/style/components/_board.scss index 678b2bf..08e3eb7 100644 --- a/src/jsMain/resources/style/components/_board.scss +++ b/src/jsMain/resources/style/components/_board.scss @@ -85,6 +85,7 @@ .board-running { display: flex; flex-wrap: wrap; + align-content: flex-start; &:empty + .board-running-empty { display: block; diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index f0ae513..0d05e06 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -3,6 +3,7 @@ package de.kif.backend import com.fasterxml.jackson.databind.SerializationFeature import de.kif.backend.route.* import de.kif.backend.route.api.* +import de.kif.backend.util.Backup import de.kif.backend.util.pushService import io.ktor.application.Application import io.ktor.application.call @@ -101,4 +102,6 @@ fun Application.main() { } logger.info { "Responding at http://${Configuration.Server.host}:${Configuration.Server.port}$prefix/" } + + Backup.startBackupService() } diff --git a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt index 31943fe..038f8b1 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt @@ -43,6 +43,7 @@ object Configuration { val uploads by required() val database by required() val announcement by required() + val backup by required() } object Path { @@ -60,6 +61,9 @@ object Configuration { val announcement by c(PathSpec.announcement) val announcementPath: java.nio.file.Path by lazy { Paths.get(announcement).toAbsolutePath() } + + val backup by c(PathSpec.backup) + val backupPath: java.nio.file.Path by lazy { Paths.get(backup).toAbsolutePath() } } private object ScheduleSpec : ConfigSpec("schedule") { @@ -93,6 +97,7 @@ object Configuration { private object GeneralSpec : ConfigSpec("general") { val allowedUploadExtensions by required("allowed_upload_extensions") val wikiUrl by required("wiki_url") + val backupInterval by required("backup_interval") } object General { @@ -101,6 +106,7 @@ object Configuration { allowedUploadExtensions.split(",").map { it.trim().toLowerCase() }.toSet() } val wikiUrl by c(GeneralSpec.wikiUrl) + val backupInterval by c(GeneralSpec.backupInterval) } private object TwitterSpec : ConfigSpec("twitter") { @@ -111,6 +117,16 @@ object Configuration { val timeline by c(TwitterSpec.timeline) } + private object ResoSpec : ConfigSpec("reso") { + val day by required() + val time by required() + } + + object Reso { + val day by c(ResoSpec.day) + val time by c(ResoSpec.time) + } + init { var config = Config { addSpec(ServerSpec) @@ -119,6 +135,7 @@ object Configuration { addSpec(SecuritySpec) addSpec(GeneralSpec) addSpec(TwitterSpec) + addSpec(ResoSpec) }.from.toml.resource("portal.toml") for (file in Files.list(Paths.get("."))) { diff --git a/src/jvmMain/kotlin/de/kif/backend/Resources.kt b/src/jvmMain/kotlin/de/kif/backend/Resources.kt index 0e4dc63..07d4a8e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Resources.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Resources.kt @@ -83,11 +83,19 @@ object Resources { Files.createDirectories(Configuration.Path.sessionsPath) Files.createDirectories(Configuration.Path.uploadsPath) Files.createDirectories(Configuration.Path.webPath) + Files.createDirectories(Configuration.Path.backupPath) + + if (!Files.exists(Configuration.Path.announcementPath)) { + Files.createFile(Configuration.Path.announcementPath) + } logger.info { "Database path: ${Configuration.Path.databasePath}" } logger.info { "Sessions path: ${Configuration.Path.sessionsPath}" } logger.info { "Uploads path: ${Configuration.Path.uploadsPath}" } logger.info { "Web path: ${Configuration.Path.webPath}" } + logger.info { "Backup path: ${Configuration.Path.backupPath}" } + + logger.info { "Announcement file: ${Configuration.Path.announcementPath}" } logger.info { "Extract web content..." } extractWeb() diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt index f0a93c8..b383637 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt @@ -60,7 +60,7 @@ fun Route.board() { val list = ScheduleRepository.getByDay(day) val rooms = RoomRepository.all() val schedules = list.groupBy { it.room }.mapValues { (_, it) -> - it.associateBy { + it.groupBy { it.time } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index 7748045..a163022 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -70,7 +70,7 @@ fun DIV.renderCalendar( from: Int, to: Int, rooms: List, - schedules: Map> + schedules: Map>> ) { val gridLabelWidth = 60 val minutesOfDay = to - from @@ -144,9 +144,9 @@ fun DIV.renderCalendar( title = room.name + " - " + timeString - val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull() + val cellSchedules = (start..end).flatMap { schedules[room]?.get(it) ?: emptyList() } - if (schedule != null) { + for(schedule in cellSchedules) { calendarEntry(schedule, diff, currentTime) } } @@ -231,7 +231,7 @@ fun Route.calendar() { val list = ScheduleRepository.getByDay(day) val schedules = list.groupBy { it.room }.mapValues { (_, it) -> - it.associateBy { + it.groupBy { it.time } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt b/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt index 70d46ba..25c48e0 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Wall.kt @@ -17,7 +17,7 @@ import kotlin.math.min data class WallData( val number: Int, - val schedules: Map>, + val schedules: Map>>, val max: Int?, val min: Int? ) @@ -26,14 +26,13 @@ suspend fun genWallData(day: Int): WallData { val list = ScheduleRepository.getByDay(day) val rooms = RoomRepository.all() - if (list.isEmpty()) return WallData(day, rooms.associateWith { emptyMap() }, null, null) + if (list.isEmpty()) return WallData(day, rooms.associateWith { emptyMap>() }, null, null) - val schedules = - rooms.associateWith { emptyMap() } + list.groupBy { it.room }.mapValues { (_, it) -> - it.associateBy { - it.time - } + val schedules = list.groupBy { it.room }.mapValues { (_, it) -> + it.groupBy { + it.time } + } var max = 0 var min = 24 * 60 diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 0947e86..c4e49c4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -2,6 +2,7 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.prefix +import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.view.TableTemplate @@ -59,15 +60,9 @@ fun Route.workGroup() { th { +"Track" } - th { - +"Beamer" - } th { +"Resolution" } - th { - +"Sprache" - } th(classes = "action") { +"Aktion" } @@ -111,13 +106,6 @@ fun Route.workGroup() { +(u.track?.name ?: "") } } - td { - span { - attributes["data-edit-type"] = "workgroup-projector" - - +u.projector.toString() - } - } td { span { attributes["data-edit-type"] = "workgroup-resolution" @@ -125,13 +113,6 @@ fun Route.workGroup() { +u.resolution.toString() } } - td { - span { - attributes["data-edit-type"] = "workgroup-language" - - +u.language.localeName - } - } td(classes = "action") { a("$prefix/workgroup/${u.id}") { i("material-icons") { +"edit" } @@ -157,6 +138,12 @@ fun Route.workGroup() { WorkGroupRepository.get(it)!! } + val rooms = editWorkGroup.constraints.mapNotNull { + it.room + }.distinct().associateWith { + RoomRepository.get(it)!! + } + respondMain { content { h1 { +"Arbeitskreis bearbeiten" } @@ -541,6 +528,22 @@ fun Route.workGroup() { } } } + ConstraintType.Room -> { + span("form-btn") { + +"Raum" + } + select( + classes = "form-control" + ) { + name = "constraint-room-$index" + + option { + selected = true + value = constraint.room.toString() + +(rooms[constraint.room!!]?.name ?: "") + } + } + } } } @@ -985,6 +988,9 @@ private fun parseConstraintParam(params: Map) = params.map { (k key.startsWith("constraint-only-after-work-group") -> { value?.toLongOrNull()?.let { WorkGroupConstraint(ConstraintType.OnlyAfterWorkGroup, workGroup = it) } } + key.startsWith("constraint-room") -> { + value?.toLongOrNull()?.let { WorkGroupConstraint(ConstraintType.Room, room = it) } + } else -> null } }.groupBy({ it.first }) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt index 000db2e..a5a03f8 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt @@ -1,6 +1,8 @@ package de.kif.backend.route.api +import de.kif.backend.Configuration import de.kif.backend.authenticate +import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.ScheduleRepository import de.kif.common.checkConstraints import de.kif.common.model.Permission @@ -14,14 +16,22 @@ fun Route.constraintsApi() { try { authenticate(Permission.SCHEDULE) { val schedules = ScheduleRepository.all() + val rooms = RoomRepository.all() - val errors = checkConstraints(schedules, schedules) + val errors = checkConstraints( + schedules, + schedules, + rooms, + Configuration.Reso.day, + Configuration.Reso.time + ) call.success(errors) } onFailure { call.error(HttpStatusCode.Unauthorized) } - } catch (_: Exception) { + } catch (e: Exception) { + e.printStackTrace() call.error(HttpStatusCode.InternalServerError) } } @@ -31,10 +41,17 @@ fun Route.constraintsApi() { authenticate(Permission.SCHEDULE) { val id = call.parameters["id"]?.toLongOrNull() val schedules = ScheduleRepository.all() + val rooms = RoomRepository.all() val check = schedules.filter { it.workGroup.id == id } - val errors = checkConstraints(check, schedules) + val errors = checkConstraints( + check, + schedules, + rooms, + Configuration.Reso.day, + Configuration.Reso.time + ) call.success(errors) } onFailure { diff --git a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt index ae33de4..d6ee823 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt @@ -1,11 +1,18 @@ package de.kif.backend.util +import de.kif.backend.Configuration import de.kif.backend.database.Connection import de.kif.backend.repository.* import de.kif.common.RepositoryType import de.kif.common.Serialization import de.kif.common.model.* +import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable +import mu.KotlinLogging +import java.lang.Exception +import java.text.SimpleDateFormat +import java.util.* +import kotlin.concurrent.thread @Serializable data class Backup( @@ -17,6 +24,8 @@ data class Backup( val workGroups: List = emptyList() ) { companion object { + private val logger = KotlinLogging.logger {} + suspend fun backup(vararg repositories: RepositoryType): String { var backup = Backup() @@ -78,7 +87,9 @@ data class Backup( var workGroup = it val track = workGroup.track if (track != null) { - workGroup = workGroup.copy(track = track.copy(id = trackMap.firstOrNull { (i,_) -> i.equalsIgnoreId(track) }?.second ?: run { + workGroup = workGroup.copy(track = track.copy(id = trackMap.firstOrNull { (i, _) -> + i.equalsIgnoreId(track) + }?.second ?: run { println("Cannot import work group, due to missing track") return@mapNotNull null })) @@ -94,14 +105,16 @@ data class Backup( newSchedules.forEach { ScheduleRepository.create( it.copy( - room = it.room.copy(id = roomMap.firstOrNull { (i,_) -> i.equalsIgnoreId(it.room) }?.second ?: run { - println("Cannot import schedule, due to missing room") - return@forEach - }), - workGroup = it.workGroup.copy(id = workGroupMap.firstOrNull { (i,_) -> i.equalsIgnoreId(it.workGroup) }?.second ?: run { - println("Cannot import schedule, due to missing work group") - return@forEach - }) + room = it.room.copy( + id = roomMap.firstOrNull { (i, _) -> i.equalsIgnoreId(it.room) }?.second ?: run { + println("Cannot import schedule, due to missing room") + return@forEach + }), + workGroup = it.workGroup.copy( + id = workGroupMap.firstOrNull { (i, _) -> i.equalsIgnoreId(it.workGroup) }?.second ?: run { + println("Cannot import schedule, due to missing work group") + return@forEach + }) ) ) @@ -114,5 +127,39 @@ data class Backup( import(data) } + + fun startBackupService() { + val backupPath = Configuration.Path.backupPath.toFile() + val backupInterval = Configuration.General.backupInterval + + val formatter = SimpleDateFormat("yyyyMMdd'T'HHmmss") + + thread( + start = true, + isDaemon = true, + name = "backup-service" + ) { + while (true) { + try { + Thread.sleep(backupInterval) + + if (!backupPath.exists()) { + backupPath.mkdirs() + } + + val date = formatter.format(Date()) + + val backupFile = backupPath.resolve("backup_$date.json") + + val backup = runBlocking { + backup(*RepositoryType.values()) + } + backupFile.writeText(backup) + } catch (e: Exception) { + logger.error(e) { "Cannot create backup" } + } + } + } + } } } diff --git a/src/jvmMain/resources/portal.toml b/src/jvmMain/resources/portal.toml index 4b271c7..809ffa1 100644 --- a/src/jvmMain/resources/portal.toml +++ b/src/jvmMain/resources/portal.toml @@ -10,12 +10,17 @@ sessions = "data/sessions" uploads = "data/uploads" database = "data/portal.db" announcement = "data/announcement.txt" +backup = "data/backup" [schedule] reference = "1970-01-01" offset = 0 wall_start = 0 +[reso] +day = 0 +time = 0 + [security] session_name = "SESSION" sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc" @@ -23,6 +28,7 @@ sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d [general] allowed_upload_extensions = "png, jpg, jpeg" wiki_url = "" +backup_interval = 3600000 [twitter] timeline = ""