diff --git a/src/main/kotlin/com/oliver/kidio/backend/domain/repository/InMemoryKidioRepository.kt b/src/main/kotlin/com/oliver/kidio/backend/domain/repository/InMemoryKidioRepository.kt index b9289c7..0d5579c 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/domain/repository/InMemoryKidioRepository.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/domain/repository/InMemoryKidioRepository.kt @@ -6,37 +6,8 @@ import com.oliver.kidio.backend.domain.models.User import org.mindrot.jbcrypt.BCrypt class InMemoryKidioRepository : KidioRepository { - private val tasks = - 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 tasks = mutableListOf() + private val screentimes = mutableMapOf( "default_user" to 1800, diff --git a/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PIngestionCli.kt b/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PIngestionCli.kt index bd82a80..7bc4d7e 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PIngestionCli.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PIngestionCli.kt @@ -21,12 +21,9 @@ object H5PIngestionCli { limit: Int = 50, ): Int = runBlocking { - // Bereits vorhandene Task-IDs aus der Datenbank auslesen val existingIds = repository.getExistingTaskIds("zum.de") 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 = client.fetchLiveOerItems( subject = subject, @@ -34,65 +31,92 @@ object H5PIngestionCli { existingIds = existingIds, ) + var importedCount = 0 for (item in items) { - var parsedList = emptyList() - - // 1. Try parsing JSON content directly (fast, no ZIP decompression needed) - if (item.rawJsonContent != null) { - parsedList = - H5PParser.parseAllTasks( - contentJson = item.rawJsonContent, - library = item.library ?: "H5P.MultiChoice", - taskId = item.id, - title = item.title, - license = item.license, - sourceUrl = item.downloadUrl, - ) - } - - // 2. Fallback to downloading .h5p ZIP archive if remote archive is specified - if (parsedList.isEmpty() && item.downloadUrl.endsWith(".h5p", ignoreCase = true)) { - val h5pBytes = client.downloadH5P(item.downloadUrl) - if (h5pBytes != null) { - val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl) - if (single != null) { - parsedList = listOf(single) - } - } - } - - for (parsed in parsedList) { - val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade) - val task = - ImportedTask( - id = parsed.id, - externalSource = "zum.de", - sourceUrl = parsed.sourceUrl ?: item.downloadUrl, - license = parsed.license, - subject = ZumApiClient.mapSubject(item.subject), - minAge = minAge, - maxAge = maxAge, - taskType = if (parsed.options.size == 2 && parsed.options.contains("Wahr")) "TRUE_FALSE" else "MULTIPLE_CHOICE", - question = parsed.question, - options = parsed.options, - correctAnswer = parsed.correctAnswer, - rewardMinutes = 5, - ) - - val success = repository.upsertTask(task) - if (success) { - importedCount++ - val shortQ = task.question.take(30) - val ageRange = "${task.minAge}-${task.maxAge}" - println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"") - } - } + importedCount += processItem(item, client, repository) } 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 { + if (item.rawJsonContent != null) { + val tasks = + H5PParser.parseAllTasks( + contentJson = item.rawJsonContent, + library = item.library ?: "H5P.MultiChoice", + taskId = item.id, + title = item.title, + license = item.license, + sourceUrl = item.downloadUrl, + ) + if (tasks.isNotEmpty()) return tasks + } + + if (item.downloadUrl.endsWith(".h5p", ignoreCase = true)) { + val h5pBytes = client.downloadH5P(item.downloadUrl) ?: return emptyList() + val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl) + + if (single != null) { + return listOf(single) + } + } + + return emptyList() + } + + private fun mapToImportedTask( + item: ZumOerItem, + parsed: ParsedH5PTask, + ): ImportedTask { + val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade) + + val isTrueFalse = parsed.options.size == 2 && parsed.options.contains("Wahr") + + return ImportedTask( + id = parsed.id, + externalSource = "zum.de", + sourceUrl = parsed.sourceUrl ?: item.downloadUrl, + license = parsed.license, + subject = ZumApiClient.mapSubject(item.subject), + minAge = minAge, + maxAge = maxAge, + taskType = if (isTrueFalse) "TRUE_FALSE" else "MULTIPLE_CHOICE", + question = parsed.question, + options = parsed.options, + correctAnswer = parsed.correctAnswer, + rewardMinutes = 5, + ) + } + + private fun logSuccess(task: ImportedTask) { + val shortQ = task.question.take(30) + val ageRange = "${task.minAge}-${task.maxAge}" + println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"") + } + @JvmStatic fun main(args: Array) { println("=== Kidio H5P/ZUM Ingestion Tool ===") diff --git a/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PParser.kt b/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PParser.kt index 32f1a4a..8ded75f 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PParser.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/ingestion/H5PParser.kt @@ -4,17 +4,6 @@ import kotlinx.serialization.json.* import java.io.InputStream import java.util.zip.ZipInputStream -data class ParsedH5PTask( - val id: String, - val title: String, - val library: String, - val question: String, - val options: List, - val correctAnswer: String, - val license: String = "CC BY-SA 4.0", - val sourceUrl: String? = null, -) - object H5PParser { private val json = Json { @@ -79,9 +68,9 @@ object H5PParser { return null } - var library = "H5P.MultiChoice" - var title = "H5P Task" - var license = "CC BY-SA 4.0" + var library = DEFAULT_LIBRARY + var title = DEFAULT_TITLE + var license = DEFAULT_LICENSE if (h5pJsonString != null) { try { @@ -89,7 +78,8 @@ object H5PParser { library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title 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( contentJson: String, - library: String = "H5P.MultiChoice", + library: String = DEFAULT_LIBRARY, taskId: String = "task-1", - title: String = "H5P Task", - license: String = "CC BY-SA 4.0", + title: String = DEFAULT_TITLE, + license: String = DEFAULT_LICENSE, sourceUrl: String? = null, ): ParsedH5PTask? { val element = @@ -133,10 +123,10 @@ object H5PParser { */ fun parseAllTasks( contentJson: String, - library: String = "H5P.MultiChoice", + library: String = DEFAULT_LIBRARY, taskId: String = "task-1", - title: String = "H5P Task", - license: String = "CC BY-SA 4.0", + title: String = DEFAULT_TITLE, + license: String = DEFAULT_LICENSE, sourceUrl: String? = null, ): List { val element = @@ -148,40 +138,46 @@ object H5PParser { if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) { val questionsArray = element["questions"]?.jsonArray ?: return emptyList() - val results = mutableListOf() - for ((idx, qElement) in questionsArray.withIndex()) { - val qObj = qElement.jsonObject - 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 questionsArray.mapIndexedNotNull { idx, qElement -> + parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl) } - return results } val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl) 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( element: JsonObject, taskId: String, diff --git a/src/main/kotlin/com/oliver/kidio/backend/ingestion/ParsedH5PTask.kt b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ParsedH5PTask.kt new file mode 100644 index 0000000..3251d20 --- /dev/null +++ b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ParsedH5PTask.kt @@ -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, + val correctAnswer: String, + val license: String = DEFAULT_LICENSE, + val sourceUrl: String? = null, +) diff --git a/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumApiClient.kt b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumApiClient.kt index eec9bf1..4a38851 100644 --- a/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumApiClient.kt +++ b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumApiClient.kt @@ -8,17 +8,6 @@ import java.net.http.HttpRequest import java.net.http.HttpResponse 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( private val httpClient: HttpClient = HttpClient.newBuilder() @@ -113,52 +102,7 @@ class ZumApiClient( for (page in 0 until maxPages) { if (results.size >= limit) break - - 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]", - ) - } - } + processPage(page, subject, limit, existingIds, seenAppIds, results) } if (results.isEmpty()) { @@ -170,6 +114,78 @@ class ZumApiClient( return results } + private fun processPage( + page: Int, + subject: String?, + limit: Int, + existingIds: Set, + seenAppIds: MutableSet, + results: MutableList, + ) { + 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, + seenAppIds: Set, + ): List { + 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( appId: String, fallbackSubject: String?, @@ -216,7 +232,7 @@ class ZumApiClient( val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull val license = 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 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( subject: String? = null, limit: Int = 20, ): List { - val allItems = - listOf( - ZumOerItem( - 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": "

Was ist das Ergebnis von 6 × 8?

", - "answers": [ - {"text": "42", "correct": false}, - {"text": "48", "correct": true}, - {"text": "54", "correct": false}, - {"text": "56", "correct": false} - ] - } - """.trimIndent(), - ), - ZumOerItem( - 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": "

Berechne: 34 + 28

", - "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": "

Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.

", - "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": "

Welches der folgenden Wörter ist ein Nomen (Namenwort)?

", - "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": "

Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“

", - "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": "

Welches dieser Tiere hält einen echten Winterschlaf?

", - "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": "

Welcher Planet ist der Sonne am nächsten?

", - "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": "

What is the English word for „Hund“?

", - "answers": [ - {"text": "Cat", "correct": false}, - {"text": "Dog", "correct": true}, - {"text": "Bird", "correct": false}, - {"text": "Rabbit", "correct": false} - ] - } - """.trimIndent(), - ), - ) + val allItems: List = + try { + val inputStream = this::class.java.getResourceAsStream("/fallback_oer_items.json") + if (inputStream != null) { + val jsonText = inputStream.bufferedReader().use { it.readText() } + json.decodeFromString(jsonText) + } else { + println("WARNUNG: fallback_oer_items.json wurde nicht gefunden!") + emptyList() + } + } catch (e: Exception) { + println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}") + emptyList() + } val normalizedSubject = subject?.let { mapSubject(it) } val filtered = diff --git a/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumOerItem.kt b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumOerItem.kt new file mode 100644 index 0000000..7e42dba --- /dev/null +++ b/src/main/kotlin/com/oliver/kidio/backend/ingestion/ZumOerItem.kt @@ -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, +) diff --git a/src/main/resources/fallback_oer_items.json b/src/main/resources/fallback_oer_items.json new file mode 100644 index 0000000..1b11825 --- /dev/null +++ b/src/main/resources/fallback_oer_items.json @@ -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\": \"

Was ist das Ergebnis von 6 × 8?

\", \"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\": \"

Berechne: 34 + 28

\", \"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\": \"

Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.

\", \"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\": \"

Welches der folgenden Wörter ist ein Nomen (Namenwort)?

\", \"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\": \"

Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“

\", \"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\": \"

Welches dieser Tiere hält einen echten Winterschlaf?

\", \"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\": \"

Welcher Planet ist der Sonne am nächsten?

\", \"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\": \"

What is the English word for „Hund“?

\", \"answers\": [ {\"text\": \"Cat\", \"correct\": false}, {\"text\": \"Dog\", \"correct\": true}, {\"text\": \"Bird\", \"correct\": false}, {\"text\": \"Rabbit\", \"correct\": false} ] }" + } +] diff --git a/src/test/kotlin/com/oliver/kidio/backend/routing/ServerTest.kt b/src/test/kotlin/com/oliver/kidio/backend/routing/ServerTest.kt index 5ee7b72..4d9c567 100644 --- a/src/test/kotlin/com/oliver/kidio/backend/routing/ServerTest.kt +++ b/src/test/kotlin/com/oliver/kidio/backend/routing/ServerTest.kt @@ -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.ImportedTask import com.oliver.kidio.backend.domain.models.SyncResponse import com.oliver.kidio.backend.domain.models.Task 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.server.application.install import io.ktor.server.testing.testApplication +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals @@ -34,6 +36,41 @@ class ServerTest { val jwtService = JwtService() 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 { install(ServerContentNegotiation) { json() @@ -193,6 +230,41 @@ class ServerTest { val jwtService = JwtService() 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 { install(ServerContentNegotiation) { json() diff --git a/src/test/kotlin/com/oliver/kidio/backend/routing/TaskRoutesTest.kt b/src/test/kotlin/com/oliver/kidio/backend/routing/TaskRoutesTest.kt index 485ecfa..ce5cb38 100644 --- a/src/test/kotlin/com/oliver/kidio/backend/routing/TaskRoutesTest.kt +++ b/src/test/kotlin/com/oliver/kidio/backend/routing/TaskRoutesTest.kt @@ -1,5 +1,6 @@ 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.TaskVerifyResponse 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.server.application.install import io.ktor.server.testing.testApplication +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals @@ -50,6 +52,41 @@ class TaskRoutesTest { val jwtService = JwtService() 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 { install(ServerContentNegotiation) { json()