portal/src/jvmMain/kotlin/de/kif/backend/route/News.kt
2019-06-08 19:39:45 +02:00

552 lines
23 KiB
Kotlin

package de.kif.backend.route
import de.kif.backend.Configuration
import de.kif.backend.Resources
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.isAuthenticated
import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.respondMain
import de.kif.common.formatDateTime
import de.kif.common.model.Permission
import de.kif.common.model.Post
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
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.respond
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import kotlinx.html.*
import java.io.File
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
var classes = "post"
if (additionalClasses.isNotBlank()) {
classes += " $additionalClasses"
}
if (post.image == null) {
classes += " post-no-image"
}
div(classes) {
attributes["data-id"] = post.id.toString()
attributes["data-pinned"] = post.pinned.toString()
a("/p/${post.url}", classes = "post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
div("post-column") {
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") {
+formatDateTime(post.createdAt)
}
}
}
}
}
fun Route.overview() {
get("") {
val user = isAuthenticated(Permission.POST)
val editable = user != null
val postList = PostRepository.all().asReversed()
respondMain { theme ->
content {
div("overview") {
div("overview-main") {
for (post in postList) {
createPost(post, editable, "overview-post")
}
}
div("overview-side") {
if (editable) {
div("overview-new") {
a("post/new", classes = "form-btn") {
+"New"
}
}
}
div("overview-twitter") {
unsafe {
raw("""
<a
class="twitter-timeline"
href="${Configuration.Twitter.timeline}"
data-chrome="transparent"
data-theme="${if (theme.dark) "dark" else "light"}"
data-link-color="${theme.primaryColor}"
data-cards="hidden"
data-lang="de"
data-dnt="true"
>Twitter wall</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
""".trimIndent())
}
}
}
}
}
}
}
get("/p/{url}") {
val user = isAuthenticated(Permission.POST)
val editable = user != null
val post = PostRepository.getByUrl(call.parameters["url"] ?: "")
if (post == null) {
call.respond(HttpStatusCode.NotFound, "Not found")
return@get
}
respondMain {
content {
div("overview") {
createPost(post, editable)
}
}
}
}
get("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editPost = PostRepository.get(postId) ?: return@get
respondMain {
content {
h1 { +"Edit post" }
div("post-edit-container") {
div("post-edit-left") {
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
div("form-group") {
label {
htmlFor = "name"
+"Name"
}
input(
name = "name",
classes = "form-control"
) {
id = "name"
placeholder = "Name"
value = editPost.name
}
}
div("form-group") {
label {
htmlFor = "url"
+"Url"
}
input(
name = "url",
classes = "form-control"
) {
id = "url"
placeholder = "Url"
value = editPost.url
}
}
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-group form-switch") {
input(
name = "pinned",
classes = "form-control",
type = InputType.checkBox
) {
id = "pinned"
checked = editPost.pinned
}
label {
htmlFor = "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") {
label {
htmlFor = "content"
+"Content"
}
textArea(rows = "10", classes = "form-control") {
name = "content"
id = "content"
+editPost.content
}
}
div("form-group") {
a("/") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Save"
}
}
}
a("/post/${editPost.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Delete"
}
}
}
div("post-edit-right post-content") {
}
}
}
}
}
}
post("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
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
params["name"]?.let { post = post.copy(name = it) }
params["url"]?.let { post = post.copy(url = it) }
params["content"]?.let { post = post.copy(content = it) }
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") {
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)
call.respondRedirect("/")
}
}
get("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
respondMain {
content {
h1 { +"Create post" }
div("post-edit-container") {
div("post-edit-left") {
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
div("form-group") {
label {
htmlFor = "name"
+"Name"
}
input(
name = "name",
classes = "form-control"
) {
id = "name"
placeholder = "Name"
value = ""
}
}
div("form-group") {
label {
htmlFor = "url"
+"Url"
}
input(
name = "url",
classes = "form-control"
) {
id = "url"
placeholder = "Url"
value = Post.generateUrl()
}
}
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-group form-switch") {
input(
name = "pinned",
classes = "form-control",
type = InputType.checkBox
) {
id = "pinned"
checked = false
}
label {
htmlFor = "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") {
label {
htmlFor = "content"
+"Content"
}
textArea(rows = "10", classes = "form-control") {
name = "content"
id = "content"
+""
}
}
div("form-group") {
a("/") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Create"
}
}
}
}
div("post-edit-right post-content") {
}
}
}
}
}
}
post("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
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
}
}
}
val name = params["name"] ?: return@post
val content = params["content"] ?: return@post
val url = params["url"] ?: return@post
val pinned = params["pinned"] == "on"
val hideOnProjector = params["hide-on-projector"] == "on"
val post = Post(null, name, content, url, imageUploadName, pinned, hideOnProjector)
PostRepository.create(post)
call.respondRedirect("/")
}
}
get("/post/{id}/delete") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
PostRepository.delete(postId)
call.respondRedirect("/")
}
}
}