Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4fe4dc1
Enable EnableGlobalMethodSecurity
manuel-delvillar May 4, 2023
502e107
Add crud methods for job code
manuel-delvillar May 5, 2023
3710513
Add paths for named references
manuel-delvillar May 8, 2023
f59585a
Remove commented lines
manuel-delvillar May 9, 2023
83d41df
Add tests for named references and job codes
manuel-delvillar May 11, 2023
acf9b98
Working with pagination
manuel-delvillar May 18, 2023
c2f930a
Add sort insensitive
manuel-delvillar May 18, 2023
10fd7b5
Add job code leven and parents
manuel-delvillar May 19, 2023
8db96ff
Delete job code
manuel-delvillar May 19, 2023
d943a1a
Fix delete test
manuel-delvillar Jun 1, 2023
3db1cdc
Remove PaginatedJobCodes
manuel-delvillar Jun 1, 2023
10a7cbe
Show job codes in list
manuel-delvillar Jun 1, 2023
f307f43
Request all on clear search
manuel-delvillar Jun 1, 2023
52e59de
Delete job code by id
manuel-delvillar Jun 1, 2023
91a57dc
Show toast when job code is deleted
manuel-delvillar Jun 1, 2023
820e650
Add confirm message & reload on delete
manuel-delvillar Jun 1, 2023
3d1a697
Fix broken tests
manuel-delvillar Jun 1, 2023
7545849
Update open api
manuel-delvillar Jun 2, 2023
b6d53ba
Update table job codes
manuel-delvillar Jun 2, 2023
0513d5a
Show parents
manuel-delvillar Jun 2, 2023
da24ba7
Sort by job code level
manuel-delvillar Jun 2, 2023
e82d925
Add validation to delete a job code
manuel-delvillar Jun 5, 2023
8b3471b
Fix create from api
manuel-delvillar Jun 5, 2023
7dd29ec
Add error messages on delete job code
manuel-delvillar Jun 5, 2023
e489ddb
Add test new functions back end
manuel-delvillar Jun 5, 2023
392b34e
Update texts
manuel-delvillar Jun 6, 2023
9070c2c
Update values for MetadataType
manuel-delvillar Jun 6, 2023
7b824c4
Add new tests
manuel-delvillar Jun 6, 2023
c1371f4
Fix broken tests
manuel-delvillar Jun 6, 2023
9009ce6
Remove unused import
manuel-delvillar Jun 7, 2023
1060764
Add id property for job code in open api
manuel-delvillar Jun 7, 2023
dd240a8
Delete multiple job codes
manuel-delvillar Jun 8, 2023
9cd12ea
Improve notifications on delete
manuel-delvillar Jun 8, 2023
da056cb
Add back to top
manuel-delvillar Jun 8, 2023
3f9bc15
Add abstract list
manuel-delvillar Jun 8, 2023
838a539
Fix text on action
manuel-delvillar Jun 8, 2023
854767f
Clean selected data after delete one or more job codes
manuel-delvillar Jun 9, 2023
7a6efe7
Fix table
manuel-delvillar Jun 9, 2023
511d169
Disable delete single if select all is checked
manuel-delvillar Jun 9, 2023
adcab88
Add property to job code in open api
manuel-delvillar Jun 9, 2023
8d9b881
Fix comments on pull request
manuel-delvillar Jun 9, 2023
5264333
Fix broken tests
manuel-delvillar Jun 9, 2023
7346581
Fix errors
manuel-delvillar Jun 9, 2023
e840212
Uncheck select all if items change
manuel-delvillar Jun 9, 2023
71c55ff
Check all selected if all are selected
manuel-delvillar Jun 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions api/src/main/kotlin/edu/wgu/osmt/api/model/ApiNamedReference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import edu.wgu.osmt.db.JobCodeLevel
import edu.wgu.osmt.jobcode.JobCode
import edu.wgu.osmt.keyword.Keyword


