From 85d1166601eaee096d20bb1319b190599412e35f Mon Sep 17 00:00:00 2001 From: David Holub <74815577+Corpratespaz@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:23:09 +0700 Subject: [PATCH 1/5] OSMT-25: Initial package refactor and abstraction for CSV and Excel exports OSMT-25: Additional package refactor to nest io OSMT-25: Further abstraction of CSV resource OSMT-25: Added endpoints and backend logic for Excel export OSMT-25: Updated endpoint security conf & OpenAPI --- api/pom.xml | 10 +++ .../edu/wgu/osmt/ImportCommandRunner.kt | 6 +- .../main/kotlin/edu/wgu/osmt/RoutePaths.kt | 2 + .../edu/wgu/osmt/auditlog/AuditLogUtils.kt | 2 +- .../osmt/collection/CollectionController.kt | 13 ++++ .../edu/wgu/osmt/io/common/TabularResource.kt | 12 +++ .../edu/wgu/osmt/io/common/TabularTask.kt | 26 +++++++ .../osmt/{ => io}/csv/BatchImportRichSkill.kt | 2 +- .../edu/wgu/osmt/{ => io}/csv/BlsImport.kt | 3 +- .../edu/wgu/osmt/{ => io}/csv/CsvBuilder.kt | 2 +- .../edu/wgu/osmt/{ => io}/csv/CsvImport.kt | 2 +- .../edu/wgu/osmt/{ => io}/csv/CsvResource.kt | 22 +++--- .../csv}/CsvTaskProcessor.kt | 39 +++------- .../csv}/ExportSkillToCsvTaskProcessor.kt | 4 +- .../edu/wgu/osmt/{ => io}/csv/OnetImport.kt | 2 +- .../csv}/RichSkillCsvExport.kt | 5 +- .../wgu/osmt/io/xlsx/RichSkillXlsxExport.kt | 52 +++++++++++++ .../edu/wgu/osmt/io/xlsx/XlsxResource.kt | 74 +++++++++++++++++++ .../edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt | 65 ++++++++++++++++ .../wgu/osmt/richskill/RichSkillController.kt | 2 + .../edu/wgu/osmt/security/SecurityConfig.kt | 4 + api/src/main/kotlin/edu/wgu/osmt/task/Task.kt | 11 +++ .../edu/wgu/osmt/task/TaskController.kt | 6 ++ .../edu/wgu/osmt/task/TaskMessageService.kt | 2 + .../{ => io}/csv/BatchImportRichSkillTest.kt | 2 +- .../wgu/osmt/{ => io}/csv/BlsImportTest.kt | 2 +- .../wgu/osmt/{ => io}/csv/CsvResourceTest.kt | 8 +- .../wgu/osmt/{ => io}/csv/OnetImportTest.kt | 2 +- .../kotlin/edu/wgu/osmt/mockdata/MockData.kt | 6 +- .../osmt/richskill/RichSkillControllerTest.kt | 4 +- .../wgu/osmt/richskill/RichSkillEsRepoTest.kt | 2 +- docs/int/osmt-v1.0.x-openapi3.yaml | 26 +++++++ docs/int/osmt-v2.x-openapi3.yaml | 26 +++++++ 33 files changed, 382 insertions(+), 64 deletions(-) create mode 100644 api/src/main/kotlin/edu/wgu/osmt/io/common/TabularResource.kt create mode 100644 api/src/main/kotlin/edu/wgu/osmt/io/common/TabularTask.kt rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/BatchImportRichSkill.kt (99%) rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/BlsImport.kt (98%) rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/CsvBuilder.kt (98%) rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/CsvImport.kt (98%) rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/CsvResource.kt (82%) rename api/src/main/kotlin/edu/wgu/osmt/{collection => io/csv}/CsvTaskProcessor.kt (61%) rename api/src/main/kotlin/edu/wgu/osmt/{richskill => io/csv}/ExportSkillToCsvTaskProcessor.kt (92%) rename api/src/main/kotlin/edu/wgu/osmt/{ => io}/csv/OnetImport.kt (99%) rename api/src/main/kotlin/edu/wgu/osmt/{richskill => io/csv}/RichSkillCsvExport.kt (96%) create mode 100644 api/src/main/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExport.kt create mode 100644 api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxResource.kt create mode 100644 api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt rename api/src/test/kotlin/edu/wgu/osmt/{ => io}/csv/BatchImportRichSkillTest.kt (98%) rename api/src/test/kotlin/edu/wgu/osmt/{ => io}/csv/BlsImportTest.kt (98%) rename api/src/test/kotlin/edu/wgu/osmt/{ => io}/csv/CsvResourceTest.kt (96%) rename api/src/test/kotlin/edu/wgu/osmt/{ => io}/csv/OnetImportTest.kt (98%) diff --git a/api/pom.xml b/api/pom.xml index 880252993..b1d60eb56 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -250,6 +250,16 @@ opencsv 5.7.1 + + org.apache.poi + poi + 5.2.3 + + + org.apache.poi + poi-ooxml + 5.2.3 + org.springframework.boot spring-boot-starter-actuator diff --git a/api/src/main/kotlin/edu/wgu/osmt/ImportCommandRunner.kt b/api/src/main/kotlin/edu/wgu/osmt/ImportCommandRunner.kt index a09095086..ccec455bb 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/ImportCommandRunner.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/ImportCommandRunner.kt @@ -1,8 +1,8 @@ package edu.wgu.osmt -import edu.wgu.osmt.csv.BatchImportRichSkill -import edu.wgu.osmt.csv.BlsImport -import edu.wgu.osmt.csv.OnetImport +import edu.wgu.osmt.io.csv.BatchImportRichSkill +import edu.wgu.osmt.io.csv.BlsImport +import edu.wgu.osmt.io.csv.OnetImport import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired diff --git a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt index ae0088a78..01cee4c59 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt @@ -31,12 +31,14 @@ 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_XLSX = "$COLLECTION_DETAIL/xlsx" const val COLLECTION_REMOVE = "$COLLECTION_DETAIL/remove" const val WORKSPACE_PATH = "$API/workspace" const val TASKS_PATH = "$API/results" const val TASK_DETAIL_TEXT = "$TASKS_PATH/text/{uuid}" + const val TASK_DETAIL_MEDIA = "$TASKS_PATH/media/{uuid}" const val TASK_DETAIL_BATCH = "$TASKS_PATH/batch/{uuid}" const val TASK_DETAIL_SKILLS = "$TASKS_PATH/skills/{uuid}" diff --git a/api/src/main/kotlin/edu/wgu/osmt/auditlog/AuditLogUtils.kt b/api/src/main/kotlin/edu/wgu/osmt/auditlog/AuditLogUtils.kt index 06e1549ff..c62568f79 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/auditlog/AuditLogUtils.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/auditlog/AuditLogUtils.kt @@ -4,7 +4,7 @@ import edu.wgu.osmt.collection.CollectionRepository import edu.wgu.osmt.collection.CollectionTable import edu.wgu.osmt.collection.diff import edu.wgu.osmt.config.AppConfig -import edu.wgu.osmt.csv.BatchImportRichSkill +import edu.wgu.osmt.io.csv.BatchImportRichSkill import edu.wgu.osmt.richskill.RichSkillDescriptorTable import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.richskill.diff 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 60986a98a..512e906c4 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/collection/CollectionController.kt @@ -26,6 +26,7 @@ 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 edu.wgu.osmt.task.XlsxTask import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpEntity import org.springframework.http.HttpStatus @@ -185,6 +186,18 @@ class CollectionController @Autowired constructor( return Task.processingResponse(task) } + @GetMapping(RoutePaths.COLLECTION_XLSX, produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) + fun getSkillsForCollectionXlsx( + @PathVariable uuid: String + ): HttpEntity { + if (collectionRepository.findByUUID(uuid)!!.status == PublishStatus.Draft && !oAuthHelper.hasRole(appConfig.roleAdmin)) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED) + } + val task = XlsxTask(collectionUuid = uuid) + taskMessageService.enqueueJob(TaskMessageService.skillsForCollectionXlsx, task) + return Task.processingResponse(task) + } + @DeleteMapping(RoutePaths.COLLECTION_REMOVE, produces = [MediaType.APPLICATION_JSON_VALUE]) fun removeCollection( @PathVariable uuid: String diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularResource.kt b/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularResource.kt new file mode 100644 index 000000000..edbef5ae8 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularResource.kt @@ -0,0 +1,12 @@ +package edu.wgu.osmt.io.common + +interface TabularResource, T> { + + /** + * Defines the columns of this table in their desired order. + */ + fun columnTranslations(data: List): Array + +} + +interface TabColumn diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularTask.kt b/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularTask.kt new file mode 100644 index 000000000..0e4f33a85 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/io/common/TabularTask.kt @@ -0,0 +1,26 @@ +package edu.wgu.osmt.io.common + +import edu.wgu.osmt.collection.CollectionRepository +import edu.wgu.osmt.config.AppConfig +import edu.wgu.osmt.richskill.RichSkillRepository +import edu.wgu.osmt.task.Task +import edu.wgu.osmt.task.TaskMessageService +import org.springframework.beans.factory.annotation.Autowired + +abstract class TabularTask { + @Autowired + lateinit var taskMessageService: TaskMessageService + + @Autowired + lateinit var collectionRepository: CollectionRepository + + @Autowired + lateinit var richSkillRepository: RichSkillRepository + + @Autowired + lateinit var appConfig: AppConfig + + abstract fun tabularSkillsInCollectionProcessor(task: T) + + abstract fun tabularSkillsInFullLibraryProcessor(task: T) +} diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/BatchImportRichSkill.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkill.kt similarity index 99% rename from api/src/main/kotlin/edu/wgu/osmt/csv/BatchImportRichSkill.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkill.kt index d11828605..8ed3ea5c7 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/BatchImportRichSkill.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkill.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import com.opencsv.bean.CsvBindByName import edu.wgu.osmt.collection.CollectionDao diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/BlsImport.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/BlsImport.kt similarity index 98% rename from api/src/main/kotlin/edu/wgu/osmt/csv/BlsImport.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/BlsImport.kt index 96d432f0f..4492a1a28 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/BlsImport.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/BlsImport.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import com.opencsv.bean.CsvBindByName import edu.wgu.osmt.config.AppConfig @@ -10,7 +10,6 @@ import edu.wgu.osmt.richskill.RichSkillRepository import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component /** diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvBuilder.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvBuilder.kt similarity index 98% rename from api/src/main/kotlin/edu/wgu/osmt/csv/CsvBuilder.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvBuilder.kt index 31ab4b323..d92b4d193 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvBuilder.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvBuilder.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv /** * DSL style entry-point to build a CsvResource. This approach should be used if a csv file is simple in structure diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvImport.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvImport.kt similarity index 98% rename from api/src/main/kotlin/edu/wgu/osmt/csv/CsvImport.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvImport.kt index ae54b92ce..9b6deee34 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvImport.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvImport.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import com.opencsv.bean.CsvToBeanBuilder import edu.wgu.osmt.jobcode.JobCodeBreakout diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvResource.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvResource.kt similarity index 82% rename from api/src/main/kotlin/edu/wgu/osmt/csv/CsvResource.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvResource.kt index 3fb5c94b3..967fe6565 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/CsvResource.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvResource.kt @@ -1,15 +1,17 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import com.opencsv.CSVWriter +import edu.wgu.osmt.io.common.TabularResource +import edu.wgu.osmt.io.common.TabColumn import java.io.StringWriter import java.io.Writer -abstract class CsvResource(val debugName: String) { +abstract class CsvResource(val debugName: String) : TabularResource, T> { /** * Defines the columns of this csv in their desired order. */ - abstract fun columnTranslations(data: List): Array> + abstract override fun columnTranslations(data: List): Array> /** * Override if opencsv defaults are not desired @@ -79,19 +81,19 @@ abstract class CsvResource(val debugName: String) { data class CsvColumn( val name: String = "", val translate: (T) -> String -) +): TabColumn /** * Configure the global attributes of a csv export */ data class CsvConfig( - val delimeter: Char = CsvConfig.delimeter, - val quoteChar: Char = CsvConfig.quoteChar, - val escapeChar: Char = CsvConfig.escapeChar, - val lineEnd: String = CsvConfig.lineEnd, - val includeHeader: Boolean = CsvConfig.includeHeader + val delimeter: Char = Defaults.delimeter, + val quoteChar: Char = Defaults.quoteChar, + val escapeChar: Char = Defaults.escapeChar, + val lineEnd: String = Defaults.lineEnd, + val includeHeader: Boolean = Defaults.includeHeader ) { - companion object Defaults { // Allows the default values to be shared with it's builder + companion object Defaults { // Allows the default values to be shared with its builder val delimeter: Char = CSVWriter.DEFAULT_SEPARATOR val quoteChar: Char = CSVWriter.DEFAULT_QUOTE_CHARACTER val escapeChar: Char = CSVWriter.DEFAULT_ESCAPE_CHARACTER diff --git a/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvTaskProcessor.kt similarity index 61% rename from api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvTaskProcessor.kt index aed3830e0..4fc8becaa 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/CsvTaskProcessor.kt @@ -1,19 +1,16 @@ -package edu.wgu.osmt.collection +package edu.wgu.osmt.io.csv import com.github.sonus21.rqueue.annotation.RqueueListener -import edu.wgu.osmt.config.AppConfig import edu.wgu.osmt.db.PublishStatus +import edu.wgu.osmt.io.common.TabularTask import edu.wgu.osmt.richskill.RichSkillAndCollections -import edu.wgu.osmt.richskill.RichSkillCsvExport import edu.wgu.osmt.richskill.RichSkillDescriptorDao -import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.task.CsvTask import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskStatus import org.jetbrains.exposed.dao.with 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 org.springframework.transaction.annotation.Transactional @@ -21,31 +18,19 @@ import org.springframework.transaction.annotation.Transactional @Component @Profile("apiserver") @Transactional -class CsvTaskProcessor { +class CsvTaskProcessor : TabularTask() { val logger: Logger = LoggerFactory.getLogger(CsvTaskProcessor::class.java) - @Autowired - lateinit var taskMessageService: TaskMessageService - - @Autowired - lateinit var collectionRepository: CollectionRepository - - @Autowired - lateinit var richSkillRepository: RichSkillRepository - - @Autowired - lateinit var appConfig: AppConfig - @RqueueListener( value = [TaskMessageService.skillsForCollectionCsv], deadLetterQueueListenerEnabled = "true", deadLetterQueue = TaskMessageService.deadLetters, concurrency = "1" ) - fun csvSkillsInCollectionProcessor(csvTask: CsvTask) { - logger.info("Started processing task id: ${csvTask.uuid}") + override fun tabularSkillsInCollectionProcessor(task: CsvTask) { + logger.info("Started processing task id: ${task.uuid}") - val csv = collectionRepository.findByUUID(csvTask.collectionUuid) + val csv = collectionRepository.findByUUID(task.collectionUuid) ?.skills ?.filter { PublishStatus.Archived != it.publishStatus() } ?.with(RichSkillDescriptorDao::collections) @@ -53,9 +38,9 @@ class CsvTaskProcessor { ?.let { RichSkillCsvExport(appConfig).toCsv(it) } taskMessageService.publishResult( - csvTask.copy(result = csv, status = TaskStatus.Ready) + task.copy(result = csv, status = TaskStatus.Ready) ) - logger.info("Task ${csvTask.uuid} completed") + logger.info("Task ${task.uuid} completed") } @RqueueListener( @@ -64,8 +49,8 @@ class CsvTaskProcessor { deadLetterQueue = TaskMessageService.deadLetters, concurrency = "1" ) - fun csvSkillsInFullLibraryProcessor(csvTask: CsvTask) { - logger.info("Started processing task for Full Library export") + override fun tabularSkillsInFullLibraryProcessor(task: CsvTask) { + logger.info("Started processing task for Full Library .csv export") val csv = richSkillRepository.findAll() ?.with(RichSkillDescriptorDao::collections) @@ -73,9 +58,9 @@ class CsvTaskProcessor { ?.let { RichSkillCsvExport(appConfig).toCsv(it) } taskMessageService.publishResult( - csvTask.copy(result = csv, status = TaskStatus.Ready) + task.copy(result = csv, status = TaskStatus.Ready) ) - logger.info("Full Library export task completed") + logger.info("Full Library export task .csv completed") } } diff --git a/api/src/main/kotlin/edu/wgu/osmt/richskill/ExportSkillToCsvTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/ExportSkillToCsvTaskProcessor.kt similarity index 92% rename from api/src/main/kotlin/edu/wgu/osmt/richskill/ExportSkillToCsvTaskProcessor.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/ExportSkillToCsvTaskProcessor.kt index f971f7f59..9eaea2970 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/richskill/ExportSkillToCsvTaskProcessor.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/ExportSkillToCsvTaskProcessor.kt @@ -1,7 +1,9 @@ -package edu.wgu.osmt.richskill +package edu.wgu.osmt.io.csv import com.github.sonus21.rqueue.annotation.RqueueListener import edu.wgu.osmt.config.AppConfig +import edu.wgu.osmt.richskill.RichSkillAndCollections +import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.task.ExportSkillsToCsvTask import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskStatus diff --git a/api/src/main/kotlin/edu/wgu/osmt/csv/OnetImport.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/OnetImport.kt similarity index 99% rename from api/src/main/kotlin/edu/wgu/osmt/csv/OnetImport.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/OnetImport.kt index 9ed9260d4..cd532d2be 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/csv/OnetImport.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/OnetImport.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import com.opencsv.bean.CsvBindByName import edu.wgu.osmt.config.AppConfig diff --git a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillCsvExport.kt b/api/src/main/kotlin/edu/wgu/osmt/io/csv/RichSkillCsvExport.kt similarity index 96% rename from api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillCsvExport.kt rename to api/src/main/kotlin/edu/wgu/osmt/io/csv/RichSkillCsvExport.kt index 2fb294a45..fc66f04f7 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillCsvExport.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/io/csv/RichSkillCsvExport.kt @@ -1,10 +1,9 @@ -package edu.wgu.osmt.richskill +package edu.wgu.osmt.io.csv import edu.wgu.osmt.config.AppConfig -import edu.wgu.osmt.csv.CsvColumn -import edu.wgu.osmt.csv.CsvResource import edu.wgu.osmt.jobcode.JobCode import edu.wgu.osmt.jobcode.JobCodeBreakout +import edu.wgu.osmt.richskill.RichSkillAndCollections class RichSkillCsvExport( private val appConfig: AppConfig diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExport.kt b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExport.kt new file mode 100644 index 000000000..dcb755d3b --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExport.kt @@ -0,0 +1,52 @@ +package edu.wgu.osmt.io.xlsx + +import edu.wgu.osmt.config.AppConfig +import edu.wgu.osmt.jobcode.JobCode +import edu.wgu.osmt.jobcode.JobCodeBreakout +import edu.wgu.osmt.richskill.RichSkillAndCollections + +class RichSkillXlsxExport( + private val appConfig: AppConfig +) : XlsxResource("RichSkillXlsxExport") { + private val listDelimiter = "; " + + override fun columnTranslations(data: List): Array> { + val columns = arrayOf( + XlsxColumn("Canonical URL") { it.rs.canonicalUrl(appConfig.baseUrl) }, + XlsxColumn("RSD Name") { it.rs.name }, + XlsxColumn("Author") { it.rs.author?.value ?: "" }, + XlsxColumn("Skill Statement") { it.rs.statement }, + XlsxColumn("Category") { it.rs.category?.value ?: "" }, + XlsxColumn("Keywords") { it.rs.searchingKeywords.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) }, + XlsxColumn("Standards") { it.rs.standards.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) }, + XlsxColumn("Certifications") { it.rs.certifications.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) }, + XlsxColumn("Occupation Major Groups") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::majorCode) }, + XlsxColumn("Occupation Minor Groups") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::minorCode) }, + XlsxColumn("Broad Occupations") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::broadCode) }, + XlsxColumn("Detailed Occupations") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::detailedCode) }, + XlsxColumn("O*Net Job Codes") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::jobRoleCode) }, + XlsxColumn("Employers") { it.rs.employers.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) } + ) + val alignmentCount = data.map { s -> s.rs.alignments.size }.maxOrNull() ?: 0 + val alignmentColumns = (0 until alignmentCount).flatMap { i -> + val label = if (i > 0) " ${i + 1}" else "" + listOf( + XlsxColumn("Alignment${label} Name") { it.rs.alignments.getOrNull(i)?.value ?: "" }, + XlsxColumn("Alignment${label} URL") { it.rs.alignments.getOrNull(i)?.uri ?: "" }, + XlsxColumn("Alignment${label} Framework") { it.rs.alignments.getOrNull(i)?.framework ?: "" } + ) + } + return columns + alignmentColumns + } + + private fun prepareJobCodePart( + codes: List, + partTransformation: (String) -> String? + ): String = codes + .asSequence() + .map { it.code } + .map(partTransformation) + .filterNotNull() + .distinct() + .joinToString(listDelimiter) +} diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxResource.kt b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxResource.kt new file mode 100644 index 000000000..8aed9e0a6 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxResource.kt @@ -0,0 +1,74 @@ +package edu.wgu.osmt.io.xlsx + +import com.opencsv.CSVWriter +import edu.wgu.osmt.io.common.TabColumn +import edu.wgu.osmt.io.common.TabularResource +import edu.wgu.osmt.richskill.RichSkillAndCollections +import org.apache.poi.ss.usermodel.BuiltinFormats +import org.apache.poi.ss.usermodel.Cell +import org.apache.poi.ss.usermodel.Row +import org.apache.poi.xssf.usermodel.XSSFSheet +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.ByteArrayOutputStream + +abstract class XlsxResource(val debugName: String) : TabularResource, T> { + private val workbook: XSSFWorkbook = XSSFWorkbook() + private var rowCount: Int = 0 + + /** + * Defines the columns of this xlsx in their desired order. + */ + abstract override fun columnTranslations(data: List): Array> + + /** + * Produce a xlsx export from the list of data using this XlsxResource's configuration and translations + */ + fun toXlsx(data: List): ByteArray { + val sheet: XSSFSheet = this.workbook.createSheet() + + writeHeaderRow(data, sheet) + writeRows(data, sheet) + + val output: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(output) + + return output.toByteArray() + } + + private fun writeHeaderRow(data: List, sheet: XSSFSheet) { + if (true) { + val headerRow = columnTranslations(data).map { column -> column.name }.toTypedArray() + writeRow(headerRow, sheet, true) + } + } + + private fun writeRows(data: List, sheet: XSSFSheet) { + val rowsList: List> = data.map { datum -> + columnTranslations(data).map { it.translate(datum) }.toTypedArray() + } + rowsList.forEach { writeRow(it, sheet) } + } + + private fun writeRow(rowData: Array, sheet: XSSFSheet, isHeader: Boolean=false) { + val row: Row = sheet.createRow(this.rowCount) + + rowData.forEachIndexed { colIndex, element -> + if (isHeader) { + val format = BuiltinFormats.getBuiltinFormat("@") + val textStyle = this.workbook.createCellStyle() + textStyle.setDataFormat(format) + sheet.setDefaultColumnStyle(colIndex, textStyle) + } + + val cell: Cell = row.createCell(colIndex) + cell.setCellValue(element) + } + + this.rowCount++ + } +} + +data class XlsxColumn( + val name: String = "", + val translate: (T) -> String +): TabColumn \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt new file mode 100644 index 000000000..d6f6283e4 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt @@ -0,0 +1,65 @@ +package edu.wgu.osmt.io.xlsx + +import com.github.sonus21.rqueue.annotation.RqueueListener +import edu.wgu.osmt.db.PublishStatus +import edu.wgu.osmt.io.common.TabularTask +import edu.wgu.osmt.richskill.RichSkillAndCollections +import edu.wgu.osmt.richskill.RichSkillDescriptorDao +import edu.wgu.osmt.task.XlsxTask +import edu.wgu.osmt.task.TaskMessageService +import edu.wgu.osmt.task.TaskStatus +import org.jetbrains.exposed.dao.with +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +@Profile("apiserver") +@Transactional +class XlsxTaskProcessor : TabularTask() { + val logger: Logger = LoggerFactory.getLogger(XlsxTaskProcessor::class.java) + + @RqueueListener( + value = [TaskMessageService.skillsForCollectionXlsx], + deadLetterQueueListenerEnabled = "true", + deadLetterQueue = TaskMessageService.deadLetters, + concurrency = "1" + ) + override fun tabularSkillsInCollectionProcessor(task: XlsxTask) { + logger.info("Started processing task id: ${task.uuid}") + + val xlsx = collectionRepository.findByUUID(task.collectionUuid) + ?.skills + ?.filter { PublishStatus.Archived != it.publishStatus() } + ?.with(RichSkillDescriptorDao::collections) + ?.map { RichSkillAndCollections.fromDao(it) } + ?.let { RichSkillXlsxExport(appConfig).toXlsx(it) } + + taskMessageService.publishResult( + task.copy(result = xlsx, status = TaskStatus.Ready) + ) + logger.info("Task ${task.uuid} completed") + } + + @RqueueListener( + value = [TaskMessageService.skillsForFullLibraryXlsx], + deadLetterQueueListenerEnabled = "true", + deadLetterQueue = TaskMessageService.deadLetters, + concurrency = "1" + ) + override fun tabularSkillsInFullLibraryProcessor(task: XlsxTask) { + logger.info("Started processing task for Full Library .xlsx export") + + val xlsx = richSkillRepository.findAll() + ?.with(RichSkillDescriptorDao::collections) + ?.map { RichSkillAndCollections.fromDao(it) } + ?.let { RichSkillXlsxExport(appConfig).toXlsx(it) } + + taskMessageService.publishResult( + task.copy(result = xlsx, status = TaskStatus.Ready) + ) + logger.info("Full Library .xlsx export task completed") + } +} diff --git a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt index fd032c847..7d1d3c9fb 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt @@ -13,6 +13,7 @@ import edu.wgu.osmt.auditlog.AuditLog import edu.wgu.osmt.auditlog.AuditLogRepository import edu.wgu.osmt.auditlog.AuditLogSortEnum import edu.wgu.osmt.config.AppConfig +import edu.wgu.osmt.io.csv.RichSkillCsvExport import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.elasticsearch.OffsetPageable import edu.wgu.osmt.elasticsearch.PaginatedLinks @@ -269,6 +270,7 @@ class RichSkillController @Autowired constructor( return Task.processingResponse(task) } + // This really appears to be never used.... @Transactional(readOnly = true) @PostMapping(RoutePaths.EXPORT_SKILLS, produces = [MediaType.APPLICATION_JSON_VALUE]) @ResponseBody 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 3e8cdc7c0..f56ea9b63 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt @@ -11,6 +11,7 @@ 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 +import edu.wgu.osmt.RoutePaths.COLLECTION_XLSX import edu.wgu.osmt.RoutePaths.SEARCH_COLLECTIONS import edu.wgu.osmt.RoutePaths.SEARCH_JOBCODES_PATH import edu.wgu.osmt.RoutePaths.SEARCH_KEYWORDS_PATH @@ -22,6 +23,7 @@ import edu.wgu.osmt.RoutePaths.SKILL_DETAIL import edu.wgu.osmt.RoutePaths.SKILL_PUBLISH import edu.wgu.osmt.RoutePaths.SKILL_UPDATE import edu.wgu.osmt.RoutePaths.TASK_DETAIL_BATCH +import edu.wgu.osmt.RoutePaths.TASK_DETAIL_MEDIA import edu.wgu.osmt.RoutePaths.TASK_DETAIL_SKILLS import edu.wgu.osmt.RoutePaths.TASK_DETAIL_TEXT import edu.wgu.osmt.RoutePaths.WORKSPACE_PATH @@ -93,6 +95,8 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { .mvcMatchers(POST, COLLECTION_SKILLS).permitAll() .mvcMatchers(GET, COLLECTION_CSV).permitAll() .mvcMatchers(GET, TASK_DETAIL_TEXT).permitAll() // public csv results + .mvcMatchers(GET, COLLECTION_XLSX).permitAll() + .mvcMatchers(GET, TASK_DETAIL_MEDIA).permitAll() // public csv results .and().exceptionHandling().authenticationEntryPoint(returnUnauthorized) .and().oauth2Login().successHandler(redirectToFrontend) 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 d615b07ad..5a10510c3 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/Task.kt @@ -21,6 +21,7 @@ import java.util.* ) @JsonSubTypes( JsonSubTypes.Type(value = CsvTask::class, name = "CsvTask"), + JsonSubTypes.Type(value = XlsxTask::class, name = "XlsxTask"), JsonSubTypes.Type(value = ApiSearch::class, name = "ApiSearch"), JsonSubTypes.Type(value = ApiBatchResult::class, name = "ApiBatchResult"), JsonSubTypes.Type(value = PublishTask::class, name = "PublishTask"), @@ -67,7 +68,17 @@ data class CsvTask( ) : Task { override val contentType = "text/csv" override val apiResultPath = RoutePaths.TASK_DETAIL_TEXT +} +data class XlsxTask( + val collectionUuid: String = "", + override val uuid: String = UUID.randomUUID().toString(), + override val start: Date = Date(), + override val result: ByteArray? = null, + override val status: TaskStatus = TaskStatus.Processing +) : Task { + override val contentType = "application/vnd.ms-excel" + override val apiResultPath = RoutePaths.TASK_DETAIL_MEDIA } data class ExportSkillsToCsvTask( diff --git a/api/src/main/kotlin/edu/wgu/osmt/task/TaskController.kt b/api/src/main/kotlin/edu/wgu/osmt/task/TaskController.kt index c394e1d6e..2ee66da68 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/TaskController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/TaskController.kt @@ -35,6 +35,12 @@ class TaskController @Autowired constructor( return taskResult(uuid) } + @GetMapping(RoutePaths.TASK_DETAIL_MEDIA) + @ResponseBody + fun mediaResult(@PathVariable uuid: String): HttpEntity<*> { + return taskResult(uuid) + } + @GetMapping(RoutePaths.TASK_DETAIL_BATCH) @ResponseBody fun batchResult(@PathVariable uuid: String): HttpEntity<*> { 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 9174368ae..fa51fa1f0 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt @@ -43,8 +43,10 @@ class TaskMessageService { const val publishSkills = "batch-publish-skills" const val updateCollectionSkills = "update-collection-skills" const val skillsForCollectionCsv = "collection-skills-csv-process" + const val skillsForCollectionXlsx = "collection-skills-xlsx-process" const val removeCollectionSkills = "remove-collection" const val skillsForFullLibraryCsv = "full-library-skills-csv-process" + const val skillsForFullLibraryXlsx = "full-library-skills-xlsx-process" const val skillsForCustomListExportCsv = "custom-rsd-list-export" } } diff --git a/api/src/test/kotlin/edu/wgu/osmt/csv/BatchImportRichSkillTest.kt b/api/src/test/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkillTest.kt similarity index 98% rename from api/src/test/kotlin/edu/wgu/osmt/csv/BatchImportRichSkillTest.kt rename to api/src/test/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkillTest.kt index 759301948..317cefc2f 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/csv/BatchImportRichSkillTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/io/csv/BatchImportRichSkillTest.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset diff --git a/api/src/test/kotlin/edu/wgu/osmt/csv/BlsImportTest.kt b/api/src/test/kotlin/edu/wgu/osmt/io/csv/BlsImportTest.kt similarity index 98% rename from api/src/test/kotlin/edu/wgu/osmt/csv/BlsImportTest.kt rename to api/src/test/kotlin/edu/wgu/osmt/io/csv/BlsImportTest.kt index e654cd6d1..a5159017e 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/csv/BlsImportTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/io/csv/BlsImportTest.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset diff --git a/api/src/test/kotlin/edu/wgu/osmt/csv/CsvResourceTest.kt b/api/src/test/kotlin/edu/wgu/osmt/io/csv/CsvResourceTest.kt similarity index 96% rename from api/src/test/kotlin/edu/wgu/osmt/csv/CsvResourceTest.kt rename to api/src/test/kotlin/edu/wgu/osmt/io/csv/CsvResourceTest.kt index 7ddb650ba..f1ffa0e1a 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/csv/CsvResourceTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/io/csv/CsvResourceTest.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -165,9 +165,9 @@ internal class CsvResourceTest { } fun getCsvTestResource( - debugName: String = "unit-test", - columnDefinitions: Array>, - configuration: CsvConfig? = null + debugName: String = "unit-test", + columnDefinitions: Array>, + configuration: CsvConfig? = null ): CsvResource { return object : CsvResource(debugName) { override fun columnTranslations(d: List): Array> = columnDefinitions diff --git a/api/src/test/kotlin/edu/wgu/osmt/csv/OnetImportTest.kt b/api/src/test/kotlin/edu/wgu/osmt/io/csv/OnetImportTest.kt similarity index 98% rename from api/src/test/kotlin/edu/wgu/osmt/csv/OnetImportTest.kt rename to api/src/test/kotlin/edu/wgu/osmt/io/csv/OnetImportTest.kt index 90c13d2c6..322b9a7f9 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/csv/OnetImportTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/io/csv/OnetImportTest.kt @@ -1,4 +1,4 @@ -package edu.wgu.osmt.csv +package edu.wgu.osmt.io.csv import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset diff --git a/api/src/test/kotlin/edu/wgu/osmt/mockdata/MockData.kt b/api/src/test/kotlin/edu/wgu/osmt/mockdata/MockData.kt index 97d41f0be..95c6d6915 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/mockdata/MockData.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/mockdata/MockData.kt @@ -3,9 +3,9 @@ package edu.wgu.osmt.mockdata import com.fasterxml.jackson.dataformat.xml.XmlMapper import edu.wgu.osmt.collection.CollectionDoc import edu.wgu.osmt.config.AppConfig -import edu.wgu.osmt.csv.BlsJobCode -import edu.wgu.osmt.csv.OnetJobCode -import edu.wgu.osmt.csv.RichSkillRow +import edu.wgu.osmt.io.csv.BlsJobCode +import edu.wgu.osmt.io.csv.OnetJobCode +import edu.wgu.osmt.io.csv.RichSkillRow import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.jobcode.JobCode import edu.wgu.osmt.keyword.Keyword diff --git a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt index 67683e492..8aa176018 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt @@ -9,8 +9,8 @@ import edu.wgu.osmt.api.model.ApiFilteredSearch import edu.wgu.osmt.api.model.ApiSearch import edu.wgu.osmt.collection.CollectionEsRepo import edu.wgu.osmt.config.AppConfig -import edu.wgu.osmt.csv.BatchImportRichSkill -import edu.wgu.osmt.csv.RichSkillRow +import edu.wgu.osmt.io.csv.BatchImportRichSkill +import edu.wgu.osmt.io.csv.RichSkillRow import edu.wgu.osmt.jobcode.JobCodeEsRepo import edu.wgu.osmt.keyword.KeywordEsRepo import edu.wgu.osmt.mockdata.MockData diff --git a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillEsRepoTest.kt b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillEsRepoTest.kt index 3cccadc36..dd72c1a64 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillEsRepoTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillEsRepoTest.kt @@ -12,7 +12,7 @@ import edu.wgu.osmt.api.model.ApiSearch import edu.wgu.osmt.api.model.ApiSimilaritySearch import edu.wgu.osmt.collection.CollectionDoc import edu.wgu.osmt.collection.CollectionEsRepo -import edu.wgu.osmt.csv.BatchImportRichSkill +import edu.wgu.osmt.io.csv.BatchImportRichSkill import edu.wgu.osmt.db.ListFieldUpdate import edu.wgu.osmt.jobcode.JobCodeEsRepo import edu.wgu.osmt.keyword.KeywordEsRepo diff --git a/docs/int/osmt-v1.0.x-openapi3.yaml b/docs/int/osmt-v1.0.x-openapi3.yaml index d018e9302..fd8444156 100644 --- a/docs/int/osmt-v1.0.x-openapi3.yaml +++ b/docs/int/osmt-v1.0.x-openapi3.yaml @@ -681,6 +681,32 @@ paths: parameters: uuid: '$response.body#/uuid' + /api/collections/{uuid}/xlsx: + get: + tags: + - Collections + summary: Retrieve the skills in a collection as a XLSX (MS Excel) + description: Retrieve the skills that belong to the collection in XLSX (MS Excel) format + parameters: + - name: uuid + in: path + description: uuid of a collection + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/vnd.ms-excel: + schema: + $ref: '#/components/schemas/TaskResult' + links: + CollectionXlsxResults: + operationRef: '/api/results/media/{uuid}' + parameters: + uuid: '$response.body#/uuid' + /api/collections/{uuid}/updateSkills: post: tags: diff --git a/docs/int/osmt-v2.x-openapi3.yaml b/docs/int/osmt-v2.x-openapi3.yaml index 8ce85d4e2..ca577704b 100644 --- a/docs/int/osmt-v2.x-openapi3.yaml +++ b/docs/int/osmt-v2.x-openapi3.yaml @@ -739,6 +739,32 @@ paths: parameters: uuid: '$response.body#/uuid' + /api/collections/{uuid}/xlsx: + get: + tags: + - Collections + summary: Retrieve the skills in a collection as a XLSX (MS Excel) + description: Retrieve the skills that belong to the collection in XLSX (MS Excel) format + parameters: + - name: uuid + in: path + description: uuid of a collection + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/vnd.ms-excel: + schema: + $ref: '#/components/schemas/TaskResult' + links: + CollectionCsvResults: + operationRef: '/api/results/media/{uuid}' + parameters: + uuid: '$response.body#/uuid' + /api/collections/{uuid}/updateSkills: post: tags: From 8d93d41f6c3ecda95bebae9e1b49f550dd6523b7 Mon Sep 17 00:00:00 2001 From: manuel-delvillar <68391066+manuel-delvillar@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:43:12 -0600 Subject: [PATCH 2/5] Add support for menu in vertical-action-bar - Item in vertical-action-bar can display a menu if available. Add vertical-action-bar-item.component Update collection-public-action-bar - Add vertical-action-bar-item.component in collection-public-action-bar. Add download-csv Add visible argument in constructor Add horizontal action bar item Fix download on public skill Add drop up menu Add drop up menu to export rsd Add drop up in export selected. Remove unused code Remove empty constructor and ng on init. Position droup content --- ui/src/app/app.module.ts | 4 ++- .../collection-public-action-bar.component.ts | 20 +++++++++++++++ ...ublic-horizontal-action-bar.component.html | 13 ++-------- ...-public-vertical-action-bar.component.html | 12 ++------- .../detail/manage-collection.component.ts | 18 ++++++++++--- .../drop-up-menu/drop-up-menu.component.html | 6 +++++ .../drop-up-menu.component.spec.ts | 25 +++++++++++++++++++ .../drop-up-menu/drop-up-menu.component.ts | 14 +++++++++++ .../horizontal-action-bar-item.component.html | 19 ++++++++++++++ .../horizontal-action-bar-item.component.scss | 0 ...rizontal-action-bar-item.component.spec.ts | 25 +++++++++++++++++++ .../horizontal-action-bar-item.component.ts | 14 +++++++++++ ui/src/app/core/osmt-core.module.ts | 25 +++++++++++++++++++ .../vertical-action-bar-item.component.html | 20 +++++++++++++++ .../vertical-action-bar-item.component.scss | 15 +++++++++++ ...vertical-action-bar-item.component.spec.ts | 25 +++++++++++++++++++ .../vertical-action-bar-item.component.ts | 19 ++++++++++++++ .../core/vertical-action-bar.component.html | 19 +++----------- .../my-workspace/my-workspace.component.ts | 15 +++++++++-- .../navigation/libraryexport.component.html | 19 +++++++++----- .../navigation/libraryexport.component.scss | 4 +++ .../app/navigation/libraryexport.component.ts | 25 ++++++++++++++++++- ...skill-action-bar-horizontal.component.html | 13 ++-------- ...c-skill-action-bar-vertical.component.html | 10 ++------ .../public-rich-skill-action-bar.component.ts | 21 ++++++++++++++++ .../richskill/list/skills-list.component.ts | 15 +++++++++-- .../action-bar-item.components.scss | 14 ++--------- 27 files changed, 347 insertions(+), 82 deletions(-) create mode 100644 ui/src/app/core/drop-up-menu/drop-up-menu.component.html create mode 100644 ui/src/app/core/drop-up-menu/drop-up-menu.component.spec.ts create mode 100644 ui/src/app/core/drop-up-menu/drop-up-menu.component.ts create mode 100644 ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.html create mode 100644 ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.scss create mode 100644 ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.spec.ts create mode 100644 ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.ts create mode 100644 ui/src/app/core/osmt-core.module.ts create mode 100644 ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.html create mode 100644 ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.scss create mode 100644 ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.spec.ts create mode 100644 ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.ts create mode 100644 ui/src/app/navigation/libraryexport.component.scss diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 745219af9..6c398af44 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -94,7 +94,8 @@ import {MyWorkspaceComponent} from "./my-workspace/my-workspace.component" import {CollectionPipe} from "./pipes" import { ConvertToCollectionComponent } from "./my-workspace/convert-to-collection/convert-to-collection.component" import {SharedModule} from "@shared/shared.module" -import {OsmtFormModule} from "./form/osmt-form.module" +import { OsmtCoreModule } from "./core/osmt-core.module" +import { OsmtFormModule } from "./form/osmt-form.module" import { SizePaginationComponent } from "./table/skills-library-table/size-pagination/size-pagination.component" export function initializeApp( @@ -212,6 +213,7 @@ export function initializeApp( ReactiveFormsModule, CommonModule, SharedModule, + OsmtCoreModule, OsmtFormModule, FormsModule ], diff --git a/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.ts b/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.ts index 56286f0d4..eb9e082a4 100644 --- a/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.ts +++ b/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.ts @@ -7,6 +7,8 @@ import {formatDate} from "@angular/common" import {ITaskResult} from "../../../../task/ApiTaskResult" import { Observable } from "rxjs" import * as FileSaver from "file-saver"; +import {TableActionDefinition} from "../../../../table/skills-library-table/has-action-definitions" + @Component({ selector: "app-collection-public-action-bar", @@ -27,6 +29,23 @@ export class CollectionPublicActionBarComponent implements OnInit { taskUuidInProgress: string | undefined csvExport: string | undefined intervalHandle: number | undefined + action = new TableActionDefinition({ + label: "Download", + icon: this.downloadIcon, + menu: [ + { + label: "Download as CSV", + visible: () => true, + callback: () => this.onDownloadCsv(), + }, + { + label: "Download as XLSX", + visible: () => true, + callback: () => this.onDownloadCsv(), + } + ], + visible: () => true + }) constructor( protected router: Router, @@ -88,4 +107,5 @@ export class CollectionPublicActionBarComponent implements OnInit { this.toastService.showToast("Success!", "JSON copied to clipboard") }) } + } diff --git a/ui/src/app/collection/detail/collection-public/action-bar/horizontal/collection-public-horizontal-action-bar.component.html b/ui/src/app/collection/detail/collection-public/action-bar/horizontal/collection-public-horizontal-action-bar.component.html index 47b34a62c..e7b206b4f 100644 --- a/ui/src/app/collection/detail/collection-public/action-bar/horizontal/collection-public-horizontal-action-bar.component.html +++ b/ui/src/app/collection/detail/collection-public/action-bar/horizontal/collection-public-horizontal-action-bar.component.html @@ -16,17 +16,8 @@ - + + - - - + + + + + diff --git a/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.scss b/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.spec.ts b/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.spec.ts new file mode 100644 index 000000000..0c1920c56 --- /dev/null +++ b/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" + +import { HorizontalActionBarItemComponent } from "./horizontal-action-bar-item.component" + +describe("HorizontalActionBarItemComponent", () => { + let component: HorizontalActionBarItemComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HorizontalActionBarItemComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(HorizontalActionBarItemComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.ts b/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.ts new file mode 100644 index 000000000..1bbad762e --- /dev/null +++ b/ui/src/app/core/horizontal-action-bar-item/horizontal-action-bar-item.component.ts @@ -0,0 +1,14 @@ +import {Component, Input, OnInit} from "@angular/core" +import {TableActionDefinition} from "../../table/skills-library-table/has-action-definitions" + +@Component({ + selector: "app-horizontal-action-bar-item", + templateUrl: "./horizontal-action-bar-item.component.html", + styleUrls: ["./horizontal-action-bar-item.component.scss", "../../table/skills-library-table/action-bar-item.components.scss"] +}) +export class HorizontalActionBarItemComponent { + + @Input() + action?: TableActionDefinition + +} diff --git a/ui/src/app/core/osmt-core.module.ts b/ui/src/app/core/osmt-core.module.ts new file mode 100644 index 000000000..8d9f2756b --- /dev/null +++ b/ui/src/app/core/osmt-core.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core" +import { CommonModule } from "@angular/common" + +import { VerticalActionBarItemComponent } from "./vertical-action-bar-item/vertical-action-bar-item.component" +import { HorizontalActionBarItemComponent } from "./horizontal-action-bar-item/horizontal-action-bar-item.component" +import { DropUpMenuComponent } from "./drop-up-menu/drop-up-menu.component" + + + +@NgModule({ + declarations: [ + VerticalActionBarItemComponent, + HorizontalActionBarItemComponent, + DropUpMenuComponent + ], + imports: [ + CommonModule + ], + exports: [ + VerticalActionBarItemComponent, + HorizontalActionBarItemComponent, + DropUpMenuComponent + ] +}) +export class OsmtCoreModule { } diff --git a/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.html b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.html new file mode 100644 index 000000000..972e33523 --- /dev/null +++ b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.html @@ -0,0 +1,20 @@ +
+ + + +
diff --git a/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.scss b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.scss new file mode 100644 index 000000000..30b6beaaa --- /dev/null +++ b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.scss @@ -0,0 +1,15 @@ +.dropup { + position: inherit; + display: inherit; + width: 100%; +} + +.dropup-content { + margin-left: 110px!important; + margin-bottom: -90px!important; + padding-right: 20px; +} + +hr { + width: 100%; +} diff --git a/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.spec.ts b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.spec.ts new file mode 100644 index 000000000..e80bc171d --- /dev/null +++ b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" + +import { VerticalActionBarItemComponent } from "./vertical-action-bar-item.component" + +describe("ActionBarItemComponent", () => { + let component: VerticalActionBarItemComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ VerticalActionBarItemComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(VerticalActionBarItemComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.ts b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.ts new file mode 100644 index 000000000..f7e001c4a --- /dev/null +++ b/ui/src/app/core/vertical-action-bar-item/vertical-action-bar-item.component.ts @@ -0,0 +1,19 @@ +import {Component, Input} from "@angular/core" +import {TableActionDefinition} from "../../table/skills-library-table/has-action-definitions" + +@Component({ + selector: "app-vertical-action-bar-item", + templateUrl: "./vertical-action-bar-item.component.html", + styleUrls: [ + "./vertical-action-bar-item.component.scss", + "../../table/skills-library-table/action-bar-item.components.scss" + ] +}) +export class VerticalActionBarItemComponent { + + @Input() + action?: TableActionDefinition + @Input() + data?: any + +} diff --git a/ui/src/app/core/vertical-action-bar.component.html b/ui/src/app/core/vertical-action-bar.component.html index 94b08b958..aa2f72cfe 100644 --- a/ui/src/app/core/vertical-action-bar.component.html +++ b/ui/src/app/core/vertical-action-bar.component.html @@ -1,19 +1,9 @@
- +
+ + +
@@ -36,4 +26,3 @@
- diff --git a/ui/src/app/my-workspace/my-workspace.component.ts b/ui/src/app/my-workspace/my-workspace.component.ts index 45196cd2e..a6d78346b 100644 --- a/ui/src/app/my-workspace/my-workspace.component.ts +++ b/ui/src/app/my-workspace/my-workspace.component.ts @@ -54,9 +54,20 @@ export class MyWorkspaceComponent extends ManageCollectionComponent implements O visible: () => true }), new TableActionDefinition({ - label: "Download as CSV", + label: "Download", icon: this.downloadIcon, - callback: () => this.generateCsv(this.collection?.name ?? ""), + menu: [ + { + label: "Download as CSV", + visible: () => true, + callback: () => this.generateCsv(this.collection?.name ?? ""), + }, + { + label: "Download as XLSX", + visible: () => true, + callback: () => this.generateCsv(this.collection?.name ?? ""), + } + ], visible: () => !this.workspaceEmpty() }), new TableActionDefinition({ diff --git a/ui/src/app/navigation/libraryexport.component.html b/ui/src/app/navigation/libraryexport.component.html index c840b94db..976ab9488 100644 --- a/ui/src/app/navigation/libraryexport.component.html +++ b/ui/src/app/navigation/libraryexport.component.html @@ -1,6 +1,13 @@ - +
+ + + +
diff --git a/ui/src/app/navigation/libraryexport.component.scss b/ui/src/app/navigation/libraryexport.component.scss new file mode 100644 index 000000000..a069bae70 --- /dev/null +++ b/ui/src/app/navigation/libraryexport.component.scss @@ -0,0 +1,4 @@ +.dropup:hover .dropup-content.dropup-content-visible { + margin-left: 20px; + margin-bottom: -133px; +} diff --git a/ui/src/app/navigation/libraryexport.component.ts b/ui/src/app/navigation/libraryexport.component.ts index 29ac42841..c7fff44bb 100644 --- a/ui/src/app/navigation/libraryexport.component.ts +++ b/ui/src/app/navigation/libraryexport.component.ts @@ -9,10 +9,15 @@ import {SvgHelper, SvgIcon} from "../core/SvgHelper" import {AbstractSearchComponent} from "./abstract-search.component" import {ApiTaskResult} from "../task/ApiTaskResult" import {ToastService} from "../toast/toast.service" +import {TableActionDefinition} from "../table/skills-library-table/has-action-definitions" @Component({ selector: "app-libraryexport", - templateUrl: "./libraryexport.component.html" + templateUrl: "./libraryexport.component.html", + styleUrls: [ + "../table/skills-library-table/action-bar-item.components.scss", + "./libraryexport.component.scss", + ] }) export class LibraryExportComponent extends AbstractSearchComponent implements OnInit { @@ -52,4 +57,22 @@ export class LibraryExportComponent extends AbstractSearchComponent implements O FileSaver.saveAs(blob, `RSD Library - OSMT ${date}.csv`) } + get action(): TableActionDefinition { + return new TableActionDefinition({ + menu: [ + { + label: "Download as CSV", + visible: () => true, + callback: () => this.onDownloadLibrary(), + }, + { + label: "Download as XLSX", + visible: () => true, + callback: () => this.onDownloadLibrary(), + } + ], + visible: () => true + }) + } + } diff --git a/ui/src/app/richskill/detail/rich-skill-public/action-bar/action-bar-horizontal/public-skill-action-bar-horizontal.component.html b/ui/src/app/richskill/detail/rich-skill-public/action-bar/action-bar-horizontal/public-skill-action-bar-horizontal.component.html index 0220002f8..bd44c8c8c 100644 --- a/ui/src/app/richskill/detail/rich-skill-public/action-bar/action-bar-horizontal/public-skill-action-bar-horizontal.component.html +++ b/ui/src/app/richskill/detail/rich-skill-public/action-bar/action-bar-horizontal/public-skill-action-bar-horizontal.component.html @@ -16,17 +16,8 @@ - + + - + +