feat/task10 - Verify PIN, Architektur-Refactoring & Sonar Fixes #11
|
|
@ -6,37 +6,8 @@ import com.oliver.kidio.backend.domain.models.User
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
class InMemoryKidioRepository : KidioRepository {
|
class InMemoryKidioRepository : KidioRepository {
|
||||||
private val tasks =
|
private val tasks = mutableListOf<ImportedTask>()
|
||||||
mutableListOf(
|
|
||||||
ImportedTask(
|
|
||||||
id = "t1",
|
|
||||||
externalSource = "manual",
|
|
||||||
subject = "Mathematik",
|
|
||||||
minAge = 6,
|
|
||||||
maxAge = 10,
|
|
||||||
taskType = "MULTIPLE_CHOICE",
|
|
||||||
question = "Was ist 12 + 15?",
|
|
||||||
options = listOf("25", "27", "30", "22"),
|
|
||||||
correctAnswer = "27",
|
|
||||||
rewardMinutes = 10,
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
sourceUrl = "local",
|
|
||||||
),
|
|
||||||
ImportedTask(
|
|
||||||
id = "t2",
|
|
||||||
externalSource = "manual",
|
|
||||||
subject = "Mathematik",
|
|
||||||
minAge = 7,
|
|
||||||
maxAge = 12,
|
|
||||||
taskType = "MULTIPLE_CHOICE",
|
|
||||||
question = "Was ist 8 x 7?",
|
|
||||||
options = listOf("54", "56", "64", "48"),
|
|
||||||
correctAnswer = "56",
|
|
||||||
rewardMinutes = 15,
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
sourceUrl = "local",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
private val screentimes =
|
private val screentimes =
|
||||||
mutableMapOf(
|
mutableMapOf(
|
||||||
"default_user" to 1800,
|
"default_user" to 1800,
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,9 @@ object H5PIngestionCli {
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
): Int =
|
): Int =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
// Bereits vorhandene Task-IDs aus der Datenbank auslesen
|
|
||||||
val existingIds = repository.getExistingTaskIds("zum.de")
|
val existingIds = repository.getExistingTaskIds("zum.de")
|
||||||
println("Bereits in DB vorhandene ZUM-Aufgaben: ${existingIds.size}")
|
println("Bereits in DB vorhandene ZUM-Aufgaben: ${existingIds.size}")
|
||||||
|
|
||||||
var importedCount = 0
|
|
||||||
// existingIds an fetchLiveOerItems übergeben, um Duplikate vor dem Download zu filtern
|
|
||||||
val items =
|
val items =
|
||||||
client.fetchLiveOerItems(
|
client.fetchLiveOerItems(
|
||||||
subject = subject,
|
subject = subject,
|
||||||
|
|
@ -34,12 +31,39 @@ object H5PIngestionCli {
|
||||||
existingIds = existingIds,
|
existingIds = existingIds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var importedCount = 0
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
var parsedList = emptyList<ParsedH5PTask>()
|
importedCount += processItem(item, client, repository)
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Try parsing JSON content directly (fast, no ZIP decompression needed)
|
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
|
||||||
|
importedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processItem(
|
||||||
|
item: ZumOerItem,
|
||||||
|
client: ZumApiClient,
|
||||||
|
repository: KidioRepository,
|
||||||
|
): Int {
|
||||||
|
var importedCount = 0
|
||||||
|
val parsedList = extractTasks(item, client)
|
||||||
|
|
||||||
|
for (parsed in parsedList) {
|
||||||
|
val task = mapToImportedTask(item, parsed)
|
||||||
|
if (repository.upsertTask(task)) {
|
||||||
|
importedCount++
|
||||||
|
logSuccess(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return importedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun extractTasks(
|
||||||
|
item: ZumOerItem,
|
||||||
|
client: ZumApiClient,
|
||||||
|
): List<ParsedH5PTask> {
|
||||||
if (item.rawJsonContent != null) {
|
if (item.rawJsonContent != null) {
|
||||||
parsedList =
|
val tasks =
|
||||||
H5PParser.parseAllTasks(
|
H5PParser.parseAllTasks(
|
||||||
contentJson = item.rawJsonContent,
|
contentJson = item.rawJsonContent,
|
||||||
library = item.library ?: "H5P.MultiChoice",
|
library = item.library ?: "H5P.MultiChoice",
|
||||||
|
|
@ -48,23 +72,30 @@ object H5PIngestionCli {
|
||||||
license = item.license,
|
license = item.license,
|
||||||
sourceUrl = item.downloadUrl,
|
sourceUrl = item.downloadUrl,
|
||||||
)
|
)
|
||||||
|
if (tasks.isNotEmpty()) return tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback to downloading .h5p ZIP archive if remote archive is specified
|
if (item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
|
||||||
if (parsedList.isEmpty() && item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
|
val h5pBytes = client.downloadH5P(item.downloadUrl) ?: return emptyList()
|
||||||
val h5pBytes = client.downloadH5P(item.downloadUrl)
|
|
||||||
if (h5pBytes != null) {
|
|
||||||
val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl)
|
val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl)
|
||||||
|
|
||||||
if (single != null) {
|
if (single != null) {
|
||||||
parsedList = listOf(single)
|
return listOf(single)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (parsed in parsedList) {
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapToImportedTask(
|
||||||
|
item: ZumOerItem,
|
||||||
|
parsed: ParsedH5PTask,
|
||||||
|
): ImportedTask {
|
||||||
val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade)
|
val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade)
|
||||||
val task =
|
|
||||||
ImportedTask(
|
val isTrueFalse = parsed.options.size == 2 && parsed.options.contains("Wahr")
|
||||||
|
|
||||||
|
return ImportedTask(
|
||||||
id = parsed.id,
|
id = parsed.id,
|
||||||
externalSource = "zum.de",
|
externalSource = "zum.de",
|
||||||
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
|
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
|
||||||
|
|
@ -72,26 +103,19 @@ object H5PIngestionCli {
|
||||||
subject = ZumApiClient.mapSubject(item.subject),
|
subject = ZumApiClient.mapSubject(item.subject),
|
||||||
minAge = minAge,
|
minAge = minAge,
|
||||||
maxAge = maxAge,
|
maxAge = maxAge,
|
||||||
taskType = if (parsed.options.size == 2 && parsed.options.contains("Wahr")) "TRUE_FALSE" else "MULTIPLE_CHOICE",
|
taskType = if (isTrueFalse) "TRUE_FALSE" else "MULTIPLE_CHOICE",
|
||||||
question = parsed.question,
|
question = parsed.question,
|
||||||
options = parsed.options,
|
options = parsed.options,
|
||||||
correctAnswer = parsed.correctAnswer,
|
correctAnswer = parsed.correctAnswer,
|
||||||
rewardMinutes = 5,
|
rewardMinutes = 5,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val success = repository.upsertTask(task)
|
private fun logSuccess(task: ImportedTask) {
|
||||||
if (success) {
|
|
||||||
importedCount++
|
|
||||||
val shortQ = task.question.take(30)
|
val shortQ = task.question.take(30)
|
||||||
val ageRange = "${task.minAge}-${task.maxAge}"
|
val ageRange = "${task.minAge}-${task.maxAge}"
|
||||||
println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"")
|
println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
|
|
||||||
importedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,6 @@ import kotlinx.serialization.json.*
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
data class ParsedH5PTask(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val library: String,
|
|
||||||
val question: String,
|
|
||||||
val options: List<String>,
|
|
||||||
val correctAnswer: String,
|
|
||||||
val license: String = "CC BY-SA 4.0",
|
|
||||||
val sourceUrl: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
object H5PParser {
|
object H5PParser {
|
||||||
private val json =
|
private val json =
|
||||||
Json {
|
Json {
|
||||||
|
|
@ -79,9 +68,9 @@ object H5PParser {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var library = "H5P.MultiChoice"
|
var library = DEFAULT_LIBRARY
|
||||||
var title = "H5P Task"
|
var title = DEFAULT_TITLE
|
||||||
var license = "CC BY-SA 4.0"
|
var license = DEFAULT_LICENSE
|
||||||
|
|
||||||
if (h5pJsonString != null) {
|
if (h5pJsonString != null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -89,7 +78,8 @@ object H5PParser {
|
||||||
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
|
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
|
||||||
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
|
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
|
||||||
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
|
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
println("WARNUNG: h5p.json konnte nicht gelesen werden. Nutze Fallback-Werte. Fehler: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,10 +98,10 @@ object H5PParser {
|
||||||
*/
|
*/
|
||||||
fun parseContentJson(
|
fun parseContentJson(
|
||||||
contentJson: String,
|
contentJson: String,
|
||||||
library: String = "H5P.MultiChoice",
|
library: String = DEFAULT_LIBRARY,
|
||||||
taskId: String = "task-1",
|
taskId: String = "task-1",
|
||||||
title: String = "H5P Task",
|
title: String = DEFAULT_TITLE,
|
||||||
license: String = "CC BY-SA 4.0",
|
license: String = DEFAULT_LICENSE,
|
||||||
sourceUrl: String? = null,
|
sourceUrl: String? = null,
|
||||||
): ParsedH5PTask? {
|
): ParsedH5PTask? {
|
||||||
val element =
|
val element =
|
||||||
|
|
@ -133,10 +123,10 @@ object H5PParser {
|
||||||
*/
|
*/
|
||||||
fun parseAllTasks(
|
fun parseAllTasks(
|
||||||
contentJson: String,
|
contentJson: String,
|
||||||
library: String = "H5P.MultiChoice",
|
library: String = DEFAULT_LIBRARY,
|
||||||
taskId: String = "task-1",
|
taskId: String = "task-1",
|
||||||
title: String = "H5P Task",
|
title: String = DEFAULT_TITLE,
|
||||||
license: String = "CC BY-SA 4.0",
|
license: String = DEFAULT_LICENSE,
|
||||||
sourceUrl: String? = null,
|
sourceUrl: String? = null,
|
||||||
): List<ParsedH5PTask> {
|
): List<ParsedH5PTask> {
|
||||||
val element =
|
val element =
|
||||||
|
|
@ -148,40 +138,46 @@ object H5PParser {
|
||||||
|
|
||||||
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
|
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
|
||||||
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
|
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
|
||||||
val results = mutableListOf<ParsedH5PTask>()
|
|
||||||
|
|
||||||
for ((idx, qElement) in questionsArray.withIndex()) {
|
return questionsArray.mapIndexedNotNull { idx, qElement ->
|
||||||
val qObj = qElement.jsonObject
|
parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl)
|
||||||
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: "H5P.MultiChoice"
|
|
||||||
val subParams = qObj["params"]?.jsonObject ?: continue
|
|
||||||
val subId = "$taskId-q${idx + 1}"
|
|
||||||
|
|
||||||
val isTrueFalse =
|
|
||||||
subLib.contains("TrueFalse", ignoreCase = true) ||
|
|
||||||
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
|
|
||||||
val isMultiChoice =
|
|
||||||
subLib.contains("MultiChoice", ignoreCase = true) || subParams.containsKey("answers")
|
|
||||||
|
|
||||||
val parsedSub =
|
|
||||||
if (isTrueFalse) {
|
|
||||||
parseTrueFalse(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
|
|
||||||
} else if (isMultiChoice) {
|
|
||||||
parseMultiChoice(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedSub != null) {
|
|
||||||
results.add(parsedSub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl)
|
val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl)
|
||||||
return if (single != null) listOf(single) else emptyList()
|
return if (single != null) listOf(single) else emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseQuestionSetItem(
|
||||||
|
qElement: JsonElement,
|
||||||
|
idx: Int,
|
||||||
|
taskId: String,
|
||||||
|
title: String,
|
||||||
|
license: String,
|
||||||
|
sourceUrl: String?,
|
||||||
|
): ParsedH5PTask? {
|
||||||
|
val qObj = qElement.jsonObject
|
||||||
|
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: DEFAULT_LIBRARY
|
||||||
|
val subParams = qObj["params"]?.jsonObject ?: return null
|
||||||
|
|
||||||
|
val subId = "$taskId-q${idx + 1}"
|
||||||
|
val subTitle = "$title #${idx + 1}"
|
||||||
|
|
||||||
|
val isTrueFalse =
|
||||||
|
subLib.contains("TrueFalse", ignoreCase = true) ||
|
||||||
|
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
|
||||||
|
|
||||||
|
val isMultiChoice =
|
||||||
|
subLib.contains("MultiChoice", ignoreCase = true) ||
|
||||||
|
subParams.containsKey("answers")
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isTrueFalse -> parseTrueFalse(subParams, subId, subTitle, subLib, license, sourceUrl)
|
||||||
|
isMultiChoice -> parseMultiChoice(subParams, subId, subTitle, subLib, license, sourceUrl)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseMultiChoice(
|
private fun parseMultiChoice(
|
||||||
element: JsonObject,
|
element: JsonObject,
|
||||||
taskId: String,
|
taskId: String,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.oliver.kidio.backend.ingestion
|
||||||
|
|
||||||
|
const val DEFAULT_LIBRARY: String = "H5P.MultiChoice"
|
||||||
|
const val DEFAULT_TITLE: String = "H5P Task"
|
||||||
|
|
||||||
|
data class ParsedH5PTask(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val library: String,
|
||||||
|
val question: String,
|
||||||
|
val options: List<String>,
|
||||||
|
val correctAnswer: String,
|
||||||
|
val license: String = DEFAULT_LICENSE,
|
||||||
|
val sourceUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
@ -8,17 +8,6 @@ import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
data class ZumOerItem(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val subject: String,
|
|
||||||
val grade: Int? = null,
|
|
||||||
val downloadUrl: String,
|
|
||||||
val license: String = "CC BY-SA 4.0",
|
|
||||||
val library: String? = null,
|
|
||||||
val rawJsonContent: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
class ZumApiClient(
|
class ZumApiClient(
|
||||||
private val httpClient: HttpClient =
|
private val httpClient: HttpClient =
|
||||||
HttpClient.newBuilder()
|
HttpClient.newBuilder()
|
||||||
|
|
@ -113,52 +102,7 @@ class ZumApiClient(
|
||||||
|
|
||||||
for (page in 0 until maxPages) {
|
for (page in 0 until maxPages) {
|
||||||
if (results.size >= limit) break
|
if (results.size >= limit) break
|
||||||
|
processPage(page, subject, limit, existingIds, seenAppIds, results)
|
||||||
val searchUrl =
|
|
||||||
if (subject.isNullOrBlank()) {
|
|
||||||
"https://apps.zum.de/apps?page=$page"
|
|
||||||
} else {
|
|
||||||
val encoded = URLEncoder.encode(subject, "UTF-8")
|
|
||||||
"https://apps.zum.de/apps?keys=$encoded&page=$page"
|
|
||||||
}
|
|
||||||
|
|
||||||
val html = fetchString(searchUrl)
|
|
||||||
if (html == null) {
|
|
||||||
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtert bereits verarbeitete oder in DB vorhandene Apps heraus
|
|
||||||
val appIds =
|
|
||||||
Regex("""href=["']/apps/(\d+)["']""")
|
|
||||||
.findAll(html)
|
|
||||||
.map { it.groupValues[1] }
|
|
||||||
.distinct()
|
|
||||||
.filter { appId ->
|
|
||||||
val fullId = "zum-$appId"
|
|
||||||
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
if (appIds.isEmpty()) {
|
|
||||||
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (appId in appIds) {
|
|
||||||
if (results.size >= limit) break
|
|
||||||
seenAppIds.add(appId)
|
|
||||||
|
|
||||||
val item = fetchAppDetails(appId, subject)
|
|
||||||
if (item != null) {
|
|
||||||
results.add(item)
|
|
||||||
val lib = item.library ?: "H5P"
|
|
||||||
val gr = item.grade ?: "k.A."
|
|
||||||
println(
|
|
||||||
"[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
if (results.isEmpty()) {
|
||||||
|
|
@ -170,6 +114,78 @@ class ZumApiClient(
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun processPage(
|
||||||
|
page: Int,
|
||||||
|
subject: String?,
|
||||||
|
limit: Int,
|
||||||
|
existingIds: Set<String>,
|
||||||
|
seenAppIds: MutableSet<String>,
|
||||||
|
results: MutableList<ZumOerItem>,
|
||||||
|
) {
|
||||||
|
val searchUrl = buildSearchUrl(subject, page)
|
||||||
|
val html = fetchString(searchUrl)
|
||||||
|
|
||||||
|
if (html == null) {
|
||||||
|
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val appIds = extractNewAppIds(html, existingIds, seenAppIds)
|
||||||
|
|
||||||
|
if (appIds.isEmpty()) {
|
||||||
|
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (appId in appIds) {
|
||||||
|
if (results.size >= limit) break
|
||||||
|
seenAppIds.add(appId)
|
||||||
|
|
||||||
|
val item = fetchAppDetails(appId, subject)
|
||||||
|
if (item != null) {
|
||||||
|
results.add(item)
|
||||||
|
logNewItem(appId, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSearchUrl(
|
||||||
|
subject: String?,
|
||||||
|
page: Int,
|
||||||
|
): String {
|
||||||
|
return if (subject.isNullOrBlank()) {
|
||||||
|
"https://apps.zum.de/apps?page=$page"
|
||||||
|
} else {
|
||||||
|
val encoded = URLEncoder.encode(subject, "UTF-8")
|
||||||
|
"https://apps.zum.de/apps?keys=$encoded&page=$page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractNewAppIds(
|
||||||
|
html: String,
|
||||||
|
existingIds: Set<String>,
|
||||||
|
seenAppIds: Set<String>,
|
||||||
|
): List<String> {
|
||||||
|
return Regex("""href=["']/apps/(\d+)["']""")
|
||||||
|
.findAll(html)
|
||||||
|
.map { it.groupValues[1] }
|
||||||
|
.distinct()
|
||||||
|
.filter { appId ->
|
||||||
|
val fullId = "zum-$appId"
|
||||||
|
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logNewItem(
|
||||||
|
appId: String,
|
||||||
|
item: ZumOerItem,
|
||||||
|
) {
|
||||||
|
val lib = item.library ?: "H5P"
|
||||||
|
val gr = item.grade ?: "k.A."
|
||||||
|
println("[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]")
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchAppDetails(
|
private fun fetchAppDetails(
|
||||||
appId: String,
|
appId: String,
|
||||||
fallbackSubject: String?,
|
fallbackSubject: String?,
|
||||||
|
|
@ -216,7 +232,7 @@ class ZumApiClient(
|
||||||
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
|
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
|
||||||
val license =
|
val license =
|
||||||
when {
|
when {
|
||||||
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> "CC BY-SA 4.0"
|
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> DEFAULT_LICENSE
|
||||||
rawLicense.contains("CC", ignoreCase = true) -> rawLicense
|
rawLicense.contains("CC", ignoreCase = true) -> rawLicense
|
||||||
else -> "CC BY-SA 4.0 ($rawLicense)"
|
else -> "CC BY-SA 4.0 ($rawLicense)"
|
||||||
}
|
}
|
||||||
|
|
@ -338,168 +354,26 @@ class ZumApiClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns curated ZUM/OER sample metadata items, filterable by subject.
|
* Returns curated ZUM/OER sample metadata items from resources, filterable by subject.
|
||||||
*/
|
*/
|
||||||
fun getCuratedOerItems(
|
fun getCuratedOerItems(
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
limit: Int = 20,
|
limit: Int = 20,
|
||||||
): List<ZumOerItem> {
|
): List<ZumOerItem> {
|
||||||
val allItems =
|
val allItems: List<ZumOerItem> =
|
||||||
listOf(
|
try {
|
||||||
ZumOerItem(
|
val inputStream = this::class.java.getResourceAsStream("/fallback_oer_items.json")
|
||||||
id = "zum-math-001",
|
if (inputStream != null) {
|
||||||
title = "Grundrechenarten: Multiplikation 3. Klasse",
|
val jsonText = inputStream.bufferedReader().use { it.readText() }
|
||||||
subject = "Mathematik",
|
json.decodeFromString(jsonText)
|
||||||
grade = 3,
|
} else {
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/math-001.h5p",
|
println("WARNUNG: fallback_oer_items.json wurde nicht gefunden!")
|
||||||
license = "CC BY-SA 4.0",
|
emptyList()
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Was ist das Ergebnis von <strong>6 × 8</strong>?</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "42", "correct": false},
|
|
||||||
{"text": "48", "correct": true},
|
|
||||||
{"text": "54", "correct": false},
|
|
||||||
{"text": "56", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
""".trimIndent(),
|
} catch (e: Exception) {
|
||||||
),
|
println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}")
|
||||||
ZumOerItem(
|
emptyList()
|
||||||
id = "zum-math-002",
|
|
||||||
title = "Zahlenraum bis 100: Addition",
|
|
||||||
subject = "Mathematik",
|
|
||||||
grade = 2,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/math-002.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Berechne: 34 + 28</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "52", "correct": false},
|
|
||||||
{"text": "62", "correct": true},
|
|
||||||
{"text": "64", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-math-003",
|
|
||||||
title = "Geometrie: Formen erkennen",
|
|
||||||
subject = "Mathematik",
|
|
||||||
grade = 4,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/math-003.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>",
|
|
||||||
"correct": true
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-de-001",
|
|
||||||
title = "Wortarten: Nomen erkennen",
|
|
||||||
subject = "Deutsch",
|
|
||||||
grade = 2,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/de-001.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Welches der folgenden Wörter ist ein <strong>Nomen (Namenwort)</strong>?</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "schnell", "correct": false},
|
|
||||||
{"text": "laufen", "correct": false},
|
|
||||||
{"text": "Schule", "correct": true},
|
|
||||||
{"text": "schön", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-de-002",
|
|
||||||
title = "Rechtschreibung: Doppel-s oder ss",
|
|
||||||
subject = "Deutsch",
|
|
||||||
grade = 4,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/de-002.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "Flus", "correct": false},
|
|
||||||
{"text": "Fluss", "correct": true},
|
|
||||||
{"text": "Fluß", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-sach-001",
|
|
||||||
title = "Natur & Tiere: Winterschlaf",
|
|
||||||
subject = "Sachunterricht",
|
|
||||||
grade = 3,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/sach-001.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Welches dieser Tiere hält einen echten <strong>Winterschlaf</strong>?</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "Igel", "correct": true},
|
|
||||||
{"text": "Eichhörnchen", "correct": false},
|
|
||||||
{"text": "Amsel", "correct": false},
|
|
||||||
{"text": "Fuchs", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-sach-002",
|
|
||||||
title = "Planetensystem: Sonnennähe",
|
|
||||||
subject = "Sachunterricht",
|
|
||||||
grade = 4,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/sach-002.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>Welcher Planet ist der Sonne am nächsten?</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "Venus", "correct": false},
|
|
||||||
{"text": "Mars", "correct": false},
|
|
||||||
{"text": "Merkur", "correct": true},
|
|
||||||
{"text": "Jupiter", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
ZumOerItem(
|
|
||||||
id = "zum-eng-001",
|
|
||||||
title = "English Vocabulary: Colors & Animals",
|
|
||||||
subject = "Englisch",
|
|
||||||
grade = 4,
|
|
||||||
downloadUrl = "https://apps.zum.de/api/h5p/eng-001.h5p",
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
rawJsonContent =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"question": "<p>What is the English word for „Hund“?</p>",
|
|
||||||
"answers": [
|
|
||||||
{"text": "Cat", "correct": false},
|
|
||||||
{"text": "Dog", "correct": true},
|
|
||||||
{"text": "Bird", "correct": false},
|
|
||||||
{"text": "Rabbit", "correct": false}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val normalizedSubject = subject?.let { mapSubject(it) }
|
val normalizedSubject = subject?.let { mapSubject(it) }
|
||||||
val filtered =
|
val filtered =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.oliver.kidio.backend.ingestion
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
const val DEFAULT_LICENSE: String = "CC BY-SA 4.0"
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ZumOerItem(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val subject: String,
|
||||||
|
val grade: Int? = null,
|
||||||
|
val downloadUrl: String,
|
||||||
|
val license: String = DEFAULT_LICENSE,
|
||||||
|
val library: String? = null,
|
||||||
|
val rawJsonContent: String? = null,
|
||||||
|
)
|
||||||
74
src/main/resources/fallback_oer_items.json
Normal file
74
src/main/resources/fallback_oer_items.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "zum-math-001",
|
||||||
|
"title": "Grundrechenarten: Multiplikation 3. Klasse",
|
||||||
|
"subject": "Mathematik",
|
||||||
|
"grade": 3,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/math-001.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Was ist das Ergebnis von <strong>6 × 8</strong>?</p>\", \"answers\": [ {\"text\": \"42\", \"correct\": false}, {\"text\": \"48\", \"correct\": true}, {\"text\": \"54\", \"correct\": false}, {\"text\": \"56\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-math-002",
|
||||||
|
"title": "Zahlenraum bis 100: Addition",
|
||||||
|
"subject": "Mathematik",
|
||||||
|
"grade": 2,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/math-002.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Berechne: 34 + 28</p>\", \"answers\": [ {\"text\": \"52\", \"correct\": false}, {\"text\": \"62\", \"correct\": true}, {\"text\": \"64\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-math-003",
|
||||||
|
"title": "Geometrie: Formen erkennen",
|
||||||
|
"subject": "Mathematik",
|
||||||
|
"grade": 4,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/math-003.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>\", \"correct\": true }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-de-001",
|
||||||
|
"title": "Wortarten: Nomen erkennen",
|
||||||
|
"subject": "Deutsch",
|
||||||
|
"grade": 2,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/de-001.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Welches der folgenden Wörter ist ein <strong>Nomen (Namenwort)</strong>?</p>\", \"answers\": [ {\"text\": \"schnell\", \"correct\": false}, {\"text\": \"laufen\", \"correct\": false}, {\"text\": \"Schule\", \"correct\": true}, {\"text\": \"schön\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-de-002",
|
||||||
|
"title": "Rechtschreibung: Doppel-s oder ss",
|
||||||
|
"subject": "Deutsch",
|
||||||
|
"grade": 4,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/de-002.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“</p>\", \"answers\": [ {\"text\": \"Flus\", \"correct\": false}, {\"text\": \"Fluss\", \"correct\": true}, {\"text\": \"Fluß\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-sach-001",
|
||||||
|
"title": "Natur & Tiere: Winterschlaf",
|
||||||
|
"subject": "Sachunterricht",
|
||||||
|
"grade": 3,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/sach-001.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Welches dieser Tiere hält einen echten <strong>Winterschlaf</strong>?</p>\", \"answers\": [ {\"text\": \"Igel\", \"correct\": true}, {\"text\": \"Eichhörnchen\", \"correct\": false}, {\"text\": \"Amsel\", \"correct\": false}, {\"text\": \"Fuchs\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-sach-002",
|
||||||
|
"title": "Planetensystem: Sonnennähe",
|
||||||
|
"subject": "Sachunterricht",
|
||||||
|
"grade": 4,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/sach-002.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>Welcher Planet ist der Sonne am nächsten?</p>\", \"answers\": [ {\"text\": \"Venus\", \"correct\": false}, {\"text\": \"Mars\", \"correct\": false}, {\"text\": \"Merkur\", \"correct\": true}, {\"text\": \"Jupiter\", \"correct\": false} ] }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zum-eng-001",
|
||||||
|
"title": "English Vocabulary: Colors & Animals",
|
||||||
|
"subject": "Englisch",
|
||||||
|
"grade": 4,
|
||||||
|
"downloadUrl": "https://apps.zum.de/api/h5p/eng-001.h5p",
|
||||||
|
"license": "CC BY-SA 4.0",
|
||||||
|
"rawJsonContent": "{ \"question\": \"<p>What is the English word for „Hund“?</p>\", \"answers\": [ {\"text\": \"Cat\", \"correct\": false}, {\"text\": \"Dog\", \"correct\": true}, {\"text\": \"Bird\", \"correct\": false}, {\"text\": \"Rabbit\", \"correct\": false} ] }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.oliver.kidio.backend.com.oliver.kidio.backend.routing
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
import com.oliver.kidio.backend.domain.models.AuthResponse
|
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||||
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
import com.oliver.kidio.backend.domain.models.SyncResponse
|
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||||
import com.oliver.kidio.backend.domain.models.Task
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||||
|
|
@ -19,6 +20,7 @@ import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.testing.testApplication
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
@ -34,6 +36,41 @@ class ServerTest {
|
||||||
val jwtService = JwtService()
|
val jwtService = JwtService()
|
||||||
val repository = InMemoryKidioRepository()
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t1",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 6,
|
||||||
|
maxAge = 10,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 12 + 15?",
|
||||||
|
options = listOf("25", "27", "30", "22"),
|
||||||
|
correctAnswer = "27",
|
||||||
|
rewardMinutes = 10,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t2",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 7,
|
||||||
|
maxAge = 12,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 8 x 7?",
|
||||||
|
options = listOf("54", "56", "64", "48"),
|
||||||
|
correctAnswer = "56",
|
||||||
|
rewardMinutes = 15,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
|
@ -193,6 +230,41 @@ class ServerTest {
|
||||||
val jwtService = JwtService()
|
val jwtService = JwtService()
|
||||||
val repository = InMemoryKidioRepository()
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t1",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 6,
|
||||||
|
maxAge = 10,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 12 + 15?",
|
||||||
|
options = listOf("25", "27", "30", "22"),
|
||||||
|
correctAnswer = "27",
|
||||||
|
rewardMinutes = 10,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t2",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 7,
|
||||||
|
maxAge = 12,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 8 x 7?",
|
||||||
|
options = listOf("54", "56", "64", "48"),
|
||||||
|
correctAnswer = "56",
|
||||||
|
rewardMinutes = 15,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.oliver.kidio.backend.routing
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
import com.oliver.kidio.backend.domain.models.Task
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
|
|
@ -17,6 +18,7 @@ import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.testing.testApplication
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
@ -50,6 +52,41 @@ class TaskRoutesTest {
|
||||||
val jwtService = JwtService()
|
val jwtService = JwtService()
|
||||||
val repository = InMemoryKidioRepository()
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t1",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 6,
|
||||||
|
maxAge = 10,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 12 + 15?",
|
||||||
|
options = listOf("25", "27", "30", "22"),
|
||||||
|
correctAnswer = "27",
|
||||||
|
rewardMinutes = 10,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
repository.upsertTask(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t2",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 7,
|
||||||
|
maxAge = 12,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 8 x 7?",
|
||||||
|
options = listOf("54", "56", "64", "48"),
|
||||||
|
correctAnswer = "56",
|
||||||
|
rewardMinutes = 15,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue