Add overview

This commit is contained in:
Lars Westermann 2019-05-24 14:29:10 +02:00
parent e88db9c75c
commit c28317aefd
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
39 changed files with 1505 additions and 71 deletions

View file

@ -22,7 +22,8 @@ version "0.1.0"
repositories { repositories {
jcenter() 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/kotlinx" }
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" } maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
mavenCentral() mavenCentral()
@ -87,6 +88,9 @@ kotlin {
implementation "de.westermann:KObserve-jvm:$observable_version" 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 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3' api 'ch.qos.logback:logback-classic:1.2.3'
api 'org.fusesource.jansi:jansi:1.8' api 'org.fusesource.jansi:jansi:1.8'

View file

@ -1 +1 @@
kotlin.code.style=official kotlin.code.style=official

View file

@ -0,0 +1,60 @@
package de.kif.common
import de.kif.common.model.Model
import de.westermann.kobserve.event.EventHandler
class CacheRepository<T : Model>(val repository: Repository<T>) : Repository<T> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
var cache: Map<Long, T> = 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<T> {
if (cacheComplete) {
return cache.values.toList()
} else {
val all = repository.all()
cache = all.associateBy { it.id!! }
cacheComplete = true
return all
}
}
}

View file

@ -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<Pair<Long, List<ConstraintError>>>
)
fun checkConstraints(
check: List<Schedule>,
against: List<Schedule>
): ConstraintMap {
val map = mutableMapOf<Long, List<ConstraintError>>()
for (schedule in check) {
if (schedule.id == null) continue
val errors = mutableListOf<ConstraintError>()
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() })
}

View file

@ -31,5 +31,5 @@ enum class MessageType {
} }
enum class RepositoryType { enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST
} }

View file

@ -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
}

View file

@ -3,5 +3,6 @@ package de.kif.common.model
import de.kif.common.SearchElement import de.kif.common.SearchElement
interface Model { interface Model {
val id : Long?
fun createSearch(): SearchElement fun createSearch(): SearchElement
} }

View file

@ -1,5 +1,5 @@
package de.kif.common.model package de.kif.common.model
enum class Permission { enum class Permission {
USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN USER, SCHEDULE, WORK_GROUP, ROOM, POST, ADMIN
} }

View file

@ -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("")
}
}

View file

