From e008ecc5d34a64b3344e936b34a9e55e15bcb40f Mon Sep 17 00:00:00 2001 From: Jan Cizmar Date: Fri, 25 Mar 2022 15:13:47 +0100 Subject: [PATCH] feat: Add project dashboard page #915 - add stats providing services --- .../v2/controllers/V2LanguagesController.kt | 4 +- .../LanguageModel.kt | 2 +- .../LanguageModelAssembler.kt | 2 +- .../api/v2/hateoas/project/ProjectModel.kt | 2 +- .../hateoas/project/ProjectModelAssembler.kt | 2 +- .../hateoas/project/ProjectWithStatsModel.kt | 2 +- .../project/ProjectWithStatsModelAssembler.kt | 2 +- .../KeysWithTranslationsPageModel.kt | 2 +- ...WithTranslationsPagedResourcesAssembler.kt | 2 +- .../tolgee/service/ProjectStatsServiceTest.kt | 32 ++++ .../tolgee/service/TranslationServiceTest.kt | 20 ++ .../testDataBuilder/data/BaseTestData.kt | 1 + .../data/ProjectStatsTestData.kt | 174 ++++++++++++++++++ .../data/TranslationsTestData.kt | 1 + .../ProjectLanguageStatsResultView.kt | 13 ++ .../views/projectStats/ProjectStatsView.kt | 8 + .../io/tolgee/service/ProjectStatsService.kt | 23 +++ .../query_builders/LanguageStatsProvider.kt | 119 ++++++++++++ .../query_builders/ProjectStatsProvider.kt | 79 ++++++++ .../io/tolgee/util/KotlinCriteriaBuilder.kt | 22 +++ 20 files changed, 502 insertions(+), 10 deletions(-) rename backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/{organization => language}/LanguageModel.kt (95%) rename backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/{organization => language}/LanguageModelAssembler.kt (93%) create mode 100644 backend/app/src/test/kotlin/io/tolgee/service/ProjectStatsServiceTest.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ProjectStatsTestData.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectLanguageStatsResultView.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectStatsView.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/service/ProjectStatsService.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/service/query_builders/LanguageStatsProvider.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/service/query_builders/ProjectStatsProvider.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/util/KotlinCriteriaBuilder.kt diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/controllers/V2LanguagesController.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/controllers/V2LanguagesController.kt index e8a87da4c1..9fbc18daeb 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/controllers/V2LanguagesController.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/controllers/V2LanguagesController.kt @@ -9,8 +9,8 @@ import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tags import io.tolgee.activity.RequestActivity import io.tolgee.activity.data.ActivityType -import io.tolgee.api.v2.hateoas.organization.LanguageModel -import io.tolgee.api.v2.hateoas.organization.LanguageModelAssembler +import io.tolgee.api.v2.hateoas.language.LanguageModel +import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler import io.tolgee.component.LanguageValidator import io.tolgee.constants.Message import io.tolgee.controllers.IController diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModel.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModel.kt similarity index 95% rename from backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModel.kt rename to backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModel.kt index 440ea30df8..070203842c 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModel.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModel.kt @@ -1,4 +1,4 @@ -package io.tolgee.api.v2.hateoas.organization +package io.tolgee.api.v2.hateoas.language import io.swagger.v3.oas.annotations.media.Schema import org.springframework.hateoas.RepresentationModel diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModelAssembler.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModelAssembler.kt similarity index 93% rename from backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModelAssembler.kt rename to backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModelAssembler.kt index e92c7373ac..1390329ab6 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/organization/LanguageModelAssembler.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/language/LanguageModelAssembler.kt @@ -1,4 +1,4 @@ -package io.tolgee.api.v2.hateoas.organization +package io.tolgee.api.v2.hateoas.language import io.tolgee.api.v2.controllers.V2LanguagesController import io.tolgee.model.Language diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModel.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModel.kt index 144918d76b..a8a4edc819 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModel.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModel.kt @@ -1,8 +1,8 @@ package io.tolgee.api.v2.hateoas.project import io.swagger.v3.oas.annotations.media.Schema +import io.tolgee.api.v2.hateoas.language.LanguageModel import io.tolgee.api.v2.hateoas.UserPermissionModel -import io.tolgee.api.v2.hateoas.organization.LanguageModel import io.tolgee.api.v2.hateoas.user_account.UserAccountModel import io.tolgee.dtos.Avatar import io.tolgee.model.Permission diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModelAssembler.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModelAssembler.kt index a6b7866cc7..fa646545c8 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModelAssembler.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectModelAssembler.kt @@ -2,8 +2,8 @@ package io.tolgee.api.v2.hateoas.project import io.tolgee.api.v2.controllers.OrganizationController import io.tolgee.api.v2.controllers.V2ProjectsController +import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler import io.tolgee.api.v2.hateoas.UserPermissionModel -import io.tolgee.api.v2.hateoas.organization.LanguageModelAssembler import io.tolgee.api.v2.hateoas.user_account.UserAccountModelAssembler import io.tolgee.model.views.ProjectWithLanguagesView import io.tolgee.service.AvatarService diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModel.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModel.kt index 0a79b2ab78..717c7b613f 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModel.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModel.kt @@ -2,7 +2,7 @@ package io.tolgee.api.v2.hateoas.project import io.swagger.v3.oas.annotations.media.Schema import io.tolgee.api.v2.hateoas.UserPermissionModel -import io.tolgee.api.v2.hateoas.organization.LanguageModel +import io.tolgee.api.v2.hateoas.language.LanguageModel import io.tolgee.api.v2.hateoas.user_account.UserAccountModel import io.tolgee.dtos.Avatar import io.tolgee.dtos.query_results.ProjectStatistics diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModelAssembler.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModelAssembler.kt index 636ed25b69..72ba5004a7 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModelAssembler.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/project/ProjectWithStatsModelAssembler.kt @@ -3,7 +3,7 @@ package io.tolgee.api.v2.hateoas.project import io.tolgee.api.v2.controllers.OrganizationController import io.tolgee.api.v2.controllers.V2ProjectsController import io.tolgee.api.v2.hateoas.UserPermissionModel -import io.tolgee.api.v2.hateoas.organization.LanguageModelAssembler +import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler import io.tolgee.api.v2.hateoas.user_account.UserAccountModelAssembler import io.tolgee.model.views.ProjectWithStatsView import io.tolgee.service.AvatarService diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPageModel.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPageModel.kt index e295fcaa0f..7a8eb7e619 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPageModel.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPageModel.kt @@ -1,7 +1,7 @@ package io.tolgee.api.v2.hateoas.translations import io.swagger.v3.oas.annotations.media.Schema -import io.tolgee.api.v2.hateoas.organization.LanguageModel +import io.tolgee.api.v2.hateoas.language.LanguageModel import org.springframework.hateoas.Link import org.springframework.hateoas.PagedModel diff --git a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPagedResourcesAssembler.kt b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPagedResourcesAssembler.kt index eca6ab05ab..0b8271ae9d 100644 --- a/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPagedResourcesAssembler.kt +++ b/backend/app/src/main/kotlin/io/tolgee/api/v2/hateoas/translations/KeysWithTranslationsPagedResourcesAssembler.kt @@ -1,6 +1,6 @@ package io.tolgee.api.v2.hateoas.translations -import io.tolgee.api.v2.hateoas.organization.LanguageModelAssembler +import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler import io.tolgee.model.Language import io.tolgee.model.views.KeyWithTranslationsView import org.springframework.data.domain.Page diff --git a/backend/app/src/test/kotlin/io/tolgee/service/ProjectStatsServiceTest.kt b/backend/app/src/test/kotlin/io/tolgee/service/ProjectStatsServiceTest.kt new file mode 100644 index 0000000000..80adba3244 --- /dev/null +++ b/backend/app/src/test/kotlin/io/tolgee/service/ProjectStatsServiceTest.kt @@ -0,0 +1,32 @@ +package io.tolgee.service + +import io.tolgee.AbstractSpringTest +import io.tolgee.development.testDataBuilder.data.ProjectStatsTestData +import io.tolgee.testing.assertions.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class ProjectStatsServiceTest : AbstractSpringTest() { + + @Autowired + lateinit var projectStatsService: ProjectStatsService + + @Test + fun getLanguageStats() { + val testData = ProjectStatsTestData() + testDataService.saveTestData(testData.root) + val data = projectStatsService.getLanguageStats(testData.projectBuilder.self.id) + assertThat(data).hasSize(3) + } + + @Test + fun getProjectStats() { + val testData = ProjectStatsTestData() + testDataService.saveTestData(testData.root) + val data = projectStatsService.getProjectStats(testData.projectBuilder.self.id) + assertThat(data.id).isPositive + assertThat(data.userCount).isEqualTo(3) + assertThat(data.keyCount).isEqualTo(5) + assertThat(data.tagCount).isEqualTo(3) + } +} diff --git a/backend/app/src/test/kotlin/io/tolgee/service/TranslationServiceTest.kt b/backend/app/src/test/kotlin/io/tolgee/service/TranslationServiceTest.kt index 6ef8178d13..ffe89ad0b2 100644 --- a/backend/app/src/test/kotlin/io/tolgee/service/TranslationServiceTest.kt +++ b/backend/app/src/test/kotlin/io/tolgee/service/TranslationServiceTest.kt @@ -1,6 +1,7 @@ package io.tolgee.service import io.tolgee.AbstractSpringTest +import io.tolgee.development.testDataBuilder.data.TranslationsTestData import io.tolgee.dtos.request.translation.SetTranslationsWithKeyDto import io.tolgee.security.AuthenticationFacade import io.tolgee.testing.assertions.Assertions.assertThat @@ -36,4 +37,23 @@ class TranslationServiceTest : AbstractSpringTest() { @Suppress("UNCHECKED_CAST") assertThat(viewData["en"] as Map).containsKey("folder.folder.translation") } + + @Transactional + @Test + fun `adds stats on translation save and update`() { + val testData = TranslationsTestData() + testDataService.saveTestData(testData.root) + val translation = testData.aKeyGermanTranslation + assertThat(translation.wordCount).isEqualTo(2) + assertThat(translation.characterCount).isEqualTo(translation.text!!.length) + + translation.text = "My dog is cool!" + translationService.save(translation) + + commitTransaction() + + val updated = translationService.get(translation.id) + assertThat(updated.wordCount).isEqualTo(4) + assertThat(updated.characterCount).isEqualTo(translation.text!!.length) + } } diff --git a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/BaseTestData.kt b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/BaseTestData.kt index b5d4e876c0..e66bf4b1c4 100644 --- a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/BaseTestData.kt +++ b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/BaseTestData.kt @@ -36,6 +36,7 @@ open class BaseTestData( tag = "en" originalName = "English" englishLanguage = this + this@buildProject.self.baseLanguage = this } this.self { diff --git a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ProjectStatsTestData.kt b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ProjectStatsTestData.kt new file mode 100644 index 0000000000..2dc7e63902 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ProjectStatsTestData.kt @@ -0,0 +1,174 @@ +package io.tolgee.development.testDataBuilder.data + +import io.tolgee.development.testDataBuilder.builders.ProjectBuilder +import io.tolgee.model.Language +import io.tolgee.model.Permission +import io.tolgee.model.enums.OrganizationRoleType +import io.tolgee.model.enums.TranslationState +import io.tolgee.model.key.Tag + +class ProjectStatsTestData : BaseTestData() { + lateinit var germanLanguage: Language + lateinit var czechLanguage: Language + + init { + projectBuilder.apply { + + addLanguages() + addKeys() + + val organizationOwner = root.addUserAccount { + name = "franta" + username = "franta" + }.self + + val organizationMember = root.addUserAccount { + name = "jindra" + username = "jindra" + }.self + + root.addOrganization { + name = "org" + projectBuilder.self.organizationOwner = this + }.build buildOrganization@{ + addRole { + user = organizationOwner + type = OrganizationRoleType.OWNER + organization = this@buildOrganization.self + } + addRole { + user = organizationMember + type = OrganizationRoleType.MEMBER + organization = this@buildOrganization.self + } + } + + addPermission { + type = Permission.ProjectPermissionType.MANAGE + user = organizationMember + } + + self.userOwner = null + } + } + + private fun ProjectBuilder.addLanguages() { + germanLanguage = addGerman().self + czechLanguage = addCzech().self + } + + private fun ProjectBuilder.addKeys() { + val tag1 = Tag().apply { + name = "Tag1" + project = this@addKeys.self + } + + val tag2 = Tag().apply { + name = "Tag2" + project = this@addKeys.self + } + + val tag3 = Tag().apply { + name = "Tag3" + project = this@addKeys.self + } + + addKey { + name = "Super key" + }.build { + addTranslation { + language = englishLanguage + text = "This text has 5 words" + } + addTranslation { + language = germanLanguage + text = "Another text" + state = TranslationState.TRANSLATED + } + + addTranslation { + language = czechLanguage + text = "Another text" + state = TranslationState.TRANSLATED + } + addMeta { + tags.add(tag1) + tags.add(tag3) + } + } + + addKey { + name = "Key with null translations" + }.build { + addTranslation { + language = englishLanguage + text = "This text has 5 words" + } + addMeta { + tags.add(tag1) + tags.add(tag2) + } + } + + addKey { + name = "Key with Untranslated values" + }.build { + addTranslation { + language = englishLanguage + text = "This text has 5 words" + } + + addTranslation { + language = germanLanguage + text = null + state = TranslationState.UNTRANSLATED + } + + addTranslation { + language = czechLanguage + text = null + state = TranslationState.UNTRANSLATED + } + } + + addKey { + name = "Key with reviewed values" + }.build { + addTranslation { + language = englishLanguage + text = "This text has 5 words" + } + addTranslation { + language = germanLanguage + text = null + state = TranslationState.REVIEWED + } + + addTranslation { + language = czechLanguage + text = null + state = TranslationState.REVIEWED + } + } + + addKey { + name = "Key with mixed values" + }.build { + addTranslation { + language = englishLanguage + text = "This text has 5 words" + } + addTranslation { + language = germanLanguage + text = null + state = TranslationState.TRANSLATED + } + + addTranslation { + language = czechLanguage + text = null + state = TranslationState.REVIEWED + } + } + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TranslationsTestData.kt b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TranslationsTestData.kt index 8c81bca48b..e6073122de 100644 --- a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TranslationsTestData.kt +++ b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TranslationsTestData.kt @@ -40,6 +40,7 @@ class TranslationsTestData { name = "English" tag = "en" originalName = "English" + this@project.self.baseLanguage = this }.self germanLanguage = addLanguage { name = "German" diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectLanguageStatsResultView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectLanguageStatsResultView.kt new file mode 100644 index 0000000000..9b47fba8b0 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectLanguageStatsResultView.kt @@ -0,0 +1,13 @@ +package io.tolgee.model.views.projectStats + +data class ProjectLanguageStatsResultView( + val languageId: Long?, + val languageTag: String?, + val languageName: String?, + val languageOriginalName: String?, + val languageFlagEmoji: String?, + val translatedKeys: Long?, + val translatedWords: Long?, + val reviewedKeys: Long?, + val reviewedWords: Long?, +) diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectStatsView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectStatsView.kt new file mode 100644 index 0000000000..b06d7208a3 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/projectStats/ProjectStatsView.kt @@ -0,0 +1,8 @@ +package io.tolgee.model.views.projectStats + +data class ProjectStatsView( + val id: Long, + val keyCount: Long, + val userCount: Long, + val tagCount: Long +) diff --git a/backend/data/src/main/kotlin/io/tolgee/service/ProjectStatsService.kt b/backend/data/src/main/kotlin/io/tolgee/service/ProjectStatsService.kt new file mode 100644 index 0000000000..6545f1bc7d --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/ProjectStatsService.kt @@ -0,0 +1,23 @@ +package io.tolgee.service + +import io.tolgee.model.views.projectStats.ProjectLanguageStatsResultView +import io.tolgee.model.views.projectStats.ProjectStatsView +import io.tolgee.service.query_builders.LanguageStatsProvider +import io.tolgee.service.query_builders.ProjectStatsProvider +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import javax.persistence.EntityManager + +@Transactional +@Service +class ProjectStatsService( + private val entityManager: EntityManager, +) { + fun getLanguageStats(projectId: Long): List { + return LanguageStatsProvider(entityManager, projectId).getResult() + } + + fun getProjectStats(projectId: Long): ProjectStatsView { + return ProjectStatsProvider(entityManager, projectId).getResult() + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/query_builders/LanguageStatsProvider.kt b/backend/data/src/main/kotlin/io/tolgee/service/query_builders/LanguageStatsProvider.kt new file mode 100644 index 0000000000..715e4b7e4f --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/query_builders/LanguageStatsProvider.kt @@ -0,0 +1,119 @@ +package io.tolgee.service.query_builders + +import io.tolgee.model.Language_ +import io.tolgee.model.Project +import io.tolgee.model.Project_ +import io.tolgee.model.enums.TranslationState +import io.tolgee.model.key.Key +import io.tolgee.model.key.Key_ +import io.tolgee.model.translation.Translation +import io.tolgee.model.translation.Translation_ +import io.tolgee.model.views.projectStats.ProjectLanguageStatsResultView +import javax.persistence.EntityManager +import javax.persistence.criteria.CriteriaBuilder +import javax.persistence.criteria.CriteriaQuery +import javax.persistence.criteria.Expression +import javax.persistence.criteria.JoinType +import javax.persistence.criteria.Path +import javax.persistence.criteria.Predicate +import javax.persistence.criteria.Root +import javax.persistence.criteria.Selection +import javax.persistence.criteria.SetJoin + +open class LanguageStatsProvider( + val entityManager: EntityManager, + private val projectId: Long +) { + + val cb: CriteriaBuilder = entityManager.criteriaBuilder + val query: CriteriaQuery = cb.createQuery(ProjectLanguageStatsResultView::class.java) + + private var project: Root = query.from(Project::class.java) + private val languageJoin = project.join(Project_.languages) + + fun getResult(): MutableList { + initQuery() + return entityManager.createQuery(query).resultList + } + + private fun initQuery() { + val counts = listOf(TranslationState.TRANSLATED, TranslationState.REVIEWED).map { state -> + selectWordCount(state) to selectKeyCount(state) + } + + val selection = mutableListOf>( + languageJoin.get(Language_.id), + languageJoin.get(Language_.tag), + languageJoin.get(Language_.name), + languageJoin.get(Language_.originalName), + languageJoin.get(Language_.flagEmoji) + ) + + counts.forEach { (wordCount, keyCount) -> + selection.add(keyCount) + selection.add(wordCount) + } + + query.multiselect(selection) + + query.groupBy(languageJoin.get(Language_.id)) + + query.where(cb.equal(project.get(Project_.id), projectId)) + } + + private fun selectKeyCount(state: TranslationState): Selection { + val sub = query.subquery(Int::class.java) + val project = sub.from(Project::class.java) + val keyJoin = project.join(Project_.keys) + val targetTranslationsJoin = joinTargetTranslations(keyJoin, state) + val count = cb.count(targetTranslationsJoin.get(Translation_.id)) as Expression + return sub.select(count) + } + + private fun selectWordCount(state: TranslationState): Selection { + val sub = query.subquery(Int::class.java) + val project = sub.from(Project::class.java) + val keyJoin = project.join(Project_.keys) + + joinTargetTranslations(keyJoin, state) + + val baseTranslationJoin = keyJoin.join(Key_.translations, JoinType.LEFT).also { translation -> + translation.on( + cb.equal(translation.get(Translation_.language), project.get(Project_.baseLanguage)) + ) + } + return sub.select(cb.sum(baseTranslationJoin.get(Translation_.wordCount))) + } + + private fun joinTargetTranslations( + keyJoin: SetJoin, + state: TranslationState + ): SetJoin { + return keyJoin.join(Key_.translations).also { translation -> + translation.on( + cb.and( + cb.equal( + translation.get( + Translation_.state + ), + state + ), + cb.equal(translation.get(Translation_.language), languageJoin) + ) + ) + } + } + + fun CriteriaBuilder.translationInState(translation: Path, state: TranslationState): Predicate { + val stateValueEquals = this.equal( + translation.get( + Translation_.state + ), + state + ) + if (state == TranslationState.UNTRANSLATED) { + return cb.or(stateValueEquals, cb.isNull(translation.get(Translation_.text))) + } + return stateValueEquals + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/query_builders/ProjectStatsProvider.kt b/backend/data/src/main/kotlin/io/tolgee/service/query_builders/ProjectStatsProvider.kt new file mode 100644 index 0000000000..642373174c --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/query_builders/ProjectStatsProvider.kt @@ -0,0 +1,79 @@ +package io.tolgee.service.query_builders + +import io.tolgee.model.OrganizationRole_ +import io.tolgee.model.Organization_ +import io.tolgee.model.Permission_ +import io.tolgee.model.Project +import io.tolgee.model.Project_ +import io.tolgee.model.UserAccount +import io.tolgee.model.UserAccount_ +import io.tolgee.model.key.Key +import io.tolgee.model.key.Key_ +import io.tolgee.model.key.Tag +import io.tolgee.model.key.Tag_ +import io.tolgee.model.views.projectStats.ProjectStatsView +import io.tolgee.util.KotlinCriteriaBuilder +import javax.persistence.EntityManager +import javax.persistence.criteria.JoinType +import javax.persistence.criteria.Root +import javax.persistence.criteria.Selection + +open class ProjectStatsProvider( + val entityManager: EntityManager, + private val projectId: Long +) : KotlinCriteriaBuilder(entityManager, ProjectStatsView::class.java) { + + private var project: Root = query.from(Project::class.java) + + fun getResult(): ProjectStatsView { + initQuery() + return entityManager.createQuery(query).singleResult + } + + private fun initQuery() { + val selection = mutableListOf>( + project.get(Project_.id), + getKeyCountSelection(), + getUserCountSelection(), + getTagSelection() + ) + + query.multiselect(selection) + + query.groupBy(project.get(Project_.id)) + + query.where(cb.equal(project.get(Project_.id), projectId)) + } + + private fun getKeyCountSelection(): Selection { + val sub = query.subquery(Long::class.java) + val key = sub.from(Key::class.java) + sub.where(key.get(Key_.project) equal project) + return sub.select(cb.countDistinct(key.get(Key_.id))) + } + + private fun getTagSelection(): Selection { + val sub = query.subquery(Long::class.java) + val tag = sub.from(Tag::class.java) + sub.where(tag.get(Tag_.project) equal project) + return sub.select(cb.coalesce(cb.countDistinct(tag.get(Tag_.id)), 0)) + } + + private fun getUserCountSelection(): Selection { + val sub = query.subquery(Long::class.java) + val subProject = sub.from(Project::class.java) + val subUserAccount = sub.from(UserAccount::class.java) + val permissionJoin = subProject.join(Project_.permissions, JoinType.LEFT) + val organizationJoin = subProject.join(Project_.organizationOwner, JoinType.LEFT) + val rolesJoin = organizationJoin.join(Organization_.memberRoles, JoinType.LEFT) + + sub.where( + project equal subProject and + ( + subUserAccount equal permissionJoin.get(Permission_.user) or + (subUserAccount equal rolesJoin.get(OrganizationRole_.user)) + ) + ) + return sub.select(cb.countDistinct(subUserAccount.get(UserAccount_.id))) + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/util/KotlinCriteriaBuilder.kt b/backend/data/src/main/kotlin/io/tolgee/util/KotlinCriteriaBuilder.kt new file mode 100644 index 0000000000..1e898161c8 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/util/KotlinCriteriaBuilder.kt @@ -0,0 +1,22 @@ +package io.tolgee.util + +import javax.persistence.EntityManager +import javax.persistence.criteria.Expression +import javax.persistence.criteria.Predicate + +abstract class KotlinCriteriaBuilder(entityManager: EntityManager, result: Class) { + val cb = entityManager.criteriaBuilder + val query = cb.createQuery(result) + + infix fun Expression<*>.equal(other: Expression<*>): Predicate { + return cb.equal(this, other) + } + + infix fun Predicate.or(other: Predicate): Predicate { + return cb.or(this, other) + } + + infix fun Predicate.and(other: Predicate): Predicate { + return cb.and(this, other) + } +}