diff --git a/build.gradle b/build.gradle index ad4ac04..860dc74 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ apply plugin: "org.grails.plugins.views-json" repositories { mavenLocal() maven { url "https://repo.grails.org/grails/core" } + jcenter() } dependencies { @@ -86,6 +87,9 @@ dependencies { testCompile "com.stehno.ersatz:ersatz:$ersatzVersion:safe@jar" testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + + compile "builders.dsl:spreadsheet-builder-poi:$spreadsheetBuilderVersion" + compile "builders.dsl:spreadsheet-builder-groovy:$spreadsheetBuilderVersion" } bootRun { @@ -95,8 +99,8 @@ bootRun { } webdriverBinaries { - chromedriver '2.36' - geckodriver '0.18.0' + chromedriver "${chromedriverVersion}" + geckodriver "${geckodriverVersion}" } test { @@ -109,6 +113,7 @@ integrationTest { tasks.withType(Test) { systemProperties System.properties + systemProperty "grails.env", 'test' systemProperty "geb.env", System.getProperty('geb.env') systemProperty "geb.build.reportsDir", reporting.file("geb/integrationTest") beforeTest { descriptor -> logger.quiet " -- $descriptor" } diff --git a/gradle.properties b/gradle.properties index aa3c774..a165ded 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,20 @@ -grailsVersion=3.3.2 -gormVersion=6.1.8.RELEASE +#Tue Jun 26 14:44:22 BST 2018 +jsonViewsVersion=1.2.7 +webdriverBinariesGradleVersion=1.4 +grailsWrapperVersion=1.0.0 +grailsVersion=3.3.6 +hamcrestVersion=1.3 +seleniumVersion=3.6.0 gradleWrapperVersion=3.5 -droolsVersion=7.5.0.Final -okHttpVersion=3.9.1 -moshiVersion=1.5.0 -mysqlVersion=5.1.46 +gormVersion=6.1.10.RELEASE grailsExecutorVersion=0.4 -seleniumVersion=3.6.0 -assetPipelineVersion=2.14.8 -webdriverBinariesGradleVersion=1.4 -poiVersion=3.17 -jsonViewsVersion=1.2.7 +mysqlVersion=5.1.46 ersatzVersion=1.6.2 -hamcrestVersion=1.3 \ No newline at end of file +assetPipelineVersion=2.14.8 +okHttpVersion=3.9.1 +moshiVersion=1.5.0 +droolsVersion=7.5.0.Final +chromedriverVersion=2.40 +geckodriverVersion=0.21.0 +spreadsheetBuilderVersion=1.0.5 +poiVersion=3.16 diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index a0cfedf..5de4f2b 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -3,9 +3,6 @@ grails: profile: web codegen: defaultPackage: uk.co.metadataconsulting.sentinel - spring: - transactionManagement: - proxies: false gorm: reactor: # Whether to translate GORM events into Reactor events @@ -65,6 +62,7 @@ grails: xml: - text/xml - application/xml + urlmapping: cache: maxsize: 1000 @@ -106,7 +104,7 @@ dataSource: environments: development: dataSource: - dbCreate: update + dbCreate: create url: '${JDBC_CONNECTION_STRING}' test: dataSource: diff --git a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordCollectionController.groovy b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordCollectionController.groovy index b81ec14..49dad61 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordCollectionController.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordCollectionController.groovy @@ -1,14 +1,25 @@ package uk.co.metadataconsulting.sentinel +import builders.dsl.spreadsheet.api.Color +import builders.dsl.spreadsheet.builder.poi.PoiSpreadsheetBuilder import grails.config.Config import grails.core.support.GrailsConfigurationAware -import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import org.apache.poi.xssf.usermodel.XSSFColor import org.springframework.context.MessageSource +import uk.co.metadataconsulting.sentinel.export.ExportRecordCollectionCommand +import uk.co.metadataconsulting.sentinel.export.ExportService +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportRowView +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportService +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportView import uk.co.metadataconsulting.sentinel.modelcatalogue.ValidationRules +import java.awt.Color + +import static org.springframework.http.HttpStatus.OK +import uk.co.metadataconsulting.sentinel.export.ExportFormat + @Slf4j -@CompileStatic class RecordCollectionController implements ValidateableErrorsMessage, GrailsConfigurationAware { static allowedMethods = [ @@ -20,6 +31,7 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon headersMapping: 'GET', cloneMapping: 'GET', cloneSave: 'POST', + export: 'GET' ] MessageSource messageSource @@ -28,6 +40,8 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon RecordCollectionGormService recordCollectionGormService + RecordGormService recordGormService + RecordCollectionService recordCollectionService ExcelImportService excelImportService @@ -36,11 +50,17 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon RuleFetcherService ruleFetcherService + RecordCollectionExportService recordCollectionExportService + + ExportService exportService + int defaultPaginationMax = 25 int defaultPaginationOffset = 0 + String separator @Override void setConfiguration(Config co) { + separator = co.getProperty('export.csv.separator', String, ',') defaultPaginationMax = co.getProperty('sentinel.pagination.max', Integer, 25) defaultPaginationOffset = co.getProperty('sentinel.pagination.offset', Integer, 0) } @@ -68,6 +88,540 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon ] } + + def export(ExportRecordCollectionCommand cmd) { + + if ( cmd.hasErrors() ) { + // TODO Maybe display an error message with flash.error and redirect to HomePage + return + } + + RecordCollectionGormEntity collectionGormEntity = recordCollectionGormService.find(cmd.recordCollectionId) + if ( !collectionGormEntity ) { + // TODO Maybe display an error message with flash.error and redirect to HomePage + return + } + final String filenameprefix = collectionGormEntity.datasetName + response.status = OK.value() + + RecordCorrectnessDropdown recordCorrectnessDropdown = RecordCorrectnessDropdown.ALL + RecordCollectionExportView viewAll = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + RecordCollectionExportView view2 = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + RecordCollectionExportView view3 = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + def outs = response.outputStream + + // viewAll.rows = findAllValid(viewAll.rows, false) + view2.rows = findAllValidRows(viewAll.rows ) + view3.rows = findAllInValidRows(viewAll.rows) + + //Ensure that all portions are sorted + viewAll.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + //Ensure that all portions are sorted + view2.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + //Ensure that all portions are sorted + view3.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + RecordCollectionExportRowView firstRow1 = (viewAll?.rows)?viewAll.rows.first():[] + viewAll.headers = firstRow1.recordPortionList*.header + List headers1 = viewAll.headers + + if(viewAll?.rows) List portionsHeaders1 = viewAll.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + RecordCollectionExportRowView firstRow2 = (view2?.rows)?view2.rows.first():[] + view2.headers = firstRow2.recordPortionList*.header + List headers2 = view2.headers + if(view2?.rows) List portionsHeaders2 = view2.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + RecordCollectionExportRowView firstRow3 = (view2?.rows)?view3.rows.first():[] + view3.headers = firstRow3.recordPortionList*.header + List headers3 = view3.headers + if(view3?.rows) List portionsHeaders3 = view3.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + + + + String filename = "${filenameprefix}.${exportService.fileExtensionForFormat(cmd.format)}" + response.setHeader "Content-disposition", "attachment; filename=${filename}" + response.contentType = exportService.mimeTypeForFormat(cmd.format) + switch (cmd.format) { + case ExportFormat.CSV: + // TODO remove this line? + headers = headers1.collect { [it] }.flatten() + outs << "${headers1.join(separator)}\n" + if ( viewAll.rows ) { + String line = "${portionsHeaders1.join(separator)}\n" + outs << line + viewAll.rows.each { RecordCollectionExportRowView row -> + println row. + outs << "${row.toCsv(separator)}\n" + } + } + break + case ExportFormat.XLSX: + PoiSpreadsheetBuilder.create(outs).build { + sheet(messageSource.getMessage("export.all".toString(), [] as Object[], 'All', request.locale)) { + row { + for (String header : headers1) { + cell { + value header + //colspan RecordPortion.toHeaderList().size() + } + } + } + + if (viewAll.rows) { + //viewAll.rows.each{List rowView -> + for (RecordCollectionExportRowView rowView : viewAll.rows) { + row { + for (RecordPortion recordPortion : rowView.recordPortionList) { + cell { + if (isValidPortionFunction(recordPortion)) { + style { + font { + color green + } + } + } + if (isInValidPortionFunction(recordPortion)) { + style { + font { + color red + } + } + } + value recordPortion.value + } + } + } + } + } + } + sheet(messageSource.getMessage("export.valid".toString(), [] as Object[], 'Valid', request.locale)) { + row { + for (String header : headers2) { + cell { + value header + } + } + } + + if (view2.rows) { + //viewAll.rows.each{List rowView -> + for (RecordCollectionExportRowView rowView : view2.rows) { + row { + for (RecordPortion recordPortion : rowView.recordPortionList) { + cell { + if (isValidPortionFunction(recordPortion)) { + style { + font { + color green + } + } + } + if (isInValidPortionFunction(recordPortion)) { + style { + font { + color red + } + } + } + value recordPortion.value + } + } + } + } + } + } + sheet(messageSource.getMessage("export.invalid".toString(), [] as Object[], 'Invalid', request.locale)) { + row { + for (String header : headers3) { + cell { + value header + //colspan RecordPortion.toHeaderList().size() + } + } + } + if (view3.rows) { + //viewAll.rows.each{List rowView -> + for (RecordCollectionExportRowView rowView : view3.rows) { + row { + for (RecordPortion recordPortion : rowView.recordPortionList) { + cell { + if (isValidPortionFunction(recordPortion)) { + style { + font { + color green + } + } + } + if (isInValidPortionFunction(recordPortion)) { + style { + font { + color red + } + } + } + value recordPortion.value + } + } + } + } + } + } + } + break + } + outs.flush() + outs.close() + } + + def exportAll(ExportRecordCollectionCommand cmd) { + + if ( cmd.hasErrors() ) { + // TODO Maybe display an error message with flash.error and redirect to HomePage + return + } + + RecordCollectionGormEntity collectionGormEntity = recordCollectionGormService.find(cmd.recordCollectionId) + if ( !collectionGormEntity ) { + // TODO Maybe display an error message with flash.error and redirect to HomePage + return + } + final String filenameprefix = collectionGormEntity.datasetName + response.status = OK.value() + + RecordCorrectnessDropdown recordCorrectnessDropdown = RecordCorrectnessDropdown.ALL + RecordCollectionExportView viewAll = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + RecordCollectionExportView view2 = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + RecordCollectionExportView view3 = recordCollectionExportService.export(cmd.recordCollectionId, recordCorrectnessDropdown) + def outs = response.outputStream + + viewAll.rows = findAllValid(viewAll.rows, true) + view2.rows = findAllInvalid(view2.rows, true) + view3.rows = findAllNotValidated(view3.rows, true) + + List headers = [] + + //Ensure that all portions are sorted + viewAll.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + //Ensure that all portions are sorted + view2.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + //Ensure that all portions are sorted + view3.rows.each { RecordCollectionExportRowView rowView -> + List dataItemList = rowView.recordPortionList + dataItemList.sort { + it.header + } + } + + RecordCollectionExportRowView firstRow1 = viewAll.rows.first() + viewAll.headers = firstRow1.recordPortionList*.header + List headers1 = viewAll.headers + List portionsHeaders1 = viewAll.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + RecordCollectionExportRowView firstRow2 = view2.rows.first() + view2.headers = firstRow2.recordPortionList*.header + List headers2 = view2.headers + List portionsHeaders2 = view2.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + RecordCollectionExportRowView firstRow3 = view3.rows.first() + view3.headers = firstRow3.recordPortionList*.header + List headers3 = view3.headers + List portionsHeaders3 = view3.rows.first().recordPortionList.collect { RecordPortion.toHeaderList() }.flatten() + + + + + String filename = "${filenameprefix}.${exportService.fileExtensionForFormat(cmd.format)}" + response.setHeader "Content-disposition", "attachment; filename=${filename}" + response.contentType = exportService.mimeTypeForFormat(cmd.format) + switch (cmd.format) { + case ExportFormat.CSV: + // TODO remove this line? + headers = headers.collect { [it, it, it, it, it, it] }.flatten() + outs << "${headers.join(separator)}\n" + if ( viewAll.rows ) { + String line = "${portionsHeaders.join(separator)}\n" + outs << line + viewAll.rows.each { RecordCollectionExportRowView row -> + outs << "${row.toCsv(separator)}\n" + } + } + break + case ExportFormat.XLSX: + PoiSpreadsheetBuilder.create(outs).build { + sheet(messageSource.getMessage("export.valid".toString(), [] as Object[], 'Valid', request.locale)) { + row { + for ( String header : headers1 ) { + cell { + value header + colspan RecordPortion.toHeaderList().size() + } + } + } + if ( portionsHeaders1 ) { + row { + for ( String recordPortionHeader : portionsHeaders1 ) { + cell { + value messageSource.getMessage("export.header.${recordPortionHeader}".toString(), + [] as Object[], + recordPortionHeader, + request.locale) + } + } + } + } + if ( viewAll.rows ) { + //viewAll.rows.each{List rowView -> + for (RecordCollectionExportRowView rowView: viewAll.rows){ + row { + for ( RecordPortion recordPortion : rowView.recordPortionList) { + for (String val : recordPortion.toList()) { + cell { + value val + } + } + } + } + } + } + } + sheet(messageSource.getMessage("export.invalid".toString(), [] as Object[], 'Invalid', request.locale)) { + row { + for ( String header : headers2 ) { + cell { + value header + colspan RecordPortion.toHeaderList().size() + } + } + } + if ( portionsHeaders1 ) { + row { + for ( String recordPortionHeader : portionsHeaders2 ) { + cell { + value messageSource.getMessage("export.header.${recordPortionHeader}".toString(), + [] as Object[], + recordPortionHeader, + request.locale) + } + } + } + } + if ( view2.rows ) { + for (RecordCollectionExportRowView rowView2 : view2.rows ) { + row { + for ( RecordPortion recordPortion : rowView2.recordPortionList) { + for (String val : recordPortion.toList()) { + cell { + value val + style{ + + } + + } + } + } + } + } + } + } + sheet(messageSource.getMessage("export.notValidated".toString(), [] as Object[], 'Not Validated', request.locale)) { + row { + for ( String header : headers3 ) { + cell { + value header + colspan RecordPortion.toHeaderList().size() + } + } + } + if ( portionsHeaders3 ) { + row { + for ( String recordPortionHeader : portionsHeaders3) { + cell { + value messageSource.getMessage("export.header.${recordPortionHeader}".toString(), + [] as Object[], + recordPortionHeader, + request.locale) + } + } + } + } + if ( view3.rows ) { + for (RecordCollectionExportRowView rowView3 : view3.rows ) { + row { + for ( RecordPortion recordPortion : rowView3.recordPortionList) { + for (String val : recordPortion.toList()) { + cell { + + style { + font { + color red + } + } + + value val + } + } + } + } + } + } + } + } + + break + } + outs.flush() + outs.close() + } + + private List findAllValidRows(List all ) { + List newList = [] + all.each{ RecordCollectionExportRowView row -> + int cntr = 0 + row.recordPortionList.each{ portion -> + boolean bResult = isValidPortionFunction( portion) + if(bResult){cntr++} + } + if(cntr > 0){ + newList.add(row) + } + } + return newList + } + private List findAllInValidRows(List all ) { + List newList = [] + all.each{ RecordCollectionExportRowView row -> + int cntr = 0 + row.recordPortionList.each{ portion -> + boolean bResult = isInValidPortionFunction( portion) + if(bResult){cntr++} + } + if(cntr > 0){ + newList.add(row) + } + } + return newList + } + + private List findAllValid(List all, boolean addRows) { + List newList = all.each{ RecordCollectionExportRowView row -> + + def workingRecordPortionList = row.recordPortionList + List validDataItemList = [] + workingRecordPortionList.each{ portion -> + boolean bResult = isValidPortionFunction( portion) + println "Is Valid:" + bResult + ":" + portion.toString() + if(bResult){ + validDataItemList.add(portion) + println "added portion" + }else{ + if(addRows){ + validDataItemList.add(new RecordPortion(portion.header)) + println "empty portion added " + } + + } + } + row.recordPortionList = validDataItemList + } + return newList + } + private List findAllInvalid(List all, boolean addRows) { + List newList = all.each{ RecordCollectionExportRowView row -> + + def workingRecordPortionList = row.recordPortionList + List validDataItemList = [] + workingRecordPortionList.each{ portion -> + boolean bResult = isInValidPortionFunction(portion) + println "Is In Valid:" + bResult + ":" + portion.toString() + + if(bResult){ + validDataItemList.add(portion) + println "added portion" + }else{ + if(addRows){ + validDataItemList.add(new RecordPortion(portion.header)) + println "empty portion added " + } + } + } + row.recordPortionList = validDataItemList + } + return newList + } + private List findAllNotValidated(List all, boolean addRows) { + List newList = all.each{ RecordCollectionExportRowView row -> + + def workingRecordPortionList = row.recordPortionList + List validDataItemList = [] + workingRecordPortionList.each{ portion -> + boolean bResult = isNotYetValidatedPortionFunction(portion) + println "Is Not Validated:" + bResult + ":" + portion.toString() + if(bResult){ + validDataItemList.add(portion) + println "added portion" + }else{ + if(addRows){ + validDataItemList.add(new RecordPortion(portion.header)) + println "empty portion added " + } + } + } + row.recordPortionList = validDataItemList + } + return newList + } + + + private boolean isValidPortionFunction(RecordPortion portion){ + ValidationStatus vStatus = portion.status as ValidationStatus + boolean bstatus = (vStatus == ValidationStatus.VALID) + return bstatus + } + + private boolean isInValidPortionFunction(RecordPortion portion){ + ValidationStatus vStatus = portion.status as ValidationStatus + boolean bstatus = (vStatus == ValidationStatus.INVALID) + return bstatus + } + + private boolean isNotYetValidatedPortionFunction(RecordPortion portion){ + ValidationStatus vStatus = portion.status as ValidationStatus + boolean bstatus = (vStatus == ValidationStatus.NOT_VALIDATED) + return bstatus + } + + def importCsv() { [:] } @@ -98,8 +652,9 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon log.debug 'Content Type {}', cmd.csvFile.contentType InputStream inputStream = cmd.csvFile.inputStream Integer batchSize = cmd.batchSize + String datasetName = cmd.datasetName CsvImport importService = csvImportByContentType (ImportContentType.of(cmd.csvFile.contentType)) - importService.save(inputStream, batchSize) + importService.save(inputStream, datasetName, batchSize) redirect controller: 'recordCollection', action: 'index' } @@ -121,9 +676,9 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon List dataModelList = ruleFetcherService.fetchDataModels()?.dataModels if ( !dataModelList ) { flash.error = messageSource.getMessage('dataModel.couldNotLoad', [] as Object[], 'Could not load data Models', request.locale) - } + } [ - dataModelList: dataModelList, + dataModelList: dataModelList, recordCollectionId: recordCollectionId, recordPortionMappingList: recordCollectionMappingGormService.findAllByRecordCollectionId(recordCollectionId) ] @@ -158,4 +713,4 @@ class RecordCollectionController implements ValidateableErrorsMessage, GrailsCon } null } -} \ No newline at end of file +} diff --git a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordFileCommand.groovy b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordFileCommand.groovy index b54acc8..14cee17 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordFileCommand.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordFileCommand.groovy @@ -8,8 +8,10 @@ import org.springframework.web.multipart.MultipartFile class RecordFileCommand implements Validateable { MultipartFile csvFile Integer batchSize = 100 + String datasetName static constraints = { + datasetName nullable: false, blank: false batchSize nullable: false csvFile validator: { MultipartFile val, RecordFileCommand obj -> if ( val == null ) { diff --git a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordIndexCommand.groovy b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordIndexCommand.groovy index ea73a52..7d98d71 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordIndexCommand.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/sentinel/RecordIndexCommand.groovy @@ -9,6 +9,7 @@ class RecordIndexCommand implements Validateable { Integer offset Integer max Long recordCollectionId + //String datasetName RecordCorrectnessDropdown correctness = RecordCorrectnessDropdown.ALL static constraints = { diff --git a/grails-app/controllers/uk/co/metadataconsulting/sentinel/UrlMappings.groovy b/grails-app/controllers/uk/co/metadataconsulting/sentinel/UrlMappings.groovy index c5f665c..3fc3e06 100644 --- a/grails-app/controllers/uk/co/metadataconsulting/sentinel/UrlMappings.groovy +++ b/grails-app/controllers/uk/co/metadataconsulting/sentinel/UrlMappings.groovy @@ -7,6 +7,7 @@ class UrlMappings { "/recordCollection/cloneMapping"(controller: 'recordCollection', action: 'cloneMapping') "/recordCollection/cloneSave"(controller: 'recordCollection', action: 'cloneSave', httpMethod: 'POST') "/recordCollection/validate"(controller: 'recordCollection', action: 'validate') + "/recordCollection/export"(controller: 'recordCollection', action: 'export') "/recordCollection/delete"(controller: 'recordCollection', action: 'delete') "/recordCollection/$recordCollectionId/mapping"(controller: 'recordCollection', action: 'headersMapping') "/recordCollectionMapping/catalogueElements/$dataModelId"(controller: 'recordCollectionMapping', action: 'catalogueElements') diff --git a/grails-app/controllers/uk/co/metadataconsulting/sentinel/export/ExportRecordCollectionCommand.groovy b/grails-app/controllers/uk/co/metadataconsulting/sentinel/export/ExportRecordCollectionCommand.groovy new file mode 100644 index 0000000..7276ac7 --- /dev/null +++ b/grails-app/controllers/uk/co/metadataconsulting/sentinel/export/ExportRecordCollectionCommand.groovy @@ -0,0 +1,16 @@ +package uk.co.metadataconsulting.sentinel.export + +import grails.compiler.GrailsCompileStatic +import grails.validation.Validateable +import uk.co.metadataconsulting.sentinel.export.ExportFormat + +@GrailsCompileStatic +class ExportRecordCollectionCommand implements Validateable { + + Long recordCollectionId + ExportFormat format = ExportFormat.XLSX + + static constraints = { + recordCollectionId nullable: false + } +} \ No newline at end of file diff --git a/grails-app/domain/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntity.groovy b/grails-app/domain/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntity.groovy index 331cc04..e7bff5d 100644 --- a/grails-app/domain/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntity.groovy +++ b/grails-app/domain/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntity.groovy @@ -5,6 +5,8 @@ import grails.compiler.GrailsCompileStatic @GrailsCompileStatic class RecordCollectionGormEntity { + String datasetName + Date dateCreated Date lastUpdated @@ -17,6 +19,7 @@ class RecordCollectionGormEntity { static constraints = { records nullable: true mappings nullable: true + datasetName nullable: false, blank: false } static mapping = { diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index b045136..335748c 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -54,3 +54,10 @@ typeMismatch.java.lang.Short=Property {0} must be a valid number typeMismatch.java.math.BigDecimal=Property {0} must be a valid number typeMismatch.java.math.BigInteger=Property {0} must be a valid number typeMismatch=Property {0} is type-mismatched + + +export.header.name=Name +export.header.value=Value +export.header.status=Status +export.header.reason=Failure Reason +export.header.numberOfRulesValidatedAgainst=# Rules Validated Against diff --git a/grails-app/init/uk/co/metadataconsulting/sentinel/BootStrap.groovy b/grails-app/init/uk/co/metadataconsulting/sentinel/BootStrap.groovy index b454d23..cbb7bf5 100644 --- a/grails-app/init/uk/co/metadataconsulting/sentinel/BootStrap.groovy +++ b/grails-app/init/uk/co/metadataconsulting/sentinel/BootStrap.groovy @@ -17,7 +17,8 @@ class BootStrap { List mapping = mappingGormUrl() File f = new File('src/test/resources/DIDS_XMLExample_01.csv') InputStream inputStream = f.newInputStream() - csvImportService.save(inputStream, 100) + String datasetName = "SampleDataset" + csvImportService.save(inputStream, datasetName, 100) } List mappingGormUrl() { diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportProcessorService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportProcessorService.groovy index 7852bdd..a23f6dc 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportProcessorService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportProcessorService.groovy @@ -40,7 +40,7 @@ class CsvImportProcessorService implements GrailsConfigurationAware, CsvImportPr } @Override - int processInputStream(InputStream inputStream, Integer batchSize, Closure headerListClosure, Closure cls) { + int processInputStream(InputStream inputStream, Integer batchSize, Closure headerListClosure, Closure cls) { int processed = 0 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)) List lines = [] diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportService.groovy index ca5384e..e8f3add 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/CsvImportService.groovy @@ -26,8 +26,9 @@ class CsvImportService implements CsvImport, Benchmark { @CompileDynamic @Override - void save(InputStream inputStream, Integer batchSize) { - RecordCollectionGormEntity recordCollection = recordCollectionGormService.save() + void save(InputStream inputStream, String datasetName, Integer batchSize ) { + + RecordCollectionGormEntity recordCollection = recordCollectionGormService.save(datasetName) executorService.submit { log.info 'fetching validation rules' diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelGeneratorService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelGeneratorService.groovy new file mode 100644 index 0000000..e69de29 diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelImportService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelImportService.groovy index d859577..58b60ea 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelImportService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/ExcelImportService.groovy @@ -20,8 +20,8 @@ class ExcelImportService implements CsvImport, Benchmark { @CompileDynamic @Override - void save(InputStream inputStream, Integer batchSize) { - RecordCollectionGormEntity recordCollection = recordCollectionGormService.save() + void save(InputStream inputStream, String datasetName, Integer batchSize) { + RecordCollectionGormEntity recordCollection = recordCollectionGormService.save(datasetName) executorService.submit { log.info 'fetching validation rules' diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordCollectionGormService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordCollectionGormService.groovy index 6cf2df9..7cbe09f 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordCollectionGormService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordCollectionGormService.groovy @@ -37,9 +37,10 @@ class RecordCollectionGormService implements GormErrorsMessage { } @Transactional - RecordCollectionGormEntity save() { + RecordCollectionGormEntity save(String datasetName) { RecordCollectionGormEntity recordCollection = new RecordCollectionGormEntity() - if ( !recordCollection.save() ) { + recordCollection.datasetName = datasetName + if ( !recordCollection.save(validate:false) ) { log.warn '{}', errorsMsg(recordCollection, messageSource) } recordCollection diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordGormService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordGormService.groovy index 1fc6833..d0497cd 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordGormService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordGormService.groovy @@ -85,6 +85,13 @@ class RecordGormService implements GormErrorsMessage { query.list(paginationQuery.toMap()) } + @ReadOnly + List findAllByRecordCollectionId(Long recordCollectionId ) { + DetachedCriteria query = queryByRecordCollectionId(recordCollectionId) + return query as List + + } + @ReadOnly RecordGormEntity findById(Long recordId, List joinProperties = null) { DetachedCriteria query = queryById(recordId) @@ -104,4 +111,19 @@ class RecordGormService implements GormErrorsMessage { Number count() { RecordGormEntity.count() } + + @ReadOnly + List findAllByIds(List ids, List propertiesToJoin = ['portions']) { + DetachedCriteria query = queryAllByIds(ids) + if ( propertiesToJoin ) { + for ( String propertyName : propertiesToJoin ) { + query.join('portions') + } + } + query.list() + } + + DetachedCriteria queryAllByIds(List ids) { + RecordGormEntity.where { id in ids } + } } \ No newline at end of file diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordService.groovy index 096b549..11158aa 100644 --- a/grails-app/services/uk/co/metadataconsulting/sentinel/RecordService.groovy +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/RecordService.groovy @@ -40,6 +40,8 @@ class RecordService { } as Set } + + @Transactional void validate(Long recordId, List recordPortionMappingList, Map validationRulesMap) { DetachedCriteria query = recordGormService.queryById(recordId) @@ -54,32 +56,39 @@ class RecordService { } List findAllByRecordCollectionId(Long recordCollectionId, RecordCorrectnessDropdown correctness, PaginationQuery paginationQuery) { - // TODO Do this with a query - DetachedCriteria query = recordGormService.queryByRecordCollectionId(recordCollectionId) - //query.join('portions') - - if ( correctness == RecordCorrectnessDropdown.ALL ) { - Map args = paginationQuery.toMap() - - List l = query.list(args) - - List ids = query.id().list(args) as List - Set invalidRecordIds = findAllInvalidRecordIds() - - return ids.collect { Long id -> - new RecordViewModel(id: id, valid: !invalidRecordIds.contains(id)) - } + switch (correctness) { + case RecordCorrectnessDropdown.ALL: + List ids = findAllIdsByRecordCollectionId(recordCollectionId, correctness, paginationQuery) + Set invalidRecordIds = findAllInvalidRecordIds() + return ids.collect { Long id -> + new RecordViewModel(id: id, valid: !invalidRecordIds.contains(id)) + } + case RecordCorrectnessDropdown.VALID: + List recordCollection = findAllIdsByRecordCollectionId(recordCollectionId, correctness, paginationQuery) + List validViews = recordCollection.collect { + new RecordViewModel(id: it, valid: true) + } + return validViews + case RecordCorrectnessDropdown.INVALID: + return findAllIdsByRecordCollectionId(recordCollectionId, correctness, paginationQuery).collect { + new RecordViewModel(id: it, valid: false) + } } + } - boolean valid = validForCorrectnes(correctness) - - if ( valid ) { - return findAllValidRecords(recordCollectionId, paginationQuery).collect { - new RecordViewModel(id:it, valid: true) - } - } - return findAllInvalidRecords(recordCollectionId, paginationQuery).collect { - new RecordViewModel(id:it, valid: false) + List findAllIdsByRecordCollectionId(Long recordCollectionId, RecordCorrectnessDropdown correctness, PaginationQuery paginationQuery) { + switch (correctness) { + case RecordCorrectnessDropdown.ALL: + // TODO Do this with a query + DetachedCriteria query = recordGormService.queryByRecordCollectionId(recordCollectionId) + //query.join('portions') + Map args = paginationQuery?.toMap() + List ids = (args ? query.id().list(args) : query.id().list()) as List + return ids + case RecordCorrectnessDropdown.VALID: + return findAllValidRecords(recordCollectionId, paginationQuery) + case RecordCorrectnessDropdown.INVALID: + return findAllInvalidRecords(recordCollectionId, paginationQuery) } } @@ -98,7 +107,7 @@ class RecordService { return [] as List } DetachedCriteria query = queryValidRecords(recordCollectionId, validRecordIds) - query.id().list(paginationQuery.toMap()) as List + return (paginationQuery ? query.id().list(paginationQuery.toMap()) : query.id().list()) as List } List findAllInvalidRecords(Long recordCollectionId, PaginationQuery paginationQuery) { @@ -107,7 +116,8 @@ class RecordService { return [] as List } DetachedCriteria query = queryInvalidRecords(recordCollectionId, invalidRecordIds) - query.id().list(paginationQuery.toMap()) as List + //query.id().list(paginationQuery.toMap()) as List + return (paginationQuery ? query.id().list(paginationQuery.toMap()) : query.id().list()) as List } Number countValidRecords(Long recordCollectionId) { diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/export/ExportService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/export/ExportService.groovy new file mode 100644 index 0000000..9b4dcb2 --- /dev/null +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/export/ExportService.groovy @@ -0,0 +1,41 @@ +package uk.co.metadataconsulting.sentinel.export + +import grails.config.Config +import grails.core.support.GrailsConfigurationAware +import groovy.transform.CompileStatic +import uk.co.metadataconsulting.sentinel.export.ExportFormat + +@CompileStatic +class ExportService implements GrailsConfigurationAware { + String xlsxMimeType + String xlsMimeType + String csvMimeType + String encoding + + @Override + void setConfiguration(Config co) { + csvMimeType = co.getProperty('grails.mime.types.csv', String, 'text/csv') + xlsMimeType = co.getProperty('grails.mime.types.xlsMimeType', String,'application/vnd.ms-excel') + xlsxMimeType = co.getProperty('grails.mime.types.xlsxMimeType', String, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + encoding = co.getProperty('grails.converters.encoding', String, 'UTF-8') + } + + String fileExtensionForFormat(ExportFormat format) { + switch (format) { + case ExportFormat.CSV: + return 'csv' + + case ExportFormat.XLSX: + return "xlsx" + } + } + String mimeTypeForFormat(ExportFormat format) { + switch (format) { + case ExportFormat.CSV: + return "${csvMimeType};charset=${encoding}" + + case ExportFormat.XLSX: + return "${xlsxMimeType};charset=${encoding}" + } + } +} \ No newline at end of file diff --git a/grails-app/services/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportService.groovy b/grails-app/services/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportService.groovy new file mode 100644 index 0000000..f95a724 --- /dev/null +++ b/grails-app/services/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportService.groovy @@ -0,0 +1,37 @@ +package uk.co.metadataconsulting.sentinel.export + +import groovy.transform.CompileStatic +import uk.co.metadataconsulting.sentinel.RecordCorrectnessDropdown +import uk.co.metadataconsulting.sentinel.RecordGormEntity +import uk.co.metadataconsulting.sentinel.RecordGormService +import uk.co.metadataconsulting.sentinel.RecordPortion +import uk.co.metadataconsulting.sentinel.RecordPortionGormEntity +import uk.co.metadataconsulting.sentinel.RecordPortionUtils +import uk.co.metadataconsulting.sentinel.RecordService +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportRowView +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportView + +@CompileStatic +class RecordCollectionExportService { + + RecordService recordService + + RecordGormService recordGormService + + RecordCollectionExportView export(Long recordCollectionId, RecordCorrectnessDropdown correctness = RecordCorrectnessDropdown.VALID) { + List recordIds = recordService.findAllIdsByRecordCollectionId(recordCollectionId, correctness, null) + List recordGormEntityList = recordGormService.findAllByIds(recordIds) + List rows = [] + List headers + if ( recordGormEntityList ) { + headers = recordGormEntityList.first().portions*.header + for ( RecordGormEntity recordGormEntity : recordGormEntityList ) { + List recordPortionList = recordGormEntity.portions.collect { RecordPortionGormEntity recordPortionGormEntity -> + RecordPortionUtils.of(recordPortionGormEntity) } + rows << new RecordCollectionExportRowView(recordPortionList: recordPortionList) + } + } + + new RecordCollectionExportView(rows: rows, headers: headers) + } +} \ No newline at end of file diff --git a/grails-app/utils/uk/co/metadataconsulting/sentinel/RecordPortionUtils.groovy b/grails-app/utils/uk/co/metadataconsulting/sentinel/RecordPortionUtils.groovy new file mode 100644 index 0000000..a10e249 --- /dev/null +++ b/grails-app/utils/uk/co/metadataconsulting/sentinel/RecordPortionUtils.groovy @@ -0,0 +1,17 @@ +package uk.co.metadataconsulting.sentinel + +import groovy.transform.CompileStatic + +@CompileStatic +class RecordPortionUtils { + + static RecordPortion of(RecordPortionGormEntity recordPortionGormEntity) { + new RecordPortion( + header: recordPortionGormEntity.header, + name: recordPortionGormEntity.name, + value: recordPortionGormEntity.value, + status: recordPortionGormEntity.status, + reason: recordPortionGormEntity.reason, + numberOfRulesValidatedAgainst: recordPortionGormEntity.numberOfRulesValidatedAgainst) + } +} diff --git a/grails-app/views/record/index.gsp b/grails-app/views/record/index.gsp index 2e0c5cf..27c2133 100644 --- a/grails-app/views/record/index.gsp +++ b/grails-app/views/record/index.gsp @@ -1,56 +1,108 @@ <%@ page import="uk.co.metadataconsulting.sentinel.RecordCorrectnessDropdown" %> +<%@ page import="uk.co.metadataconsulting.sentinel.export.ExportFormat" %> + Records - diff --git a/grails-app/views/recordCollection/importCsv.gsp b/grails-app/views/recordCollection/importCsv.gsp index 36c35c4..3491e13 100644 --- a/grails-app/views/recordCollection/importCsv.gsp +++ b/grails-app/views/recordCollection/importCsv.gsp @@ -23,6 +23,10 @@ +
+ +
diff --git a/grails-app/views/recordCollection/index.gsp b/grails-app/views/recordCollection/index.gsp index 8fe5b0b..0420dec 100644 --- a/grails-app/views/recordCollection/index.gsp +++ b/grails-app/views/recordCollection/index.gsp @@ -18,6 +18,7 @@ + @@ -25,6 +26,7 @@ +
${recordCollection.datasetName} ${recordCollection.lastUpdated} diff --git a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/CsvImportServiceIntegrationSpec.groovy b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/CsvImportServiceIntegrationSpec.groovy index 62867e2..62e90d5 100644 --- a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/CsvImportServiceIntegrationSpec.groovy +++ b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/CsvImportServiceIntegrationSpec.groovy @@ -32,7 +32,7 @@ class CsvImportServiceIntegrationSpec extends Specification { expectedNumberOfRows when: - csvImportService.save(f.newInputStream(), 50) + csvImportService.save(f.newInputStream(), "DIDS_XMLExample_20", 50) then: recordCollectionGormService.count() == old(recordCollectionGormService.count()) + 1 diff --git a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordCollectionControllerIntegrationSpec.groovy b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordCollectionControllerIntegrationSpec.groovy index a90a3a1..0deb0b6 100644 --- a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordCollectionControllerIntegrationSpec.groovy +++ b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordCollectionControllerIntegrationSpec.groovy @@ -50,7 +50,7 @@ class RecordCollectionControllerIntegrationSpec extends Specification { ruleFetcherService.metadataUrl = ersatz.httpUrl when: - RecordCollectionGormEntity recordCollection= recordCollectionGormService.save() + RecordCollectionGormEntity recordCollection= recordCollectionGormService.save("Test") then: recordCollectionGormService.count() == old(recordCollectionGormService.count()) + 1 diff --git a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordControllerIntegrationSpec.groovy b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordControllerIntegrationSpec.groovy index e23dc94..6b4ac73 100644 --- a/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordControllerIntegrationSpec.groovy +++ b/src/integration-test/groovy/uk.co.metadataconsulting.sentinel/RecordControllerIntegrationSpec.groovy @@ -57,7 +57,7 @@ class RecordControllerIntegrationSpec extends Specification { ruleFetcherService.metadataUrl = ersatz.httpUrl when: - RecordCollectionGormEntity recordCollection= recordCollectionGormService.save() + RecordCollectionGormEntity recordCollection= recordCollectionGormService.save("Test") then: recordCollectionGormService.count() == old(recordCollectionGormService.count()) + 1 diff --git a/src/integration-test/resources/GebConfig.groovy b/src/integration-test/resources/GebConfig.groovy index 0d97e35..95c7681 100644 --- a/src/integration-test/resources/GebConfig.groovy +++ b/src/integration-test/resources/GebConfig.groovy @@ -1,6 +1,7 @@ import org.openqa.selenium.chrome.ChromeDriver import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.firefox.FirefoxDriver +import org.openqa.selenium.firefox.FirefoxOptions environments { @@ -17,6 +18,15 @@ environments { new ChromeDriver(o) } } + + // run via “./gradlew -Dgeb.env=firefoxHeadless iT” + firefoxHeadless { + driver = { + FirefoxOptions o = new FirefoxOptions() + o.addArguments('-headless') + new FirefoxDriver(o) + } + } // run via “./gradlew -Dgeb.env=firefox iT” firefox { diff --git a/src/main/groovy/uk/co/metadataconsulting/sentinel/CsvImport.groovy b/src/main/groovy/uk/co/metadataconsulting/sentinel/CsvImport.groovy index 453c201..bb98c5b 100644 --- a/src/main/groovy/uk/co/metadataconsulting/sentinel/CsvImport.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/sentinel/CsvImport.groovy @@ -4,5 +4,5 @@ import groovy.transform.CompileStatic @CompileStatic interface CsvImport { - void save(InputStream inputStream, Integer batchSize) + void save(InputStream inputStream, String datasetName, Integer batchSize) } \ No newline at end of file diff --git a/src/main/groovy/uk/co/metadataconsulting/sentinel/RecordPortion.groovy b/src/main/groovy/uk/co/metadataconsulting/sentinel/RecordPortion.groovy index 89e9415..7f2b36d 100644 --- a/src/main/groovy/uk/co/metadataconsulting/sentinel/RecordPortion.groovy +++ b/src/main/groovy/uk/co/metadataconsulting/sentinel/RecordPortion.groovy @@ -12,4 +12,16 @@ class RecordPortion { ValidationStatus status = ValidationStatus.NOT_VALIDATED String reason Integer numberOfRulesValidatedAgainst = 0 + + static List toHeaderList() { + ['name', 'value', 'status', 'reason', 'numberOfRulesValidatedAgainst'] + } + + List toList() { + [name ?: '', value ?: '', status.toString() ?: '', reason ?: '', "${numberOfRulesValidatedAgainst}".toString()] + } + + String toCsv(String separator = ';') { + toList().join(separator) + } } diff --git a/src/main/groovy/uk/co/metadataconsulting/sentinel/export/ExportFormat.groovy b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/ExportFormat.groovy new file mode 100644 index 0000000..0dde274 --- /dev/null +++ b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/ExportFormat.groovy @@ -0,0 +1,8 @@ +package uk.co.metadataconsulting.sentinel.export + +import groovy.transform.CompileStatic + +@CompileStatic +enum ExportFormat { + XLSX, CSV +} diff --git a/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportRowView.groovy b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportRowView.groovy new file mode 100644 index 0000000..99fbf64 --- /dev/null +++ b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportRowView.groovy @@ -0,0 +1,17 @@ +package uk.co.metadataconsulting.sentinel.export + +import groovy.transform.CompileStatic +import uk.co.metadataconsulting.sentinel.RecordPortion + +@CompileStatic +class RecordCollectionExportRowView { + List recordPortionList + + String toCsv(String separator = ',') { + recordPortionList.collect { it.toCsv(separator) }.join(separator) + } + + List toList() { + recordPortionList.collect { it.toList() }.flatten() as List + } +} diff --git a/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportView.groovy b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportView.groovy new file mode 100644 index 0000000..158a6cc --- /dev/null +++ b/src/main/groovy/uk/co/metadataconsulting/sentinel/export/RecordCollectionExportView.groovy @@ -0,0 +1,9 @@ +package uk.co.metadataconsulting.sentinel.export + +import groovy.transform.CompileStatic + +@CompileStatic +class RecordCollectionExportView { + List headers = [] + List rows = [] +} diff --git a/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionControllerAllowedMethodsSpec.groovy b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionControllerAllowedMethodsSpec.groovy index 6f12553..924177e 100644 --- a/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionControllerAllowedMethodsSpec.groovy +++ b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionControllerAllowedMethodsSpec.groovy @@ -3,12 +3,43 @@ package uk.co.metadataconsulting.sentinel import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification import spock.lang.Unroll +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportService +import uk.co.metadataconsulting.sentinel.export.RecordCollectionExportView + import static javax.servlet.http.HttpServletResponse.SC_OK import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY class RecordCollectionControllerAllowedMethodsSpec extends Specification implements ControllerUnitTest { + @Unroll + def "test RecordCollectionController.export does not accept #method requests"(String method) { + when: + request.method = method + controller.export() + + then: + response.status == SC_METHOD_NOT_ALLOWED + + where: + method << ['PATCH', 'DELETE', 'POST', 'PUT'] + } + + def "test RecordCollectionController.export accepts GET requests"() { + given: + controller.recordCollectionExportService = Stub(RecordCollectionExportService) { + export(_,_) >> new RecordCollectionExportView() + } + + when: + request.method = 'GET' + controller.export() + + then: + response.status == SC_OK + } + + @Unroll def "test RecordCollectionController.cloneSave does not accept #method requests"(String method) { when: diff --git a/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntityConstraintsSpec.groovy b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntityConstraintsSpec.groovy new file mode 100644 index 0000000..c129fd4 --- /dev/null +++ b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordCollectionGormEntityConstraintsSpec.groovy @@ -0,0 +1,22 @@ +package uk.co.metadataconsulting.sentinel + +import grails.testing.gorm.DomainUnitTest +import spock.lang.Specification + +class RecordCollectionGormEntityConstraintsSpec extends Specification implements DomainUnitTest { + + void 'verify datasetName is required and cannot be null'() { + when: + domain.datasetName = null + + then: + !domain.validate(['datasetName']) + + when: + domain.datasetName = '' + + then: + !domain.validate(['datasetName']) + } + +} diff --git a/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordFileCommandConstraintsSpec.groovy b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordFileCommandConstraintsSpec.groovy index 69acc3b..a8a0195 100644 --- a/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordFileCommandConstraintsSpec.groovy +++ b/src/test/groovy/uk/co/metadataconsulting/sentinel/RecordFileCommandConstraintsSpec.groovy @@ -18,4 +18,18 @@ class RecordFileCommandConstraintsSpec extends Specification { !cmd.validate(['batchSize']) cmd.errors['batchSize'].code == 'nullable' } + + void 'verify datasetName is required and cannot be null'() { + when: + cmd.datasetName = null + + then: + !cmd.validate(['datasetName']) + + when: + cmd.datasetName = '' + + then: + !cmd.validate(['datasetName']) + } } diff --git a/src/test/groovy/uk/co/metadataconsulting/sentinel/UrlMappingsSpec.groovy b/src/test/groovy/uk/co/metadataconsulting/sentinel/UrlMappingsSpec.groovy index edab982..c7d9f65 100644 --- a/src/test/groovy/uk/co/metadataconsulting/sentinel/UrlMappingsSpec.groovy +++ b/src/test/groovy/uk/co/metadataconsulting/sentinel/UrlMappingsSpec.groovy @@ -12,6 +12,7 @@ class UrlMappingsSpec extends Specification implements UrlMappingsUnitTest