@ -5,10 +5,14 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Room( data class Room(
val id: Long? = null, override val id: Long? = null,
val name: String, val name: String,
val places: Int, val places: Int,
val projector: Boolean val projector: Boolean,
val internet: Boolean,
val whiteboard: Boolean,
val blackboard: Boolean,
val accessible: Boolean
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Schedule( data class Schedule(
val id: Long?, override val id: Long?,
val workGroup: WorkGroup, val workGroup: WorkGroup,
val room: Room, val room: Room,
val day: Int, val day: Int,
@ -22,4 +22,7 @@ data class Schedule(
"day" to day.toDouble() "day" to day.toDouble()
) )
) )
fun getAbsoluteStartTime(): Int = day * 60 * 24 + time
fun getAbsoluteEndTime(): Int = getAbsoluteStartTime() + workGroup.length
} }

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Track( data class Track(
val id: Long?, override val id: Long?,
val name: String, val name: String,
val color: Color val color: Color
) : Model { ) : Model {

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class User( data class User(
val id: Long?, override val id: Long?,
val username: String, val username: String,
val password: String, val password: String,
val permissions: Set<Permission> val permissions: Set<Permission>

View file

@ -5,14 +5,19 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class WorkGroup( data class WorkGroup(
val id: Long?, override val id: Long?,
val name: String, val name: String,
val interested: Int, val interested: Int,
val track: Track?, val track: Track?,
val projector: Boolean, val projector: Boolean,
val resolution: Boolean, val resolution: Boolean,
val internet: Boolean,
val whiteboard: Boolean,
val blackboard: Boolean,
val accessible: Boolean,
val length: Int, val length: Int,
val language: Language val language: Language,
val constraints: List<Constraint>
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -2,6 +2,7 @@ package de.kif.frontend
import de.kif.frontend.views.calendar.initCalendar import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initTableLayout import de.kif.frontend.views.initTableLayout
import de.kif.frontend.views.initWorkGroupConstraints
import de.westermann.kwebview.components.init import de.westermann.kwebview.components.init
import kotlin.browser.document import kotlin.browser.document
@ -14,4 +15,7 @@ fun main() = init {
if (document.getElementsByClassName("table-layout").length > 0) { if (document.getElementsByClassName("table-layout").length > 0) {
initTableLayout() initTableLayout()
} }
if (document.getElementsByClassName("work-group-constraints").length > 0) {
initWorkGroupConstraints()
}
} }

View file

@ -1,5 +1,6 @@
package de.kif.frontend.repository package de.kif.frontend.repository
import de.kif.common.ConstraintMap
import de.kif.common.Message import de.kif.common.Message
import de.kif.common.Repository import de.kif.common.Repository
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
@ -49,4 +50,14 @@ object ScheduleRepository : Repository<Schedule> {
override fun onDelete(id: Long) = onDelete.emit(id) 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())
}
} }

View file

@ -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<View>(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<View>(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<HTMLDivElement>()) {
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<HTMLDivElement>()) {
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<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Not with").apply { classList += "form-btn" }.html)
val select = createHtmlView<HTMLSelectElement>()
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<HTMLOptionElement>()
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<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("After AK").apply { classList += "form-btn" }.html)
val select = createHtmlView<HTMLSelectElement>()
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<HTMLOptionElement>()
option.value = wg.id.toString()
option.textContent = wg.name
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
}

View file

@ -4,6 +4,7 @@ import de.kif.frontend.iterator
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.browser.document import kotlin.browser.document
@ -30,7 +31,7 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
} }
fun scrollHorizontalTo(pixel: Double) { fun scrollHorizontalTo(pixel: Double) {
calendarTable.scrollTo (ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH)) calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH))
} }
init { 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<HTMLAnchorElement>())
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()
}
}
}
}
}
} }
} }

View file

@ -31,6 +31,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
private lateinit var workGroup: WorkGroup private lateinit var workGroup: WorkGroup
var pending by classList.property("pending") var pending by classList.property("pending")
var error by classList.property("error")
private var nextScroll = 0.0 private var nextScroll = 0.0
var editable: Boolean = false var editable: Boolean = false

View file

@ -144,6 +144,6 @@ abstract class View(view: HTMLElement = createHtmlView()) {
} }
companion object { companion object {
fun wrap(htmlElement: HTMLElement) = object : View(htmlElement) {} fun wrap(htmlElement: HTMLElement, init: View.() -> Unit = {}) = object : View(htmlElement) {}.also(init)
} }
} }

View file

@ -296,6 +296,7 @@ a {
span { span {
padding: 0 0.5rem; padding: 0 0.5rem;
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); 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 { select:-moz-focusring {
color: transparent; color: transparent;
text-shadow: 0 0 0 $text-primary-color; text-shadow: 0 0 0 $text-primary-color;
@ -671,6 +678,10 @@ form {
opacity: 0.6; opacity: 0.6;
} }
&.error {
outline: solid 0.4rem $error-color;
}
@include no-select() @include no-select()
} }
@ -908,4 +919,115 @@ form {
width: 4rem; 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;
}
} }

View file

