Add backup
This commit is contained in:
parent
67f24adfdf
commit
c6620d3395
|
@ -12,6 +12,7 @@ data class Post(
|
||||||
val url: String,
|
val url: String,
|
||||||
val image: String?,
|
val image: String?,
|
||||||
val pinned: Boolean,
|
val pinned: Boolean,
|
||||||
|
val hideOnProjector: Boolean,
|
||||||
override val createdAt: Long = 0,
|
override val createdAt: Long = 0,
|
||||||
override val updateAt: Long = 0
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package de.kif.frontend.views
|
package de.kif.frontend.views
|
||||||
|
|
||||||
|
import de.kif.frontend.iterator
|
||||||
import de.kif.frontend.launch
|
import de.kif.frontend.launch
|
||||||
import de.kif.frontend.repository.WorkGroupRepository
|
import de.kif.frontend.repository.WorkGroupRepository
|
||||||
import de.westermann.kobserve.event.EventListener
|
import de.westermann.kobserve.event.EventListener
|
||||||
|
@ -14,16 +15,12 @@ fun initWorkGroupConstraints() {
|
||||||
var index = 10000
|
var index = 10000
|
||||||
|
|
||||||
val constraints =
|
val constraints =
|
||||||
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints")[0] as HTMLElement)
|
document.getElementsByClassName("work-group-constraints")[0] as HTMLElement
|
||||||
val addButton =
|
val addButton =
|
||||||
View.wrap(document.getElementsByClassName("work-group-constraints-add")[0] as HTMLElement)
|
View.wrap(document.getElementsByClassName("work-group-constraints-add")[0] as HTMLElement)
|
||||||
val addList =
|
val addList =
|
||||||
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints-add-list")[0] as HTMLElement)
|
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints-add-list")[0] as HTMLElement)
|
||||||
|
|
||||||
console.log(constraints.html)
|
|
||||||
console.log(addButton.html)
|
|
||||||
console.log(addList.html)
|
|
||||||
|
|
||||||
addButton.onClick {
|
addButton.onClick {
|
||||||
addList.classList += "active"
|
addList.classList += "active"
|
||||||
|
|
||||||
|
@ -39,9 +36,12 @@ fun initWorkGroupConstraints() {
|
||||||
|
|
||||||
addList.textView("Add only on day") {
|
addList.textView("Add only on day") {
|
||||||
onClick {
|
onClick {
|
||||||
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
||||||
classList += "input-group"
|
classList += "input-group"
|
||||||
html.appendChild(TextView("On day").apply { classList += "form-btn" }.html)
|
html.appendChild(TextView("On day").apply {
|
||||||
|
classList += "form-btn"
|
||||||
|
onClick { this@wrap.html.remove() }
|
||||||
|
}.html)
|
||||||
html.appendChild(InputView(InputType.NUMBER).apply {
|
html.appendChild(InputView(InputType.NUMBER).apply {
|
||||||
classList += "form-control"
|
classList += "form-control"
|
||||||
html.name = "constraint-only-on-day-${index++}"
|
html.name = "constraint-only-on-day-${index++}"
|
||||||
|
@ -53,9 +53,12 @@ fun initWorkGroupConstraints() {
|
||||||
}
|
}
|
||||||
addList.textView("Add only after time") {
|
addList.textView("Add only after time") {
|
||||||
onClick {
|
onClick {
|
||||||
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
||||||
classList += "input-group"
|
classList += "input-group"
|
||||||
html.appendChild(TextView("After time").apply { classList += "form-btn" }.html)
|
html.appendChild(TextView("After time").apply {
|
||||||
|
classList += "form-btn"
|
||||||
|
onClick { this@wrap.html.remove() }
|
||||||
|
}.html)
|
||||||
html.appendChild(InputView(InputType.NUMBER).apply {
|
html.appendChild(InputView(InputType.NUMBER).apply {
|
||||||
classList += "form-control"
|
classList += "form-control"
|
||||||
html.name = "constraint-only-after-time-${index++}"
|
html.name = "constraint-only-after-time-${index++}"
|
||||||
|
@ -67,9 +70,12 @@ fun initWorkGroupConstraints() {
|
||||||
}
|
}
|
||||||
addList.textView("Add not at same time") {
|
addList.textView("Add not at same time") {
|
||||||
onClick {
|
onClick {
|
||||||
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
||||||
classList += "input-group"
|
classList += "input-group"
|
||||||
html.appendChild(TextView("Not with").apply { classList += "form-btn" }.html)
|
html.appendChild(TextView("Not with").apply {
|
||||||
|
classList += "form-btn"
|
||||||
|
onClick { this@wrap.html.remove() }
|
||||||
|
}.html)
|
||||||
|
|
||||||
val select = createHtmlView<HTMLSelectElement>()
|
val select = createHtmlView<HTMLSelectElement>()
|
||||||
select.classList.add("form-control")
|
select.classList.add("form-control")
|
||||||
|
@ -92,9 +98,12 @@ fun initWorkGroupConstraints() {
|
||||||
}
|
}
|
||||||
addList.textView("Add only after work group") {
|
addList.textView("Add only after work group") {
|
||||||
onClick {
|
onClick {
|
||||||
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
constraints.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
|
||||||
classList += "input-group"
|
classList += "input-group"
|
||||||
html.appendChild(TextView("After AK").apply { classList += "form-btn" }.html)
|
html.appendChild(TextView("After AK").apply {
|
||||||
|
classList += "form-btn"
|
||||||
|
onClick { this@wrap.html.remove() }
|
||||||
|
}.html)
|
||||||
|
|
||||||
val select = createHtmlView<HTMLSelectElement>()
|
val select = createHtmlView<HTMLSelectElement>()
|
||||||
select.classList.add("form-control")
|
select.classList.add("form-control")
|
||||||
|
@ -115,4 +124,18 @@ fun initWorkGroupConstraints() {
|
||||||
}.html)
|
}.html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(constraints)
|
||||||
|
for (child in constraints.children.iterator()) {
|
||||||
|
console.log(child)
|
||||||
|
if (child.classList.contains("input-group")) {
|
||||||
|
val span = child.firstElementChild as HTMLElement
|
||||||
|
console.log(span)
|
||||||
|
|
||||||
|
span.addEventListener("click", org.w3c.dom.events.EventListener {
|
||||||
|
println("click")
|
||||||
|
child.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -43,7 +43,7 @@ open class TableLine(line: HTMLElement) : View(line) {
|
||||||
protected fun setupBoolean(view: TextView, onSave: () -> Unit) {
|
protected fun setupBoolean(view: TextView, onSave: () -> Unit) {
|
||||||
view.classList += "no-select"
|
view.classList += "no-select"
|
||||||
view.tabIndex = 0
|
view.tabIndex = 0
|
||||||
view.onDblClick {
|
view.onClick {
|
||||||
onSave()
|
onSave()
|
||||||
}
|
}
|
||||||
view.onKeyDown {
|
view.onKeyDown {
|
||||||
|
|
|
@ -23,6 +23,7 @@ $input-border-color: #888;
|
||||||
$table-border-color: rgba($text-primary-color, 0.1);
|
$table-border-color: rgba($text-primary-color, 0.1);
|
||||||
$table-header-color: rgba($text-primary-color, 0.06);
|
$table-header-color: rgba($text-primary-color, 0.06);
|
||||||
|
|
||||||
|
$border-radius: 0.2rem;
|
||||||
$transitionTime: 150ms;
|
$transitionTime: 150ms;
|
||||||
|
|
||||||
$bg-disabled-color: rgba($text-primary-color, .26);
|
$bg-disabled-color: rgba($text-primary-color, .26);
|
||||||
|
@ -45,7 +46,7 @@ body, html {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
& > *:last-child {
|
& > *:last-child {
|
||||||
margin-bottom: 1rem;
|
//margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +324,7 @@ a {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $background-primary-color;
|
background-color: $background-primary-color;
|
||||||
border-radius: 0.2rem;
|
border-radius: $border-radius;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
transition: border-color $transitionTime;
|
transition: border-color $transitionTime;
|
||||||
|
|
||||||
|
@ -433,7 +434,7 @@ select:-moz-focusring {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 0.6rem;
|
margin-right: 0.6rem;
|
||||||
|
|
||||||
border-radius: 0.2rem;
|
border-radius: $border-radius;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
@ -484,8 +485,14 @@ form {
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
@ -547,7 +554,7 @@ form {
|
||||||
.calendar-work-group {
|
.calendar-work-group {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 0.2rem;
|
border-radius: $border-radius;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -615,7 +622,7 @@ form {
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 0.2rem;
|
border-radius: $border-radius;
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
@ -643,7 +650,7 @@ form {
|
||||||
.calendar-entry {
|
.calendar-entry {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 0.2rem;
|
border-radius: $border-radius;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
@ -934,26 +941,62 @@ form {
|
||||||
|
|
||||||
.work-group-constraints {
|
.work-group-constraints {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
& > label {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 4rem;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
content: 'DELETE';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
color: $primary-text-color;
|
||||||
|
background: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-group-constraints-add {
|
.work-group-constraints-add {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -0.5rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-group-constraints-add-list {
|
.work-group-constraints-add-list {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -2rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: none;
|
display: none;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
background: $background-primary-color;
|
background: $background-primary-color;
|
||||||
border: solid 1px $table-border-color;
|
border: solid 1px $input-border-color;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
display: block;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $table-header-color;
|
background-color: $table-header-color;
|
||||||
|
@ -1022,6 +1065,10 @@ form {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-column-left, .post-column-right {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.post-image {
|
.post-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0.4rem 0 0;
|
margin: 0.4rem 0 0;
|
||||||
|
|
82
src/jvmMain/kotlin/de/kif/backend/backup/Backup.kt
Normal file
82
src/jvmMain/kotlin/de/kif/backend/backup/Backup.kt
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package de.kif.backend.backup
|
||||||
|
|
||||||
|
import de.kif.backend.database.Connection
|
||||||
|
import de.kif.backend.repository.*
|
||||||
|
import de.kif.common.Message
|
||||||
|
import de.kif.common.RepositoryType
|
||||||
|
import de.kif.common.model.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Backup(
|
||||||
|
val posts: List<Post> = emptyList(),
|
||||||
|
val rooms: List<Room> = emptyList(),
|
||||||
|
val schedules: List<Schedule> = emptyList(),
|
||||||
|
val tracks: List<Track> = emptyList(),
|
||||||
|
val users: List<User> = emptyList(),
|
||||||
|
val workGroups: List<WorkGroup> = emptyList()
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
suspend fun backup(vararg repositories: RepositoryType): String {
|
||||||
|
var backup = Backup()
|
||||||
|
|
||||||
|
val repositorySet = repositories.toMutableSet()
|
||||||
|
|
||||||
|
if (RepositoryType.SCHEDULE in repositorySet) {
|
||||||
|
repositorySet += RepositoryType.ROOM
|
||||||
|
repositorySet += RepositoryType.WORK_GROUP
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RepositoryType.WORK_GROUP in repositorySet) {
|
||||||
|
repositorySet += RepositoryType.TRACK
|
||||||
|
}
|
||||||
|
|
||||||
|
for (repository in repositorySet) {
|
||||||
|
backup = when (repository) {
|
||||||
|
RepositoryType.ROOM -> backup.copy(rooms = RoomRepository.all())
|
||||||
|
RepositoryType.SCHEDULE -> backup.copy(schedules = ScheduleRepository.all())
|
||||||
|
RepositoryType.TRACK -> backup.copy(tracks = TrackRepository.all())
|
||||||
|
RepositoryType.USER -> backup.copy(users = UserRepository.all())
|
||||||
|
RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all())
|
||||||
|
RepositoryType.POST -> backup.copy(posts = PostRepository.all())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Message.json.stringify(serializer(), backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
suspend fun import(data: String) {
|
||||||
|
val backup = Message.json.parse(serializer(), data)
|
||||||
|
|
||||||
|
val userMap = backup.users.associateWith { UserRepository.create(it) }
|
||||||
|
val postMap = backup.posts.associateWith { PostRepository.create(it) }
|
||||||
|
|
||||||
|
val roomMap = backup.rooms.associateWith { RoomRepository.create(it) }
|
||||||
|
val trackMap = backup.tracks.associateWith { TrackRepository.create(it) }
|
||||||
|
val workGroupMap = backup.workGroups.associateWith {
|
||||||
|
var workGroup = it
|
||||||
|
val track = workGroup.track
|
||||||
|
if (track != null) {
|
||||||
|
workGroup = workGroup.copy(track = track.copy(id = trackMap[track] ?: return@associateWith -1L))
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkGroupRepository.create(workGroup)
|
||||||
|
}
|
||||||
|
val scheduleMap = backup.schedules.associateWith {
|
||||||
|
ScheduleRepository.create(
|
||||||
|
it.copy(
|
||||||
|
room = it.room.copy(id = roomMap[it.room] ?: return@associateWith -1L),
|
||||||
|
workGroup = it.workGroup.copy(id = workGroupMap[it.workGroup] ?: return@associateWith -1L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restore(data: String) {
|
||||||
|
Connection.reset()
|
||||||
|
|
||||||
|
import(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,28 +3,72 @@ package de.kif.backend.database
|
||||||
import de.kif.backend.Configuration
|
import de.kif.backend.Configuration
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.nio.file.Files
|
||||||
import java.sql.Connection.TRANSACTION_SERIALIZABLE
|
import java.sql.Connection.TRANSACTION_SERIALIZABLE
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
object Connection {
|
object Connection {
|
||||||
fun init() {
|
|
||||||
val dbPath = Configuration.Path.databasePath.toString()
|
|
||||||
Database.connect("jdbc:sqlite:$dbPath", "org.sqlite.JDBC")
|
|
||||||
TransactionManager.manager.defaultIsolationLevel = TRANSACTION_SERIALIZABLE
|
|
||||||
|
|
||||||
transaction {
|
private val logger = KotlinLogging.logger {}
|
||||||
SchemaUtils.create(
|
|
||||||
|
private val schemaList = arrayOf(
|
||||||
DbTrack, DbWorkGroup,
|
DbTrack, DbWorkGroup,
|
||||||
DbRoom, DbSchedule,
|
DbRoom, DbSchedule,
|
||||||
DbUser, DbUserPermission,
|
DbUser, DbUserPermission,
|
||||||
DbPost
|
DbPost
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
val dbPath = Configuration.Path.databasePath.toString()
|
||||||
|
Database.connect("jdbc:sqlite:$dbPath", "org.sqlite.JDBC")
|
||||||
|
TransactionManager.manager.defaultIsolationLevel = TRANSACTION_SERIALIZABLE
|
||||||
|
|
||||||
|
try {
|
||||||
|
create()
|
||||||
|
} catch (e: ExposedSQLException) {
|
||||||
|
logger.error { "Cannot initialize the database!" }
|
||||||
|
|
||||||
|
print("Do you want to recreate the database (you will lose all data)? [y/N]: ")
|
||||||
|
val result = readLine()
|
||||||
|
|
||||||
|
if (result == null || result.toLowerCase() !in "yes") {
|
||||||
|
exitProcess(1)
|
||||||
|
} else {
|
||||||
|
reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun create() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.createMissingTablesAndColumns(*schemaList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delete() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.drop(*schemaList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
delete()
|
||||||
|
|
||||||
|
Configuration.Path.uploadsPath.toFile().deleteRecursively()
|
||||||
|
Configuration.Path.sessionsPath.toFile().deleteRecursively()
|
||||||
|
|
||||||
|
Files.createDirectory(Configuration.Path.uploadsPath)
|
||||||
|
Files.createDirectory(Configuration.Path.sessionsPath)
|
||||||
|
|
||||||
|
create()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> dbQuery(block: () -> T): T = withContext(Dispatchers.IO) {
|
suspend fun <T> dbQuery(block: () -> T): T = withContext(Dispatchers.IO) {
|
||||||
|
|
|
@ -85,6 +85,7 @@ object DbPost : Table() {
|
||||||
val url = varchar("url", 64).uniqueIndex()
|
val url = varchar("url", 64).uniqueIndex()
|
||||||
val image = varchar("image", 64).nullable()
|
val image = varchar("image", 64).nullable()
|
||||||
val pinned = bool("pinned")
|
val pinned = bool("pinned")
|
||||||
|
val hideOnProjector = bool("hideOnProjector")
|
||||||
|
|
||||||
val createdAt = long("createdAt")
|
val createdAt = long("createdAt")
|
||||||
val updatedAt = long("updatedAt")
|
val updatedAt = long("updatedAt")
|
||||||
|
|
|
@ -25,11 +25,12 @@ object PostRepository : Repository<Post> {
|
||||||
val url = row[DbPost.url]
|
val url = row[DbPost.url]
|
||||||
val image = row[DbPost.image]
|
val image = row[DbPost.image]
|
||||||
val pinned = row[DbPost.pinned]
|
val pinned = row[DbPost.pinned]
|
||||||
|
val hideOnProjector = row[DbPost.hideOnProjector]
|
||||||
|
|
||||||
val createdAt = row[DbPost.createdAt]
|
val createdAt = row[DbPost.createdAt]
|
||||||
val updatedAt = row[DbPost.updatedAt]
|
val updatedAt = row[DbPost.updatedAt]
|
||||||
|
|
||||||
return Post(id, name, content, url, image, pinned, createdAt, updatedAt)
|
return Post(id, name, content, url, image, pinned, hideOnProjector, createdAt, updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): Post? {
|
override suspend fun get(id: Long): Post? {
|
||||||
|
@ -60,6 +61,7 @@ object PostRepository : Repository<Post> {
|
||||||
it[url] = model.url
|
it[url] = model.url
|
||||||
it[image] = model.image
|
it[image] = model.image
|
||||||
it[pinned] = model.pinned
|
it[pinned] = model.pinned
|
||||||
|
it[hideOnProjector] = model.hideOnProjector
|
||||||
|
|
||||||
it[createdAt] = now
|
it[createdAt] = now
|
||||||
it[updatedAt] = now
|
it[updatedAt] = now
|
||||||
|
@ -91,6 +93,7 @@ object PostRepository : Repository<Post> {
|
||||||
it[url] = model.url
|
it[url] = model.url
|
||||||
it[image] = model.image
|
it[image] = model.image
|
||||||
it[pinned] = model.pinned
|
it[pinned] = model.pinned
|
||||||
|
it[hideOnProjector] = model.hideOnProjector
|
||||||
|
|
||||||
it[updatedAt] = now
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,26 @@
|
||||||
package de.kif.backend.route
|
package de.kif.backend.route
|
||||||
|
|
||||||
|
import de.kif.backend.authenticate
|
||||||
import de.kif.backend.authenticateOrRedirect
|
import de.kif.backend.authenticateOrRedirect
|
||||||
|
import de.kif.backend.backup.Backup
|
||||||
|
import de.kif.backend.route.api.error
|
||||||
import de.kif.backend.view.MainTemplate
|
import de.kif.backend.view.MainTemplate
|
||||||
import de.kif.backend.view.MenuTemplate
|
import de.kif.backend.view.MenuTemplate
|
||||||
|
import de.kif.common.RepositoryType
|
||||||
|
import de.kif.common.model.Permission
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.html.respondHtmlTemplate
|
import io.ktor.html.respondHtmlTemplate
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
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.response.respondRedirect
|
||||||
|
import io.ktor.response.respondText
|
||||||
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 kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
|
||||||
fun Route.account() {
|
fun Route.account() {
|
||||||
|
@ -30,8 +44,175 @@ fun Route.account() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
if (user.checkPermission(Permission.ROOM)) {
|
||||||
|
a("/account/backup/rooms.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "rooms-backup"
|
||||||
|
+"Create room backup"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (user.checkPermission(Permission.USER)) {
|
||||||
|
a("/account/backup/users.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "users-backup"
|
||||||
|
+"Create user backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user.checkPermission(Permission.POST)) {
|
||||||
|
a("/account/backup/posts.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "posts-backup"
|
||||||
|
+"Create post backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user.checkPermission(Permission.WORK_GROUP)) {
|
||||||
|
a("/account/backup/work-groups.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "work-groups-backup"
|
||||||
|
+"Create work group backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
user.checkPermission(Permission.WORK_GROUP) &&
|
||||||
|
user.checkPermission(Permission.ROOM) &&
|
||||||
|
user.checkPermission(Permission.SCHEDULE)
|
||||||
|
) {
|
||||||
|
a("/account/backup/schedules.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "schedules-backup"
|
||||||
|
+"Create schedule backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.checkPermission(Permission.ADMIN)) {
|
||||||
|
a("/account/backup.json", classes = "form-btn") {
|
||||||
|
attributes["download"] = "backup.json"
|
||||||
|
+"Create backup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
form(
|
||||||
|
action = "/account/import",
|
||||||
|
method = FormMethod.post,
|
||||||
|
encType = FormEncType.multipartFormData
|
||||||
|
) {
|
||||||
|
div("form-group") {
|
||||||
|
label {
|
||||||
|
htmlFor = "backup"
|
||||||
|
+"Backup image"
|
||||||
|
}
|
||||||
|
input(
|
||||||
|
name = "backup",
|
||||||
|
classes = "form-btn",
|
||||||
|
type = InputType.file
|
||||||
|
) {
|
||||||
|
id = "backup"
|
||||||
|
value = "Select backup image"
|
||||||
|
accept = ".json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div("form-switch-group") {
|
||||||
|
div("form-group form-switch") {
|
||||||
|
input(
|
||||||
|
name = "reset",
|
||||||
|
classes = "form-control",
|
||||||
|
type = InputType.checkBox
|
||||||
|
) {
|
||||||
|
id = "reset"
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
htmlFor = "reset"
|
||||||
|
+"Reset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div("form-group") {
|
||||||
|
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
|
||||||
|
+"Import"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup/rooms.json") {
|
||||||
|
authenticate(Permission.ROOM) {
|
||||||
|
call.respondText(Backup.backup(RepositoryType.ROOM), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup/users.json") {
|
||||||
|
authenticate(Permission.USER) {
|
||||||
|
call.respondText(Backup.backup(RepositoryType.USER), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup/posts.json") {
|
||||||
|
authenticate(Permission.POST) {
|
||||||
|
call.respondText(Backup.backup(RepositoryType.POST), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup/work-groups.json") {
|
||||||
|
authenticate(Permission.WORK_GROUP) {
|
||||||
|
call.respondText(Backup.backup(RepositoryType.WORK_GROUP), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup/schedules.json") {
|
||||||
|
authenticate(Permission.ROOM, Permission.WORK_GROUP, Permission.SCHEDULE) {
|
||||||
|
call.respondText(Backup.backup(RepositoryType.SCHEDULE), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/account/backup.json") {
|
||||||
|
authenticate(Permission.ADMIN) {
|
||||||
|
call.respondText(Backup.backup(*RepositoryType.values()), ContentType.Application.Json, HttpStatusCode.OK)
|
||||||
|
} onFailure {
|
||||||
|
call.error(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/account/import") {
|
||||||
|
authenticateOrRedirect(Permission.ADMIN) {
|
||||||
|
var reset = false
|
||||||
|
var import = ""
|
||||||
|
|
||||||
|
call.receiveMultipart().forEachPart { part ->
|
||||||
|
val name = part.name ?: return@forEachPart
|
||||||
|
when (part) {
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
if (name == "reset" && part.value == "on") {
|
||||||
|
reset = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
import = part.streamProvider().bufferedReader().readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
Backup.restore(import)
|
||||||
|
} else {
|
||||||
|
Backup.import(import)
|
||||||
|
}
|
||||||
|
|
||||||
|
call.respondRedirect("/account")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,6 +236,20 @@ fun Route.overview() {
|
||||||
+"Pinned"
|
+"Pinned"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div("form-group form-switch") {
|
||||||
|
input(
|
||||||
|
name = "hide-on-projector",
|
||||||
|
classes = "form-control",
|
||||||
|
type = InputType.checkBox
|
||||||
|
) {
|
||||||
|
id = "hide-on-projector"
|
||||||
|
checked = editPost.hideOnProjector
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
htmlFor = "hide-on-projector"
|
||||||
|
+"Hide on projector"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
|
@ -317,6 +331,7 @@ fun Route.overview() {
|
||||||
params["url"]?.let { post = post.copy(url = it) }
|
params["url"]?.let { post = post.copy(url = it) }
|
||||||
params["content"]?.let { post = post.copy(content = it) }
|
params["content"]?.let { post = post.copy(content = it) }
|
||||||
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
|
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
|
||||||
|
params["hide-on-projector"]?.let { post = post.copy(hideOnProjector = it == "on") }
|
||||||
|
|
||||||
if (params["image-delete"] == "on") {
|
if (params["image-delete"] == "on") {
|
||||||
val currentImage = post.image
|
val currentImage = post.image
|
||||||
|
@ -430,6 +445,20 @@ fun Route.overview() {
|
||||||
+"Pinned"
|
+"Pinned"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div("form-group form-switch") {
|
||||||
|
input(
|
||||||
|
name = "hide-on-projector",
|
||||||
|
classes = "form-control",
|
||||||
|
type = InputType.checkBox
|
||||||
|
) {
|
||||||
|
id = "hide-on-projector"
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
htmlFor = "hide-on-projector"
|
||||||
|
+"Hide on projector"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
|
@ -502,8 +531,9 @@ fun Route.overview() {
|
||||||
val content = params["content"] ?: return@post
|
val content = params["content"] ?: return@post
|
||||||
val url = params["url"] ?: return@post
|
val url = params["url"] ?: return@post
|
||||||
val pinned = params["pinned"] == "on"
|
val pinned = params["pinned"] == "on"
|
||||||
|
val hideOnProjector = params["hide-on-projector"] == "on"
|
||||||
|
|
||||||
val post = Post(null, name, content, url, imageUploadName, pinned)
|
val post = Post(null, name, content, url, imageUploadName, pinned, hideOnProjector)
|
||||||
|
|
||||||
PostRepository.create(post)
|
PostRepository.create(post)
|
||||||
|
|
||||||
|
|
|
@ -354,8 +354,6 @@ fun Route.workGroup() {
|
||||||
+"Accessible"
|
+"Accessible"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div("work-group-constraints")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div("form-group work-group-constraints") {
|
div("form-group work-group-constraints") {
|
||||||
|
|
18
src/jvmMain/kotlin/de/kif/backend/util/LogbackFilter.kt
Normal file
18
src/jvmMain/kotlin/de/kif/backend/util/LogbackFilter.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package de.kif.backend.util
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||||
|
import ch.qos.logback.core.filter.Filter
|
||||||
|
import ch.qos.logback.core.spi.FilterReply
|
||||||
|
|
||||||
|
class LogbackFilter : Filter<ILoggingEvent>() {
|
||||||
|
override fun decide(event: ILoggingEvent?): FilterReply = if (event == null) {
|
||||||
|
FilterReply.NEUTRAL
|
||||||
|
} else {
|
||||||
|
if (event.loggerName.contains("Exposed".toRegex())) {
|
||||||
|
if (event.level.toInt() > Level.ERROR_INT) FilterReply.ACCEPT else FilterReply.DENY
|
||||||
|
} else {
|
||||||
|
FilterReply.ACCEPT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,18 +9,22 @@ import kotlinx.html.*
|
||||||
|
|
||||||
class MainTemplate : Template<HTML> {
|
class MainTemplate : Template<HTML> {
|
||||||
val content = Placeholder<HtmlBlockTag>()
|
val content = Placeholder<HtmlBlockTag>()
|
||||||
val menuTemplate =TemplatePlaceholder<MenuTemplate>()
|
val menuTemplate = TemplatePlaceholder<MenuTemplate>()
|
||||||
|
|
||||||
override fun HTML.apply() {
|
override fun HTML.apply() {
|
||||||
head {
|
head {
|
||||||
meta(charset = "utf-8")
|
meta(charset = "utf-8")
|
||||||
meta(name="viewport",content = "width=device-width, initial-scale=1.0")
|
meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
|
||||||
|
|
||||||
title("KIF Portal")
|
title("KIF Portal")
|
||||||
|
|
||||||
link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
||||||
link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
||||||
link(href = "https://fonts.googleapis.com/css?family=Bungee|Oswald", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
link(
|
||||||
|
href = "https://fonts.googleapis.com/css?family=Bungee|Oswald",
|
||||||
|
type = LinkType.textCss,
|
||||||
|
rel = LinkRel.stylesheet
|
||||||
|
)
|
||||||
link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
link(href = "/static/style/style.css", type = LinkType.textCss, rel = LinkRel.stylesheet)
|
||||||
|
|
||||||
script(src = "/static/require.min.js") {}
|
script(src = "/static/require.min.js") {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<configuration debug="false">
|
<configuration debug="false">
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
|
||||||
<filter class="de.westermann.robots.server.util.LogFilter" />
|
<filter class="de.kif.backend.util.LogbackFilter" />
|
||||||
|
|
||||||
<withJansi>true</withJansi>
|
<withJansi>true</withJansi>
|
||||||
<encoder>
|
<encoder>
|
||||||
|
|
Loading…
Reference in a new issue