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 (
schedule != s &&
leader in s.workGroup.leader &&
start <= s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() <= end
start < s.getAbsoluteEndTime() &&
s.getAbsoluteStartTime() < end
) {
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 {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT
}
object Serialization {

View file

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

View file

@ -2,6 +2,7 @@ package de.kif.frontend
import de.kif.frontend.views.board.initBoard
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.initWorkGroupConstraints
import de.kif.frontend.views.overview.initOverviewMain
@ -34,4 +35,7 @@ fun main() = init {
if (document.getElementsByClassName("board").length > 0) {
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;
}
}
.board-announcement {
position: fixed;
bottom: 3rem;
z-index: 10;
margin-bottom: 0 !important;
}

View file

@ -182,3 +182,20 @@ body.offline {
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()
constraintsApi()
postApi()
announcementApi()
// Web socket push notifications
pushService()

View file

@ -42,6 +42,7 @@ object Configuration {
val sessions by required<String>()
val uploads by required<String>()
val database by required<String>()
val announcement by required<String>()
}
object Path {
@ -56,6 +57,9 @@ object Configuration {
val database by c(PathSpec.database)
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") {

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

View file

@ -1,6 +1,7 @@
package de.kif.backend.route
import de.kif.backend.*
import de.kif.backend.repository.AnnouncementRepository
import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml
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.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.request.receiveParameters
import io.ktor.response.respond
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.*
import java.io.File
@ -75,9 +78,12 @@ fun Route.overview() {
content {
if (editable) {
div("overview-new") {
a("post/new", classes = "form-btn") {
a("$prefix/post/new", classes = "form-btn") {
+"Neuer Eintrag"
}
a("$prefix/announcement", classes = "form-btn") {
+"Ankündigungsbanner bearbeiten"
}
}
}
div("overview") {
@ -545,4 +551,57 @@ fun Route.overview() {
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.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all())
RepositoryType.POST -> backup.copy(posts = PostRepository.all())
else -> backup
}
}

View file

@ -32,7 +32,7 @@ object PushService {
fun getMessages(timestamp: Long?): MessageBox {
return MessageBox(
System.currentTimeMillis(),
true,
timestamp != null && timestamp > leastValidTimestamp,
emptyList()
)
}
@ -64,4 +64,5 @@ fun Route.pushService() {
UserRepository.registerPushService()
WorkGroupRepository.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.Resources
import de.kif.backend.prefix
import de.kif.backend.repository.AnnouncementRepository
import de.kif.common.model.User
import io.ktor.application.ApplicationCall
import io.ktor.application.call
@ -79,6 +80,19 @@ class MainTemplate(
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"
div(containerClasses) {
div("main") {

View file

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