Compare commits

...

3 commits

Author SHA1 Message Date
Lars Westermann
594ac544dd
Add announcement 2019-06-11 13:50:11 +02:00
Lars Westermann
7ac2e1c208
Remove warnings 2019-06-11 10:55:44 +02:00
Lars Westermann
7b7a9b0fc2
Remove websockets 2019-06-11 10:36:12 +02:00
32 changed files with 505 additions and 156 deletions

View file

@ -83,7 +83,6 @@ kotlin {
implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-auth:$ktor_version" implementation "io.ktor:ktor-auth:$ktor_version"
implementation "io.ktor:ktor-server-sessions:$ktor_version" implementation "io.ktor:ktor-server-sessions:$ktor_version"
implementation "io.ktor:ktor-websockets:$ktor_version"
implementation "io.ktor:ktor-jackson:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version"
implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21' implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21'

View file

@ -48,8 +48,8 @@ fun checkConstraints(
if ( if (
schedule != s && schedule != s &&
leader in s.workGroup.leader && leader in s.workGroup.leader &&
start <= s.getAbsoluteEndTime() && start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() <= end s.getAbsoluteStartTime() < end
) { ) {
errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!") errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!")
} }

View file

@ -1,35 +1,38 @@
package de.kif.common package de.kif.common
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
@Serializable
data class MessageBox(
val timestamp: Long,
val valid: Boolean,
val messages: List<Message>
)
@Serializable @Serializable
data class Message( data class Message(
val type: MessageType, val type: MessageType,
val repository: RepositoryType, val repository: RepositoryType,
val id: Long val id: Long
) { )
fun stringify(): String {
return json.stringify(serializer(), this)
}
companion object {
private val jsonContext = SerializersModule {}
val json = Json(context = jsonContext)
fun parse(data: String): Message {
return json.parse(serializer(), data)
}
}
}
enum class MessageType { enum class MessageType {
CREATE, UPDATE, DELETE CREATE, UPDATE, DELETE
} }
enum class RepositoryType { enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT
}
object Serialization {
private val jsonContext = SerializersModule {}
val json = Json(context = jsonContext)
fun <T> stringify(serializer: SerializationStrategy<T>, obj: T) = json.stringify(serializer, obj)
fun <T> parse(serializer: DeserializationStrategy<T>, str: String) = json.parse(serializer, str)
} }

View file

@ -10,12 +10,12 @@ data class SearchElement(
) { ) {
fun stringify(): String { fun stringify(): String {
return Message.json.stringify(serializer(), this) return Serialization.stringify(serializer(), this)
} }
companion object { companion object {
fun parse(data: String): SearchElement { fun parse(data: String): SearchElement {
return Message.json.parse(serializer(), data) return Serialization.parse(serializer(), data)
} }
} }
} }

View file

