Add image upload

This commit is contained in:
Lars Westermann 2019-05-30 12:32:40 +02:00
parent 997f374fe4
commit 67f24adfdf
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
24 changed files with 614 additions and 92 deletions

View file

@ -21,17 +21,19 @@ group "de.kif"
version "0.1.0" version "0.1.0"
repositories { repositories {
mavenCentral()
jcenter() jcenter()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://dl.bintray.com/kotlin/ktor" } maven { url "https://dl.bintray.com/kotlin/ktor" }
maven { url "https://dl.bintray.com/jetbrains/markdown" } maven { url "https://dl.bintray.com/jetbrains/markdown" }
maven { url "https://kotlin.bintray.com/kotlinx" } maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" } maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
mavenCentral() maven { url "https://dl.bintray.com/soywiz/soywiz" }
} }
def ktor_version = '1.1.5' def ktor_version = '1.1.5'
def serialization_version = '0.11.0' def serialization_version = '0.11.0'
def observable_version = '0.9.3' def observable_version = '0.9.3'
def klockVersion = "1.4.0"
kotlin { kotlin {
jvm() { jvm() {
@ -58,6 +60,8 @@ kotlin {
dependencies { dependencies {
implementation kotlin('stdlib-common') implementation kotlin('stdlib-common')
implementation "de.westermann:KObserve-metadata:$observable_version" implementation "de.westermann:KObserve-metadata:$observable_version"
implementation "com.soywiz:klock-metadata:$klockVersion"
implementation "com.soywiz:klock-locale-metadata:$klockVersion"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
} }
@ -89,6 +93,8 @@ kotlin {
implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.mindrot:jbcrypt:0.4'
implementation "de.westermann:KObserve-jvm:$observable_version" implementation "de.westermann:KObserve-jvm:$observable_version"
implementation "com.soywiz:klock-jvm:$klockVersion"
implementation "com.soywiz:klock-locale-jvm:$klockVersion"
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT' implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10' implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
@ -110,6 +116,8 @@ kotlin {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
implementation "de.westermann:KObserve-js:$observable_version" implementation "de.westermann:KObserve-js:$observable_version"
implementation "com.soywiz:klock-js:$klockVersion"
implementation "com.soywiz:klock-locale-js:$klockVersion"
} }
} }
jsTest { jsTest {

View file

@ -0,0 +1,11 @@
package de.kif.common
import com.soywiz.klock.DateFormat
import com.soywiz.klock.KlockLocale
import com.soywiz.klock.format
import com.soywiz.klock.locale.german
fun formatDate(unix: Long) =
DateFormat("EEEE, d. MMMM y HH:mm")
.withLocale(KlockLocale.german)
.format(unix)

View file

@ -5,4 +5,7 @@ import de.kif.common.SearchElement
interface Model { interface Model {
val id : Long? val id : Long?
fun createSearch(): SearchElement fun createSearch(): SearchElement
}
val createdAt: Long
val updateAt: Long
}

View file

@ -10,7 +10,10 @@ data class Post(
val name: String, val name: String,
val content: String, val content: String,
val url: String, val url: String,
val pinned: Boolean = false val image: String?,
val pinned: Boolean,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(
@ -22,6 +25,7 @@ data class Post(
companion object { companion object {
private const val chars = "abcdefghijklmnopqrstuvwxyz" private const val chars = "abcdefghijklmnopqrstuvwxyz"
private const val length = 32 private const val length = 32
fun generateUrl() = (0 until length).asSequence() fun generateUrl() = (0 until length).asSequence()
.map { Random.nextInt(chars.length) } .map { Random.nextInt(chars.length) }
.map { chars[it] } .map { chars[it] }

View file

@ -12,7 +12,9 @@ data class Room(
val internet: Boolean, val internet: Boolean,
val whiteboard: Boolean, val whiteboard: Boolean,
val blackboard: Boolean, val blackboard: Boolean,
val accessible: Boolean val accessible: Boolean,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -9,7 +9,9 @@ data class Schedule(
val workGroup: WorkGroup, val workGroup: WorkGroup,
val room: Room, val room: Room,
val day: Int, val day: Int,
val time: Int val time: Int,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -7,7 +7,9 @@ import kotlinx.serialization.Serializable
data class Track( data class Track(
override val id: Long?, override val id: Long?,
val name: String, val name: String,
val color: Color val color: Color,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -8,7 +8,9 @@ data class User(
override val id: Long?, override val id: Long?,
val username: String, val username: String,
val password: String, val password: String,
val permissions: Set<Permission> val permissions: Set<Permission>,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
fun checkPermission(permission: Permission): Boolean { fun checkPermission(permission: Permission): Boolean {

View file

@ -17,7 +17,9 @@ data class WorkGroup(
val accessible: Boolean, val accessible: Boolean,
val length: Int, val length: Int,
val language: Language, val language: Language,
val constraints: List<Constraint> val constraints: List<Constraint>,
override val createdAt: Long = 0,
override val updateAt: Long = 0
) : Model { ) : Model {
override fun createSearch() = SearchElement( override fun createSearch() = SearchElement(

View file

@ -5,9 +5,13 @@ import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.subscribe import de.westermann.kobserve.event.subscribe
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.EventListener import org.w3c.dom.events.EventListener
import org.w3c.dom.get import org.w3c.dom.get
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.get
import kotlin.browser.document import kotlin.browser.document
import kotlin.dom.clear import kotlin.dom.clear
@ -50,6 +54,7 @@ fun initPosts() {
} }
fun initPostEdit() { fun initPostEdit() {
// Content preview
val textArea = document.getElementById("content") as HTMLTextAreaElement val textArea = document.getElementById("content") as HTMLTextAreaElement
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
@ -67,4 +72,45 @@ fun initPostEdit() {
launch { launch {
preview.innerHTML = PostRepository.render(textArea.value) preview.innerHTML = PostRepository.render(textArea.value)
} }
// Image preview
val imageView = document.getElementsByClassName("post-edit-image")[0] as HTMLElement
val uploadButton = document.getElementById("image") as HTMLInputElement
val deleteSwitch = document.getElementById("image-delete") as? HTMLInputElement
var file: File? = null
val original = imageView.style.backgroundImage
fun updateImage() {
val deleteState = deleteSwitch?.checked == true
val f = file
when {
deleteState -> {
imageView.removeAttribute("style")
uploadButton.value = ""
file = null
}
f == null -> imageView.style.backgroundImage = original
else -> {
val reader = FileReader()
reader.onload = {
val dataUrl = it.target.asDynamic().result as String
imageView.style.backgroundImage = "url(\"$dataUrl\")"
Unit
}
reader.readAsDataURL(f)
}
}
}
uploadButton.addEventListener("change", EventListener {
val files = uploadButton.files ?: return@EventListener
file = files[0]
updateImage()
})
deleteSwitch?.addEventListener("change", EventListener {
updateImage()
})
} }

View file

@ -1,5 +1,6 @@
package de.kif.frontend.views.overview package de.kif.frontend.views.overview
import de.kif.common.formatDate
import de.kif.frontend.launch import de.kif.frontend.launch
import de.kif.frontend.repository.PostRepository import de.kif.frontend.repository.PostRepository
import de.westermann.kobserve.event.emit import de.westermann.kobserve.event.emit
@ -25,37 +26,44 @@ class PostView(
} }
private val nameView: Link private val nameView: Link
private val contentView: View private val contentView: HTMLElement
private val footerView: HTMLElement
private val imageView: HTMLElement
private fun reload() { private fun reload() {
launch { launch {
val p = PostRepository.get(postId) ?: return@launch val p = PostRepository.get(postId) ?: return@launch
classList["post-no-image"] = p.image == null
nameView.text = p.name nameView.text = p.name
nameView.target = "/p/${p.url}" nameView.target = "/p/${p.url}"
pinned = p.pinned pinned = p.pinned
contentView.html.innerHTML = PostRepository.htmlByUrl(p.url) if (p.image == null) {
imageView.removeAttribute("style")
} else {
imageView.style.backgroundImage = "url(\"/images/${p.image}\")"
}
contentView.innerHTML = PostRepository.htmlByUrl(p.url)
footerView.innerText = formatDate(p.createdAt)
emit(PostChangeEvent(postId)) emit(PostChangeEvent(postId))
} }
} }
init { init {
val nameHtml = view.getElementsByClassName("post-name")[0] nameView = Link.wrap(view.getByClassOrCreate("post-name"))
nameView = nameHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link().also {
html.appendChild(it.html)
it.classList += "post-name"
}
// val editHtml = view.getElementsByClassName("post-edit")[0] val postColumn = view.getByClassOrCreate<HTMLElement>("post-column")
// editView = editHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link() val postColumnLeft = postColumn.getByClassOrCreate<HTMLElement>("post-column-left")
val postColumnRight = postColumn.getByClassOrCreate<HTMLElement>("post-column-right")
val contentHtml = view.getElementsByClassName("post-content")[0] imageView = postColumnLeft.getByClassOrCreate("post-image", "figure")
contentView = contentHtml?.let { wrap(it as HTMLElement) } ?: wrap(createHtmlView()).also {
html.appendChild(it.html) contentView = postColumnRight.getByClassOrCreate("post-content")
it.classList += "post-content" footerView = postColumnRight.getByClassOrCreate("post-footer")
}
PostRepository.onUpdate { PostRepository.onUpdate {
if (it == postId) { if (it == postId) {
@ -69,7 +77,7 @@ class PostView(
companion object { companion object {
fun create(postId: Long): PostView { fun create(postId: Long): PostView {
val div = document.createElement("div") as HTMLElement val div = createHtmlView<HTMLElement>()
div.classList.add("post") div.classList.add("post")
div.dataset["id"] = postId.toString() div.dataset["id"] = postId.toString()
return PostView(div).also(PostView::reload) return PostView(div).also(PostView::reload)
@ -78,3 +86,16 @@ class PostView(
} }
data class PostChangeEvent(val id: Long) data class PostChangeEvent(val id: Long)
inline fun <reified T : HTMLElement> HTMLElement.getByClassOrCreate(name: String, newTagName: String? = null): T {
val v = this.getElementsByClassName(name)[0] as? T
if (v != null) {
return v
}
val h = createHtmlView<T>(newTagName)
h.classList.add(name)
this.appendChild(h)
return h
}

View file

@ -11,6 +11,7 @@ $background-primary-color: #fff;
$background-secondary-color: #fcfcfc; $background-secondary-color: #fcfcfc;
$text-primary-color: #333; $text-primary-color: #333;
$text-secondary-color: rgba($text-primary-color, 0.5);
$primary-color: #B11D33; $primary-color: #B11D33;
$primary-text-color: #fff; $primary-text-color: #fff;
@ -18,7 +19,9 @@ $primary-text-color: #fff;
$error-color: #D55225; $error-color: #D55225;
$error-text-color: #fff; $error-text-color: #fff;
$border-color: #888; $input-border-color: #888;
$table-border-color: rgba($text-primary-color, 0.1);
$table-header-color: rgba($text-primary-color, 0.06);
$transitionTime: 150ms; $transitionTime: 150ms;
@ -40,6 +43,10 @@ body, html {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
& > *:last-child {
margin-bottom: 1rem;
}
} }
.no-select { .no-select {
@ -128,7 +135,7 @@ a {
} }
&:hover { &:hover {
background-color: rgba($primary-text-color, 0.1); background-color: $table-border-color;
&::after { &::after {
background: $primary-color; background: $primary-color;
@ -165,8 +172,8 @@ a {
position: absolute; position: absolute;
background-color: $background-secondary-color; background-color: $background-secondary-color;
z-index: 5; z-index: 5;
left: 1rem; left: 0;
right: 1rem; right: 0;
&::before { &::before {
content: ''; content: '';
@ -183,7 +190,11 @@ a {
line-height: 3rem; line-height: 3rem;
&:after { &:after {
bottom: 0.2rem; bottom: 0.55rem;
}
&:last-child {
margin-bottom: 0.5rem;
} }
} }
} }
@ -220,7 +231,7 @@ a {
} }
.main { .main {
padding: 0 1rem; //padding: 0 1rem;
} }
.table-layout-search { .table-layout-search {
@ -252,14 +263,14 @@ a {
} }
tr { tr {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px $table-border-color;
&:nth-child(odd) { &:nth-child(odd) {
//background-color: rgba($text-primary-color, 0.01); //background-color: rgba($text-primary-color, 0.01);
} }
&:first-child { &:first-child {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
height: 2.5rem; height: 2.5rem;
line-height: 2.5rem; line-height: 2.5rem;
} }
@ -269,7 +280,7 @@ a {
line-height: 2rem; line-height: 2rem;
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
} }
} }
@ -292,20 +303,20 @@ a {
z-index: 1; z-index: 1;
background: $background-primary-color; background: $background-primary-color;
width: 100%; width: 100%;
border: solid 1px rgba($text-primary-color, 0.1); border: solid 1px $table-border-color;
span { span {
padding: 0 0.5rem; padding: 0 0.5rem;
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
} }
} }
} }
.form-control { .form-control {
border: solid 1px $border-color; border: solid 1px $input-border-color;
outline: none; outline: none;
padding: 0 1rem; padding: 0 1rem;
line-height: 2.5rem; line-height: 2.5rem;
@ -412,7 +423,7 @@ select:-moz-focusring {
} }
.form-btn { .form-btn {
border: solid 1px $border-color; border: solid 1px $input-border-color;
outline: none; outline: none;
padding: 0 1rem; padding: 0 1rem;
line-height: 2rem; line-height: 2rem;
@ -588,7 +599,7 @@ form {
.calendar[data-editable = "true"].edit { .calendar[data-editable = "true"].edit {
.calendar-table { .calendar-table {
width: calc(100% - 16rem); width: calc(100% - 16rem);
border-right: solid 1px rgba($text-primary-color, 0.1); border-right: solid 1px $table-border-color;
} }
.calendar-edit-main { .calendar-edit-main {
@ -608,8 +619,8 @@ form {
display: none; display: none;
z-index: 10; z-index: 10;
border: solid 1px $border-color; border: solid 1px $input-border-color;
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color); box-shadow: 0 0.1rem 0.2rem $primary-text-color;
a { a {
padding: 0.2rem; padding: 0.2rem;
@ -707,14 +718,14 @@ form {
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1); border-left: solid 1px $table-border-color;
position: absolute; position: absolute;
} }
} }
} }
.calendar-row { .calendar-row {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px $table-border-color;
line-height: 3rem; line-height: 3rem;
height: 3rem; height: 3rem;
@ -727,12 +738,12 @@ form {
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1); border-left: solid 1px $table-border-color;
position: absolute; position: absolute;
} }
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
.calendar-entry { .calendar-entry {
@ -749,7 +760,7 @@ form {
width: 6rem; width: 6rem;
left: 0; left: 0;
text-align: center; text-align: center;
border-right: solid 1px rgba($text-primary-color, 0.1); border-right: solid 1px $table-border-color;
} }
.calendar-link { .calendar-link {
@ -780,7 +791,7 @@ form {
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1); border-left: solid 1px $table-border-color;
position: absolute; position: absolute;
} }
} }
@ -800,12 +811,12 @@ form {
width: 100%; width: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1); border-left: solid 1px $table-border-color;
position: absolute; position: absolute;
} }
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
.calendar-entry { .calendar-entry {
@ -826,7 +837,7 @@ form {
} }
&:nth-child(4n + 2) .calendar-cell::before { &:nth-child(4n + 2) .calendar-cell::before {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px $table-border-color;
} }
} }
@ -939,13 +950,13 @@ form {
display: none; display: none;
background: $background-primary-color; background: $background-primary-color;
border: solid 1px rgba($text-primary-color, 0.1); border: solid 1px $table-border-color;
span { span {
padding: 0 0.5rem; padding: 0 0.5rem;
&:hover { &:hover {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
} }
@ -956,11 +967,13 @@ form {
.overview { .overview {
display: flex; display: flex;
flex-direction: column;
} }
.overview-main { .overview-main {
flex-grow: 4; flex-grow: 4;
margin-right: 1rem; margin-right: 1rem;
width: 100%;
} }
.overview-side { .overview-side {
@ -985,6 +998,7 @@ form {
font-size: 1.2rem; font-size: 1.2rem;
color: $primary-color; color: $primary-color;
line-height: 2rem; line-height: 2rem;
padding: 0 1rem;
&:empty::before { &:empty::before {
display: block; display: block;
@ -999,11 +1013,30 @@ form {
.post-edit { .post-edit {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 1rem;
line-height: 2rem; line-height: 2rem;
} }
.post-column {
display: flex;
flex-direction: column;
}
.post-image {
width: 100%;
margin: 0.4rem 0 0;
padding-top: 75%;
background-position: center;
background-size: cover;
}
.post-no-image .post-column-left {
display: none;
}
.post-content { .post-content {
padding: 0 1rem;
h1, h2, h3, h4, h5, h6, p { h1, h2, h3, h4, h5, h6, p {
margin: 0.7rem 0; margin: 0.7rem 0;
padding: 0; padding: 0;
@ -1039,11 +1072,11 @@ form {
} }
td { td {
border-top: solid 1px rgba($text-primary-color, 0.1); border-top: solid 1px $table-border-color;
} }
th { th {
background-color: rgba($text-primary-color, 0.06); background-color: $table-header-color;
} }
td, th { td, th {
@ -1051,6 +1084,14 @@ form {
} }
} }
.post-footer {
color: $text-secondary-color;
padding: 0 1rem;
font-size: 0.8rem;
font-weight: 400;
margin-top: -0.3rem;
}
.post-edit-container { .post-edit-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1060,29 +1101,84 @@ form {
margin-left: 0; margin-left: 0;
} }
.post-edit-image-box {
width: 100%;
display: flex;
margin-top: 0.4rem;
& > div:nth-child(1) {
width: 20%;
}
& > div:nth-child(2) {
width: 80%;
padding-left: 1rem;
}
}
.post-edit-image {
width: 100%;
padding-top: 75%;
border: solid 1px $input-border-color;
margin: 0;
background-color: $background-primary-color;
background-position: center;
background-size: cover;
}
.overview-post {
margin-bottom: 2rem;
&::after {
content: '';
position: absolute;
display: block;
bottom: -1rem;
height: 1px;
left: 0;
right: 0;
background: $table-border-color;
}
}
@media (min-width: 768px) { @media (min-width: 768px) {
.post-column {
padding: 0 1rem;
}
.overview-post::after {
left: 1rem;
right: 1rem;
}
.post-content, .post-footer {
padding: 0;
}
.overview-post {
.post-column {
flex-direction: row;
}
.post-column-left {
width: 30%;
margin-right: 1rem;
}
.post-column-right {
width: 70%;
}
}
}
@media (min-width: 992px) {
.post-edit-container { .post-edit-container {
flex-direction: row; flex-direction: row;
} }
.post-edit-right { .post-edit-right {
margin-left: 2rem; margin-left: 2rem;
} }
} .overview {
flex-direction: row;
/*
.overview-post {
max-height: 20rem;
overflow: hidden;
margin-bottom: 1rem;
&::after {
content: '';
position: absolute;
display: block;
height: 1.5rem;
top: 18.5rem;
width: 100%;
background: linear-gradient(0deg, $background-primary-color, transparent);
} }
} }
*/

View file

@ -34,6 +34,10 @@ fun Application.main() {
files(Configuration.Path.webPath.toFile()) files(Configuration.Path.webPath.toFile())
} }
static("/images") {
files(Configuration.Path.uploadsPath.toFile())
}
// UI routes // UI routes
overview() overview()
calendar() calendar()

View file

@ -71,17 +71,30 @@ object Configuration {
val signKey by c(SecuritySpec.signKey) val signKey by c(SecuritySpec.signKey)
} }
private object GeneralSpec : ConfigSpec("general") {
val allowedUploadExtensions by required<String>("allowed_upload_extensions")
}
object General {
val allowedUploadExtensions by c(GeneralSpec.allowedUploadExtensions)
val allowedUploadExtensionSet by lazy {
allowedUploadExtensions.split(",").map { it.trim().toLowerCase() }.toSet()
}
}
init { init {
var config = Config { var config = Config {
addSpec(ServerSpec) addSpec(ServerSpec)
addSpec(PathSpec) addSpec(PathSpec)
addSpec(ScheduleSpec) addSpec(ScheduleSpec)
addSpec(SecuritySpec) addSpec(SecuritySpec)
addSpec(GeneralSpec)
}.from.toml.resource("portal.toml") }.from.toml.resource("portal.toml")
try { try {
config = config.from.toml.file("portal.toml") config = config.from.toml.file("portal.toml")
} catch (_: FileNotFoundException) { } } catch (_: FileNotFoundException) {
}
this.config = config.from.env() this.config = config.from.env()
.from.systemProperties() .from.systemProperties()

View file

@ -1,8 +1,7 @@
package de.kif.backend package de.kif.backend
import mu.KotlinLogging import mu.KotlinLogging
import java.io.File import java.io.InputStream
import java.net.URI
import java.nio.file.* import java.nio.file.*
/** /**
@ -52,6 +51,18 @@ object Resources {
logger.info { "Successfully extract web content" } logger.info { "Successfully extract web content" }
} }
fun deleteUpload(name: String) {
Files.deleteIfExists(Configuration.Path.uploadsPath.resolve(name))
}
fun existsUpload(name: String): Boolean {
return Files.exists(Configuration.Path.uploadsPath.resolve(name))
}
fun createUpload(name: String, input: InputStream) {
Files.copy(input, Configuration.Path.uploadsPath.resolve(name))
}
/** /**
* List of js modules to be included. * List of js modules to be included.
*/ */

View file

@ -8,6 +8,9 @@ object DbTrack : Table() {
val id = long("id").autoIncrement().primaryKey() val id = long("id").autoIncrement().primaryKey()
val name = varchar("name", 64) val name = varchar("name", 64)
val color = varchar("color", 32) val color = varchar("color", 32)
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }
object DbWorkGroup : Table() { object DbWorkGroup : Table() {
@ -28,6 +31,9 @@ object DbWorkGroup : Table() {
val length = integer("length") val length = integer("length")
val constraints = text("constraints") val constraints = text("constraints")
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }
object DbRoom : Table() { object DbRoom : Table() {
@ -41,6 +47,9 @@ object DbRoom : Table() {
val whiteboard = bool("whiteboard") val whiteboard = bool("whiteboard")
val blackboard = bool("blackboard") val blackboard = bool("blackboard")
val accessible = bool("accessible") val accessible = bool("accessible")
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }
object DbSchedule : Table() { object DbSchedule : Table() {
@ -49,12 +58,18 @@ object DbSchedule : Table() {
val roomId = long("room_id").index() val roomId = long("room_id").index()
val day = integer("day").index() val day = integer("day").index()
val time = integer("time_slot") val time = integer("time_slot")
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }
object DbUser : Table() { object DbUser : Table() {
val id = long("id").autoIncrement().primaryKey() val id = long("id").autoIncrement().primaryKey()
val username = varchar("username", 64).uniqueIndex() val username = varchar("username", 64).uniqueIndex()
val password = varchar("password", 64) val password = varchar("password", 64)
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }
object DbUserPermission : Table() { object DbUserPermission : Table() {
@ -68,5 +83,9 @@ object DbPost : Table() {
val content = text("content") val content = text("content")
val url = varchar("url", 64).uniqueIndex() val url = varchar("url", 64).uniqueIndex()
val image = varchar("image", 64).nullable()
val pinned = bool("pinned") val pinned = bool("pinned")
val createdAt = long("createdAt")
val updatedAt = long("updatedAt")
} }

View file

@ -10,6 +10,7 @@ import de.kif.common.model.Post
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object PostRepository : Repository<Post> { object PostRepository : Repository<Post> {
@ -22,9 +23,13 @@ object PostRepository : Repository<Post> {
val name = row[DbPost.name] val name = row[DbPost.name]
val content = row[DbPost.content] val content = row[DbPost.content]
val url = row[DbPost.url] val url = row[DbPost.url]
val image = row[DbPost.image]
val pinned = row[DbPost.pinned] val pinned = row[DbPost.pinned]
return Post(id, name, content, url, pinned) val createdAt = row[DbPost.createdAt]
val updatedAt = row[DbPost.updatedAt]
return Post(id, name, content, url, image, pinned, createdAt, updatedAt)
} }
override suspend fun get(id: Long): Post? { override suspend fun get(id: Long): Post? {
@ -40,6 +45,8 @@ object PostRepository : Repository<Post> {
} }
} }
val now = Date().time
return dbQuery { return dbQuery {
if (model.pinned) { if (model.pinned) {
DbPost.update({ DbPost.pinned eq true }) { DbPost.update({ DbPost.pinned eq true }) {
@ -51,7 +58,11 @@ object PostRepository : Repository<Post> {
it[name] = model.name it[name] = model.name
it[content] = model.content it[content] = model.content
it[url] = model.url it[url] = model.url
it[image] = model.image
it[pinned] = model.pinned it[pinned] = model.pinned
it[createdAt] = now
it[updatedAt] = now
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!") }[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -71,12 +82,17 @@ object PostRepository : Repository<Post> {
} }
} }
val now = Date().time
dbQuery { dbQuery {
DbPost.update({ DbPost.id eq model.id }) { DbPost.update({ DbPost.id eq model.id }) {
it[name] = model.name it[name] = model.name
it[content] = model.content it[content] = model.content
it[url] = model.url it[url] = model.url
it[image] = model.image
it[pinned] = model.pinned it[pinned] = model.pinned
it[updatedAt] = now
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)
@ -112,7 +128,7 @@ object PostRepository : Repository<Post> {
} }
} }
suspend fun getPinned(): List<Post> { private suspend fun getPinned(): List<Post> {
return dbQuery { return dbQuery {
val result = DbPost.select { DbPost.pinned eq true } val result = DbPost.select { DbPost.pinned eq true }

View file

@ -8,6 +8,7 @@ import de.kif.common.model.Room
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object RoomRepository : Repository<Room> { object RoomRepository : Repository<Room> {
@ -25,7 +26,21 @@ object RoomRepository : Repository<Room> {
val blackboard = row[DbRoom.blackboard] val blackboard = row[DbRoom.blackboard]
val accessible = row[DbRoom.accessible] val accessible = row[DbRoom.accessible]
return Room(id, name, places, projector, internet, whiteboard, blackboard, accessible) val createdAt = row[DbRoom.createdAt]
val updatedAt = row[DbRoom.updatedAt]
return Room(
id,
name,
places,
projector,
internet,
whiteboard,
blackboard,
accessible,
createdAt,
updatedAt
)
} }
override suspend fun get(id: Long): Room? { override suspend fun get(id: Long): Room? {
@ -35,6 +50,8 @@ object RoomRepository : Repository<Room> {
} }
override suspend fun create(model: Room): Long { override suspend fun create(model: Room): Long {
val now = Date().time
return dbQuery { return dbQuery {
val id = DbRoom.insert { val id = DbRoom.insert {
it[name] = model.name it[name] = model.name
@ -44,6 +61,8 @@ object RoomRepository : Repository<Room> {
it[whiteboard] = model.whiteboard it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard it[blackboard] = model.blackboard
it[accessible] = model.accessible it[accessible] = model.accessible
it[createdAt] = now
it[updatedAt] = now
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!") }[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -54,6 +73,9 @@ object RoomRepository : Repository<Room> {
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!")
val now = Date().time
dbQuery { dbQuery {
DbRoom.update({ DbRoom.id eq model.id }) { DbRoom.update({ DbRoom.id eq model.id }) {
it[name] = model.name it[name] = model.name
@ -63,6 +85,7 @@ object RoomRepository : Repository<Room> {
it[whiteboard] = model.whiteboard it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard it[blackboard] = model.blackboard
it[accessible] = model.accessible it[accessible] = model.accessible
it[updatedAt] = now
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -8,6 +8,7 @@ import de.kif.common.model.Schedule
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object ScheduleRepository : Repository<Schedule> { object ScheduleRepository : Repository<Schedule> {
@ -22,12 +23,15 @@ object ScheduleRepository : Repository<Schedule> {
val day = row[DbSchedule.day] val day = row[DbSchedule.day]
val time = row[DbSchedule.time] val time = row[DbSchedule.time]
val createdAt = row[DbSchedule.createdAt]
val updatedAt = row[DbSchedule.updatedAt]
val workGroup = WorkGroupRepository.get(workGroupId) val workGroup = WorkGroupRepository.get(workGroupId)
?: throw IllegalStateException("Work group for schedule does not exist!") ?: throw IllegalStateException("Work group for schedule does not exist!")
val room = RoomRepository.get(roomId) val room = RoomRepository.get(roomId)
?: throw IllegalStateException("Room for schedule does not exist!") ?: throw IllegalStateException("Room for schedule does not exist!")
return Schedule(id, workGroup, room, day, time) return Schedule(id, workGroup, room, day, time, createdAt, updatedAt)
} }
override suspend fun get(id: Long): Schedule? { override suspend fun get(id: Long): Schedule? {
@ -41,12 +45,17 @@ object ScheduleRepository : Repository<Schedule> {
} }
override suspend fun create(model: Schedule): Long { override suspend fun create(model: Schedule): Long {
val now = Date().time
return dbQuery { return dbQuery {
val id = DbSchedule.insert { val id = DbSchedule.insert {
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!") it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!") it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
it[day] = model.day it[day] = model.day
it[time] = model.time it[time] = model.time
it[createdAt] = now
it[updatedAt] = now
}[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!") }[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -57,12 +66,17 @@ object ScheduleRepository : Repository<Schedule> {
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!")
val now = Date().time
dbQuery { dbQuery {
DbSchedule.update({ DbSchedule.id eq model.id }) { DbSchedule.update({ DbSchedule.id eq model.id }) {
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!") it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!") it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
it[day] = model.day it[day] = model.day
it[time] = model.time it[time] = model.time
it[updatedAt] = now
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -11,6 +11,7 @@ import de.kif.common.model.parseColor
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object TrackRepository : Repository<Track> { object TrackRepository : Repository<Track> {
@ -23,7 +24,10 @@ object TrackRepository : Repository<Track> {
val name = row[DbTrack.name] val name = row[DbTrack.name]
val color = row[DbTrack.color].parseColor() val color = row[DbTrack.color].parseColor()
return Track(id, name, color) val createdAt = row[DbTrack.createdAt]
val updatedAt = row[DbTrack.updatedAt]
return Track(id, name, color, createdAt, updatedAt)
} }
override suspend fun get(id: Long): Track? { override suspend fun get(id: Long): Track? {
@ -33,10 +37,15 @@ object TrackRepository : Repository<Track> {
} }
override suspend fun create(model: Track): Long { override suspend fun create(model: Track): Long {
val now = Date().time
return dbQuery { return dbQuery {
val id = DbTrack.insert { val id = DbTrack.insert {
it[name] = model.name it[name] = model.name
it[color] = model.color.toString() it[color] = model.color.toString()
it[createdAt] = now
it[updatedAt] = now
}[DbTrack.id] ?: throw IllegalStateException("Cannot create model!") }[DbTrack.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -47,10 +56,15 @@ object TrackRepository : Repository<Track> {
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!")
val now = Date().time
dbQuery { dbQuery {
DbTrack.update({ DbTrack.id eq model.id }) { DbTrack.update({ DbTrack.id eq model.id }) {
it[name] = model.name it[name] = model.name
it[color] = model.color.toString() it[color] = model.color.toString()
it[updatedAt] = now
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -11,6 +11,7 @@ import de.kif.common.model.User
import de.westermann.kobserve.event.EventHandler import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object UserRepository : Repository<User> { object UserRepository : Repository<User> {
@ -23,11 +24,14 @@ object UserRepository : Repository<User> {
val username = row[DbUser.username] val username = row[DbUser.username]
val password = row[DbUser.password] val password = row[DbUser.password]
val createdAt = row[DbUser.createdAt]
val updatedAt = row[DbUser.updatedAt]
val permissions = DbUserPermission.slice(DbUserPermission.permission).select { val permissions = DbUserPermission.slice(DbUserPermission.permission).select {
DbUserPermission.userId eq id DbUserPermission.userId eq id
}.map { it[DbUserPermission.permission] }.toSet() }.map { it[DbUserPermission.permission] }.toSet()
return User(id, username, password, permissions) return User(id, username, password, permissions, createdAt, updatedAt)
} }
override suspend fun get(id: Long): User? { override suspend fun get(id: Long): User? {
@ -37,10 +41,15 @@ object UserRepository : Repository<User> {
} }
override suspend fun create(model: User): Long { override suspend fun create(model: User): Long {
val now = Date().time
return dbQuery { return dbQuery {
val id = DbUser.insert { val id = DbUser.insert {
it[username] = model.username it[username] = model.username
it[password] = model.password it[password] = model.password
it[createdAt] = now
it[updatedAt] = now
}[DbUser.id] ?: throw IllegalStateException("Cannot create model!") }[DbUser.id] ?: throw IllegalStateException("Cannot create model!")
for (permission in model.permissions) { for (permission in model.permissions) {
@ -58,10 +67,15 @@ object UserRepository : Repository<User> {
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!")
val now = Date().time
dbQuery { dbQuery {
DbUser.update({ DbUser.id eq model.id }) { DbUser.update({ DbUser.id eq model.id }) {
it[username] = model.username it[username] = model.username
it[password] = model.password it[password] = model.password
it[updatedAt] = now
} }
DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id } DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id }

View file

@ -13,6 +13,7 @@ import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.list import kotlinx.serialization.list
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import java.util.Date
object WorkGroupRepository : Repository<WorkGroup> { object WorkGroupRepository : Repository<WorkGroup> {
@ -35,6 +36,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
val language = row[DbWorkGroup.language] val language = row[DbWorkGroup.language]
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val createdAt = row[DbWorkGroup.createdAt]
val updatedAt = row[DbWorkGroup.updatedAt]
val track = trackId?.let { TrackRepository.get(it) } val track = trackId?.let { TrackRepository.get(it) }
return WorkGroup( return WorkGroup(
@ -50,7 +54,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
accessible, accessible,
length, length,
language, language,
constraints constraints,
createdAt,
updatedAt
) )
} }
@ -65,6 +71,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
} }
override suspend fun create(model: WorkGroup): Long { override suspend fun create(model: WorkGroup): Long {
val now = Date().time
return dbQuery { return dbQuery {
val id = DbWorkGroup.insert { val id = DbWorkGroup.insert {
it[name] = model.name it[name] = model.name
@ -79,6 +87,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[length] = model.length it[length] = model.length
it[language] = model.language it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
it[createdAt] = now
it[updatedAt] = now
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!") }[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id) onCreate.emit(id)
@ -89,6 +100,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
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!")
val now = Date().time
dbQuery { dbQuery {
DbWorkGroup.update({ DbWorkGroup.id eq model.id }) { DbWorkGroup.update({ DbWorkGroup.id eq model.id }) {
it[name] = model.name it[name] = model.name
@ -103,6 +117,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[length] = model.length it[length] = model.length
it[language] = model.language it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
it[updatedAt] = now
} }
onUpdate.emit(model.id) onUpdate.emit(model.id)

View file

@ -1,30 +1,39 @@
package de.kif.backend.route package de.kif.backend.route
import de.kif.backend.Configuration
import de.kif.backend.Resources
import de.kif.backend.authenticateOrRedirect import de.kif.backend.authenticateOrRedirect
import de.kif.backend.isAuthenticated import de.kif.backend.isAuthenticated
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.MainTemplate import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate import de.kif.backend.view.MenuTemplate
import de.kif.common.formatDate
import de.kif.common.model.Permission import de.kif.common.model.Permission
import de.kif.common.model.Post import de.kif.common.model.Post
import io.ktor.application.call import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate import io.ktor.html.respondHtmlTemplate
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.request.receiveParameters 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.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
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") { fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post" var classes = "post"
if (additionalClasses.isNotBlank()) { if (additionalClasses.isNotBlank()) {
classes += " $additionalClasses" classes += " $additionalClasses"
} }
if (post.image == null) {
classes += " post-no-image"
}
div(classes) { div(classes) {
attributes["data-id"] = post.id.toString() attributes["data-id"] = post.id.toString()
attributes["data-pinned"] = post.pinned.toString() attributes["data-pinned"] = post.pinned.toString()
@ -37,9 +46,24 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
i("material-icons") { +"edit" } i("material-icons") { +"edit" }
} }
} }
div("post-content") {
unsafe { div("post-column") {
raw(markdownToHtml(post.content)) div("post-column-left") {
figure(classes = "post-image") {
if (post.image != null) {
attributes["style"] = "background-image: url(\"/images/${post.image}\")"
}
}
}
div("post-column-right") {
div("post-content") {
unsafe {
raw(markdownToHtml(post.content))
}
}
div("post-footer") {
+formatDate(post.createdAt)
}
} }
} }
} }
@ -118,7 +142,7 @@ fun Route.overview() {
h1 { +"Edit post" } h1 { +"Edit post" }
div("post-edit-container") { div("post-edit-container") {
div("post-edit-left") { div("post-edit-left") {
form(method = FormMethod.post) { form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
div("form-group") { div("form-group") {
label { label {
htmlFor = "name" htmlFor = "name"
@ -149,6 +173,54 @@ fun Route.overview() {
} }
} }
label {
htmlFor = "image"
+"Image"
}
div("post-edit-image-box") {
div {
figure(classes = "post-edit-image") {
if (editPost.image != null) {
attributes["style"] =
"background-image: url(\"/images/${editPost.image}\")"
}
}
}
div {
div("form-group") {
input(
name = "image",
classes = "form-btn",
type = InputType.file
) {
id = "image"
value = "Upload image"
accept =
Configuration.General
.allowedUploadExtensionSet
.joinToString(",") {
".$it"
}
}
}
div("form-group form-switch") {
input(
name = "image-delete",
classes = "form-control",
type = InputType.checkBox
) {
id = "image-delete"
checked = false
}
label {
htmlFor = "image-delete"
+"Delete image"
}
}
}
}
div("form-switch-group") { div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
@ -208,9 +280,37 @@ fun Route.overview() {
post("/post/{id}") { post("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull() var imageUploadName: String? = null
val params = mutableMapOf<String, String>()
call.receiveMultipart().forEachPart { part ->
val name = part.name ?: return@forEachPart
when (part) {
is PartData.FormItem -> {
params[name] = part.value
}
is PartData.FileItem -> {
val extension = File(part.originalFileName).extension
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
var uploadName = Post.generateUrl() + "." + extension
while (true) {
if (Resources.existsUpload(uploadName)) {
uploadName = Post.generateUrl() + "." + extension
} else {
break
}
}
Resources.createUpload(uploadName, part.streamProvider())
imageUploadName = uploadName
}
}
} }
var post = PostRepository.get(postId) ?: return@post var post = PostRepository.get(postId) ?: return@post
params["name"]?.let { post = post.copy(name = it) } params["name"]?.let { post = post.copy(name = it) }
@ -218,6 +318,28 @@ fun Route.overview() {
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") }
if (params["image-delete"] == "on") {
val currentImage = post.image
if (currentImage != null) {
post = post.copy(image = null)
Resources.deleteUpload(currentImage)
}
val upload = imageUploadName
if (upload != null) {
Resources.deleteUpload(upload)
}
} else {
imageUploadName?.let {
val currentImage = post.image
if (currentImage != null) {
Resources.deleteUpload(currentImage)
}
post = post.copy(image = it)
}
}
PostRepository.update(post) PostRepository.update(post)
call.respondRedirect("/") call.respondRedirect("/")
@ -235,7 +357,7 @@ fun Route.overview() {
h1 { +"Create post" } h1 { +"Create post" }
div("post-edit-container") { div("post-edit-container") {
div("post-edit-left") { div("post-edit-left") {
form(method = FormMethod.post) { form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
div("form-group") { div("form-group") {
label { label {
htmlFor = "name" htmlFor = "name"
@ -265,6 +387,34 @@ fun Route.overview() {
} }
} }
label {
htmlFor = "image"
+"Image"
}
div("post-edit-image-box") {
div {
figure(classes = "post-edit-image") {}
}
div {
div("form-group") {
input(
name = "image",
classes = "form-btn",
type = InputType.file
) {
id = "image"
value = "Upload image"
accept =
Configuration.General
.allowedUploadExtensionSet
.joinToString(",") {
".$it"
}
}
}
}
}
div("form-switch-group") { div("form-switch-group") {
div("form-group form-switch") { div("form-group form-switch") {
input( input(
@ -318,8 +468,34 @@ fun Route.overview() {
post("/post/new") { post("/post/new") {
authenticateOrRedirect(Permission.POST) { user -> authenticateOrRedirect(Permission.POST) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) -> var imageUploadName: String? = null
list.firstOrNull()
val params = mutableMapOf<String, String>()
call.receiveMultipart().forEachPart { part ->
val name = part.name ?: return@forEachPart
when (part) {
is PartData.FormItem -> {
params[name] = part.value
}
is PartData.FileItem -> {
val extension = File(part.originalFileName).extension
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
var uploadName = Post.generateUrl() + "." + extension
while (true) {
if (Resources.existsUpload(uploadName)) {
uploadName = Post.generateUrl() + "." + extension
} else {
break
}
}
Resources.createUpload(uploadName, part.streamProvider())
imageUploadName = uploadName
}
}
} }
val name = params["name"] ?: return@post val name = params["name"] ?: return@post
@ -327,7 +503,7 @@ fun Route.overview() {
val url = params["url"] ?: return@post val url = params["url"] ?: return@post
val pinned = params["pinned"] == "on" val pinned = params["pinned"] == "on"
val post = Post(null, name, content, url, pinned) val post = Post(null, name, content, url, imageUploadName, pinned)
PostRepository.create(post) PostRepository.create(post)

View file

@ -14,3 +14,6 @@ reference = "2019-03-27"
[security] [security]
session_name = "SESSION" session_name = "SESSION"
sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc" sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc"
[general]
allowed_upload_extensions = "png, jpg, jpeg"