diff --git a/db/v-2/tables/project.xml b/db/v-2/tables/project.xml index a257ffdb3f..8fe1801d8f 100644 --- a/db/v-2/tables/project.xml +++ b/db/v-2/tables/project.xml @@ -64,4 +64,8 @@ + + + + \ No newline at end of file diff --git a/save-backend/backend-api-docs.json b/save-backend/backend-api-docs.json index 3aa39eb071..45564570f9 100644 --- a/save-backend/backend-api-docs.json +++ b/save-backend/backend-api-docs.json @@ -3598,8 +3598,8 @@ "tags": [ "projects" ], - "summary": "Get all avaliable projects.", - "description": "Get all projects, avaliable for current user.", + "summary": "Get all available projects.", + "description": "Get all projects, available for current user.", "operationId": "getProjects", "parameters": [ { @@ -4352,6 +4352,73 @@ ] } }, + "/api/v1/organizations/{organizationName}/get-organization-contest-rating": { + "get": { + "tags": [ + "organizations" + ], + "summary": "Get organization contest rating.", + "description": "Get organization contest rating.", + "operationId": "getOrganizationContestRating", + "parameters": [ + { + "name": "organizationName", + "in": "path", + "description": "name of an organization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Authorization-Source", + "in": "header", + "required": true, + "example": "basic" + } + ], + "responses": { + "200": { + "description": "Successfully get an organization contest rating.", + "content": { + "*/*": { + "schema": { + "type": "number", + "format": "double" + } + } + } + }, + "404": { + "description": "Could not find an organization with such name.", + "content": { + "*/*": { + "schema": { + "type": "number", + "format": "double" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "*/*": { + "schema": { + "type": "number", + "format": "double" + } + } + } + } + }, + "security": [ + { + "basic": [] + } + ] + } + }, "/api/v1/organizations/{organizationName}/avatar": { "get": { "tags": [ @@ -7053,8 +7120,8 @@ "$ref": "#/components/schemas/Organization" }, "contestRating": { - "type": "integer", - "format": "int64" + "type": "number", + "format": "double" }, "id": { "type": "integer", diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/OrganizationController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/OrganizationController.kt index 9de757583a..9a3b0ccebe 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/OrganizationController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/OrganizationController.kt @@ -7,6 +7,7 @@ import com.saveourtool.save.backend.security.OrganizationPermissionEvaluator import com.saveourtool.save.backend.service.GitService import com.saveourtool.save.backend.service.LnkUserOrganizationService import com.saveourtool.save.backend.service.OrganizationService +import com.saveourtool.save.backend.service.ProjectService import com.saveourtool.save.backend.service.TestSuitesService import com.saveourtool.save.backend.service.TestSuitesSourceService import com.saveourtool.save.backend.storage.TestSuitesSourceSnapshotStorage @@ -59,6 +60,7 @@ internal class OrganizationController( private val testSuitesSourceService: TestSuitesSourceService, private val testSuitesService: TestSuitesService, private val testSuitesSourceSnapshotStorage: TestSuitesSourceSnapshotStorage, + private val projectService: ProjectService ) { @GetMapping("/all") @PreAuthorize("permitAll()") @@ -427,6 +429,41 @@ internal class OrganizationController( ResponseEntity.ok("Git credentials and corresponding data successfully deleted") } + /** + * @param organizationName + * @param authentication + * @return contest rating for organization + */ + @GetMapping("/{organizationName}/get-organization-contest-rating") + @RequiresAuthorizationSourceHeader + @PreAuthorize("isAuthenticated()") + @Operation( + method = "Get", + summary = "Get organization contest rating.", + description = "Get organization contest rating.", + ) + @Parameters( + Parameter(name = "organizationName", `in` = ParameterIn.PATH, description = "name of an organization", required = true), + ) + @ApiResponse(responseCode = "200", description = "Successfully get an organization contest rating.") + @ApiResponse(responseCode = "404", description = "Could not find an organization with such name.") + fun getOrganizationContestRating( + @PathVariable organizationName: String, + authentication: Authentication, + ): Mono = Mono.just(organizationName) + .flatMap { + organizationService.findByName(it).toMono() + } + .switchIfEmptyToNotFound { + "Could not find an organization with name $organizationName." + } + .flatMap { + projectService.getNotDeletedProjectsByOrganizationName(organizationName, authentication).collectList() + } + .map { projectsList -> + projectsList.sumOf { it.contestRating } + } + private fun cleanupStorageData(testSuite: TestSuite) { testSuitesSourceSnapshotStorage.findKey( testSuite.source.organization.name, diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ProjectController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ProjectController.kt index e4d890d7c0..0763681f35 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ProjectController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ProjectController.kt @@ -69,8 +69,8 @@ class ProjectController( @PreAuthorize("permitAll()") @Operation( method = "GET", - summary = "Get all avaliable projects.", - description = "Get all projects, avaliable for current user.", + summary = "Get all available projects.", + description = "Get all projects, available for current user.", ) @ApiResponse(responseCode = "200", description = "Projects successfully fetched.") fun getProjects( @@ -159,13 +159,7 @@ class ProjectController( fun getNonDeletedProjectsByOrganizationName( @RequestParam organizationName: String, authentication: Authentication?, - ): Flux = projectService.findByOrganizationName(organizationName) - .filter { - it.status != ProjectStatus.DELETED - } - .filter { - projectPermissionEvaluator.hasPermission(authentication, it, Permission.READ) - } + ): Flux = projectService.getNotDeletedProjectsByOrganizationName(organizationName, authentication) @PostMapping("/save") @RequiresAuthorizationSourceHeader diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt index 6352a52c43..23a472313e 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt @@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional class LnkContestProjectService( private val lnkContestProjectRepository: LnkContestProjectRepository, private val lnkContestExecutionService: LnkContestExecutionService, + private val projectService: ProjectService, ) { /** * @param contestName name of a [Contest] @@ -96,9 +97,21 @@ class LnkContestProjectService( lnkContestProject.bestExecution = newExecution lnkContestProject.bestScore = newExecution.score lnkContestProjectRepository.save(lnkContestProject) + + updateProjectContestRating(project) } } + private fun updateProjectContestRating(project: Project) { + val projectContestRating = lnkContestProjectRepository.findByProject(project).mapNotNull { + it.bestScore + }.sum() + + projectService.updateProject(project.apply { + this.contestRating = projectContestRating + }) + } + /** * @param projectCoordinates * @param contestName diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ProjectService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ProjectService.kt index 26ad7a30aa..7d97064150 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ProjectService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ProjectService.kt @@ -95,6 +95,22 @@ class ProjectService( return projects } + /** + * @param organizationName + * @param authentication + * @return list of not deleted projects + */ + fun getNotDeletedProjectsByOrganizationName( + organizationName: String, + authentication: Authentication?, + ): Flux = findByOrganizationName(organizationName) + .filter { + it.status != ProjectStatus.DELETED + } + .filter { + projectPermissionEvaluator.hasPermission(authentication, it, Permission.READ) + } + /** * @param projectFilters * @return project's with filter diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestExecutionService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestExecutionService.kt index 2be79cb1a0..222a9b1912 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestExecutionService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestExecutionService.kt @@ -18,6 +18,7 @@ import com.saveourtool.save.utils.ScoreType import com.saveourtool.save.utils.calculateScore import com.saveourtool.save.utils.debug import com.saveourtool.save.utils.getLogger +import com.saveourtool.save.utils.isValidScore import org.apache.commons.io.FilenameUtils import org.slf4j.Logger @@ -246,7 +247,12 @@ class TestExecutionService(private val testExecutionRepository: TestExecutionRep expectedChecks += counters.expectedChecks unexpectedChecks += counters.unexpectedChecks - score = toDto().calculateScore(scoreType = ScoreType.F_MEASURE) + val executionScore = toDto().calculateScore(scoreType = ScoreType.F_MEASURE) + + if (!executionScore.isValidScore()) { + log.error("Execution score for execution id $id is invalid: $executionScore") + } + score = executionScore } executionRepository.save(execution) } diff --git a/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt index 5af7e05d94..df6599332e 100644 --- a/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt +++ b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt @@ -10,6 +10,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.mockito.kotlin.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.MockBeans import org.springframework.context.annotation.Import import org.springframework.test.context.junit.jupiter.SpringExtension import java.util.Optional @@ -17,6 +18,9 @@ import kotlin.math.abs @ExtendWith(SpringExtension::class) @Import(LnkContestProjectService::class) +@MockBeans( + MockBean(ProjectService::class), +) @Suppress("UnsafeCallOnNullableType") class LnkContestProjectServiceTest { @Autowired private lateinit var lnkContestProjectService: LnkContestProjectService diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt index 4084e8c9b6..76b4bf7d48 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt @@ -43,7 +43,7 @@ data class Project( columnDefinition = "", ) var organization: Organization, - var contestRating: Long = 0, + var contestRating: Double = 0.0, ) { /** * id of project diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/ExecutionScoreUtils.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/ExecutionScoreUtils.kt index b70cd02024..39756dab02 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/ExecutionScoreUtils.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/ExecutionScoreUtils.kt @@ -45,13 +45,23 @@ fun ExecutionDto.calculateScore(scoreType: ScoreType): Double = when (type) { else -> 0.0 } +/** + * @return true if value is in range (0, 100); false otherwise + */ +fun Double.isValidScore() = this.toInt().isValidScore() + +/** + * @return true if value is in range (0, 100); false otherwise + */ +fun Int.isValidScore() = this in 0..100 + private fun ExecutionDto.calculateScoreForContestMode(scoreType: ScoreType): Double = when (scoreType) { ScoreType.F_MEASURE -> calculateFmeasure() else -> TODO("Invalid score type for contest mode!") } private fun ExecutionDto.calculateFmeasure(): Double { - val denominator = getPrecisionRate() + getRecallRate() + val denominator = (getPrecisionRate() + getRecallRate()) return if (denominator == 0) { 0.0 } else { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ExecutionLabels.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ExecutionLabels.kt index 17432f95b3..4e4fa72e1d 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ExecutionLabels.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ExecutionLabels.kt @@ -14,6 +14,7 @@ import com.saveourtool.save.frontend.externals.fontawesome.fontAwesomeIcon import com.saveourtool.save.utils.calculateRate import com.saveourtool.save.utils.getPrecisionRate import com.saveourtool.save.utils.getRecallRate +import com.saveourtool.save.utils.isValidScore import csstype.ClassName import csstype.Width @@ -100,12 +101,22 @@ internal class ExecutionStatisticsValues(executionDto: ExecutionDto?) { ?: "0" this.precisionRate = executionDto ?.let { - "${it.getPrecisionRate()}" + val precisionRate = it.getPrecisionRate() + if (precisionRate.isValidScore()) { + precisionRate.toString() + } else { + "N/A" + } } ?: "0" this.recallRate = executionDto ?.let { - "${it.getRecallRate()}" + val recallRate = it.getRecallRate() + if (recallRate.isValidScore()) { + recallRate.toString() + } else { + "N/A" + } } ?: "0" } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt index 45f7062380..0ea7928247 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/OrganizationView.kt @@ -613,7 +613,7 @@ class OrganizationView : AbstractView( topProject?.let { scoreCard { name = it.name - contestScore = it.contestRating.toDouble() + contestScore = it.contestRating url = "#/${props.organizationName}/${it.name}" } }