@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class ApiNamedReference(
val id: String? = null,
Expand Down Expand Up @@ -75,16 +74,44 @@ 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,
val frameworkName: String? = null,
val level: JobCodeLevel? = null,
val parents: List<ApiJobCode>? = null
val parents: List<ApiJobCode>? = null,
val jobCodeLevelAsNumber: Int? = null
) {
companion object factory {
fun fromJobCode(jobCode: JobCode, level: JobCodeLevel? = null, parents: List<ApiJobCode>? = 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,
jobCodeLevelAsNumber = jobCode.jobCodeLevelAsNumber
)
}

fun getLevelFromJobCode(jobCode: JobCode): JobCodeLevel {
return when (jobCode.code) {
jobCode.majorCode -> {
JobCodeLevel.Major
}
jobCode.minorCode -> {
JobCodeLevel.Minor
}
jobCode.broadCode -> {
JobCodeLevel.Broad
}
else -> {
JobCodeLevel.Detailed
}
}
}
}
}
38 changes: 38 additions & 0 deletions api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeSortEnum.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package edu.wgu.osmt.api.model

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.sort_insensitive").ascending()
},
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()
},
JobCodeAsc("jobCodeLevel.asc") {
override val sort = Sort.by("jobCodeLevelAsNumber").ascending()
},
JobCodeDesc("jobCodeLevel.desc") {
override val sort = Sort.by("jobCodeLevelAsNumber").descending()
};

companion object : SortOrderCompanion<JobCodeSortEnum> {
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")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ data class JobCodeUpdate(
@JsonProperty("parents")
val parents: List<JobCodeUpdate>? = null
) {
}
}
23 changes: 21 additions & 2 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,7 +76,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
Expand Down Expand Up @@ -121,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 {
Expand Down
48 changes: 40 additions & 8 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package edu.wgu.osmt.jobcode;

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.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
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
Expand All @@ -26,18 +31,43 @@ 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])
@PreAuthorize("isAuthenticated()")
fun allPaginated(
@RequestParam(required = true) size: Int,
@RequestParam(required = true) from: Int,
@RequestParam(required = false) sort: String?
@RequestParam(required = false) sort: String?,
@RequestParam(required = false) query: String?
): HttpEntity<List<ApiJobCode>> {
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 {
val jobCodeLevel = ApiJobCode.getLevelFromJobCode(it.content)
val parents = mutableListOf<JobCodeDao>()
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])
Expand Down Expand Up @@ -68,15 +98,17 @@ class JobCodeController @Autowired constructor(
@PathVariable id: Int,
@RequestBody jobCodeUpdate: JobCodeUpdate
): HttpEntity<ApiJobCode> {
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)
@PreAuthorize("hasAuthority(@appConfig.roleAdmin)")
fun deleteJobCode(
@PathVariable id: Int,
): HttpEntity<TaskResult> {
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 Task.processingResponse(task)
}

}
}
14 changes: 7 additions & 7 deletions api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface CustomJobCodeRepository {
val elasticSearchTemplate: ElasticsearchRestTemplate
fun typeAheadSearch(query: String): SearchHits<JobCode>

fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits<JobCode>
fun typeAheadSearch(query: String?, pageable: OffsetPageable): SearchHits<JobCode>

fun deleteIndex() {
elasticSearchTemplate.indexOps(IndexCoordinates.of(INDEX_JOBCODE_DOC)).delete()
Expand All @@ -30,12 +30,12 @@ interface CustomJobCodeRepository {
class CustomJobCodeRepositoryImpl @Autowired constructor(override val elasticSearchTemplate: ElasticsearchRestTemplate) :
CustomJobCodeRepository {

override fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits<JobCode> {
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<JobCode> {
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)
}

Expand Down
68 changes: 67 additions & 1 deletion api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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
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
Expand All @@ -18,10 +23,12 @@ 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<JobCodeUpdate>): List<JobCodeDao>
fun onetsByDetailCode(detailedCode: String): SizedIterable<JobCodeDao>
fun remove(jobCodeId: Long): ApiBatchResult

companion object {
const val BLS_FRAMEWORK = "bls"
Expand All @@ -37,6 +44,10 @@ class JobCodeRepositoryImpl: JobCodeRepository {
@Lazy
lateinit var jobCodeEsRepo: JobCodeEsRepo

@Autowired
@Lazy
lateinit var richSkillJobCodeRepository: RichSkillJobCodeRepository

val dao = JobCodeDao.Companion
override val table = JobCodeTable

Expand All @@ -61,7 +72,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()) }
}
Expand All @@ -72,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 {
Expand All @@ -92,4 +112,50 @@ class JobCodeRepositoryImpl: JobCodeRepository {
override fun onetsByDetailCode(detailedCode: String): SizedIterable<JobCodeDao> {
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())
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 = false,
modifiedCount = 0,
totalCount = 0,
message = JobCodeErrorMessages.forDeleteError(hasChildren, hasRSDs)
)
}
}

enum class JobCodeErrorMessages(val apiValue: String) {
JobCodeNotExist("You cannot delete this occupation because you doesn't exist"),
JobCodeHasChildren("You cannot delete this occupation because has children"),
JobCodeHasRSD("You cannot delete this occupation 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
}
}
}
}
Loading