From 4fe4dc1b38bca43c77b24abc292e4951e610c69e Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 4 May 2023 15:58:10 -0600 Subject: [PATCH 01/45] Enable EnableGlobalMethodSecurity - Add EnableGlobalMethodSecurity annotation in SecurityConfig --- api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt index 1e6b47949..ba6a00055 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt @@ -12,6 +12,7 @@ import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS_UPDATE import edu.wgu.osmt.RoutePaths.COLLECTION_UPDATE import edu.wgu.osmt.RoutePaths.COLLECTION_XLSX +import edu.wgu.osmt.RoutePaths.JOB_CODE_PATH import edu.wgu.osmt.RoutePaths.SEARCH_COLLECTIONS import edu.wgu.osmt.RoutePaths.SEARCH_JOBCODES_PATH import edu.wgu.osmt.RoutePaths.SEARCH_KEYWORDS_PATH @@ -85,6 +86,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(GET, TASK_DETAIL_BATCH).authenticated() .mvcMatchers(GET, SEARCH_JOBCODES_PATH).authenticated() .mvcMatchers(GET, SEARCH_KEYWORDS_PATH).authenticated() + // .mvcMatchers(JOB_CODE_PATH).hasAnyAuthority("ROLE_Osmt_View", "ROLE_Osmt_Admin") // public search endpoints .mvcMatchers(POST, SEARCH_SKILLS).permitAll() @@ -131,6 +133,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(POST, SKILL_UPDATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, SKILLS_CREATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, SKILL_PUBLISH).hasAnyAuthority(ADMIN) + // .mvcMatchers(JOB_CODE_PATH).hasAnyAuthority(ADMIN, VIEW) .mvcMatchers(POST, COLLECTION_CREATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, COLLECTION_PUBLISH).hasAnyAuthority(ADMIN) From 502e107942613e232800b7fd20ac9b52fc37f432 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 5 May 2023 10:32:11 -0600 Subject: [PATCH 02/45] Add crud methods for job code --- .../edu/wgu/osmt/api/model/JobCodeUpdate.kt | 2 +- .../edu/wgu/osmt/jobcode/JobCodeController.kt | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt index 509448550..47eb9b621 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt @@ -17,4 +17,4 @@ data class JobCodeUpdate( @JsonProperty("parents") val parents: List? = null ) { -} \ No newline at end of file +} diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index 99fb58d6c..552d7a460 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -1,5 +1,6 @@ package edu.wgu.osmt.jobcode; +import edu.wgu.osmt.PaginationDefaults import edu.wgu.osmt.RoutePaths import edu.wgu.osmt.api.model.ApiJobCode import edu.wgu.osmt.api.model.JobCodeUpdate @@ -28,13 +29,14 @@ class JobCodeController @Autowired constructor( val jobCodeEsRepo: JobCodeEsRepo, val jobCodeRepository: JobCodeRepository ) { + val dao = JobCodeDao.Companion @GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("isAuthenticated()") fun allPaginated( - @RequestParam(required = true) size: Int, - @RequestParam(required = true) from: Int, - @RequestParam(required = false) sort: String? + @RequestParam query: String, + @RequestParam(required = false, defaultValue = PaginationDefaults.size.toString()) size: Int, + @RequestParam(required = false, defaultValue = "0") from: Int, ): HttpEntity> { val searchResults = jobCodeEsRepo.typeAheadSearch("", OffsetPageable(from, size, null)) return ResponseEntity.status(200).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) @@ -76,7 +78,14 @@ class JobCodeController @Autowired constructor( fun deleteJobCode( @PathVariable id: Int, ): HttpEntity { - return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) + return ResponseEntity.status(200).body( + TaskResult( + uuid = "uuid", + contentType = "application/json", + status = TaskStatus.Processing, + apiResultPath = "path" + ) + ) } -} \ No newline at end of file +} From 3710513e947376bedf98af4ada86cbffb6a9263f Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Mon, 8 May 2023 09:27:10 -0600 Subject: [PATCH 03/45] Add paths for named references --- .../kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt | 1 - .../main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt | 2 +- .../edu/wgu/osmt/keyword/NamedReferencesController.kt | 11 +++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index 552d7a460..267bcf4b1 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -29,7 +29,6 @@ class JobCodeController @Autowired constructor( val jobCodeEsRepo: JobCodeEsRepo, val jobCodeRepository: JobCodeRepository ) { - val dao = JobCodeDao.Companion @GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("isAuthenticated()") diff --git a/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt b/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt index c530c8884..bd6499700 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt @@ -28,4 +28,4 @@ data class NamedReference( } } -} \ No newline at end of file +} diff --git a/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt b/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt index a2dc13b2b..a8e88c0cb 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt @@ -79,7 +79,14 @@ class NamedReferencesController @Autowired constructor( fun deleteNamedReference( @PathVariable id: Int, ): HttpEntity { - return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) + return ResponseEntity.status(200).body( + TaskResult( + uuid = "uuid", + contentType = "application/json", + status = TaskStatus.Processing, + apiResultPath = "path" + ) + ) } -} \ No newline at end of file +} From f59585a6c201aba5e53c5dfb201fa8de4e339d62 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Tue, 9 May 2023 14:11:44 -0600 Subject: [PATCH 04/45] Remove commented lines --- api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt index ba6a00055..1e6b47949 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt @@ -12,7 +12,6 @@ import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS_UPDATE import edu.wgu.osmt.RoutePaths.COLLECTION_UPDATE import edu.wgu.osmt.RoutePaths.COLLECTION_XLSX -import edu.wgu.osmt.RoutePaths.JOB_CODE_PATH import edu.wgu.osmt.RoutePaths.SEARCH_COLLECTIONS import edu.wgu.osmt.RoutePaths.SEARCH_JOBCODES_PATH import edu.wgu.osmt.RoutePaths.SEARCH_KEYWORDS_PATH @@ -86,7 +85,6 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(GET, TASK_DETAIL_BATCH).authenticated() .mvcMatchers(GET, SEARCH_JOBCODES_PATH).authenticated() .mvcMatchers(GET, SEARCH_KEYWORDS_PATH).authenticated() - // .mvcMatchers(JOB_CODE_PATH).hasAnyAuthority("ROLE_Osmt_View", "ROLE_Osmt_Admin") // public search endpoints .mvcMatchers(POST, SEARCH_SKILLS).permitAll() @@ -133,7 +131,6 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(POST, SKILL_UPDATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, SKILLS_CREATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, SKILL_PUBLISH).hasAnyAuthority(ADMIN) - // .mvcMatchers(JOB_CODE_PATH).hasAnyAuthority(ADMIN, VIEW) .mvcMatchers(POST, COLLECTION_CREATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, COLLECTION_PUBLISH).hasAnyAuthority(ADMIN) From 83d41df6e24c68cbc34286f142f135ec2b7e66d9 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 11 May 2023 14:18:41 -0600 Subject: [PATCH 05/45] Add tests for named references and job codes --- .../main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt | 7 ++++--- .../kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt | 4 ++-- .../edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index 267bcf4b1..8fde57666 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -33,9 +33,10 @@ class JobCodeController @Autowired constructor( @GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("isAuthenticated()") fun allPaginated( - @RequestParam query: String, - @RequestParam(required = false, defaultValue = PaginationDefaults.size.toString()) size: Int, - @RequestParam(required = false, defaultValue = "0") from: Int, + @RequestParam(required = true) size: Int, + @RequestParam(required = true) from: Int, + @RequestParam(required = false) sort: String?, + @RequestParam(required = false) query: String? ): HttpEntity> { val searchResults = jobCodeEsRepo.typeAheadSearch("", OffsetPageable(from, size, null)) return ResponseEntity.status(200).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt index d722194fc..864f3b5e7 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt @@ -33,7 +33,7 @@ internal class JobCodeControllerTest @Autowired constructor( this.name = "my name" this.major = "my major" }.also { jobCodeEsRepo.save(it.toModel()) } - val result = jobCodeController.allPaginated(50, 0, null) + val result = jobCodeController.allPaginated(50, 0, "name.asc", "name") Assertions.assertThat(result.body).hasSizeGreaterThan(0) } @@ -83,4 +83,4 @@ internal class JobCodeControllerTest @Autowired constructor( } -} \ No newline at end of file +} diff --git a/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt index 29b7509f1..1973fa1e9 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt @@ -82,4 +82,4 @@ internal class NamedReferencesControllerTest @Autowired constructor( Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) } -} \ No newline at end of file +} From acf9b988b19b42eba768727319aff08059d609e5 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 18 May 2023 09:28:46 -0600 Subject: [PATCH 06/45] Working with pagination --- .../edu/wgu/osmt/api/model/JobCodeSortEnum.kt | 27 +++++++++++++++++++ .../kotlin/edu/wgu/osmt/jobcode/JobCode.kt | 3 ++- .../edu/wgu/osmt/jobcode/JobCodeController.kt | 10 +++++-- .../edu/wgu/osmt/jobcode/JobCodeEsRepo.kt | 14 +++++----- .../wgu/osmt/jobcode/JobCodeControllerTest.kt | 1 + 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt new file mode 100644 index 000000000..2d0612a7b --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt @@ -0,0 +1,27 @@ +package edu.wgu.osmt.api.model + +import edu.wgu.osmt.config.NAME_SORT_INSENSITIVE +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.data.domain.Sort + +enum class JobCodeSortEnum(override val apiValue: String): SortOrder { + NameAsc("name.asc") { + override val sort = Sort.by("name.keyword").ascending() + }, + NameDesc("name.desc") { + override val sort = Sort.by("name.keyword").descending() + }; + + companion object : SortOrderCompanion { + override val logger: Logger = LoggerFactory.getLogger(JobCodeSortEnum::class.java) + + override val defaultSort = NameAsc + + override fun forApiValue(apiValue: String): JobCodeSortEnum { + return values().find { it.apiValue == apiValue } ?: NameAsc.also { + logger.warn("Sort with value $apiValue could not be found; using default ${NameAsc.apiValue} sort") + } + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt index f00bcb7ca..1daffd63f 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt @@ -75,7 +75,8 @@ data class JobCode( otherFields = [ InnerField(suffix = "", type = FieldType.Search_As_You_Type), InnerField(suffix = "raw", analyzer = "whitespace_exact", type = FieldType.Text), - InnerField(suffix = "keyword", type = FieldType.Keyword) + InnerField(suffix = "keyword", type = FieldType.Keyword), + // InnerField(suffix = "sort_insensitive", type = FieldType.Keyword, normalizer = "lowercase_normalizer") ] ) val name: String? = null, // human readable label diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index 8fde57666..87ffa1f15 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -3,12 +3,15 @@ package edu.wgu.osmt.jobcode; import edu.wgu.osmt.PaginationDefaults import edu.wgu.osmt.RoutePaths import edu.wgu.osmt.api.model.ApiJobCode +import edu.wgu.osmt.api.model.JobCodeSortEnum import edu.wgu.osmt.api.model.JobCodeUpdate +import edu.wgu.osmt.api.model.KeywordSortEnum import edu.wgu.osmt.elasticsearch.OffsetPageable import edu.wgu.osmt.task.TaskResult import edu.wgu.osmt.task.TaskStatus import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -38,8 +41,11 @@ class JobCodeController @Autowired constructor( @RequestParam(required = false) sort: String?, @RequestParam(required = false) query: String? ): HttpEntity> { - val searchResults = jobCodeEsRepo.typeAheadSearch("", OffsetPageable(from, size, null)) - return ResponseEntity.status(200).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) + val sortEnum: JobCodeSortEnum = JobCodeSortEnum.forValueOrDefault(sort) + val searchResults = jobCodeEsRepo.typeAheadSearch(query, OffsetPageable(from, size, sortEnum.sort)) + val responseHeaders = HttpHeaders() + responseHeaders.add("X-Total-Count", searchResults.totalHits.toString()) + return ResponseEntity.status(200).headers(responseHeaders).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) } @GetMapping(RoutePaths.JOB_CODE_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE]) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt index f2e1b21a7..fae1a425a 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt @@ -20,7 +20,7 @@ interface CustomJobCodeRepository { val elasticSearchTemplate: ElasticsearchRestTemplate fun typeAheadSearch(query: String): SearchHits - fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits + fun typeAheadSearch(query: String?, pageable: OffsetPageable): SearchHits fun deleteIndex() { elasticSearchTemplate.indexOps(IndexCoordinates.of(INDEX_JOBCODE_DOC)).delete() @@ -30,12 +30,12 @@ interface CustomJobCodeRepository { class CustomJobCodeRepositoryImpl @Autowired constructor(override val elasticSearchTemplate: ElasticsearchRestTemplate) : CustomJobCodeRepository { - override fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits { - val nsq: NativeSearchQueryBuilder - val disjunctionQuery = JobCodeQueries.multiPropertySearch(query) - nsq = - NativeSearchQueryBuilder().withPageable(pageable).withQuery(disjunctionQuery) - .withSort(SortBuilders.fieldSort("${JobCode::code.name}.keyword").order(SortOrder.ASC)) + override fun typeAheadSearch(query: String?, pageable: OffsetPageable): SearchHits { + val nsq: NativeSearchQueryBuilder = NativeSearchQueryBuilder().withPageable(pageable) + if (!query.isNullOrEmpty()) { + val disjunctionQuery = JobCodeQueries.multiPropertySearch(query) + nsq.withQuery(disjunctionQuery) + } return elasticSearchTemplate.search(nsq.build(), JobCode::class.java) } diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt index 864f3b5e7..1f0429e06 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt @@ -3,6 +3,7 @@ package edu.wgu.osmt.jobcode import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset import edu.wgu.osmt.SpringTest +import edu.wgu.osmt.api.model.JobCodeSortEnum import edu.wgu.osmt.api.model.JobCodeUpdate import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test From c2f930a82a0b0af3ac14e63a605eb3429bcee55e Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 18 May 2023 16:56:03 -0600 Subject: [PATCH 07/45] Add sort insensitive --- .../main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt | 5 ++--- api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt index 2d0612a7b..7f4cf5c51 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt @@ -1,16 +1,15 @@ package edu.wgu.osmt.api.model -import edu.wgu.osmt.config.NAME_SORT_INSENSITIVE import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.data.domain.Sort enum class JobCodeSortEnum(override val apiValue: String): SortOrder { NameAsc("name.asc") { - override val sort = Sort.by("name.keyword").ascending() + override val sort = Sort.by("name.sort_insensitive").ascending() }, NameDesc("name.desc") { - override val sort = Sort.by("name.keyword").descending() + override val sort = Sort.by("name.sort_insensitive").descending() }; companion object : SortOrderCompanion { diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt index 1daffd63f..5e69ba543 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt @@ -76,7 +76,7 @@ data class JobCode( InnerField(suffix = "", type = FieldType.Search_As_You_Type), InnerField(suffix = "raw", analyzer = "whitespace_exact", type = FieldType.Text), InnerField(suffix = "keyword", type = FieldType.Keyword), - // InnerField(suffix = "sort_insensitive", type = FieldType.Keyword, normalizer = "lowercase_normalizer") + InnerField(suffix = "sort_insensitive", type = FieldType.Keyword, normalizer = "lowercase_normalizer") ] ) val name: String? = null, // human readable label From 10fd7b5b29f64107a0f0c273a91390dc58369834 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 19 May 2023 12:23:16 -0600 Subject: [PATCH 08/45] Add job code leven and parents --- .../wgu/osmt/api/model/ApiNamedReference.kt | 18 +++++++++++++++ .../edu/wgu/osmt/jobcode/JobCodeController.kt | 23 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt index 0727d03db..2d4d720ce 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt @@ -6,6 +6,7 @@ import edu.wgu.osmt.collection.Collection import edu.wgu.osmt.db.JobCodeLevel import edu.wgu.osmt.jobcode.JobCode import edu.wgu.osmt.keyword.Keyword +import kotlinx.coroutines.Job @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -86,5 +87,22 @@ data class ApiJobCode( fun fromJobCode(jobCode: JobCode, level: JobCodeLevel? = null, parents: List? = null): ApiJobCode { return ApiJobCode(code=jobCode.code, targetNodeName=jobCode.name, targetNode=jobCode.url, frameworkName=jobCode.framework, level=level, parents=parents) } + + fun getLevelFromJobCode(jobCode: JobCode): JobCodeLevel { + return when (jobCode.code) { + jobCode.majorCode -> { + JobCodeLevel.Major + } + jobCode.minorCode -> { + JobCodeLevel.Minor + } + jobCode.broadCode -> { + JobCodeLevel.Broad + } + else -> { + JobCodeLevel.Detailed + } + } + } } } diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index 87ffa1f15..f81485dc9 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -6,6 +6,7 @@ import edu.wgu.osmt.api.model.ApiJobCode import edu.wgu.osmt.api.model.JobCodeSortEnum import edu.wgu.osmt.api.model.JobCodeUpdate import edu.wgu.osmt.api.model.KeywordSortEnum +import edu.wgu.osmt.db.JobCodeLevel import edu.wgu.osmt.elasticsearch.OffsetPageable import edu.wgu.osmt.task.TaskResult import edu.wgu.osmt.task.TaskStatus @@ -45,7 +46,27 @@ class JobCodeController @Autowired constructor( val searchResults = jobCodeEsRepo.typeAheadSearch(query, OffsetPageable(from, size, sortEnum.sort)) val responseHeaders = HttpHeaders() responseHeaders.add("X-Total-Count", searchResults.totalHits.toString()) - return ResponseEntity.status(200).headers(responseHeaders).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) + return ResponseEntity.status(200).headers(responseHeaders).body(searchResults.map { + val jobCodeLevel = ApiJobCode.getLevelFromJobCode(it.content) + val parents = mutableListOf() + val majorCode = it.content.majorCode + val minorCode = it.content.minorCode + val broadCode = it.content.broadCode + val detailedCode = it.content.detailedCode + if (detailedCode != null && jobCodeLevel != JobCodeLevel.Detailed) { + jobCodeRepository.findByCode(detailedCode)?.let { jobCodeDao -> parents.add(jobCodeDao) } + } + if (broadCode != null && jobCodeLevel != JobCodeLevel.Broad) { + jobCodeRepository.findByCode(broadCode)?.let { jobCodeDao -> parents.add(jobCodeDao) } + } + if (minorCode != null && jobCodeLevel != JobCodeLevel.Minor) { + jobCodeRepository.findByCode(minorCode)?.let { jobCodeDao -> parents.add(jobCodeDao) } + } + if (majorCode != null && jobCodeLevel != JobCodeLevel.Major) { + jobCodeRepository.findByCode(majorCode)?.let { jobCodeDao -> parents.add(jobCodeDao) } + } + ApiJobCode.fromJobCode(it.content, jobCodeLevel, parents.map { it2 -> ApiJobCode.fromJobCode(it2.toModel(), ApiJobCode.getLevelFromJobCode(it2.toModel())) }) + }.toList()) } @GetMapping(RoutePaths.JOB_CODE_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE]) From 8db96ff6e0b51383d54e5a50f1556fa67b030c58 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 19 May 2023 13:32:45 -0600 Subject: [PATCH 09/45] Delete job code --- .../edu/wgu/osmt/jobcode/JobCodeController.kt | 19 ++++---- .../edu/wgu/osmt/jobcode/JobCodeRepository.kt | 25 ++++++++++ .../wgu/osmt/jobcode/JobCodeTaskProcessor.kt | 46 +++++++++++++++++++ api/src/main/kotlin/edu/wgu/osmt/task/Task.kt | 14 +++++- .../edu/wgu/osmt/task/TaskMessageService.kt | 1 + 5 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeTaskProcessor.kt diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index f81485dc9..b2a615537 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -5,9 +5,11 @@ import edu.wgu.osmt.RoutePaths import edu.wgu.osmt.api.model.ApiJobCode import edu.wgu.osmt.api.model.JobCodeSortEnum import edu.wgu.osmt.api.model.JobCodeUpdate -import edu.wgu.osmt.api.model.KeywordSortEnum import edu.wgu.osmt.db.JobCodeLevel import edu.wgu.osmt.elasticsearch.OffsetPageable +import edu.wgu.osmt.task.RemoveJobCodeTask +import edu.wgu.osmt.task.Task +import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskResult import edu.wgu.osmt.task.TaskStatus import org.springframework.beans.factory.annotation.Autowired @@ -31,7 +33,8 @@ import org.springframework.web.server.ResponseStatusException @Transactional class JobCodeController @Autowired constructor( val jobCodeEsRepo: JobCodeEsRepo, - val jobCodeRepository: JobCodeRepository + val jobCodeRepository: JobCodeRepository, + val taskMessageService: TaskMessageService, ) { @GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) @@ -105,14 +108,10 @@ class JobCodeController @Autowired constructor( fun deleteJobCode( @PathVariable id: Int, ): HttpEntity { - return ResponseEntity.status(200).body( - TaskResult( - uuid = "uuid", - contentType = "application/json", - status = TaskStatus.Processing, - apiResultPath = "path" - ) - ) + val task = RemoveJobCodeTask(jobCodeId = id.toLong()) + taskMessageService.enqueueJob(TaskMessageService.removeJobCode, task) + // return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) + return Task.processingResponse(task) } } diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt index 32138c1de..682de0b64 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -1,10 +1,13 @@ package edu.wgu.osmt.jobcode +import edu.wgu.osmt.api.model.ApiBatchResult import edu.wgu.osmt.api.model.JobCodeUpdate import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Repository @@ -22,6 +25,7 @@ interface JobCodeRepository { fun create(code: String, framework: String? = null): JobCodeDao fun createFromApi(jobCodes: List): List fun onetsByDetailCode(detailedCode: String): SizedIterable + fun remove(jobCodeId: Long): ApiBatchResult companion object { const val BLS_FRAMEWORK = "bls" @@ -92,4 +96,25 @@ class JobCodeRepositoryImpl: JobCodeRepository { override fun onetsByDetailCode(detailedCode: String): SizedIterable { return table.select {table.code regexp "${detailedCode}.[0-90-9]"}.let{dao.wrapRows(it)} } + + override fun remove(jobCodeId: Long): ApiBatchResult { + val jobCodeFound = findById(jobCodeId) + val jobCodeEsFound = jobCodeEsRepo.findById(jobCodeId.toInt()) + if (jobCodeFound != null && jobCodeEsFound.isPresent) { + transaction { + table.deleteWhere{ table.id eq jobCodeFound.id } + jobCodeEsRepo.delete(jobCodeEsFound.get()) + } + return ApiBatchResult( + success = true, + modifiedCount = 1, + totalCount = 1 + ) + } + return ApiBatchResult( + success = false, + modifiedCount = 0, + totalCount = 0 + ) + } } diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeTaskProcessor.kt new file mode 100644 index 000000000..b9ea644f8 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeTaskProcessor.kt @@ -0,0 +1,46 @@ +package edu.wgu.osmt.jobcode + +import com.github.sonus21.rqueue.annotation.RqueueListener +import edu.wgu.osmt.collection.UpdateCollectionSkillsTaskProcessor +import edu.wgu.osmt.task.RemoveJobCodeTask +import edu.wgu.osmt.task.TaskMessageService +import edu.wgu.osmt.task.TaskStatus +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import javax.transaction.Transactional + +@Component +@Profile("apiserver") +@Transactional +class JobCodeTaskProcessor { + + val logger: Logger = LoggerFactory.getLogger(UpdateCollectionSkillsTaskProcessor::class.java) + + @Autowired + lateinit var taskMessageService: TaskMessageService + + @Autowired + lateinit var jobCodeRepository: JobCodeRepository + + @RqueueListener( + value = [TaskMessageService.removeJobCode], + deadLetterQueueListenerEnabled = "true", + deadLetterQueue = TaskMessageService.deadLetters, + concurrency = "1" + ) + fun removeJobCode(task: RemoveJobCodeTask) { + logger.info("Started processing to remove job code task id: ${task.jobCodeId}") + + val batchResult = jobCodeRepository.remove(task.jobCodeId) + + taskMessageService.publishResult( + task.copy(result=batchResult, status= TaskStatus.Ready) + ) + + logger.info("Task ${task.uuid} completed") + } + +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt b/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt index 39573e7af..e54aad73e 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt @@ -30,7 +30,8 @@ import java.util.* JsonSubTypes.Type(value = CreateSkillsTask::class, name = "CreateSkillsTask"), JsonSubTypes.Type(value = ExportSkillsToCsvTask::class, name = "ExportSkillsToCsvTask"), JsonSubTypes.Type(value = ExportSkillsToXlsxTask::class, name = "ExportSkillsToXlsxTask"), - JsonSubTypes.Type(value = RemoveCollectionSkillsTask::class, name = "RemoveCollectionSkillsTask") + JsonSubTypes.Type(value = RemoveCollectionSkillsTask::class, name = "RemoveCollectionSkillsTask"), + JsonSubTypes.Type(value = RemoveJobCodeTask::class, name = "RemoveJobCodeTask") ) interface Task { @@ -165,6 +166,17 @@ data class RemoveCollectionSkillsTask( override val apiResultPath = RoutePaths.TASK_DETAIL_BATCH } +data class RemoveJobCodeTask( + val jobCodeId: Long = 0, + override val uuid: String = UUID.randomUUID().toString(), + override val start: Date = Date(), + override val result: ApiBatchResult? = null, + override val status: TaskStatus = TaskStatus.Processing, +): Task { + override val contentType = MediaType.APPLICATION_JSON_VALUE + override val apiResultPath = RoutePaths.TASK_DETAIL_BATCH +} + enum class TaskStatus { Processing, Ready diff --git a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt index f8ac08e5d..3a99652ba 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt @@ -49,5 +49,6 @@ class TaskMessageService { const val skillsForFullLibraryXlsx = "full-library-skills-xlsx-process" const val skillsForCustomListExportCsv = "custom-rsd-list-csv-export" const val skillsForCustomListExportXlsx = "custom-rsd-list-xlsx-export" + const val removeJobCode = "remove-job-code" } } From d943a1a530f80c4c6fde857466ee883abfced242 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:37:54 -0600 Subject: [PATCH 10/45] Fix delete test --- api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt | 3 --- .../test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index b2a615537..bfd83fc84 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -1,6 +1,5 @@ package edu.wgu.osmt.jobcode; -import edu.wgu.osmt.PaginationDefaults import edu.wgu.osmt.RoutePaths import edu.wgu.osmt.api.model.ApiJobCode import edu.wgu.osmt.api.model.JobCodeSortEnum @@ -11,7 +10,6 @@ import edu.wgu.osmt.task.RemoveJobCodeTask import edu.wgu.osmt.task.Task import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskResult -import edu.wgu.osmt.task.TaskStatus import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders @@ -110,7 +108,6 @@ class JobCodeController @Autowired constructor( ): HttpEntity { val task = RemoveJobCodeTask(jobCodeId = id.toLong()) taskMessageService.enqueueJob(TaskMessageService.removeJobCode, task) - // return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) return Task.processingResponse(task) } diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt index 1f0429e06..43487aadb 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt @@ -3,7 +3,6 @@ package edu.wgu.osmt.jobcode import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset import edu.wgu.osmt.SpringTest -import edu.wgu.osmt.api.model.JobCodeSortEnum import edu.wgu.osmt.api.model.JobCodeUpdate import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test @@ -76,11 +75,12 @@ internal class JobCodeControllerTest @Autowired constructor( @Test fun `Delete should return status 200`() { + jobCodeController.taskMessageService.rqueueMessageSender.registerQueue("remove-job-code") val result = jobCodeController.deleteJobCode( 1 ) Assertions.assertThat(result).isNotNull - Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.ACCEPTED) } From 3db1cdc89ee516d52937dd50d3d4eeed07d6b820 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:22:01 -0600 Subject: [PATCH 11/45] Remove PaginatedJobCodes --- .../metadata-list/metadata-list.component.ts | 27 +++++++++---------- .../service/job-code.service.spec.ts | 13 ++++----- ui/test/resource/mock-data.ts | 6 ++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 368b2610c..5c82c0037 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -11,6 +11,7 @@ import { TableActionDefinition } from "../../../table/skills-library-table/has-a import { ButtonAction } from "../../../auth/auth-roles" import { AuthService } from "../../../auth/auth-service" import { MetadataType } from "../../rsd-metadata.enum" +import { JobCodeService } from "../../job-codes/service/job-code.service" @Component({ selector: "app-metadata-list", @@ -37,16 +38,7 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { searchForm = new FormGroup({ search: new FormControl("") }) - sampleJobCodeResult = new PaginatedMetadata([ - new ApiJobCode({code: "code1", targetNodeName: "targetNodeName1", frameworkName: "frameworkName1", url: "url1", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code2", targetNodeName: "targetNodeName2", frameworkName: "frameworkName2", url: "url2", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code3", targetNodeName: "targetNodeName3", frameworkName: "frameworkName3", url: "url3", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code4", targetNodeName: "targetNodeName4", frameworkName: "frameworkName4", url: "url4", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code5", targetNodeName: "targetNodeName5", frameworkName: "frameworkName5", url: "url5", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code6", targetNodeName: "targetNodeName6", frameworkName: "frameworkName6", url: "url6", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code7", targetNodeName: "targetNodeName7", frameworkName: "frameworkName7", url: "url7", broad: "broad1", level: "Broad"}), - new ApiJobCode({code: "code8", targetNodeName: "targetNodeName8", frameworkName: "frameworkName8", url: "url8", broad: "broad1", level: "Broad"}), - ], 8) + sampleJobCodeResult = new PaginatedMetadata([], 0) sampleNamedReferenceResult = new PaginatedMetadata([ new ApiNamedReference({id: "id1", framework: "framework1", name: "name1", type: MetadataType.Category, value: "value1"}), @@ -62,7 +54,10 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { results: PaginatedMetadata = this.sampleNamedReferenceResult clearSelectedItemsFromTable = new Subject() - constructor(protected authService: AuthService) { + constructor( + protected authService: AuthService, + protected jobCodeService: JobCodeService + ) { super() } @@ -85,13 +80,17 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { return false } - handleDefaultSubmit(): boolean { + handleDefaultSubmit(): void { this.loadNextPage() this.from = 0 + } - return false + loadNextPage(): void { + this.jobCodeService.paginatedJobCodes(this.size, this.from, this.columnSort, this.matchingQuery).subscribe( + jobCodes => this.results = jobCodes + ) } - loadNextPage(): void {} + handleSelectAll(selectAllChecked: boolean): void {} handleNewSelection(selected: IJobCode[]|INamedReference[]): void { diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts index 3d9122dd7..dcac2016b 100644 --- a/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts +++ b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts @@ -2,7 +2,7 @@ import { fakeAsync, TestBed, tick } from "@angular/core/testing" import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" import { Location } from "@angular/common" import { Router } from "@angular/router" -import { JobCodeService, PaginatedJobCodes } from "./job-code.service" +import { JobCodeService } from "./job-code.service" import { AuthServiceData, AuthServiceStub, RouterData, RouterStub } from "@test/resource/mock-stubs" import { AppConfig } from "../../../app.config" import { EnvironmentService } from "../../../core/environment.service" @@ -10,11 +10,12 @@ import { AuthService } from "../../../auth/auth-service" import { apiTaskResultForDeleteJobCode, createMockJobcode, - createMockPaginatedJobCodes + createMockPaginatedMetaDataWithJobCodes } from "@test/resource/mock-data" import { ApiSortOrder } from "../../../richskill/ApiSkill" import { ApiJobCode, ApiJobCodeUpdate } from "../Jobcode" import { ApiBatchResult } from "../../../richskill/ApiBatchResult" +import { PaginatedMetadata } from "../../PaginatedMetadata" const ASYNC_WAIT_PERIOD = 3000 @@ -49,15 +50,15 @@ describe("JobCodeService", () => { RouterData.commands = [] AuthServiceData.isDown = false const path = "api/metadata/jobcodes?sort=name.asc&size=3&from=0" - const testData: PaginatedJobCodes = createMockPaginatedJobCodes(3, 10) + const testData: PaginatedMetadata = createMockPaginatedMetaDataWithJobCodes(3, 10) // Act // noinspection LocalVariableNamingConventionJS - const result$ = testService.getJobCodes(testData.jobCodes.length, 0, ApiSortOrder.NameAsc) + const result$ = testService.paginatedJobCodes(testData.metadata.length, 0, ApiSortOrder.NameAsc, undefined) // Assert result$ - .subscribe((data: PaginatedJobCodes) => { + .subscribe((data: PaginatedMetadata) => { expect(data).toEqual(testData) expect(RouterData.commands).toEqual([ ]) // No errors expect(AuthServiceData.isDown).toEqual(false) @@ -65,7 +66,7 @@ describe("JobCodeService", () => { const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) expect(req.request.method).toEqual("GET") - req.flush(testData.jobCodes, { + req.flush(testData.metadata, { headers: { "x-total-count": "" + testData.totalCount} }) }) diff --git a/ui/test/resource/mock-data.ts b/ui/test/resource/mock-data.ts index 43430c317..7ae41b1ac 100644 --- a/ui/test/resource/mock-data.ts +++ b/ui/test/resource/mock-data.ts @@ -19,7 +19,7 @@ import { ApiReferenceListUpdate, IRichSkillUpdate, IStringListUpdate } from "../ import { PaginatedCollections, PaginatedSkills } from "../../src/app/richskill/service/rich-skill-search.service" import { ApiTaskResult, ITaskResult } from "../../src/app/task/ApiTaskResult" import { IJobCode } from "../../src/app/metadata/job-codes/Jobcode"; -import { PaginatedJobCodes } from "../../src/app/metadata/job-codes/service/job-code.service" +import { PaginatedMetadata } from "../../src/app/metadata/PaginatedMetadata" // Add mock data here. // For more examples, see https://github.com/WGU-edu/ema-eval-ui/blob/develop/src/app/admin/pages/edit-user/edit-user.component.spec.ts @@ -322,7 +322,7 @@ export const apiTaskResultForDeleteJobCode: ApiTaskResult = { id: "/api/results/batch/5ca6ea7f-e008-44fc-9108-eda19b01fa6b" } -export function createMockPaginatedJobCodes(jobCodeCount = 1, total = 10): PaginatedJobCodes { +export function createMockPaginatedMetaDataWithJobCodes(jobCodeCount = 1, total = 10): PaginatedMetadata { if (jobCodeCount > total) { throw new RangeError(`'pageCount' must be <= 'total'`) } @@ -334,7 +334,7 @@ export function createMockPaginatedJobCodes(jobCodeCount = 1, total = 10): Pagin ) } - return new PaginatedJobCodes( + return new PaginatedMetadata( jobCodes, total ) From 10a7cbea4470793e720c7b954863f15802c554d7 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:28:33 -0600 Subject: [PATCH 12/45] Show job codes in list --- .../metadata-list/metadata-list.component.ts | 24 +++++----- .../job-code-list-row.component.html | 2 +- .../job-code-list-row.component.ts | 2 +- .../job-codes/service/job-code.service.ts | 44 ++++++++----------- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 5c82c0037..d44f0c369 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup } from "@angular/forms" import { Observable, Subject } from "rxjs" import { PaginatedMetadata } from "../../PaginatedMetadata" import { ApiSortOrder } from "../../../richskill/ApiSkill" -import { ApiJobCode, IJobCode } from "../../job-codes/Jobcode" +import { IJobCode } from "../../job-codes/Jobcode" import { TableActionBarComponent } from "../../../table/skills-library-table/table-action-bar.component" import { Whitelabelled } from "../../../../whitelabel" import { ApiNamedReference, INamedReference } from "../../named-references/NamedReference" @@ -24,7 +24,7 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { title = "Metadata" handleSelectedMetadata?: IJobCode[]|INamedReference[] selectedMetadataType = "category" - matchingQuery: string = "" + matchingQuery = "" typeControl: FormControl = new FormControl(this.selectedMetadataType) columnSort: ApiSortOrder = ApiSortOrder.NameAsc @@ -38,7 +38,6 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { searchForm = new FormGroup({ search: new FormControl("") }) - sampleJobCodeResult = new PaginatedMetadata([], 0) sampleNamedReferenceResult = new PaginatedMetadata([ new ApiNamedReference({id: "id1", framework: "framework1", name: "name1", type: MetadataType.Category, value: "value1"}), @@ -65,14 +64,10 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { this.typeControl.valueChanges.subscribe( value => { this.selectedMetadataType = value - if (this.selectedMetadataType === MetadataType.JobCode) { - this.results = this.sampleJobCodeResult - } - else { - this.results = this.sampleNamedReferenceResult - } + this.loadNextPage() }) this.searchForm.get("search")?.valueChanges.subscribe( value => this.matchingQuery = value!) + this.loadNextPage() } clearSearch(): boolean { @@ -86,9 +81,13 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { } loadNextPage(): void { - this.jobCodeService.paginatedJobCodes(this.size, this.from, this.columnSort, this.matchingQuery).subscribe( - jobCodes => this.results = jobCodes - ) + if (this.selectedMetadataType === MetadataType.JobCode) { + this.jobCodeService.paginatedJobCodes(this.size, this.from, this.columnSort, this.matchingQuery).subscribe( + jobCodes => this.results = jobCodes + ) + } else { + this.results = this.sampleNamedReferenceResult + } } handleSelectAll(selectAllChecked: boolean): void {} @@ -143,7 +142,6 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { return this.curPageCount < 1 } get isJobCodeDataSelected(): boolean { - console.log(this.selectedMetadataType === MetadataType.JobCode.toString()) return this.selectedMetadataType === MetadataType.JobCode } diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html index fa7651888..2a5064f52 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html @@ -7,7 +7,7 @@ {{jobCode.level}} - {{jobCode.parents}} + {{jobCode.parents | json}} {{jobCode.frameworkName}} diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts index 09f3b223a..8005d9ccd 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core" +import { Component, EventEmitter, Input, Output } from "@angular/core" import { TableActionDefinition } from "../../../table/skills-library-table/has-action-definitions" import { SvgHelper, SvgIcon } from "../../../core/SvgHelper" import { ApiJobCode, IJobCode } from "../Jobcode" diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.ts b/ui/src/app/metadata/job-codes/service/job-code.service.ts index 67698b34c..2710cfeaf 100644 --- a/ui/src/app/metadata/job-codes/service/job-code.service.ts +++ b/ui/src/app/metadata/job-codes/service/job-code.service.ts @@ -1,20 +1,21 @@ import { Location } from "@angular/common" -import { HttpClient, HttpHeaders } from "@angular/common/http" +import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http" import { Injectable } from "@angular/core" import { Router } from "@angular/router" import { Observable } from "rxjs" import { map, share } from "rxjs/operators" import { ApiJobCode, IJobCode, IJobCodeUpdate } from "../Jobcode" -import { AuthService } from "../../../auth/auth-service"; -import { AbstractDataService } from "../../abstract-data.service"; -import { ApiSortOrder } from "../../../richskill/ApiSkill"; -import { ApiBatchResult } from "../../../richskill/ApiBatchResult"; -import { ApiTaskResult, ITaskResult } from "../../../task/ApiTaskResult"; +import { AuthService } from "../../../auth/auth-service" +import { AbstractDataService } from "../../abstract-data.service" +import { ApiSortOrder } from "../../../richskill/ApiSkill" +import { ApiBatchResult } from "../../../richskill/ApiBatchResult" +import { ApiTaskResult, ITaskResult } from "../../../task/ApiTaskResult" +import { PaginatedMetadata } from "../../PaginatedMetadata" @Injectable({ providedIn: "root" }) -export class JobCodeService extends AbstractDataService{ +export class JobCodeService extends AbstractDataService { private baseServiceUrl = "api/metadata/jobcodes" @@ -23,19 +24,21 @@ export class JobCodeService extends AbstractDataService{ super(httpClient, authService, router, location) } - getJobCodes( - size: number = 50, - from: number = 0, - sort: ApiSortOrder | undefined - ): Observable { - const params = this.buildTableParams(size, from, undefined, sort) + paginatedJobCodes( + size = 50, + from = 0, + sort: ApiSortOrder | undefined, + query: string | undefined + ): Observable { + const params = new HttpParams({ + fromObject: {size, from, sort: sort ?? "", query: query ?? ""} + }) return this.get({ path: `${this.baseServiceUrl}`, params, - }) - .pipe(share()) + }).pipe(share()) .pipe(map(({body, headers}) => { - return new PaginatedJobCodes( + return new PaginatedMetadata( body || [], Number(headers.get("X-Total-Count")) ) @@ -86,12 +89,3 @@ export class JobCodeService extends AbstractDataService{ .pipe(map((body) => new ApiTaskResult(this.safeUnwrapBody(body, "unwrap failure")))) } } - -export class PaginatedJobCodes { - totalCount = 0 - jobCodes: ApiJobCode[] = [] - constructor(jobCodes: ApiJobCode[], totalCount: number) { - this.jobCodes = jobCodes - this.totalCount = totalCount - } -} From f307f4381b4739b2bd20839c4f5cfee2d53533b6 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:09:06 -0600 Subject: [PATCH 13/45] Request all on clear search --- .../detail/metadata-list/metadata-list.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index d44f0c369..09b33c328 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -24,7 +24,7 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { title = "Metadata" handleSelectedMetadata?: IJobCode[]|INamedReference[] selectedMetadataType = "category" - matchingQuery = "" + matchingQuery?: string = "" typeControl: FormControl = new FormControl(this.selectedMetadataType) columnSort: ApiSortOrder = ApiSortOrder.NameAsc @@ -66,18 +66,18 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { this.selectedMetadataType = value this.loadNextPage() }) - this.searchForm.get("search")?.valueChanges.subscribe( value => this.matchingQuery = value!) + this.searchForm.get("search")?.valueChanges.subscribe( value => this.matchingQuery = value ?? "") this.loadNextPage() } - clearSearch(): boolean { - this.searchForm.reset() - return false + clearSearch(): void { + this.searchForm.get("search")?.patchValue("") + this.handleDefaultSubmit() } handleDefaultSubmit(): void { - this.loadNextPage() this.from = 0 + this.loadNextPage() } loadNextPage(): void { From 52e59de7d5cbfc742569ffe343207df6f37374dc Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:18:57 -0600 Subject: [PATCH 14/45] Delete job code by id - Add id in ApiJobCode (ui & api) - Call request to delete in ui --- .../edu/wgu/osmt/api/model/ApiNamedReference.kt | 3 ++- .../kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt | 2 +- .../detail/metadata-list/metadata-list.component.ts | 12 +++++++----- ui/src/app/metadata/job-codes/Jobcode.ts | 2 ++ .../metadata/job-codes/service/job-code.service.ts | 4 ++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt index 2d4d720ce..7b6e0e13c 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt @@ -76,6 +76,7 @@ data class ApiStringListUpdate( @JsonInclude(JsonInclude.Include.NON_EMPTY) data class ApiJobCode( + val id: Long? = null, val code: String, val targetNode: String? = null, val targetNodeName: String? = null, @@ -85,7 +86,7 @@ data class ApiJobCode( ) { companion object factory { fun fromJobCode(jobCode: JobCode, level: JobCodeLevel? = null, parents: List? = null): ApiJobCode { - return ApiJobCode(code=jobCode.code, targetNodeName=jobCode.name, targetNode=jobCode.url, frameworkName=jobCode.framework, level=level, parents=parents) + return ApiJobCode(id = jobCode.id, code=jobCode.code, targetNodeName=jobCode.name, targetNode=jobCode.url, frameworkName=jobCode.framework, level=level, parents=parents) } fun getLevelFromJobCode(jobCode: JobCode): JobCodeLevel { diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt index bfd83fc84..57bd48475 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -98,7 +98,7 @@ class JobCodeController @Autowired constructor( @PathVariable id: Int, @RequestBody jobCodeUpdate: JobCodeUpdate ): HttpEntity { - return ResponseEntity.status(200).body(ApiJobCode(code = "1", targetNode = "target", targetNodeName = "targetNodeName", frameworkName = "frameworkName", parents = listOf())) + return ResponseEntity.status(200).body(ApiJobCode(id = 1, code = "1", targetNode = "target", targetNodeName = "targetNodeName", frameworkName = "frameworkName", parents = listOf())) } @DeleteMapping(RoutePaths.JOB_CODE_REMOVE) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 09b33c328..2c8715382 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup } from "@angular/forms" import { Observable, Subject } from "rxjs" import { PaginatedMetadata } from "../../PaginatedMetadata" import { ApiSortOrder } from "../../../richskill/ApiSkill" -import { IJobCode } from "../../job-codes/Jobcode" +import { ApiJobCode, IJobCode } from "../../job-codes/Jobcode" import { TableActionBarComponent } from "../../../table/skills-library-table/table-action-bar.component" import { Whitelabelled } from "../../../../whitelabel" import { ApiNamedReference, INamedReference } from "../../named-references/NamedReference" @@ -81,7 +81,7 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { } loadNextPage(): void { - if (this.selectedMetadataType === MetadataType.JobCode) { + if (this.isJobCodeDataSelected) { this.jobCodeService.paginatedJobCodes(this.size, this.from, this.columnSort, this.matchingQuery).subscribe( jobCodes => this.results = jobCodes ) @@ -181,13 +181,15 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { if (this.canDeleteMetadata) { tableActions.push(new TableActionDefinition({ label: `Delete`, - callback: (action: TableActionDefinition, skill?: IJobCode|INamedReference) => this.handleClickDeleteItem(action, skill), + callback: (action: TableActionDefinition, jobCode?: IJobCode | INamedReference) => this.handleClickDeleteItem(jobCode), })) } return tableActions } - // tslint:disable-next-line:typedef - private handleClickDeleteItem(action: TableActionDefinition, skill: IJobCode|INamedReference | undefined) { + private handleClickDeleteItem(jobCode: IJobCode | INamedReference | undefined): void { + if (this.isJobCodeDataSelected) { + this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe() + } } } diff --git a/ui/src/app/metadata/job-codes/Jobcode.ts b/ui/src/app/metadata/job-codes/Jobcode.ts index 5217fbed0..c2582e305 100644 --- a/ui/src/app/metadata/job-codes/Jobcode.ts +++ b/ui/src/app/metadata/job-codes/Jobcode.ts @@ -1,6 +1,7 @@ export type JobCodeLevel = "Major" | "Minor" | "Broad" | "Detailed" | "Onet" export interface IJobCode { + id?: number targetNodeName?: string code: string targetNode?: number @@ -18,6 +19,7 @@ export interface IJobCode { } export class ApiJobCode implements IJobCode { + id?: number code = "" targetNodeName?: string targetNode?: number diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.ts b/ui/src/app/metadata/job-codes/service/job-code.service.ts index 2710cfeaf..3d80e787e 100644 --- a/ui/src/app/metadata/job-codes/service/job-code.service.ts +++ b/ui/src/app/metadata/job-codes/service/job-code.service.ts @@ -74,11 +74,11 @@ export class JobCodeService extends AbstractDataService { .pipe(map(({body}) => new ApiJobCode(this.safeUnwrapBody(body, errorMsg)))) } - deleteJobCodeWithResult(id: string): Observable { + deleteJobCodeWithResult(id: number): Observable { return this.pollForTaskResult(this.deleteJobCode(id)) } - deleteJobCode(id: string): Observable { + deleteJobCode(id: number): Observable { return this.httpClient.delete(this.buildUrl("api/metadata/jobcodes/" + id + "/remove"), { headers: this.wrapHeaders(new HttpHeaders({ Accept: "application/json" From 91a57dc2c71c846b83c1cfd05eab9176e3d6689a Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:34:06 -0600 Subject: [PATCH 15/45] Show toast when job code is deleted --- .../detail/metadata-list/metadata-list.component.ts | 10 ++++++++-- .../app/metadata/job-codes/service/job-code.service.ts | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 2c8715382..4a4a9a955 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -12,6 +12,7 @@ import { ButtonAction } from "../../../auth/auth-roles" import { AuthService } from "../../../auth/auth-service" import { MetadataType } from "../../rsd-metadata.enum" import { JobCodeService } from "../../job-codes/service/job-code.service" +import { ToastService } from "../../../toast/toast.service" @Component({ selector: "app-metadata-list", @@ -55,7 +56,8 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { clearSelectedItemsFromTable = new Subject() constructor( protected authService: AuthService, - protected jobCodeService: JobCodeService + protected jobCodeService: JobCodeService, + protected toastService: ToastService ) { super() } @@ -189,7 +191,11 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { private handleClickDeleteItem(jobCode: IJobCode | INamedReference | undefined): void { if (this.isJobCodeDataSelected) { - this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe() + this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe(data => { + if (data?.success) { + this.toastService.showToast("Success", "You deleted a job code with name " + (jobCode as ApiJobCode)?.targetNodeName) + } + }) } } } diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.ts b/ui/src/app/metadata/job-codes/service/job-code.service.ts index 3d80e787e..22a061fbe 100644 --- a/ui/src/app/metadata/job-codes/service/job-code.service.ts +++ b/ui/src/app/metadata/job-codes/service/job-code.service.ts @@ -19,8 +19,12 @@ export class JobCodeService extends AbstractDataService { private baseServiceUrl = "api/metadata/jobcodes" - constructor(protected httpClient: HttpClient, protected authService: AuthService, - protected router: Router, protected location: Location) { + constructor( + protected httpClient: HttpClient, + protected authService: AuthService, + protected router: Router, + protected location: Location + ) { super(httpClient, authService, router, location) } From 820e650d67d1316a0d43d747cd2531192f48f90c Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:39:42 -0600 Subject: [PATCH 16/45] Add confirm message & reload on delete --- .../detail/metadata-list/metadata-list.component.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 4a4a9a955..4ecfd6056 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -191,11 +191,14 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { private handleClickDeleteItem(jobCode: IJobCode | INamedReference | undefined): void { if (this.isJobCodeDataSelected) { - this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe(data => { - if (data?.success) { - this.toastService.showToast("Success", "You deleted a job code with name " + (jobCode as ApiJobCode)?.targetNodeName) - } - }) + if (confirm("Confirm that you want to delete the job code with name " + (jobCode as ApiJobCode)?.targetNodeName)) { + this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe(data => { + if (data?.success) { + this.toastService.showToast("Success", "You deleted a job code with name " + (jobCode as ApiJobCode)?.targetNodeName) + this.loadNextPage() + } + }) + } } } } From 3d1a697529d22181f42d185fe8dd7214e390af4d Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Thu, 1 Jun 2023 17:04:55 -0600 Subject: [PATCH 17/45] Fix broken tests --- .../detail/metadata-list/metadata-list.component.spec.ts | 6 ++++++ .../metadata/job-codes/service/job-code.service.spec.ts | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts index 7f7c951ab..5374bc84f 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts @@ -5,6 +5,8 @@ import { PaginatedMetadata } from "../../PaginatedMetadata" import { ApiJobCode } from "../../job-codes/Jobcode" import { AuthServiceStub } from "@test/resource/mock-stubs"; import { AuthService } from "../../../auth/auth-service"; +import { JobCodeService } from "../../job-codes/service/job-code.service" +import { HttpClientTestingModule } from "@angular/common/http/testing" describe("ManageMetadataComponent", () => { let component: MetadataListComponent @@ -15,6 +17,10 @@ describe("ManageMetadataComponent", () => { declarations: [ MetadataListComponent ], providers: [ { provide: AuthService, useClass: AuthServiceStub }, + JobCodeService + ], + imports: [ + HttpClientTestingModule ] }) .compileComponents() diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts index dcac2016b..c30e4092f 100644 --- a/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts +++ b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts @@ -49,7 +49,7 @@ describe("JobCodeService", () => { // Arrange RouterData.commands = [] AuthServiceData.isDown = false - const path = "api/metadata/jobcodes?sort=name.asc&size=3&from=0" + const path = "api/metadata/jobcodes?size=3&from=0&sort=name.asc&query=" const testData: PaginatedMetadata = createMockPaginatedMetaDataWithJobCodes(3, 10) // Act @@ -164,13 +164,14 @@ describe("JobCodeService", () => { }) it("deleteJobCodeWithResult() should works", fakeAsync(() => { - const result$ = testService.deleteJobCodeWithResult(apiTaskResultForDeleteJobCode.uuid) + const jobCodeId = 2 + const result$ = testService.deleteJobCodeWithResult(jobCodeId) tick(ASYNC_WAIT_PERIOD) // Assert result$.subscribe((data: ApiBatchResult) => { expect(RouterData.commands).toEqual([]) // No Errors }) - const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + `/api/metadata/jobcodes/${apiTaskResultForDeleteJobCode.uuid}/remove`) + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + `/api/metadata/jobcodes/${jobCodeId}/remove`) expect(req.request.method).toEqual("DELETE") expect(req.request.headers.get("Accept")).toEqual("application/json") })) From 7545849ae2ee2ad2e95fb06a0bdbace3c5408a11 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 2 Jun 2023 08:45:49 -0600 Subject: [PATCH 18/45] Update open api --- docs/int/openapi.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/int/openapi.yaml b/docs/int/openapi.yaml index 7ae132fe3..d7db1468c 100644 --- a/docs/int/openapi.yaml +++ b/docs/int/openapi.yaml @@ -518,13 +518,14 @@ paths: /api/metadata/jobcodes: get: summary: Get all Job Codes - description: Returns all Job Codes + description: Returns all Job Codes with pagination, also supports search by query. tags: - Metadata parameters: - $ref: '#/components/parameters/PaginationSizeQueryRequired' - $ref: '#/components/parameters/PaginationFromQueryRequired' - $ref: '#/components/parameters/SortOrderQuery' + - $ref: '#/components/parameters/Query' responses: '200': description: OK @@ -2039,6 +2040,14 @@ components: items: $ref: '#/components/schemas/PublishStatus' + Query: + name: query + in: query + description: Matching query to search + required: false + schema: + type: string + SortOrderQuery: name: sort in: query From b6d53ba001e03e9805bb538fd26ab07bb7b4a2c6 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 2 Jun 2023 11:24:58 -0600 Subject: [PATCH 19/45] Update table job codes - Sort by code - Remove framework name column --- .../edu/wgu/osmt/api/model/JobCodeSortEnum.kt | 8 ++++- .../job-code-list-row.component.html | 6 ++-- .../job-code-table.component.html | 20 +++++++++---- .../job-code-table.component.ts | 30 +++++++++++++++++++ ui/src/app/richskill/ApiSkill.ts | 5 ++-- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt index 7f4cf5c51..9875d47b1 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt @@ -10,6 +10,12 @@ enum class JobCodeSortEnum(override val apiValue: String): SortOrder { }, NameDesc("name.desc") { override val sort = Sort.by("name.sort_insensitive").descending() + }, + CodeAsc("code.asc") { + override val sort = Sort.by("code.keyword").ascending() + }, + CodeDesc("code.desc") { + override val sort = Sort.by("code.keyword").descending() }; companion object : SortOrderCompanion { @@ -23,4 +29,4 @@ enum class JobCodeSortEnum(override val apiValue: String): SortOrder { } } } -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html index 2a5064f52..0f1428877 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html @@ -4,13 +4,13 @@ {{jobCode.targetNodeName}} - {{jobCode.level}} + {{jobCode.code}} - {{jobCode.parents | json}} + {{jobCode.level}} - {{jobCode.frameworkName}} + {{jobCode.parents | json}} diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html index e7e622ca2..d1e8603d8 100644 --- a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html @@ -25,21 +25,29 @@ - - - Job Code Level - + + + + + Code + + - Parents + Job Code Level - Framework Name + Parents diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts index 7fdece162..74501c205 100644 --- a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, Component, Input, QueryList, ViewChildren } from "@angul import { AbstractTableComponent } from "../../../table/abstract-table.component" import { JobCodeListRowComponent } from "../job-code-list-row/job-code-list-row.component" import { IJobCode } from "../Jobcode" +import { ApiSortOrder } from "../../../richskill/ApiSkill" @Component({ selector: "app-job-code-table", @@ -18,5 +19,34 @@ export class JobCodeTableComponent extends AbstractTableComponent impl this.rowReferences.first.focusFirstColumnInRow() } } + + sortColumn(column: string, ascending: boolean): void { + if (column.toLowerCase() === "name") { + if (ascending) { + this.currentSort = ApiSortOrder.NameAsc + } else { + this.currentSort = ApiSortOrder.NameDesc + } + } else if (column.toLowerCase() === "code") { + if (ascending) { + this.currentSort = ApiSortOrder.CodeAsc + } else { + this.currentSort = ApiSortOrder.CodeDesc + } + } else { + if (ascending) { + this.currentSort = ApiSortOrder.SkillAsc + } else { + this.currentSort = ApiSortOrder.SkillDesc + } + } + this.columnSorted.emit(this.currentSort) + } + + getCodeSort(): boolean | undefined { + return this.currentSort == ApiSortOrder.CodeAsc; + + } + } diff --git a/ui/src/app/richskill/ApiSkill.ts b/ui/src/app/richskill/ApiSkill.ts index 7a985f9aa..6de30d949 100644 --- a/ui/src/app/richskill/ApiSkill.ts +++ b/ui/src/app/richskill/ApiSkill.ts @@ -203,10 +203,11 @@ export enum ApiSortOrder { SkillAsc = "skill.asc", SkillDesc = "skill.desc", NameAsc = "name.asc", - NameDesc = "name.desc" + NameDesc = "name.desc", + CodeAsc = "code.asc", + CodeDesc = "code.desc" } - export enum AuditOperationType { Insert = "Insert", Update = "Update", From 0513d5ae0f2d19a04c9fc0e951f576e1e96eeaa1 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 2 Jun 2023 11:58:41 -0600 Subject: [PATCH 20/45] Show parents - Add pipe to show parents --- ui/src/app/app.module.ts | 4 +- .../job-code-list-row.component.html | 2 +- ui/src/app/pipes/index.ts | 1 + .../app/pipes/job-code-parents.pipe.spec.ts | 38 +++++++++++++++++++ ui/src/app/pipes/job-code-parents.pipe.ts | 20 ++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 ui/src/app/pipes/job-code-parents.pipe.spec.ts create mode 100644 ui/src/app/pipes/job-code-parents.pipe.ts diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index f28be2974..07a6a0a4d 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -117,7 +117,8 @@ import { JobCodeListRowComponent } from "./metadata/job-codes/job-code-list-row/ import { JobCodeTableComponent } from "./metadata/job-codes/job-code-table/job-code-table.component" import { NamedReferenceListRowComponent } from "./metadata/named-references/named-reference-list-row/named-reference-list-row.component" import { NamedReferenceTableComponent } from "./metadata/named-references/named-reference-table/named-reference-table.component" -import { MetadataSelectorComponent } from "./metadata/detail/metadata-selector/metadata-selector.component" +import { MetadataSelectorComponent } from "./metadata/detail/metadata-selector/metadata-selector.component"; +import { JobCodeParentsPipe } from "./pipes" export function initializeApp( appConfig: AppConfig, @@ -243,6 +244,7 @@ export function initializeApp( NamedReferenceListRowComponent, NamedReferenceTableComponent, MetadataSelectorComponent, + JobCodeParentsPipe, ], imports: [ NgIdleKeepaliveModule.forRoot(), diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html index 0f1428877..adcb139a8 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html @@ -10,7 +10,7 @@ {{jobCode.level}} - {{jobCode.parents | json}} + {{jobCode.parents | jobCodeParents}} diff --git a/ui/src/app/pipes/index.ts b/ui/src/app/pipes/index.ts index 795d12242..6fb5acf18 100644 --- a/ui/src/app/pipes/index.ts +++ b/ui/src/app/pipes/index.ts @@ -1 +1,2 @@ export * from "./collection.pipe" +export * from "./job-code-parents.pipe" diff --git a/ui/src/app/pipes/job-code-parents.pipe.spec.ts b/ui/src/app/pipes/job-code-parents.pipe.spec.ts new file mode 100644 index 000000000..f11acfb97 --- /dev/null +++ b/ui/src/app/pipes/job-code-parents.pipe.spec.ts @@ -0,0 +1,38 @@ +import { JobCodeParentsPipe } from './job-code-parents.pipe'; +import { ApiJobCodeUpdate, IJobCode } from "../metadata/job-codes/Jobcode" + +describe("JobCodeParentsPipe", () => { + + it("create an instance", () => { + const pipe = new JobCodeParentsPipe(); + expect(pipe).toBeTruthy(); + }); + + it("should transform correctly", () => { + const pipe = new JobCodeParentsPipe(); + const parents: IJobCode[] = [ + { + id: 111, + code: "13-2010", + targetNodeName: "Accountants and Auditors", + frameworkName: "bls", + level: "Broad", + }, + { + id: 110, + code: "13-2000", + targetNodeName: "Financial Specialists", + frameworkName: "bls", + level: "Minor" + }, + { + id: 74, + code: "13-0000", + targetNodeName: "Business and Financial Operations Occupations", + frameworkName: "bls", + level: "Major" + } + ] + expect(pipe.transform(parents)).toEqual("13-0000 Business and Financial Operations Occupations,13-2000 Financial Specialists,13-2010 Accountants and Auditors") + }) +}); diff --git a/ui/src/app/pipes/job-code-parents.pipe.ts b/ui/src/app/pipes/job-code-parents.pipe.ts new file mode 100644 index 000000000..e93043c0f --- /dev/null +++ b/ui/src/app/pipes/job-code-parents.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IJobCode } from "../metadata/job-codes/Jobcode" + +@Pipe({ + name: 'jobCodeParents' +}) +export class JobCodeParentsPipe implements PipeTransform { + + transform(value: IJobCode[]): string { + return value?.sort((a, b) => { + if (a.code < b.code) { + return -1 + } else if (a.code > b.code) { + return 1 + } + return 0 + }).map(jobCode => jobCode.code + " " + jobCode.targetNodeName).join(","); + } + +} From da24ba7d8877cff64a26dd7f9d256c524dd8b970 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:20:04 -0600 Subject: [PATCH 21/45] Sort by job code level --- .../edu/wgu/osmt/api/model/JobCodeSortEnum.kt | 6 ++++++ .../kotlin/edu/wgu/osmt/jobcode/JobCode.kt | 20 ++++++++++++++++++- .../job-code-table.component.html | 16 +++++++++++---- .../job-code-table.component.ts | 9 +++++++-- ui/src/app/richskill/ApiSkill.ts | 4 +++- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt index 9875d47b1..cb6d2a03f 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt @@ -16,6 +16,12 @@ enum class JobCodeSortEnum(override val apiValue: String): SortOrder { }, CodeDesc("code.desc") { override val sort = Sort.by("code.keyword").descending() + }, + JobCodeAsc("jobCodeLevel.asc") { + override val sort = Sort.by("jobCodeLevelAsNumber").ascending() + }, + JobCodeDesc("jobCodeLevel.desc") { + override val sort = Sort.by("jobCodeLevelAsNumber").descending() }; companion object : SortOrderCompanion { diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt index 5e69ba543..ffe3041b4 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt @@ -3,6 +3,7 @@ package edu.wgu.osmt.jobcode import com.fasterxml.jackson.annotation.JsonIgnore import edu.wgu.osmt.config.INDEX_JOBCODE_DOC import edu.wgu.osmt.db.DatabaseData +import edu.wgu.osmt.db.JobCodeLevel import org.elasticsearch.core.Nullable import org.springframework.data.elasticsearch.annotations.* import java.time.LocalDateTime @@ -122,7 +123,24 @@ data class JobCode( @Field @Nullable - val jobRoleCode: String? = JobCodeBreakout.jobRoleCode(code) + val jobRoleCode: String? = JobCodeBreakout.jobRoleCode(code), + + @Field(excludeFromSource = true) + @Nullable + val jobCodeLevelAsNumber: Int = when (code) { + majorCode -> { + 1 + } + minorCode -> { + 2 + } + broadCode -> { + 3 + } + else -> { + 4 + } + } ) : DatabaseData { companion object { diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html index d1e8603d8..196258ea8 100644 --- a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html @@ -39,10 +39,18 @@ - - - Job Code Level - + + + + + Job Code Level + + diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts index 74501c205..16c9b00c4 100644 --- a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts @@ -35,9 +35,9 @@ export class JobCodeTableComponent extends AbstractTableComponent impl } } else { if (ascending) { - this.currentSort = ApiSortOrder.SkillAsc + this.currentSort = ApiSortOrder.JobCodeLevelAsc } else { - this.currentSort = ApiSortOrder.SkillDesc + this.currentSort = ApiSortOrder.JobCodeLevelDesc } } this.columnSorted.emit(this.currentSort) @@ -48,5 +48,10 @@ export class JobCodeTableComponent extends AbstractTableComponent impl } + getJobCodeLevelSort(): boolean | undefined { + return this.currentSort == ApiSortOrder.JobCodeLevelAsc; + + } + } diff --git a/ui/src/app/richskill/ApiSkill.ts b/ui/src/app/richskill/ApiSkill.ts index 6de30d949..8f1445abb 100644 --- a/ui/src/app/richskill/ApiSkill.ts +++ b/ui/src/app/richskill/ApiSkill.ts @@ -205,7 +205,9 @@ export enum ApiSortOrder { NameAsc = "name.asc", NameDesc = "name.desc", CodeAsc = "code.asc", - CodeDesc = "code.desc" + CodeDesc = "code.desc", + JobCodeLevelAsc = "jobCodeLevel.asc", + JobCodeLevelDesc = "jobCodeLevel.desc", } export enum AuditOperationType { From e82d9254b6fda73183aa7e1b4b98797dd7b4d8b1 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:32:43 -0600 Subject: [PATCH 22/45] Add validation to delete a job code - You cannot delete a jobcode with RSDs - You cannot delete a job code with children --- .../edu/wgu/osmt/jobcode/JobCodeRepository.kt | 18 ++++++++++++++- .../richskill/RichSkillJobCodeRepository.kt | 23 +++++++++++++++++++ .../metadata-list/metadata-list.component.ts | 4 +++- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepository.kt diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt index 682de0b64..2da41e0ec 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -2,6 +2,7 @@ package edu.wgu.osmt.jobcode import edu.wgu.osmt.api.model.ApiBatchResult import edu.wgu.osmt.api.model.JobCodeUpdate +import edu.wgu.osmt.richskill.RichSkillJobCodeRepository import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and @@ -21,6 +22,7 @@ interface JobCodeRepository { fun findById(id: Long): JobCodeDao? fun findByCode(code: String): JobCodeDao? fun findByCodeOrCreate(code: String, framework: String? = null): JobCodeDao + fun hasChildren(jobCodeDao: JobCodeDao): Boolean fun findBlsCode(code: String): JobCodeDao? fun create(code: String, framework: String? = null): JobCodeDao fun createFromApi(jobCodes: List): List @@ -41,6 +43,10 @@ class JobCodeRepositoryImpl: JobCodeRepository { @Lazy lateinit var jobCodeEsRepo: JobCodeEsRepo + @Autowired + @Lazy + lateinit var richSkillJobCodeRepository: RichSkillJobCodeRepository + val dao = JobCodeDao.Companion override val table = JobCodeTable @@ -76,6 +82,16 @@ class JobCodeRepositoryImpl: JobCodeRepository { return existing ?: create(code, framework) } + override fun hasChildren(jobCodeDao: JobCodeDao): Boolean { + return findAll().any { jobCode -> + jobCode.code != jobCodeDao.code && + (jobCodeDao.code == JobCodeBreakout.majorCode(jobCode.code) || + jobCodeDao.code == JobCodeBreakout.minorCode(jobCode.code) || + jobCodeDao.code == JobCodeBreakout.broadCode(jobCode.code) || + jobCodeDao.code == JobCodeBreakout.detailedCode(jobCode.code)) + } + } + override fun create(code: String, framework: String?): JobCodeDao { val maybeDetailed = JobCodeBreakout.detailedCode(code).let{ findBlsCode(code) } return dao.new { @@ -100,7 +116,7 @@ class JobCodeRepositoryImpl: JobCodeRepository { override fun remove(jobCodeId: Long): ApiBatchResult { val jobCodeFound = findById(jobCodeId) val jobCodeEsFound = jobCodeEsRepo.findById(jobCodeId.toInt()) - if (jobCodeFound != null && jobCodeEsFound.isPresent) { + if (jobCodeFound != null && jobCodeEsFound.isPresent && !hasChildren(jobCodeFound) && richSkillJobCodeRepository.hasRSDs(jobCodeFound)) { transaction { table.deleteWhere{ table.id eq jobCodeFound.id } jobCodeEsRepo.delete(jobCodeEsFound.get()) diff --git a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepository.kt new file mode 100644 index 000000000..706a0f201 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepository.kt @@ -0,0 +1,23 @@ +package edu.wgu.osmt.richskill + +import edu.wgu.osmt.jobcode.JobCodeDao +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +interface RichSkillJobCodeRepository { + val table: Table + + fun hasRSDs(jobCodeDao: JobCodeDao): Boolean +} + +@Repository +@Transactional +class RichSkillJobCodeRepositoryImpl: RichSkillJobCodeRepository { + override val table = RichSkillJobCodes + + override fun hasRSDs(jobCodeDao: JobCodeDao): Boolean { + return !table.select { table.jobCodeId eq jobCodeDao.id }.empty() + } +} diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 4ecfd6056..f951a94f5 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -193,9 +193,11 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { if (this.isJobCodeDataSelected) { if (confirm("Confirm that you want to delete the job code with name " + (jobCode as ApiJobCode)?.targetNodeName)) { this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe(data => { - if (data?.success) { + if (data && data.success) { this.toastService.showToast("Success", "You deleted a job code with name " + (jobCode as ApiJobCode)?.targetNodeName) this.loadNextPage() + } else if (data && !data.success) { + this.toastService.showToast("Warning", "You cannot delete this job code") } }) } From 8b3471b5bfa6e3a811f800be59d833a020ca01ea Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:34:53 -0600 Subject: [PATCH 23/45] Fix create from api --- api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt index 2da41e0ec..9b146f64a 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -71,7 +71,6 @@ class JobCodeRepositoryImpl: JobCodeRepository { this.framework = jobCodeUpdate.framework this.name = jobCodeUpdate.targetNodeName this.creationDate = LocalDateTime.now(ZoneOffset.UTC) - this.name = "my name" this.major = "my major" }.also { jobCodeEsRepo.save(it.toModel()) } } @@ -116,7 +115,7 @@ class JobCodeRepositoryImpl: JobCodeRepository { override fun remove(jobCodeId: Long): ApiBatchResult { val jobCodeFound = findById(jobCodeId) val jobCodeEsFound = jobCodeEsRepo.findById(jobCodeId.toInt()) - if (jobCodeFound != null && jobCodeEsFound.isPresent && !hasChildren(jobCodeFound) && richSkillJobCodeRepository.hasRSDs(jobCodeFound)) { + if (jobCodeFound != null && jobCodeEsFound.isPresent && !hasChildren(jobCodeFound) && !richSkillJobCodeRepository.hasRSDs(jobCodeFound)) { transaction { table.deleteWhere{ table.id eq jobCodeFound.id } jobCodeEsRepo.delete(jobCodeEsFound.get()) From 7dd29ec1afde487e258358529479b6e5458c9c08 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Mon, 5 Jun 2023 15:51:39 -0600 Subject: [PATCH 24/45] Add error messages on delete job code --- .../edu/wgu/osmt/jobcode/JobCodeRepository.kt | 46 +++++++++++++++---- .../metadata-list/metadata-list.component.ts | 4 +- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt index 9b146f64a..13e5d95d6 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -2,6 +2,7 @@ package edu.wgu.osmt.jobcode import edu.wgu.osmt.api.model.ApiBatchResult import edu.wgu.osmt.api.model.JobCodeUpdate +import edu.wgu.osmt.db.JobCodeLevel import edu.wgu.osmt.richskill.RichSkillJobCodeRepository import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.Table @@ -115,21 +116,46 @@ class JobCodeRepositoryImpl: JobCodeRepository { override fun remove(jobCodeId: Long): ApiBatchResult { val jobCodeFound = findById(jobCodeId) val jobCodeEsFound = jobCodeEsRepo.findById(jobCodeId.toInt()) - if (jobCodeFound != null && jobCodeEsFound.isPresent && !hasChildren(jobCodeFound) && !richSkillJobCodeRepository.hasRSDs(jobCodeFound)) { - transaction { - table.deleteWhere{ table.id eq jobCodeFound.id } - jobCodeEsRepo.delete(jobCodeEsFound.get()) + var hasChildren = false + var hasRSDs = false + if (jobCodeFound != null && jobCodeEsFound.isPresent) { + hasChildren = hasChildren(jobCodeFound) + hasRSDs = richSkillJobCodeRepository.hasRSDs(jobCodeFound) + if (!hasChildren && !hasRSDs) { + transaction { + table.deleteWhere{ table.id eq jobCodeFound.id } + jobCodeEsRepo.delete(jobCodeEsFound.get()) + } + return ApiBatchResult( + success = true, + modifiedCount = 1, + totalCount = 1 + ) } - return ApiBatchResult( - success = true, - modifiedCount = 1, - totalCount = 1 - ) } return ApiBatchResult( success = false, modifiedCount = 0, - totalCount = 0 + totalCount = 0, + message = JobCodeErrorMessages.forDeleteError(hasChildren, hasRSDs) ) } } + +enum class JobCodeErrorMessages(val apiValue: String) { + JobCodeNotExist("You cannot delete this job code because you doesn't exist"), + JobCodeHasChildren("You cannot delete this job code because has children"), + JobCodeHasRSD("You cannot delete this job code because is used in one or more RSDs"); + + companion object { + fun forDeleteError(hasChildren: Boolean, hasRSDs: Boolean): String { + return if (hasChildren) { + JobCodeHasChildren.apiValue + } else if (hasRSDs) { + JobCodeHasRSD.apiValue + } else { + JobCodeNotExist.apiValue + } + } + } +} diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index f951a94f5..469425600 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -194,10 +194,10 @@ export class MetadataListComponent extends Whitelabelled implements OnInit { if (confirm("Confirm that you want to delete the job code with name " + (jobCode as ApiJobCode)?.targetNodeName)) { this.jobCodeService.deleteJobCodeWithResult((jobCode as ApiJobCode)?.id ?? 0).subscribe(data => { if (data && data.success) { - this.toastService.showToast("Success", "You deleted a job code with name " + (jobCode as ApiJobCode)?.targetNodeName) + this.toastService.showToast("Success", "You deleted a job code " + (jobCode as ApiJobCode)?.targetNodeName) this.loadNextPage() } else if (data && !data.success) { - this.toastService.showToast("Warning", "You cannot delete this job code") + this.toastService.showToast("Warning", data.message ?? "You cannot delete this job code") } }) } From e489ddb3e84df442fb7c3f63b5917b7be0355dd0 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:22:29 -0600 Subject: [PATCH 25/45] Add test new functions back end --- .../collection/CollectionRepositoryTest.kt | 6 +-- .../osmt/jobcode/JobCodeErrorMessagesTest.kt | 29 ++++++++++++++ .../wgu/osmt/jobcode/JobCodeRepositoryTest.kt | 33 +++++++++++++++ .../wgu/osmt/keyword/KeywordControllerTest.kt | 12 +----- .../RichSkillJobCodeRepositoryTest.kt | 40 +++++++++++++++++++ 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeErrorMessagesTest.kt create mode 100644 api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepositoryTest.kt diff --git a/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt b/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt index 9f9207b15..8a2c995a6 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt @@ -69,8 +69,8 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes private fun random_skill(): RichSkillDescriptorDao { return richSkillRepository.create(RsdUpdateObject( - name=UUID.randomUUID().toString(), - statement=UUID.randomUUID().toString() + name =UUID.randomUUID().toString(), + statement =UUID.randomUUID().toString() ), userString)!! } @@ -247,4 +247,4 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes assertThat(batchResult?.success).isEqualTo(false) } -} \ No newline at end of file +} diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeErrorMessagesTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeErrorMessagesTest.kt new file mode 100644 index 000000000..6ea278932 --- /dev/null +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeErrorMessagesTest.kt @@ -0,0 +1,29 @@ +package edu.wgu.osmt.jobcode + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +class JobCodeErrorMessagesTest { + + @Test + fun `Error should be doesn't exist`() { + val hasChildren = false + val hasRSDs = false + Assertions.assertThat(JobCodeErrorMessages.forDeleteError(hasChildren, hasRSDs)).isEqualTo(JobCodeErrorMessages.JobCodeNotExist.apiValue) + } + + @Test + fun `Error should be job code has children`() { + val hasChildren = true + val hasRSDs = false + Assertions.assertThat(JobCodeErrorMessages.forDeleteError(hasChildren, hasRSDs)).isEqualTo(JobCodeErrorMessages.JobCodeHasChildren.apiValue) + } + + @Test + fun `Error should be job code has RSDs`() { + val hasChildren = false + val hasRSDs = true + Assertions.assertThat(JobCodeErrorMessages.forDeleteError(hasChildren, hasRSDs)).isEqualTo(JobCodeErrorMessages.JobCodeHasRSD.apiValue) + } + +} diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeRepositoryTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeRepositoryTest.kt index c2c3ba327..3ce1cfb5b 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeRepositoryTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeRepositoryTest.kt @@ -40,4 +40,37 @@ class JobCodeRepositoryTest @Autowired constructor( assertThat(result.map{it.code}).containsAll(listOf(jobCode1, jobCode2, jobCode3)) assertThat(result.count()).isEqualTo(3) } + + @Test + fun `JobCode has children`() { + val majorJobCode = jobCodeRepository.create("95-0000") + jobCodeRepository.create("95-1000") + jobCodeRepository.create("95-1100") + val majorJobCodeHasChildren = jobCodeRepository.hasChildren(majorJobCode) + assertThat(majorJobCodeHasChildren).isEqualTo(true) + } + + @Test + fun `Job code doesn't have children`() { + val majorJobCode = jobCodeRepository.create("96-0000") + val majorJobCodeHasChildren = jobCodeRepository.hasChildren(majorJobCode) + assertThat(majorJobCodeHasChildren).isEqualTo(false) + } + + @Test + fun `Job code cannot be deleted`() { + val majorJobCode = jobCodeRepository.create("97-0000") + jobCodeRepository.create("97-1000") + val apiBatchResult = jobCodeRepository.remove(majorJobCode.id.value) + assertThat(apiBatchResult.success).isEqualTo(false) + assertThat(apiBatchResult.totalCount).isEqualTo(0) + } + + @Test + fun `Job code can be deleted`() { + val majorJobCode = jobCodeRepository.create("98-0000") + val apiBatchResult = jobCodeRepository.remove(majorJobCode.id.value) + assertThat(apiBatchResult.success).isEqualTo(true) + assertThat(apiBatchResult.totalCount).isEqualTo(1) + } } diff --git a/api/src/test/kotlin/edu/wgu/osmt/keyword/KeywordControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/keyword/KeywordControllerTest.kt index ca87f991a..dc54bed23 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/keyword/KeywordControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/keyword/KeywordControllerTest.kt @@ -6,28 +6,18 @@ import edu.wgu.osmt.HasElasticsearchReset import edu.wgu.osmt.SpringTest import edu.wgu.osmt.api.model.KeywordSortEnum import edu.wgu.osmt.api.model.SkillSortEnum -import edu.wgu.osmt.api.model.SortOrder import edu.wgu.osmt.collection.CollectionEsRepo import edu.wgu.osmt.db.ListFieldUpdate import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.jobcode.JobCodeEsRepo import edu.wgu.osmt.mockdata.MockData -import edu.wgu.osmt.richskill.RichSkillDescriptor import edu.wgu.osmt.richskill.RichSkillEsRepo import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.richskill.RsdUpdateObject -import io.mockk.InternalPlatformDsl.toArray -import io.mockk.every -import io.mockk.mockk import org.assertj.core.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.context.SecurityContext -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.oauth2.core.user.OAuth2UserAuthority -import org.springframework.security.oauth2.jwt.Jwt import org.springframework.test.util.ReflectionTestUtils import org.springframework.transaction.annotation.Transactional import org.springframework.web.util.UriComponentsBuilder @@ -166,4 +156,4 @@ internal class KeywordControllerTest @Autowired constructor( Assertions.assertThat(result).isNotNull Assertions.assertThat(result.body?.size == 1) } -} \ No newline at end of file +} diff --git a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepositoryTest.kt b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepositoryTest.kt new file mode 100644 index 000000000..e26acc6ba --- /dev/null +++ b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillJobCodeRepositoryTest.kt @@ -0,0 +1,40 @@ +package edu.wgu.osmt.richskill + +import edu.wgu.osmt.BaseDockerizedTest +import edu.wgu.osmt.HasDatabaseReset +import edu.wgu.osmt.SpringTest +import edu.wgu.osmt.db.ListFieldUpdate +import edu.wgu.osmt.jobcode.JobCodeRepository +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional + +@Transactional +class RichSkillJobCodeRepositoryTest @Autowired constructor( + val richSkillRepository: RichSkillRepository, + val richSkillJobCodeRepository: RichSkillJobCodeRepository, + val jobCodeRepository: JobCodeRepository +) : SpringTest(), BaseDockerizedTest, HasDatabaseReset { + + + val userString = "unittestuser" + + @Test + fun `The job code should have RSDs`() { + val jobCodeDao = jobCodeRepository.create("95-0000") + richSkillRepository.create( + RsdUpdateObject(statement = "This is an statement", name = "Test 20230506", jobCodes = ListFieldUpdate(add = listOf(jobCodeDao))), + userString + ) + assertThat(richSkillJobCodeRepository.hasRSDs(jobCodeDao)).isEqualTo(true) + } + + @Test + fun `The job code should not have RSDs`() { + val jobCodeDao = jobCodeRepository.create("96-0000") + assertThat(richSkillJobCodeRepository.hasRSDs(jobCodeDao)).isEqualTo(false) + } + +} From 392b34ef549ec59e83972131d9e7ca9ce5aec8ac Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:14:24 -0600 Subject: [PATCH 26/45] Update texts --- .../metadata-list/metadata-list.component.ts | 2 +- .../job-code-list-row.component.html | 6 +++++- .../job-code-list-row.component.ts | 11 ++++++++++ .../job-code-table.component.html | 10 +++++----- .../job-code-table.component.ts | 20 +++---------------- ui/src/app/pipes/job-code-parents.pipe.ts | 11 ++-------- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts index 469425600..eb8e95305 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, ViewChild} from "@angular/core" +import { Component, OnInit, ViewChild } from "@angular/core" import { FormControl, FormGroup } from "@angular/forms" import { Observable, Subject } from "rxjs" import { PaginatedMetadata } from "../../PaginatedMetadata" diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html index adcb139a8..b40ec904f 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html @@ -10,7 +10,11 @@ {{jobCode.level}} - {{jobCode.parents | jobCodeParents}} +
    + +
diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts index 8005d9ccd..14fba521f 100644 --- a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts @@ -38,4 +38,15 @@ export class JobCodeListRowComponent { return false } } + + sortedParents(): IJobCode[] { + return this.jobCode?.parents?.sort((a, b) => { + if (a.code < b.code) { + return -1 + } else if (a.code > b.code) { + return 1 + } + return 0 + }) ?? [] + } } diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html index 196258ea8..95421b3a9 100644 --- a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html @@ -4,7 +4,7 @@