@ -12,7 +12,9 @@ import io.ktor.http.content.static
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import kotlinx.serialization.ImplicitReflectionSerializer
@ImplicitReflectionSerializer
fun Application.main() { fun Application.main() {
install(DefaultHeaders) install(DefaultHeaders)
install(CallLogging) install(CallLogging)
@ -35,7 +37,7 @@ fun Application.main() {
} }
// UI routes // UI routes
dashboard() overview()
calendar() calendar()
login() login()
account() account()
@ -53,6 +55,7 @@ fun Application.main() {
trackApi() trackApi()
userApi() userApi()
workGroupApi() workGroupApi()
constraintsApi()
// Web socket push notifications // Web socket push notifications
pushService() pushService()

View file

@ -8,8 +8,10 @@ import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
object Main { object Main {
@ImplicitReflectionSerializer
@Suppress("UnusedMainParameter") @Suppress("UnusedMainParameter")
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {

View file

@ -18,7 +18,8 @@ object Connection {
SchemaUtils.create( SchemaUtils.create(
DbTrack, DbWorkGroup, DbTrack, DbWorkGroup,
DbRoom, DbSchedule, DbRoom, DbSchedule,
DbUser, DbUserPermission DbUser, DbUserPermission,
DbPost
) )
} }
} }

View file

@ -12,15 +12,22 @@ object DbTrack : Table() {
object DbWorkGroup : Table() { object DbWorkGroup : Table() {
val id = long("id").autoIncrement().primaryKey() val id = long("id").autoIncrement().primaryKey()
val name = varchar("first_name", 64) val name = varchar("name", 64)
val interested = integer("interested") val interested = integer("interested")
val trackId = long("track_id").nullable() val trackId = long("track_id").nullable()
val projector = bool("projector") val projector = bool("projector")
val resolution = bool("resolution") 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 language = enumeration("language", Language::class)
val length = integer("length") val length = integer("length")
val constraints = text("constraints")
} }
object DbRoom : Table() { object DbRoom : Table() {
@ -29,6 +36,11 @@ object DbRoom : Table() {
val places = integer("places") val places = integer("places")
val projector = bool("projector") val projector = bool("projector")
val internet = bool("internet")
val whiteboard = bool("whiteboard")
val blackboard = bool("blackboard")
val accessible = bool("accessible")
} }
object DbSchedule : Table() { object DbSchedule : Table() {
@ -49,3 +61,11 @@ object DbUserPermission : Table() {
val userId = long("id").primaryKey(0) val userId = long("id").primaryKey(0)
val permission = enumeration("permission", Permission::class).primaryKey(1) 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()
}

View file

@ -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<Post> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
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<Post> {
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)
}
}
}
}

View file

@ -20,8 +20,12 @@ object RoomRepository : Repository<Room> {
val name = row[DbRoom.name] val name = row[DbRoom.name]
val places = row[DbRoom.places] val places = row[DbRoom.places]
val projector = row[DbRoom.projector] 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? { override suspend fun get(id: Long): Room? {
@ -36,6 +40,10 @@ object RoomRepository : Repository<Room> {
it[name] = model.name it[name] = model.name
it[places] = model.places it[places] = model.places
it[projector] = model.projector 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!") }[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -51,6 +59,10 @@ object RoomRepository : Repository<Room> {
it[name] = model.name it[name] = model.name
it[places] = model.places it[places] = model.places
it[projector] = model.projector it[projector] = model.projector
it[internet] = model.internet
it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard
it[accessible] = model.accessible
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -3,12 +3,15 @@ package de.kif.backend.repository
import de.kif.backend.database.DbWorkGroup import de.kif.backend.database.DbWorkGroup
import de.kif.backend.database.dbQuery import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService import de.kif.backend.util.PushService
import de.kif.common.Message
import de.kif.common.MessageType import de.kif.common.MessageType
import de.kif.common.Repository import de.kif.common.Repository
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
import de.kif.common.model.Constraint
import de.kif.common.model.WorkGroup import de.kif.common.model.WorkGroup
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.list
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
object WorkGroupRepository : Repository<WorkGroup> { object WorkGroupRepository : Repository<WorkGroup> {
@ -24,12 +27,31 @@ object WorkGroupRepository : Repository<WorkGroup> {
val trackId = row[DbWorkGroup.trackId] val trackId = row[DbWorkGroup.trackId]
val projector = row[DbWorkGroup.projector] val projector = row[DbWorkGroup.projector]
val resolution = row[DbWorkGroup.resolution] 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 length = row[DbWorkGroup.length]
val language = row[DbWorkGroup.language] val language = row[DbWorkGroup.language]
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val track = trackId?.let { TrackRepository.get(it) } 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? { override suspend fun get(id: Long): WorkGroup? {
@ -50,8 +72,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[trackId] = model.track?.id it[trackId] = model.track?.id
it[projector] = model.projector it[projector] = model.projector
it[resolution] = model.resolution 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[length] = model.length
it[language] = model.language it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!") }[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -69,8 +96,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[trackId] = model.track?.id it[trackId] = model.track?.id
it[projector] = model.projector it[projector] = model.projector
it[resolution] = model.resolution 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[length] = model.length
it[language] = model.language it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -136,7 +136,7 @@ private fun DIV.renderTimeToRoom(
val minutes = (time % 60).toString().padStart(2, '0') val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0') val hours = (time / 60).toString().padStart(2, '0')
title = "$hours:$minutes" title = "$hours:$minutes"
attributes["data-time"] = time.toString() attributes["data-time"] = start.toString()
attributes["data-room"] = room.id.toString() attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString() attributes["data-day"] = day.toString()
@ -203,7 +203,7 @@ private fun DIV.renderRoomToTime(
for (room in rooms) { for (room in rooms) {
div("calendar-cell") { div("calendar-cell") {
attributes["data-time"] = time.toString() attributes["data-time"] = start.toString()
attributes["data-room"] = room.id.toString() attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString() attributes["data-day"] = day.toString()
title = timeString title = timeString
@ -251,6 +251,7 @@ fun Route.calendar() {
get("/calendar/{day}") { get("/calendar/{day}") {
val user = isAuthenticated(Permission.SCHEDULE) val user = isAuthenticated(Permission.SCHEDULE)
val editable = user != null
val day = call.parameters["day"]?.toIntOrNull() ?: return@get val day = call.parameters["day"]?.toIntOrNull() ?: return@get
@ -280,11 +281,14 @@ fun Route.calendar() {
min = h1 min = h1
} }
if (editable) {
min = min(min, 0)
max = max(max, 24 * 60)
}
min = (min / 60 - 1) * 60 min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60 max = (max / 60 + 2) * 60
min = min(min, 0)
call.respondHtmlTemplate(MainTemplate()) { call.respondHtmlTemplate(MainTemplate()) {
menuTemplate { menuTemplate {
this.user = user this.user = user
@ -314,7 +318,6 @@ fun Route.calendar() {
} }
div("calendar") { div("calendar") {
val editable = user != null
attributes["data-day"] = day.toString() attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString() attributes["data-editable"] = editable.toString()

View file

@ -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<PortalSession>()?.getUser(call)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.DASHBOARD
}
content {
h1 { +"Dashboard" }
}
}
}
}

View file

@ -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("/")
}
}
}

View file

@ -36,7 +36,6 @@ fun Route.room() {
active = MenuTemplate.Tab.ROOM active = MenuTemplate.Tab.ROOM
} }
content { content {
h1 { +"Rooms" }
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search 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") { div("form-group") {
a("/room") { a("/room") {
button(classes = "form-btn") { button(classes = "form-btn") {
@ -201,6 +268,10 @@ fun Route.room() {
params["name"]?.let { room = room.copy(name = it) } params["name"]?.let { room = room.copy(name = it) }
params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) } params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) }
params["projector"]?.let { room = room.copy(projector = it == "on") } 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) 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") { div("form-group") {
a("/room") { a("/room") {
button(classes = "form-btn") { button(classes = "form-btn") {
@ -293,8 +432,12 @@ fun Route.room() {
val name = params["name"] ?: return@post val name = params["name"] ?: return@post
val places = (params["places"] ?: return@post).toIntOrNull() ?: 0 val places = (params["places"] ?: return@post).toIntOrNull() ?: 0
val projector = params["projector"] == "on" 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) RoomRepository.create(room)

View file

@ -94,7 +94,6 @@ fun Route.track() {
active = MenuTemplate.Tab.WORK_GROUP active = MenuTemplate.Tab.WORK_GROUP
} }
content { content {
h1 { +"Tracks" }
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search

View file

@ -37,7 +37,6 @@ fun Route.user() {
active = MenuTemplate.Tab.USER active = MenuTemplate.Tab.USER
} }
content { content {
h1 { +"Users" }
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search

View file

@ -1,6 +1,5 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository 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.MenuTemplate
import de.kif.backend.view.TableTemplate import de.kif.backend.view.TableTemplate
import de.kif.common.Search import de.kif.common.Search
import de.kif.common.model.Language import de.kif.common.model.*
import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.insert import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate import io.ktor.html.respondHtmlTemplate
@ -36,7 +33,6 @@ fun Route.workGroup() {
active = MenuTemplate.Tab.WORK_GROUP active = MenuTemplate.Tab.WORK_GROUP
} }
content { content {
h1 { +"Work groups" }
insert(TableTemplate()) { insert(TableTemplate()) {
searchValue = search searchValue = search
@ -157,6 +153,17 @@ fun Route.workGroup() {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get
val tracks = TrackRepository.all() 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()) { call.respondHtmlTemplate(MainTemplate()) {
menuTemplate { menuTemplate {
this.user = user this.user = user
@ -201,7 +208,6 @@ fun Route.workGroup() {
htmlFor = "track" htmlFor = "track"
+"Track" +"Track"
} }
//div("input-group") {
select( select(
classes = "form-control" classes = "form-control"
) { ) {
@ -220,12 +226,6 @@ fun Route.workGroup() {
} }
} }
} }
/*
a("/tracks", classes = "form-btn") {
i("material-icons") { +"edit" }
}
}
*/
} }
div("form-group") { div("form-group") {
label { label {
@ -294,6 +294,149 @@ fun Route.workGroup() {
+"Resolution" +"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") { div("form-group") {
@ -333,12 +476,37 @@ fun Route.workGroup() {
} }
params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") } params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") }
params["resolution"]?.let { editWorkGroup = editWorkGroup.copy(resolution = 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["length"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(length = it) }
params["language"]?.let { params["language"]?.let {
editWorkGroup = editWorkGroup =
editWorkGroup.copy(language = Language.values().find { l -> l.code == it } ?: Language.GERMAN) 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) WorkGroupRepository.update(editWorkGroup)
call.respondRedirect("/workgroups") call.respondRedirect("/workgroups")
@ -477,6 +645,78 @@ fun Route.workGroup() {
+"Resolution" +"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") { div("form-group") {
@ -511,6 +751,29 @@ fun Route.workGroup() {
Language.values().find { l -> l.code == it } ?: Language.GERMAN 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( val workGroup = WorkGroup(
null, null,
name = name, name = name,
@ -518,8 +781,13 @@ fun Route.workGroup() {
track = track, track = track,
projector = projector, projector = projector,
resolution = resolution, resolution = resolution,
internet = internet,
whiteboard = whiteboard,
blackboard = blackboard,
accessible = accessible,
length = length, length = length,
language = language language = language,
constraints = constraints
) )
WorkGroupRepository.create(workGroup) WorkGroupRepository.create(workGroup)

View file

@ -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)
}
}
}

View file

@ -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, "<br />\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
}

View file

@ -50,4 +50,5 @@ fun Route.pushService() {
TrackRepository.registerPushService() TrackRepository.registerPushService()
UserRepository.registerPushService() UserRepository.registerPushService()
WorkGroupRepository.registerPushService() WorkGroupRepository.registerPushService()
PostRepository.registerPushService()
} }

View file

@ -7,14 +7,14 @@ import kotlinx.html.*
class MenuTemplate() : Template<FlowContent> { class MenuTemplate() : Template<FlowContent> {
var active: Tab = Tab.DASHBOARD var active: Tab = Tab.BOARD
var user: User? = null var user: User? = null
override fun FlowContent.apply() { override fun FlowContent.apply() {
nav("menu") { nav("menu") {
div("container") { div("container") {
div("menu-left") { div("menu-left") {
a("/", classes = if (active == Tab.DASHBOARD) "active" else null) { a("/", classes = if (active == Tab.BOARD) "active" else null) {
+"Dashboard" +"Dashboard"
} }
a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) { a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) {
@ -42,7 +42,7 @@ class MenuTemplate() : Template<FlowContent> {
+"Rooms" +"Rooms"
} }
} }
if (user.checkPermission(Permission.PERSON)) { if (user.checkPermission(Permission.USER)) {
a("/users", classes = if (active == Tab.USER) "active" else null) { a("/users", classes = if (active == Tab.USER) "active" else null) {
+"Users" +"Users"
} }
@ -58,6 +58,6 @@ class MenuTemplate() : Template<FlowContent> {
} }
enum class Tab { enum class Tab {
DASHBOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER BOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER
} }
} }