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
|
||||
|
||||
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<ImportedTask>()
|
||||
|
||||
private val screentimes =
|
||||
mutableMapOf(
|
||||
"default_user" to 1800,
|
||||
|
|
|
|||
|
|
@ -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,12 +31,39 @@ object H5PIngestionCli {
|
|||
existingIds = existingIds,
|
||||
)
|
||||
|
||||
var importedCount = 0
|
||||
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) {
|
||||
parsedList =
|
||||
val tasks =
|
||||
H5PParser.parseAllTasks(
|
||||
contentJson = item.rawJsonContent,
|
||||
library = item.library ?: "H5P.MultiChoice",
|
||||
|
|
@ -48,23 +72,30 @@ object H5PIngestionCli {
|
|||
license = item.license,
|
||||
sourceUrl = item.downloadUrl,
|
||||
)
|
||||
if (tasks.isNotEmpty()) return tasks
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
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 task =
|
||||
ImportedTask(
|
||||
|
||||
val isTrueFalse = parsed.options.size == 2 && parsed.options.contains("Wahr")
|
||||
|
||||
return ImportedTask(
|
||||
id = parsed.id,
|
||||
externalSource = "zum.de",
|
||||
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
|
||||
|
|
@ -72,26 +103,19 @@ object H5PIngestionCli {
|
|||
subject = ZumApiClient.mapSubject(item.subject),
|
||||
minAge = minAge,
|
||||
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,
|
||||
options = parsed.options,
|
||||
correctAnswer = parsed.correctAnswer,
|
||||
rewardMinutes = 5,
|
||||
)
|
||||
}
|
||||
|
||||
val success = repository.upsertTask(task)
|
||||
if (success) {
|
||||
importedCount++
|
||||
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...\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
|
||||
importedCount
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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<ParsedH5PTask> {
|
||||
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<ParsedH5PTask>()
|
||||
|
||||
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
|
||||
return questionsArray.mapIndexedNotNull { idx, qElement ->
|
||||
parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl)
|
||||
}
|
||||
|
||||
if (parsedSub != null) {
|
||||
results.add(parsedSub)
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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.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<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(
|
||||
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<ZumOerItem> {
|
||||
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": "<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}
|
||||
]
|
||||
val allItems: List<ZumOerItem> =
|
||||
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()
|
||||
}
|
||||
""".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": "<p>Berechne: 34 + 28</p>",
|
||||
"answers": [
|
||||
{"text": "52", "correct": false},
|
||||
{"text": "62", "correct": true},
|
||||
{"text": "64", "correct": false}
|
||||
]
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
""".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 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.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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue