From 486f5fc8a5c73349bcdc086b647e776aa2a2bc89 Mon Sep 17 00:00:00 2001 From: yostane <1958676+yostane@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:14:36 +0200 Subject: [PATCH] Some improvements and bug fixes --- .../build.gradle.kts | 4 +- .../quiz-collector-bruno/reset.bru | 11 + .../quiz-collector-bruno/respond 1.bru | 154 ++++++- .../quiz-collector-bruno/respond 2.bru | 186 ++++++-- .../settings.gradle.kts | 2 +- .../ktor_quiz_collector/Application.kt | 26 ++ .../ktor_quiz_collector}/plugins/HTTP.kt | 2 +- .../ktor_quiz_collector}/plugins/Routing.kt | 2 +- .../plugins/Serialization.kt | 6 +- .../plugins/Templating.kt | 7 +- .../ktor_quiz_collector/routes/MiscRoutes.kt | 79 ++++ .../routes/QuizCollectorRoutes.kt | 100 +++++ .../ktor_quiz_collector/utils/DataUtils.kt | 93 ++++ .../ktor_quiz_collector/utils/Models.kt | 20 + .../ktor_quiz_collector/utils/SampleValues.kt | 399 ++++++++++++++++++ .../main/kotlin/example/com/Application.kt | 20 - .../src/main/kotlin/example/com/Models.kt | 312 -------------- .../main/kotlin/example/com/QuizCollector.kt | 106 ----- .../src/main/resources/application.yaml | 5 +- .../src/main/resources/static/auto-reload.js | 7 + .../src/main/resources/static/favicon.ico | Bin 0 -> 85886 bytes .../ktor_quiz_collector}/ApplicationTest.kt | 4 +- 22 files changed, 1040 insertions(+), 505 deletions(-) create mode 100644 material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/reset.bru create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/Application.kt rename material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/{example/com => com/worldline/training/ktor_quiz_collector}/plugins/HTTP.kt (92%) rename material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/{example/com => com/worldline/training/ktor_quiz_collector}/plugins/Routing.kt (93%) rename material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/{example/com => com/worldline/training/ktor_quiz_collector}/plugins/Serialization.kt (75%) rename material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/{example/com => com/worldline/training/ktor_quiz_collector}/plugins/Templating.kt (87%) create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/MiscRoutes.kt create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/QuizCollectorRoutes.kt create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/DataUtils.kt create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/Models.kt create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/SampleValues.kt delete mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Application.kt delete mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Models.kt delete mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/QuizCollector.kt create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/auto-reload.js create mode 100644 material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/favicon.ico rename material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/{example/com => com/worldline/training/ktor_quiz_collector}/ApplicationTest.kt (75%) diff --git a/material/rest-api-ui-ktor-quiz-collector/build.gradle.kts b/material/rest-api-ui-ktor-quiz-collector/build.gradle.kts index 80972bb9..3a397a79 100644 --- a/material/rest-api-ui-ktor-quiz-collector/build.gradle.kts +++ b/material/rest-api-ui-ktor-quiz-collector/build.gradle.kts @@ -5,8 +5,8 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" } -group = "example.com" -version = "0.0.1" +group = "com.worldline.training" +version = "1.0.0" application { mainClass.set("io.ktor.server.cio.EngineMain") diff --git a/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/reset.bru b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/reset.bru new file mode 100644 index 00000000..51b73b8f --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/reset.bru @@ -0,0 +1,11 @@ +meta { + name: reset + type: http + seq: 7 +} + +get { + url: http://localhost:8080/reset + body: none + auth: none +} diff --git a/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 1.bru b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 1.bru index b42005a2..d2a3284f 100644 --- a/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 1.bru +++ b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 1.bru @@ -12,15 +12,149 @@ post { body:json { { - "responses": [ - { - "question": "What is the primary goal of Kotlin Multiplatform?", - "answer": "Muzukashi" - }, - { - "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "answer": "By sharing business logic and adapting UI" + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + }, + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + } + ], + "score": 1, + "nickname": "user-424" } - ] - } } diff --git a/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 2.bru b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 2.bru index b1c0ad3d..4e2ecbb0 100644 --- a/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 2.bru +++ b/material/rest-api-ui-ktor-quiz-collector/quiz-collector-bruno/respond 2.bru @@ -12,47 +12,149 @@ post { body:json { { - "responses": [ - { - "question": "What is the primary goal of Kotlin Multiplatform?", - "answer": "To share code between multiple platforms" - }, - { - "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "answer": "By sharing business logic and adapting UI" - }, - { - "question": "Which platforms does Kotlin Multiplatform support?", - "answer": "Android, iOS, and web" - }, - { - "question": "What is a common use case for Kotlin Multiplatform?", - "answer": "Developing a cross-platform app" - }, - { - "question": "What is a shared code module in Kotlin Multiplatform called?", - "answer": "Shared module" - }, - { - "question": "How does Kotlin Multiplatform handle platform-specific implementations?", - "answer": "Through expect and actual declarations" - }, - { - "question": "What languages can be interoperable with Kotlin Multiplatform?", - "answer": "Java, JavaScript, Swift" - }, - { - "question": "What tooling supports Kotlin Multiplatform development?", - "answer": "IntelliJ IDEA, Android Studio" - }, - { - "question": "What is the benefit of using Kotlin Multiplatform for mobile development?", - "answer": "Code reuse and sharing" - }, - { - "question": "How does Kotlin Multiplatform differ from Kotlin Native and Kotlin/JS?", - "answer": "Kotlin Multiplatform allows sharing code between different platforms using common modules." + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + }, + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + } + ], + "score": 1, + "nickname": "user-100" } - ] - } } diff --git a/material/rest-api-ui-ktor-quiz-collector/settings.gradle.kts b/material/rest-api-ui-ktor-quiz-collector/settings.gradle.kts index fb145df6..3a13f879 100644 --- a/material/rest-api-ui-ktor-quiz-collector/settings.gradle.kts +++ b/material/rest-api-ui-ktor-quiz-collector/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "com.worldline.training.quiz-collector" +rootProject.name = "com.worldline.training.ktor_quiz_collector" diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/Application.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/Application.kt new file mode 100644 index 00000000..ff3d200a --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/Application.kt @@ -0,0 +1,26 @@ +package com.worldline.training.ktor_quiz_collector + +import com.worldline.training.ktor_quiz_collector.plugins.configureHTTP +import com.worldline.training.ktor_quiz_collector.plugins.configureRouting +import com.worldline.training.ktor_quiz_collector.plugins.configureSerialization +import com.worldline.training.ktor_quiz_collector.plugins.configureTemplating +import com.worldline.training.ktor_quiz_collector.routes.configureMiscRoutes +import com.worldline.training.ktor_quiz_collector.routes.configureQuizCollector +import com.worldline.training.ktor_quiz_collector.utils.addSampleValues +import io.ktor.server.application.* + +fun main(args: Array) { + io.ktor.server.cio.EngineMain.main(args) +} + +fun Application.module() { +// (1..10).forEach { _ -> +// addSampleValues() +// } + configureHTTP() + configureTemplating() + configureSerialization() + configureRouting() + configureMiscRoutes() + configureQuizCollector() +} diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/HTTP.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/HTTP.kt similarity index 92% rename from material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/HTTP.kt rename to material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/HTTP.kt index ee847ca4..3bb1152e 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/HTTP.kt +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/HTTP.kt @@ -1,4 +1,4 @@ -package example.com.plugins +package com.worldline.training.ktor_quiz_collector.plugins import io.ktor.http.* import io.ktor.server.application.* diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Routing.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Routing.kt similarity index 93% rename from material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Routing.kt rename to material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Routing.kt index 3a5b552f..225f2b2c 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Routing.kt +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Routing.kt @@ -1,4 +1,4 @@ -package example.com.plugins +package com.worldline.training.ktor_quiz_collector.plugins import io.github.smiley4.ktorswaggerui.SwaggerUI import io.ktor.http.* diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Serialization.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Serialization.kt similarity index 75% rename from material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Serialization.kt rename to material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Serialization.kt index 51d19f54..2b11990b 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Serialization.kt +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Serialization.kt @@ -1,4 +1,4 @@ -package example.com.plugins +package com.worldline.training.ktor_quiz_collector.plugins import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* @@ -12,7 +12,7 @@ fun Application.configureSerialization() { } routing { get("/json/kotlinx-serialization") { - call.respond(mapOf("hello" to "world")) - } + call.respond(mapOf("hello" to "world")) + } } } diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Templating.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Templating.kt similarity index 87% rename from material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Templating.kt rename to material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Templating.kt index f11fdc1b..e9b221e1 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/plugins/Templating.kt +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/plugins/Templating.kt @@ -1,4 +1,4 @@ -package example.com.plugins +package com.worldline.training.ktor_quiz_collector.plugins import io.ktor.http.* import io.ktor.server.application.* @@ -6,6 +6,7 @@ import io.ktor.server.html.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.css.* +import kotlinx.css.properties.boxShadow import kotlinx.html.* fun Application.configureTemplating() { @@ -33,7 +34,7 @@ fun Application.configureTemplating() { } } } - + get("/html-css-dsl") { call.respondHtml { head { @@ -50,5 +51,5 @@ fun Application.configureTemplating() { } suspend inline fun ApplicationCall.respondCss(builder: CSSBuilder.() -> Unit) { - this.respondText(CSSBuilder().apply(builder).toString(), ContentType.Text.CSS) + this.respondText(CSSBuilder().apply(builder).toString(), ContentType.Text.CSS) } diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/MiscRoutes.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/MiscRoutes.kt new file mode 100644 index 00000000..b5a51982 --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/MiscRoutes.kt @@ -0,0 +1,79 @@ +package com.worldline.training.ktor_quiz_collector.routes + +import com.worldline.training.ktor_quiz_collector.plugins.respondCss +import com.worldline.training.ktor_quiz_collector.utils.getCorrectStats +import com.worldline.training.ktor_quiz_collector.utils.quizResponses +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.http.content.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.css.* +import kotlinx.css.properties.boxShadow + +fun Application.configureMiscRoutes() { + routing { + staticResources("/static", "static") + + get("/styles-graph.css") { + call.respondCss { + body { + backgroundColor = Color.lightCyan + margin(0.px) + padding(0.px) + } + h1 { + textAlign = TextAlign.center + backgroundColor = Color.lightGreen + marginTop = 0.px + padding(1.5.rem) + } + p { + textAlign = TextAlign.center + } + rule("div") { + display = Display.flex + flexWrap = FlexWrap.wrap + justifyContent = JustifyContent.center + gap = Gap("1rem") + } + svg { + border = "1px solid black" + boxShadow(Color.black, 2.px, 2.px, 2.px) + } + } + } + + get("/favicon.ico") { + val image = call.resolveResource("static/favicon.ico") + if (image != null) { + call.respond(image) + } else { + call.respond(HttpStatusCode.NotFound) + } + } + + get("/responses") { + call.respond(quizResponses.map { quizResponse -> + quizResponse.responses.fold(mutableMapOf()) { acc, element -> + acc[element.question] = element.answer + acc + } + }) + } + + get("/correct-stats") { + call.respond(getCorrectStats()) + } + + get("/table2") { + call.respond(quizResponses) + } + + get("/table") { + val result = quizResponses.flatMap { it.responses }.groupBy { it.question } + .mapValues { it.value.map { value -> value.answer } } + call.respond(result) + } + } +} \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/QuizCollectorRoutes.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/QuizCollectorRoutes.kt new file mode 100644 index 00000000..0162000f --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/routes/QuizCollectorRoutes.kt @@ -0,0 +1,100 @@ +package com.worldline.training.ktor_quiz_collector.routes + +import com.worldline.training.ktor_quiz_collector.utils.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.html.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.html.* +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.kandy.dsl.plot +import org.jetbrains.kotlinx.kandy.letsplot.export.toHTML +import org.jetbrains.kotlinx.kandy.letsplot.export.toSVG +import org.jetbrains.kotlinx.kandy.letsplot.layers.bars + + +fun Application.configureQuizCollector() { + routing { + post("/respond") { + val quizResponse = call.receive() + quizResponses.add(quizResponse) + call.respond(CollectResponse(quizResponse.score)) + } + + get("/raw-responses") { + call.respond(quizResponses) + } + + get("/reset") { + quizResponses.clear() + call.respond(HttpStatusCode.OK) + } + + get("/parts/graphs") { + call.respondHtml { + body { + div { + unsafe { +generateCorrectQuestionsPlot().toSVG() } + unsafe { +generateLeaderBoardPlot().toSVG() } + } + } + } + } + + get("/") { + call.respondHtml { + head { + title = "Quiz Collector" + link(rel = "stylesheet", href = "/styles-graph.css", type = "text/css") + script { + src = "/static/auto-reload.js" + } + } + body { + h1 { +"Ktor + Kandy Quiz Collector 📊" } + p { + +"Pages are generated on the server side with Kotlinx.HTML and Kotlinx.CSS" + } + p { + +"The plots are generated by Kandy and displayed as SVG" + } + div { + id = "graphs" + unsafe { +generateCorrectQuestionsPlot().toSVG() } + unsafe { +generateLeaderBoardPlot().toSVG() } + } + } + } + } + + get("/no-polling") { + call.respondHtml { + head { + title = "Quiz Collector" + link(rel = "stylesheet", href = "/styles-graph.css", type = "text/css") + } + body { + h1 { +"Ktor + Kandy Quiz Collector 📊" } + div { + unsafe { +generateCorrectQuestionsPlot().toSVG() } + unsafe { +generateLeaderBoardPlot().toSVG() } + } + } + } + } + get("/ui/correct") { + val correctStats = getCorrectStats() + val statsDataFrame = dataFrameOf("question" to correctStats.map { it.question }, + "correct" to correctStats.map { it.correct }) + val html = statsDataFrame.plot { + bars { + y("question") + x("correct") + } + }.toHTML(false) + call.respondText(html, ContentType.Text.Html) + } + } +} \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/DataUtils.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/DataUtils.kt new file mode 100644 index 00000000..f8750eef --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/DataUtils.kt @@ -0,0 +1,93 @@ +package com.worldline.training.ktor_quiz_collector.utils + +import kotlinx.serialization.Serializable +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.dataframe.api.sortByDesc +import org.jetbrains.kotlinx.dataframe.api.toDataFrame +import org.jetbrains.kotlinx.kandy.dsl.continuous +import org.jetbrains.kotlinx.kandy.dsl.plot +import org.jetbrains.kotlinx.kandy.ir.Plot +import org.jetbrains.kotlinx.kandy.letsplot.feature.layout +import org.jetbrains.kotlinx.kandy.letsplot.layers.bars +import org.jetbrains.kotlinx.kandy.letsplot.layers.barsH +import org.jetbrains.kotlinx.kandy.letsplot.scales.categoricalColorHue +import org.jetbrains.kotlinx.kandy.letsplot.x +import org.jetbrains.kotlinx.kandy.letsplot.y +import org.jetbrains.kotlinx.kandy.util.color.Color + +@Serializable +data class QuestionStats(val question: String, val correct: Int) + +fun getCorrectStats() = + com.worldline.training.ktor_quiz_collector.utils.quizResponses.flatMap { it.responses }.groupBy { it.question } + .mapValues { it.value.count { qr -> qr.correctAnswerId == qr.answerId } } + .map { + com.worldline.training.ktor_quiz_collector.utils.QuestionStats( + it.key.take(10) + "..." + it.key.takeLast( + 10 + ), it.value + ) + } + +fun generateCorrectQuestionsPlot(): Plot { + val correctStats = com.worldline.training.ktor_quiz_collector.utils.getCorrectStats() + val statsDataFrame = dataFrameOf("question" to correctStats.map { it.question }, + "correct" to correctStats.map { it.correct }) + val responseCount = if (quizResponses.isEmpty()) 0 else quizResponses[0].responses.size + return statsDataFrame.plot { + layout { + title = "Correct Answers" + xAxisLabel = "Total correct answers" + yAxisLabel = "Question" + } + y("question") + barsH { + x.constant(responseCount) + width = 0.5 + fillColor = Color.GREY + alpha = 0.3 + } + barsH { + y("question") + x("correct") + fillColor("question") { + scale = categoricalColorHue() + } + } + } +} + +fun generateLeaderBoardPlot(): Plot { + val userScores = mapOf("user" to quizResponses.map { it.nickname }, + "score" to quizResponses.map { it.score }) + val rowCount = quizResponses.count() + val maxScore = quizResponses.maxOfOrNull { it.score } ?: 0 + val df = if (rowCount > 0) userScores.toDataFrame().sortByDesc("score") else userScores.toDataFrame() + return plot(df) { + layout { + title = "Leaderboard" + xAxisLabel = "Nickame" + yAxisLabel = "Score" + size = 1200 to 600 + } + x("user") + bars { + y.constant(maxScore) + width = 0.5 + fillColor = Color.GREY + alpha = 0.3 + } + bars { + y("score") { + scale = continuous(0..maxScore) + } + x("user") + if (rowCount > 2) { + fillColor("user") { + scale = categoricalColorHue() + } + } + + } + } +} \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/Models.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/Models.kt new file mode 100644 index 00000000..469d7568 --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/Models.kt @@ -0,0 +1,20 @@ +package com.worldline.training.ktor_quiz_collector.utils + +import kotlinx.serialization.Serializable + +@Serializable +data class QuestionResponse( + val question: String, + val answer: String, + val id: Long, + val answerId: Long, + val correctAnswerId: Long +) + +@Serializable +data class QuizResponse(val score: Int, val nickname: String, val responses: List) + +@Serializable +data class CollectResponse(val score: Int) + +val quizResponses = mutableListOf() \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/SampleValues.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/SampleValues.kt new file mode 100644 index 00000000..5e910dad --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/com/worldline/training/ktor_quiz_collector/utils/SampleValues.kt @@ -0,0 +1,399 @@ +package com.worldline.training.ktor_quiz_collector.utils + +import io.ktor.server.application.* +import kotlinx.serialization.json.Json + +val json1 = """ + { + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To share code between multiple platforms", + "answerId": 1, + "id": 1, + "correctAnswerId": 1 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By sharing business logic and adapting UI", + "answerId": 1, + "id": 2, + "correctAnswerId": 1 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Android, iOS, desktop and web", + "answerId": 4, + "id": 3, + "correctAnswerId": 4 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Developing a cross-platform app", + "answerId": 4, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kotlin Multiplatform Mobile (KMM)", + "answerId": 4, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "Through expect and actual declarations", + "answerId": 3, + "id": 6, + "correctAnswerId": 3 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 3, + "id": 7, + "correctAnswerId": 3 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kodee", + "answerId": 4, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinConf", + "answerId": 4, + "id": 9, + "correctAnswerId": 4 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Copenhagen, Denmark", + "answerId": 4, + "id": 10, + "correctAnswerId": 4 + } + ], + "score": 10, + "nickname": "user-650" + } +""".trimIndent() + +val json2 = """ + { + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To exclusively compile code to JavaScript", + "answerId": 1, + "id": 1, + "correctAnswerId": 3 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By sharing business logic and adapting UI", + "answerId": 2, + "id": 2, + "correctAnswerId": 2 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Android, iOS, desktop and web", + "answerId": 3, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Developing a cross-platform app", + "answerId": 2, + "id": 4, + "correctAnswerId": 2 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 3, + "id": 5, + "correctAnswerId": 2 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By restricting to a single platform", + "answerId": 4, + "id": 6, + "correctAnswerId": 1 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 4, + "id": 7, + "correctAnswerId": 4 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Hadee", + "answerId": 2, + "id": 8, + "correctAnswerId": 3 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KConf", + "answerId": 1, + "id": 9, + "correctAnswerId": 4 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Amsterdam, Netherlands", + "answerId": 3, + "id": 10, + "correctAnswerId": 1 + } + ], + "score": 4, + "nickname": "user-140" + } +""".trimIndent() + + +val json3 = """ + { + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To create iOS-only applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 2 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By sharing business logic and adapting UI", + "answerId": 1, + "id": 2, + "correctAnswerId": 1 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Android, iOS, desktop and web", + "answerId": 1, + "id": 3, + "correctAnswerId": 1 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Creating a server-side application", + "answerId": 1, + "id": 4, + "correctAnswerId": 2 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 3 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By excluding platform-specific features", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 2 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KodeeConf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + } + ], + "score": 3, + "nickname": "user-717" + } +""".trimIndent() + +val json4 = """ + { + "responses": [ + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + }, + { + "question": "What is the primary goal of Kotlin Multiplatform?", + "answer": "To build only Android applications", + "answerId": 1, + "id": 1, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform facilitate code sharing between platforms?", + "answer": "By writing separate code for each platform", + "answerId": 1, + "id": 2, + "correctAnswerId": 3 + }, + { + "question": "Which platforms does Kotlin Multiplatform support?", + "answer": "Only web applications", + "answerId": 1, + "id": 3, + "correctAnswerId": 3 + }, + { + "question": "What is a common use case for Kotlin Multiplatform?", + "answer": "Building a desktop-only application", + "answerId": 1, + "id": 4, + "correctAnswerId": 4 + }, + { + "question": "Which naming of KMP is deprecated?", + "answer": "Kodee multiplatform", + "answerId": 1, + "id": 5, + "correctAnswerId": 4 + }, + { + "question": "How does Kotlin Multiplatform handle platform-specific implementations?", + "answer": "By automatically translating code", + "answerId": 1, + "id": 6, + "correctAnswerId": 4 + }, + { + "question": "At which Google I/O, Google announced first-class support for Kotlin on Android?", + "answer": "2017", + "answerId": 1, + "id": 7, + "correctAnswerId": 1 + }, + { + "question": "What is the name of the Kotlin mascot?", + "answer": "Kotee", + "answerId": 1, + "id": 8, + "correctAnswerId": 4 + }, + { + "question": "The international yearly Kotlin conference is called...", + "answer": "KotlinKonf", + "answerId": 1, + "id": 9, + "correctAnswerId": 3 + }, + { + "question": "Where will be located the next international yearly Kotlin conference?", + "answer": "Lille, France", + "answerId": 1, + "id": 10, + "correctAnswerId": 3 + } + ], + "score": 1, + "nickname": "user-424" + } +""".trimIndent() + +fun Application.addSampleValues() { + val data = listOf(json1, json2, json3, json4).map { json -> + val q = Json.decodeFromString(json) + QuizResponse(q.score, "user ${(1..1000).random()}", q.responses) + } + quizResponses.addAll(data) +} \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Application.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Application.kt deleted file mode 100644 index cc8544e7..00000000 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Application.kt +++ /dev/null @@ -1,20 +0,0 @@ -package example.com - -import example.com.plugins.configureHTTP -import example.com.plugins.configureRouting -import example.com.plugins.configureSerialization -import example.com.plugins.configureTemplating -import io.ktor.server.application.* - -fun main(args: Array) { - io.ktor.server.cio.EngineMain.main(args) -} - -fun Application.module() { - quizResponses.addAll(sampleResponses) - configureHTTP() - configureTemplating() - configureSerialization() - configureRouting() - configureQuizCollector() -} diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Models.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Models.kt deleted file mode 100644 index 5800b7b7..00000000 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/Models.kt +++ /dev/null @@ -1,312 +0,0 @@ -package example.com - -import kotlinx.serialization.Serializable - -@Serializable -data class QuestionResponse( - val question: String, - val answer: String, - val id: Long, - val answerId: Long, - val correctAnswerId: Long -) - -@Serializable -data class QuizResponse(val score: Int, val nickname: String, val responses: List) - -@Serializable -data class CollectResponse(val score: Int) - -val quizResponses = mutableListOf() - -// sample data with correct and wrong answers -val sampleResponses = listOf( - QuizResponse( - 0, "user1", - listOf( - QuestionResponse( - question = "What is the primary goal of Kotlin Multiplatform?", - answer = "To share", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "By sharing business logic and adapting UI", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - question = "Which platforms does Kotlin Multiplatform support?", answer = "Android, iOS, and web", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a common use case for Kotlin Multiplatform?", "Developing a cross-platform app", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a shared code module in Kotlin Multiplatform called?", "Shared module", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform handle platform-specific implementations?", - "actual", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What languages can be interoperable with Kotlin Multiplatform?", - "Java, Swift", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What tooling supports Kotlin Multiplatform development?", - "IntelliJ IDEA", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is the benefit of using Kotlin Multiplatform for mobile development?", - "and sharing", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform differ from Kotlin Native and Kotlin/JS?", - "None", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ) - ) - ), - QuizResponse( - 10, "user2", - listOf( - QuestionResponse( - "What is the primary goal of Kotlin Multiplatform?", - "To share code between multiple platforms", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "By sharing business logic and adapting UI", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "Which platforms does Kotlin Multiplatform support?", "Android, iOS, and web", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a common use case for Kotlin Multiplatform?", "Developing a cross-platform app", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a shared code module in Kotlin Multiplatform called?", "Shared module", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform handle platform-specific implementations?", - "Through expect and actual declarations", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What languages can be interoperable with Kotlin Multiplatform?", - "Java, JavaScript, Swift", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What tooling supports Kotlin Multiplatform development?", - "IntelliJ IDEA, Android Studio", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is the benefit of using Kotlin Multiplatform for mobile development?", - "Code reuse and sharing", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform differ from Kotlin Native and Kotlin/JS?", - "Kotlin Multiplatform allows sharing code between different platforms using common modules.", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ) - ) - ), - QuizResponse( - 2, "user4", - listOf( - QuestionResponse( - "What is the primary goal of Kotlin Multiplatform?", - "To share code between multiple platforms", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "By sharing business logic and adapting UI", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "Which platforms does Kotlin Multiplatform support?", "Android, iOS, and web", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a common use case for Kotlin Multiplatform?", "Developing a cross-platform app", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a shared code module in Kotlin Multiplatform called?", "Shared module", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform handle platform-specific implementations?", - "Through expect and actual declarations", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What languages can be interoperable with Kotlin Multiplatform?", - "Java, JavaScript, Swift", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What tooling supports Kotlin Multiplatform development?", - "IntelliJ IDEA, Android Studio", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is the benefit of using Kotlin Multiplatform for mobile development?", - "Code reuse and sharing", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform differ from Kotlin Native and Kotlin/JS?", - "Kotlin Multiplatform allows sharing code between different platforms using common modules.", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ) - ) - ), - QuizResponse( - 2, "user3", - listOf( - QuestionResponse( - "What is the primary goal of Kotlin Multiplatform?", - "To share code between multiple platforms", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform facilitate code sharing between platforms?", - "By sharing business logic and adapting UI", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "Which platforms does Kotlin Multiplatform support?", "Android, iOS, and web", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a common use case for Kotlin Multiplatform?", "Developing a cross-platform app", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is a shared code module in Kotlin Multiplatform called?", "Shared module", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform handle platform-specific implementations?", - "Through expect and actual declarations", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What languages can be interoperable with Kotlin Multiplatform?", - "Java, JavaScript, Swift", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What tooling supports Kotlin Multiplatform development?", - "IntelliJ IDEA, Android Studio", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "What is the benefit of using Kotlin Multiplatform for mobile development?", - "Code reuse and sharing", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ), - QuestionResponse( - "How does Kotlin Multiplatform differ from Kotlin Native and Kotlin/JS?", - "Kotlin Multiplatform allows sharing code between different platforms using common modules.", - id = (1..10).random().toLong(), - answerId = (1..4).random().toLong(), - correctAnswerId = (1..4).random().toLong() - ) - ) - ), -) \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/QuizCollector.kt b/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/QuizCollector.kt deleted file mode 100644 index caa9e671..00000000 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/kotlin/example/com/QuizCollector.kt +++ /dev/null @@ -1,106 +0,0 @@ -package example.com - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.html.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.html.* -import kotlinx.serialization.Serializable -import org.jetbrains.kotlinx.dataframe.api.dataFrameOf -import org.jetbrains.kotlinx.kandy.dsl.plot -import org.jetbrains.kotlinx.kandy.letsplot.export.toHTML -import org.jetbrains.kotlinx.kandy.letsplot.export.toSVG -import org.jetbrains.kotlinx.kandy.letsplot.layers.bars -import org.jetbrains.kotlinx.kandy.letsplot.layers.barsH -import org.jetbrains.kotlinx.kandy.letsplot.scales.categoricalColorHue - -@Serializable -data class QuestionStats(val question: String, val correct: Int) - -fun Application.configureQuizCollector() { - fun getCorrectStats() = quizResponses.flatMap { it.responses }.groupBy { it.question } - .mapValues { it.value.count { qr -> qr.correctAnswerId == qr.answerId } } - .map { QuestionStats(it.key, it.value) } - - routing { - post("/respond") { - val quizResponse = call.receive() - quizResponses.add(quizResponse) - call.respond(CollectResponse(quizResponse.score)) - } - - get("/raw-responses") { - call.respond(quizResponses) - } - - get("/responses") { - call.respond(quizResponses.map { quizResponse -> - quizResponse.responses.fold(mutableMapOf()) { acc, element -> - acc[element.question] = element.answer - acc - } - }) - } - - get("/correct-stats") { - call.respond(getCorrectStats()) - } - - get("/table2") { - call.respond(quizResponses) - } - - get("/table") { - val result = quizResponses.flatMap { it.responses }.groupBy { it.question } - .mapValues { it.value.map { value -> value.answer } } - call.respond(result) - } - - get("/reset") { - quizResponses.clear() - call.respond(HttpStatusCode.OK) - } - - get("/") { - call.respondHtml { - head { - title = "Quiz Collector" - } - body { - h1 { +"Quiz Collector" } - div { - val correctStats = getCorrectStats() - val statsDataFrame = dataFrameOf("question" to correctStats.map { it.question }, - "correct" to correctStats.map { it.correct }) - val content = statsDataFrame.plot { - barsH { - y("question") - x("correct") - - fillColor("question") { - scale = categoricalColorHue() - } - } - - }.toSVG() - unsafe { +content } - } - } - } - } - get("/ui/correct") { - val correctStats = getCorrectStats() - val statsDataFrame = dataFrameOf("question" to correctStats.map { it.question }, - "correct" to correctStats.map { it.correct }) - val html = statsDataFrame.plot { - bars { - y("question") - x("correct") - } - }.toHTML(false) - call.respondText(html, ContentType.Text.Html) - } - } -} \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/resources/application.yaml b/material/rest-api-ui-ktor-quiz-collector/src/main/resources/application.yaml index 5d3834a6..e9845d32 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/main/resources/application.yaml +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/resources/application.yaml @@ -2,8 +2,9 @@ ktor: #development: true application: modules: - - example.com.ApplicationKt.module + - com.worldline.training.ktor_quiz_collector.ApplicationKt.module deployment: port: "$PORT:8080" watch: - - classes \ No newline at end of file + - classes + - resources \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/auto-reload.js b/material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/auto-reload.js new file mode 100644 index 00000000..52252170 --- /dev/null +++ b/material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/auto-reload.js @@ -0,0 +1,7 @@ +setInterval(() => { + fetch('/parts/graphs') + .then(response => response.text()) + .then(data => { + document.getElementById('graphs').innerHTML = data; + }); +}, 2000); \ No newline at end of file diff --git a/material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/favicon.ico b/material/rest-api-ui-ktor-quiz-collector/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..237028184b6e3fdf5a49febf216fb0814d51c563 GIT binary patch literal 85886 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 a5!ihMb{~P=M_~66*nI?cAAub_0{;)7M|M8| literal 0 HcmV?d00001 diff --git a/material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/example/com/ApplicationTest.kt b/material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/com/worldline/training/ktor_quiz_collector/ApplicationTest.kt similarity index 75% rename from material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/example/com/ApplicationTest.kt rename to material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/com/worldline/training/ktor_quiz_collector/ApplicationTest.kt index 1c76138f..d42a8cea 100644 --- a/material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/example/com/ApplicationTest.kt +++ b/material/rest-api-ui-ktor-quiz-collector/src/test/kotlin/com/worldline/training/ktor_quiz_collector/ApplicationTest.kt @@ -1,6 +1,6 @@ -package example.com +package com.worldline.training.ktor_quiz_collector -import example.com.plugins.configureRouting +import com.worldline.training.ktor_quiz_collector.plugins.configureRouting import io.ktor.client.request.* import io.ktor.http.* import io.ktor.server.testing.*