From 9896ce863ef9972838103ddf6c2c8e42cf4badd1 Mon Sep 17 00:00:00 2001 From: sszuev Date: Sat, 25 Nov 2023 22:01:26 +0300 Subject: [PATCH 1/6] db & core: replace `CardsDbResponse#sourceLanguageId` with `CardsDbResponse#dictionaries` --- .../kotlin/repositories/DbCardRepository.kt | 7 ++-- .../kotlin/processes/CardProcessWorkers.kt | 27 +++++++------ .../kotlin/processes/SearchCardsHelper.kt | 13 ++++++- db-common/src/main/kotlin/CommonErrors.kt | 9 ----- .../src/main/kotlin/documents/xml/DOMUtils.kt | 32 +++++---------- .../kotlin/DbCardRepositoryTest.kt | 39 ++++++++++++++----- db-mem/src/main/kotlin/MemDbCardRepository.kt | 33 +++++++--------- db-pg/src/main/kotlin/PgDbCardRepository.kt | 33 +++++++--------- 8 files changed, 93 insertions(+), 100 deletions(-) diff --git a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt index aeac20a1..d9374c05 100644 --- a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt +++ b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt @@ -6,8 +6,8 @@ import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId import com.gitlab.sszuev.flashcards.model.domain.CardLearn +import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.LangId /** * Database repository to work with cards. @@ -57,11 +57,12 @@ interface DbCardRepository { data class CardsDbResponse( val cards: List = emptyList(), - val sourceLanguageId: LangId = LangId.NONE, + val dictionaries: List = emptyList(), val errors: List = emptyList(), ) { + companion object { - val EMPTY = CardsDbResponse(cards = emptyList(), errors = emptyList()) + val EMPTY = CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = emptyList()) } } diff --git a/core/src/main/kotlin/processes/CardProcessWorkers.kt b/core/src/main/kotlin/processes/CardProcessWorkers.kt index 3e92f410..4737a2c1 100644 --- a/core/src/main/kotlin/processes/CardProcessWorkers.kt +++ b/core/src/main/kotlin/processes/CardProcessWorkers.kt @@ -6,7 +6,6 @@ import com.gitlab.sszuev.flashcards.corlib.ChainDSL import com.gitlab.sszuev.flashcards.corlib.worker import com.gitlab.sszuev.flashcards.model.common.AppStatus import com.gitlab.sszuev.flashcards.model.domain.CardOperation -import com.gitlab.sszuev.flashcards.model.domain.LangId import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet import com.gitlab.sszuev.flashcards.model.domain.TTSResourceId import com.gitlab.sszuev.flashcards.repositories.CardDbResponse @@ -149,21 +148,21 @@ fun ChainDSL.processDeleteCard() = worker { } private suspend fun CardContext.postProcess(res: CardsDbResponse) { - if (res.errors.isNotEmpty()) { - this.errors.addAll(res.errors) - } - if (res.sourceLanguageId != LangId.NONE) { - val tts = this.repositories.ttsClientRepository(this.workMode) - this.responseCardEntityList = res.cards.map { card -> - val words = card.words.map { word -> - val audio = tts.findResourceId(TTSResourceGet(word.word, res.sourceLanguageId).normalize()) - this.errors.addAll(audio.errors) - if (audio.id != TTSResourceId.NONE) word.copy(sound = audio.id) else word + this.errors.addAll(res.errors) + val sourceLangByDictionary = res.dictionaries.associate { it.dictionaryId to it.sourceLang.langId } + val tts = this.repositories.ttsClientRepository(this.workMode) + this.responseCardEntityList = res.cards.map { card -> + val sourceLang = sourceLangByDictionary[card.dictionaryId] ?: return@map card + val words = card.words.map { word -> + val audio = tts.findResourceId(TTSResourceGet(word.word, sourceLang).normalize()) + this.errors.addAll(audio.errors) + if (audio.id != TTSResourceId.NONE) { + word.copy(sound = audio.id) + } else { + word } - card.copy(words = words) } - } else { - this.responseCardEntityList = res.cards + card.copy(words = words) } this.status = if (this.errors.isNotEmpty()) AppStatus.FAIL else AppStatus.RUN } diff --git a/core/src/main/kotlin/processes/SearchCardsHelper.kt b/core/src/main/kotlin/processes/SearchCardsHelper.kt index b1664bf4..08755f0c 100644 --- a/core/src/main/kotlin/processes/SearchCardsHelper.kt +++ b/core/src/main/kotlin/processes/SearchCardsHelper.kt @@ -7,6 +7,15 @@ import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse * Prepares a card deck for a tutor-session. */ fun CardContext.findCardDeck(): CardsDbResponse { - val userId = this.contextUserEntity.id - return this.repositories.cardRepository(this.workMode).searchCard(userId, this.normalizedRequestCardFilter) + return if (this.normalizedRequestCardFilter.random) { + // For random mode, do not use the database support since logic is quite complicated. + // We load everything into memory, since the dictionary can hardly contain more than a thousand words, + // i.e., this is quite small data. + //TODO: implement new logic + return this.repositories.cardRepository(this.workMode) + .searchCard(this.contextUserEntity.id, this.normalizedRequestCardFilter) + } else { + this.repositories.cardRepository(this.workMode) + .searchCard(this.contextUserEntity.id, this.normalizedRequestCardFilter) + } } \ No newline at end of file diff --git a/db-common/src/main/kotlin/CommonErrors.kt b/db-common/src/main/kotlin/CommonErrors.kt index 1b0c91a5..0bdd669f 100644 --- a/db-common/src/main/kotlin/CommonErrors.kt +++ b/db-common/src/main/kotlin/CommonErrors.kt @@ -7,15 +7,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardId import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -fun wrongDictionaryLanguageFamiliesDbError( - operation: String, - dictionaryIds: Collection, -) = dbError( - operation = operation, - fieldName = dictionaryIds.joinToString { it.asString() }, - details = """specified dictionaries belong to different language families, ids="${dictionaryIds.map { it.asString() }}"""" -) - fun forbiddenEntityDbError( operation: String, entityId: Id, diff --git a/db-common/src/main/kotlin/documents/xml/DOMUtils.kt b/db-common/src/main/kotlin/documents/xml/DOMUtils.kt index 32ae92ae..26d625f7 100644 --- a/db-common/src/main/kotlin/documents/xml/DOMUtils.kt +++ b/db-common/src/main/kotlin/documents/xml/DOMUtils.kt @@ -1,11 +1,14 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + package com.gitlab.sszuev.flashcards.common.documents.xml import org.w3c.dom.Element import org.w3c.dom.Node +import org.w3c.dom.NodeList internal object DOMUtils { /** - * Gets element by the specified tag or throws an error. + * Gets an element by the specified tag or throws an error. * * @param [tag][String] * @return [Element] @@ -22,31 +25,14 @@ internal object DOMUtils { * @return [Sequence] of [Element]s */ fun Element.elements(tag: String): Sequence { - return children(this).mapNotNull { it as? Element } .filter { it.tagName == tag } - } - - /** - * Lists all direct children of the given element. - * - * @param [parent][Element] - * @return [Sequence] of [Element]s - */ - private fun children(parent: Element): Sequence { - return listChildren(parent).asSequence() + return this.children().mapNotNull { it as? Element }.filter { it.tagName == tag } } - private fun listChildren(parent: Element): Iterator { - val list = parent.childNodes - val length = list.length - return object : Iterator { - var index = 0 - override fun hasNext(): Boolean { - return index < length - 1 - } + fun Element.children(): Sequence = this.childNodes.children() - override fun next(): Node { - return list.item(index++) as Node - } + fun NodeList.children(): Sequence = sequence { + (0 until this@children.length).forEach { + yield(this@children.item(it)) } } diff --git a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt index ff2e0cef..9c8f6647 100644 --- a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt +++ b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt @@ -3,12 +3,24 @@ package com.gitlab.sszuev.flashcards.dbcommon import com.gitlab.sszuev.flashcards.model.common.AppError import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.common.NONE -import com.gitlab.sszuev.flashcards.model.domain.* +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardFilter +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardLearn +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.model.domain.Stage import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.DbCardRepository import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse import kotlinx.datetime.Instant -import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder /** * Note: all implementations must have the same ids in tests for the same entities to have deterministic behavior. @@ -179,14 +191,18 @@ abstract class DbCardRepositoryTest { val res1 = repository.getAllCards(userId, DictionaryId("1")) Assertions.assertEquals(244, res1.cards.size) Assertions.assertEquals(0, res1.errors.size) + Assertions.assertEquals(1, res1.dictionaries.size) + Assertions.assertEquals("1", res1.dictionaries.single().dictionaryId.asString()) // Weather dictionary val res2 = repository.getAllCards(userId, DictionaryId("2")) Assertions.assertEquals(65, res2.cards.size) Assertions.assertEquals(0, res2.errors.size) + Assertions.assertEquals(1, res2.dictionaries.size) + Assertions.assertEquals("2", res2.dictionaries.single().dictionaryId.asString()) - Assertions.assertEquals(LangId("en"), res1.sourceLanguageId) - Assertions.assertEquals(LangId("en"), res2.sourceLanguageId) + Assertions.assertEquals(LangId("en"), res1.dictionaries.single().sourceLang.langId) + Assertions.assertEquals(LangId("en"), res2.dictionaries.single().sourceLang.langId) } @Order(2) @@ -259,11 +275,16 @@ abstract class DbCardRepositoryTest { Assertions.assertEquals(300, res1.cards.size) Assertions.assertEquals(300, res2.cards.size) Assertions.assertNotEquals(res1, res2) - Assertions.assertEquals(setOf(DictionaryId("1"), DictionaryId("2")), res1.cards.map { it.dictionaryId }.toSet()) - Assertions.assertEquals(setOf(DictionaryId("1"), DictionaryId("2")), res2.cards.map { it.dictionaryId }.toSet()) - - Assertions.assertEquals(LangId("en"), res1.sourceLanguageId) - Assertions.assertEquals(LangId("en"), res2.sourceLanguageId) + Assertions.assertEquals(setOf("1", "2"), res1.cards.map { it.dictionaryId }.map { it.asString() }.toSet()) + Assertions.assertEquals(setOf("1", "2"), res2.cards.map { it.dictionaryId }.map { it.asString() }.toSet()) + Assertions.assertEquals( + setOf("1", "2"), + res1.dictionaries.map { it.dictionaryId }.map { it.asString() }.toSet() + ) + Assertions.assertEquals( + setOf("1", "2"), + res2.dictionaries.map { it.dictionaryId }.map { it.asString() }.toSet() + ) } @Order(6) diff --git a/db-mem/src/main/kotlin/MemDbCardRepository.kt b/db-mem/src/main/kotlin/MemDbCardRepository.kt index 5b175c3f..d96cbd28 100644 --- a/db-mem/src/main/kotlin/MemDbCardRepository.kt +++ b/db-mem/src/main/kotlin/MemDbCardRepository.kt @@ -12,7 +12,6 @@ import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate import com.gitlab.sszuev.flashcards.common.validateCardLearns -import com.gitlab.sszuev.flashcards.common.wrongDictionaryLanguageFamiliesDbError import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary import com.gitlab.sszuev.flashcards.model.Id @@ -37,8 +36,8 @@ class MemDbCardRepository( private val database = MemDatabase.get(dbConfig.dataLocation) override fun getCard(userId: AppUserId, cardId: CardId): CardDbResponse { - val card = database.findCardById(cardId.asLong()) ?: - return CardDbResponse(noCardFoundDbError("getCard", cardId)) + val card = + database.findCardById(cardId.asLong()) ?: return CardDbResponse(noCardFoundDbError("getCard", cardId)) val errors = mutableListOf() checkDictionaryUser("getCard", userId, card.dictionaryId.asDictionaryId(), cardId, errors) if (errors.isNotEmpty()) { @@ -51,37 +50,31 @@ class MemDbCardRepository( val id = dictionaryId.asLong() val errors = mutableListOf() val dictionary = checkDictionaryUser("getAllCards", userId, dictionaryId, dictionaryId, errors) - if (errors.isNotEmpty()) { + if (errors.isNotEmpty() || dictionary == null) { return CardsDbResponse(errors = errors) } val cards = database.findCardsByDictionaryId(id).map { it.toCardEntity() }.toList() + val dictionaries = listOf(dictionary.toDictionaryEntity()) return CardsDbResponse( cards = cards, - sourceLanguageId = checkNotNull(dictionary).sourceLanguage.toLangEntity().langId, + dictionaries = dictionaries, errors = emptyList() ) } override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { val ids = filter.dictionaryIds.map { it.asLong() } - val dictionaries = database.findDictionariesByIds(ids).sortedBy { it.id }.toSet() - if (dictionaries.isEmpty()) { - return CardsDbResponse(cards = emptyList()) + val dictionariesFromDb = database.findDictionariesByIds(ids).sortedBy { it.id }.toSet() + if (dictionariesFromDb.isEmpty()) { + return CardsDbResponse() } - val forbiddenIds = dictionaries.filter { it.userId != userId.asLong() }.map { checkNotNull(it.id) }.toSet() + val forbiddenIds = + dictionariesFromDb.filter { it.userId != userId.asLong() }.map { checkNotNull(it.id) }.toSet() val errors = forbiddenIds.map { forbiddenEntityDbError("searchCards", it.asDictionaryId(), userId) } - .toMutableList() - val candidates = dictionaries.filterNot { it.id in forbiddenIds } - val sourceLanguages = candidates.map { it.sourceLanguage.toLangEntity().langId }.toSet() - val targetLanguages = candidates.map { it.targetLanguage.toLangEntity().langId }.toSet() - if (sourceLanguages.size > 1 || targetLanguages.size > 1) { - errors.add( - wrongDictionaryLanguageFamiliesDbError("searchCard", candidates.map { it.id.asDictionaryId() }) - ) - } if (errors.isNotEmpty()) { - return CardsDbResponse(cards = emptyList(), errors = errors) + return CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = errors) } + val dictionaries = dictionariesFromDb.filterNot { it.id in forbiddenIds }.map { it.toDictionaryEntity() } var cardsFromDb = database.findCardsByDictionaryIds(ids) if (!filter.withUnknown) { cardsFromDb = cardsFromDb.filter { sysConfig.status(it.answered) != DocumentCardStatus.LEARNED } @@ -90,7 +83,7 @@ class MemDbCardRepository( cardsFromDb = cardsFromDb.shuffled(Random.Default) } val cards = cardsFromDb.take(filter.length).map { it.toCardEntity() }.toList() - return CardsDbResponse(cards = cards, sourceLanguageId = sourceLanguages.single()) + return CardsDbResponse(cards = cards, dictionaries = dictionaries) } override fun createCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse { diff --git a/db-pg/src/main/kotlin/PgDbCardRepository.kt b/db-pg/src/main/kotlin/PgDbCardRepository.kt index 58b66057..22a99bdf 100644 --- a/db-pg/src/main/kotlin/PgDbCardRepository.kt +++ b/db-pg/src/main/kotlin/PgDbCardRepository.kt @@ -10,7 +10,6 @@ import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate import com.gitlab.sszuev.flashcards.common.validateCardLearns -import com.gitlab.sszuev.flashcards.common.wrongDictionaryLanguageFamiliesDbError import com.gitlab.sszuev.flashcards.dbpg.dao.Cards import com.gitlab.sszuev.flashcards.dbpg.dao.Dictionaries import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbCard @@ -23,7 +22,6 @@ import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryId -import com.gitlab.sszuev.flashcards.model.domain.LangId import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse import com.gitlab.sszuev.flashcards.repositories.DbCardRepository @@ -66,14 +64,15 @@ class PgDbCardRepository( return connection.execute { val errors = mutableListOf() val dictionary = checkDictionaryUser("getAllCards", userId, dictionaryId, dictionaryId, errors) - if (errors.isNotEmpty()) { + if (errors.isNotEmpty() || dictionary == null) { return@execute CardsDbResponse(errors = errors) } val cards = PgDbCard.find { Cards.dictionaryId eq dictionaryId.asLong() }.map { it.toCardEntity() } + val dictionaries = listOf(dictionary.toDictionaryEntity()) CardsDbResponse( cards = cards, - sourceLanguageId = LangId(checkNotNull(dictionary).sourceLang), - errors = emptyList() + dictionaries = dictionaries, + errors = emptyList(), ) } } @@ -83,24 +82,18 @@ class PgDbCardRepository( val learned = sysConfig.numberOfRightAnswers val random = CustomFunction("random", DoubleColumnType()) return connection.execute { - val dictionaries = PgDbDictionary.find(Dictionaries.id inList dictionaryIds) - val forbiddenIds = dictionaries.filter { it.userId.value != userId.asLong() }.map { it.id.value }.toSet() - val errors = forbiddenIds.map { forbiddenEntityDbError("searchCards", it.asDictionaryId(), userId) } - .toMutableList() - val candidates = dictionaries.filterNot { it.id.value in forbiddenIds } - val sourceLanguages = candidates.map { it.sourceLang }.toSet() - val targetLanguages = candidates.map { it.targetLang }.toSet() - if (sourceLanguages.size > 1 || targetLanguages.size > 1) { - errors.add( - wrongDictionaryLanguageFamiliesDbError( - operation = "searchCard", - dictionaryIds = candidates.map { it.id.asDictionaryId() }, - ) - ) + val dictionariesFromDb = PgDbDictionary.find(Dictionaries.id inList dictionaryIds) + if (dictionariesFromDb.empty()) { + return@execute CardsDbResponse(cards = emptyList(), dictionaries = emptyList(), errors = emptyList()) } + val forbiddenIds = + dictionariesFromDb.filter { it.userId.value != userId.asLong() }.map { it.id.value }.toSet() + val errors = forbiddenIds.map { forbiddenEntityDbError("searchCards", it.asDictionaryId(), userId) } if (errors.isNotEmpty()) { return@execute CardsDbResponse(cards = emptyList(), errors = errors) } + val dictionaries = + dictionariesFromDb.filterNot { it.id.value in forbiddenIds }.map { it.toDictionaryEntity() } val cards = PgDbCard.find { Cards.dictionaryId inList dictionaryIds and (if (filter.withUnknown) Op.TRUE else Cards.answered.isNull() or Cards.answered.lessEq(learned)) @@ -108,7 +101,7 @@ class PgDbCardRepository( .orderBy(Cards.dictionaryId to SortOrder.ASC) .limit(filter.length) .map { it.toCardEntity() } - CardsDbResponse(cards = cards, sourceLanguageId = LangId(sourceLanguages.single())) + CardsDbResponse(cards = cards, dictionaries = dictionaries) } } From d11f0780ecdedb08a2cfc4231f74cabb8555c342 Mon Sep 17 00:00:00 2001 From: sszuev Date: Fri, 8 Mar 2024 17:25:05 +0300 Subject: [PATCH 2/6] core: [#26] advanced calculation of a next deck (select dissimilar cards) --- .../kotlin/processes/CardProcessWorkers.kt | 1 + .../kotlin/processes/SearchCardsHelper.kt | 97 ++++++++++++++++++- .../kotlin/validators/CardValidateWorkers.kt | 6 +- .../kotlin/CardCorProcessorRunCardsTest.kt | 2 +- core/src/test/kotlin/SearchCardsHelperTest.kt | 38 ++++++++ db-mem/src/main/kotlin/MemDbCardRepository.kt | 4 +- db-pg/src/main/kotlin/PgDbCardRepository.kt | 9 +- 7 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 core/src/test/kotlin/SearchCardsHelperTest.kt diff --git a/core/src/main/kotlin/processes/CardProcessWorkers.kt b/core/src/main/kotlin/processes/CardProcessWorkers.kt index 4737a2c1..0bdef07b 100644 --- a/core/src/main/kotlin/processes/CardProcessWorkers.kt +++ b/core/src/main/kotlin/processes/CardProcessWorkers.kt @@ -148,6 +148,7 @@ fun ChainDSL.processDeleteCard() = worker { } private suspend fun CardContext.postProcess(res: CardsDbResponse) { + check(res != CardsDbResponse.EMPTY) { "Null response" } this.errors.addAll(res.errors) val sourceLangByDictionary = res.dictionaries.associate { it.dictionaryId to it.sourceLang.langId } val tts = this.repositories.ttsClientRepository(this.workMode) diff --git a/core/src/main/kotlin/processes/SearchCardsHelper.kt b/core/src/main/kotlin/processes/SearchCardsHelper.kt index 08755f0c..775aeb5e 100644 --- a/core/src/main/kotlin/processes/SearchCardsHelper.kt +++ b/core/src/main/kotlin/processes/SearchCardsHelper.kt @@ -1,8 +1,18 @@ package com.gitlab.sszuev.flashcards.core.processes import com.gitlab.sszuev.flashcards.CardContext +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse +private val comparator: Comparator = Comparator { left, right -> + val la = left.answered ?: 0 + val ra = right.answered ?: 0 + la.compareTo(ra) +}.thenComparing { lc, rc -> + lc.changedAt.compareTo(rc.changedAt) +} + /** * Prepares a card deck for a tutor-session. */ @@ -10,12 +20,89 @@ fun CardContext.findCardDeck(): CardsDbResponse { return if (this.normalizedRequestCardFilter.random) { // For random mode, do not use the database support since logic is quite complicated. // We load everything into memory, since the dictionary can hardly contain more than a thousand words, - // i.e., this is quite small data. - //TODO: implement new logic - return this.repositories.cardRepository(this.workMode) - .searchCard(this.contextUserEntity.id, this.normalizedRequestCardFilter) + // i.e., this is relatively small data. + val filter = this.normalizedRequestCardFilter.copy(random = false, length = -1) + val res = this.repositories.cardRepository(this.workMode).searchCard(this.contextUserEntity.id, filter) + if (res.errors.isNotEmpty()) { + return res + } + var cards = res.cards.shuffled().sortedWith(comparator) + if (this.normalizedRequestCardFilter.length > 0) { + val set = mutableSetOf() + collectCardDeck(cards, set, this.normalizedRequestCardFilter.length) + cards = set.toList() + cards = cards.shuffled() + } + return res.copy(cards = cards) } else { this.repositories.cardRepository(this.workMode) .searchCard(this.contextUserEntity.id, this.normalizedRequestCardFilter) } -} \ No newline at end of file +} + +private fun collectCardDeck(all: List, res: MutableSet, num: Int) { + if (all.size <= num) { + res.addAll(all) + return + } + val rest = mutableListOf() + all.forEach { candidate -> + if (isSimilar(candidate, res)) { + rest.add(candidate) + } else { + res.add(candidate) + if (res.size == num) { + return + } + } + } + res.addAll(rest.sortedWith(comparator).take(num - res.size)) +} + +internal fun isSimilar(candidate: CardEntity, res: Set): Boolean = res.any { it.isSimilar(candidate) } + +internal fun CardEntity.isSimilar(other: CardEntity): Boolean = + if (this == other) true else this.words.isSimilar(other.words) + +private fun List.isSimilar(other: List): Boolean { + forEach { left -> + other.forEach { right -> + if (left.isSimilar(right)) { + return true + } + } + } + return false +} + +private fun CardWordEntity.isSimilar(other: CardWordEntity): Boolean { + if (this == other) { + return true + } + if (word.isSimilar(other.word)) { + return true + } + val otherTranslations = other.translations.flatten() + translations.flatten().forEach { left -> + otherTranslations.forEach { right -> + if (left.isSimilar(right)) { + return true + } + } + } + return false +} + +private fun String.isSimilar(other: String): Boolean { + if (this == other) { + return true + } + return prefix(3) == other.prefix(3) +} + +private fun String.prefix(num: Int): String { + if (length <= num) { + return lowercase() + } + return substring(0, num).lowercase() +} diff --git a/core/src/main/kotlin/validators/CardValidateWorkers.kt b/core/src/main/kotlin/validators/CardValidateWorkers.kt index 3acc8e0b..db312a59 100644 --- a/core/src/main/kotlin/validators/CardValidateWorkers.kt +++ b/core/src/main/kotlin/validators/CardValidateWorkers.kt @@ -35,7 +35,9 @@ fun ChainDSL.validateCardId(getCardId: (CardContext) -> CardId) = w fun ChainDSL.validateCardEntityWords(getCard: (CardContext) -> CardEntity) = worker { this.name = "Test card-word" - test { getCard(this).words.any { !isCorrectWrong(it.word) } } + test { + getCard(this).words.any { !isCorrectWrong(it.word) } + } process { fail(validationError(fieldName = "card-word")) } @@ -47,7 +49,7 @@ fun ChainDSL.validateCardFilterLength(getCardFilter: (CardContext) getCardFilter(this).length <= 0 } process { - fail(validationError(fieldName = "card-filter-length", description = "must be greater zero")) + fail(validationError(fieldName = "card-filter-length", description = "must be greater than zero")) } } diff --git a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt index d57f9894..78c6231f 100644 --- a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt +++ b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt @@ -241,7 +241,7 @@ context.errors.forEach { // TODO fun `test search-cards success`() = runTest { val testFilter = CardFilter( dictionaryIds = listOf(DictionaryId("21"), DictionaryId("42")), - random = true, + random = false, withUnknown = true, length = 42, ) diff --git a/core/src/test/kotlin/SearchCardsHelperTest.kt b/core/src/test/kotlin/SearchCardsHelperTest.kt new file mode 100644 index 00000000..89ab5d53 --- /dev/null +++ b/core/src/test/kotlin/SearchCardsHelperTest.kt @@ -0,0 +1,38 @@ +package com.gitlab.sszuev.flashcards.core + +import com.gitlab.sszuev.flashcards.core.processes.isSimilar +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class SearchCardsHelperTest { + + @Test + fun `test isSimilar #1`() { + val card1 = CardEntity(words = listOf(CardWordEntity("w"))) + val card2 = CardEntity(words = listOf(CardWordEntity("w"))) + Assertions.assertTrue(card1.isSimilar(card2)) + } + + @Test + fun `test isSimilar #2`() { + val card1 = CardEntity(words = listOf(CardWordEntity("w"))) + val card2 = CardEntity(words = listOf(CardWordEntity("q"))) + Assertions.assertFalse(card1.isSimilar(card2)) + } + + @Test + fun `test isSimilar #3`() { + val card1 = CardEntity(words = listOf(CardWordEntity("word"))) + val card2 = CardEntity(words = listOf(CardWordEntity("world"))) + Assertions.assertTrue(card1.isSimilar(card2)) + } + + @Test + fun `test isSimilar #4`() { + val card1 = CardEntity(words = listOf(CardWordEntity("ххх", translations = listOf(listOf("a", "slovo"))))) + val card2 = CardEntity(words = listOf(CardWordEntity("word", translations = listOf(listOf("b", "slo"))))) + Assertions.assertTrue(card1.isSimilar(card2)) + } +} \ No newline at end of file diff --git a/db-mem/src/main/kotlin/MemDbCardRepository.kt b/db-mem/src/main/kotlin/MemDbCardRepository.kt index d96cbd28..6fe1806f 100644 --- a/db-mem/src/main/kotlin/MemDbCardRepository.kt +++ b/db-mem/src/main/kotlin/MemDbCardRepository.kt @@ -63,6 +63,7 @@ class MemDbCardRepository( } override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { + require(filter.length != 0) { "zero length is specified" } val ids = filter.dictionaryIds.map { it.asLong() } val dictionariesFromDb = database.findDictionariesByIds(ids).sortedBy { it.id }.toSet() if (dictionariesFromDb.isEmpty()) { @@ -82,7 +83,8 @@ class MemDbCardRepository( if (filter.random) { cardsFromDb = cardsFromDb.shuffled(Random.Default) } - val cards = cardsFromDb.take(filter.length).map { it.toCardEntity() }.toList() + val cards = + (if (filter.length < 0) cardsFromDb else cardsFromDb.take(filter.length)).map { it.toCardEntity() }.toList() return CardsDbResponse(cards = cards, dictionaries = dictionaries) } diff --git a/db-pg/src/main/kotlin/PgDbCardRepository.kt b/db-pg/src/main/kotlin/PgDbCardRepository.kt index 22a99bdf..b174dd27 100644 --- a/db-pg/src/main/kotlin/PgDbCardRepository.kt +++ b/db-pg/src/main/kotlin/PgDbCardRepository.kt @@ -78,6 +78,7 @@ class PgDbCardRepository( } override fun searchCard(userId: AppUserId, filter: CardFilter): CardsDbResponse { + require(filter.length != 0) { "zero length is specified" } val dictionaryIds = filter.dictionaryIds.map { it.asLong() } val learned = sysConfig.numberOfRightAnswers val random = CustomFunction("random", DoubleColumnType()) @@ -94,13 +95,15 @@ class PgDbCardRepository( } val dictionaries = dictionariesFromDb.filterNot { it.id.value in forbiddenIds }.map { it.toDictionaryEntity() } - val cards = PgDbCard.find { + var cardsIterable = PgDbCard.find { Cards.dictionaryId inList dictionaryIds and (if (filter.withUnknown) Op.TRUE else Cards.answered.isNull() or Cards.answered.lessEq(learned)) }.orderBy(random to SortOrder.ASC) .orderBy(Cards.dictionaryId to SortOrder.ASC) - .limit(filter.length) - .map { it.toCardEntity() } + if (filter.length > 0) { + cardsIterable = cardsIterable.limit(filter.length) + } + val cards = cardsIterable.map { it.toCardEntity() } CardsDbResponse(cards = cards, dictionaries = dictionaries) } } From 2492288797333fc2d64775498e1044d42d0ea9d9 Mon Sep 17 00:00:00 2001 From: sszuev Date: Sat, 9 Mar 2024 15:13:19 +0300 Subject: [PATCH 3/6] frontend: change the learning logic; implement api call --- frontend/src/main/resources/static/api.js | 22 ++++++++++++--- frontend/src/main/resources/static/data.js | 28 +++++++++++++++++-- frontend/src/main/resources/static/tutor.js | 21 +++++++------- .../src/main/resources/application.properties | 2 +- 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/frontend/src/main/resources/static/api.js b/frontend/src/main/resources/static/api.js index 9d15b9b1..783f1f2e 100644 --- a/frontend/src/main/resources/static/api.js +++ b/frontend/src/main/resources/static/api.js @@ -13,6 +13,7 @@ const getAllCardsURI = '/v1/api/cards/get-all' const searchCardsURI = '/v1/api/cards/search' const createCardURI = '/v1/api/cards/create' const updateCardURI = '/v1/api/cards/update' +const learnCardURI = '/v1/api/cards/learn' const resetCardURI = '/v1/api/cards/reset' const deleteCardURI = '/v1/api/cards/delete' const getAudioURI = '/v1/api/sounds/get' @@ -26,6 +27,7 @@ const getAllCardsRequestType = 'getAllCards' const searchCardsRequestType = 'searchCards' const createCardRequestType = 'createCard' const updateCardRequestType = 'updateCard' +const learnCardRequestType = 'learnCard' const resetCardRequestType = 'resetCard' const deleteCardRequestType = 'deleteCard' const getAudioRequestType = 'getAudio' @@ -257,10 +259,22 @@ function resetCard(cardId, onDone) { }) } -function learnCard(cards, onDone) { - // TODO - console.log("learnCard") - onDone() +function learnCard(learns, onDone) { + const data = { + 'requestId': uuid(), + 'requestType': learnCardRequestType, + 'debug': {'mode': runMode}, + 'cards': learns + } + post(learnCardURI, data, function (res) { + if (hasResponseErrors(res)) { + handleResponseErrors(res) + } else { + if (onDone !== undefined) { + onDone() + } + } + }) } function playAudio(resourcePath, callback) { diff --git a/frontend/src/main/resources/static/data.js b/frontend/src/main/resources/static/data.js index 3c23f1c5..972a4325 100644 --- a/frontend/src/main/resources/static/data.js +++ b/frontend/src/main/resources/static/data.js @@ -59,7 +59,7 @@ function rememberAnswer(card, stage, booleanAnswer) { if (card.currentDetails == null) { card.currentDetails = {} } - card.currentDetails[stage] = booleanAnswer + card.currentDetails[stage] = booleanAnswer ? 1 : -1 } function hasStage(card, stage) { @@ -81,13 +81,37 @@ function isAnsweredRight(card) { if (!details.hasOwnProperty(key)) { continue } - if (!details[key]) { + if (details[key] !== 1) { return false } } return true } +/** + * Sums all answers to get a number to add to `card.answered`. + * @param card + * @returns {number} + */ +function sumAnswers(card) { + const details = card.currentDetails + if (details == null || !Object.keys(details).length) { + return 0 + } + let res = 0 + for (let key in details) { + if (!details.hasOwnProperty(key)) { + continue + } + if (details[key]) { + res += 1 + } else { + res -= 1 + } + } + return res +} + /** * Answers of an array with non-answered items to process. * @param cards input array diff --git a/frontend/src/main/resources/static/tutor.js b/frontend/src/main/resources/static/tutor.js index 541a04d9..889afca7 100644 --- a/frontend/src/main/resources/static/tutor.js +++ b/frontend/src/main/resources/static/tutor.js @@ -443,21 +443,20 @@ function drawAndPlayAudio(parent, audio) { } function updateCardAndCallNext(cards, nextStageCallback) { + const res = [] cards.forEach(function (card) { if (card.answered === undefined) { card.answered = 0 } - if (isAnsweredRight(card)) { - if (!card.wascorrect) { - card.answered++ - card.wascorrect = true - } - } else { - if (card.wascorrect && !card.incorrect) { - card.answered-- - card.incorrect = true - } + card.answered += sumAnswers(card) + if (card.answered < 0) { + card.answered = 0 } + const learn = {} + learn['cardId'] = card.cardId + learn['details'] = card.currentDetails + res.push(learn) + card.currentDetails = {} }) - learnCard(cards, () => nextStageCallback()) + learnCard(res, () => nextStageCallback()) } \ No newline at end of file diff --git a/tts-lib/src/main/resources/application.properties b/tts-lib/src/main/resources/application.properties index dc29dbbc..f82f8689 100644 --- a/tts-lib/src/main/resources/application.properties +++ b/tts-lib/src/main/resources/application.properties @@ -8,5 +8,5 @@ tts.get-resource-timeout-ms=5000 ## voice-rss settings, see http://voicerss.org/api/ tts.service.voicerss.api=api.voicerss.org tts.service.voicerss.key=secret -tts.service.voicerss.format=8khz_8bit_mono +tts.service.voicerss.format=16khz_16bit_stereo tts.service.voicerss.codec=mp3 From 4d23f11a0739bccd957833435b7179d7a8f88b3f Mon Sep 17 00:00:00 2001 From: sszuev Date: Sat, 9 Mar 2024 21:41:58 +0300 Subject: [PATCH 4/6] db: add bulk update (`DbCardRepository#updateCards`) --- .../kotlin/repositories/DbCardRepository.kt | 5 ++ .../repositories/NoOpDbCardRepository.kt | 8 ++ .../kotlin/DbCardRepositoryTest.kt | 80 ++++++++++++++++--- .../kotlin/mocks/MockDbCardRepository.kt | 9 +++ db-mem/src/main/kotlin/MemDatabase.kt | 8 ++ db-mem/src/main/kotlin/MemDbCardRepository.kt | 33 ++++++++ db-mem/src/main/kotlin/MemDbEntityMapper.kt | 2 +- db-pg/src/main/kotlin/PgDbCardRepository.kt | 40 ++++++++++ .../impl/VoicerssTextToSpeechServiceTest.kt | 18 +++-- 9 files changed, 185 insertions(+), 18 deletions(-) diff --git a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt index d9374c05..0bd88be1 100644 --- a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt +++ b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt @@ -39,6 +39,11 @@ interface DbCardRepository { */ fun updateCard(userId: AppUserId, cardEntity: CardEntity): CardDbResponse + /** + * Performs bulk update. + */ + fun updateCards(userId: AppUserId, cardIds: Iterable, update: (CardEntity) -> CardEntity): CardsDbResponse + /** * Updates cards details. */ diff --git a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt b/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt index 5d92c959..b3852194 100644 --- a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt +++ b/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt @@ -28,6 +28,14 @@ object NoOpDbCardRepository : DbCardRepository { noOp() } + override fun updateCards( + userId: AppUserId, + cardIds: Iterable, + update: (CardEntity) -> CardEntity + ): CardsDbResponse { + noOp() + } + override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { noOp() } diff --git a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt index 9c8f6647..6e226030 100644 --- a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt +++ b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt @@ -1,5 +1,6 @@ package com.gitlab.sszuev.flashcards.dbcommon +import com.gitlab.sszuev.flashcards.common.asLong import com.gitlab.sszuev.flashcards.model.common.AppError import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.common.NONE @@ -15,6 +16,7 @@ import com.gitlab.sszuev.flashcards.model.domain.Stage import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.DbCardRepository import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.MethodOrderer @@ -34,6 +36,43 @@ abstract class DbCardRepositoryTest { companion object { private val userId = AppUserId("42") + private val drawCardEntity = CardEntity( + cardId = CardId("38"), + dictionaryId = DictionaryId("1"), + words = listOf( + CardWordEntity( + word = "draw", + partOfSpeech = "verb", + translations = listOf(listOf("рисовать"), listOf("чертить")), + examples = emptyList(), + ), + CardWordEntity( + word = "drew", + ), + CardWordEntity( + word = "drawn", + ), + ), + ) + private val forgiveCardEntity = CardEntity( + cardId = CardId("58"), + dictionaryId = DictionaryId("1"), + words = listOf( + CardWordEntity( + word = "forgive", + partOfSpeech = "verb", + translations = listOf(listOf("прощать")), + examples = emptyList(), + ), + CardWordEntity( + word = "forgave", + ), + CardWordEntity( + word = "forgiven", + ), + ), + ) + private val weatherCardEntity = CardEntity( cardId = CardId("246"), dictionaryId = DictionaryId("2"), @@ -223,16 +262,6 @@ abstract class DbCardRepositoryTest { Assertions.assertNull(error.exception) } - @Order(21) - @Test - fun `test create card success`() { - val request = newMurkyCardEntity - val res = repository.createCard(userId, request) - assertNoErrors(res) - assertCard(expected = request, actual = res.card, ignoreChangeAt = true, ignoreId = true) - Assertions.assertTrue(res.card.cardId.asString().matches("\\d+".toRegex())) - } - @Order(4) @Test fun `test create card error unknown dictionary`() { @@ -378,6 +407,37 @@ abstract class DbCardRepositoryTest { assertCard(expected = expected, actual = now, ignoreChangeAt = true, ignoreId = false) } + @Order(11) + @Test + fun `test bulk update success`() { + val now = Clock.System.now() + val res = repository.updateCards( + userId, + setOf(forgiveCardEntity.cardId, snowCardEntity.cardId, drawCardEntity.cardId), + ) { + it.copy(answered = 42) + } + Assertions.assertEquals(0, res.errors.size) + Assertions.assertEquals(3, res.cards.size) + val actual = res.cards.sortedBy { it.cardId.asLong() } + assertCard(expected = drawCardEntity.copy(answered = 42), actual = actual[0], ignoreChangeAt = true) + assertCard(expected = forgiveCardEntity.copy(answered = 42), actual = actual[1], ignoreChangeAt = true) + assertCard(expected = snowCardEntity.copy(answered = 42), actual = actual[2], ignoreChangeAt = true) + actual.forEach { + Assertions.assertTrue(it.changedAt >= now) + } + } + + @Order(21) + @Test + fun `test create card success`() { + val request = newMurkyCardEntity + val res = repository.createCard(userId, request) + assertNoErrors(res) + assertCard(expected = request, actual = res.card, ignoreChangeAt = true, ignoreId = true) + Assertions.assertTrue(res.card.cardId.asString().matches("\\d+".toRegex())) + } + @Order(42) @Test fun `test get card & delete card success`() { diff --git a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt index a4d09fbc..f94138b6 100644 --- a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt +++ b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt @@ -21,6 +21,7 @@ class MockDbCardRepository( private val invokeSearchCards: (AppUserId, CardFilter) -> CardsDbResponse = { _, _ -> CardsDbResponse.EMPTY }, private val invokeCreateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, private val invokeUpdateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, + private val invokeUpdateCards: (AppUserId, Iterable, (CardEntity) -> CardEntity) -> CardsDbResponse = { _, _, _ -> CardsDbResponse.EMPTY }, private val invokeLearnCards: (AppUserId, List) -> CardsDbResponse = { _, _ -> CardsDbResponse.EMPTY }, private val invokeResetCard: (AppUserId, CardId) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, private val invokeDeleteCard: (AppUserId, CardId) -> RemoveCardDbResponse = { _, _ -> RemoveCardDbResponse.EMPTY }, @@ -46,6 +47,14 @@ class MockDbCardRepository( return invokeUpdateCard(userId, cardEntity) } + override fun updateCards( + userId: AppUserId, + cardIds: Iterable, + update: (CardEntity) -> CardEntity + ): CardsDbResponse { + return invokeUpdateCards(userId, cardIds, update) + } + override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { return invokeLearnCards(userId, cardLearns) } diff --git a/db-mem/src/main/kotlin/MemDatabase.kt b/db-mem/src/main/kotlin/MemDatabase.kt index a17aab5d..d09e1541 100644 --- a/db-mem/src/main/kotlin/MemDatabase.kt +++ b/db-mem/src/main/kotlin/MemDatabase.kt @@ -115,6 +115,14 @@ class MemDatabase private constructor( return dictionaryResources().mapNotNull { it.cards[cardId] }.singleOrNull() } + fun findCardsById(cardIds: Collection): Sequence { + val ids = cardIds.toSet() + return dictionaryResources() + .flatMap { it.cards.entries.asSequence() } + .filter { ids.contains(it.key) } + .map { it.value } + } + fun saveCard(card: MemDbCard): MemDbCard { val dictionaryId = requireNotNull(card.dictionaryId) { "No dictionaryId in the card $card" } val resource = diff --git a/db-mem/src/main/kotlin/MemDbCardRepository.kt b/db-mem/src/main/kotlin/MemDbCardRepository.kt index 6fe1806f..1e500265 100644 --- a/db-mem/src/main/kotlin/MemDbCardRepository.kt +++ b/db-mem/src/main/kotlin/MemDbCardRepository.kt @@ -127,6 +127,39 @@ class MemDbCardRepository( ) } + override fun updateCards( + userId: AppUserId, + cardIds: Iterable, + update: (CardEntity) -> CardEntity + ): CardsDbResponse { + val timestamp = systemNow() + val ids = cardIds.map { it.asLong() } + val dbCards = database.findCardsById(ids).associateBy { checkNotNull(it.id) } + val errors = mutableListOf() + val dbDictionaries = mutableMapOf() + dbCards.forEach { + val dictionary = dbDictionaries.computeIfAbsent(checkNotNull(it.value.dictionaryId)) { k -> + checkNotNull(database.findDictionaryById(k)) + } + if (dictionary.userId != userId.asLong()) { + errors.add(forbiddenEntityDbError("updateCards", it.key.asCardId(), userId)) + } + } + if (errors.isNotEmpty()) { + return CardsDbResponse(errors = errors) + } + val cards = dbCards.values.map { + val dbCard = update(it.toCardEntity()).toMemDbCard().copy(changedAt = timestamp) + database.saveCard(dbCard).toCardEntity() + } + val dictionaries = dbDictionaries.values.map { it.toDictionaryEntity() } + return CardsDbResponse( + cards = cards, + dictionaries = dictionaries, + errors = emptyList(), + ) + } + override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { validateCardLearns(cardLearns) val timestamp = systemNow() diff --git a/db-mem/src/main/kotlin/MemDbEntityMapper.kt b/db-mem/src/main/kotlin/MemDbEntityMapper.kt index 98ef1b02..b0009889 100644 --- a/db-mem/src/main/kotlin/MemDbEntityMapper.kt +++ b/db-mem/src/main/kotlin/MemDbEntityMapper.kt @@ -222,7 +222,7 @@ private fun Long.asUserId(): AppUserId = AppUserId(toString()) private fun String.asLangId(): LangId = LangId(this) -private fun Long.asCardId(): CardId = CardId(toString()) +internal fun Long.asCardId(): CardId = CardId(toString()) internal fun Long.asDictionaryId(): DictionaryId = DictionaryId(toString()) diff --git a/db-pg/src/main/kotlin/PgDbCardRepository.kt b/db-pg/src/main/kotlin/PgDbCardRepository.kt index b174dd27..38df2b12 100644 --- a/db-pg/src/main/kotlin/PgDbCardRepository.kt +++ b/db-pg/src/main/kotlin/PgDbCardRepository.kt @@ -151,6 +151,46 @@ class PgDbCardRepository( } } + override fun updateCards( + userId: AppUserId, + cardIds: Iterable, + update: (CardEntity) -> CardEntity + ): CardsDbResponse { + return connection.execute { + val timestamp = systemNow() + val ids = cardIds.map { it.asLong() } + val dbCards = PgDbCard.find { Cards.id inList ids }.associateBy { it.id.value } + val errors = mutableListOf() + ids.filterNot { it in dbCards.keys }.forEach { + errors.add(noCardFoundDbError(operation = "updateCards", id = it.asCardId())) + } + val dbDictionaries = mutableMapOf() + dbCards.forEach { + val dictionary = dbDictionaries.computeIfAbsent(it.value.dictionaryId.value) { k -> + checkNotNull(PgDbDictionary.findById(k)) + } + if (dictionary.userId.value != userId.asLong()) { + errors.add(forbiddenEntityDbError("updateCards", it.key.asCardId(), userId)) + } + } + if (errors.isNotEmpty()) { + return@execute CardsDbResponse(errors = errors) + } + val cards = dbCards.values.onEach { + val new = update(it.toCardEntity()) + writeCardEntityToPgDbCard(from = new, to = it, timestamp = timestamp) + }.map { + it.toCardEntity() + } + val dictionaries = dbDictionaries.values.map { it.toDictionaryEntity() } + CardsDbResponse( + cards = cards, + dictionaries = dictionaries, + errors = emptyList(), + ) + } + } + override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { validateCardLearns(cardLearns) return connection.execute { diff --git a/tts-lib/src/test/kotlin/impl/VoicerssTextToSpeechServiceTest.kt b/tts-lib/src/test/kotlin/impl/VoicerssTextToSpeechServiceTest.kt index 6f2739c7..5be82331 100644 --- a/tts-lib/src/test/kotlin/impl/VoicerssTextToSpeechServiceTest.kt +++ b/tts-lib/src/test/kotlin/impl/VoicerssTextToSpeechServiceTest.kt @@ -1,12 +1,16 @@ package com.gitlab.sszuev.flashcards.speaker.impl import com.gitlab.sszuev.flashcards.speaker.TTSConfig -import io.ktor.client.* -import io.ktor.client.engine.* -import io.ktor.client.engine.mock.* -import io.ktor.client.plugins.* -import io.ktor.http.* -import io.ktor.utils.io.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandleScope +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.HttpTimeout +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -83,7 +87,7 @@ internal class VoicerssTextToSpeechServiceTest { createMockEngine( res = testData, lang = "en-us", - format = "8khz_8bit_mono", + format = "16khz_16bit_stereo", key = "secret", word = "test-2", ) From 2bca710d6001d5c4476147e58f15c1f3d8a36caa Mon Sep 17 00:00:00 2001 From: sszuev Date: Sun, 10 Mar 2024 13:17:04 +0300 Subject: [PATCH 5/6] core & db & frontend: delete `DbCardRepository#learnCards` with moving logic to core (use `*#updateCards` instead), fix some mistakes in front and db-mem --- .../kotlin/repositories/DbCardRepository.kt | 6 ---- .../repositories/NoOpDbCardRepository.kt | 5 --- .../kotlin/processes/CardProcessWorkers.kt | 5 +-- .../kotlin/processes/UpdateCardsHelper.kt | 22 ++++++++++++ .../kotlin/validators/CardValidateWorkers.kt | 4 +-- .../kotlin/CardCorProcessorRunCardsTest.kt | 16 ++++----- .../kotlin/CardCorProcessorValidationTest.kt | 15 ++++++-- core/src/test/kotlin/SearchCardsHelperTest.kt | 15 ++++++++ .../kotlin/DbCardRepositoryTest.kt | 35 ------------------- .../kotlin/mocks/MockDbCardRepository.kt | 6 ---- db-mem/src/main/kotlin/MemDbCardRepository.kt | 33 ----------------- db-mem/src/main/kotlin/MemDbEntityMapper.kt | 6 +--- db-pg/src/main/kotlin/PgDbCardRepository.kt | 32 ----------------- frontend/src/main/resources/static/data.js | 12 +++---- frontend/src/main/resources/static/tutor.js | 10 ++++-- 15 files changed, 72 insertions(+), 150 deletions(-) create mode 100644 core/src/main/kotlin/processes/UpdateCardsHelper.kt diff --git a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt index 0bd88be1..552ec856 100644 --- a/common/src/commonMain/kotlin/repositories/DbCardRepository.kt +++ b/common/src/commonMain/kotlin/repositories/DbCardRepository.kt @@ -5,7 +5,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId @@ -44,11 +43,6 @@ interface DbCardRepository { */ fun updateCards(userId: AppUserId, cardIds: Iterable, update: (CardEntity) -> CardEntity): CardsDbResponse - /** - * Updates cards details. - */ - fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse - /** * Resets status. */ diff --git a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt b/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt index b3852194..3645c3ef 100644 --- a/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt +++ b/common/src/commonMain/kotlin/repositories/NoOpDbCardRepository.kt @@ -4,7 +4,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryId object NoOpDbCardRepository : DbCardRepository { @@ -36,10 +35,6 @@ object NoOpDbCardRepository : DbCardRepository { noOp() } - override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { - noOp() - } - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { noOp() } diff --git a/core/src/main/kotlin/processes/CardProcessWorkers.kt b/core/src/main/kotlin/processes/CardProcessWorkers.kt index 0bdef07b..a21c8140 100644 --- a/core/src/main/kotlin/processes/CardProcessWorkers.kt +++ b/core/src/main/kotlin/processes/CardProcessWorkers.kt @@ -107,10 +107,7 @@ fun ChainDSL.processLearnCards() = worker { this.status == AppStatus.RUN } process { - val userId = this.contextUserEntity.id - val res = - this.repositories.cardRepository(this.workMode).learnCards(userId, this.normalizedRequestCardLearnList) - this.postProcess(res) + this.postProcess(this.learnCards()) } onException { this.handleThrowable(CardOperation.LEARN_CARDS, it) diff --git a/core/src/main/kotlin/processes/UpdateCardsHelper.kt b/core/src/main/kotlin/processes/UpdateCardsHelper.kt new file mode 100644 index 00000000..0c721c0c --- /dev/null +++ b/core/src/main/kotlin/processes/UpdateCardsHelper.kt @@ -0,0 +1,22 @@ +package com.gitlab.sszuev.flashcards.core.processes + +import com.gitlab.sszuev.flashcards.CardContext +import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse + +fun CardContext.learnCards(): CardsDbResponse { + val cards = this.normalizedRequestCardLearnList.associateBy { it.cardId } + return this.repositories.cardRepository(this.workMode).updateCards(this.contextUserEntity.id, cards.keys) { card -> + val learn = checkNotNull(cards[card.cardId]) + var answered = card.answered?.toLong() ?: 0L + val details = card.stats.toMutableMap() + learn.details.forEach { + answered += it.value.toInt() + require(answered < Int.MAX_VALUE && answered > Int.MIN_VALUE) + if (answered < 0) { + answered = 0 + } + details.merge(it.key, it.value) { a, b -> a + b } + } + card.copy(stats = details, answered = answered.toInt()) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/validators/CardValidateWorkers.kt b/core/src/main/kotlin/validators/CardValidateWorkers.kt index db312a59..c1755682 100644 --- a/core/src/main/kotlin/validators/CardValidateWorkers.kt +++ b/core/src/main/kotlin/validators/CardValidateWorkers.kt @@ -91,8 +91,8 @@ fun ChainDSL.validateCardLearnListDetails(getCardLearn: (CardContex } } ) { (_, score) -> - // right not just check score is positive and not big - score <= 0 || score > 42 + // right not just check score is not big + score < -42 || score > 42 } private fun ChainDSL.validateIds( diff --git a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt index 78c6231f..49b6a870 100644 --- a/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt +++ b/core/src/test/kotlin/CardCorProcessorRunCardsTest.kt @@ -33,7 +33,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource -@Suppress("OPT_IN_USAGE") internal class CardCorProcessorRunCardsTest { companion object { private val testUser = AppUserEntity(AppUserId("42"), AppAuthId("00000000-0000-0000-0000-000000000000")) @@ -225,10 +224,6 @@ internal class CardCorProcessorRunCardsTest { val context = testContext(CardOperation.CREATE_CARD, repository) context.requestCardEntity = testRequestEntity -context.errors.forEach { // TODO - println(it) -} - CardCorProcessor().execute(context) Assertions.assertTrue(wasCalled) @@ -358,10 +353,10 @@ context.errors.forEach { // TODO var wasCalled = false val repository = MockDbCardRepository( - invokeLearnCards = { _, it -> + invokeUpdateCards = { _, givenIds, _ -> wasCalled = true CardsDbResponse( - cards = if (it == testLearn) testResponseEntities else emptyList(), + cards = if (givenIds == setOf(stubCard.cardId)) testResponseEntities else emptyList(), ) } ) @@ -384,6 +379,7 @@ context.errors.forEach { // TODO CardLearn(cardId = CardId("1"), details = mapOf(Stage.SELF_TEST to 42)), CardLearn(cardId = CardId("2"), details = mapOf(Stage.OPTIONS to 2, Stage.MOSAIC to 3)) ) + val ids = testLearn.map { it.cardId }.toSet() val testResponseEntities = stubCards val testResponseErrors = listOf( @@ -392,11 +388,11 @@ context.errors.forEach { // TODO var wasCalled = false val repository = MockDbCardRepository( - invokeLearnCards = { _, it -> + invokeUpdateCards = { _, givenIds, _ -> wasCalled = true CardsDbResponse( - cards = if (it == testLearn) testResponseEntities else emptyList(), - errors = if (it == testLearn) testResponseErrors else emptyList() + cards = if (givenIds == ids) testResponseEntities else emptyList(), + errors = if (givenIds == ids) testResponseErrors else emptyList() ) } ) diff --git a/core/src/test/kotlin/CardCorProcessorValidationTest.kt b/core/src/test/kotlin/CardCorProcessorValidationTest.kt index 82009ccc..f36de508 100644 --- a/core/src/test/kotlin/CardCorProcessorValidationTest.kt +++ b/core/src/test/kotlin/CardCorProcessorValidationTest.kt @@ -5,7 +5,16 @@ import com.gitlab.sszuev.flashcards.model.common.AppAuthId import com.gitlab.sszuev.flashcards.model.common.AppError import com.gitlab.sszuev.flashcards.model.common.AppMode import com.gitlab.sszuev.flashcards.model.common.AppRequestId -import com.gitlab.sszuev.flashcards.model.domain.* +import com.gitlab.sszuev.flashcards.model.domain.CardEntity +import com.gitlab.sszuev.flashcards.model.domain.CardFilter +import com.gitlab.sszuev.flashcards.model.domain.CardId +import com.gitlab.sszuev.flashcards.model.domain.CardLearn +import com.gitlab.sszuev.flashcards.model.domain.CardOperation +import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity +import com.gitlab.sszuev.flashcards.model.domain.DictionaryId +import com.gitlab.sszuev.flashcards.model.domain.LangId +import com.gitlab.sszuev.flashcards.model.domain.Stage +import com.gitlab.sszuev.flashcards.model.domain.TTSResourceGet import com.gitlab.sszuev.flashcards.stubs.stubCard import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -15,7 +24,7 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource -import java.util.* +import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class) internal class CardCorProcessorValidationTest { @@ -314,7 +323,7 @@ internal class CardCorProcessorValidationTest { val context = testContext(CardOperation.LEARN_CARDS) context.requestCardLearnList = listOf( testCardLearn.copy(cardId = CardId("42"), details = mapOf(Stage.OPTIONS to 4200, Stage.WRITING to 42)), - testCardLearn.copy(cardId = CardId("21"), details = mapOf(Stage.MOSAIC to -12, Stage.SELF_TEST to 0)), + testCardLearn.copy(cardId = CardId("21"), details = mapOf(Stage.MOSAIC to -4200, Stage.SELF_TEST to -4200)), ) processor.execute(context) Assertions.assertEquals(3, context.errors.size) diff --git a/core/src/test/kotlin/SearchCardsHelperTest.kt b/core/src/test/kotlin/SearchCardsHelperTest.kt index 89ab5d53..f45ab0cf 100644 --- a/core/src/test/kotlin/SearchCardsHelperTest.kt +++ b/core/src/test/kotlin/SearchCardsHelperTest.kt @@ -35,4 +35,19 @@ internal class SearchCardsHelperTest { val card2 = CardEntity(words = listOf(CardWordEntity("word", translations = listOf(listOf("b", "slo"))))) Assertions.assertTrue(card1.isSimilar(card2)) } + + @Test + fun `test isSimilar #5`() { + val card1 = CardEntity( + words = listOf( + CardWordEntity("moist", translations = listOf(listOf("сырой"), listOf("влажный"), listOf("мокрый"))) + ) + ) + val card2 = CardEntity( + words = listOf( + CardWordEntity("wet", translations = listOf(listOf("влажный"), listOf("сырой"))) + ) + ) + Assertions.assertTrue(card1.isSimilar(card2)) + } } \ No newline at end of file diff --git a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt index 6e226030..00a1735d 100644 --- a/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt +++ b/db-common/src/testFixtures/kotlin/DbCardRepositoryTest.kt @@ -7,7 +7,6 @@ import com.gitlab.sszuev.flashcards.model.common.NONE import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.CardWordEntity import com.gitlab.sszuev.flashcards.model.domain.CardWordExampleEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId @@ -128,25 +127,6 @@ abstract class DbCardRepositoryTest { ), ) - private val rainCardEntity = CardEntity( - cardId = CardId("248"), - dictionaryId = DictionaryId("2"), - words = listOf( - CardWordEntity( - word = "rain", - transcription = "rein", - partOfSpeech = "noun", - translations = listOf(listOf("дождь")), - examples = listOf( - CardWordExampleEntity(text = "It rains.", translation = "Идет дождь."), - CardWordExampleEntity(text = "heavy rain", translation = "проливной дождь, ливень"), - CardWordExampleEntity(text = "drizzling rain", translation = "изморось"), - CardWordExampleEntity(text = "torrential rain", translation = "проливной дождь"), - ), - ), - ), - ) - private val newMurkyCardEntity = CardEntity( dictionaryId = DictionaryId("2"), words = listOf( @@ -375,21 +355,6 @@ abstract class DbCardRepositoryTest { ) } - @Order(9) - @Test - fun `test learn cards success`() { - val request = CardLearn( - cardId = rainCardEntity.cardId, - details = mapOf(Stage.SELF_TEST to 3, Stage.WRITING to 4), - ) - val res = repository.learnCards(userId, listOf(request)) - Assertions.assertEquals(0, res.errors.size) { "Has errors: ${res.errors}" } - Assertions.assertEquals(1, res.cards.size) - val card = res.cards[0] - val expectedCard = rainCardEntity.copy(stats = request.details) - assertCard(expected = expectedCard, actual = card, ignoreChangeAt = true, ignoreId = false) - } - @Order(10) @Test fun `test get card & reset card success`() { diff --git a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt index f94138b6..33938374 100644 --- a/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt +++ b/db-common/src/testFixtures/kotlin/mocks/MockDbCardRepository.kt @@ -4,7 +4,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse @@ -22,7 +21,6 @@ class MockDbCardRepository( private val invokeCreateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, private val invokeUpdateCard: (AppUserId, CardEntity) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, private val invokeUpdateCards: (AppUserId, Iterable, (CardEntity) -> CardEntity) -> CardsDbResponse = { _, _, _ -> CardsDbResponse.EMPTY }, - private val invokeLearnCards: (AppUserId, List) -> CardsDbResponse = { _, _ -> CardsDbResponse.EMPTY }, private val invokeResetCard: (AppUserId, CardId) -> CardDbResponse = { _, _ -> CardDbResponse.EMPTY }, private val invokeDeleteCard: (AppUserId, CardId) -> RemoveCardDbResponse = { _, _ -> RemoveCardDbResponse.EMPTY }, ) : DbCardRepository { @@ -55,10 +53,6 @@ class MockDbCardRepository( return invokeUpdateCards(userId, cardIds, update) } - override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { - return invokeLearnCards(userId, cardLearns) - } - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { return invokeResetCard(userId, cardId) } diff --git a/db-mem/src/main/kotlin/MemDbCardRepository.kt b/db-mem/src/main/kotlin/MemDbCardRepository.kt index 1e500265..3dccc0bd 100644 --- a/db-mem/src/main/kotlin/MemDbCardRepository.kt +++ b/db-mem/src/main/kotlin/MemDbCardRepository.kt @@ -11,8 +11,6 @@ import com.gitlab.sszuev.flashcards.common.status import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate -import com.gitlab.sszuev.flashcards.common.validateCardLearns -import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbCard import com.gitlab.sszuev.flashcards.dbmem.dao.MemDbDictionary import com.gitlab.sszuev.flashcards.model.Id import com.gitlab.sszuev.flashcards.model.common.AppError @@ -20,13 +18,11 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse import com.gitlab.sszuev.flashcards.repositories.DbCardRepository import com.gitlab.sszuev.flashcards.repositories.RemoveCardDbResponse -import java.time.LocalDateTime import kotlin.random.Random class MemDbCardRepository( @@ -160,17 +156,6 @@ class MemDbCardRepository( ) } - override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { - validateCardLearns(cardLearns) - val timestamp = systemNow() - val errors = mutableListOf() - val cards = cardLearns.mapNotNull { learnCard(it, userId, errors, timestamp) } - if (errors.isNotEmpty()) { - return CardsDbResponse(errors = errors) - } - return CardsDbResponse(cards = cards.map { it.toCardEntity() }, errors = errors) - } - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { val timestamp = systemNow() val card = @@ -199,24 +184,6 @@ class MemDbCardRepository( return RemoveCardDbResponse(card = card.copy(changedAt = timestamp).toCardEntity()) } - private fun learnCard( - learn: CardLearn, - userId: AppUserId, - errors: MutableList, - timestamp: LocalDateTime, - ): MemDbCard? { - val cardId = learn.cardId - val card = database.findCardById(cardId.asLong()) - if (card == null) { - errors.add(noCardFoundDbError("learnCard", cardId)) - return null - } - if (checkDictionaryUser("learnCard", userId, card.dictionaryId.asDictionaryId(), cardId, errors) == null) { - return null - } - return database.saveCard(card.copy(details = learn.details.toMemDbCardDetails(), changedAt = timestamp)) - } - @Suppress("DuplicatedCode") private fun checkDictionaryUser( operation: String, diff --git a/db-mem/src/main/kotlin/MemDbEntityMapper.kt b/db-mem/src/main/kotlin/MemDbEntityMapper.kt index b0009889..1a483f70 100644 --- a/db-mem/src/main/kotlin/MemDbEntityMapper.kt +++ b/db-mem/src/main/kotlin/MemDbEntityMapper.kt @@ -20,7 +20,6 @@ import com.gitlab.sszuev.flashcards.common.parseUserDetailsJson import com.gitlab.sszuev.flashcards.common.toCardEntityDetails import com.gitlab.sszuev.flashcards.common.toCardEntityStats import com.gitlab.sszuev.flashcards.common.toCardWordEntity -import com.gitlab.sszuev.flashcards.common.toCommonCardDtoDetails import com.gitlab.sszuev.flashcards.common.toCommonWordDtoList import com.gitlab.sszuev.flashcards.common.toDocumentExamples import com.gitlab.sszuev.flashcards.common.toDocumentTranslations @@ -41,7 +40,6 @@ import com.gitlab.sszuev.flashcards.model.domain.DictionaryEntity import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.model.domain.LangEntity import com.gitlab.sszuev.flashcards.model.domain.LangId -import com.gitlab.sszuev.flashcards.model.domain.Stage import java.util.UUID internal fun MemDbUser.detailsAsJsonString(): String { @@ -65,7 +63,7 @@ internal fun MemDbCard.detailsAsJsonString(): String { } internal fun fromJsonStringToMemDbCardDetails(json: String): Map { - return parseCardDetailsJson(json).mapValues { it.toString() } + return parseCardDetailsJson(json).mapValues { it.value.toString() } } internal fun MemDbCard.wordsAsJsonString(): String { @@ -216,8 +214,6 @@ internal fun MemDbCard.detailsAsCommonCardDetailsDto(): CommonCardDetailsDto = C private fun CommonCardDetailsDto.toMemDbCardDetails(): Map = this.mapValues { it.value.toString() } -internal fun Map.toMemDbCardDetails(): Map = toCommonCardDtoDetails().toMemDbCardDetails() - private fun Long.asUserId(): AppUserId = AppUserId(toString()) private fun String.asLangId(): LangId = LangId(this) diff --git a/db-pg/src/main/kotlin/PgDbCardRepository.kt b/db-pg/src/main/kotlin/PgDbCardRepository.kt index 38df2b12..d84ca34a 100644 --- a/db-pg/src/main/kotlin/PgDbCardRepository.kt +++ b/db-pg/src/main/kotlin/PgDbCardRepository.kt @@ -9,7 +9,6 @@ import com.gitlab.sszuev.flashcards.common.noDictionaryFoundDbError import com.gitlab.sszuev.flashcards.common.systemNow import com.gitlab.sszuev.flashcards.common.validateCardEntityForCreate import com.gitlab.sszuev.flashcards.common.validateCardEntityForUpdate -import com.gitlab.sszuev.flashcards.common.validateCardLearns import com.gitlab.sszuev.flashcards.dbpg.dao.Cards import com.gitlab.sszuev.flashcards.dbpg.dao.Dictionaries import com.gitlab.sszuev.flashcards.dbpg.dao.PgDbCard @@ -20,7 +19,6 @@ import com.gitlab.sszuev.flashcards.model.common.AppUserId import com.gitlab.sszuev.flashcards.model.domain.CardEntity import com.gitlab.sszuev.flashcards.model.domain.CardFilter import com.gitlab.sszuev.flashcards.model.domain.CardId -import com.gitlab.sszuev.flashcards.model.domain.CardLearn import com.gitlab.sszuev.flashcards.model.domain.DictionaryId import com.gitlab.sszuev.flashcards.repositories.CardDbResponse import com.gitlab.sszuev.flashcards.repositories.CardsDbResponse @@ -191,36 +189,6 @@ class PgDbCardRepository( } } - override fun learnCards(userId: AppUserId, cardLearns: List): CardsDbResponse { - validateCardLearns(cardLearns) - return connection.execute { - val learns = cardLearns.associateBy { it.cardId.asLong() } - val found = PgDbCard.find { Cards.id inList learns.keys }.associateBy { it.id.value } - val errors = mutableListOf() - cardLearns.filterNot { it.cardId.asLong() in found.keys }.forEach { - errors.add(noCardFoundDbError(operation = "learnCards", id = it.cardId)) - } - val dictionaries = mutableMapOf() - found.forEach { - val dictionary = dictionaries.computeIfAbsent(it.value.dictionaryId.value) { k -> - checkNotNull(PgDbDictionary.findById(k)) - } - if (dictionary.userId.value != userId.asLong()) { - errors.add(forbiddenEntityDbError("learnCards", it.key.asCardId(), userId)) - } - } - if (errors.isNotEmpty()) { - return@execute CardsDbResponse(errors = errors) - } - val cards = learns.values.map { learn -> - val record = checkNotNull(found[learn.cardId.asLong()]) - record.details = learn.details.toPgDbCardDetailsJson() - record.toCardEntity() - } - CardsDbResponse(cards = cards, errors = errors) - } - } - override fun resetCard(userId: AppUserId, cardId: CardId): CardDbResponse { return connection.execute { val timestamp = systemNow() diff --git a/frontend/src/main/resources/static/data.js b/frontend/src/main/resources/static/data.js index 972a4325..55eb2ba5 100644 --- a/frontend/src/main/resources/static/data.js +++ b/frontend/src/main/resources/static/data.js @@ -56,14 +56,14 @@ function findById(cards, cardId) { } function rememberAnswer(card, stage, booleanAnswer) { - if (card.currentDetails == null) { - card.currentDetails = {} + if (card.stageStats == null) { + card.stageStats = {} } - card.currentDetails[stage] = booleanAnswer ? 1 : -1 + card.stageStats[stage] = booleanAnswer ? 1 : -1 } function hasStage(card, stage) { - return card.currentDetails != null && card.currentDetails[stage] != null + return card.stageStats != null && card.stageStats[stage] != null } /** @@ -73,7 +73,7 @@ function hasStage(card, stage) { * @returns {boolean|undefined} */ function isAnsweredRight(card) { - const details = card.currentDetails + const details = card.sessionStats if (details == null || !Object.keys(details).length) { return undefined } @@ -94,7 +94,7 @@ function isAnsweredRight(card) { * @returns {number} */ function sumAnswers(card) { - const details = card.currentDetails + const details = card.stageStats if (details == null || !Object.keys(details).length) { return 0 } diff --git a/frontend/src/main/resources/static/tutor.js b/frontend/src/main/resources/static/tutor.js index 889afca7..206e60c5 100644 --- a/frontend/src/main/resources/static/tutor.js +++ b/frontend/src/main/resources/static/tutor.js @@ -387,7 +387,7 @@ function drawResultCardPage() { $('#result-learned').html(learned); // remove state details flashcards.forEach(function (item) { - delete item.currentDetails + delete item.stageStats }); } @@ -448,15 +448,19 @@ function updateCardAndCallNext(cards, nextStageCallback) { if (card.answered === undefined) { card.answered = 0 } + if (card.sessionStats === undefined) { + card.sessionStats = {} + } card.answered += sumAnswers(card) if (card.answered < 0) { card.answered = 0 } const learn = {} learn['cardId'] = card.cardId - learn['details'] = card.currentDetails + learn['details'] = card.stageStats res.push(learn) - card.currentDetails = {} + card.sessionStats = {...card.sessionStats, ...card.stageStats} + card.stageStats = {} }) learnCard(res, () => nextStageCallback()) } \ No newline at end of file From 2e210f522b4d088234ae97fb1bbc8320c52a424c Mon Sep 17 00:00:00 2001 From: sszuev Date: Mon, 11 Mar 2024 12:42:45 +0300 Subject: [PATCH 6/6] db-common: [#9] ligvo languages and parts of speech --- .../kotlin/documents/xml/LingvoMappings.kt | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt b/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt index 3ab1b779..45428d6c 100644 --- a/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt +++ b/db-common/src/main/kotlin/documents/xml/LingvoMappings.kt @@ -14,17 +14,69 @@ internal object LingvoMappings { private val LANGUAGE_MAP: Map = mapOf( "1033" to "en", - "1049" to "ru", + "1025" to "ar", // Arabic + "1026" to "bg", // Bulgarian + "1028" to "ch", // Chinese + "1029" to "cs", // Czech + "1030" to "da", // Danish + "1031" to "de", // German + "1032" to "el", // Greek + "1034" to "es", // Spanish (Traditional) + "1035" to "fi", // Finnish + "1036" to "fr", // French + "1038" to "hu", // Hungarian + "1039" to "is", // Icelandic + "1040" to "it", // Italian + "1043" to "ni", // Dutch + "1044" to "no", // Norwegian (Bokmal) + "1045" to "pl", // Polish + "1048" to "ro", // Romanian + "1049" to "ru", // Russian + "1051" to "sk", // Slovak + "1053" to "sv", // Swedish + "1055" to "tr", // Turkish + "1057" to "in", // Indonesian + "1058" to "uk", // Ukrainian + "1059" to "be", // Belarusian + "1060" to "sl", // Slovenian + "1061" to "et", // Estonian + "1062" to "lv", // Latvian + "1063" to "lt", // Lithuanian + "1067" to "hy", // Armenian + "1069" to "eu", // Basque + "1078" to "af", // Afrikaans + "1079" to "ka", // Georgian + "1086" to "ms", // Malay + "1087" to "kk", // Kazakh + "1089" to "sw", // Swahili + "1092" to "tt", // Tatar + "1142" to "la", // Latin + "1561" to "ba", // Bashkir + "2052" to "ch", // Chinese traditional + "2068" to "no", // Norwegian (Nynorsk) + "2070" to "pt", // Portuguese + "3082" to "es", // Spanish (Modern) + "3098" to "sr", // Serbian (Cyrillic) + "32811" to "hy", // Armenian Western ) private val PART_OF_SPEECH_MAP = mapOf( "1" to "noun", "2" to "adjective", "3" to "verb", + "4" to "phrasal", + "5" to "phrasal verb", + "6" to "adverb", + "7" to "conjuction", + "8" to "idiom", + "9" to "numeral", + "10" to "preposition", + "11" to "pronoun", + "12" to "question word", ) private val STATUS_MAP = mapOf( "2" to DocumentCardStatus.UNKNOWN, "3" to DocumentCardStatus.IN_PROCESS, - "4" to DocumentCardStatus.LEARNED + "4" to DocumentCardStatus.LEARNED, ) fun toLanguageTag(lingvoId: String): String {