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..72cac367a 100644
--- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt
+++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt
@@ -6,6 +6,8 @@ object RoutePaths {
const val EXPORT = "$API/export"
const val SEARCH_SKILLS = "$SEARCH_PATH/skills"
const val EXPORT_LIBRARY = "$EXPORT/library"
+ const val EXPORT_LIBRARY_CSV = "$EXPORT_LIBRARY/csv"
+ const val EXPORT_LIBRARY_XLSX = "$EXPORT_LIBRARY/xlsx"
const val SEARCH_SIMILAR_SKILLS = "$SEARCH_SKILLS/similarity"
const val SEARCH_SIMILARITIES = "$SEARCH_SKILLS/similarities"
const val SEARCH_COLLECTIONS = "$SEARCH_PATH/collections"
@@ -16,9 +18,12 @@ object RoutePaths {
const val SKILLS_FILTER = "$SKILLS_PATH/filter"
const val SKILL_PUBLISH = "$SKILLS_PATH/publish"
const val SKILL_DETAIL = "$SKILLS_PATH/{uuid}"
+ const val SKILL_DETAIL_XLSX = "$SKILLS_PATH/{uuid}/xlsx"
const val SKILL_UPDATE = "$SKILL_DETAIL/update"
const val SKILL_AUDIT_LOG = "$SKILL_DETAIL/log"
const val EXPORT_SKILLS = "$EXPORT/skills"
+ const val EXPORT_SKILLS_CSV = "$EXPORT_SKILLS/csv"
+ const val EXPORT_SKILLS_XLSX = "$EXPORT_SKILLS/xlsx"
const val COLLECTIONS_PATH = "$API/collections"
@@ -31,12 +36,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 93%
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..30639fd62 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
@@ -48,13 +48,13 @@ class CsvColumnBuilder {
}
class CsvConfigBuilder {
- var delimeter: Char = CsvConfig.delimeter
+ var delimiter: Char = CsvConfig.delimiter
var quoteChar: Char = CsvConfig.quoteChar
var escapeChar: Char = CsvConfig.escapeChar
var lineEnd: String = CsvConfig.lineEnd
var includeHeader: Boolean = CsvConfig.includeHeader
fun build(): CsvConfig {
- return CsvConfig(delimeter, quoteChar, escapeChar, lineEnd, includeHeader)
+ return CsvConfig(delimiter, quoteChar, escapeChar, lineEnd, includeHeader)
}
}
\ No newline at end of file
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 79%
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..99317b246 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
@@ -40,7 +42,7 @@ abstract class CsvResource(val debugName: String) {
val config = configureCsv()
return CSVWriter(writer,
- config.delimeter,
+ config.delimiter,
config.quoteChar,
config.escapeChar,
config.lineEnd
@@ -79,20 +81,20 @@ 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 delimiter: Char = Defaults.delimiter,
+ 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
- val delimeter: Char = CSVWriter.DEFAULT_SEPARATOR
+ companion object Defaults { // Allows the default values to be shared with its builder
+ val delimiter: Char = CSVWriter.DEFAULT_SEPARATOR
val quoteChar: Char = CSVWriter.DEFAULT_QUOTE_CHARACTER
val escapeChar: Char = CSVWriter.DEFAULT_ESCAPE_CHARACTER
val lineEnd: String = CSVWriter.DEFAULT_LINE_END
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 57%
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..b8f8847a3 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,41 +18,28 @@ 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)
?.map { RichSkillAndCollections.fromDao(it) }
?.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,18 +48,17 @@ 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)
?.map { RichSkillAndCollections.fromDao(it) }
?.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 72%
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..ac901c0c0 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
@@ -15,18 +14,18 @@ class RichSkillCsvExport(
val columns = arrayOf(
CsvColumn("Canonical URL") { it.rs.canonicalUrl(appConfig.baseUrl) },
CsvColumn("RSD Name") { it.rs.name },
- CsvColumn("Authors") { it.rs.authors.map { author -> author.value ?: "" }.joinToString(listDelimiter) },
+ CsvColumn("Authors") { it.rs.authors.joinToString(listDelimiter) { author -> author.value ?: "" } },
CsvColumn("Skill Statement") { it.rs.statement },
- CsvColumn("Categories") { it.rs.categories.map{ category -> category.value ?: "" }.joinToString(listDelimiter) },
- CsvColumn("Keywords") { it.rs.searchingKeywords.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) },
- CsvColumn("Standards") { it.rs.standards.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) },
- CsvColumn("Certifications") { it.rs.certifications.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) },
+ CsvColumn("Categories") { it.rs.categories.joinToString(listDelimiter) { category -> category.value ?: "" } },
+ CsvColumn("Keywords") { it.rs.searchingKeywords.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
+ CsvColumn("Standards") { it.rs.standards.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
+ CsvColumn("Certifications") { it.rs.certifications.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
CsvColumn("Occupation Major Groups") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::majorCode) },
CsvColumn("Occupation Minor Groups") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::minorCode) },
CsvColumn("Broad Occupations") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::broadCode) },
CsvColumn("Detailed Occupations") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::detailedCode) },
CsvColumn("O*Net Job Codes") { prepareJobCodePart(it.rs.jobCodes, JobCodeBreakout::jobRoleCode) },
- CsvColumn("Employers") { it.rs.employers.map { keyword -> keyword.value ?: "" }.joinToString(listDelimiter) }
+ CsvColumn("Employers") { it.rs.employers.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } }
)
val alignmentCount = data.map { s -> s.rs.alignments.size }.maxOrNull() ?: 0
val alignmentColumns = (0 until alignmentCount).flatMap { i ->
diff --git a/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/ExportSkillToXlsxTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/ExportSkillToXlsxTaskProcessor.kt
new file mode 100644
index 000000000..7a2b6c3f8
--- /dev/null
+++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/ExportSkillToXlsxTaskProcessor.kt
@@ -0,0 +1,51 @@
+package edu.wgu.osmt.io.xlsx
+
+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.ExportSkillsToXlsxTask
+import edu.wgu.osmt.task.TaskMessageService
+import edu.wgu.osmt.task.TaskStatus
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Profile
+import org.springframework.stereotype.Component
+import org.springframework.transaction.annotation.Transactional
+
+@Component
+@Profile("apiserver")
+@Transactional
+class ExportSkillToXlsxTaskProcessor {
+ val logger: Logger = LoggerFactory.getLogger(ExportSkillToXlsxTaskProcessor::class.java)
+
+ @Autowired
+ lateinit var taskMessageService: TaskMessageService
+
+ @Autowired
+ lateinit var richSkillRepository: RichSkillRepository
+
+ @Autowired
+ lateinit var appConfig: AppConfig
+
+ @RqueueListener(
+ value = [TaskMessageService.skillsForCustomListExportXlsx],
+ deadLetterQueueListenerEnabled = "true",
+ deadLetterQueue = TaskMessageService.deadLetters,
+ concurrency = "1"
+ )
+ fun xlsxSkillsInCustomRsdListProcessor(task: ExportSkillsToXlsxTask) {
+ logger.info("Started processing task for Custom RSD List .xlsx export")
+
+ val xlsx = task.uuids?.map { richSkillRepository.findByUUID(it) }
+ ?.map { RichSkillAndCollections.fromDao(it!!) }
+ ?.let { RichSkillXlsxExport(appConfig).toXlsx(it) }
+
+ taskMessageService.publishResult(
+ task.copy(result = xlsx, status = TaskStatus.Ready)
+ )
+ logger.info("Custom RSD List .xlsx export task completed")
+ }
+
+}
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..186cd884e
--- /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("Authors") { it.rs.authors.joinToString(listDelimiter) { author -> author.value ?: "" } },
+ XlsxColumn("Skill Statement") { it.rs.statement },
+ XlsxColumn("Categories") { it.rs.categories.joinToString(listDelimiter) { category -> category.value ?: "" } },
+ XlsxColumn("Keywords") { it.rs.searchingKeywords.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
+ XlsxColumn("Standards") { it.rs.standards.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
+ XlsxColumn("Certifications") { it.rs.certifications.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } },
+ 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.joinToString(listDelimiter) { keyword -> keyword.value ?: "" } }
+ )
+ 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..e5d8272b2
--- /dev/null
+++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxResource.kt
@@ -0,0 +1,72 @@
+package edu.wgu.osmt.io.xlsx
+
+import edu.wgu.osmt.io.common.TabColumn
+import edu.wgu.osmt.io.common.TabularResource
+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
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..c9117fc75
--- /dev/null
+++ b/api/src/main/kotlin/edu/wgu/osmt/io/xlsx/XlsxTaskProcessor.kt
@@ -0,0 +1,63 @@
+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() }
+ ?.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()
+ ?.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/jobcode/JobCodeBreakout.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeBreakout.kt
index 52ed509ee..1ae530471 100644
--- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeBreakout.kt
+++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeBreakout.kt
@@ -4,28 +4,28 @@ package edu.wgu.osmt.jobcode
// formats: x-xxxx.xx and xx.xxxx.xx which means that a little extra processing needs to be done.
object JobCodeBreakout {
- private val codePartDelimeter = "[-.]".toRegex() // used to split code on either hyphen (-) or period (.)
+ private val codePartDelimiter = "[-.]".toRegex() // used to split code on either hyphen (-) or period (.)
- private fun majorPart(code: String): String? = code.split(codePartDelimeter)
+ private fun majorPart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.isNotEmpty() }
?.let { it[0] }
?.takeIf { it.toIntOrNull() != null }
- private fun minorPart(code: String): String? = code.split(codePartDelimeter)
+ private fun minorPart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.size > 1 }
?.let { it[1] }
?.takeIf { it.length >= 2 }
?.substring(0, 2)
?.takeIf { it.toIntOrNull() != null }
- private fun minorLeadingPart(code: String): String? = code.split(codePartDelimeter)
+ private fun minorLeadingPart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.size > 1 }
?.let { it[1] }
?.takeIf { it.length >= 2 }
?.substring(0, 1)
?.takeIf { it.toIntOrNull() != null }
- private fun broadPart(code: String): String? = code.split(codePartDelimeter)
+ private fun broadPart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.size > 1 }
?.let { it[1] }
?.takeIf { it.length >= 3 }
@@ -33,14 +33,14 @@ object JobCodeBreakout {
?.takeIf { it.toIntOrNull() != null }
- private fun detailedPart(code: String): String? = code.split(codePartDelimeter)
+ private fun detailedPart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.size > 1 }
?.let { it[1] }
?.takeIf { it.length >= 4 }
?.substring(3, 4)
?.takeIf { it.toIntOrNull() != null }
- private fun jobRolePart(code: String): String? = code.split(codePartDelimeter)
+ private fun jobRolePart(code: String): String? = code.split(codePartDelimiter)
.takeIf { it.size > 2 }
?.let { it[2] }
?.takeIf { it.toIntOrNull() != null }
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..660539ce7 100644
--- a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt
+++ b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt
@@ -16,16 +16,20 @@ import edu.wgu.osmt.config.AppConfig
import edu.wgu.osmt.db.PublishStatus
import edu.wgu.osmt.elasticsearch.OffsetPageable
import edu.wgu.osmt.elasticsearch.PaginatedLinks
+import edu.wgu.osmt.io.csv.RichSkillCsvExport
+import edu.wgu.osmt.io.xlsx.RichSkillXlsxExport
import edu.wgu.osmt.keyword.KeywordDao
import edu.wgu.osmt.security.OAuthHelper
import edu.wgu.osmt.task.AppliesToType
import edu.wgu.osmt.task.CreateSkillsTask
import edu.wgu.osmt.task.CsvTask
import edu.wgu.osmt.task.ExportSkillsToCsvTask
+import edu.wgu.osmt.task.ExportSkillsToXlsxTask
import edu.wgu.osmt.task.PublishTask
import edu.wgu.osmt.task.Task
import edu.wgu.osmt.task.TaskMessageService
import edu.wgu.osmt.task.TaskResult
+import edu.wgu.osmt.task.XlsxTask
import org.apache.commons.lang3.StringUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity
@@ -251,9 +255,9 @@ class RichSkillController @Autowired constructor(
}
@Transactional(readOnly = true)
- @GetMapping(RoutePaths.EXPORT_LIBRARY, produces = [MediaType.APPLICATION_JSON_VALUE])
+ @GetMapping(RoutePaths.EXPORT_LIBRARY_CSV, produces = [MediaType.APPLICATION_JSON_VALUE])
@ResponseBody
- fun exportLibrary(
+ fun exportLibraryCsv(
@AuthenticationPrincipal user: Jwt?
): HttpEntity {
if (!appConfig.allowPublicSearching && user === null) {
@@ -270,9 +274,28 @@ class RichSkillController @Autowired constructor(
}
@Transactional(readOnly = true)
- @PostMapping(RoutePaths.EXPORT_SKILLS, produces = [MediaType.APPLICATION_JSON_VALUE])
+ @GetMapping(RoutePaths.EXPORT_LIBRARY_XLSX, produces = [MediaType.APPLICATION_JSON_VALUE])
@ResponseBody
- fun exportCustomList(
+ fun exportLibraryXlsx(
+ @AuthenticationPrincipal user: Jwt?
+ ): HttpEntity {
+ if (!appConfig.allowPublicSearching && user === null) {
+ throw GeneralApiException("Unauthorized", HttpStatus.UNAUTHORIZED)
+ }
+ if (!oAuthHelper.hasRole(appConfig.roleAdmin)) {
+ throw GeneralApiException("OSMT user must have an Admin role.", HttpStatus.UNAUTHORIZED)
+ }
+
+ val task = XlsxTask(collectionUuid = "FullLibrary")
+ taskMessageService.enqueueJob(TaskMessageService.skillsForFullLibraryXlsx, task)
+
+ return Task.processingResponse(task)
+ }
+
+ @Transactional(readOnly = true)
+ @PostMapping(RoutePaths.EXPORT_SKILLS_CSV, produces = [MediaType.APPLICATION_JSON_VALUE])
+ @ResponseBody
+ fun exportCustomListCsv(
@RequestBody uuids: List?,
@AuthenticationPrincipal user: Jwt?
): HttpEntity {
@@ -285,4 +308,21 @@ class RichSkillController @Autowired constructor(
return Task.processingResponse(task)
}
+
+ @Transactional(readOnly = true)
+ @PostMapping(RoutePaths.EXPORT_SKILLS_XLSX, produces = [MediaType.APPLICATION_JSON_VALUE])
+ @ResponseBody
+ fun exportCustomListXlsx(
+ @RequestBody uuids: List?,
+ @AuthenticationPrincipal user: Jwt?
+ ): HttpEntity {
+ if (!appConfig.allowPublicSearching && user === null) {
+ throw GeneralApiException("Unauthorized", HttpStatus.UNAUTHORIZED)
+ }
+
+ val task = ExportSkillsToXlsxTask(collectionUuid = "CustomList", uuids)
+ taskMessageService.enqueueJob(TaskMessageService.skillsForCustomListExportXlsx, task)
+
+ return Task.processingResponse(task)
+ }
}
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..95b106eaf 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 excel 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..39573e7af 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"),
@@ -28,6 +29,7 @@ import java.util.*
JsonSubTypes.Type(value = UpdateCollectionSkillsTask::class, name = "UpdateCollectionSkillsTask"),
JsonSubTypes.Type(value = CreateSkillsTask::class, name = "CreateSkillsTask"),
JsonSubTypes.Type(value = ExportSkillsToCsvTask::class, name = "ExportSkillsToCsvTask"),
+ JsonSubTypes.Type(value = ExportSkillsToXlsxTask::class, name = "ExportSkillsToXlsxTask"),
JsonSubTypes.Type(value = RemoveCollectionSkillsTask::class, name = "RemoveCollectionSkillsTask")
)
@@ -67,7 +69,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(
@@ -82,6 +94,18 @@ data class ExportSkillsToCsvTask(
override val apiResultPath = RoutePaths.TASK_DETAIL_BATCH
}
+data class ExportSkillsToXlsxTask(
+ val collectionUuid: String = "",
+ val uuids: List? = null,
+ 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_BATCH
+}
+
data class CreateSkillsTask(
val apiSkillUpdates: List = listOf(),
val userString: String = "",
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..f8ac08e5d 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,11 @@ 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 skillsForCustomListExportCsv = "custom-rsd-list-export"
+ const val skillsForFullLibraryXlsx = "full-library-skills-xlsx-process"
+ const val skillsForCustomListExportCsv = "custom-rsd-list-csv-export"
+ const val skillsForCustomListExportXlsx = "custom-rsd-list-xlsx-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/io/xlsx/RichSkillXlsxExportTest.kt b/api/src/test/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExportTest.kt
new file mode 100644
index 000000000..8f81d7601
--- /dev/null
+++ b/api/src/test/kotlin/edu/wgu/osmt/io/xlsx/RichSkillXlsxExportTest.kt
@@ -0,0 +1,76 @@
+package edu.wgu.osmt.io.xlsx
+
+import edu.wgu.osmt.SpringTest
+import edu.wgu.osmt.collection.Collection
+import edu.wgu.osmt.config.AppConfig
+import edu.wgu.osmt.db.PublishStatus
+import edu.wgu.osmt.keyword.Keyword
+import edu.wgu.osmt.keyword.KeywordTypeEnum
+import edu.wgu.osmt.richskill.RichSkillAndCollections
+import edu.wgu.osmt.richskill.RichSkillDescriptor
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import java.time.LocalDateTime
+import java.util.UUID
+
+internal class RichSkillXlsxExportTest @Autowired constructor(
+ appConfig: AppConfig
+) : SpringTest() {
+
+ val richSkillXlsExport = RichSkillXlsxExport(appConfig)
+
+ val collection = Collection(
+ id = 123,
+ creationDate = LocalDateTime.now(),
+ name = "name",
+ status = PublishStatus.Draft,
+ updateDate = LocalDateTime.now(),
+ uuid = UUID.randomUUID().toString()
+ )
+
+ @Test
+ fun`columnTranslations() should retrieve only 14 columns if no Alignments are present`() {
+ //Arrange
+ val rsd = RichSkillDescriptor(
+ id = 123,
+ creationDate = LocalDateTime.now(),
+ name = "name",
+ statement = "statement",
+ updateDate = LocalDateTime.now(),
+ uuid = UUID.randomUUID().toString()
+ )
+
+ //Act
+ val result = richSkillXlsExport.columnTranslations(listOf(RichSkillAndCollections(rsd, setOf(collection))))
+
+ //Assert
+ Assertions.assertThat(result).hasSize(14)
+ }
+
+ @Test
+ fun`columnTranslations() should retrieve 17 columns if Alignments are present`() {
+ //Arrange
+ val rsd = RichSkillDescriptor(
+ id = 123,
+ creationDate = LocalDateTime.now(),
+ keywords = listOf(
+ Keyword(
+ id = 123,
+ creationDate = LocalDateTime.now(),
+ type = KeywordTypeEnum.Alignment,
+ updateDate = LocalDateTime.now()
+ )),
+ name = "name",
+ statement = "statement",
+ updateDate = LocalDateTime.now(),
+ uuid = UUID.randomUUID().toString()
+ )
+
+ //Act
+ val result = richSkillXlsExport.columnTranslations(listOf(RichSkillAndCollections(rsd, setOf(collection))))
+
+ //Assert
+ Assertions.assertThat(result).hasSize(17)
+ }
+}
\ No newline at end of file
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..31b257c6e 100644
--- a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt
+++ b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt
@@ -3,14 +3,14 @@ package edu.wgu.osmt.richskill
import edu.wgu.osmt.BaseDockerizedTest
import edu.wgu.osmt.HasDatabaseReset
import edu.wgu.osmt.HasElasticsearchReset
-import edu.wgu.osmt.RoutePaths.EXPORT_LIBRARY
+import edu.wgu.osmt.RoutePaths.EXPORT_LIBRARY_CSV
import edu.wgu.osmt.SpringTest
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
@@ -252,7 +252,7 @@ internal class RichSkillControllerTest @Autowired constructor(
val headers : MutableMap = HashMap()
headers["key"] = "value"
val notNullJwt : Jwt? = Jwt("tokenValue", Instant.MIN, Instant.MAX,headers,headers)
- val csvTaskResult = TaskResult(UUID.randomUUID().toString(),MediaType.APPLICATION_JSON_VALUE,TaskStatus.Processing, EXPORT_LIBRARY)
+ val csvTaskResult = TaskResult(UUID.randomUUID().toString(),MediaType.APPLICATION_JSON_VALUE,TaskStatus.Processing, EXPORT_LIBRARY_CSV)
val service = mockk()
@@ -261,7 +261,7 @@ internal class RichSkillControllerTest @Autowired constructor(
mockkStatic(TaskResult::class)
every { Task.processingResponse(any()) } returns HttpEntity(csvTaskResult)
- val result = richSkillController.exportLibrary(user = notNullJwt)
+ val result = richSkillController.exportLibraryCsv(user = notNullJwt)
assertThat(result.body?.uuid).isNotBlank()
}
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:
diff --git a/ui/karma.ci.conf.js b/ui/karma.ci.conf.js
index 8e4d57219..0f9c67583 100644
--- a/ui/karma.ci.conf.js
+++ b/ui/karma.ci.conf.js
@@ -13,7 +13,12 @@ module.exports = function(config) {
require('karma-sonarqube-unit-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
- files: ['src/app/**/*.spec.ts'],
+ files: [
+ {
+ pattern: 'src/app/**/*.spec.ts',
+ type: 'js'
+ }
+ ],
reporters: ['progress', 'sonarqubeUnit', 'coverage'],
port: 9876, // karma web server port
colors: true,
diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts
index 745219af9..a0045af9b 100644
--- a/ui/src/app/app.module.ts
+++ b/ui/src/app/app.module.ts
@@ -92,9 +92,12 @@ import {LabelWithSelectComponent} from "./table/skills-library-table/label-with-
import {LibraryExportComponent} from "./navigation/libraryexport.component"
import {MyWorkspaceComponent} from "./my-workspace/my-workspace.component"
import {CollectionPipe} from "./pipes"
+import { SharedModule } from "@shared/shared.module"
+import { OsmtCoreModule } from "./core/osmt-core.module"
+import { ExportCollectionComponent } from "./export/export-collection.component"
+import { ExportRsdComponent } from "./export/export-rsd.component"
+import { OsmtFormModule } from "./form/osmt-form.module"
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 { SizePaginationComponent } from "./table/skills-library-table/size-pagination/size-pagination.component"
export function initializeApp(
@@ -149,6 +152,8 @@ export function initializeApp(
CollectionsListComponent,
CollectionSearchResultsComponent,
PublishCollectionComponent,
+ ExportCollectionComponent,
+ ExportRsdComponent,
DetailCardComponent,
DetailCardSectionComponent,
@@ -212,6 +217,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.spec.ts b/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.spec.ts
index 45407faf5..1aa87a616 100644
--- a/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.spec.ts
+++ b/ui/src/app/collection/detail/collection-public/action-bar/collection-public-action-bar.component.spec.ts
@@ -1,8 +1,9 @@
import { Component, Type } from "@angular/core"
-import { async, ComponentFixture, TestBed } from "@angular/core/testing"
+import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"
import { By } from "@angular/platform-browser"
import { Router } from "@angular/router"
import * as FileSaver from "file-saver"
+import { ExportCollectionComponent } from "src/app/export/export-collection.component"
import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec"
import { createMockTaskResult } from "../../../../../../test/resource/mock-data"
import { CollectionServiceStub } from "../../../../../../test/resource/mock-stubs"
@@ -31,30 +32,30 @@ class TestHostComponent {
export function createComponent(T: Type): Promise {
- hostFixture = TestBed.createComponent(T)
- hostComponent = hostFixture.componentInstance
+ hostFixture = TestBed.createComponent(T);
+ hostComponent = hostFixture.componentInstance;
- const debugEl = hostFixture.debugElement.query(By.directive(CollectionPublicActionBarComponent))
- childComponent = debugEl.componentInstance
+ const debugEl = hostFixture.debugElement.query(By.directive(CollectionPublicActionBarComponent));
+ childComponent = debugEl.componentInstance;
// 1st change detection triggers ngOnInit which gets a hero
- hostFixture.detectChanges()
+ hostFixture.detectChanges();
return hostFixture.whenStable().then(() => {
// 2nd change detection displays the async-fetched hero
- hostFixture.detectChanges()
- })
+ hostFixture.detectChanges();
+ });
}
-let hostFixture: ComponentFixture
-let hostComponent: TestHostComponent
-let childComponent: CollectionPublicActionBarComponent
+let hostFixture: ComponentFixture;
+let hostComponent: TestHostComponent;
+let childComponent: CollectionPublicActionBarComponent;
describe("CollectionPublicActionBarComponent", () => {
- beforeEach(async(() => {
- const routerSpy = ActivatedRouteStubSpec.createRouterSpy()
+ beforeEach(waitForAsync(() => {
+ const routerSpy = ActivatedRouteStubSpec.createRouterSpy();
TestBed.configureTestingModule({
declarations: [
@@ -64,91 +65,41 @@ describe("CollectionPublicActionBarComponent", () => {
providers: [
ToastService,
{ provide: CollectionService, useClass: CollectionServiceStub },
- { provide: Router, useValue: routerSpy }
+ { provide: Router, useValue: routerSpy },
+ ExportCollectionComponent
]
})
- .compileComponents()
+ .compileComponents();
- spyOn(FileSaver, "saveAs").and.stub()
+ spyOn(FileSaver, "saveAs").and.stub();
- createComponent(TestHostComponent)
- }))
+ createComponent(TestHostComponent);
+ }));
it("should be created", () => {
- expect(hostComponent).toBeTruthy()
- expect(childComponent).toBeTruthy()
- })
-
- it("pollCsv should return", (done) => {
- // Arrange
- childComponent.taskUuidInProgress = "123"
- childComponent.intervalHandle = 1
-
- // Act
- childComponent.pollCsv()
-
- // Assert
- /* Delay the handling to give time for the async method to complete. */
- setTimeout(() => {
- expect(childComponent.taskUuidInProgress).toBeFalsy()
- expect(FileSaver.saveAs).toHaveBeenCalled()
- done()
- }, 2000)
- })
- it("pollCsv should fail", (done) => {
- // Arrange
- childComponent.taskUuidInProgress = undefined
- childComponent.intervalHandle = 1
-
- // Act
- childComponent.pollCsv()
-
- // Assert
- /* Delay the handling to give time for the async method to complete. */
- setTimeout(() => {
- expect(FileSaver.saveAs).not.toHaveBeenCalled()
- done()
- }, 2000)
- })
+ expect(hostComponent).toBeTruthy();
+ expect(childComponent).toBeTruthy();
+ });
it("onCopyURL should return", () => {
// Arrange
- const element: HTMLTextAreaElement = document.createElement("TextArea") as HTMLTextAreaElement
+ const element: HTMLTextAreaElement = document.createElement("TextArea") as HTMLTextAreaElement;
// Act
- childComponent.onCopyURL(element)
+ childComponent.onCopyURL(element);
// Assert
/* Nothing to check */
- })
+ });
it("onCopyJSON should return", () => {
// Arrange
- const element: HTMLTextAreaElement = document.createElement("TextArea") as HTMLTextAreaElement
+ const element: HTMLTextAreaElement = document.createElement("TextArea") as HTMLTextAreaElement;
// Act
- childComponent.onCopyJSON(element)
+ childComponent.onCopyJSON(element);
// Assert
/* Nothing to check */
})
-
- it("onDownloadCsv should return", (done) => {
- // Arrange
- const task = createMockTaskResult()
- childComponent.collectionUuid = "myUUID"
- childComponent.taskUuidInProgress = undefined
-
- // Act
- childComponent.onDownloadCsv()
- const result = childComponent.taskUuidInProgress
-
- // Assert
- /* Delay the handling to give time for the async method to complete. */
- setTimeout(() => {
- expect(result === task.uuid).toBeTrue() // Due to conditional types, cannot use .toEqual()
- expect(FileSaver.saveAs).toHaveBeenCalled()
- done()
- }, 2000)
- })
-})
+});
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..6d6cacc71 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
@@ -1,12 +1,14 @@
-import {Component, Inject, Input, LOCALE_ID, OnInit} from "@angular/core"
-import {CollectionService} from "../../../service/collection.service"
-import {Router} from "@angular/router"
-import {ToastService} from "../../../../toast/toast.service"
-import {SvgHelper, SvgIcon} from "../../../../core/SvgHelper"
-import {formatDate} from "@angular/common"
-import {ITaskResult} from "../../../../task/ApiTaskResult"
+import { Component, Inject, Input, LOCALE_ID, OnInit } from "@angular/core"
+import { Router } from "@angular/router"
+
import { Observable } from "rxjs"
-import * as FileSaver from "file-saver";
+
+import { SvgHelper, SvgIcon } from "../../../../core/SvgHelper"
+import { ExportCollectionComponent } from "../../../../export/export-collection.component"
+import { CollectionService } from "../../../service/collection.service"
+import { TableActionDefinition } from "../../../../table/skills-library-table/has-action-definitions"
+import { ToastService } from "../../../../toast/toast.service"
+
@Component({
selector: "app-collection-public-action-bar",
@@ -22,11 +24,36 @@ export class CollectionPublicActionBarComponent implements OnInit {
downloadIcon = SvgHelper.path(SvgIcon.DOWNLOAD)
collectionJsonObservable = new Observable()
+ exporter = new ExportCollectionComponent(
+ this.collectionService,
+ this.toastService,
+ this.locale
+ )
jsonClipboard = ""
- 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.exporter.getCollectionCsv(
+ this.collectionUuid,
+ this.collectionName
+ )
+ },
+ {
+ label: "Download as Excel Workbook",
+ visible: () => true,
+ callback: () => this.exporter.getCollectionXlsx(
+ this.collectionUuid,
+ this.collectionName
+ )
+ }
+ ],
+ visible: () => true
+ })
constructor(
protected router: Router,
@@ -44,27 +71,6 @@ export class CollectionPublicActionBarComponent implements OnInit {
}
}
- pollCsv(): void {
- if (this.taskUuidInProgress === undefined) { // fail fast
- clearInterval(this.intervalHandle)
- return
- }
-
- this.collectionService.getCsvTaskResultsIfComplete(this.taskUuidInProgress)
- .subscribe(({body, status}) => {
- if (status === 200) {
- this.csvExport = body as string
- this.taskUuidInProgress = undefined
-
- clearInterval(this.intervalHandle)
-
- const blob = new Blob([body], { type: "text/csv;charset=utf-8;" })
- const date = formatDate(new Date(), "yyyy-MM-dd", this.locale)
- FileSaver.saveAs(blob, `RSD Skills - ${this.collectionName} ${date}.csv`)
- }
- })
- }
-
onCopyURL(fullPath: HTMLTextAreaElement): void {
fullPath.select()
document.execCommand("copy")
@@ -72,14 +78,6 @@ export class CollectionPublicActionBarComponent implements OnInit {
this.toastService.showToast("Success!", "URL copied to clipboard")
}
- onDownloadCsv(): void {
- this.collectionService.requestCollectionSkillsCsv(this.collectionUuid)
- .subscribe((taskStarted: ITaskResult) => {
- this.taskUuidInProgress = taskStarted.uuid
- this.intervalHandle = setInterval(() => this.pollCsv(), 1000)
- })
- }
-
onCopyJSON(collectionJson: HTMLTextAreaElement): void {
this.collectionJsonObservable.subscribe(() => {
collectionJson.select()
@@ -88,4 +86,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 @@
-
+
+
-
-
-
+
+
-
+
+
-
+
+