From c28317aefdab35626f37492a09c0a1e6059e1f83 Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Fri, 24 May 2019 14:29:10 +0200 Subject: [PATCH] Add overview --- build.gradle | 6 +- gradle.properties | 2 +- .../kotlin/de/kif/common/CacheRepository.kt | 60 ++++ .../de/kif/common/ConstraintChecking.kt | 86 +++++ .../kotlin/de/kif/common/Message.kt | 2 +- .../kotlin/de/kif/common/model/Constraint.kt | 13 + .../kotlin/de/kif/common/model/Model.kt | 1 + .../kotlin/de/kif/common/model/Permission.kt | 2 +- .../kotlin/de/kif/common/model/Post.kt | 29 ++ .../kotlin/de/kif/common/model/Room.kt | 8 +- .../kotlin/de/kif/common/model/Schedule.kt | 5 +- .../kotlin/de/kif/common/model/Track.kt | 2 +- .../kotlin/de/kif/common/model/User.kt | 2 +- .../kotlin/de/kif/common/model/WorkGroup.kt | 9 +- src/jsMain/kotlin/de/kif/frontend/main.kt | 4 + .../frontend/repository/ScheduleRepository.kt | 11 + .../frontend/views/WorkGroupConstraints.kt | 118 +++++++ .../kif/frontend/views/calendar/Calendar.kt | 24 +- .../frontend/views/calendar/CalendarEntry.kt | 1 + .../kotlin/de/westermann/kwebview/View.kt | 2 +- src/jsMain/resources/style/style.scss | 122 ++++++++ .../kotlin/de/kif/backend/Application.kt | 5 +- src/jvmMain/kotlin/de/kif/backend/Main.kt | 2 + .../de/kif/backend/database/Connection.kt | 3 +- .../kotlin/de/kif/backend/database/Schema.kt | 22 +- .../kif/backend/repository/PostRepository.kt | 107 +++++++ .../kif/backend/repository/RoomRepository.kt | 14 +- .../backend/repository/WorkGroupRepository.kt | 34 +- .../kotlin/de/kif/backend/route/Calendar.kt | 13 +- .../kotlin/de/kif/backend/route/Dashboard.kt | 27 -- .../kotlin/de/kif/backend/route/Overview.kt | 281 +++++++++++++++++ .../kotlin/de/kif/backend/route/Room.kt | 147 ++++++++- .../kotlin/de/kif/backend/route/Track.kt | 1 - .../kotlin/de/kif/backend/route/User.kt | 1 - .../kotlin/de/kif/backend/route/WorkGroup.kt | 294 +++++++++++++++++- .../de/kif/backend/route/api/Constraints.kt | 47 +++ .../de/kif/backend/util/ParseMarkdown.kt | 60 ++++ .../kotlin/de/kif/backend/util/PushService.kt | 1 + .../de/kif/backend/view/MenuTemplate.kt | 8 +- 39 files changed, 1505 insertions(+), 71 deletions(-) create mode 100644 src/commonMain/kotlin/de/kif/common/CacheRepository.kt create mode 100644 src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/Constraint.kt create mode 100644 src/commonMain/kotlin/de/kif/common/model/Post.kt create mode 100644 src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/repository/PostRepository.kt delete mode 100644 src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/Overview.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt create mode 100644 src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt diff --git a/build.gradle b/build.gradle index e2f226c..4fa2035 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,8 @@ version "0.1.0" repositories { jcenter() - maven { url "http://dl.bintray.com/kotlin/ktor" } + maven { url "https://dl.bintray.com/kotlin/ktor" } + maven { url "https://dl.bintray.com/jetbrains/markdown" } maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" } mavenCentral() @@ -87,6 +88,9 @@ kotlin { implementation "de.westermann:KObserve-jvm:$observable_version" + //api 'org.jetbrains:markdown:0.1.28' + //implementation 'com.atlassian.commonmark:commonmark:0.12.1' + implementation 'com.vladsch.flexmark:flexmark-all:0.42.10' api 'io.github.microutils:kotlin-logging:1.6.23' api 'ch.qos.logback:logback-classic:1.2.3' api 'org.fusesource.jansi:jansi:1.8' diff --git a/gradle.properties b/gradle.properties index 29e08e8..7fc6f1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official diff --git a/src/commonMain/kotlin/de/kif/common/CacheRepository.kt b/src/commonMain/kotlin/de/kif/common/CacheRepository.kt new file mode 100644 index 0000000..f205dfe --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/CacheRepository.kt @@ -0,0 +1,60 @@ +package de.kif.common + +import de.kif.common.model.Model +import de.westermann.kobserve.event.EventHandler + + +class CacheRepository(val repository: Repository) : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + var cache: Map = emptyMap() + var cacheComplete: Boolean = false + + override suspend fun get(id: Long): T? { + return if (id in cache) { + cache[id] + } else { + val element = repository.get(id) + + if (element != null) { + cache = cache + (id to element) + } + + element + } + } + + override suspend fun create(model: T): Long { + return repository.create(model) + } + + override suspend fun update(model: T) { + val id = model.id + if (id != null) { + cache = cache - id + } + repository.update(model) + } + + override suspend fun delete(id: Long) { + cache = cache - id + repository.delete(id) + } + + override suspend fun all(): List { + if (cacheComplete) { + return cache.values.toList() + } else { + val all = repository.all() + + cache = all.associateBy { it.id!! } + cacheComplete = true + + return all + } + } + +} diff --git a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt new file mode 100644 index 0000000..de7761d --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt @@ -0,0 +1,86 @@ +package de.kif.common + +import de.kif.common.model.ConstraintType +import de.kif.common.model.Schedule +import kotlinx.serialization.Serializable + +@Serializable +data class ConstraintError( + val reason: String = "" +) + +@Serializable +data class ConstraintMap( + val map: List>> +) + +fun checkConstraints( + check: List, + against: List +): ConstraintMap { + val map = mutableMapOf>() + + for (schedule in check) { + if (schedule.id == null) continue + val errors = mutableListOf() + + if (schedule.workGroup.projector && !schedule.room.projector) { + errors += ConstraintError("Work group requires projector, but room does not have one!") + } + if (schedule.workGroup.internet && !schedule.room.internet) { + errors += ConstraintError("Work group requires internet, but room does not have one!") + } + if (schedule.workGroup.whiteboard && !schedule.room.whiteboard) { + errors += ConstraintError("Work group requires whiteboard, but room does not have one!") + } + if (schedule.workGroup.blackboard && !schedule.room.blackboard) { + errors += ConstraintError("Work group requires blackboard, but room does not have one!") + } + if (schedule.workGroup.accessible && !schedule.room.accessible) { + errors += ConstraintError("Work group requires accessible, but room does not have one!") + } + + for (constraint in schedule.workGroup.constraints) { + when (constraint.type) { + ConstraintType.OnlyOnDay -> { + if (constraint.number.toInt() != schedule.day) { + errors += ConstraintError("Work group requires day ${constraint.number}, but is on ${schedule.day}!") + } + } + ConstraintType.OnlyAfterTime -> { + if (constraint.number.toInt() > schedule.time) { + errors += ConstraintError("Work group requires time after ${constraint.number}, but is on ${schedule.time}!") + } + } + ConstraintType.NotAtSameTime -> { + val start = schedule.getAbsoluteStartTime() + val end = schedule.getAbsoluteEndTime() + for (s in against) { + if ( + s.workGroup.id == constraint.number && + start <= s.getAbsoluteEndTime() && + s.getAbsoluteStartTime() <= end + ) { + errors += ConstraintError("Work group requires not same time with ${s.workGroup.name}!") + } + } + } + ConstraintType.OnlyAfterWorkGroup -> { + val start = schedule.getAbsoluteStartTime() + for (s in against) { + if ( + s.workGroup.id == constraint.number && + s.getAbsoluteEndTime() > start + ) { + errors += ConstraintError("Work group requires after ${s.workGroup.name}!") + } + } + } + } + } + + map[schedule.id] = errors + } + + return ConstraintMap(map.map { it.toPair() }) +} diff --git a/src/commonMain/kotlin/de/kif/common/Message.kt b/src/commonMain/kotlin/de/kif/common/Message.kt index 5eb4276..e239280 100644 --- a/src/commonMain/kotlin/de/kif/common/Message.kt +++ b/src/commonMain/kotlin/de/kif/common/Message.kt @@ -31,5 +31,5 @@ enum class MessageType { } enum class RepositoryType { - ROOM, SCHEDULE, TRACK, USER, WORK_GROUP + ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST } diff --git a/src/commonMain/kotlin/de/kif/common/model/Constraint.kt b/src/commonMain/kotlin/de/kif/common/model/Constraint.kt new file mode 100644 index 0000000..2bb39f3 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Constraint.kt @@ -0,0 +1,13 @@ +package de.kif.common.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Constraint( + val type: ConstraintType, + val number: Long +) + +enum class ConstraintType { + OnlyOnDay, OnlyAfterTime, NotAtSameTime, OnlyAfterWorkGroup +} diff --git a/src/commonMain/kotlin/de/kif/common/model/Model.kt b/src/commonMain/kotlin/de/kif/common/model/Model.kt index 5a7d2e1..1e9e770 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Model.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Model.kt @@ -3,5 +3,6 @@ package de.kif.common.model import de.kif.common.SearchElement interface Model { + val id : Long? fun createSearch(): SearchElement } \ No newline at end of file diff --git a/src/commonMain/kotlin/de/kif/common/model/Permission.kt b/src/commonMain/kotlin/de/kif/common/model/Permission.kt index ac5cedd..593bde2 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Permission.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Permission.kt @@ -1,5 +1,5 @@ package de.kif.common.model enum class Permission { - USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN + USER, SCHEDULE, WORK_GROUP, ROOM, POST, ADMIN } diff --git a/src/commonMain/kotlin/de/kif/common/model/Post.kt b/src/commonMain/kotlin/de/kif/common/model/Post.kt new file mode 100644 index 0000000..1bf4904 --- /dev/null +++ b/src/commonMain/kotlin/de/kif/common/model/Post.kt @@ -0,0 +1,29 @@ +package de.kif.common.model + +import de.kif.common.SearchElement +import kotlin.random.Random + +data class Post( + override val id: Long? = null, + val name: String, + val content: String, + val url: String +) : Model { + + override fun createSearch() = SearchElement( + mapOf( + "name" to name + ) + ) + + companion object { + private const val chars = "abcdefghijklmnopqrstuvwxyz" + private const val length = 32 + fun generateUrl() = (0 until length).asSequence() + .map { Random.nextInt(chars.length) } + .map { chars[it] } + .map { + if (Random.nextBoolean()) it else it.toUpperCase() + }.joinToString("") + } +} diff --git a/src/commonMain/kotlin/de/kif/common/model/Room.kt b/src/commonMain/kotlin/de/kif/common/model/Room.kt index 47e6eee..d823f60 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Room.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Room.kt @@ -5,10 +5,14 @@ import kotlinx.serialization.Serializable @Serializable data class Room( - val id: Long? = null, + override val id: Long? = null, val name: String, val places: Int, - val projector: Boolean + val projector: Boolean, + val internet: Boolean, + val whiteboard: Boolean, + val blackboard: Boolean, + val accessible: Boolean ) : Model { override fun createSearch() = SearchElement( diff --git a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt index dfa7b25..bc3efad 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Schedule.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Schedule.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class Schedule( - val id: Long?, + override val id: Long?, val workGroup: WorkGroup, val room: Room, val day: Int, @@ -22,4 +22,7 @@ data class Schedule( "day" to day.toDouble() ) ) + + fun getAbsoluteStartTime(): Int = day * 60 * 24 + time + fun getAbsoluteEndTime(): Int = getAbsoluteStartTime() + workGroup.length } diff --git a/src/commonMain/kotlin/de/kif/common/model/Track.kt b/src/commonMain/kotlin/de/kif/common/model/Track.kt index 7fbc33d..5e62586 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Track.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Track.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class Track( - val id: Long?, + override val id: Long?, val name: String, val color: Color ) : Model { diff --git a/src/commonMain/kotlin/de/kif/common/model/User.kt b/src/commonMain/kotlin/de/kif/common/model/User.kt index 2ee0ec5..1e86af6 100644 --- a/src/commonMain/kotlin/de/kif/common/model/User.kt +++ b/src/commonMain/kotlin/de/kif/common/model/User.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class User( - val id: Long?, + override val id: Long?, val username: String, val password: String, val permissions: Set diff --git a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt index 224c352..ca54cb5 100644 --- a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt +++ b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt @@ -5,14 +5,19 @@ import kotlinx.serialization.Serializable @Serializable data class WorkGroup( - val id: Long?, + override val id: Long?, val name: String, val interested: Int, val track: Track?, val projector: Boolean, val resolution: Boolean, + val internet: Boolean, + val whiteboard: Boolean, + val blackboard: Boolean, + val accessible: Boolean, val length: Int, - val language: Language + val language: Language, + val constraints: List ) : Model { override fun createSearch() = SearchElement( diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index c54ffcd..83515c4 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -2,6 +2,7 @@ package de.kif.frontend import de.kif.frontend.views.calendar.initCalendar import de.kif.frontend.views.initTableLayout +import de.kif.frontend.views.initWorkGroupConstraints import de.westermann.kwebview.components.init import kotlin.browser.document @@ -14,4 +15,7 @@ fun main() = init { if (document.getElementsByClassName("table-layout").length > 0) { initTableLayout() } + if (document.getElementsByClassName("work-group-constraints").length > 0) { + initWorkGroupConstraints() + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt index 4d7d74b..c400c9c 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt @@ -1,5 +1,6 @@ package de.kif.frontend.repository +import de.kif.common.ConstraintMap import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType @@ -49,4 +50,14 @@ object ScheduleRepository : Repository { override fun onDelete(id: Long) = onDelete.emit(id) } + + suspend fun checkConstraints(): ConstraintMap { + val json = repositoryGet("/api/constraints") + return parser.parse(json, ConstraintMap.serializer()) + } + + suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap { + val json = repositoryGet("/api/constraint/${schedule.id}") + return parser.parse(json, ConstraintMap.serializer()) + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt new file mode 100644 index 0000000..80e4fc9 --- /dev/null +++ b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt @@ -0,0 +1,118 @@ +package de.kif.frontend.views + +import de.kif.frontend.launch +import de.kif.frontend.repository.WorkGroupRepository +import de.westermann.kobserve.event.EventListener +import de.westermann.kwebview.View +import de.westermann.kwebview.async +import de.westermann.kwebview.components.* +import de.westermann.kwebview.createHtmlView +import org.w3c.dom.* +import kotlin.browser.document + +fun initWorkGroupConstraints() { + var index = 10000 + + val constraints = + ListView.wrap(document.getElementsByClassName("work-group-constraints")[0] as HTMLElement) + val addButton = + View.wrap(document.getElementsByClassName("work-group-constraints-add")[0] as HTMLElement) + val addList = + ListView.wrap(document.getElementsByClassName("work-group-constraints-add-list")[0] as HTMLElement) + + console.log(constraints.html) + console.log(addButton.html) + console.log(addList.html) + + addButton.onClick { + addList.classList += "active" + + var listener: EventListener<*>? = null + + async { + listener = Body.onClick.reference { + addList.classList -= "active" + listener?.detach() + } + } + } + + addList.textView("Add only on day") { + onClick { + constraints.html.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("On day").apply { classList += "form-btn" }.html) + html.appendChild(InputView(InputType.NUMBER).apply { + classList += "form-control" + html.name = "constraint-only-on-day-${index++}" + min = -1337.0 + max = 1337.0 + }.html) + }.html) + } + } + addList.textView("Add only after time") { + onClick { + constraints.html.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("After time").apply { classList += "form-btn" }.html) + html.appendChild(InputView(InputType.NUMBER).apply { + classList += "form-control" + html.name = "constraint-only-after-time-${index++}" + min = -1337.0 + max = 133700.0 + }.html) + }.html) + } + } + addList.textView("Add not at same time") { + onClick { + constraints.html.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("Not with").apply { classList += "form-btn" }.html) + + val select = createHtmlView() + select.classList.add("form-control") + select.name = "constraint-not-at-same-time-${index++}" + + launch { + val all = WorkGroupRepository.all() + + for (wg in all) { + val option = createHtmlView() + option.value = wg.id.toString() + option.textContent = wg.name + select.appendChild(option) + } + } + + html.appendChild(select) + }.html) + } + } + addList.textView("Add only after work group") { + onClick { + constraints.html.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("After AK").apply { classList += "form-btn" }.html) + + val select = createHtmlView() + select.classList.add("form-control") + select.name = "constraint-only-after-work-group-${index++}" + + launch { + val all = WorkGroupRepository.all() + + for (wg in all) { + val option = createHtmlView() + option.value = wg.id.toString() + option.textContent = wg.name + select.appendChild(option) + } + } + + html.appendChild(select) + }.html) + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt index 9d3d3ac..379651d 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt @@ -4,6 +4,7 @@ import de.kif.frontend.iterator import de.kif.frontend.launch import de.kif.frontend.repository.ScheduleRepository import de.westermann.kwebview.View +import de.westermann.kwebview.createHtmlView import org.w3c.dom.* import kotlin.browser.document @@ -30,7 +31,7 @@ class Calendar(calendar: HTMLElement) : View(calendar) { } fun scrollHorizontalTo(pixel: Double) { - calendarTable.scrollTo (ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH)) + calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH)) } init { @@ -76,6 +77,27 @@ class Calendar(calendar: HTMLElement) : View(calendar) { } } } + + val cont = document.getElementsByClassName("header-right")[0] as HTMLElement + + val view = View.wrap(createHtmlView()) + cont.appendChild(view.html) + view.html.textContent = "Check" + view.onClick { + launch { + val errors = ScheduleRepository.checkConstraints() + + println(errors) + + for ((s, l) in errors.map) { + for (entry in calendarEntries) { + if (entry.scheduleId == s) { + entry.error = l.isNotEmpty() + } + } + } + } + } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt index 8a8a72e..3364628 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/calendar/CalendarEntry.kt @@ -31,6 +31,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi private lateinit var workGroup: WorkGroup var pending by classList.property("pending") + var error by classList.property("error") private var nextScroll = 0.0 var editable: Boolean = false diff --git a/src/jsMain/kotlin/de/westermann/kwebview/View.kt b/src/jsMain/kotlin/de/westermann/kwebview/View.kt index e556042..568412a 100644 --- a/src/jsMain/kotlin/de/westermann/kwebview/View.kt +++ b/src/jsMain/kotlin/de/westermann/kwebview/View.kt @@ -144,6 +144,6 @@ abstract class View(view: HTMLElement = createHtmlView()) { } companion object { - fun wrap(htmlElement: HTMLElement) = object : View(htmlElement) {} + fun wrap(htmlElement: HTMLElement, init: View.() -> Unit = {}) = object : View(htmlElement) {}.also(init) } } diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 4eedb78..d5052d1 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -296,6 +296,7 @@ a { span { padding: 0 0.5rem; + &:hover { background-color: rgba($text-primary-color, 0.06); } @@ -321,6 +322,12 @@ a { } } +textarea.form-control { + height: 16rem; + line-height: 1.1rem; + padding: 0.6rem 1rem; +} + select:-moz-focusring { color: transparent; text-shadow: 0 0 0 $text-primary-color; @@ -671,6 +678,10 @@ form { opacity: 0.6; } + &.error { + outline: solid 0.4rem $error-color; + } + @include no-select() } @@ -908,4 +919,115 @@ form { width: 4rem; } } +} + +.work-group-constraints { + position: relative; +} + +.work-group-constraints-add { + position: absolute; + top: 0; + right: 0; +} + +.work-group-constraints-add-list { + position: absolute; + top: 0; + right: 0; + z-index: 1; + display: none; + + background: $background-primary-color; + border: solid 1px rgba($text-primary-color, 0.1); + + span { + padding: 0 0.5rem; + + &:hover { + background-color: rgba($text-primary-color, 0.06); + } + } + + &.active { + display: block; + } +} + +.overview { + display: flex; +} + +.overview-main { + flex-grow: 4; + margin-right: 1rem; +} + +.overview-side { + min-width: 20%; +} + +.overview-shortcuts { + a { + display: block; + } +} + +.overview-twitter { + height: 20rem; + background-color: #b3e6f9; +} + +.post { + position: relative; + padding-top: 2rem; +} + +.post-name { + position: absolute; + top: 0; + left: 0; + font-size: 1.2rem; + color: $primary-color; + line-height: 2rem; + + &:empty::before { + display: block; + content: 'No title'; + opacity: 0.5; + font-size: 1.2rem; + line-height: 2rem; + color: $text-primary-color; + } +} +.post-edit { + position: absolute; + top: 0; + right: 0; + line-height: 2rem; +} + +.post-content { + h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + } + h1 { + font-size: 1rem; + } + h2 { + font-size: 1rem; + } + h3 { + font-size: 1rem; + } + h4 { + font-size: 1rem; + } + h5 { + font-size: 1rem; + } + h6 { + font-size: 1rem; + } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index 7a18bae..15ccea2 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -12,7 +12,9 @@ import io.ktor.http.content.static import io.ktor.jackson.jackson import io.ktor.routing.routing import io.ktor.websocket.WebSockets +import kotlinx.serialization.ImplicitReflectionSerializer +@ImplicitReflectionSerializer fun Application.main() { install(DefaultHeaders) install(CallLogging) @@ -35,7 +37,7 @@ fun Application.main() { } // UI routes - dashboard() + overview() calendar() login() account() @@ -53,6 +55,7 @@ fun Application.main() { trackApi() userApi() workGroupApi() + constraintsApi() // Web socket push notifications pushService() diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index 08abe04..e1db3fb 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -8,8 +8,10 @@ import io.ktor.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import kotlinx.coroutines.runBlocking +import kotlinx.serialization.ImplicitReflectionSerializer object Main { + @ImplicitReflectionSerializer @Suppress("UnusedMainParameter") @JvmStatic fun main(args: Array) { diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt index c0f7168..89c8f7c 100644 --- a/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt +++ b/src/jvmMain/kotlin/de/kif/backend/database/Connection.kt @@ -18,7 +18,8 @@ object Connection { SchemaUtils.create( DbTrack, DbWorkGroup, DbRoom, DbSchedule, - DbUser, DbUserPermission + DbUser, DbUserPermission, + DbPost ) } } diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt index cab06c7..7849ad5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt +++ b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt @@ -12,15 +12,22 @@ object DbTrack : Table() { object DbWorkGroup : Table() { val id = long("id").autoIncrement().primaryKey() - val name = varchar("first_name", 64) + val name = varchar("name", 64) val interested = integer("interested") val trackId = long("track_id").nullable() val projector = bool("projector") val resolution = bool("resolution") + + val internet = bool("internet") + val whiteboard = bool("whiteboard") + val blackboard = bool("blackboard") + val accessible = bool("accessible") + val language = enumeration("language", Language::class) val length = integer("length") + val constraints = text("constraints") } object DbRoom : Table() { @@ -29,6 +36,11 @@ object DbRoom : Table() { val places = integer("places") val projector = bool("projector") + + val internet = bool("internet") + val whiteboard = bool("whiteboard") + val blackboard = bool("blackboard") + val accessible = bool("accessible") } object DbSchedule : Table() { @@ -49,3 +61,11 @@ object DbUserPermission : Table() { val userId = long("id").primaryKey(0) val permission = enumeration("permission", Permission::class).primaryKey(1) } + +object DbPost : Table() { + val id = long("id").autoIncrement().primaryKey() + val name = varchar("name", 64) + + val content = text("content") + val url = varchar("url", 64).uniqueIndex() +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/PostRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/PostRepository.kt new file mode 100644 index 0000000..e7ea704 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/repository/PostRepository.kt @@ -0,0 +1,107 @@ +package de.kif.backend.repository + +import de.kif.backend.database.DbPost +import de.kif.backend.database.dbQuery +import de.kif.backend.util.PushService +import de.kif.common.MessageType +import de.kif.common.Repository +import de.kif.common.RepositoryType +import de.kif.common.model.Post +import de.westermann.kobserve.event.EventHandler +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* + +object PostRepository : Repository { + + override val onCreate = EventHandler() + override val onUpdate = EventHandler() + override val onDelete = EventHandler() + + private fun rowToModel(row: ResultRow): Post { + val id = row[DbPost.id] + val name = row[DbPost.name] + val content = row[DbPost.content] + val url = row[DbPost.url] + + return Post(id, name, content, url) + } + + override suspend fun get(id: Long): Post? { + return dbQuery { + rowToModel(DbPost.select { DbPost.id eq id }.firstOrNull() ?: return@dbQuery null) + } + } + + override suspend fun create(model: Post): Long { + return dbQuery { + val id = DbPost.insert { + it[name] = model.name + it[content] = model.content + it[url] = model.url + }[DbPost.id] ?: throw IllegalStateException("Cannot create model!") + + onCreate.emit(id) + + id + } + } + + override suspend fun update(model: Post) { + if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") + dbQuery { + DbPost.update({ DbPost.id eq model.id }) { + it[name] = model.name + it[content] = model.content + it[url] = model.url + } + + onUpdate.emit(model.id) + } + } + + override suspend fun delete(id: Long) { + onDelete.emit(id) + + dbQuery { + DbPost.deleteWhere { DbPost.id eq id } + } + } + + override suspend fun all(): List { + return dbQuery { + val result = DbPost.selectAll() + + result.map(this::rowToModel) + } + } + + suspend fun getByUrl(url: String): Post? { + return dbQuery { + val result = DbPost.select { DbPost.url eq url } + + runBlocking { + result.firstOrNull()?.let { + rowToModel(it) + } + } + } + } + + fun registerPushService() { + onCreate { + runBlocking { + PushService.notify(MessageType.CREATE, RepositoryType.POST, it) + } + } + onUpdate { + runBlocking { + PushService.notify(MessageType.UPDATE, RepositoryType.POST, it) + } + } + onDelete { + runBlocking { + PushService.notify(MessageType.DELETE, RepositoryType.POST, it) + } + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt index a83b3b2..db0b2f1 100644 --- a/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt +++ b/src/jvmMain/kotlin/de/kif/backend/repository/RoomRepository.kt @@ -20,8 +20,12 @@ object RoomRepository : Repository { val name = row[DbRoom.name] val places = row[DbRoom.places] val projector = row[DbRoom.projector] + val internet = row[DbRoom.internet] + val whiteboard = row[DbRoom.whiteboard] + val blackboard = row[DbRoom.blackboard] + val accessible = row[DbRoom.accessible] - return Room(id, name, places, projector) + return Room(id, name, places, projector, internet, whiteboard, blackboard, accessible) } override suspend fun get(id: Long): Room? { @@ -36,6 +40,10 @@ object RoomRepository : Repository { it[name] = model.name it[places] = model.places it[projector] = model.projector + it[internet] = model.internet + it[whiteboard] = model.whiteboard + it[blackboard] = model.blackboard + it[accessible] = model.accessible }[DbRoom.id] ?: throw IllegalStateException("Cannot create model!") onCreate.emit(id) @@ -51,6 +59,10 @@ object RoomRepository : Repository { it[name] = model.name it[places] = model.places it[projector] = model.projector + it[internet] = model.internet + it[whiteboard] = model.whiteboard + it[blackboard] = model.blackboard + it[accessible] = model.accessible } onUpdate.emit(model.id) diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt index 7117854..5d22d37 100644 --- a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt +++ b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt @@ -3,12 +3,15 @@ package de.kif.backend.repository import de.kif.backend.database.DbWorkGroup import de.kif.backend.database.dbQuery import de.kif.backend.util.PushService +import de.kif.common.Message import de.kif.common.MessageType import de.kif.common.Repository import de.kif.common.RepositoryType +import de.kif.common.model.Constraint import de.kif.common.model.WorkGroup import de.westermann.kobserve.event.EventHandler import kotlinx.coroutines.runBlocking +import kotlinx.serialization.list import org.jetbrains.exposed.sql.* object WorkGroupRepository : Repository { @@ -24,12 +27,31 @@ object WorkGroupRepository : Repository { val trackId = row[DbWorkGroup.trackId] val projector = row[DbWorkGroup.projector] val resolution = row[DbWorkGroup.resolution] + val internet = row[DbWorkGroup.internet] + val whiteboard = row[DbWorkGroup.whiteboard] + val blackboard = row[DbWorkGroup.blackboard] + val accessible = row[DbWorkGroup.accessible] val length = row[DbWorkGroup.length] val language = row[DbWorkGroup.language] + val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) val track = trackId?.let { TrackRepository.get(it) } - return WorkGroup(id, name, interested, track, projector, resolution, length, language) + return WorkGroup( + id, + name, + interested, + track, + projector, + resolution, + internet, + whiteboard, + blackboard, + accessible, + length, + language, + constraints + ) } override suspend fun get(id: Long): WorkGroup? { @@ -50,8 +72,13 @@ object WorkGroupRepository : Repository { it[trackId] = model.track?.id it[projector] = model.projector it[resolution] = model.resolution + it[internet] = model.internet + it[whiteboard] = model.whiteboard + it[blackboard] = model.blackboard + it[accessible] = model.accessible it[length] = model.length it[language] = model.language + it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) }[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!") onCreate.emit(id) @@ -69,8 +96,13 @@ object WorkGroupRepository : Repository { it[trackId] = model.track?.id it[projector] = model.projector it[resolution] = model.resolution + it[internet] = model.internet + it[whiteboard] = model.whiteboard + it[blackboard] = model.blackboard + it[accessible] = model.accessible it[length] = model.length it[language] = model.language + it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) } onUpdate.emit(model.id) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt index ed78f62..86f00bc 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt @@ -136,7 +136,7 @@ private fun DIV.renderTimeToRoom( val minutes = (time % 60).toString().padStart(2, '0') val hours = (time / 60).toString().padStart(2, '0') title = "$hours:$minutes" - attributes["data-time"] = time.toString() + attributes["data-time"] = start.toString() attributes["data-room"] = room.id.toString() attributes["data-day"] = day.toString() @@ -203,7 +203,7 @@ private fun DIV.renderRoomToTime( for (room in rooms) { div("calendar-cell") { - attributes["data-time"] = time.toString() + attributes["data-time"] = start.toString() attributes["data-room"] = room.id.toString() attributes["data-day"] = day.toString() title = timeString @@ -251,6 +251,7 @@ fun Route.calendar() { get("/calendar/{day}") { val user = isAuthenticated(Permission.SCHEDULE) + val editable = user != null val day = call.parameters["day"]?.toIntOrNull() ?: return@get @@ -280,11 +281,14 @@ fun Route.calendar() { min = h1 } + if (editable) { + min = min(min, 0) + max = max(max, 24 * 60) + } + min = (min / 60 - 1) * 60 max = (max / 60 + 2) * 60 - min = min(min, 0) - call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -314,7 +318,6 @@ fun Route.calendar() { } div("calendar") { - val editable = user != null attributes["data-day"] = day.toString() attributes["data-editable"] = editable.toString() diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt b/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt deleted file mode 100644 index c157043..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/route/Dashboard.kt +++ /dev/null @@ -1,27 +0,0 @@ -package de.kif.backend.route - -import de.kif.backend.PortalSession -import de.kif.backend.view.MainTemplate -import de.kif.backend.view.MenuTemplate -import io.ktor.application.call -import io.ktor.html.respondHtmlTemplate -import io.ktor.routing.Route -import io.ktor.routing.get -import io.ktor.sessions.get -import io.ktor.sessions.sessions -import kotlinx.html.h1 - -fun Route.dashboard() { - get("") { - val user = call.sessions.get()?.getUser(call) - call.respondHtmlTemplate(MainTemplate()) { - menuTemplate { - this.user = user - active = MenuTemplate.Tab.DASHBOARD - } - content { - h1 { +"Dashboard" } - } - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt new file mode 100644 index 0000000..59db722 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt @@ -0,0 +1,281 @@ +package de.kif.backend.route + +import de.kif.backend.authenticateOrRedirect +import de.kif.backend.isAuthenticated +import de.kif.backend.repository.PostRepository +import de.kif.backend.util.markdownToHtml +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate +import de.kif.common.model.Permission +import de.kif.common.model.Post +import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate +import io.ktor.request.receiveParameters +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.util.toMap +import kotlinx.html.* + +fun Route.overview() { + get("") { + val user = isAuthenticated(Permission.POST) + val editable = user != null + + val postList = PostRepository.all().asReversed() + + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.BOARD + } + content { + div("overview") { + div("overview-main") { + if (editable) { + div("overview-new") { + a("post/new", classes = "form-btn") { + +"New" + } + } + } + + for (post in postList) { + div("overview-post post") { + span("post-name") { + +post.name + } + if (editable) { + a("/post/${post.id}", classes = "post-edit") { + i("material-icons") { +"edit" } + } + } + div("post-content") { + unsafe { + raw(markdownToHtml(post.content)) + } + } + } + } + } + div("overview-side") { + div("overview-shortcuts") { + a { + +"Wiki" + } + a { + +"Wiki" + } + a { + +"Wiki" + } + a { + +"Wiki" + } + a { + +"Wiki" + } + a { + +"Wiki" + } + } + div("overview-twitter") { + +"The Twitter Wall" + } + } + } + } + } + } + + + get("/post/{id}") { + authenticateOrRedirect(Permission.POST) { user -> + val postId = call.parameters["id"]?.toLongOrNull() ?: return@get + val editPost = PostRepository.get(postId) ?: return@get + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.BOARD + } + content { + h1 { +"Edit post" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = editPost.name + } + } + div("form-group") { + label { + htmlFor = "url" + +"Url" + } + input( + name = "url", + classes = "form-control" + ) { + id = "places" + placeholder = "Places" + value = editPost.url + } + } + + div("form-group") { + label { + htmlFor = "content" + +"Content" + } + textArea(rows = "10", classes = "form-control") { + name = "content" + id = "projector" + + +editPost.content + } + } + + div("form-group") { + a("/") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } + } + } + a("/post/${editPost.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } + } + } + } + } + } + + post("/post/{id}") { + authenticateOrRedirect(Permission.POST) { user -> + val postId = call.parameters["id"]?.toLongOrNull() ?: return@post + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + var post = PostRepository.get(postId) ?: return@post + + params["name"]?.let { post = post.copy(name = it) } + params["url"]?.let { post = post.copy(url = it) } + params["content"]?.let { post = post.copy(content = it) } + + PostRepository.update(post) + + call.respondRedirect("/") + } + } + + get("/post/new") { + authenticateOrRedirect(Permission.POST) { user -> + call.respondHtmlTemplate(MainTemplate()) { + menuTemplate { + this.user = user + active = MenuTemplate.Tab.BOARD + } + content { + h1 { +"Create post" } + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = "" + } + } + div("form-group") { + label { + htmlFor = "url" + +"Url" + } + input( + name = "url", + classes = "form-control" + ) { + id = "places" + placeholder = "Places" + value = Post.generateUrl() + } + } + + div("form-group") { + label { + htmlFor = "content" + +"Content" + } + textArea(rows = "10", classes = "form-control") { + name = "content" + id = "projector" + + +"" + } + } + + div("form-group") { + a("/") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } + } + } + } + } + } + } + + post("/post/new") { + authenticateOrRedirect(Permission.POST) { user -> + val params = call.receiveParameters().toMap().mapValues { (_, list) -> + list.firstOrNull() + } + + val name = params["name"] ?: return@post + val content = params["content"] ?: return@post + val url = params["url"] ?: return@post + + val post = Post(null, name, content, url) + + PostRepository.create(post) + + call.respondRedirect("/") + } + } + + get("/post/{id}/delete") { + authenticateOrRedirect(Permission.POST) { user -> + val postId = call.parameters["id"]?.toLongOrNull() ?: return@get + + PostRepository.delete(postId) + + call.respondRedirect("/") + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index 4676784..c923121 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -36,7 +36,6 @@ fun Route.room() { active = MenuTemplate.Tab.ROOM } content { - h1 { +"Rooms" } insert(TableTemplate()) { searchValue = search @@ -169,6 +168,74 @@ fun Route.room() { } } + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "internet", + classes = "form-control", + type = InputType.checkBox + ) { + id = "internet" + checked = editRoom.internet + } + label { + htmlFor = "internet" + +"Internet" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "whiteboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "whiteboard" + checked = editRoom.whiteboard + } + label { + htmlFor = "whiteboard" + +"Whiteboard" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "blackboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "blackboard" + checked = editRoom.blackboard + } + label { + htmlFor = "blackboard" + +"Blackboard" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "accessible", + classes = "form-control", + type = InputType.checkBox + ) { + id = "accessible" + checked = editRoom.accessible + } + label { + htmlFor = "accessible" + +"Accessible" + } + } + } + div("form-group") { a("/room") { button(classes = "form-btn") { @@ -201,6 +268,10 @@ fun Route.room() { params["name"]?.let { room = room.copy(name = it) } params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) } params["projector"]?.let { room = room.copy(projector = it == "on") } + params["internet"]?.let { room = room.copy(internet = it == "on") } + params["whiteboard"]?.let { room = room.copy(whiteboard = it == "on") } + params["blackboard"]?.let { room = room.copy(blackboard = it == "on") } + params["accessible"]?.let { room = room.copy(accessible = it == "on") } RoomRepository.update(room) @@ -268,6 +339,74 @@ fun Route.room() { } } + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "internet", + classes = "form-control", + type = InputType.checkBox + ) { + id = "internet" + checked = false + } + label { + htmlFor = "internet" + +"Internet" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "whiteboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "whiteboard" + checked = false + } + label { + htmlFor = "whiteboard" + +"Whiteboard" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "blackboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "blackboard" + checked = false + } + label { + htmlFor = "blackboard" + +"Blackboard" + } + } + } + + div("form-switch-group") { + div("form-group form-switch") { + input( + name = "accessible", + classes = "form-control", + type = InputType.checkBox + ) { + id = "accessible" + checked = false + } + label { + htmlFor = "accessible" + +"Accessible" + } + } + } + div("form-group") { a("/room") { button(classes = "form-btn") { @@ -293,8 +432,12 @@ fun Route.room() { val name = params["name"] ?: return@post val places = (params["places"] ?: return@post).toIntOrNull() ?: 0 val projector = params["projector"] == "on" + val internet = params["internet"] == "on" + val whiteboard = params["whiteboard"] == "on" + val blackboard = params["blackboard"] == "on" + val accessible = params["accessible"] == "on" - val room = Room(null, name, places, projector) + val room = Room(null, name, places, projector, internet, whiteboard, blackboard, accessible) RoomRepository.create(room) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index 644e22c..e358148 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -94,7 +94,6 @@ fun Route.track() { active = MenuTemplate.Tab.WORK_GROUP } content { - h1 { +"Tracks" } insert(TableTemplate()) { searchValue = search diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index ef07acb..746cfe1 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -37,7 +37,6 @@ fun Route.user() { active = MenuTemplate.Tab.USER } content { - h1 { +"Users" } insert(TableTemplate()) { searchValue = search diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 5e1c53b..8fccf48 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -1,6 +1,5 @@ package de.kif.backend.route - import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.WorkGroupRepository @@ -8,9 +7,7 @@ import de.kif.backend.view.MainTemplate import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate import de.kif.common.Search -import de.kif.common.model.Language -import de.kif.common.model.Permission -import de.kif.common.model.WorkGroup +import de.kif.common.model.* import io.ktor.application.call import io.ktor.html.insert import io.ktor.html.respondHtmlTemplate @@ -36,7 +33,6 @@ fun Route.workGroup() { active = MenuTemplate.Tab.WORK_GROUP } content { - h1 { +"Work groups" } insert(TableTemplate()) { searchValue = search @@ -157,6 +153,17 @@ fun Route.workGroup() { val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get val tracks = TrackRepository.all() + + val workGroups = editWorkGroup.constraints.mapNotNull { + when (it.type) { + ConstraintType.NotAtSameTime -> it.number + ConstraintType.OnlyAfterWorkGroup -> it.number + else -> null + } + }.distinct().associateWith { + WorkGroupRepository.get(it)!! + } + call.respondHtmlTemplate(MainTemplate()) { menuTemplate { this.user = user @@ -201,7 +208,6 @@ fun Route.workGroup() { htmlFor = "track" +"Track" } - //div("input-group") { select( classes = "form-control" ) { @@ -220,12 +226,6 @@ fun Route.workGroup() { } } } - /* - a("/tracks", classes = "form-btn") { - i("material-icons") { +"edit" } - } - } - */ } div("form-group") { label { @@ -294,6 +294,149 @@ fun Route.workGroup() { +"Resolution" } } + + div("form-group form-switch") { + input( + name = "internet", + classes = "form-control", + type = InputType.checkBox + ) { + id = "internet" + checked = editWorkGroup.internet + } + label { + htmlFor = "internet" + +"Internet" + } + } + + div("form-group form-switch") { + input( + name = "whiteboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "whiteboard" + checked = editWorkGroup.whiteboard + } + label { + htmlFor = "whiteboard" + +"Whiteboard" + } + } + + div("form-group form-switch") { + input( + name = "blackboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "blackboard" + checked = editWorkGroup.blackboard + } + label { + htmlFor = "blackboard" + +"Blackboard" + } + } + + div("form-group form-switch") { + input( + name = "accessible", + classes = "form-control", + type = InputType.checkBox + ) { + id = "accessible" + checked = editWorkGroup.accessible + } + label { + htmlFor = "accessible" + +"Accessible" + } + } + + div("work-group-constraints") + } + + div("form-group work-group-constraints") { + label { + +"Constraints" + } + span("form-btn work-group-constraints-add") { + i("material-icons") { +"add" } + } + div("work-group-constraints-add-list") { + + } + for ((index, constraint) in editWorkGroup.constraints.withIndex()) { + div("input-group") { + when (constraint.type) { + ConstraintType.OnlyOnDay -> { + span("form-btn") { + +"On day" + } + input( + name = "constraint-only-on-day-$index", + classes = "form-control", + type = InputType.number + ) { + value = constraint.number.toString() + + min = "-1337" + max = "1337" + } + } + ConstraintType.OnlyAfterTime -> { + span("form-btn") { + +"After time" + } + input( + name = "constraint-only-after-time-$index", + classes = "form-control", + type = InputType.number + ) { + value = constraint.number.toString() + + min = "-1337" + max = "133700" + } + } + ConstraintType.NotAtSameTime -> { + span("form-btn") { + +"Not with" + } + select( + classes = "form-control" + ) { + name = "constraint-not-at-same-time-$index" + + option { + selected = true + value = constraint.number.toString() + +(workGroups[constraint.number]?.name ?: "") + } + } + } + ConstraintType.OnlyAfterWorkGroup -> { + span("form-btn") { + +"After AK" + } + select( + classes = "form-control" + ) { + name = "constraint-only-after-work-group-$index" + + option { + selected = true + value = constraint.number.toString() + +(workGroups[constraint.number]?.name ?: "") + } + } + } + } + + } + } } div("form-group") { @@ -333,12 +476,37 @@ fun Route.workGroup() { } params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") } params["resolution"]?.let { editWorkGroup = editWorkGroup.copy(resolution = it == "on") } + params["internet"]?.let { editWorkGroup = editWorkGroup.copy(internet = it == "on") } + params["whiteboard"]?.let { editWorkGroup = editWorkGroup.copy(whiteboard = it == "on") } + params["blackboard"]?.let { editWorkGroup = editWorkGroup.copy(blackboard = it == "on") } + params["accessible"]?.let { editWorkGroup = editWorkGroup.copy(accessible = it == "on") } + params["length"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(length = it) } params["language"]?.let { editWorkGroup = editWorkGroup.copy(language = Language.values().find { l -> l.code == it } ?: Language.GERMAN) } + val constraints = params.mapNotNull { (key, value) -> + when { + key.startsWith("constraint-only-on-day") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyOnDay, it) } + } + key.startsWith("constraint-only-after-time") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, it) } + } + key.startsWith("constraint-not-at-same-time") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.NotAtSameTime, it) } + } + key.startsWith("constraint-only-after-work-group") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterWorkGroup, it) } + } + else -> null + } + } + + editWorkGroup = editWorkGroup.copy(constraints = constraints) + WorkGroupRepository.update(editWorkGroup) call.respondRedirect("/workgroups") @@ -477,6 +645,78 @@ fun Route.workGroup() { +"Resolution" } } + + div("form-group form-switch") { + input( + name = "internet", + classes = "form-control", + type = InputType.checkBox + ) { + id = "internet" + checked = false + } + label { + htmlFor = "internet" + +"Internet" + } + } + + div("form-group form-switch") { + input( + name = "whiteboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "whiteboard" + checked = false + } + label { + htmlFor = "whiteboard" + +"Whiteboard" + } + } + + div("form-group form-switch") { + input( + name = "blackboard", + classes = "form-control", + type = InputType.checkBox + ) { + id = "blackboard" + checked = false + } + label { + htmlFor = "blackboard" + +"Blackboard" + } + } + + div("form-group form-switch") { + input( + name = "accessible", + classes = "form-control", + type = InputType.checkBox + ) { + id = "accessible" + checked = false + } + label { + htmlFor = "accessible" + +"Accessible" + } + } + } + + div("form-group work-group-constraints") { + label { + +"Constraints" + } + span("form-btn work-group-constraints-add") { + i("material-icons") { +"add" } + } + div("work-group-constraints-add-list") { + + } } div("form-group") { @@ -511,6 +751,29 @@ fun Route.workGroup() { Language.values().find { l -> l.code == it } ?: Language.GERMAN } + val internet = params["internet"] == "on" + val whiteboard = params["whiteboard"] == "on" + val blackboard = params["blackboard"] == "on" + val accessible = params["accessible"] == "on" + + val constraints = params.mapNotNull { (key, value) -> + when { + key.startsWith("constraint-only-on-day") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyOnDay, it) } + } + key.startsWith("constraint-only-after-time") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, it) } + } + key.startsWith("constraint-not-at-same-time") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.NotAtSameTime, it) } + } + key.startsWith("constraint-only-after-work-group") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterWorkGroup, it) } + } + else -> null + } + } + val workGroup = WorkGroup( null, name = name, @@ -518,8 +781,13 @@ fun Route.workGroup() { track = track, projector = projector, resolution = resolution, + internet = internet, + whiteboard = whiteboard, + blackboard = blackboard, + accessible = accessible, length = length, - language = language + language = language, + constraints = constraints ) WorkGroupRepository.create(workGroup) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt new file mode 100644 index 0000000..000db2e --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Constraints.kt @@ -0,0 +1,47 @@ +package de.kif.backend.route.api + +import de.kif.backend.authenticate +import de.kif.backend.repository.ScheduleRepository +import de.kif.common.checkConstraints +import de.kif.common.model.Permission +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.routing.Route +import io.ktor.routing.get + +fun Route.constraintsApi() { + get("/api/constraints") { + try { + authenticate(Permission.SCHEDULE) { + val schedules = ScheduleRepository.all() + + val errors = checkConstraints(schedules, schedules) + + call.success(errors) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } + + get("/api/constraint/{id}") { + try { + authenticate(Permission.SCHEDULE) { + val id = call.parameters["id"]?.toLongOrNull() + val schedules = ScheduleRepository.all() + + val check = schedules.filter { it.workGroup.id == id } + + val errors = checkConstraints(check, schedules) + + call.success(errors) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt b/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt new file mode 100644 index 0000000..843060b --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt @@ -0,0 +1,60 @@ +package de.kif.backend.util + +import com.vladsch.flexmark.ext.autolink.AutolinkExtension +import com.vladsch.flexmark.ext.emoji.EmojiExtension +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension +import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension +import com.vladsch.flexmark.ext.tables.TablesExtension +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.options.MutableDataSet; +import java.util.* + +/* +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.html.HtmlGenerator +import org.intellij.markdown.parser.MarkdownParser + +fun markdownToHtml(content: String): String { + val flavour = CommonMarkFlavourDescriptor() + val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content) + val html = HtmlGenerator(content, parsedTree, flavour).generateHtml() + return html +} + */ +/* +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer + +fun markdownToHtml(content: String): String { + val parser = Parser.builder().build() + val document = parser.parse(content) + val renderer = HtmlRenderer.builder().build() + return renderer.render(document) +} + + */ + +fun markdownToHtml(content: String): String { + val options = MutableDataSet() + + options.set(Parser.EXTENSIONS, Arrays.asList( + TablesExtension.create(), + StrikethroughExtension.create(), + TaskListExtension.create(), + EmojiExtension.create(), + AutolinkExtension.create() + )); + + //options.set(HtmlRenderer.SOFT_BREAK, "
\n"); + + val parser = Parser.builder(options).build() + val renderer = HtmlRenderer.builder(options).build() + + // You can re-use parser and renderer instances + val document = parser.parse(content) + val html = renderer.render(document) + + return html +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt index 8977931..4a0c5d3 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt @@ -50,4 +50,5 @@ fun Route.pushService() { TrackRepository.registerPushService() UserRepository.registerPushService() WorkGroupRepository.registerPushService() + PostRepository.registerPushService() } \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt index e0fe5f4..364adc0 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MenuTemplate.kt @@ -7,14 +7,14 @@ import kotlinx.html.* class MenuTemplate() : Template { - var active: Tab = Tab.DASHBOARD + var active: Tab = Tab.BOARD var user: User? = null override fun FlowContent.apply() { nav("menu") { div("container") { div("menu-left") { - a("/", classes = if (active == Tab.DASHBOARD) "active" else null) { + a("/", classes = if (active == Tab.BOARD) "active" else null) { +"Dashboard" } a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { @@ -42,7 +42,7 @@ class MenuTemplate() : Template { +"Rooms" } } - if (user.checkPermission(Permission.PERSON)) { + if (user.checkPermission(Permission.USER)) { a("/users", classes = if (active == Tab.USER) "active" else null) { +"Users" } @@ -58,6 +58,6 @@ class MenuTemplate() : Template { } enum class Tab { - DASHBOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER + BOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER } }