Add announcement

This commit is contained in:
Lars Westermann 2019-06-11 13:50:11 +02:00
parent 7ac2e1c208
commit 594ac544dd
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
18 changed files with 285 additions and 8 deletions

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

@ -25,7 +25,7 @@ enum class MessageType {
} }
enum class RepositoryType { enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT
} }
object Serialization { object Serialization {

View file

@ -95,7 +95,8 @@ class WebSocketClient {
TrackRepository.handler, TrackRepository.handler,
UserRepository.handler, UserRepository.handler,
WorkGroupRepository.handler, WorkGroupRepository.handler,
PostRepository.handler PostRepository.handler,
AnnouncementRepository.handler
) )
init { init {

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

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

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

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

@ -182,3 +182,20 @@ body.offline {
display: block; 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

@ -90,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

@ -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") {
@ -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

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

@ -40,6 +40,7 @@ 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
} }
} }

View file

@ -32,7 +32,7 @@ object PushService {
fun getMessages(timestamp: Long?): MessageBox { fun getMessages(timestamp: Long?): MessageBox {
return MessageBox( return MessageBox(
System.currentTimeMillis(), System.currentTimeMillis(),
true, timestamp != null && timestamp > leastValidTimestamp,
emptyList() emptyList()
) )
} }
@ -64,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
@ -79,6 +80,19 @@ class MainTemplate(
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") {

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"