- Timestamp:
- Jul 21, 2011, 4:36:15 PM (12 years ago)
- Location:
- trunk/grails-app
- Files:
-
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/grails-app/controllers/dbnp/query/AdvancedQueryController.groovy
r1962 r1969 726 726 ]] 727 727 case "Sample": 728 return [] 728 def ids = [] 729 s.filterResults(selectedTokens).each { 730 ids << it.id 731 } 732 733 def paramString = ids.collect { return 'ids=' + it }.join( '&' ); 734 735 return [[ 736 module: "gscf", 737 name:"excel", 738 type: "export", 739 description: "Export as CSV", 740 url: createLink( controller: "assay", action: "exportToSamplesToCsv", params: [ 'ids' : ids ] ), 741 submitUrl: createLink( controller: "assay", action: "exportSamplesToCsv" ), 742 paramString: paramString 743 ]] 729 744 default: 730 745 return []; -
trunk/grails-app/controllers/dbnp/studycapturing/AssayController.groovy
r1921 r1969 142 142 flow.fieldMap = assayService.collectAssayTemplateFields(flow.assay) 143 143 144 144 flash.errorMessage = flow.fieldMap.remove('ModuleError') 145 145 flow.measurementTokens = flow.fieldMap.remove('Module Measurement Data') 146 146 }.to "selectFields" … … 151 151 selectFields { 152 152 on ("submit"){ 153 153 154 154 def fieldMapSelection = [:] 155 155 … … 176 176 } 177 177 178 178 // collect the assay data according to user selecting 179 179 def assayData = assayService.collectAssayData(flow.assay, fieldMapSelection, measurementTokens) 180 180 181 181 flash.errorMessage = assayData.remove('ModuleError') 182 182 183 183 flow.rowData = assayService.convertColumnToRowStructure(assayData) 184 184 185 185 // prepare the assay data preview 186 186 def previewRows = Math.min(flow.rowData.size() as int, 5) - 1 187 187 def previewCols = Math.min(flow.rowData[0].size() as int, 5) - 1 … … 189 189 flow.assayDataPreview = flow.rowData[0..previewRows].collect{ it[0..previewCols] as ArrayList } 190 190 191 192 191 // store the selected file type in the flow 192 flow.exportFileType = params.exportFileType 193 193 194 194 }.to "compileExportData" … … 199 199 compileExportData { 200 200 on ("ok"){ 201 202 203 201 session.rowData = flow.rowData 202 session.exportFileType = flow.exportFileType 203 }.to "export" 204 204 on ("cancel").to "selectAssay" 205 205 } … … 220 220 def doExport = { 221 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 222 // make sure we're coming from the export flow, otherwise redirect there 223 if (!(session.rowData && session.exportFileType)) 224 redirect(action: 'assayExportFlow') 225 226 // process requested output file type 227 def outputDelimiter, outputFileExtension, locale = java.util.Locale.US 228 229 switch(session.exportFileType) { 230 case '2': // Comma delimited csv 231 outputDelimiter = ',' 232 outputFileExtension = 'csv' 233 break 234 case '3': // Semicolon delimited csv 235 outputDelimiter = ';' 236 outputFileExtension = 'csv' 237 locale = java.util.Locale.GERMAN // force use of comma as decimal separator 238 break 239 default: // Tab delimited with .txt extension 240 outputDelimiter = '\t' 241 outputFileExtension = 'txt' 242 } 243 243 244 244 def filename = "export.$outputFileExtension" … … 284 284 def exportToExcelAsSheets = { 285 285 def assays = getAssaysFromParams( params ); 286 286 287 287 if( !assays ) 288 288 return; … … 326 326 def exportToExcelAsList = { 327 327 def assays = getAssaysFromParams( params ); 328 328 329 329 if( !assays ) 330 330 return; … … 367 367 } 368 368 } 369 370 /** 371 * Method to export one or more samples to csv in separate sheets. 372 * 373 * @param params.ids One or more sample ids to export 374 */ 375 def exportSamplesToCsv = { 376 def samples = getSamplesFromParams( params ); 377 378 if( !samples ) { 379 return; 380 } 381 382 // Determine a list of assays these samples have been involved in. That way, we can 383 // retrieve the data for that assay once, and save precious time doing HTTP calls 384 def assays = [:]; 385 386 samples.each { sample -> 387 def thisAssays = sample.getAssays(); 388 389 // Loop through all assays. If it already exists, add the sample it to the list 390 thisAssays.each { assay -> 391 if( !assays[ assay.id ] ) { 392 assays[ assay.id ] = [ 'assay': assay, 'samples': [] ] 393 } 394 395 assays[ assay.id ].samples << sample 396 } 397 } 398 399 // Now collect data for all assays 400 try { 401 // Loop through all assays to collect the data 402 def columnWiseAssayData = []; 403 404 assays.each { assayInfo -> 405 def assay = assayInfo.value.assay; 406 def assaySamples = assayInfo.value.samples; 407 408 // Determine which fields should be exported for this assay 409 def fieldMap = assayService.collectAssayTemplateFields(assay) 410 def measurementTokens = fieldMap.remove('Module Measurement Data') 411 412 // Retrieve row based data for this assay 413 def assayData = assayService.collectAssayData( assay, fieldMap, measurementTokens, assaySamples ); 414 415 // Prepend study and assay data to the list 416 assayData = assayService.prependAssayData( assayData, assay, assaySamples.size() ) 417 assayData = assayService.prependStudyData( assayData, assay, assaySamples.size() ) 418 419 // Make sure the assay data can be distinguished later 420 assayData.put( "Assay data - " + assay.name, assayData.remove( "Assay Data") ) 421 assayData.put( "Module measurement data - " + assay.name, assayData.remove( "Module Measurement Data") ) 422 423 // Add the sample IDs to the list, in order to be able to combine 424 // data for a sample that has been processed in multiple assays 425 assayData[ "Sample Data" ][ "id" ] = assaySamples*.id; 426 427 println "Assay data" 428 assayData.each { println it } 429 430 columnWiseAssayData << assayData; 431 } 432 433 def mergedColumnWiseData = assayService.mergeColumnWiseDataOfMultipleStudiesForASetOfSamples( columnWiseAssayData ); 434 435 def rowData = assayService.convertColumnToRowStructure(mergedColumnWiseData) 436 437 // Send headers to the browser so the user can download the file 438 def filename = 'export.csv' 439 response.setHeader("Content-disposition", "attachment;filename=\"${filename}\"") 440 response.setContentType("application/octet-stream") 441 442 assayService.exportRowWiseDataToCSVFile( rowData, response.getOutputStream() ) 443 444 response.outputStream.flush() 445 446 } catch (Exception e) { 447 throw e; 448 } 449 } 450 369 451 370 452 def getAssaysFromParams( params ) { … … 392 474 assays << assay; 393 475 } 394 476 395 477 if( !assays ) { 396 478 flash.errorMessage = "No assays found"; … … 398 480 return []; 399 481 } 400 482 401 483 return assays.unique(); 402 484 } 403 485 486 def getSamplesFromParams( params ) { 487 def ids = params.list( 'ids' ).findAll { it.isLong() }.collect { Long.valueOf( it ) }; 488 def tokens = params.list( 'tokens' ); 489 490 if( !ids && !tokens ) { 491 flash.errorMessage = "No sample ids given"; 492 redirect( action: "errorPage" ); 493 return []; 494 } 495 496 // Find all assays for the given ids 497 def samples = []; 498 ids.each { id -> 499 def sample = Sample.get( id ); 500 if( sample ) 501 samples << sample; 502 } 503 504 // Also accept tokens for defining studies 505 tokens.each { token -> 506 def sample = Sample.findBySampleUUID( token ); 507 if( sample ) 508 samples << sample; 509 } 510 511 if( !samples ) { 512 flash.errorMessage = "No assays found"; 513 redirect( action: "errorPage" ); 514 return []; 515 } 516 517 return samples.unique(); 518 } 519 520 404 521 def errorPage = { 405 522 render(view: 'assayExport/errorPage') -
trunk/grails-app/domain/dbnp/studycapturing/Sample.groovy
r1945 r1969 124 124 } 125 125 126 /** 127 * Returns all assays this samples has been processed in 128 * @return List of assays 129 */ 130 public List getAssays() { 131 return Assay.executeQuery( 'select distinct a from Assay a inner join a.samples s where s = :sample', ['sample': this] ) 132 } 133 126 134 def String toString() { 127 135 return name -
trunk/grails-app/services/dbnp/studycapturing/AssayService.groovy
r1937 r1969 34 34 * module's measurements. 35 35 * 36 * @param assay the assay for which to collect the fields 36 * @param assay the assay for which to collect the fields 37 * @param samples list of samples to retrieve the field names for. If not given, all samples from the assay are used. 37 38 * @return a map of categories as keys and field names or measurements as 38 39 * values 39 40 */ 40 def collectAssayTemplateFields(assay ) throws Exception {41 def collectAssayTemplateFields(assay, samples = null) throws Exception { 41 42 42 43 def getUsedTemplateFields = { templateEntities -> … … 50 51 } 51 52 52 def moduleError = '', moduleMeasurements = [] 53 54 try { 55 moduleMeasurements = requestModuleMeasurementNames(assay) 56 } catch (e) { 57 moduleError = e.message 58 } 59 60 def samples = assay.samples 53 def moduleError = '', moduleMeasurements = [] 54 55 try { 56 moduleMeasurements = requestModuleMeasurementNames(assay) 57 } catch (e) { 58 moduleError = e.message 59 } 60 61 if( !samples ) 62 samples = assay.samples 63 61 64 [ 'Subject Data' : getUsedTemplateFields( samples*."parentSubject".unique() ), 62 65 'Sampling Event Data' : getUsedTemplateFields( samples*."parentEvent".unique() ), … … 64 67 'Event Group' : [[name: 'name', comment: 'Name of Event Group', displayName: 'name']], 65 68 'Module Measurement Data': moduleMeasurements, 66 67 ]69 'ModuleError': moduleError 70 ] 68 71 69 72 } … … 80 83 * @param fieldMap map with categories as keys and fields as values 81 84 * @param measurementTokens selection of measurementTokens 85 * @param samples list of samples for which the data should be retrieved. 86 * Defaults to all samples from this assay. 82 87 * @return The assay data structure as described above. 83 88 */ 84 def collectAssayData(assay, fieldMap, measurementTokens ) throws Exception {89 def collectAssayData(assay, fieldMap, measurementTokens, samples = null) throws Exception { 85 90 86 91 def collectFieldValuesForTemplateEntities = { headerFields, templateEntities -> … … 92 97 map + [(headerField.displayName): templateEntities.collect { entity -> 93 98 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 99 // default to an empty string 100 def val = '' 101 102 if (entity) { 103 def field 104 try { 105 106 val = entity.getFieldValue(headerField.name) 107 108 // Convert RelTime fields to human readable strings 109 field = entity.getField(headerField.name) 110 if (field.type == TemplateFieldType.RELTIME) 111 val = new RelTime( val as long ) 112 113 } catch (NoSuchFieldException e) { /* pass */ } 114 } 115 116 (val instanceof Number) ? val : val.toString()}] 112 117 } 113 118 } … … 172 177 173 178 // Find samples and sort by name 174 def samples = assay.samples.toList().sort { it.name } 179 if( !samples ) 180 samples = assay.samples.toList().sort { it.name } 175 181 176 182 def eventFieldMap = [:] … … 190 196 if (measurementTokens) { 191 197 192 193 194 195 196 197 198 199 } 200 198 try { 199 moduleMeasurementData = requestModuleMeasurements(assay, measurementTokens, samples) 200 } catch (e) { 201 moduleMeasurementData = ['error' : ['Module error, module not available or unknown assay'] * samples.size() ] 202 moduleError = e.message 203 } 204 205 } 206 201 207 [ 'Subject Data' : getFieldValues(samples, fieldMap['Subject Data'], 'parentSubject'), 202 'Sampling Event Data' : getFieldValues(samples, fieldMap['Sampling Event Data'], 'parentEvent'),203 204 205 206 208 'Sampling Event Data' : getFieldValues(samples, fieldMap['Sampling Event Data'], 'parentEvent'), 209 'Sample Data' : getFieldValues(samples, fieldMap['Sample Data']), 210 'Event Group' : eventFieldMap, 211 'Module Measurement Data' : moduleMeasurementData, 212 'ModuleError' : moduleError 207 213 ] 208 214 } … … 268 274 269 275 def path = moduleUrl + "/rest/getMeasurements/query" 270 271 272 273 274 275 276 276 def query = "assayToken=${assay.giveUUID()}" 277 def jsonArray 278 279 try { 280 jsonArray = moduleCommunicationService.callModuleMethod(moduleUrl, path, query, "POST") 281 } catch (e) { 282 throw new Exception("An error occured while trying to get the measurement tokens from the $assay.module.name. \ 277 283 This means the module containing the measurement data is not available right now. Please try again \ 278 284 later or notify the system administrator if the problem persists. URL: $path?$query.") 279 285 } 280 286 281 287 def result = jsonArray.collect { … … 285 291 return it.toString() 286 292 } 287 293 288 294 return result 289 295 } … … 309 315 def path = moduleUrl + "/rest/getMeasurementData/query" 310 316 311 317 def query = "assayToken=$assay.assayUUID$tokenString" 312 318 313 319 def sampleTokens = [], measurementTokens = [], moduleData = [] 314 320 315 316 317 318 321 try { 322 (sampleTokens, measurementTokens, moduleData) = moduleCommunicationService.callModuleMethod(moduleUrl, path, query, "POST") 323 } catch (e) { 324 throw new Exception("An error occured while trying to get the measurement data from the $assay.module.name. \ 319 325 This means the module containing the measurement data is not available right now. Please try again \ 320 326 later or notify the system administrator if the problem persists. URL: $path?$query.") 321 327 } 322 328 323 329 if (!sampleTokens?.size()) return [] … … 350 356 } else { 351 357 352 353 354 355 356 357 358 358 def val 359 def measurement = moduleData[ valueIndex ] 360 361 if (measurement == JSONObject.NULL) val = "" 362 else if (measurement instanceof Number) val = measurement 363 else if (measurement.isDouble()) val = measurement.toDouble() 364 else val = measurement.toString() 359 365 measurements << val 360 366 } … … 463 469 464 470 /** 471 * Merges the data from multiple studies into a structure that can be exported to an excel file. The format for each assay is 472 * 473 * [Category1: 474 * [Column1: [1,2,3], Column2: [4,5,6]], 475 * Category2: 476 * [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]] 477 * 478 * Where the category describes the category of data that is presented (e.g. subject, sample etc.) and the column names describe 479 * the fields that are present. Each entry in the lists shows the value for that column for an entity. In this case, 3 entities are described. 480 * Each field should give values for all entities, so the length of all value-lists should be the same. 481 * 482 * Example: If the following input is given (2 assays) 483 * 484 * [ 485 * [Category1: 486 * [Column1: [1,2,3], Column2: [4,5,6]], 487 * Category2: 488 * [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]], 489 * [Category1: 490 * [Column1: [16,17], Column6: [18,19]], 491 * Category3: 492 * [Column3: [20,21], Column8: [22,23]]] 493 * ] 494 * 495 * the output will be (5 entries for each column, empty values for fields that don't exist in some assays) 496 * 497 * [ 498 * [Category1: 499 * [Column1: [1,2,3,16,17], Column2: [4,5,6,,], Column6: [,,,18,19]], 500 * Category2: 501 * [Column3: [7,8,9,,], Column4: [10,11,12,,], Column5: [13,14,15,,]], 502 * Category3: 503 * [Column3: [,,,20,21], Column8: [,,,22,23]] 504 * ] 505 * 506 * 507 * @param columnWiseAssayData List with each entry being the column wise data of an assay. The format for each 508 * entry is described above. The data MUST have a category named 'Sample Data' and in that map a field 509 * named 'id'. This field is used for matching rows. However, the column is removed, unless 510 * removeIdColumn is set to false 511 * @param removeIdColumn If set to true (default), the values for the sample id are removed from the output. 512 * @return Hashmap Combined assay data, in the same structure as each input entry. Empty values are given as an empty string. 513 * So for input entries 514 */ 515 def mergeColumnWiseDataOfMultipleStudiesForASetOfSamples(def columnWiseAssayData, boolean removeIdColumn = true ) { 516 // Merge all assays and studies into one list 517 def mergedData = mergeColumnWiseDataOfMultipleStudies( columnWiseAssayData ) 518 519 // A map with keys being the sampleIds, and the values are the indices of that sample in the values list 520 def idMap = [:] 521 522 // A map with the key being an index in the value list, and the value is the index the values should be copied to 523 def convertMap = [:] 524 525 for( int i = 0; i < mergedData[ "Sample Data" ][ "id" ].size(); i++ ) { 526 def id = mergedData[ "Sample Data" ][ "id" ][ i ]; 527 528 if( idMap[ id ] == null ) { 529 // This id occurs for the first time 530 idMap[ id ] = i; 531 convertMap[ i ] = i; 532 } else { 533 convertMap[ i ] = idMap[ id ]; 534 } 535 } 536 537 /* 538 * Example output: 539 * idMap: [ 12: 0, 24: 1, 26: 3 ] 540 * convertMap: [ 0: 0, 1: 1, 2: 0, 3: 3, 4: 3 ] 541 * (meaning: rows 0, 1 and 3 should remain, row 2 should be merged with row 0 and row 4 should be merged with row 3) 542 * 543 * The value in the convertMap is always lower than its key. So we sort the convertMap on the keys. That way, we can 544 * loop through the values and remove the row that has been merged. 545 */ 546 547 convertMap.sort { a, b -> b.key <=> a.key }.each { 548 def row = it.key; 549 def mergeWith = it.value; 550 551 if( row != mergeWith ) { 552 // Combine the data on row [row] with the data on row [mergeWith] 553 554 mergedData.each { 555 def cat = it.key; def fields = it.value; 556 fields.each { fieldData -> 557 def fieldName = fieldData.key; 558 def fieldValues = fieldData.value; 559 560 // If one of the fields to merge is empty, use the other one 561 // Otherwise the values should be the same (e.g. study, subject, sample data) 562 fieldValues[ mergeWith ] = ( fieldValues[ mergeWith ] == null || fieldValues[ mergeWith ] == "" ) ? fieldValues[ row ] : fieldValues[ mergeWith ] 563 564 // Remove the row from this list 565 fieldValues.remove( row ); 566 } 567 } 568 } 569 } 570 571 // Remove sample id if required 572 if( removeIdColumn ) 573 mergedData[ "Sample Data" ].remove( "id" ); 574 575 return mergedData 576 } 577 578 /** 465 579 * Converts column 466 580 * @param columnData multidimensional map containing column data. … … 564 678 /** 565 679 * Export row wise data in CSV to a stream. All values are surrounded with 566 680 * double quotes (" "). 567 681 * 568 682 * @param rowData List of lists containing for each row all cell values … … 572 686 def exportRowWiseDataToCSVFile(rowData, outputStream, outputDelimiter = '\t', locale = java.util.Locale.US) { 573 687 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 688 def formatter = NumberFormat.getNumberInstance(locale) 689 formatter.setGroupingUsed false // we don't want grouping (thousands) separators 690 691 outputStream << rowData.collect { row -> 692 row.collect{ 693 694 // omit quotes in case of numeric values and format using chosen locale 695 if (it instanceof Number) return formatter.format(it) 696 697 def s = it?.toString() ?: '' 698 699 def addQuotes = false 700 701 // escape double quotes with double quotes if they exist and 702 // enable surround with quotes 703 if (s.contains('"')) { 704 addQuotes = true 705 s = s.replaceAll('"','""') 706 } else { 707 // enable surround with quotes in case of comma's 708 if (s.contains(',') || s.contains('\n')) addQuotes = true 709 } 710 711 addQuotes ? "\"$s\"" : s 712 713 }.join(outputDelimiter) 714 }.join('\n') 601 715 602 716 outputStream.close()
Note: See TracChangeset
for help on using the changeset viewer.