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}"
}
}