diff --git a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt index 06078e898..8ccc01009 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt @@ -30,6 +30,7 @@ object RoutePaths { const val COLLECTION_SKILLS = "$COLLECTION_DETAIL/skills" const val COLLECTION_AUDIT_LOG = "$COLLECTION_DETAIL/log" const val COLLECTION_CSV = "$COLLECTION_DETAIL/csv" + const val COLLECTION_REMOVE = "$COLLECTION_DETAIL/remove" const val TASKS_PATH = "$API/results" diff --git a/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt b/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt index b4bbdb7a5..38c40e24f 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt @@ -3,7 +3,11 @@ package edu.wgu.osmt.collection import edu.wgu.osmt.HasAllPaginated import edu.wgu.osmt.RoutePaths import edu.wgu.osmt.api.GeneralApiException -import edu.wgu.osmt.api.model.* +import edu.wgu.osmt.api.model.ApiCollection +import edu.wgu.osmt.api.model.ApiCollectionUpdate +import edu.wgu.osmt.api.model.ApiSearch +import edu.wgu.osmt.api.model.ApiSkillListUpdate +import edu.wgu.osmt.api.model.CollectionSortEnum import edu.wgu.osmt.auditlog.AuditLog import edu.wgu.osmt.auditlog.AuditLogRepository import edu.wgu.osmt.auditlog.AuditLogSortEnum @@ -12,7 +16,14 @@ import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.elasticsearch.OffsetPageable import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.security.OAuthHelper -import edu.wgu.osmt.task.* +import edu.wgu.osmt.task.AppliesToType +import edu.wgu.osmt.task.CsvTask +import edu.wgu.osmt.task.PublishTask +import edu.wgu.osmt.task.RemoveCollectionSkillsTask +import edu.wgu.osmt.task.Task +import edu.wgu.osmt.task.TaskMessageService +import edu.wgu.osmt.task.TaskResult +import edu.wgu.osmt.task.UpdateCollectionSkillsTask import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpEntity import org.springframework.http.HttpStatus @@ -22,7 +33,14 @@ 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.* +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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.server.ResponseStatusException import org.springframework.web.util.UriComponentsBuilder @@ -164,6 +182,16 @@ class CollectionController @Autowired constructor( return Task.processingResponse(task) } + @DeleteMapping(RoutePaths.COLLECTION_REMOVE, produces = [MediaType.APPLICATION_JSON_VALUE]) + fun removeCollection( + @PathVariable uuid: String + ): HttpEntity { + + val task = RemoveCollectionSkillsTask(collectionUuid = uuid) + taskMessageService.enqueueJob(TaskMessageService.removeCollectionSkills, task) + return Task.processingResponse(task) + } + @GetMapping(RoutePaths.COLLECTION_AUDIT_LOG, produces = ["application/json"]) fun collectionAuditLog( @PathVariable uuid: String diff --git a/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionRepository.kt index 48c8fe1c1..df8237c52 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionRepository.kt @@ -1,5 +1,6 @@ package edu.wgu.osmt.collection +import edu.wgu.osmt.PaginationDefaults import edu.wgu.osmt.api.FormValidationException import edu.wgu.osmt.api.model.ApiBatchResult import edu.wgu.osmt.api.model.ApiCollectionUpdate @@ -11,13 +12,22 @@ import edu.wgu.osmt.db.ListFieldUpdate import edu.wgu.osmt.db.NullableFieldUpdate import edu.wgu.osmt.keyword.KeywordRepository import edu.wgu.osmt.keyword.KeywordTypeEnum -import edu.wgu.osmt.richskill.* +import edu.wgu.osmt.richskill.RichSkillDescriptor +import edu.wgu.osmt.richskill.RichSkillDescriptorDao +import edu.wgu.osmt.richskill.RichSkillDescriptorTable +import edu.wgu.osmt.richskill.RichSkillDoc +import edu.wgu.osmt.richskill.RichSkillEsRepo +import edu.wgu.osmt.richskill.RichSkillRepository +import edu.wgu.osmt.richskill.diff import edu.wgu.osmt.task.PublishTask import edu.wgu.osmt.task.UpdateCollectionSkillsTask import org.jetbrains.exposed.sql.SizedIterable +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.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional @@ -36,6 +46,7 @@ interface CollectionRepository { fun create(name: String, user: String): CollectionDao? fun create(updateObject: CollectionUpdateObject, user: String): CollectionDao? fun update(updateObject: CollectionUpdateObject, user: String): CollectionDao? + fun remove(uuid: String): ApiBatchResult fun createFromApi( apiUpdates: List, @@ -80,6 +91,7 @@ class CollectionRepositoryImpl @Autowired constructor( override val table = CollectionTable override val dao = CollectionDao.Companion + val collectionSkillsTable = CollectionSkills override fun findAll() = dao.all() @@ -197,6 +209,32 @@ class CollectionRepositoryImpl @Autowired constructor( return daoObject } + override fun remove(uuid: String): ApiBatchResult { + + val collectionFound = findByUUID(uuid) + val esCollectionFound = collectionFound?.let { collectionEsRepo.findByUuid(it.uuid, PageRequest.of(0, PaginationDefaults.size))} + + if (esCollectionFound != null && esCollectionFound.content.isNotEmpty()) { + transaction { + table.deleteWhere { table.id eq collectionFound.id } + collectionEsRepo.delete(collectionFound.toDoc()) + + } + return ApiBatchResult( + success = true, + modifiedCount = 1, + totalCount = 1 + ) + } + + return ApiBatchResult( + success = false, + modifiedCount = 0, + totalCount = 0 + ) + + } + override fun createFromApi( apiUpdates: List, richSkillRepository: RichSkillRepository, diff --git a/api/src/main/kotlin/edu/wgu/osmt/collection/UpdateCollectionSkillsTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/collection/UpdateCollectionSkillsTaskProcessor.kt index 779ec0bee..6a0a84d43 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/UpdateCollectionSkillsTaskProcessor.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/collection/UpdateCollectionSkillsTaskProcessor.kt @@ -2,6 +2,7 @@ package edu.wgu.osmt.collection import com.github.sonus21.rqueue.annotation.RqueueListener import edu.wgu.osmt.richskill.RichSkillRepository +import edu.wgu.osmt.task.RemoveCollectionSkillsTask import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskStatus import edu.wgu.osmt.task.UpdateCollectionSkillsTask @@ -44,4 +45,22 @@ class UpdateCollectionSkillsTaskProcessor { logger.info("Task ${task.uuid} completed") } + + @RqueueListener( + value = [TaskMessageService.removeCollectionSkills], + deadLetterQueueListenerEnabled = "true", + deadLetterQueue = TaskMessageService.deadLetters, + concurrency = "1" + ) + fun removeCollectionSkills(task: RemoveCollectionSkillsTask) { + logger.info("Started processing to remove collection task id: ${task.uuid}") + + val batchResult = collectionRepository.remove(task.collectionUuid) + + 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/security/SecurityConfig.kt b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt index 9aca6835b..8510e3bba 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt @@ -7,6 +7,7 @@ import edu.wgu.osmt.RoutePaths.COLLECTION_CREATE import edu.wgu.osmt.RoutePaths.COLLECTION_CSV import edu.wgu.osmt.RoutePaths.COLLECTION_DETAIL import edu.wgu.osmt.RoutePaths.COLLECTION_PUBLISH +import edu.wgu.osmt.RoutePaths.COLLECTION_REMOVE import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS import edu.wgu.osmt.RoutePaths.COLLECTION_SKILLS_UPDATE import edu.wgu.osmt.RoutePaths.COLLECTION_UPDATE @@ -29,8 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile -import org.springframework.http.HttpMethod.GET -import org.springframework.http.HttpMethod.POST +import org.springframework.http.HttpMethod.* import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter @@ -129,6 +129,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(POST, COLLECTION_PUBLISH).hasAnyAuthority(ADMIN) .mvcMatchers(POST, COLLECTION_UPDATE).hasAnyAuthority(ADMIN, CURATOR) .mvcMatchers(POST, COLLECTION_SKILLS_UPDATE).hasAnyAuthority(ADMIN) + .mvcMatchers(DELETE, COLLECTION_REMOVE).hasAnyAuthority(ADMIN) .mvcMatchers("/api/**").hasAnyAuthority(ADMIN, CURATOR, VIEW, READ) } @@ -146,6 +147,7 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(POST, COLLECTION_PUBLISH).authenticated() .mvcMatchers(POST, COLLECTION_UPDATE).authenticated() .mvcMatchers(POST, COLLECTION_SKILLS_UPDATE).authenticated() + .mvcMatchers(DELETE, COLLECTION_REMOVE).denyAll() // fall-through .mvcMatchers("/api/**").permitAll() 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 917ddc686..3f8c815f7 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt @@ -27,7 +27,8 @@ import java.util.* JsonSubTypes.Type(value = ApiSkillListUpdate::class, name = "ApiSkillListUpdate"), JsonSubTypes.Type(value = UpdateCollectionSkillsTask::class, name = "UpdateCollectionSkillsTask"), JsonSubTypes.Type(value = CreateSkillsTask::class, name = "CreateSkillsTask"), - JsonSubTypes.Type(value = ExportSkillsToCsvTask::class, name = "ExportSkillsToCsvTask") + JsonSubTypes.Type(value = ExportSkillsToCsvTask::class, name = "ExportSkillsToCsvTask"), + JsonSubTypes.Type(value = RemoveCollectionSkillsTask::class, name = "RemoveCollectionSkillsTask") ) interface Task { @@ -128,6 +129,17 @@ data class UpdateCollectionSkillsTask( override val apiResultPath = RoutePaths.TASK_DETAIL_BATCH } +data class RemoveCollectionSkillsTask( + val collectionUuid: String = "", + 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 74b0d70ec..9174368ae 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt @@ -43,6 +43,7 @@ class TaskMessageService { const val publishSkills = "batch-publish-skills" const val updateCollectionSkills = "update-collection-skills" const val skillsForCollectionCsv = "collection-skills-csv-process" + const val removeCollectionSkills = "remove-collection" const val skillsForFullLibraryCsv = "full-library-skills-csv-process" const val skillsForCustomListExportCsv = "custom-rsd-list-export" } 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 40ab8bf34..3f32415d2 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/collection/CollectionRepositoryTest.kt @@ -16,6 +16,7 @@ import edu.wgu.osmt.richskill.RsdUpdateObject import edu.wgu.osmt.task.PublishTask import edu.wgu.osmt.task.UpdateCollectionSkillsTask import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.exposed.sql.selectAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.transaction.annotation.Transactional @@ -32,6 +33,7 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes val userString = "unittestuser" + @Test fun `should not create a blank collection`() { assertThat(collectionRepository.create(CollectionUpdateObject(), userString)).isNull() @@ -69,7 +71,7 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes ), userString)!! } - private fun random_collection_update(): ApiCollectionUpdate { + private fun random_collection_update(): ApiCollectionUpdate { val name = UUID.randomUUID().toString() val author = UUID.randomUUID().toString() val status = PublishStatus.Published @@ -165,7 +167,6 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes } - @Test fun testChangeStatusesForTaskWithCollectionId() { // Arrange val skillCount = 3 @@ -189,4 +190,33 @@ class CollectionRepositoryTest: SpringTest(), BaseDockerizedTest, HasDatabaseRes assertThat(batchResult?.modifiedCount).isEqualTo(skillCount*3) } + @Test + fun `remove finds and successfully removes an existing collection`() { + // Arrange + val collection = collectionRepository.create(UUID.randomUUID().toString(), userString)!!.toModel() + val updateObject = RsdUpdateObject(name = "test skill", statement = testUser) + val skillDao = richSkillRepository.create(updateObject, testUser) + collection.id?.let { CollectionSkills.create(it, skillDao!!.id.value) } + + // Act + val batchResult = collectionRepository.remove(collection.uuid) + + // Assert + assertThat(CollectionTable.selectAll()).isEmpty() + assertThat(CollectionSkills.selectAll()).isEmpty() + assertThat(batchResult?.modifiedCount).isEqualTo(1) + assertThat(batchResult?.success).isEqualTo(true) + + } + + @Test + fun `remove fails to remove a non-existing collection`() { + // Act + val batchResult = collectionRepository.remove(UUID.randomUUID().toString()) + + // Assert + assertThat(batchResult?.modifiedCount).isEqualTo(0) + assertThat(batchResult?.success).isEqualTo(false) + } + } \ No newline at end of file diff --git a/ui/src/app/auth/auth-roles.ts b/ui/src/app/auth/auth-roles.ts index 83bdd355a..58bbe6485 100644 --- a/ui/src/app/auth/auth-roles.ts +++ b/ui/src/app/auth/auth-roles.ts @@ -12,7 +12,8 @@ export enum ButtonAction { CollectionPublish, CollectionSkillsUpdate, LibraryExport, - ExportDraftCollection + ExportDraftCollection, + DeleteCollection, } export const ActionByRoles = new Map([ @@ -24,7 +25,8 @@ export const ActionByRoles = new Map([ [ButtonAction.CollectionPublish, [OSMT_ADMIN]], [ButtonAction.CollectionSkillsUpdate, [OSMT_ADMIN]], [ButtonAction.LibraryExport, [OSMT_ADMIN]], - [ButtonAction.ExportDraftCollection, [OSMT_ADMIN]] + [ButtonAction.ExportDraftCollection, [OSMT_ADMIN]], + [ButtonAction.DeleteCollection, [OSMT_ADMIN]] ]) //TODO migrate AuthServiceWgu & AuthService.hasRole & isEnabledByRoles into a singleton here. HDN Sept 15, 2022 diff --git a/ui/src/app/collection/detail/manage-collection.component.html b/ui/src/app/collection/detail/manage-collection.component.html index 3a93010a5..ec40bb504 100644 --- a/ui/src/app/collection/detail/manage-collection.component.html +++ b/ui/src/app/collection/detail/manage-collection.component.html @@ -1,135 +1,159 @@ + + - +