forked from SPC/spc-site
Refactor
This commit is contained in:
parent
bafbf200f2
commit
4cef4ee402
@ -20,13 +20,15 @@ application {
|
||||
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
||||
}
|
||||
|
||||
//tasks.withType<KotlinCompile>{
|
||||
// kotlinOptions{
|
||||
// freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
|
||||
// }
|
||||
//}
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>{
|
||||
kotlinOptions{
|
||||
languageVersion = "1.7"
|
||||
apiVersion = "1.7"
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
|
||||
}
|
||||
}
|
||||
|
||||
val dataforgeVersion by extra("0.6.0-dev-3")
|
||||
val dataforgeVersion by extra("0.6.0-dev-4")
|
||||
val ktorVersion = KScienceVersions.ktorVersion
|
||||
|
||||
dependencies {
|
||||
|
@ -6,7 +6,7 @@ import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.html.respondHtml
|
||||
import io.ktor.server.http.content.resources
|
||||
import io.ktor.server.http.content.files
|
||||
import io.ktor.server.http.content.static
|
||||
import io.ktor.server.netty.Netty
|
||||
import io.ktor.server.plugins.statuspages.StatusPages
|
||||
@ -14,43 +14,48 @@ import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.route
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.css.CssBuilder
|
||||
import kotlinx.html.CommonAttributeGroupFacade
|
||||
import kotlinx.html.style
|
||||
import ru.mipt.plugins.configureTemplating
|
||||
import ru.mipt.spc.magprog.DataSetSiteContext
|
||||
import ru.mipt.spc.magprog.SiteContext
|
||||
import ru.mipt.spc.magprog.DataSetPageContext
|
||||
import ru.mipt.spc.magprog.PageContext
|
||||
import ru.mipt.spc.magprog.magProgPage
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.io.io
|
||||
import space.kscience.dataforge.io.yaml.YamlPlugin
|
||||
import space.kscience.snark.DirectoryDataTree
|
||||
import space.kscience.snark.SnarkPlugin
|
||||
import java.nio.file.Path
|
||||
|
||||
fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
|
||||
style = CssBuilder().block().toString()
|
||||
}
|
||||
|
||||
class AuthenticationException : RuntimeException()
|
||||
class AuthorizationException : RuntimeException()
|
||||
|
||||
internal fun Application.magProgSite(prefix: String = "magprog"){
|
||||
val context = Context("spc-site"){
|
||||
plugin(YamlPlugin)
|
||||
internal fun Application.magProgPage(rootPath: Path, prefix: String = "magprog") {
|
||||
val context = Context("spc-site") {
|
||||
plugin(SnarkPlugin)
|
||||
}
|
||||
|
||||
val io = context.io
|
||||
val directory = javaClass.getResource("/magprog/content")!!.toURI()
|
||||
val content = DirectoryDataTree(io, Path.of(directory))
|
||||
val content = DirectoryDataTree(io, rootPath.resolve("content"))
|
||||
|
||||
|
||||
val magprogSiteContext: SiteContext = DataSetSiteContext(context,"magprog", content)
|
||||
val magprogPageContext: PageContext = DataSetPageContext(context, prefix, content)
|
||||
|
||||
routing {
|
||||
route(prefix){
|
||||
route(prefix) {
|
||||
get {
|
||||
call.respondHtml {
|
||||
with(magprogSiteContext){
|
||||
with(magprogPageContext) {
|
||||
magProgPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
static {
|
||||
resources("magprog/assets")
|
||||
files(rootPath.resolve("assets").toFile())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,12 +64,12 @@ internal fun Application.magProgSite(prefix: String = "magprog"){
|
||||
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
|
||||
magProgSite()
|
||||
magProgPage(rootPath = Path.of(javaClass.getResource("/magprog")!!.toURI()))
|
||||
install(StatusPages) {
|
||||
exception<AuthenticationException> { call, cause ->
|
||||
exception<AuthenticationException> { call, _ ->
|
||||
call.respond(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
exception<AuthorizationException> { call, cause ->
|
||||
exception<AuthorizationException> { call, _ ->
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
}
|
||||
|
@ -19,15 +19,16 @@ import space.kscience.dataforge.meta.toMeta
|
||||
import space.kscience.dataforge.misc.DFInternal
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.snark.DirectoryDataTree.Companion.META_FILE_EXTENSION_KEY
|
||||
import space.kscience.snark.HtmlData
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.isSubtypeOf
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
class DataSetSiteContext(
|
||||
class DataSetPageContext(
|
||||
override val context: Context,
|
||||
val prefix: String,
|
||||
val dataSet: DataSet<Any>,
|
||||
) : SiteContext {
|
||||
) : PageContext {
|
||||
|
||||
override fun resolveResource(name: String): String = "$prefix/$name"
|
||||
|
@ -9,10 +9,11 @@ import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.misc.DFInternal
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.startsWith
|
||||
import space.kscience.snark.HtmlData
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
interface SiteContext: ContextAware {
|
||||
interface PageContext: ContextAware {
|
||||
|
||||
/**
|
||||
* Resolve a resource full path by its name
|
||||
@ -37,12 +38,12 @@ interface SiteContext: ContextAware {
|
||||
}
|
||||
|
||||
@OptIn(DFInternal::class)
|
||||
inline fun <reified T: Any> SiteContext.resolve(name: Name): Data<T>? = resolve(typeOf<T>(), name)
|
||||
inline fun <reified T: Any> PageContext.resolve(name: Name): Data<T>? = resolve(typeOf<T>(), name)
|
||||
|
||||
@OptIn(DFInternal::class)
|
||||
inline fun <reified T:Any> SiteContext.resolveAll(noinline filter: (name: Name, meta: Meta) -> Boolean): DataSet<T> =
|
||||
inline fun <reified T:Any> PageContext.resolveAll(noinline filter: (name: Name, meta: Meta) -> Boolean): DataSet<T> =
|
||||
resolveAll(typeOf<T>(), filter)
|
||||
|
||||
fun SiteContext.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
||||
fun PageContext.findByType(contentType: String, baseName: Name = Name.EMPTY) = resolveAllHtml { name, meta ->
|
||||
name.startsWith(baseName) && meta["content_type"].string == contentType
|
||||
}
|
@ -2,14 +2,19 @@ package ru.mipt.spc.magprog
|
||||
|
||||
import kotlinx.css.*
|
||||
import kotlinx.html.*
|
||||
import ru.mipt.css
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.snark.HtmlData
|
||||
import space.kscience.snark.htmlData
|
||||
import space.kscience.snark.id
|
||||
import space.kscience.snark.order
|
||||
|
||||
class Person(val block: HtmlData) : HtmlData by block {
|
||||
val name: String by meta.string { error("Mentor name is not defined") }
|
||||
val photo: String? by meta.string()
|
||||
}
|
||||
|
||||
context(SiteContext)
|
||||
context(PageContext)
|
||||
private fun FlowContent.personCards(list: List<Person>, prefix: String) {
|
||||
list.forEach { mentor ->
|
||||
section {
|
||||
@ -34,7 +39,7 @@ private fun FlowContent.personCards(list: List<Person>, prefix: String) {
|
||||
}
|
||||
}
|
||||
|
||||
context(SiteContext)
|
||||
context(PageContext)
|
||||
fun FlowContent.mentors() {
|
||||
val mentors = findByType("magprog_mentor").values.map {
|
||||
Person(it)
|
||||
@ -54,7 +59,7 @@ fun FlowContent.mentors() {
|
||||
personCards(mentors,"mentor")
|
||||
}
|
||||
|
||||
context(SiteContext)
|
||||
context(PageContext)
|
||||
fun FlowContent.team() {
|
||||
val team = findByType("magprog_team").values.map { Person(it) }.sortedBy { it.order }
|
||||
|
||||
|
@ -3,6 +3,7 @@ package ru.mipt.spc.magprog
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.css.*
|
||||
import kotlinx.html.*
|
||||
import ru.mipt.css
|
||||
import space.kscience.dataforge.data.await
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.get
|
||||
@ -11,6 +12,9 @@ import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.plus
|
||||
import space.kscience.snark.HtmlData
|
||||
import space.kscience.snark.htmlData
|
||||
import space.kscience.snark.id
|
||||
|
||||
//fun CssBuilder.magProgCss() {
|
||||
// rule(".magprog-body") {
|
||||
@ -66,13 +70,13 @@ private val PROGRAM_PATH: Name = CONTENT_NODE_NAME + "program"
|
||||
private val RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
|
||||
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners"
|
||||
|
||||
context(SiteContext) private fun FlowContent.programSection() {
|
||||
context(PageContext) private fun FlowContent.programSection() {
|
||||
val programBlock = resolveHtml(PROGRAM_PATH)!!
|
||||
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
|
||||
div("inner") {
|
||||
h2 { +"Учебная программа" }
|
||||
htmlData(programBlock)
|
||||
button(classes = "fit btn btn-primary btn-lg") {
|
||||
button(classes = "fit btn btn-secondary") {
|
||||
attributes["data-bs-toggle"] = "collapse"
|
||||
attributes["data-bs-target"] = "#recommended-courses-collapse-text"
|
||||
attributes["aria-expanded"] = "false"
|
||||
@ -81,14 +85,14 @@ context(SiteContext) private fun FlowContent.programSection() {
|
||||
}
|
||||
div("collapse pt-3") {
|
||||
id = "recommended-courses-collapse-text"
|
||||
div("card card-body") {
|
||||
div {
|
||||
htmlData(recommendedBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context(SiteContext) private fun FlowContent.partners() {
|
||||
context(PageContext) private fun FlowContent.partners() {
|
||||
//val partnersData: Meta = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
|
||||
val partnersData: Meta = runBlocking { resolve<Meta>(PARTNERS_PATH)?.await()} ?: Meta.EMPTY
|
||||
div("inner") {
|
||||
@ -113,7 +117,7 @@ context(SiteContext) private fun FlowContent.partners() {
|
||||
}
|
||||
}
|
||||
|
||||
context(SiteContext) fun HTML.magProgPage() {
|
||||
context(PageContext) fun HTML.magProgPage() {
|
||||
val sections = listOf<MagProgSection>(
|
||||
wrapSection(resolveHtml(INTRO_PATH)!!, "intro"),
|
||||
MagProgSection(
|
||||
@ -158,6 +162,10 @@ context(SiteContext) fun HTML.magProgPage() {
|
||||
name = "viewport"
|
||||
content = "width=device-width, initial-scale=1, user-scalable=no"
|
||||
}
|
||||
link {
|
||||
rel = "stylesheet"
|
||||
href = resolveResource("css/bootstrap.min.css")
|
||||
}
|
||||
link {
|
||||
rel = "stylesheet"
|
||||
href = resolveResource("css/main.css")
|
||||
@ -237,6 +245,9 @@ context(SiteContext) fun HTML.magProgPage() {
|
||||
script {
|
||||
src = resolveResource("js/util.js")
|
||||
}
|
||||
script {
|
||||
src = resolveResource("js/bootstrap.min.js")
|
||||
}
|
||||
script {
|
||||
src = resolveResource("js/main.js")
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package ru.mipt.spc.magprog
|
||||
|
||||
import kotlinx.css.CssBuilder
|
||||
import kotlinx.html.CommonAttributeGroupFacade
|
||||
import kotlinx.html.style
|
||||
|
||||
fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
|
||||
style = CssBuilder().block().toString()
|
||||
}
|
@ -13,10 +13,8 @@ import space.kscience.dataforge.names.NameToken
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.plus
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import kotlin.io.path.*
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@ -32,7 +30,10 @@ class DirectoryDataTree(val io: IOPlugin, val path: Path) : DataTree<ByteArray>
|
||||
val meta = envelope.meta.copy {
|
||||
META_FILE_PATH_KEY put filePath.toString()
|
||||
META_FILE_EXTENSION_KEY put filePath.extension
|
||||
//TODO add other file information
|
||||
|
||||
val attributes = filePath.readAttributes<BasicFileAttributes>()
|
||||
META_FILE_UPDATE_TIME_KEY put attributes.lastModifiedTime().toInstant().toString()
|
||||
META_FILE_CREATE_TIME_KEY put attributes.creationTime().toInstant().toString()
|
||||
}
|
||||
return Data(meta){
|
||||
envelope.data?.toByteArray() ?: ByteArray(0)
|
||||
@ -57,6 +58,8 @@ class DirectoryDataTree(val io: IOPlugin, val path: Path) : DataTree<ByteArray>
|
||||
val META_FILE_KEY = "file".asName()
|
||||
val META_FILE_PATH_KEY = META_FILE_KEY + "path"
|
||||
val META_FILE_EXTENSION_KEY = META_FILE_KEY + "extension"
|
||||
val META_FILE_CREATE_TIME_KEY = META_FILE_KEY +"created"
|
||||
val META_FILE_UPDATE_TIME_KEY = META_FILE_KEY +"update"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.spc.magprog
|
||||
package space.kscience.snark
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.html.FlowContent
|
20
src/main/kotlin/space/kscience/snark/SnarkHtmlParser.kt
Normal file
20
src/main/kotlin/space/kscience/snark/SnarkHtmlParser.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package space.kscience.snark
|
||||
|
||||
import io.ktor.http.ContentType
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.unsafe
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
object SnarkHtmlParser:SnarkParser<HtmlFragment> {
|
||||
override val contentType: ContentType = ContentType.Text.Html
|
||||
override val fileExtensions: Set<String> = setOf("html")
|
||||
override val resultType: KType = typeOf<HtmlFragment>()
|
||||
|
||||
override suspend fun parse(bytes: ByteArray, meta: Meta): HtmlFragment = {
|
||||
div{
|
||||
unsafe { +bytes.decodeToString() }
|
||||
}
|
||||
}
|
||||
}
|
@ -8,8 +8,10 @@ import space.kscience.dataforge.io.yaml.YamlPlugin
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.misc.Type
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
|
||||
@ -19,18 +21,20 @@ interface SnarkParser<R : Any> {
|
||||
|
||||
val fileExtensions: Set<String>
|
||||
|
||||
val priority: Int
|
||||
val priority: Int get() = DEFAULT_PRIORITY
|
||||
|
||||
val resultType: KType
|
||||
|
||||
suspend fun parse(bytes: ByteArray): R
|
||||
suspend fun parse(bytes: ByteArray, meta: Meta): R
|
||||
|
||||
companion object {
|
||||
const val TYPE = "snark.parser"
|
||||
const val DEFAULT_PRIORITY = 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(DFExperimental::class)
|
||||
class SnarkPlugin : AbstractPlugin() {
|
||||
val yaml by require(YamlPlugin)
|
||||
val io get() = yaml.io
|
||||
@ -40,13 +44,30 @@ class SnarkPlugin : AbstractPlugin() {
|
||||
private val parsers: Map<Name, SnarkParser<*>> by lazy { context.gather(SnarkParser.TYPE, true) }
|
||||
|
||||
private val parseAction = Action.map<ByteArray, Any> {
|
||||
result { bytes ->
|
||||
parsers.values.filter { parser ->
|
||||
val parser: SnarkParser<*>? = parsers.values.filter { parser ->
|
||||
parser.contentType.toString() == meta["contentType"].string ||
|
||||
meta[DirectoryDataTree.META_FILE_EXTENSION_KEY].string in parser.fileExtensions
|
||||
}.maxByOrNull {
|
||||
it.priority
|
||||
}?.parse(bytes) ?: bytes
|
||||
}
|
||||
|
||||
//ensure that final type is correct
|
||||
if (parser == null) {
|
||||
logger.warn { "The parser is not found for data with meta $meta" }
|
||||
result { it }
|
||||
} else {
|
||||
result(parser.resultType) { bytes ->
|
||||
parser.parse(bytes, meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun content(target: String): Map<Name, Any> {
|
||||
return when(target){
|
||||
SnarkParser.TYPE -> mapOf(
|
||||
"html".asName() to SnarkHtmlParser
|
||||
)
|
||||
else ->super.content(target)
|
||||
}
|
||||
}
|
||||
|
||||
|
7
src/main/resources/magprog/assets/css/bootstrap.min.css
vendored
Normal file
7
src/main/resources/magprog/assets/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/main/resources/magprog/assets/js/bootstrap.min.js
vendored
Normal file
7
src/main/resources/magprog/assets/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -3,6 +3,7 @@ package ru.mipt
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.testing.testApplication
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -10,7 +11,7 @@ class ApplicationTest {
|
||||
@Test
|
||||
fun testRoot() = testApplication {
|
||||
application {
|
||||
magProgSite()
|
||||
magProgPage(rootPath = Path.of(javaClass.getResource("/magprog")!!.toURI()))
|
||||
}
|
||||
client.get("/magprog").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
|
Loading…
Reference in New Issue
Block a user