Compare commits
No commits in common. "594ac544dda098a421fc48654652a7e1c8d6591d" and "8f8242a97a555bd22e26d9b4b458fdb579acee19" have entirely different histories.
594ac544dd
...
8f8242a97a
32 changed files with 156 additions and 505 deletions
|
@ -83,6 +83,7 @@ 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'
|
||||||
|
|
||||||
|
|
|
@ -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}!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,35 @@
|
||||||
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, ANNOUNCEMENT
|
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ data class SearchElement(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun stringify(): String {
|
fun stringify(): String {
|
||||||
return Serialization.stringify(serializer(), this)
|
return Message.json.stringify(serializer(), this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(data: String): SearchElement {
|
fun parse(data: String): SearchElement {
|
||||||
return Serialization.parse(serializer(), data)
|
return Message.json.parse(serializer(), data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +1,68 @@
|
||||||
package de.kif.frontend
|
package de.kif.frontend
|
||||||
|
|
||||||
import de.kif.common.MessageBox
|
import de.kif.common.Message
|
||||||
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.clearInterval
|
import de.westermann.kwebview.async
|
||||||
import de.westermann.kwebview.createHtmlView
|
import org.w3c.dom.MessageEvent
|
||||||
import de.westermann.kwebview.interval
|
import org.w3c.dom.WebSocket
|
||||||
import kotlinx.serialization.DynamicObjectParser
|
import org.w3c.dom.events.Event
|
||||||
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 var timestamp = body.dataset["timestamp"]?.toLongOrNull() ?: 0L
|
private val useSsl = "https" in window.location.protocol
|
||||||
private var intervalId: Int? = null
|
private val wsProtocol = if (useSsl) "wss" else "ws"
|
||||||
|
private val url = "$wsProtocol://${window.location.host}$prefix/websocket"
|
||||||
|
|
||||||
private fun reload() {
|
private lateinit var ws: WebSocket
|
||||||
val id = intervalId ?: return
|
private var reconnect = false
|
||||||
clearInterval(id)
|
|
||||||
intervalId = null
|
|
||||||
|
|
||||||
window.location.reload()
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
private fun onOpen(event: Event) {
|
||||||
|
console.log("Connected!")
|
||||||
|
if (reconnect) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMessage(messageBox: MessageBox) {
|
private fun onMessage(messageEvent: MessageEvent) {
|
||||||
body.classList.remove("offline")
|
val message = Message.parse(messageEvent.data?.toString() ?: "")
|
||||||
|
|
||||||
if (messageBox.valid) {
|
for (handler in messageHandlers) {
|
||||||
timestamp = messageBox.timestamp
|
if (handler.repository == message.repository) {
|
||||||
|
when (message.type) {
|
||||||
for (message in messageBox.messages) {
|
MessageType.CREATE -> handler.onCreate(message.id)
|
||||||
for (handler in messageHandlers) {
|
MessageType.UPDATE -> handler.onUpdate(message.id)
|
||||||
if (handler.repository == message.repository) {
|
MessageType.DELETE -> handler.onDelete(message.id)
|
||||||
when (message.type) {
|
|
||||||
MessageType.CREATE -> handler.onCreate(message.id)
|
|
||||||
MessageType.UPDATE -> handler.onUpdate(message.id)
|
|
||||||
MessageType.DELETE -> handler.onDelete(message.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
reload()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onError(code: Int) {
|
@Suppress("UNUSED_PARAMETER")
|
||||||
if (!body.classList.contains("offline")) {
|
private fun onClose(event: Event) {
|
||||||
console.log("Offline reason: $code")
|
console.log("Disconnected!")
|
||||||
|
reconnect = true
|
||||||
|
async(1000) {
|
||||||
|
connect()
|
||||||
}
|
}
|
||||||
body.classList.add("offline")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun request() {
|
@Suppress("UNUSED_PARAMETER")
|
||||||
val xmlHttpRequest = XMLHttpRequest()
|
private fun onError(event: Event) {
|
||||||
|
console.log("An error occurred!")
|
||||||
|
}
|
||||||
|
|
||||||
xmlHttpRequest.onreadystatechange = {
|
private fun connect() {
|
||||||
try {
|
ws = WebSocket(url)
|
||||||
if (xmlHttpRequest.readyState == 4.toShort()) {
|
|
||||||
if (xmlHttpRequest.status == 200.toShort() || xmlHttpRequest.status == 304.toShort()) {
|
|
||||||
val json = JSON.parse<JsonResponse>(xmlHttpRequest.responseText)
|
|
||||||
|
|
||||||
if (json.OK) {
|
ws.onopen = this::onOpen
|
||||||
val message = parser.parse(json.data, MessageBox.serializer())
|
ws.onmessage = this::onMessage
|
||||||
onMessage(message)
|
ws.onclose = this::onClose
|
||||||
} else {
|
ws.onerror = this::onError
|
||||||
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(
|
||||||
|
@ -95,14 +71,11 @@ class WebSocketClient {
|
||||||
TrackRepository.handler,
|
TrackRepository.handler,
|
||||||
UserRepository.handler,
|
UserRepository.handler,
|
||||||
WorkGroupRepository.handler,
|
WorkGroupRepository.handler,
|
||||||
PostRepository.handler,
|
PostRepository.handler
|
||||||
AnnouncementRepository.handler
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
intervalId = interval(500) {
|
connect()
|
||||||
request()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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
|
||||||
|
@ -35,7 +34,4 @@ fun main() = init {
|
||||||
if (document.getElementsByClassName("board").length > 0) {
|
if (document.getElementsByClassName("board").length > 0) {
|
||||||
initBoard()
|
initBoard()
|
||||||
}
|
}
|
||||||
if (document.getElementsByClassName("announcement").length > 0) {
|
|
||||||
initAnnouncement()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,10 @@ 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<JsonResponse> { resolve, reject ->
|
val promise = Promise<dynamic> { resolve, reject ->
|
||||||
val xhttp = XMLHttpRequest()
|
val xhttp = XMLHttpRequest()
|
||||||
|
|
||||||
xhttp.onreadystatechange = {
|
xhttp.onreadystatechange = {
|
||||||
|
@ -52,7 +47,7 @@ suspend fun repositoryPost(
|
||||||
url: String,
|
url: String,
|
||||||
data: String? = null
|
data: String? = null
|
||||||
): dynamic {
|
): dynamic {
|
||||||
val promise = Promise<JsonResponse> { resolve, reject ->
|
val promise = Promise<dynamic> { resolve, reject ->
|
||||||
val xhttp = XMLHttpRequest()
|
val xhttp = XMLHttpRequest()
|
||||||
|
|
||||||
xhttp.onreadystatechange = {
|
xhttp.onreadystatechange = {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -26,13 +25,13 @@ object PostRepository : Repository<Post> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Post): Long {
|
override suspend fun create(model: Post): Long {
|
||||||
return repositoryPost("$prefix/api/posts", Serialization.stringify(Post.serializer(), model))
|
return repositoryPost("$prefix/api/posts", Message.json.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}", Serialization.stringify(Post.serializer(), model))
|
repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -26,13 +25,13 @@ object RoomRepository : Repository<Room> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Room): Long {
|
override suspend fun create(model: Room): Long {
|
||||||
return repositoryPost("$prefix/api/rooms", Serialization.stringify(Room.serializer(), model))
|
return repositoryPost("$prefix/api/rooms", Message.json.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}", Serialization.stringify(Room.serializer(), model))
|
repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package de.kif.frontend.repository
|
package de.kif.frontend.repository
|
||||||
|
|
||||||
import de.kif.common.*
|
import de.kif.common.ConstraintMap
|
||||||
|
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
|
||||||
|
@ -28,13 +31,13 @@ object ScheduleRepository : Repository<Schedule> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Schedule): Long {
|
override suspend fun create(model: Schedule): Long {
|
||||||
return repositoryPost("$prefix/api/schedules", Serialization.stringify(Schedule.serializer(), model))
|
return repositoryPost("$prefix/api/schedules", Message.json.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}", Serialization.stringify(Schedule.serializer(), model))
|
repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -26,13 +25,13 @@ object TrackRepository : Repository<Track> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Track): Long {
|
override suspend fun create(model: Track): Long {
|
||||||
return repositoryPost("$prefix/api/tracks", Serialization.stringify(Track.serializer(), model))
|
return repositoryPost("$prefix/api/tracks", Message.json.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}", Serialization.stringify(Track.serializer(), model))
|
repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -26,13 +25,13 @@ object UserRepository : Repository<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: User): Long {
|
override suspend fun create(model: User): Long {
|
||||||
return repositoryPost("$prefix/api/users", Serialization.stringify(User.serializer(), model))
|
return repositoryPost("$prefix/api/users", Message.json.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}", Serialization.stringify(User.serializer(), model))
|
repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -26,13 +25,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: WorkGroup): Long {
|
override suspend fun create(model: WorkGroup): Long {
|
||||||
return repositoryPost("$prefix/api/workgroups", Serialization.stringify(WorkGroup.serializer(), model))
|
return repositoryPost("$prefix/api/workgroups", Message.json.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}", Serialization.stringify(WorkGroup.serializer(), model))
|
repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Long) {
|
override suspend fun delete(id: Long) {
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 var(--shadow-color);
|
box-shadow: 0 0 4px black;
|
||||||
top: -1rem;
|
top: -1rem;
|
||||||
padding-top: 1.2rem;
|
padding-top: 1.2rem;
|
||||||
}
|
}
|
||||||
|
@ -205,10 +205,3 @@
|
||||||
margin-top: -1px !important;
|
margin-top: -1px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-announcement {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 3rem;
|
|
||||||
z-index: 10;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -160,42 +160,3 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ fun Application.main() {
|
||||||
install(ConditionalHeaders)
|
install(ConditionalHeaders)
|
||||||
install(Compression)
|
install(Compression)
|
||||||
install(DataConversion)
|
install(DataConversion)
|
||||||
install(AutoHeadResponse)
|
install(WebSockets)
|
||||||
|
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
exception<Throwable> {
|
exception<Throwable> {
|
||||||
|
@ -90,7 +91,6 @@ fun Application.main() {
|
||||||
workGroupApi()
|
workGroupApi()
|
||||||
constraintsApi()
|
constraintsApi()
|
||||||
postApi()
|
postApi()
|
||||||
announcementApi()
|
|
||||||
|
|
||||||
// Web socket push notifications
|
// Web socket push notifications
|
||||||
pushService()
|
pushService()
|
||||||
|
|
|
@ -42,7 +42,6 @@ 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 {
|
||||||
|
@ -57,9 +56,6 @@ 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") {
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,10 @@ 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.*
|
import de.kif.common.Message
|
||||||
|
import de.kif.common.MessageType
|
||||||
|
import de.kif.common.Repository
|
||||||
|
import de.kif.common.RepositoryType
|
||||||
import de.kif.common.model.Constraint
|
import de.kif.common.model.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
|
||||||
|
@ -33,8 +36,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 = Serialization.parse(String.serializer().list, row[DbWorkGroup.leader])
|
val leader = Message.json.parse(String.serializer().list, row[DbWorkGroup.leader])
|
||||||
val constraints = Serialization.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
|
val constraints = Message.json.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]
|
||||||
|
@ -89,8 +92,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] = Serialization.stringify(String.serializer().list, model.leader)
|
it[leader] = Message.json.stringify(String.serializer().list, model.leader)
|
||||||
it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
|
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||||
|
|
||||||
it[createdAt] = now
|
it[createdAt] = now
|
||||||
it[updatedAt] = now
|
it[updatedAt] = now
|
||||||
|
@ -121,8 +124,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] = Serialization.stringify(String.serializer().list, model.leader)
|
it[leader] = Message.json.stringify(String.serializer().list, model.leader)
|
||||||
it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints)
|
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||||
|
|
||||||
it[updatedAt] = now
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
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
|
||||||
|
@ -89,18 +87,6 @@ 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") {
|
||||||
|
@ -156,7 +142,7 @@ fun Route.board() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("board-logo") {
|
div("board-logo") {
|
||||||
img("KIF 47.0", "$prefix/static/images/logo.svg")
|
img("KIF 47.0", "/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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
@ -14,13 +13,11 @@ 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
|
||||||
|
|
||||||
|
@ -78,12 +75,9 @@ fun Route.overview() {
|
||||||
content {
|
content {
|
||||||
if (editable) {
|
if (editable) {
|
||||||
div("overview-new") {
|
div("overview-new") {
|
||||||
a("$prefix/post/new", classes = "form-btn") {
|
a("post/new", classes = "form-btn") {
|
||||||
+"Neuer Eintrag"
|
+"Neuer Eintrag"
|
||||||
}
|
}
|
||||||
a("$prefix/announcement", classes = "form-btn") {
|
|
||||||
+"Ankündigungsbanner bearbeiten"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("overview") {
|
div("overview") {
|
||||||
|
@ -139,7 +133,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/post/{id}") {
|
get("/post/{id}") {
|
||||||
authenticateOrRedirect(Permission.POST) {
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
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 {
|
||||||
|
@ -297,7 +291,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/post/{id}") {
|
post("/post/{id}") {
|
||||||
authenticateOrRedirect(Permission.POST) {
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
|
||||||
|
|
||||||
var imageUploadName: String? = null
|
var imageUploadName: String? = null
|
||||||
|
@ -367,7 +361,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/post/new") {
|
get("/post/new") {
|
||||||
authenticateOrRedirect(Permission.POST) {
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
respondMain {
|
respondMain {
|
||||||
content {
|
content {
|
||||||
h1 { +"Beitrag erstellen" }
|
h1 { +"Beitrag erstellen" }
|
||||||
|
@ -497,7 +491,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/post/new") {
|
post("/post/new") {
|
||||||
authenticateOrRedirect(Permission.POST) {
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
var imageUploadName: String? = null
|
var imageUploadName: String? = null
|
||||||
|
|
||||||
val params = mutableMapOf<String, String>()
|
val params = mutableMapOf<String, String>()
|
||||||
|
@ -543,7 +537,7 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/post/{id}/delete") {
|
get("/post/{id}/delete") {
|
||||||
authenticateOrRedirect(Permission.POST) {
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
||||||
|
|
||||||
PostRepository.delete(postId)
|
PostRepository.delete(postId)
|
||||||
|
@ -551,57 +545,4 @@ 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/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import de.kif.backend.prefix
|
||||||
fun Route.room() {
|
fun Route.room() {
|
||||||
|
|
||||||
get("/rooms") {
|
get("/rooms") {
|
||||||
authenticateOrRedirect(Permission.ROOM) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.ROOM) { user ->
|
||||||
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
||||||
|
|
||||||
RoomRepository.delete(roomId)
|
RoomRepository.delete(roomId)
|
||||||
|
|
|
@ -83,7 +83,7 @@ fun DIV.colorPicker(color: Color?) {
|
||||||
|
|
||||||
fun Route.track() {
|
fun Route.track() {
|
||||||
get("/tracks") {
|
get("/tracks") {
|
||||||
authenticateOrRedirect(Permission.WORK_GROUP) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
||||||
|
|
||||||
TrackRepository.delete(trackId)
|
TrackRepository.delete(trackId)
|
||||||
|
|
|
@ -26,8 +26,8 @@ import kotlin.collections.component2
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
fun Route.user() {
|
fun Route.user() {
|
||||||
get("/users") {
|
get("/users") { param ->
|
||||||
authenticateOrRedirect(Permission.USER) {
|
authenticateOrRedirect(Permission.USER) { user ->
|
||||||
val search = call.parameters["search"] ?: ""
|
val search = call.parameters["search"] ?: ""
|
||||||
val list = UserRepository.all()
|
val list = UserRepository.all()
|
||||||
respondMain {
|
respondMain {
|
||||||
|
|
|
@ -25,7 +25,7 @@ private const val separator = "###"
|
||||||
|
|
||||||
fun Route.workGroup() {
|
fun Route.workGroup() {
|
||||||
get("workgroups") {
|
get("workgroups") {
|
||||||
authenticateOrRedirect(Permission.WORK_GROUP) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
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) {
|
authenticateOrRedirect(Permission.WORK_GROUP) { user ->
|
||||||
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
|
||||||
|
|
||||||
WorkGroupRepository.delete(workGroupId)
|
WorkGroupRepository.delete(workGroupId)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ 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
|
||||||
|
|
||||||
|
@ -40,16 +39,15 @@ 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 Serialization.stringify(serializer(), backup)
|
return Message.json.stringify(serializer(), backup)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
suspend fun import(data: String) {
|
suspend fun import(data: String) {
|
||||||
val backup = Serialization.parse(serializer(), data)
|
val backup = Message.json.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") }
|
||||||
|
|
|
@ -1,60 +1,48 @@
|
||||||
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.backend.route.api.error
|
import de.kif.common.*
|
||||||
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.application.call
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.cio.websocket.readText
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.routing.get
|
import io.ktor.websocket.WebSocketServerSession
|
||||||
|
import io.ktor.websocket.webSocket
|
||||||
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
|
|
||||||
object PushService {
|
object PushService {
|
||||||
|
|
||||||
var leastValidTimestamp = System.currentTimeMillis()
|
var clients: List<WebSocketServerSession> = emptyList()
|
||||||
|
|
||||||
/**
|
suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) {
|
||||||
* Save the message with the current timestamp
|
try {
|
||||||
*/
|
val data = Message(type, repository, id).stringify()
|
||||||
fun notify(type: MessageType, repository: RepositoryType, id: Long) {
|
for (client in clients) {
|
||||||
val timestamp = System.currentTimeMillis()
|
client.outgoing.send(Frame.Text(data))
|
||||||
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() {
|
||||||
get("/api/updates") {
|
webSocket("/websocket") {
|
||||||
|
PushService.clients += this
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val timestamp = call.request.queryParameters["timestamp"]?.toLongOrNull()
|
while (true) {
|
||||||
|
val text = (incoming.receive() as Frame.Text).readText()
|
||||||
val messageBox = PushService.getMessages(timestamp)
|
println("onMessage($text)")
|
||||||
|
outgoing.send(Frame.Text(text))
|
||||||
call.success(messageBox)
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: ClosedReceiveChannelException) {
|
||||||
call.error(HttpStatusCode.InternalServerError)
|
PushService.clients -= this
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("onFailure ${closeReason.await()}")
|
||||||
|
e.printStackTrace()
|
||||||
|
PushService.clients -= this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,5 +52,4 @@ fun Route.pushService() {
|
||||||
UserRepository.registerPushService()
|
UserRepository.registerPushService()
|
||||||
WorkGroupRepository.registerPushService()
|
WorkGroupRepository.registerPushService()
|
||||||
PostRepository.registerPushService()
|
PostRepository.registerPushService()
|
||||||
AnnouncementRepository.registerPushService()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -74,25 +73,10 @@ 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") {
|
||||||
|
@ -100,10 +84,6 @@ class MainTemplate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("offline-banner") {
|
|
||||||
span { +"Die Verbindung zum Server ist unterbrochen" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noMenu) {
|
if (!noMenu) {
|
||||||
div("footer") {
|
div("footer") {
|
||||||
div("container") {
|
div("container") {
|
||||||
|
|
|
@ -9,7 +9,6 @@ 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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue