diff --git a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt index ad11b2ba7..0c21b50a0 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt @@ -37,6 +37,7 @@ object RoutePaths { const val SKILL_AUDIT_LOG = "$SKILL_DETAIL/log" const val METADATA_PATH = "/metadata" + const val KEYWORD_PATH = "${METADATA_PATH}/keywords" const val KEYWORD_LIST = KEYWORD_PATH const val KEYWORD_CREATE = KEYWORD_PATH @@ -45,6 +46,14 @@ object RoutePaths { const val KEYWORD_REMOVE = "$KEYWORD_DETAIL/remove" const val KEYWORD_SKILLS = "${KEYWORD_DETAIL}/skills" + const val JOB_CODE_PATH = "$METADATA_PATH/jobcodes" + const val JOB_CODE_CREATE = JOB_CODE_PATH + const val JOB_CODE_LIST = JOB_CODE_PATH + const val JOB_CODE_DETAIL = "$JOB_CODE_PATH/{id}" + const val JOB_CODE_UPDATE = "$JOB_CODE_DETAIL/update" + const val JOB_CODE_REMOVE = "$JOB_CODE_DETAIL/remove" + const val JOB_CODE_SKILLS = "${JOB_CODE_DETAIL}/skills" + //collections private const val COLLECTIONS_PATH = "/collections" const val COLLECTIONS_LIST = COLLECTIONS_PATH @@ -74,13 +83,6 @@ object RoutePaths { const val ES_ADMIN_DELETE_INDICES = "$ES_ADMIN/delete-indices" const val ES_ADMIN_REINDEX = "$ES_ADMIN/reindex" - const val JOB_CODE_PATH = "$METADATA_PATH/jobcodes" - const val JOB_CODE_CREATE = JOB_CODE_PATH - const val JOB_CODE_LIST = JOB_CODE_PATH - const val JOB_CODE_DETAIL = "$JOB_CODE_PATH/{id}" - const val JOB_CODE_UPDATE = "$JOB_CODE_DETAIL/update" - const val JOB_CODE_REMOVE = "$JOB_CODE_DETAIL/remove" - object QueryParams { const val FROM = "from" const val SIZE = "size" 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 6bd4fb621..c75178ab4 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -1,11 +1,16 @@ 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.* import edu.wgu.osmt.db.JobCodeLevel +import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.elasticsearch.OffsetPageable +import edu.wgu.osmt.elasticsearch.PaginatedLinks +import edu.wgu.osmt.keyword.KeywordTypeEnum +import edu.wgu.osmt.richskill.RichSkillDoc +import edu.wgu.osmt.richskill.RichSkillEsRepo +import edu.wgu.osmt.security.OAuthHelper import edu.wgu.osmt.task.RemoveJobCodeTask import edu.wgu.osmt.task.Task import edu.wgu.osmt.task.TaskMessageService @@ -17,22 +22,22 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.jwt.Jwt import org.springframework.stereotype.Controller import org.springframework.transaction.annotation.Transactional -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.* import org.springframework.web.server.ResponseStatusException +import org.springframework.web.util.UriComponentsBuilder @Controller @Transactional class JobCodeController @Autowired constructor( val jobCodeEsRepo: JobCodeEsRepo, val jobCodeRepository: JobCodeRepository, + val richSkillEsRepo: RichSkillEsRepo, val taskMessageService: TaskMessageService, + val oAuthHelper: OAuthHelper, ) { @GetMapping( @@ -132,4 +137,90 @@ class JobCodeController @Autowired constructor( return Task.processingResponse(task) } + @PostMapping( + path = ["${RoutePaths.API}${RoutePaths.API_V3}${RoutePaths.JOB_CODE_SKILLS}"], + produces = [MediaType.APPLICATION_JSON_VALUE] + ) + @ResponseBody + @PreAuthorize("isAuthenticated()") + fun searchJobCodeSkills ( + uriComponentsBuilder: UriComponentsBuilder, + @PathVariable id: Long, + @RequestParam(required = false, defaultValue = PaginationDefaults.size.toString()) size: Int, + @RequestParam(required = false, defaultValue = "0") from: Int, + @RequestParam( + required = false, + defaultValue = PublishStatus.DEFAULT_API_PUBLISH_STATUS_SET + ) status: Array, + @RequestParam(required = false) sort: String? = null, + @RequestBody(required = false) apiSearch: ApiSearch? = null, + @AuthenticationPrincipal user: Jwt? = null + ): HttpEntity> { + val sortEnum = sort?.let{ SkillSortEnum.forApiValue(it)} + + val jobCode = jobCodeRepository.findById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + return searchRelatedSkills( + uriComponentsBuilder = uriComponentsBuilder, + jobCode = jobCode, + size = size, + from = from, + statusFilters = status, + sort = sortEnum ?: SkillSortEnum.defaultSort, + apiSearch = apiSearch ?: ApiSearch(), + user = user + ) + } + + private fun searchRelatedSkills ( + uriComponentsBuilder: UriComponentsBuilder, + jobCode: JobCodeDao, + size: Int, + from: Int, + statusFilters: Array, + sort: SkillSortEnum, + apiSearch: ApiSearch, + user: Jwt? + ): HttpEntity> { + + val pageable = OffsetPageable(offset = from, limit = size, sort = sort.sort) + val statuses = statusFilters.mapNotNull { PublishStatus.forApiValue(it) }.toMutableSet() + + if (user == null) { + statuses.remove(PublishStatus.Deleted) + statuses.remove(PublishStatus.Draft) + } + + val search = ApiSearch ( + query = apiSearch.query, + advanced = apiSearch.advanced, + uuids = apiSearch.uuids, + filtered = ApiFilteredSearch( + jobCodes = apiSearch.filtered?.jobCodes, + ) + ) + + val countByApiSearch = richSkillEsRepo.countByApiSearch(search, statuses, pageable) + val searchHits = richSkillEsRepo.byApiSearch(search, statuses, pageable) + + val responseHeaders = HttpHeaders() + responseHeaders.add("X-Total-Count", countByApiSearch.toString()) + + uriComponentsBuilder + .path(RoutePaths.SEARCH_SKILLS) + .queryParam(RoutePaths.QueryParams.FROM, from) + .queryParam(RoutePaths.QueryParams.SIZE, size) + .queryParam(RoutePaths.QueryParams.SORT, sort) + .queryParam(RoutePaths.QueryParams.STATUS, statusFilters.joinToString(",").lowercase()) + + PaginatedLinks( + pageable, + searchHits.totalHits.toInt(), + uriComponentsBuilder + ).addToHeaders(responseHeaders) + + return ResponseEntity.status(200).headers(responseHeaders) + .body(searchHits.map { it.content }.toList()) + } + } 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 b5225c9f8..d70272237 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -3,8 +3,9 @@ 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 edu.wgu.osmt.richskill.* import org.jetbrains.exposed.sql.SizedIterable +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere @@ -19,6 +20,9 @@ import java.time.ZoneOffset interface JobCodeRepository { val table: Table + val richSkillJobCodes: RichSkillJobCodes + val richSkillRepository: RichSkillRepository + fun findAll(): SizedIterable fun findById(id: Long): JobCodeDao? fun findByCode(code: String): JobCodeDao? @@ -27,6 +31,7 @@ interface JobCodeRepository { fun findBlsCode(code: String): JobCodeDao? fun create(code: String, framework: String? = null): JobCodeDao fun createFromApi(jobCodes: List): List + fun updateFromApi(existingJobCodeId: Long, apiJobCodeUpdate: JobCodeUpdate, username: String): JobCodeDao? fun onetsByDetailCode(detailedCode: String): SizedIterable fun remove(jobCodeId: Long): ApiBatchResult @@ -48,8 +53,13 @@ class JobCodeRepositoryImpl: JobCodeRepository { @Lazy lateinit var richSkillJobCodeRepository: RichSkillJobCodeRepository + @Autowired + @Lazy + override lateinit var richSkillRepository: RichSkillRepository + val dao = JobCodeDao.Companion override val table = JobCodeTable + override val richSkillJobCodes = RichSkillJobCodes override fun findAll() = dao.all() @@ -77,6 +87,31 @@ class JobCodeRepositoryImpl: JobCodeRepository { } } + override fun updateFromApi(existingJobCodeId: Long, apiJobCodeUpdate: JobCodeUpdate, username: String): JobCodeDao? { + val found = dao.findById(existingJobCodeId) + if (found!=null) { + transaction { + found.code = apiJobCodeUpdate.code + found.name = apiJobCodeUpdate.targetNodeName + found.framework = apiJobCodeUpdate.framework + jobCodeEsRepo.save(found.toModel()) + + // update rich skill after values changes in keyword and reindex + richSkillJobCodes.select { richSkillJobCodes.jobCodeId eq found.id }.forEach { it -> + val richSkillId = it[richSkillJobCodes.richSkillId] + // richSkillRepository.findById(richSkillId.value)?.keywords?.forEach { it2 -> println(it2.value) } + richSkillRepository.update( + RsdUpdateObject( + id = richSkillId.value + ), + username + ) + } + } + } + return found + } + override fun findByCodeOrCreate(code: String, framework: String?): JobCodeDao { val existing = findByCode(code) return existing ?: create(code, framework) diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index 0d3267f80..d68f2bfd7 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -30,6 +30,7 @@ import { MetadataListComponent } from "./metadata/detail/metadata-list/metadata- import { MetadataManageComponent } from "./metadata/detail/metadata-manage/metadata-manage.component" import { MetadataPublicComponent } from "./metadata/detail/metadata-public/metadata-public.component" import { NamedReferenceFormComponent } from "./metadata/named-reference/named-reference-form/named-reference-form.component" +import { JobCodeFormComponent } from "./metadata/job-code/job-code-form/job-code-form.component"; const routes: Routes = [ { path: "", redirectTo: "/skills", pathMatch: "full" }, @@ -103,13 +104,13 @@ const routes: Routes = [ roles: ActionByRoles.get(ButtonAction.MetadataCreate) }, }, - /*{path: "job-codes/create", - component: MetadataFormComponent, + {path: "job-codes/create", + component: JobCodeFormComponent, canActivate: [AuthGuard], data: { roles: ActionByRoles.get(ButtonAction.MetadataCreate) }, - },*/ + }, // edit metadata { path: "named-references/:id/edit", @@ -119,13 +120,13 @@ const routes: Routes = [ roles: ActionByRoles.get(ButtonAction.MetadataUpdate) }, }, - /*{path: "job-codes/:id/edit", - component: MetadataFormComponent, + {path: "job-codes/:id/edit", + component: JobCodeFormComponent, canActivate: [AuthGuard], data: { roles: ActionByRoles.get(ButtonAction.MetadataUpdate) }, - },*/ + }, // public metadata detail { path: "job-codes/:id", diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 8074ed374..e5046d0f2 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -125,6 +125,7 @@ import { MetadataCardComponent } from "./metadata/detail/metadata-card/metadata- import { ManageMetadataActionBarVerticalComponent } from "./metadata/detail/metadata-manage/action-bar-vertical/metadata-manage-action-bar-vertical.component" import { PublicMetadataActionBarVerticalComponent } from "./metadata/detail/metadata-public/action-bar-vertical/metadata-public-action-bar-vertical.component"; import { NamedReferenceFormComponent } from "./metadata/named-reference/named-reference-form/named-reference-form.component" +import { JobCodeFormComponent } from "./metadata/job-code/job-code-form/job-code-form.component"; export function initializeApp( appConfig: AppConfig, @@ -254,7 +255,8 @@ export function initializeApp( InlineHeadingComponent, JobCodeParentsPipe, InlineHeadingComponent, - NamedReferenceFormComponent + NamedReferenceFormComponent, + JobCodeFormComponent ], imports: [ NgIdleKeepaliveModule.forRoot(), diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html index 903abc8f7..eecc4949a 100644 --- a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html @@ -29,7 +29,7 @@