From 94c35b90674483aef2267a48a39b8fbe286810ce Mon Sep 17 00:00:00 2001
From: Lars Westermann <lars-westermann@live.de>
Date: Wed, 12 Jun 2019 19:04:44 +0200
Subject: [PATCH 1/2] Add reso check and room list

---
 portal.toml                                   |  4 ++
 .../de/kif/common/ConstraintChecking.kt       | 37 +++++++++++++--
 .../kif/common/model/WorkGroupConstraint.kt   | 24 ++++++----
 .../frontend/views/WorkGroupConstraints.kt    | 47 ++++++++++++++++++-
 .../kif/frontend/views/table/TableLayout.kt   | 34 ++++++++------
 .../views/table/WorkGroupTableLine.kt         | 30 ++----------
 .../kotlin/de/kif/backend/Configuration.kt    | 11 +++++
 .../kotlin/de/kif/backend/route/Board.kt      |  2 +-
 .../kotlin/de/kif/backend/route/Calendar.kt   |  8 ++--
 .../kotlin/de/kif/backend/route/Wall.kt       | 13 +++--
 .../kotlin/de/kif/backend/route/WorkGroup.kt  | 46 ++++++++++--------
 .../de/kif/backend/route/api/Constraints.kt   | 23 +++++++--
 src/jvmMain/resources/portal.toml             |  4 ++
 13 files changed, 193 insertions(+), 90 deletions(-)

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<Schedule>,
-    against: List<Schedule>
+    against: List<Schedule>,
+    rooms: List<Room>,
+    resoDay: Int,
+    resoTime: Int
 ): ConstraintMap {
     val map = mutableMapOf<Long, List<ConstraintError>>()
 
+    val roomMap = rooms.associateBy { it.id }
+
     for (schedule in check) {
         if (schedule.id == null) continue
         val errors = mutableListOf<ConstraintError>()
@@ -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<HTMLOptionElement>()
                         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<HTMLOptionElement>()
                         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<HTMLDivElement>()) {
+                classList += "input-group"
+                html.appendChild(TextView("Raum").apply {
+                    classList += "form-btn"
+                    onClick { this@wrap.html.remove() }
+                }.html)
+
+                val select = createHtmlView<HTMLSelectElement>()
+                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<HTMLOptionElement>()
+                        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<Track>) : 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<Track?>(null) + TrackRepository.all()
+            val list = listOf<Track?>(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/jvmMain/kotlin/de/kif/backend/Configuration.kt b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt
index 31943fe..688a46b 100644
--- a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt
+++ b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt
@@ -111,6 +111,16 @@ object Configuration {
         val timeline by c(TwitterSpec.timeline)
     }
 
+    private object ResoSpec : ConfigSpec("reso") {
+        val day by required<Int>()
+        val time by required<Int>()
+    }
+
+    object Reso {
+        val day by c(ResoSpec.day)
+        val time by c(ResoSpec.time)
+    }
+
     init {
         var config = Config {
             addSpec(ServerSpec)
@@ -119,6 +129,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/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<Room>,
-    schedules: Map<Room, Map<Int, Schedule>>
+    schedules: Map<Room, Map<Int, List<Schedule>>>
 ) {
     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<Room, Map<Int, Schedule>>,
+    val schedules: Map<Room, Map<Int, List<Schedule>>>,
     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<Int, Schedule>() }, null, null)
+    if (list.isEmpty()) return WallData(day, rooms.associateWith { emptyMap<Int, List<Schedule>>() }, null, null)
 
-    val schedules =
-        rooms.associateWith { emptyMap<Int, Schedule>() } + 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<String, String?>) = 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/resources/portal.toml b/src/jvmMain/resources/portal.toml
index 4b271c7..e92d2c3 100644
--- a/src/jvmMain/resources/portal.toml
+++ b/src/jvmMain/resources/portal.toml
@@ -16,6 +16,10 @@ 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"

From 07a619f826f665a51cd2a6e454201be10f487835 Mon Sep 17 00:00:00 2001
From: Lars Westermann <lars-westermann@live.de>
Date: Wed, 12 Jun 2019 20:54:21 +0200
Subject: [PATCH 2/2] Add backup

---
 .../resources/style/components/_board.scss    |  1 +
 .../kotlin/de/kif/backend/Application.kt      |  3 +
 .../kotlin/de/kif/backend/Configuration.kt    |  6 ++
 .../kotlin/de/kif/backend/Resources.kt        |  8 +++
 .../kotlin/de/kif/backend/util/Backup.kt      | 65 ++++++++++++++++---
 src/jvmMain/resources/portal.toml             |  2 +
 6 files changed, 76 insertions(+), 9 deletions(-)

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 688a46b..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<String>()
         val database by required<String>()
         val announcement by required<String>()
+        val backup by required<String>()
     }
 
     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<String>("allowed_upload_extensions")
         val wikiUrl by required<String>("wiki_url")
+        val backupInterval by required<Long>("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") {
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/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<WorkGroup> = 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 e92d2c3..809ffa1 100644
--- a/src/jvmMain/resources/portal.toml
+++ b/src/jvmMain/resources/portal.toml
@@ -10,6 +10,7 @@ sessions = "data/sessions"
 uploads = "data/uploads"
 database = "data/portal.db"
 announcement = "data/announcement.txt"
+backup = "data/backup"
 
 [schedule]
 reference = "1970-01-01"
@@ -27,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 = ""