@ -1,68 +1,92 @@
package de.kif.frontend package de.kif.frontend
import de.kif.common.Message import de.kif.common.MessageBox
import de.kif.common.MessageType import de.kif.common.MessageType
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
import de.kif.frontend.repository.* import de.kif.frontend.repository.*
import de.westermann.kwebview.async import de.westermann.kwebview.clearInterval
import org.w3c.dom.MessageEvent import de.westermann.kwebview.createHtmlView
import org.w3c.dom.WebSocket import de.westermann.kwebview.interval
import org.w3c.dom.events.Event import kotlinx.serialization.DynamicObjectParser
import org.w3c.dom.get
import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document
import kotlin.browser.window import kotlin.browser.window
class WebSocketClient() { class WebSocketClient {
private val prefix = js("prefix") private val prefix = js("prefix")
private val url = "$prefix/api/updates"
private val body = document.body ?: createHtmlView()
private val parser = DynamicObjectParser()
private val useSsl = "https" in window.location.protocol private var timestamp = body.dataset["timestamp"]?.toLongOrNull() ?: 0L
private val wsProtocol = if (useSsl) "wss" else "ws" private var intervalId: Int? = null
private val url = "$wsProtocol://${window.location.host}$prefix/websocket"
private lateinit var ws: WebSocket private fun reload() {
private var reconnect = false val id = intervalId ?: return
clearInterval(id)
intervalId = null
@Suppress("UNUSED_PARAMETER") window.location.reload()
private fun onOpen(event: Event) {
console.log("Connected!")
if (reconnect) {
window.location.reload()
}
} }
private fun onMessage(messageEvent: MessageEvent) { private fun onMessage(messageBox: MessageBox) {
val message = Message.parse(messageEvent.data?.toString() ?: "") body.classList.remove("offline")
for (handler in messageHandlers) { if (messageBox.valid) {
if (handler.repository == message.repository) { timestamp = messageBox.timestamp
when (message.type) {
MessageType.CREATE -> handler.onCreate(message.id) for (message in messageBox.messages) {
MessageType.UPDATE -> handler.onUpdate(message.id) for (handler in messageHandlers) {
MessageType.DELETE -> handler.onDelete(message.id) if (handler.repository == message.repository) {
when (message.type) {
MessageType.CREATE -> handler.onCreate(message.id)
MessageType.UPDATE -> handler.onUpdate(message.id)
MessageType.DELETE -> handler.onDelete(message.id)
}
}
} }
} }
} else {
reload()
} }
} }
@Suppress("UNUSED_PARAMETER") private fun onError(code: Int) {
private fun onClose(event: Event) { if (!body.classList.contains("offline")) {
console.log("Disconnected!") console.log("Offline reason: $code")
reconnect = true
async(1000) {
connect()
} }
body.classList.add("offline")
} }
@Suppress("UNUSED_PARAMETER") private fun request() {
private fun onError(event: Event) { val xmlHttpRequest = XMLHttpRequest()
console.log("An error occurred!")
}
private fun connect() { xmlHttpRequest.onreadystatechange = {
ws = WebSocket(url) try {
if (xmlHttpRequest.readyState == 4.toShort()) {
if (xmlHttpRequest.status == 200.toShort() || xmlHttpRequest.status == 304.toShort()) {
val json = JSON.parse<JsonResponse>(xmlHttpRequest.responseText)
ws.onopen = this::onOpen if (json.OK) {
ws.onmessage = this::onMessage val message = parser.parse(json.data, MessageBox.serializer())
ws.onclose = this::onClose onMessage(message)
ws.onerror = this::onError } else {
onError(-1)
}
} else {
onError(xmlHttpRequest.status.toInt())
}
}
} catch (e: Exception) {
console.error(e)
onError(-2)
}
}
xmlHttpRequest.open("GET", "$url?timestamp=$timestamp", true)
xmlHttpRequest.overrideMimeType("application/json")
xmlHttpRequest.send()
} }
private val messageHandlers: List<MessageHandler> = listOf( private val messageHandlers: List<MessageHandler> = listOf(
@ -71,11 +95,14 @@ class WebSocketClient() {
TrackRepository.handler, TrackRepository.handler,
UserRepository.handler, UserRepository.handler,
WorkGroupRepository.handler, WorkGroupRepository.handler,
PostRepository.handler PostRepository.handler,
AnnouncementRepository.handler
) )
init { init {
connect() intervalId = interval(500) {
request()
}
} }
} }

View file

@ -2,6 +2,7 @@ package de.kif.frontend
import de.kif.frontend.views.board.initBoard import de.kif.frontend.views.board.initBoard
import de.kif.frontend.views.calendar.initCalendar import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initAnnouncement
import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.table.initTableLayout
import de.kif.frontend.views.initWorkGroupConstraints import de.kif.frontend.views.initWorkGroupConstraints
import de.kif.frontend.views.overview.initOverviewMain import de.kif.frontend.views.overview.initOverviewMain
@ -34,4 +35,7 @@ fun main() = init {
if (document.getElementsByClassName("board").length > 0) { if (document.getElementsByClassName("board").length > 0) {
initBoard() initBoard()
} }
if (document.getElementsByClassName("announcement").length > 0) {
initAnnouncement()
}
} }

View file

@ -8,10 +8,15 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.js.Promise import kotlin.js.Promise
interface JsonResponse {
val OK: Boolean
val data: dynamic
}
suspend fun repositoryGet( suspend fun repositoryGet(
url: String url: String
): dynamic { ): dynamic {
val promise = Promise<dynamic> { resolve, reject -> val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest() val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = { xhttp.onreadystatechange = {
@ -47,7 +52,7 @@ suspend fun repositoryPost(
url: String, url: String,
data: String? = null data: String? = null
): dynamic { ): dynamic {
val promise = Promise<dynamic> { resolve, reject -> val promise = Promise<JsonResponse> { resolve, reject ->
val xhttp = XMLHttpRequest() val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = { xhttp.onreadystatechange = {

View file

@ -0,0 +1,40 @@
package de.kif.frontend.repository
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.Room
import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler
import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.list
import kotlinx.serialization.serializer
object AnnouncementRepository {
private val prefix = js("prefix")
val onUpdate = EventHandler<Unit>()
private val parser = DynamicObjectParser()
suspend fun getAnnouncement(): String {
val json = repositoryGet("$prefix/api/announcement") ?: return ""
return parser.parse(json, String.serializer())
}
suspend fun setAnnouncement(value: String){
return repositoryPost("$prefix/api/announcement", Serialization.stringify(String.serializer(), value))
?: throw IllegalStateException("Cannot set announcement!")
}
val handler = object : MessageHandler(RepositoryType.ROOM) {
override fun onCreate(id: Long) {}
override fun onUpdate(id: Long) = onUpdate.emit(Unit)
override fun onDelete(id: Long) {}
}
}

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
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
import de.kif.common.Serialization
import de.kif.common.model.Post import de.kif.common.model.Post
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object PostRepository : Repository<Post> {
} }
override suspend fun create(model: Post): Long { override suspend fun create(model: Post): Long {
return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model)) return repositoryPost("$prefix/api/posts", Serialization.stringify(Post.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Post) { override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) repositoryPost("$prefix/api/post/${model.id}", Serialization.stringify(Post.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
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
import de.kif.common.Serialization
import de.kif.common.model.Room import de.kif.common.model.Room
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object RoomRepository : Repository<Room> {
} }
override suspend fun create(model: Room): Long { override suspend fun create(model: Room): Long {
return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model)) return repositoryPost("$prefix/api/rooms", Serialization.stringify(Room.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Room) { override suspend fun update(model: Room) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model)) repositoryPost("$prefix/api/room/${model.id}", Serialization.stringify(Room.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -1,9 +1,6 @@
package de.kif.frontend.repository package de.kif.frontend.repository
import de.kif.common.ConstraintMap import de.kif.common.*
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Schedule import de.kif.common.model.Schedule
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -31,13 +28,13 @@ object ScheduleRepository : Repository<Schedule> {
} }
override suspend fun create(model: Schedule): Long { override suspend fun create(model: Schedule): Long {
return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model)) return repositoryPost("$prefix/api/schedules", Serialization.stringify(Schedule.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Schedule) { override suspend fun update(model: Schedule) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model)) repositoryPost("$prefix/api/schedule/${model.id}", Serialization.stringify(Schedule.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
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
import de.kif.common.Serialization
import de.kif.common.model.Track import de.kif.common.model.Track
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object TrackRepository : Repository<Track> {
} }
override suspend fun create(model: Track): Long { override suspend fun create(model: Track): Long {
return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model)) return repositoryPost("$prefix/api/tracks", Serialization.stringify(Track.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: Track) { override suspend fun update(model: Track) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model)) repositoryPost("$prefix/api/track/${model.id}", Serialization.stringify(Track.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
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
import de.kif.common.Serialization
import de.kif.common.model.User import de.kif.common.model.User
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object UserRepository : Repository<User> {
} }
override suspend fun create(model: User): Long { override suspend fun create(model: User): Long {
return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model)) return repositoryPost("$prefix/api/users", Serialization.stringify(User.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: User) { override suspend fun update(model: User) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model)) repositoryPost("$prefix/api/user/${model.id}", Serialization.stringify(User.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -3,6 +3,7 @@ package de.kif.frontend.repository
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
import de.kif.common.Serialization
import de.kif.common.model.WorkGroup import de.kif.common.model.WorkGroup
import de.kif.frontend.MessageHandler import de.kif.frontend.MessageHandler
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
@ -25,13 +26,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
} }
override suspend fun create(model: WorkGroup): Long { override suspend fun create(model: WorkGroup): Long {
return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model)) return repositoryPost("$prefix/api/workgroups", Serialization.stringify(WorkGroup.serializer(), model))
?: throw IllegalStateException("Cannot create model!") ?: throw IllegalStateException("Cannot create model!")
} }
override suspend fun update(model: WorkGroup) { override suspend fun update(model: WorkGroup) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model)) repositoryPost("$prefix/api/workgroup/${model.id}", Serialization.stringify(WorkGroup.serializer(), model))
} }
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {

View file

@ -0,0 +1,21 @@
package de.kif.frontend.views
import de.kif.frontend.launch
import de.kif.frontend.repository.AnnouncementRepository
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
import kotlin.browser.document
fun initAnnouncement() {
val announcement = document.getElementsByClassName("announcement")[0] as? HTMLElement ?: return
val span = announcement.children[0] as? HTMLElement ?: return
AnnouncementRepository.onUpdate {
launch {
val text = AnnouncementRepository.getAnnouncement()
announcement.classList.toggle("announcement-blank", text.isBlank())
span.textContent = text
}
}
}

View file

@ -58,7 +58,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
border-bottom: solid 1px var(--table-border-color); border-bottom: solid 1px var(--table-border-color);
box-shadow: 0 0 4px black; box-shadow: 0 0 4px var(--shadow-color);
top: -1rem; top: -1rem;
padding-top: 1.2rem; padding-top: 1.2rem;
} }
@ -205,3 +205,10 @@
margin-top: -1px !important; margin-top: -1px !important;
} }
} }
.board-announcement {
position: fixed;
bottom: 3rem;
z-index: 10;
margin-bottom: 0 !important;
}

View file

@ -160,3 +160,42 @@ a {
} }
} }
} }
.offline-banner {
position: fixed;
bottom: 3rem;
left: 0;
right: 0;
height: 3rem;
line-height: 3rem;
text-align: center;
font-size: 1.2rem;
background-color: var(--primary-color);
color: var(--primary-text-color);
z-index: 12;
box-shadow: 0 1px 4px var(--shadow-color);
display: none;
}
body.offline {
.offline-banner {
display: block;
}
}
.announcement {
height: 3rem;
line-height: 3rem;
text-align: center;
width: 100%;
font-size: 1.2rem;
background-color: var(--primary-color);
color: var(--primary-text-color);
box-shadow: 0 1px 4px var(--shadow-color);
margin-bottom: 1rem;
display: block;
&.announcement-blank {
display: none;
}
}

View file

@ -15,7 +15,6 @@ import io.ktor.jackson.jackson
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.route import io.ktor.routing.route
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.websocket.WebSockets
import org.slf4j.event.Level import org.slf4j.event.Level
import java.nio.file.Paths import java.nio.file.Paths
@ -31,7 +30,7 @@ fun Application.main() {
install(ConditionalHeaders) install(ConditionalHeaders)
install(Compression) install(Compression)
install(DataConversion) install(DataConversion)
install(WebSockets) install(AutoHeadResponse)
install(StatusPages) { install(StatusPages) {
exception<Throwable> { exception<Throwable> {
@ -91,6 +90,7 @@ fun Application.main() {
workGroupApi() workGroupApi()
constraintsApi() constraintsApi()
postApi() postApi()
announcementApi()
// Web socket push notifications // Web socket push notifications
pushService() pushService()

View file

@ -42,6 +42,7 @@ object Configuration {
val sessions by required<String>() val sessions by required<String>()
val uploads by required<String>() val uploads by required<String>()
val database by required<String>() val database by required<String>()
val announcement by required<String>()
} }
object Path { object Path {
@ -56,6 +57,9 @@ object Configuration {
val database by c(PathSpec.database) val database by c(PathSpec.database)
val databasePath: java.nio.file.Path by lazy { Paths.get(database).toAbsolutePath() } val databasePath: java.nio.file.Path by lazy { Paths.get(database).toAbsolutePath() }
val announcement by c(PathSpec.announcement)
val announcementPath: java.nio.file.Path by lazy { Paths.get(announcement).toAbsolutePath() }
} }
private object ScheduleSpec : ConfigSpec("schedule") { private object ScheduleSpec : ConfigSpec("schedule") {

View file

@ -0,0 +1,53 @@
package de.kif.backend.repository
import de.kif.backend.Configuration
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.RepositoryType
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
object AnnouncementRepository {
val onUpdate = EventHandler<Unit>()
private val file = Configuration.Path.announcementPath.toFile()
private var announcement = ""
fun setAnnouncement(value: String) {
val str = value.replace("\n", " ").trim()
if (announcement == str) return
announcement = str
try {
file.writeText(str)
} catch (e: Exception) {
}
onUpdate.emit(Unit)
}
fun getAnnouncement(): String {
return announcement
}
fun registerPushService() {
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.ANNOUNCEMENT, 0L)
}
}
}
init {
try {
if (!file.exists()) {
file.createNewFile()
}
announcement = file.readText().trim()
} catch (e: Exception) {
}
}
}

View file

@ -3,10 +3,7 @@ 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.*
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.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
@ -36,8 +33,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
val accessible = row[DbWorkGroup.accessible] 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 leader = Message.json.parse(String.serializer().list, row[DbWorkGroup.leader]) val leader = Serialization.parse(String.serializer().list, row[DbWorkGroup.leader])
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) val constraints = Serialization.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val createdAt = row[DbWorkGroup.createdAt] val createdAt = row[DbWorkGroup.createdAt]
val updatedAt = row[DbWorkGroup.updatedAt] val updatedAt = row[DbWorkGroup.updatedAt]
@ -92,8 +89,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[accessible] = model.accessible it[accessible] = model.accessible
it[length] = model.length it[length] = model.length
it[language] = model.language it[language] = model.language
it[leader] = Message.json.stringify(String.serializer().list, model.leader) it[leader] = Serialization.stringify(String.serializer().list, model.leader)
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
it[createdAt] = now it[createdAt] = now
it[updatedAt] = now it[updatedAt] = now
@ -124,8 +121,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[accessible] = model.accessible it[accessible] = model.accessible
it[length] = model.length it[length] = model.length
it[language] = model.language it[language] = model.language
it[leader] = Message.json.stringify(String.serializer().list, model.leader) it[leader] = Serialization.stringify(String.serializer().list, model.leader)
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
it[updatedAt] = now it[updatedAt] = now
} }

View file

@ -1,6 +1,8 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.Configuration import de.kif.backend.Configuration
import de.kif.backend.prefix
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
@ -87,6 +89,18 @@ fun Route.board() {
val nowLocale = now.time + timeOffset val nowLocale = now.time + timeOffset
respondMain(true, true) { theme -> respondMain(true, true) { theme ->
content { content {
val announcement = AnnouncementRepository.getAnnouncement()
var classes = "board-announcement announcement"
if (announcement.isBlank()) {
classes += " announcement-blank"
}
div(classes) {
span {
+announcement
}
}
div("board") { div("board") {
div("board-header") { div("board-header") {
div("board-running") { div("board-running") {
@ -142,7 +156,7 @@ fun Route.board() {
} }
} }
div("board-logo") { div("board-logo") {
img("KIF 47.0", "/static/images/logo.svg") img("KIF 47.0", "$prefix/static/images/logo.svg")
div("board-header-date") { div("board-header-date") {
attributes["data-now"] = (now.time - timeOffset).toString() attributes["data-now"] = (now.time - timeOffset).toString()
} }

View file

@ -1,6 +1,7 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.* import de.kif.backend.*
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.PostRepository import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml import de.kif.backend.util.markdownToHtml
import de.kif.backend.view.respondMain import de.kif.backend.view.respondMain
@ -13,11 +14,13 @@ import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart import io.ktor.request.receiveMultipart
import io.ktor.request.receiveParameters
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondRedirect import io.ktor.response.respondRedirect
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.* import kotlinx.html.*
import java.io.File import java.io.File
@ -75,9 +78,12 @@ fun Route.overview() {
content { content {
if (editable) { if (editable) {
div("overview-new") { div("overview-new") {
a("post/new", classes = "form-btn") { a("$prefix/post/new", classes = "form-btn") {
+"Neuer Eintrag" +"Neuer Eintrag"
} }
a("$prefix/announcement", classes = "form-btn") {
+"Ankündigungsbanner bearbeiten"
}
} }
} }
div("overview") { div("overview") {
@ -133,7 +139,7 @@ fun Route.overview() {
} }
get("/post/{id}") { get("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editPost = PostRepository.get(postId) ?: return@get val editPost = PostRepository.get(postId) ?: return@get
respondMain { respondMain {
@ -291,7 +297,7 @@ fun Route.overview() {
} }
post("/post/{id}") { post("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
var imageUploadName: String? = null var imageUploadName: String? = null
@ -361,7 +367,7 @@ fun Route.overview() {
} }
get("/post/new") { get("/post/new") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) {
respondMain { respondMain {
content { content {
h1 { +"Beitrag erstellen" } h1 { +"Beitrag erstellen" }
@ -491,7 +497,7 @@ fun Route.overview() {
} }
post("/post/new") { post("/post/new") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) {
var imageUploadName: String? = null var imageUploadName: String? = null
val params = mutableMapOf<String, String>() val params = mutableMapOf<String, String>()
@ -537,7 +543,7 @@ fun Route.overview() {
} }
get("/post/{id}/delete") { get("/post/{id}/delete") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) {
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
PostRepository.delete(postId) PostRepository.delete(postId)
@ -545,4 +551,57 @@ fun Route.overview() {
call.respondRedirect("$prefix/") call.respondRedirect("$prefix/")
} }
} }
get("/announcement") {
authenticateOrRedirect(Permission.POST) {
respondMain {
content {
h1 { +"Ankündigungsbanner bearbeiten" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "announcement"
+"Ankündigung"
}
input(
name = "announcement",
classes = "form-control"
) {
id = "announcement"
placeholder = "Ankündigung"
value = AnnouncementRepository.getAnnouncement()
}
}
div("form-group") {
a("$prefix/") {
button(classes = "form-btn") {
+"Abbrechen"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Speichern"
}
}
}
}
}
}
}
post("/announcement") {
authenticateOrRedirect(Permission.POST) {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val announcement = params["announcement"] ?: return@post
AnnouncementRepository.setAnnouncement(announcement)
call.respondRedirect("$prefix/")
}
}
} }

View file

@ -29,7 +29,7 @@ import de.kif.backend.prefix
fun Route.room() { fun Route.room() {
get("/rooms") { get("/rooms") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = RoomRepository.all() val list = RoomRepository.all()
@ -106,7 +106,7 @@ fun Route.room() {
} }
get("/room/{id}") { get("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editRoom = RoomRepository.get(roomId) ?: return@get val editRoom = RoomRepository.get(roomId) ?: return@get
respondMain { respondMain {
@ -260,7 +260,7 @@ fun Route.room() {
} }
post("/room/{id}") { post("/room/{id}") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
@ -283,7 +283,7 @@ fun Route.room() {
} }
get("/room/new") { get("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
respondMain { respondMain {
content { content {
h1 { +"Raum erstellen" } h1 { +"Raum erstellen" }
@ -430,7 +430,7 @@ fun Route.room() {
} }
post("/room/new") { post("/room/new") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
} }
@ -453,7 +453,7 @@ fun Route.room() {
} }
get("/room/{id}/delete") { get("/room/{id}/delete") {
authenticateOrRedirect(Permission.ROOM) { user -> authenticateOrRedirect(Permission.ROOM) {
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
RoomRepository.delete(roomId) RoomRepository.delete(roomId)

View file

@ -83,7 +83,7 @@ fun DIV.colorPicker(color: Color?) {
fun Route.track() { fun Route.track() {
get("/tracks") { get("/tracks") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = TrackRepository.all() val list = TrackRepository.all()
respondMain { respondMain {
@ -145,7 +145,7 @@ fun Route.track() {
} }
get("/track/{id}") { get("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editTrack = TrackRepository.get(trackId) ?: return@get val editTrack = TrackRepository.get(trackId) ?: return@get
respondMain { respondMain {
@ -192,7 +192,7 @@ fun Route.track() {
} }
post("/track/{id}") { post("/track/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
@ -212,7 +212,7 @@ fun Route.track() {
} }
get("/track/new") { get("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
respondMain { respondMain {
content { content {
h1 { +"Track erstellen" } h1 { +"Track erstellen" }
@ -251,7 +251,7 @@ fun Route.track() {
} }
post("/track/new") { post("/track/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
} }
@ -270,7 +270,7 @@ fun Route.track() {
} }
get("track/{id}/delete") { get("track/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
TrackRepository.delete(trackId) TrackRepository.delete(trackId)

View file

@ -26,8 +26,8 @@ import kotlin.collections.component2
import kotlin.collections.set import kotlin.collections.set
fun Route.user() { fun Route.user() {
get("/users") { param -> get("/users") {
authenticateOrRedirect(Permission.USER) { user -> authenticateOrRedirect(Permission.USER) {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = UserRepository.all() val list = UserRepository.all()
respondMain { respondMain {

View file

@ -25,7 +25,7 @@ private const val separator = "###"
fun Route.workGroup() { fun Route.workGroup() {
get("workgroups") { get("workgroups") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val search = call.parameters["search"] ?: "" val search = call.parameters["search"] ?: ""
val list = WorkGroupRepository.all() val list = WorkGroupRepository.all()
respondMain { respondMain {
@ -146,7 +146,7 @@ fun Route.workGroup() {
} }
get("/workgroup/{id}") { get("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
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()
@ -532,7 +532,7 @@ fun Route.workGroup() {
} }
post("/workgroup/{id}") { post("/workgroup/{id}") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
@ -574,7 +574,7 @@ fun Route.workGroup() {
} }
get("/workgroup/new") { get("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val tracks = TrackRepository.all() val tracks = TrackRepository.all()
respondMain { respondMain {
content { content {
@ -823,7 +823,7 @@ fun Route.workGroup() {
} }
post("/workgroup/new") { post("/workgroup/new") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val params = call.receiveParameters().toMap().mapValues { (_, list) -> val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() list.firstOrNull()
} }
@ -872,7 +872,7 @@ fun Route.workGroup() {
} }
get("/workgroup/{id}/delete") { get("/workgroup/{id}/delete") {
authenticateOrRedirect(Permission.WORK_GROUP) { user -> authenticateOrRedirect(Permission.WORK_GROUP) {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
WorkGroupRepository.delete(workGroupId) WorkGroupRepository.delete(workGroupId)

View file

@ -0,0 +1,39 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.RoomRepository
import de.kif.common.model.Permission
import de.kif.common.model.Room
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
fun Route.announcementApi() {
get("/api/announcement") {
try {
call.success(AnnouncementRepository.getAnnouncement())
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
post("/api/announcement") {
try {
authenticate(Permission.POST) {
val announcement = call.receive<String>()
AnnouncementRepository.setAnnouncement(announcement)
call.success()
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -4,6 +4,7 @@ import de.kif.backend.database.Connection
import de.kif.backend.repository.* import de.kif.backend.repository.*
import de.kif.common.Message import de.kif.common.Message
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
import de.kif.common.Serialization
import de.kif.common.model.* import de.kif.common.model.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -39,15 +40,16 @@ data class Backup(
RepositoryType.USER -> backup.copy(users = UserRepository.all()) RepositoryType.USER -> backup.copy(users = UserRepository.all())
RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all()) RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all())
RepositoryType.POST -> backup.copy(posts = PostRepository.all()) RepositoryType.POST -> backup.copy(posts = PostRepository.all())
else -> backup
} }
} }
return Message.json.stringify(serializer(), backup) return Serialization.stringify(serializer(), backup)
} }
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
suspend fun import(data: String) { suspend fun import(data: String) {
val backup = Message.json.parse(serializer(), data) val backup = Serialization.parse(serializer(), data)
backup.users.forEach { UserRepository.create(it); println("Import user ${it.username}") } backup.users.forEach { UserRepository.create(it); println("Import user ${it.username}") }
backup.posts.forEach { PostRepository.create(it); println("Import post") } backup.posts.forEach { PostRepository.create(it); println("Import post") }

View file

@ -1,48 +1,60 @@
package de.kif.backend.util package de.kif.backend.util
import de.kif.backend.prefix
import de.kif.backend.repository.* import de.kif.backend.repository.*
import de.kif.common.* import de.kif.backend.route.api.error
import de.kif.backend.route.api.success
import de.kif.common.Message
import de.kif.common.MessageBox
import de.kif.common.MessageType
import de.kif.common.RepositoryType import de.kif.common.RepositoryType
import io.ktor.http.cio.websocket.Frame import io.ktor.application.call
import io.ktor.http.cio.websocket.readText import io.ktor.http.HttpStatusCode
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.websocket.WebSocketServerSession import io.ktor.routing.get
import io.ktor.websocket.webSocket
import kotlinx.coroutines.channels.ClosedReceiveChannelException
object PushService { object PushService {
var clients: List<WebSocketServerSession> = emptyList() var leastValidTimestamp = System.currentTimeMillis()
suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) { /**
try { * Save the message with the current timestamp
val data = Message(type, repository, id).stringify() */
for (client in clients) { fun notify(type: MessageType, repository: RepositoryType, id: Long) {
client.outgoing.send(Frame.Text(data)) val timestamp = System.currentTimeMillis()
} val message = Message(type, repository, id)
} catch (e: Exception) { }
e.printStackTrace()
} /**
* Get all messages created after the given timestamp.
* The return Box has the current timestamp. If the leastValidTimestamp is less then the current timestamp set the
* valid flag to false and return an empty message list.
*/
fun getMessages(timestamp: Long?): MessageBox {
return MessageBox(
System.currentTimeMillis(),
timestamp != null && timestamp > leastValidTimestamp,
emptyList()
)
}
/**
* Delete all messages before the given timestamp
*/
fun gc(timestamp: Long) {
leastValidTimestamp = timestamp
} }
} }
fun Route.pushService() { fun Route.pushService() {
webSocket("/websocket") { get("/api/updates") {
PushService.clients += this
try { try {
while (true) { val timestamp = call.request.queryParameters["timestamp"]?.toLongOrNull()
val text = (incoming.receive() as Frame.Text).readText()
println("onMessage($text)") val messageBox = PushService.getMessages(timestamp)
outgoing.send(Frame.Text(text))
} call.success(messageBox)
} catch (_: ClosedReceiveChannelException) { } catch (_: Exception) {
PushService.clients -= this call.error(HttpStatusCode.InternalServerError)
} catch (e: Throwable) {
println("onFailure ${closeReason.await()}")
e.printStackTrace()
PushService.clients -= this
} }
} }
@ -52,4 +64,5 @@ fun Route.pushService() {
UserRepository.registerPushService() UserRepository.registerPushService()
WorkGroupRepository.registerPushService() WorkGroupRepository.registerPushService()
PostRepository.registerPushService() PostRepository.registerPushService()
AnnouncementRepository.registerPushService()
} }

View file

@ -3,6 +3,7 @@ package de.kif.backend.view
import de.kif.backend.PortalSession import de.kif.backend.PortalSession
import de.kif.backend.Resources import de.kif.backend.Resources
import de.kif.backend.prefix import de.kif.backend.prefix
import de.kif.backend.repository.AnnouncementRepository
import de.kif.common.model.User import de.kif.common.model.User
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
@ -73,10 +74,25 @@ class MainTemplate(
} }
} }
body { body {
attributes["data-timestamp"] = currentTimeMillis().toString()
if (!noMenu) { if (!noMenu) {
insert(MenuTemplate(url, user)) {} insert(MenuTemplate(url, user)) {}
} }
if (!noMenu) {
val announcement = AnnouncementRepository.getAnnouncement()
var classes = "announcement"
if (announcement.isBlank()) {
classes += " announcement-blank"
}
div(classes) {
span {
+announcement
}
}
}
val containerClasses = if (stretch) "container-full" else "container" val containerClasses = if (stretch) "container-full" else "container"
div(containerClasses) { div(containerClasses) {
div("main") { div("main") {
@ -84,6 +100,10 @@ class MainTemplate(
} }
} }
div("offline-banner") {
span { +"Die Verbindung zum Server ist unterbrochen" }
}
if (!noMenu) { if (!noMenu) {
div("footer") { div("footer") {
div("container") { div("container") {

View file

@ -9,6 +9,7 @@ web = "web"
sessions = "data/sessions" sessions = "data/sessions"
uploads = "data/uploads" uploads = "data/uploads"
database = "data/portal.db" database = "data/portal.db"
announcement = "data/announcement.txt"
[schedule] [schedule]
reference = "1970-01-01" reference = "1970-01-01"