- Timestamp:
- Feb 9, 2011, 9:04:29 AM (12 years ago)
- Location:
- trunk
- Files:
-
- 6 added
- 16 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/grails-app/conf/BaseFilters.groovy
r9 r12 16 16 import grails.converters.* 17 17 import javax.servlet.http.HttpServletResponse 18 19 import org.apache.catalina.connector.Response; 18 20 import org.codehaus.groovy.grails.commons.ConfigurationHolder 19 21 20 22 import nl.tno.metagenomics.auth.User; 23 import nl.tno.metagenomics.integration.*; 21 24 22 25 class BaseFilters { … … 121 124 session.user = new User(identifier: user.id, username: user.username).save(flush: true) 122 125 } 126 127 return true; 123 128 } catch(Exception e) { 124 129 log.error("Unable to fetch user from GSCF", e) … … 134 139 } 135 140 } 141 restSynchronization(controller:'rest', action:'*') { 142 before = { 143 // Synchronize studies normally, but never issue a redirect 144 synchronizationService.sessionToken = session.sessionToken 145 synchronizationService.user = session.user 136 146 137 fullSynchronization(controller:'*', action:'*') { 138 before = { 139 // Never perform full synchronization on rest call when the synchronize controller is used 147 try { 148 synchronizationService.synchronizeStudies() 149 } catch( NotAuthenticatedException e ) { 150 // Something went wrong in authentication. Redirect the user to GSCF to authenticate again 151 log.info "User is not authenticated during synchronizatino. Returning 401 status" 152 response.setStatus( Response.SC_UNAUTHORIZED, "User is not authorized" ) 153 return false; 154 } catch( Exception e ) { 155 // Synchronization fails. Log error and continue; don't bother the user with it 156 log.error e.getMessage() 157 } 158 return true; 159 } 160 } 161 synchronization(controller:'*', action:'*') { 162 before = { 163 // Never perform full synchronization on rest call or when the synchronize controller is used 140 164 if( controllerName == "rest" || controllerName == "synchronize" || controllerName == "dbUtil" ) { 141 165 return true; … … 149 173 redirect( url: synchronizationService.urlForFullSynchronization( params ) ); 150 174 return false 175 } else { 176 // Synchronize studies normally 177 synchronizationService.sessionToken = session.sessionToken 178 synchronizationService.user = session.user 179 180 try { 181 synchronizationService.synchronizeStudies() 182 } catch( NotAuthenticatedException e ) { 183 // Something went wrong in authentication. Redirect the user to GSCF to authenticate again 184 log.info "User is not authenticated during synchronizatino. Redirecting to GSCF." 185 redirect( url: gscfService.urlAuthRemote(params, session.sessionToken) ) 186 } catch( Exception e ) { 187 // Synchronization fails. Log error and continue; don't bother the user with it 188 log.error e.getMessage() 189 } 151 190 } 152 191 -
trunk/grails-app/controllers/nl/tno/metagenomics/AssayController.groovy
r9 r12 194 194 assay.assaySamples.each { assaySample -> 195 195 def assaySampleParams = sampleParams.get( assaySample.id as String ); 196 println assaySampleParams 196 197 197 if( assaySampleParams ) { 198 198 assaySample.tagName = assaySampleParams.tagName -
trunk/grails-app/controllers/nl/tno/metagenomics/FastaController.groovy
r7 r12 25 25 if( !names ) { 26 26 flash.message = "No files uploaded for processing" 27 redirect( controller: params.entityType, action: 'show', 'id': params.id) 27 if( params.entityType && params.id) 28 redirect( controller: params.entityType, action: 'show', 'id': params.id) 29 else 30 redirect( url: "" ) 31 28 32 return 29 33 } … … 115 119 * The second parameter is a callback function to update progress indicators 116 120 */ 121 def httpSession = session; 117 122 def parsedFiles = fastaService.parseFiles( filenames, { files, bytes, totalFiles, totalBytes -> 118 session.processProgress.numFiles += totalFiles;119 session.processProgress.numBytes += totalBytes;120 session.processProgress.filesProcessed = files;121 session.processProgress.bytesProcessed = bytes;123 httpSession.processProgress.numFiles += totalFiles; 124 httpSession.processProgress.numBytes += totalBytes; 125 httpSession.processProgress.filesProcessed = files; 126 httpSession.processProgress.bytesProcessed = bytes; 122 127 } ); 123 128 -
trunk/grails-app/controllers/nl/tno/metagenomics/RunController.groovy
r9 r12 45 45 return 46 46 } 47 47 48 48 Assay assay = null 49 49 if( params.assayId ) { 50 50 assay = getAssay( params.assayId ) 51 51 52 52 if( !assay ) { 53 53 render flash.error; … … 93 93 // Set properties to the run 94 94 params.parameterFile = params.editParameterFile 95 96 println "Edit run: " + params 95 97 96 run.setPropertiesFromForm( params ); 98 97 … … 102 101 flash.error = "Run could not be saved: " + run.getErrors(); 103 102 } 104 103 105 104 Assay assay = getAssay(params.assayId); 106 105 flash.error = ""; 107 106 108 107 if( assay ) { 109 108 redirect( controller: 'assay', action: 'show', id: assay.id) … … 247 246 return 248 247 } 249 248 250 249 // Make it only possible to update samples writable by the user 251 250 def assaySamples = run.assaySamples.findAll { it.assay.study.canWrite( session.user ) } 252 251 253 252 def excelData = sampleExcelService.updateTagsByExcel( matchColumns, session.possibleFields, file, assaySamples ); 254 253 … … 304 303 * 305 304 *************************************************************************/ 305 306 /** 307 * Adds existing samples to this run 308 */ 309 def addSamples = { 310 Run run = getRun( params.id ); 311 312 if( !run ) { 313 redirect(controller: 'study') 314 return 315 } 316 317 // Add checked runs to this assay 318 def assaySamples = params.assaySamples 319 if( assaySamples instanceof String ) { 320 assaySamples = [ assaySamples ] 321 } 322 323 def numAdded = 0; 324 assaySamples.each { assaySampleId -> 325 try { 326 def assaySample = AssaySample.findById( assaySampleId as Long ) 327 if( run.assaySamples == null || !run.assaySamples.contains( assaySample ) ) { 328 run.addToAssaySamples( assaySample ); 329 numAdded++; 330 } 331 } catch( Exception e ) {} 332 } 333 334 flash.message = numAdded + " samples are added to this run." 335 redirect( action: 'show', id: params.id) 336 } 337 338 /** 339 * Removes sample from this run 340 */ 341 def removeSample = { 342 Run run = getRun( params.id ); 343 344 if( !run ) { 345 redirect(controller: 'study') 346 return 347 } 348 349 if( !params.assaySampleId ) { 350 flash.message = "No sample id given" 351 redirect(action: 'show', id: params.id) 352 return 353 } 354 355 def assaySample 356 357 try { 358 assaySample = AssaySample.findById( params.assaySampleId as Long ) 359 } catch( Exception e ) { 360 log.error e 361 flash.message = "Incorrect assaysample id given: " + params.assaySampleId 362 redirect(action: 'show', id: params.id) 363 return 364 } 365 366 if( run.assaySamples.contains( assaySample ) ) { 367 run.removeFromAssaySamples( assaySample ); 368 flash.message = "The sample has been removed from this run." 369 } else { 370 flash.message = "The given sample was not associated with this run." 371 } 372 373 redirect( action: 'show', id: params.id) 374 } 375 306 376 307 377 /** … … 333 403 } 334 404 335 flash.message = numAdded + " runs are added to this assay."405 flash.message = numAdded + " assays are added to this run." 336 406 redirect( action: 'show', id: params.id) 337 407 } … … 347 417 return 348 418 } 349 419 350 420 if( !params.assay_id ) { 351 421 flash.message = "No assay id given" -
trunk/grails-app/controllers/nl/tno/metagenomics/StudyController.groovy
r4 r12 8 8 9 9 def index = { 10 // Synchronize all studies11 synchronizationService.sessionToken = session.sessionToken12 synchronizationService.user = session.user13 14 synchronizationService.synchronizeStudies()15 16 10 // Clean the upload directory 17 11 fileService.cleanDirectory(); -
trunk/grails-app/controllers/nl/tno/metagenomics/integration/RestController.groovy
r9 r12 1 1 package nl.tno.metagenomics.integration 2 3 2 4 3 import grails.converters.* 5 4 import nl.tno.metagenomics.Study 5 import nl.tno.metagenomics.Sample 6 import nl.tno.metagenomics.Assay 7 8 import org.apache.catalina.connector.Response; 6 9 import org.codehaus.groovy.grails.commons.ConfigurationHolder 7 10 … … 33 36 class RestController { 34 37 def synchronizationService 38 39 /****************************************************************/ 40 /* REST resource for handling study change in GSCF */ 41 /****************************************************************/ 42 43 /** 44 * Is called by GSCF when a study is added, changed or deleted. 45 * Sets the 'dirty' flag of a study to true, so that it will be updated 46 * next time the study is asked for. 47 * 48 * @param studyToken 49 */ 50 def notifyStudyChange = { 51 def studyToken = params.studyToken 52 53 if( !studyToken ) { 54 response.sendError(400, "No studyToken given" ) 55 return 56 } 57 58 // Search for the changed study 59 def study = Study.findByStudyToken( studyToken ); 60 61 // If the study is not found, it is added in GSCF. Add a dummy (dirty) study, in order to 62 // update it immediately when asked for 63 if( !study ) { 64 log.info( "METAGENOMICS: GSCF notification for new study " + studyToken ); 65 study = new Study( 66 name: "", 67 studyToken: studyToken, 68 isDirty: true 69 ) 70 } else { 71 log.info( "METAGENOMICS: GSCF notification for existing study " + studyToken ); 72 study.isDirty = true; 73 } 74 study.save(flush:true); 75 76 def jsonData = [ 'studyToken': studyToken, message: "Notify succesful" ]; 77 78 render jsonData as JSON 79 } 80 81 /** 82 * Return URL to view an assay. 83 * 84 * @param assayToken 85 * @return URL to view an assay as single hash entry with key 'url'. 86 * 87 */ 88 def getAssayURL = { 89 def assayToken = params.assayToken 90 91 if( !assayToken ) { 92 render [] as JSON 93 return 94 } 95 96 def assay = Assay.findByAssayToken( assayToken ) 97 98 // If the assay is not found, try synchronizing 99 synchronizationService.sessionToken = session.sessionToken 100 101 if( !assay ) { 102 synchronizationService.synchronizeStudies() 103 assay = Assay.findByAssayToken( assayToken ); 104 105 if( !assay ) { 106 response.sendError(404, "Not Found" ) 107 return; 108 } 109 } else { 110 try { 111 synchronizationService.synchronizeAssay(assay); 112 } catch( Exception e ) { 113 response.sendError( 500, e.getMessage()) 114 return 115 } 116 117 def url = [ 'url' : ConfigurationHolder.config.grails.serverURL + '/assay/show/' + assay.id.toString() ] 118 119 render url as JSON 120 } 121 } 122 123 /***************************************************/ 124 /* REST resources related to the querying in GSCF */ 125 /***************************************************/ 126 127 /** 128 * Retrieves a list of fields that could be queried when searching for a specific entity. 129 * 130 * The module is allowed to return different fields when the user searches for different entities 131 * 132 * Example call: [moduleurl]/rest/getQueryableFields?entity=Study&entity=Sample 133 * Example response: { "Study": [ "# sequences" ], "Sample": [ "# sequences", "# bacteria" ] } 134 * 135 * @param params.entity Entity that is searched for. Might be more than one. If no entity is given, 136 * a list of searchable fields for all entities is given 137 * @return JSON List with the names of the fields 138 */ 139 def getQueryableFields = { 140 // We don't really care about the entity. The only thing is that this module 141 // is only aware of studies, assays and samples, but doesn't know anything about 142 // subjects or events. If the user searches for those entities (maybe in the future) 143 // this module doesn't have anything to search for. 144 145 def entities = params.entity ?: [] 146 147 if( entities instanceof String ) 148 entities = [entities] 149 else 150 entities = entities.toList() 151 152 if( !entities ) 153 entities = [ "Study", "Assay", "Sample" ] 154 155 156 def fields = [:]; 157 entities.unique().each { entity -> 158 switch( entity ) { 159 case "Study": 160 case "Assay": 161 case "Sample": 162 fields[ entity ] = [ "# sequences", "Tag name", "Run name" ] 163 break; 164 default: 165 // Do nothing 166 break; 167 } 168 } 169 170 render fields as JSON 171 } 172 173 /** 174 * Returns data for the given field and entities. 175 * 176 * Example call: [moduleurl]/rest/getQueryableFieldData?entity=Study&tokens=abc1&tokens=abc2&fields=# sequences&fields=# bacteria 177 * Example response: { "abc1": { "# sequences": 141, "# bacteria": 0 }, "abc2": { "#sequences": 412 } } 178 * 179 * @param params.entity Entity that is searched for 180 * @param params.tokens One or more tokens of the entities that the data should be returned for 181 * @param params.fields One or more field names of the data to be returned. 182 * @return JSON Map with keys being the entity tokens and the values being maps with entries [field] = [value]. Not all 183 * fields and tokens that are asked for have to be returned by the module (e.g. when a specific entity can 184 * not be found, or a value is not present for an entity) 185 */ 186 def getQueryableFieldData = { 187 def entity = params.entity; 188 def tokens = params.tokens ?: [] 189 def fields = params.fields ?: [] 190 191 if( tokens instanceof String ) 192 tokens = [tokens] 193 else 194 tokens = tokens.toList(); 195 196 if( fields instanceof String ) 197 fields = [fields] 198 else 199 fields = fields.toList(); 200 201 // Only search for unique tokens and fields 202 tokens = tokens.unique() 203 fields = fields.unique() 204 205 // Without tokens or fields we can only return an empty list 206 def map = [:] 207 if( tokens.size() == 0 || fields.size() == 0 ) { 208 log.trace "Return empty string for getQueryableFieldData: #tokens: " + tokens.size() + " #fields: " + fields.size() 209 render map as JSON 210 return; 211 } 212 213 for( token in tokens ) { 214 def object = getQueryableObject( entity, token ); 215 216 if( object ) { 217 // Check whether the user has sufficient privileges: 218 def study; 219 switch( entity ) { 220 case "Study": 221 study = object; 222 break; 223 case "Assay": 224 case "Sample": 225 study = object.study 226 break; 227 default: 228 log.error "Incorrect entity used: " + entity; 229 continue; 230 } 231 232 if( !study.canRead( session.user ) ) { 233 log.error "Data was requested for " + entity.toLowerCase() + " " + object.name + " but the user " + session.user.username + " doesn't have the right privileges." 234 continue; 235 } else { 236 log.error "Data was requested for " + entity.toLowerCase() + " " + object.name + ": " + session.user.username + " - " + study.canRead( session.user ) 237 } 238 239 map[ token ] = [:] 240 fields.each { field -> 241 def v = getQueryableFieldValue( entity, object, field ); 242 if( v != null ) 243 map[ token ][ field ] = v 244 } 245 } else { 246 log.trace "No " + entity + " with token " + token + " found." 247 } 248 } 249 250 render map as JSON 251 } 252 253 /** 254 * Searches for a specific entity 255 * 256 * @param entity Entity to search in 257 * @param token Token of the entity to search in 258 * @return 259 */ 260 protected def getQueryableObject( def entity, def token ) { 261 switch( entity ) { 262 case "Study": 263 return Study.findByStudyToken( token ); 264 case "Assay": 265 return Assay.findByAssayToken( token ); 266 case "Sample": 267 return Sample.findBySampleToken( token ); 268 default: 269 // Other entities can't be handled 270 return null; 271 } 272 } 273 274 /** 275 * Searches for the value of a specific field in a specific entity 276 * 277 * @param entity Entity of the given object 278 * @param object Object to search in 279 * @param field Field value to retrieve 280 * @return 281 */ 282 protected def getQueryableFieldValue( def entity, def object, def field ) { 283 if( !entity || !object || !field ) 284 return null; 285 286 // First determine all assaysamples involved, in order to return data later. 287 // All data that has to be returned is found in assaysamples 288 def assaySamples 289 290 switch( entity ) { 291 case "Study": 292 assaySamples = object.assays*.assaySamples; 293 if( assaySamples ) { 294 assaySamples = assaySamples.flatten() 295 } 296 break; 297 case "Assay": 298 case "Sample": 299 assaySamples = object.assaySamples; 300 break; 301 default: 302 // Other entities can't be handled 303 return null; 304 } 305 306 // If no assaySamples are involved, return null as empty value 307 if( !assaySamples ) { 308 return null; 309 } 310 311 // Now determine the exact field to return 312 switch( field ) { 313 // Returns the total number of sequences in this sample 314 case "# sequences": 315 return assaySamples.collect { it.numSequences() }.sum(); 316 // Returns the unique tag names 317 case "Tag name": 318 return assaySamples.collect { it.tagName }.findAll { it != null }.unique(); 319 case "Run name": 320 return assaySamples.collect { it.run?.name }.findAll { it != null }.unique(); 321 // Other fields are not handled 322 default: 323 return null; 324 } 325 326 } 327 328 private def checkAssayToken( def assayToken ) { 329 if( !assayToken || assayToken == null ) { 330 return false 331 } 332 def list = [] 333 def assay = Assay.findByAssayToken( assayToken ) 334 335 if( !assay || assay == null ) { 336 return false; 337 } 338 339 return assay; 340 } 341 342 /****************************************************************/ 343 /* REST resources for exporting data from GSCF */ 344 /****************************************************************/ 345 346 /** 347 * Retrieves a list of actions that can be performed on data with a specific entity. 348 * 349 * The module is allowed to return different fields when the user searches for different entities 350 * 351 * Example call: [moduleurl]/rest/getPossibleActions?entity=Assay&entity=Sample 352 * Example response: { "Assay": [ { name: "excel", description: "Export as excel" } ], 353 * "Sample": [ { name: "excel", description: "Export as excel" }, { name: "fasta", description: : "Export as fasta" } ] } 354 * 355 * @param params.entity Entity that is searched for. Might be more than one. If no entity is given, 356 * a list of searchable fields for all entities is given 357 * @return JSON Hashmap with keys being the entities and the values are lists with the action this module can 358 * perform on this entity. The actions as hashmaps themselves, with keys 'name' and 'description' 359 */ 360 def getPossibleActions = { 361 def entities = params.entity ?: [] 362 363 if( entities instanceof String ) 364 entities = [entities] 365 else 366 entities = entities.toList() 367 368 if( !entities ) 369 entities = [ "Study", "Assay", "Sample" ] 370 371 def actions = [:]; 372 entities.unique().each { entity -> 373 switch( entity ) { 374 case "Study": 375 actions[ entity ] = [ [ name: "excel", description: "Export as excel" ] ] 376 break; 377 case "Assay": 378 actions[ entity ] = [ [ name: "fasta", description: "Export as fasta" ] ] 379 break; 380 case "Sample": 381 actions[ entity ] = [ [ name: "fasta", description: "Export as fasta" ] ] 382 break; 383 default: 384 // Do nothing 385 break; 386 } 387 } 388 389 render actions as JSON 390 } 35 391 36 392 /****************************************************************/ … … 68 424 } 69 425 70 71 426 /** 72 427 * Return measurement metadata for measurement … … 109 464 } 110 465 111 112 /* 466 /** 113 467 * Return list of measurement data. 114 468 * … … 265 619 } 266 620 267 /** 268 * Is called by GSCF when a study is added, changed or deleted. 269 * Sets the 'dirty' flag of a study to true, so that it will be updated 270 * next time the study is asked for. 271 * 272 * @param studyToken 273 */ 274 def notifyStudyChange = { 275 def studyToken = params.studyToken 276 277 if( !studyToken ) { 278 response.sendError(400, "No studyToken given" ) 279 return 280 } 281 282 // Search for the changed study 283 def study = Study.findByStudyToken( studyToken ); 284 285 // If the study is not found, it is added in GSCF. Add a dummy (dirty) study, in order to 286 // update it immediately when asked for 287 if( !study ) { 288 log.info( "METAGENOMICS: GSCF notification for new study " + studyToken ); 289 study = new Study( 290 name: "", 291 studyToken: studyToken, 292 isDirty: true 293 ) 294 } else { 295 log.info( "METAGENOMICS: GSCF notification for existing study " + studyToken ); 296 study.isDirty = true; 297 } 298 study.save(flush:true); 299 300 def jsonData = [ 'studyToken': studyToken, message: "Notify succesful" ]; 301 302 render jsonData as JSON 303 } 304 305 /** 306 * Return URL to view an assay. 307 * 308 * @param assayToken 309 * @return URL to view an assay as single hash entry with key 'url'. 310 * 311 */ 312 def getAssayURL = { 313 def assayToken = params.assayToken 314 315 if( !assayToken ) { 316 render [] as JSON 317 return 318 } 319 320 def assay = Assay.findByAssayToken( assayToken ) 321 322 // If the assay is not found, try synchronizing 323 synchronizationService.sessionToken = session.sessionToken 324 325 if( !assay ) { 326 synchronizationService.synchronizeStudies() 327 assay = Assay.findByAssayToken( assayToken ); 328 329 if( !assay ) { 330 response.sendError(404, "Not Found" ) 331 return; 332 } 333 } else { 334 try { 335 synchronizationService.synchronizeAssay(assay); 336 } catch( Exception e ) { 337 response.sendError( 500, e.getMessage()) 338 return 339 } 340 341 def url = [ 'url' : ConfigurationHolder.config.grails.serverURL + '/assay/show/' + assay.id.toString() ] 342 343 render url as JSON 344 } 345 } 346 347 /***************************************************/ 348 /* REST resources related to the querying in GSCF */ 349 /***************************************************/ 350 351 /** 352 * Retrieves a list of fields that could be queried when searching for a specific entity. 353 * 354 * The module is allowed to return different fields when the user searches for different entities 355 * 356 * Example call: [moduleurl]/rest/getQueryableFields?entity=Study&entity=Sample 357 * Example response: { "Study": [ "# sequences" ], "Sample": [ "# sequences", "# bacteria" ] } 358 * 359 * @param params.entity Entity that is searched for. Might be more than one. If no entity is given, 360 * a list of searchable fields for all entities is given 361 * @return JSON List with the names of the fields 362 */ 363 def getQueryableFields = { 364 // We don't really care about the entity. The only thing is that this module 365 // is only aware of studies, assays and samples, but doesn't know anything about 366 // subjects or events. If the user searches for those entities (maybe in the future) 367 // this module doesn't have anything to search for. 368 369 def entities = params.entity ?: [] 370 371 if( entities instanceof String ) 372 entities = [entities] 373 else 374 entities = entities.toList() 375 376 if( !entities ) 377 entities = [ "Study", "Assay", "Sample" ] 378 379 380 def fields = [:]; 381 entities.unique().each { entity -> 382 switch( entity ) { 383 case "Study": 384 case "Assay": 385 case "Sample": 386 fields[ entity ] = [ "# sequences" ] 387 break; 388 default: 389 // Do nothing 390 break; 391 } 392 } 393 394 render fields as JSON 395 } 396 397 /** 398 * Returns data for the given field and entities. 399 * 400 * Example call: [moduleurl]/rest/getQueryableFieldData?entity=Study&tokens=abc1&tokens=abc2&fields=# sequences&fields=# bacteria 401 * Example response: { "abc1": { "# sequences": 141, "# bacteria": 0 }, "abc2": { "#sequences": 412 } } 402 * 403 * @param params.entity Entity that is searched for 404 * @param params.tokens One or more tokens of the entities that the data should be returned for 405 * @param params.fields One or more field names of the data to be returned. 406 * @return JSON Map with keys being the entity tokens and the values being maps with entries [field] = [value]. Not all 407 * fields and tokens that are asked for have to be returned by the module (e.g. when a specific entity can 408 * not be found, or a value is not present for an entity) 409 */ 410 def getQueryableFieldData = { 411 def entity = params.entity; 412 def tokens = params.tokens ?: [] 413 def fields = params.fields ?: [] 414 415 if( tokens instanceof String ) 416 tokens = [tokens] 417 else 418 tokens = tokens.toList(); 419 420 if( fields instanceof String ) 421 fields = [fields] 422 else 423 fields = fields.toList(); 424 425 // Only search for unique tokens and fields 426 tokens = tokens.unique() 427 fields = fields.unique() 428 429 // Without tokens or fields we can only return an empty list 430 def map = [:] 431 if( tokens.size() == 0 || fields.size() == 0 ) { 432 log.trace "Return empty string for getQueryableFieldData: #tokens: " + tokens.size() + " #fields: " + fields.size() 433 render map as JSON 434 return; 435 } 436 437 tokens.each { token -> 438 def object = getQueryableObject( entity, token ); 439 if( object ) { 440 map[ token ] = [:] 441 fields.each { field -> 442 def v = getQueryableFieldValue( entity, object, field ); 443 if( v != null ) 444 map[ token ][ field ] = v 445 } 446 } else { 447 log.trace "No " + entity + " with token " + token + " found." 448 } 449 } 450 451 render map as JSON 452 } 453 454 /** 455 * Searches for a specific entity 456 * 457 * @param entity Entity to search in 458 * @param token Token of the entity to search in 459 * @return 460 */ 461 protected def getQueryableObject( def entity, def token ) { 462 switch( entity ) { 463 case "Study": 464 return Study.findByStudyToken( token ); 465 case "Assay": 466 return Assay.findByAssayToken( token ); 467 case "Sample": 468 return Sample.findBySampleToken( token ); 469 default: 470 // Other entities can't be handled 471 return null; 472 } 473 } 474 475 /** 476 * Searches for the value of a specific field in a specific entity 477 * 478 * @param entity Entity of the given object 479 * @param object Object to search in 480 * @param field Field value to retrieve 481 * @return 482 */ 483 protected def getQueryableFieldValue( def entity, def object, def field ) { 484 if( !entity || !object || !field ) 485 return null; 486 487 switch( entity ) { 488 case "Study": 489 switch( field ) { 490 // Returns the total number of sequences in this study 491 case "# sequences": 492 def assaySamples = object.assays*.assaySamples; 493 if( assaySamples ) { 494 assaySamples = assaySamples.flatten() 495 return assaySamples.collect { it.numSequences() }.sum(); 496 } else { 497 return null; 498 } 499 // Other fields are not handled 500 default: 501 return null; 502 } 503 case "Assay": 504 switch( field ) { 505 // Returns the total number of sequences in this study 506 case "# sequences": 507 def assaySamples = assay.assaySamples; 508 if( assaySamples ) { 509 return assaySamples.collect { it.numSequences() }.sum(); 510 } else { 511 return null; 512 } 513 // Other fields are not handled 514 default: 515 return null; 516 } 517 case "Sample": 518 switch( field ) { 519 // Returns the total number of sequences in this study 520 case "# sequences": 521 def assaySamples = sample.assaySamples; 522 if( assaySamples ) { 523 return assaySamples.collect { it.numSequences() }.sum(); 524 } else { 525 return null; 526 } 527 // Other fields are not handled 528 default: 529 return null; 530 } 531 default: 532 // Other entities can't be handled 533 return null; 534 } 535 } 536 537 /** 538 * @param (no parameters) 539 * @return metadata fields that can be available for any assay stored in the module 540 */ 541 def getAssayMetadataFields = { 542 def exampleType = Assay.find() 543 def list = [] 544 545 exampleType.giveFields.each{ 546 list.push( it.name ) 547 } 548 549 render list as JSON 550 } 551 552 /** 553 * 554 * @param assayToken 555 * @param measurementTokens 556 * @return metadata values for the specified assay 557 */ 558 def getAssayMetadataValues = { 559 def error = CommunicationManager.hasValidParameters( params, 'assayToken' ) 560 if(error) { 561 render error as JSON 562 return 563 } 564 def assay = SimpleAssay.findByExternalAssayID( params.assayToken ) 565 def types = [] 566 def hash = [] 567 token.getMeasurements.each { 568 types.push( it.type ) 569 } 570 types.unique.each { type -> 571 type.fields.each { field -> 572 hash[field.name] = field.value 573 } 574 } 575 render hash as JSON 576 } 577 578 579 580 /** 581 * @param assayToken 582 * @return list of metadata fields that are available for the samples in the specified assay 583 */ 584 def getSampleMetadataFields = { 585 def error = CommunicationManager.hasValidParameters( params, 'assayToken' ) 586 if(error) { 587 render error as JSON 588 return 589 } 590 def assay = SimpleAssay.findByExternalAssayID( params.assayToken ) 591 def types = [] 592 assay.getTypes.each{ 593 types.push(it.name) 594 } 595 render types as JSON 596 } 597 598 599 600 /** 601 * @param assayToken 602 * @param list of sampleTokens 603 * @return List of sample metadata values for the specified samples in the specified assay. 604 * Each element of the list is a hash containing values for one sample. 605 * The hash contains the key "sampleToken" to indentify the sample. The other keys 606 * of the hash contain information on the actual metadata values for the sample. 607 * The keys are metadata fields, the values are the values of the corresponding 608 * SimpleAssayMeasurements. 609 */ 610 def getSampleMetadata = { 611 def error = CommunicationManager.hasValidParameters( params, 'assayToken', 'sampleToken' ) 612 if(error) { 613 render error as JSON 614 return 615 } 616 def assay = SimpleAssay.findByExternalAssayID( params.assayToken ) 617 def samples = [] 618 def list = [] 619 params.sampleToken.each{ 620 if( SimpleAssaySample.findByName(it).assay == assay ) { 621 samples.push(it) 622 } 623 } 624 samples.each{ sample -> 625 def hash = ['sampleToken':sample.name] 626 SimpleAssayMeasurement.findAllBySample(sample).each{ measurement -> 627 hash[measurement.type.name]=measumrent.value 628 } 629 list.push hash 630 } 631 render list as JSON 632 } 633 634 private def checkAssayToken( def assayToken ) { 635 if( !assayToken || assayToken == null ) { 636 return false 637 } 638 def list = [] 639 def assay = Assay.findByAssayToken( assayToken ) 640 641 if( !assay || assay == null ) { 642 return false; 643 } 644 645 return assay; 646 } 647 621 648 622 /* helper function for getSamples 649 623 * -
trunk/grails-app/controllers/nl/tno/metagenomics/integration/TrashController.groovy
r7 r12 53 53 redirect( action: "index" ); 54 54 } 55 println "Restore To: " + restoreSample 56 println "Original Sample: " + originalSample 55 57 56 trashService.restoreSample( restoreSample, originalSample ); 58 57 restoreSample.sample?.delete(); -
trunk/grails-app/domain/nl/tno/metagenomics/AssaySample.groovy
r9 r12 11 11 private long _numSequences = -1; 12 12 private float _averageQuality = -1.0; 13 private long _numQualScores = -1; 13 14 14 15 Integer numUniqueSequences // Number of unique sequences / OTUs. Is only available after preprocessing … … 137 138 return numSequences; 138 139 } 139 140 141 /** 142 * Returns the number of quality scores in the files on the system, belonging to this 143 * assay-sample combination. 144 * 145 * @return 146 */ 147 public long numQualScores() { 148 if( _numQualScores > -1 ) 149 return _numQualScores; 150 151 if( !sequenceData ) 152 return 0 153 154 long numQualScores = 0; 155 sequenceData.each { numQualScores += it.numQualScores() } 156 157 // Save as cache 158 _numQualScores = numQualScores; 159 160 return numQualScores; 161 } 140 162 /** 141 163 * Returns the average quality of the sequences in the files on the system, … … 170 192 public void resetStats() { 171 193 _numSequences = -1; 194 _numQualScores = -1; 172 195 _averageQuality = -1; 173 196 } -
trunk/grails-app/domain/nl/tno/metagenomics/SequenceData.groovy
r7 r12 33 33 } 34 34 35 36 /** 37 * Returns the number of quality scores in this data object. This equals 38 * the number of sequences iff a quality score is attached to this object 39 * 40 * @return Number of quality scores in this object 41 */ 42 public int numQualScores() { 43 if( qualityFile != null ) 44 return numSequences 45 else 46 return 0; 47 } 48 35 49 def beforeDelete = { 36 50 def permanentDir = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir.toString() ) -
trunk/grails-app/services/nl/tno/metagenomics/FastaService.groovy
r5 r12 1 1 package nl.tno.metagenomics 2 2 3 import java.io.BufferedWriter; 3 4 import java.io.File; 4 5 import java.util.ArrayList; 6 import java.util.zip.ZipEntry 7 import java.util.zip.ZipOutputStream 5 8 import org.codehaus.groovy.grails.commons.ConfigurationHolder 6 9 … … 53 56 for( int i = 0; i < filenames.size(); i++ ) { 54 57 def filename = filenames[ i ]; 55 58 56 59 if( fileService.isZipFile( filename ) ) { 57 60 // ZIP files are extracted and appended to the filenames list. 58 61 def newfiles = fileService.extractZipFile( filename, { files, bytes, totalFiles, totalBytes -> 59 60 61 62 62 filesProcessed += files; 63 bytesProcessed += bytes; 64 65 onProgress( filesProcessed, bytesProcessed, totalFiles, totalBytes ); 63 66 } ); 64 67 if( newfiles ) { … … 70 73 def file = fileService.get( filename ); 71 74 String filetype = fileService.determineFileType( file ); 72 75 73 76 if( !fileTypeValid( filetype ) ) { 74 77 // If files are not valid for parsing, delete them and return a message to the user … … 80 83 filesProcessed += files; 81 84 bytesProcessed += bytes; 82 85 83 86 onProgress( filesProcessed, bytesProcessed, 0, 0 ); 84 87 } ); 85 88 86 89 contents.filename = file.getName(); 87 90 contents.originalfilename = fileService.originalFilename( contents.filename ) 88 91 89 92 if( contents.success ) { 90 93 success << contents; … … 253 256 } 254 257 255 256 257 258 } 258 259 … … 396 397 } 397 398 398 399 /** 400 * Exports the fasta data of a list of assaysamples 401 * @param assaySamples Assaysamples to export 402 * @param outStream Outputstream to send the data to 403 * @return 404 */ 405 public def export( List assaySamples, OutputStream outStream, String name ) { 406 if( !assaySamples || assaySamples.size() == 0 ) 407 return false; 408 409 // Determine the directory the uploaded files are stored in 410 File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir ); 411 412 // First check whether qual files should be exported or not 413 // It is only exported if qual scores are available for all sequences 414 def exportQual = ( assaySamples*.numSequences().sum() == assaySamples*.numQualScores().sum() ); 415 416 // First create tags for every sample 417 def tags = []; 418 419 // Determine new tag length. Since we can use 4 characters per 420 // tag position, we only have to use 4log( #samples) 421 int tagLength = Math.ceil( Math.log( assaySamples.size() ) / Math.log( 4 ) ) 422 int tagNumber = 0; 423 424 assaySamples.each { assaySample -> 425 if( assaySample.numSequences() > 0 ) { 426 // Create a new tag for this assaysample 427 def tag = createTag( tagLength, tagNumber++); 428 429 // Save the tag for exporting 430 tags << [assaySampleId: assaySample.id, sampleName: assaySample.sample.name, assayName: assaySample.assay.name, tag: tag] 431 } 432 } 433 434 // Now create zip file for fasta and qual files 435 ZipOutputStream zipFile = new ZipOutputStream( new BufferedOutputStream( outStream ) ); 436 BufferedWriter zipWriter = new BufferedWriter( new OutputStreamWriter( zipFile ) ); 437 438 // We have to loop twice through the sequenceData, since we can't write part of the sequence 439 // file and part of the qual files mixed. We have to write the full sequence file first. 440 try { 441 zipFile.putNextEntry( new ZipEntry( name + ".fasta" ) ); 442 443 assaySamples.each { assaySample -> 444 if( assaySample.numSequences() > 0 ) { 445 def currentTag = tags.find { it.assaySampleId == assaySample.id }; 446 447 assaySample.sequenceData.each { sequenceData -> 448 copyFastaFileForExport( fileService.get( sequenceData.sequenceFile, permanentDirectory ), currentTag.tag, zipWriter) 449 } 450 } 451 } 452 zipWriter.flush(); 453 zipFile.closeEntry(); 454 455 if( exportQual ) { 456 zipFile.putNextEntry( new ZipEntry( name + ".qual" ) ); 457 458 assaySamples.each { assaySample -> 459 if( assaySample.numSequences() > 0 ) { 460 def currentTag = tags.find { it.assaySampleId == assaySample.id }; 461 462 assaySample.sequenceData.each { sequenceData -> 463 copyQualFileForExport( fileService.get( sequenceData.qualityFile, permanentDirectory ), currentTag.tag, zipWriter) 464 } 465 } 466 } 467 468 zipWriter.flush(); 469 zipFile.closeEntry(); 470 } 471 472 } catch( Exception e ) { 473 log.error "Error while writing to fastafile or qualfile: " + e.getMessage(); 474 } finally { 475 // Always close zip entry 476 try { 477 zipFile.closeEntry(); 478 } catch( Exception e ) { 479 log.error "Error while closing zip entry for fasta and qual: " + e.getMessage(); 480 } 481 } 482 483 zipFile.close(); 484 485 // Export a tab delimited file with tags 486 //exportSampleTagFile( tags ); 487 } 488 489 /** 490 * Creates a unique tag for the given number 491 * @param length 492 * @param tagNumber 493 * @return 494 */ 495 public String createTag( int length, int tagNumber ) { 496 def chars = ["C", "A", "G", "T"]; 497 def numChars = chars.size(); 498 499 if( tagNumber > numChars ** length ) 500 throw new Exception( "Given tag number (" + tagNumber + ") is too large for the specified length (" + length + ")") 501 502 String tag = ""; 503 504 for( def i = 0; i < length; i++ ) { 505 int currentChar = tagNumber % numChars 506 507 tag = chars[ currentChar ] + tag; 508 509 tagNumber = Math.floor( tagNumber / numChars ); 510 } 511 512 return tag 513 } 514 515 /** 516 * Copies the contents of the given sequence file to the output file and prepends the tag to every sequences 517 * @param inFile Filename of the file to be read 518 * @param tag 519 * @param outWriter 520 * @return 521 */ 522 protected boolean copyFastaFileForExport( File inFile, String tag, BufferedWriter outWriter ) { 523 // Walk through the lines in the file, starting with '>' 524 // (and where the following line contains a character other than '>') 525 526 try { 527 BufferedReader inReader = new BufferedReader( new FileReader( inFile ) ); 528 529 String line = null 530 String newLine = null 531 String sequence = ""; 532 533 def lengthPattern = ~/length=(\d+)/ 534 def lengthMatches 535 int length = 0; 536 int tagLength = tag.size(); 537 538 while( ( line = inReader.readLine()) != null) { 539 if( line.size() == 0 ) { 540 // Print the sequence we collected, before writing the empty line 541 printSequence( outWriter, sequence, tag ); 542 sequence = ""; 543 544 // Empty line 545 outWriter.newLine(); 546 } else if( line[ 0 ] == '>' ) { 547 // Print the sequence we collected, before writing the new comments tag 548 printSequence( outWriter, sequence, tag ); 549 sequence = ""; 550 551 // Comments line: replace length=### with the 552 // updated length, and put the line in the 553 lengthMatches = ( line =~ lengthPattern ); 554 if( lengthMatches ) { 555 length = Integer.valueOf( lengthMatches[0][1] ) + tagLength; 556 newLine = lengthMatches.replaceAll( "length=" + length ); 557 } 558 559 outWriter.write(newLine); 560 outWriter.newLine(); 561 } else { 562 // This is part of the sequence. We collect the whole sequence and 563 // determine in the end how to write it to the file 564 sequence += line; 565 } 566 } 567 568 // Print the sequence we collected, before ending the file 569 printSequence( outWriter, sequence, tag ); 570 sequence = ""; 571 572 } catch( Exception e ) { 573 log.error( "An error occurred while copying contents from " + inFile.getName() + ": " + e.getMessage() ); 574 return false; 575 } 576 } 577 578 /** 579 * Prints a sequence to the output file 580 * @param outWriter 581 * @param sequence 582 * @param tag 583 */ 584 private void printSequence( BufferedWriter outWriter, String sequence, String tag, int maxWidth = 60 ) { 585 // If no sequence is given, also don't prepend it with the tag 586 if( sequence.size() == 0 ) 587 return 588 589 // Prepend the tag to the sequence 590 sequence = tag + sequence; 591 592 // Write the sequence with a width of maxWidth characters per line 593 while( sequence ) { 594 if( sequence.size() > maxWidth ) { 595 outWriter.write( sequence[0..maxWidth-1] ); 596 sequence = sequence[maxWidth..-1] 597 } else { 598 outWriter.write( sequence ); 599 sequence = null; 600 } 601 outWriter.newLine(); 602 } 603 } 604 605 /** 606 * Copies the contents of the given qual file to the output file and prepends the tag quality score to every sequence. 607 * For every tag character '40' is prepended to the qual scores 608 * 609 * @param inFile Filename of the file to be read 610 * @param tag 611 * @param outWriter 612 * @return 613 */ 614 protected boolean copyQualFileForExport( File inFile, String tag, BufferedWriter outWriter ) { 615 // Walk through the lines in the file, starting with '>' 616 // (and where the following line contains a character other than '>') 617 try { 618 BufferedReader inReader = new BufferedReader( new FileReader( inFile ) ); 619 620 String line = null 621 String newLine = null 622 List<Integer> qualScores = [] 623 624 def lengthPattern = ~/length=(\d+)/ 625 def lengthMatches 626 int length = 0; 627 int tagLength = tag.size(); 628 629 while( ( line = inReader.readLine()) != null) { 630 if( line.size() == 0 ) { 631 // Print the quality scores we collected, before writing the empty line 632 printQualScore( outWriter, qualScores, tagLength ); 633 qualScores = []; 634 635 // Empty line 636 outWriter.newLine(); 637 } else if( line[ 0 ] == '>' ) { 638 // Print the quality scores we collected, before writing the empty line 639 printQualScore( outWriter, qualScores, tagLength ); 640 qualScores = []; 641 642 // Comments line: replace length=### with the 643 // updated length, and put the line in the 644 lengthMatches = ( line =~ lengthPattern ); 645 if( lengthMatches ) { 646 length = Integer.valueOf( lengthMatches[0][1] ) + tagLength; 647 newLine = lengthMatches.replaceAll( "length=" + length ); 648 } 649 650 outWriter.write(newLine); 651 outWriter.newLine(); 652 } else { 653 // This is part of the quality score. We collect the whole set of quality 654 // scores and determine in the end how to write it to the file 655 qualScores += line.split( " " ).collect { 656 if( !it.isInteger() ) 657 return 0; 658 else 659 return Integer.parseInt( it ); 660 }; 661 } 662 } 663 664 // Print the quality scores we collected, before ending the file 665 printQualScore( outWriter, qualScores, tagLength ); 666 qualScores = []; 667 668 } catch( Exception e ) { 669 log.error( "An error occurred while copying contents from " + inFile.getName() + ": " + e.getMessage() ); 670 return false; 671 } 672 } 673 674 /** 675 * Prints a sequence to the output file 676 * @param outWriter 677 * @param sequence 678 * @param tag 679 */ 680 private void printQualScore( BufferedWriter outWriter, List<Integer> qualScores, int tagLength, int maxWidth = 60 ) { 681 // If no qualScores are given, also don't prepend it with the tag 682 if( qualScores.size() == 0 ) 683 return 684 685 // Prepend the tag to the sequence 686 qualScores = Collections.nCopies( tagLength, 40 ) + qualScores; 687 688 // Write the sequence with a width of maxWidth characters per line 689 while( qualScores ) { 690 if( qualScores.size() > maxWidth ) { 691 outWriter.write( qualScores[0..maxWidth-1].join( " " ) ); 692 qualScores = qualScores[maxWidth..-1] 693 } else { 694 outWriter.write( qualScores.join( " " ) ); 695 qualScores = null; 696 } 697 outWriter.newLine(); 698 } 699 } 399 700 400 701 } -
trunk/grails-app/services/nl/tno/metagenomics/files/FileService.groovy
r5 r12 147 147 directory = getUploadDir() 148 148 149 println "Moving " + file + " with originalname " + originalFilename + " to " + directory150 151 149 try { 152 150 if( file.exists() ) { … … 238 236 } 239 237 } 240 241 238 242 239 /** … … 274 271 275 272 /** 276 * Returns the absolute path for the given pathname. I tthe pathname is relative, it is taken relative to the web-app directory273 * Returns the absolute path for the given pathname. If the pathname is relative, it is taken relative to the web-app directory 277 274 * @param pathname 278 275 * @return … … 359 356 } 360 357 361 362 358 /********************************************************************************** 359 * 360 * Methods for handling zip files 361 * 362 **********************************************************************************/ 363 363 364 /** 364 365 * Determines whether a given file is a parsable zip file -
trunk/grails-app/services/nl/tno/metagenomics/integration/SynchronizationService.groovy
r9 r12 91 91 * @return ArrayList List of studies or null if the synchronization has failed 92 92 */ 93 public ArrayList<Study> synchronizeStudies() {93 public ArrayList<Study> synchronizeStudies() throws NotAuthenticatedException, Exception { 94 94 if( !performSynchronization() ) 95 95 return Study.findAllWhereTrashcan(false) … … 467 467 468 468 // Copy properties from gscf object 469 println "GSCF auth: " + gscfAuthorization470 471 469 if( gscfAuthorization.canRead instanceof Boolean ) 472 470 a.canRead = gscfAuthorization.canRead.booleanValue() … … 478 476 a.isOwner = gscfAuthorization.isOwner.booleanValue() 479 477 480 println "Saved auth: " + a.canRead.toString() + " - " + a.canWrite.toString() + " - " + a.isOwner.toString()481 482 478 a.save() 483 479 -
trunk/grails-app/views/assay/index.gsp
r7 r12 18 18 <th>Study</th> 19 19 <th># samples</th> 20 <th>avg sequences / sample</th> 20 21 </tr> 21 22 </thead> … … 28 29 <td><a href="${study.viewUrl()}">${study.name}</a></td> 29 30 <td>${assay.assaySamples?.size()}</td> 31 <td> 32 <g:if test="${assay.assaySamples?.size()}"> 33 <g:formatNumber number="${assay.numSequences() / assay.assaySamples?.size()}" format="0"/> 34 </g:if> 35 <g:else> 36 - 37 </g:else> 38 </td> 30 39 </tr> 31 40 </g:each> -
trunk/grails-app/views/assay/show.gsp
r10 r12 66 66 <th nowrap>name</th> 67 67 <th nowrap>run</th> 68 <th nowrap> subject</th>69 <th nowrap> event</th>68 <th nowrap>tag name</th> 69 <th nowrap>tag sequence</th> 70 70 <th nowrap># sequences</th> 71 <th nowrap># qual</th> 71 72 </tr> 72 73 </thead> … … 77 78 <td><a href="#" onClick="showSample(${assaySample.id}, 'assay'); return false;">${assaySample.sample.name}</a></td> 78 79 <td>${assaySample.run?.name}</td> 79 <td>${assaySample.sample.subject}</td> 80 <td>${assaySample.sample.event}</td> 81 <td>${assaySample.numSequences()}</td> 80 <td>${assaySample.tagName}</td> 81 <td>${assaySample.tagSequence}</td> 82 <td> 83 <g:if test="${assaySample.numSequenceFiles() > 0}"> 84 ${assaySample.numSequences()} 85 </g:if> 86 <g:else> 87 - 88 </g:else> 89 </td> 90 <td> 91 <g:if test="${assaySample.numQualityFiles() > 0}"> 92 ${assaySample.numQualScores()} 93 </g:if> 94 <g:else> 95 - 96 </g:else> 97 </td> 82 98 </tr> 83 99 </g:each> -
trunk/grails-app/views/fasta/showProcessScreen.gsp
r7 r12 43 43 clearInterval( progressInterval ); 44 44 45 alert( "Error " + xhr. responseCode + ": " + textStatus);46 window.location.replace( "<g:createLink controller="${entityType}" action="show" id="${entityId}" />" );45 alert( "Error " + xhr.getStatus() + ": " + xhr.getStatusText() ); 46 //window.location.replace( "<g:createLink controller="${entityType}" action="show" id="${entityId}" />" ); 47 47 } 48 48 }); -
trunk/grails-app/views/run/show.gsp
r10 r12 13 13 <g:javascript src="run.show.assayDialog.js" /> 14 14 <g:javascript src="run.show.runDialog.js" /> 15 <g:javascript src="run.show.addSamplesDialog.js" /> 15 16 <g:javascript src="showSampleDialogUniversal.js" /> 16 17 … … 89 90 <tr> 90 91 <th nowrap>name</th> 92 <th nowrap>study</th> 91 93 <th nowrap>assay</th> 92 <th nowrap> subject</th>93 <th nowrap> event</th>94 <th nowrap>tag name</th> 95 <th nowrap>tag sequence</th> 94 96 <th nowrap># sequences</th> 97 <th nowrap># qual</th> 98 <th class="nonsortable"></th> 95 99 </tr> 96 100 </thead> … … 100 104 <tr> 101 105 <td><a href="#" onClick="showSample(${assaySample.id}, 'run'); return false;">${assaySample.sample.name}</a></td> 102 <td>${assaySample.assay.study.name} - ${assaySample.assay.name}</td> 103 <td>${assaySample.sample.subject}</td> 104 <td>${assaySample.sample.event}</td> 105 <td>${assaySample.numSequences()}</td> 106 <td>${assaySample.assay.study.name}</td> 107 <td>${assaySample.assay.name}</td> 108 <td>${assaySample.tagName}</td> 109 <td>${assaySample.tagSequence}</td> 110 <td> 111 <g:if test="${assaySample.numSequenceFiles() > 0}"> 112 ${assaySample.numSequences()} 113 </g:if> 114 <g:else> 115 - 116 </g:else> 117 </td> 118 <td> 119 <g:if test="${assaySample.numQualityFiles() > 0}"> 120 ${assaySample.numQualScores()} 121 </g:if> 122 <g:else> 123 - 124 </g:else> 125 </td> 126 <td class="button"> 127 <g:if test="${!assaySample.assay.study.canWrite(session.user)}"> 128 <img src="${fam.icon(name: 'application_delete')}" class="disabled" title="You can't remove this sample because you don't have sufficient privileges." /> 129 </g:if> 130 <g:else> 131 <g:link onClick="return confirm( 'Are you sure you want to remove the selected sample from this run?' );" controller="run" action="removeSample" id="${run.id}" params="${[assaySampleId: assaySample.id]}" ><img src="${fam.icon(name: 'application_delete')}" /></g:link> 132 </g:else> 133 </td> 106 134 </tr> 107 135 </g:each> … … 120 148 <input type="button" value="Add sequence files" disabled="disabled"> 121 149 </g:else> 122 150 <input type="button" value="Add sample" onClick="showAddSamplesDialog();"> 151 <g:render template="addSamplesDialog" model="[run: run]" /> 123 152 </g:if> 124 153 <div id="showSampleDialog" class="dialog"></div> … … 164 193 </g:if> 165 194 <g:else> 166 <g:link onClick="return confirm( 'Are you sure you want to remove the selected assay from this run?' );" controller="run" action="removeAssay" id="${run.id}" params="${[assay_id: assay.id]}" ><img src="${fam.icon(name: 'application_delete')}" /></g:link> 195 <g:if test="${!assay.study.canWrite(session.user)}"> 196 <img src="${fam.icon(name: 'application_delete')}" class="disabled" title="You can't remove this assay because you don't have sufficient privileges." /> 197 </g:if> 198 <g:else> 199 <g:link onClick="return confirm( 'Are you sure you want to remove the selected assay from this run?' );" controller="run" action="removeAssay" id="${run.id}" params="${[assay_id: assay.id]}" ><img src="${fam.icon(name: 'application_delete')}" /></g:link> 200 </g:else> 167 201 </g:else> 168 202 </td>
Note: See TracChangeset
for help on using the changeset viewer.