1
0
forked from SPC/spc-site
This commit is contained in:
Alexander Nozik 2022-05-02 09:49:18 +03:00
parent bafbf200f2
commit 4cef4ee402
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
14 changed files with 136 additions and 61 deletions

View File

@ -20,13 +20,15 @@ application {
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
} }
//tasks.withType<KotlinCompile>{ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>{
// kotlinOptions{ kotlinOptions{
// freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers" 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 val ktorVersion = KScienceVersions.ktorVersion
dependencies { dependencies {

View File

@ -6,7 +6,7 @@ import io.ktor.server.application.call
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.html.respondHtml 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.http.content.static
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.server.plugins.statuspages.StatusPages 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.get
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.server.routing.routing 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.plugins.configureTemplating
import ru.mipt.spc.magprog.DataSetSiteContext import ru.mipt.spc.magprog.DataSetPageContext
import ru.mipt.spc.magprog.SiteContext import ru.mipt.spc.magprog.PageContext
import ru.mipt.spc.magprog.magProgPage import ru.mipt.spc.magprog.magProgPage
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.io import space.kscience.dataforge.io.io
import space.kscience.dataforge.io.yaml.YamlPlugin
import space.kscience.snark.DirectoryDataTree import space.kscience.snark.DirectoryDataTree
import space.kscience.snark.SnarkPlugin
import java.nio.file.Path import java.nio.file.Path
fun CommonAttributeGroupFacade.css(block: CssBuilder.() -> Unit) {
style = CssBuilder().block().toString()
}
class AuthenticationException : RuntimeException() class AuthenticationException : RuntimeException()
class AuthorizationException : RuntimeException() class AuthorizationException : RuntimeException()
internal fun Application.magProgSite(prefix: String = "magprog"){ internal fun Application.magProgPage(rootPath: Path, prefix: String = "magprog") {
val context = Context("spc-site"){ val context = Context("spc-site") {
plugin(YamlPlugin) plugin(SnarkPlugin)
} }
val io = context.io val io = context.io
val directory = javaClass.getResource("/magprog/content")!!.toURI() val content = DirectoryDataTree(io, rootPath.resolve("content"))
val content = DirectoryDataTree(io, Path.of(directory))
val magprogSiteContext: SiteContext = DataSetSiteContext(context,"magprog", content) val magprogPageContext: PageContext = DataSetPageContext(context, prefix, content)
routing { routing {
route(prefix){ route(prefix) {
get { get {
call.respondHtml { call.respondHtml {
with(magprogSiteContext){ with(magprogPageContext) {
magProgPage() magProgPage()
} }
} }
} }
static { static {
resources("magprog/assets") files(rootPath.resolve("assets").toFile())
} }
} }
} }
@ -59,12 +64,12 @@ internal fun Application.magProgSite(prefix: String = "magprog"){
fun main() { fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") { embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
magProgSite() magProgPage(rootPath = Path.of(javaClass.getResource("/magprog")!!.toURI()))
install(StatusPages) { install(StatusPages) {
exception<AuthenticationException> { call, cause -> exception<AuthenticationException> { call, _ ->
call.respond(HttpStatusCode.Unauthorized) call.respond(HttpStatusCode.Unauthorized)
} }
exception<AuthorizationException> { call, cause -> exception<AuthorizationException> { call, _ ->
call.respond(HttpStatusCode.Forbidden) call.respond(HttpStatusCode.Forbidden)
} }
} }

View File

@ -19,15 +19,16 @@ import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.misc.DFInternal import space.kscience.dataforge.misc.DFInternal
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.snark.DirectoryDataTree.Companion.META_FILE_EXTENSION_KEY import space.kscience.snark.DirectoryDataTree.Companion.META_FILE_EXTENSION_KEY
import space.kscience.snark.HtmlData
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
class DataSetSiteContext( class DataSetPageContext(
override val context: Context, override val context: Context,
val prefix: String, val prefix: String,
val dataSet: DataSet<Any>, val dataSet: DataSet<Any>,
) : SiteContext { ) : PageContext {
override fun resolveResource(name: String): String = "$prefix/$name" override fun resolveResource(name: String): String = "$prefix/$name"

View File

@ -9,10 +9,11 @@ import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFInternal import space.kscience.dataforge.misc.DFInternal
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.startsWith import space.kscience.dataforge.names.startsWith
import space.kscience.snark.HtmlData
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
interface SiteContext: ContextAware { interface PageContext: ContextAware {
/** /**
* Resolve a resource full path by its name * Resolve a resource full path by its name
@ -37,12 +38,12 @@ interface SiteContext: ContextAware {
} }
@OptIn(DFInternal::class) @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) @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) 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 name.startsWith(baseName) && meta["content_type"].string == contentType
} }

View File

@ -2,14 +2,19 @@ package ru.mipt.spc.magprog
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.* import kotlinx.html.*
import ru.mipt.css
import space.kscience.dataforge.meta.string 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 { class Person(val block: HtmlData) : HtmlData by block {
val name: String by meta.string { error("Mentor name is not defined") } val name: String by meta.string { error("Mentor name is not defined") }
val photo: String? by meta.string() val photo: String? by meta.string()
} }
context(SiteContext) context(PageContext)
private fun FlowContent.personCards(list: List<Person>, prefix: String) { private fun FlowContent.personCards(list: List<Person>, prefix: String) {
list.forEach { mentor -> list.forEach { mentor ->
section { section {
@ -34,7 +39,7 @@ private fun FlowContent.personCards(list: List<Person>, prefix: String) {
} }
} }
context(SiteContext) context(PageContext)
fun FlowContent.mentors() { fun FlowContent.mentors() {
val mentors = findByType("magprog_mentor").values.map { val mentors = findByType("magprog_mentor").values.map {
Person(it) Person(it)
@ -54,7 +59,7 @@ fun FlowContent.mentors() {
personCards(mentors,"mentor") personCards(mentors,"mentor")
} }
context(SiteContext) context(PageContext)
fun FlowContent.team() { fun FlowContent.team() {
val team = findByType("magprog_team").values.map { Person(it) }.sortedBy { it.order } val team = findByType("magprog_team").values.map { Person(it) }.sortedBy { it.order }

View File

@ -3,6 +3,7 @@ package ru.mipt.spc.magprog
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.* import kotlinx.html.*
import ru.mipt.css
import space.kscience.dataforge.data.await import space.kscience.dataforge.data.await
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get 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.Name
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.plus
import space.kscience.snark.HtmlData
import space.kscience.snark.htmlData
import space.kscience.snark.id
//fun CssBuilder.magProgCss() { //fun CssBuilder.magProgCss() {
// rule(".magprog-body") { // 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 RECOMMENDED_COURSES_PATH: Name = CONTENT_NODE_NAME + "recommendedCourses"
private val PARTNERS_PATH: Name = CONTENT_NODE_NAME + "partners" 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 programBlock = resolveHtml(PROGRAM_PATH)!!
val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!! val recommendedBlock = resolveHtml(RECOMMENDED_COURSES_PATH)!!
div("inner") { div("inner") {
h2 { +"Учебная программа" } h2 { +"Учебная программа" }
htmlData(programBlock) htmlData(programBlock)
button(classes = "fit btn btn-primary btn-lg") { button(classes = "fit btn btn-secondary") {
attributes["data-bs-toggle"] = "collapse" attributes["data-bs-toggle"] = "collapse"
attributes["data-bs-target"] = "#recommended-courses-collapse-text" attributes["data-bs-target"] = "#recommended-courses-collapse-text"
attributes["aria-expanded"] = "false" attributes["aria-expanded"] = "false"
@ -81,14 +85,14 @@ context(SiteContext) private fun FlowContent.programSection() {
} }
div("collapse pt-3") { div("collapse pt-3") {
id = "recommended-courses-collapse-text" id = "recommended-courses-collapse-text"
div("card card-body") { div {
htmlData(recommendedBlock) 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 = resolve<Any>(PARTNERS_PATH)?.meta ?: Meta.EMPTY
val partnersData: Meta = runBlocking { resolve<Meta>(PARTNERS_PATH)?.await()} ?: Meta.EMPTY val partnersData: Meta = runBlocking { resolve<Meta>(PARTNERS_PATH)?.await()} ?: Meta.EMPTY
div("inner") { 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>( val sections = listOf<MagProgSection>(
wrapSection(resolveHtml(INTRO_PATH)!!, "intro"), wrapSection(resolveHtml(INTRO_PATH)!!, "intro"),
MagProgSection( MagProgSection(
@ -158,6 +162,10 @@ context(SiteContext) fun HTML.magProgPage() {
name = "viewport" name = "viewport"
content = "width=device-width, initial-scale=1, user-scalable=no" content = "width=device-width, initial-scale=1, user-scalable=no"
} }
link {
rel = "stylesheet"
href = resolveResource("css/bootstrap.min.css")
}
link { link {
rel = "stylesheet" rel = "stylesheet"
href = resolveResource("css/main.css") href = resolveResource("css/main.css")
@ -237,6 +245,9 @@ context(SiteContext) fun HTML.magProgPage() {
script { script {
src = resolveResource("js/util.js") src = resolveResource("js/util.js")
} }
script {
src = resolveResource("js/bootstrap.min.js")
}
script { script {
src = resolveResource("js/main.js") src = resolveResource("js/main.js")
} }

View File

@ -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()
}

View File

@ -13,10 +13,8 @@ import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.plus
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.extension import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.isDirectory import kotlin.io.path.*
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.nameWithoutExtension
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -32,7 +30,10 @@ class DirectoryDataTree(val io: IOPlugin, val path: Path) : DataTree<ByteArray>
val meta = envelope.meta.copy { val meta = envelope.meta.copy {
META_FILE_PATH_KEY put filePath.toString() META_FILE_PATH_KEY put filePath.toString()
META_FILE_EXTENSION_KEY put filePath.extension 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){ return Data(meta){
envelope.data?.toByteArray() ?: ByteArray(0) 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_KEY = "file".asName()
val META_FILE_PATH_KEY = META_FILE_KEY + "path" val META_FILE_PATH_KEY = META_FILE_KEY + "path"
val META_FILE_EXTENSION_KEY = META_FILE_KEY + "extension" 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"
} }
} }

View File

@ -1,4 +1,4 @@
package ru.mipt.spc.magprog package space.kscience.snark
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.html.FlowContent import kotlinx.html.FlowContent

View 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() }
}
}
}

View File

@ -8,8 +8,10 @@ import space.kscience.dataforge.io.yaml.YamlPlugin
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@ -19,18 +21,20 @@ interface SnarkParser<R : Any> {
val fileExtensions: Set<String> val fileExtensions: Set<String>
val priority: Int val priority: Int get() = DEFAULT_PRIORITY
val resultType: KType val resultType: KType
suspend fun parse(bytes: ByteArray): R suspend fun parse(bytes: ByteArray, meta: Meta): R
companion object { companion object {
const val TYPE = "snark.parser" const val TYPE = "snark.parser"
const val DEFAULT_PRIORITY = 10
} }
} }
@OptIn(DFExperimental::class)
class SnarkPlugin : AbstractPlugin() { class SnarkPlugin : AbstractPlugin() {
val yaml by require(YamlPlugin) val yaml by require(YamlPlugin)
val io get() = yaml.io 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 parsers: Map<Name, SnarkParser<*>> by lazy { context.gather(SnarkParser.TYPE, true) }
private val parseAction = Action.map<ByteArray, Any> { private val parseAction = Action.map<ByteArray, Any> {
result { bytes -> val parser: SnarkParser<*>? = parsers.values.filter { parser ->
parsers.values.filter { parser ->
parser.contentType.toString() == meta["contentType"].string || parser.contentType.toString() == meta["contentType"].string ||
meta[DirectoryDataTree.META_FILE_EXTENSION_KEY].string in parser.fileExtensions meta[DirectoryDataTree.META_FILE_EXTENSION_KEY].string in parser.fileExtensions
}.maxByOrNull { }.maxByOrNull {
it.priority 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)
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,7 @@ package ru.mipt
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication import io.ktor.server.testing.testApplication
import java.nio.file.Path
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -10,7 +11,7 @@ class ApplicationTest {
@Test @Test
fun testRoot() = testApplication { fun testRoot() = testApplication {
application { application {
magProgSite() magProgPage(rootPath = Path.of(javaClass.getResource("/magprog")!!.toURI()))
} }
client.get("/magprog").apply { client.get("/magprog").apply {
assertEquals(HttpStatusCode.OK, status) assertEquals(HttpStatusCode.OK, status)