Changeset 3
- Timestamp:
- Jan 12, 2011, 9:45:08 PM (13 years ago)
- Location:
- trunk
- Files:
-
- 13 added
- 1 deleted
- 19 edited
- 1 moved
Legend:
- Unmodified
- Added
- Removed
-
trunk
- Property svn:ignore
-
old new 4 4 .classpath 5 5 .project 6 fileuploads
-
- Property svn:ignore
-
trunk/grails-app/conf/BaseFilters.groovy
r2 r3 21 21 class BaseFilters { 22 22 def gscfService 23 def synchronizationService 23 24 24 25 // define filters … … 93 94 } 94 95 96 fullSynchronization(controller:'*', action:'*') { 97 before = { 98 // Never perform full synchronization on rest call when the synchronize controller is used 99 if( controllerName == "rest" || controllerName == "synchronize" ) { 100 return true; 101 } 102 103 // Never perform synchronization on a POST request 104 if( request.method == "POST" ) 105 return true; 106 107 if( synchronizationService.timeForFullSynchronization() ) { 108 redirect( url: synchronizationService.urlForFullSynchronization( params ) ); 109 return false 110 } 111 112 return true 113 114 } 115 } 116 95 117 defineStyle(controller: '*', action: '*') { 96 118 // before every execution -
trunk/grails-app/conf/Config.groovy
r2 r3 1 // ################################################################################## 2 // DO NOT Change these settings!!! 3 // You can overwrite them by adding your own configuration file in ~/.grails/[environment]-[appName].properties 4 1 5 // locations to search for config files that get merged into the main config 2 6 // config files can either be Java properties files or ConfigSlurper scripts 7 grails.config.locations = [ 8 // the default per-environment configuration 9 // (e.g. grails-app/conf/config-development.properties) 10 "classpath:config-${grails.util.GrailsUtil.environment}.properties", 11 "file:${ basedir }/grails-app/conf/config-${grails.util.GrailsUtil.environment}.properties", 12 "classpath:config-${grails.util.GrailsUtil.environment}.groovy", 13 14 // the external configuration to override the default 15 // configuration (e.g. ~/.grails-config/ci-gscf.properties) 16 "file:${userHome}/.grails-config/${grails.util.GrailsUtil.environment}-${appName}.properties", 17 "file:${userHome}/.grails-config/${grails.util.GrailsUtil.environment}-${appName}.groovy", 18 "file:${userHome}/.grails/${grails.util.GrailsUtil.environment}-${appName}.properties", 19 "file:${userHome}/.grails/${grails.util.GrailsUtil.environment}-${appName}.groovy" 20 ] 3 21 4 // grails.config.locations = [ "classpath:${appName}-config.properties",5 // "classpath:${appName}-config.groovy", 6 // "file:${userHome}/.grails/${appName}-config.properties", 7 // "file:${userHome}/.grails/${appName}-config.groovy"] 22 // Add extra location if desired 23 if(System.properties["${appName}.config.location"]) { 24 grails.config.locations << "file:" + System.properties["${appName}.config.location"] 25 } 8 26 9 // if(System.properties["${appName}.config.location"]) { 10 // grails.config.locations << "file:" + System.properties["${appName}.config.location"] 11 // } 27 // Whether to synchronize with gscf or not. By default, synchronization is turned on 28 metagenomics.synchronization = true 29 30 // By default, Metagenomics only fetches studies that have been changed by GSCF. GSCF sends 31 // notifications to the module, when something changes. However, it is possible that these messages 32 // can't be delivered or some other error happens. In that case, the data in the module is out of sync. 33 // To prevent that, every now and then a complete synchronization is performed. The frequency of this 34 // operation can be set by the following parameter. The parameter determines the number of seconds 35 // between two consecutive full synchronizations. Setting it to zero disables full synchronization, but 36 // is strongly discouraged. 37 metagenomics.fullSynchronization = 24 * 3600 // 1 day 38 39 // Temporary directory to upload files to. 40 // If the directory is given relative (e.g. 'fileuploads/temp'), it is taken relative to the web-app directory 41 // Otherwise, it should be given as an absolute path (e.g. '/home/user/sequences') 42 // The directory should be writable by the webserver user 43 metagenomics.fileUploadDir = "filuploads/temp" 44 45 // Maximum age that uploaded files should be kept on the server before deleting them. When a user uploads a file, 46 // but doesn't process the file (e.g. because he leaves the page beforehand or his computer crashes), the files 47 // remain in the upload directory. This mechanism ensures that the upload directory is cleaned after some time. 48 // The time is given in seconds. 49 metagenomics.fileUploadMaxAge = 2 * 24 * 3600 // 2 days 50 51 // Directory to save the uploaded files permanently 52 // If the directory is given relative (e.g. 'fileuploads/temp'), it is taken relative to the web-app directory 53 // Otherwise, it should be given as an absolute path (e.g. '/home/user/sequences') 54 // The directory should be writable by the webserver user 55 metagenomics.fileDir = "fileuploads/permanent" 56 57 // Path in GSCF that is used after baseURL for adding a new study 58 gscf.addStudyPath = "/studyWizard/index?jump=create" 12 59 13 60 grails.project.groupId = appName // change this to alter the default package name and Maven publishing destination … … 35 82 grails.views.gsp.encoding = "UTF-8" 36 83 grails.converters.encoding = "UTF-8" 84 37 85 // enable Sitemesh preprocessing of GSP pages 38 86 grails.views.gsp.sitemesh.preprocess = true 39 // scaffolding templates configuration40 grails.scaffolding.templates.domainSuffix = 'Instance'41 87 42 88 // Set to false to use the new Grails 1.2 JSONBuilder in the render method 43 89 grails.json.legacy.builder = false 90 44 91 // enabled native2ascii conversion of i18n properties files 45 92 grails.enable.native2ascii = true 93 46 94 // whether to install the java.util.logging bridge for sl4j. Disable for AppEngine! 47 95 grails.logging.jul.usebridge = true 96 48 97 // packages to include in Spring bean scanning 49 98 grails.spring.bean.packages = [] 50 51 // ##################################################################################52 // DO NOT Change these settings!!!53 // You can overwrite them by adding a /grails-app/conf/LocalConfig.groovy.54 // Use /grails-app/conf/_LocalConfig.groovy as an example!55 // Make sure to set SVN to ignore this file!56 57 // Whether to synchronize with gscf or not. By default, synchronization is turned on58 metagenomics.synchronization = true59 60 // Temporary directory to upload files to.61 // If the directory is given relative (e.g. 'fileuploads/temp'), it is taken relative to the web-app directory62 // Otherwise, it should be given as an absolute path (e.g. '/home/user/sequences')63 // The directory should be writable by the webserver user64 metagenomics.fileUploadDir = "fileuploads"65 66 // Maximum age that uploaded files should be kept on the server before deleting them. When a user uploads a file,67 // but doesn't process the file (e.g. because he leaves the page beforehand or his computer crashes), the files68 // remain in the upload directory. This mechanism ensures that the upload directory is cleaned after some time.69 // The time is given in seconds.70 metagenomics.fileUploadMaxAge = 2 * 24 * 360071 72 // Directory to save the uploaded files permanently73 // If the directory is given relative (e.g. 'fileuploads/temp'), it is taken relative to the web-app directory74 // Otherwise, it should be given as an absolute path (e.g. '/home/user/sequences')75 // The directory should be writable by the webserver user76 metagenomics.fileDir = "/home/robert/projects/grails/metagenomics/sequences"77 78 // Path in GSCF that is used after baseURL for adding a new study79 gscf.addStudyPath = "/studyWizard/index?jump=create"80 81 environments { // set per-environment serverURL stem for creating absolute links82 development {83 //development Environment (German Server)84 grails.serverURL = "http://localhost:8184"85 86 gscf.baseURL = "http://localhost:8080/gscf"87 //gscf.baseURL = "http://ci.gscf.nmcdsp.org"88 metagenomics.baseURL = "http://localhost:8184/metagenomics"89 metagenomics.consumerID = metagenomics.baseURL90 91 // Turn off synchronization in development mode92 metagenomics.synchronization = true93 }94 ci {95 //CI Environment (German Server)96 grails.serverURL = "http://ci.metagenomics.nmcdsp.org"97 gscf.baseURL = "http://ci.gscf.nmcdsp.org"98 metagenomics.baseURL = "http://ci.metagenomics.nmcdsp.org"99 metagenomics.consumerID = metagenomics.baseURL100 }101 test {102 //Test Environment (German Server)103 grails.serverURL = "http://test.gscf.nmcdsp.org"104 gscf.baseURL = "http://test.gscf.nmcdsp.org"105 metagenomics.baseURL = "http://test.metagenomics.nmcdsp.org"106 metagenomics.consumerID = metagenomics.baseURL107 }108 nbx14 {109 // nbx14 instance110 grails.serverURL = "http://nbx14.osx.eu"111 gscf.baseURL = "http://nbx14.osx.eu"112 metagenomics.baseURL = "http://metagenomics.nbx14.osx.eu"113 metagenomics.consumerID = metagenomics.baseURL114 }115 demo {116 // demo instance117 grails.serverURL = "http://demo.nbx14.osx.eu"118 gscf.baseURL = "http://demo.nbx14.osx.eu"119 metagenomics.baseURL = "http://demo.metagenomics.nbx14.osx.eu"120 metagenomics.consumerID = metagenomics.baseURL121 }122 production {123 //Production Environment (German Server)124 grails.serverURL = "http://www.nmcdsp.org"125 gscf.baseURL = "http://www.nmcdsp.org"126 metagenomics.baseURL = "http://metagenomics.nmcdsp.org"127 metagenomics.consumerID = metagenomics.baseURL128 }129 www {130 //Production Environment used in deploy scripts (German Server)131 grails.serverURL = "http://metagenomics.nmcdsp.org"132 gscf.baseURL = "http://www.nmcdsp.org"133 metagenomics.baseURL = "http://metagenomics.nmcdsp.org"134 metagenomics.consumerID = metagenomics.baseURL135 }136 }137 // ##################################################################################138 139 99 140 100 // log4j configuration … … 167 127 grails.views.javascript.library = "jquery" 168 128 jquery.version = '1.4.4' 169 170 // Include local config to overwrite defaults171 def localConfigFile = "${System.properties['base.dir']}/grails-app/conf/LocalConfig.groovy"172 173 if (new File(localConfigFile)?.canRead()) {174 grails.config.locations << "file:${localConfigFile}"175 } -
trunk/grails-app/conf/DataSource.groovy
r2 r3 10 10 cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider' 11 11 } 12 // environment specific settings13 environments {14 development {15 dataSource {16 //dbCreate = "create-drop" // one of 'create', 'create-drop','update'17 //url = "jdbc:hsqldb:mem:devDB"18 19 dbCreate = "update"20 username = "metagenomics"21 password = "ETBryeunTczeHYDt"22 23 // MySQL24 driverClassName = "com.mysql.jdbc.Driver"25 url = "jdbc:mysql://localhost/metagenomics"26 }27 }28 ci {29 // used by deploy scripts30 dataSource {31 dbCreate = "update"32 username = "metagenomics"33 password = "metagenomics"34 35 // PostgreSQL36 driverClassName = "org.postgresql.Driver"37 url = "jdbc:postgresql://localhost:5432/metagenomics-ci"38 dialect = org.hibernate.dialect.PostgreSQLDialect39 }40 }41 test {42 // used by deploy scripts43 dataSource {44 dbCreate = "create-drop" // one of 'create', 'create-drop','update'45 url = "jdbc:hsqldb:mem:devDB"46 }47 }48 nbx14 {49 // used by deploy scripts50 dataSource {51 dbCreate = "update"52 username = "metagenomics"53 password = "metagenomics"54 55 // PostgreSQL56 driverClassName = "org.postgresql.Driver"57 url = "jdbc:postgresql://localhost:5432/metagenomics-nbx14"58 dialect = org.hibernate.dialect.PostgreSQLDialect59 }60 }61 demo {62 // used by deploy scripts63 dataSource {64 dbCreate = "update"65 username = "metagenomics"66 password = "metagenomics"67 68 // PostgreSQL69 driverClassName = "org.postgresql.Driver"70 url = "jdbc:postgresql://localhost:5432/metagenomics-demo"71 dialect = org.hibernate.dialect.PostgreSQLDialect72 }73 }74 production {75 dataSource {76 dbCreate = "update"77 username = "metagenomics"78 password = "metagenomics"79 80 // PostgreSQL81 driverClassName = "org.postgresql.Driver"82 url = "jdbc:postgresql://localhost:5432/metagenomics-www"83 dialect = org.hibernate.dialect.PostgreSQLDialect84 }85 }86 www {87 // used by deploy scripts88 dataSource {89 dbCreate = "update"90 username = "metagenomics"91 password = "metagenomics"92 93 // PostgreSQL94 driverClassName = "org.postgresql.Driver"95 url = "jdbc:postgresql://localhost:5432/metagenomics-www"96 dialect = org.hibernate.dialect.PostgreSQLDialect97 }98 }99 } -
trunk/grails-app/controllers/nl/tno/metagenomics/AssayController.groovy
r2 r3 9 9 def fileService 10 10 def excelService 11 def fastaService12 11 13 12 // Fields to be edited using excel file and manually … … 494 493 } 495 494 496 /**************************************************************************497 *498 * Methods for handling uploaded sequence and quality files499 *500 *************************************************************************/501 502 /**503 * Processes uploaded files and tries to combine them with samples504 */505 def process = {506 // load study with id specified by param.id507 def assay = Assay.get(params.id as Long)508 509 if (!assay) {510 flash.message = "No assay found with id: $params.id"511 redirect('action': 'errorPage')512 return513 }514 515 // Check whether files are given516 def names = params.sequencefiles517 518 if( !names ) {519 flash.message = "No files uploaded for processing"520 redirect('action': 'show', 'id': params.id)521 return522 }523 524 // If only 1 file is uploaded, it is given as String525 ArrayList filenames = []526 if( names instanceof String )527 filenames << names528 else529 names.each { filenames << it }530 531 /* Parses uploaded files, discards files we can not handle532 *533 * [534 * success: [535 * [filename: 'abc.fasta', type: FASTA, numSequences: 190]536 * [filename: 'cde.fasta', type: FASTA, numSequences: 140]537 * [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]538 * [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]539 * ],540 * failure: [541 * [filename: 'testing.xls', message: 'Type not recognized']542 * ]543 * ]544 */545 def parsedFiles = fastaService.parseFiles( filenames );546 547 // Match files with samples in the database548 def matchedFiles = fastaService.matchFiles( parsedFiles.success, assay.assaySamples );549 550 // Sort files on filename551 matchedFiles.sort { a,b -> a.fasta?.originalfilename <=> b.fasta?.originalfilename }552 553 // Saved file matches in session to use them later on554 session.processedFiles = [ parsed: parsedFiles, matched: matchedFiles ];555 556 [assay: assay, parsedFiles: parsedFiles, matchedFiles: matchedFiles, selectedRun: params.selectedRun ]557 }558 559 /**560 * Saves processed files to the database, based on the selections made by the user561 */562 def saveProcessedFiles = {563 // load study with id specified by param.id564 def assay = Assay.get(params.id as Long)565 566 if (!assay) {567 flash.message = "No assay found with id: $params.id"568 redirect('action': 'errorPage')569 return570 }571 572 // Check whether files are given573 def files = params.file574 575 if( !files ) {576 flash.message = "No files were selected."577 redirect('action': 'show', 'id': params.id)578 return579 }580 581 File permanentDir = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir )582 int numSuccesful = 0;583 def errors = [];584 585 // Loop through all files Those are the numeric elements in the 'files' array586 def digitRE = ~/^\d+$/;587 files.findAll { it.key.matches( digitRE ) }.each { file ->588 def filevalue = file.value;589 590 // Check if the file is selected591 if( filevalue.include == "on" ) {592 if( fileService.fileExists( filevalue.fasta ) ) {593 try {594 def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, session.processedFiles );595 596 // Save the data into the database597 SequenceData sd = new SequenceData();598 599 sd.sequenceFile = permanent.fasta600 sd.qualityFile = permanent.qual601 sd.numSequences = permanent.numSequences602 sd.averageQuality = permanent.avgQuality603 604 // Couple the data to the right run and sample605 def run = Run.get( filevalue.run )606 if( run )607 run.addToSequenceData( sd );608 609 def sample = AssaySample.get( filevalue.assaySample );610 if( sample )611 sample.addToSequenceData( sd );612 613 if( !sd.validate() ) {614 errors << "an error occurred while saving " + filevalue.fasta + ": validation of SequenceData failed.";615 } else {616 sd.save(flush:true);617 }618 619 numSuccesful++;620 } catch( Exception e ) {621 errors << "an error occurred while saving " + filevalue.fasta + ": " + e.getMessage()622 }623 }624 } else {625 // File doesn't need to be included in the system. Delete it also from disk626 fileService.delete( filevalue.fasta );627 }628 }629 630 // Return a message to the user631 if( numSuccesful == 0 ) {632 flash.error = "None of the files were imported, because "633 634 if( errors.size() > 0 ) {635 errors.each {636 flash.error += "<br />- " + it637 }638 } else {639 flash.error = "none of the files were selected for import."640 }641 } else {642 flash.message = numSuccesful + " files have been added to the system. "643 644 if( errors.size() > 0 ) {645 flash.error += errors.size() + " errors occurred during import: "646 errors.each {647 flash.error += "<br />- " + it648 }649 }650 }651 652 redirect( action: "show", id: params.id )653 }654 655 495 } -
trunk/grails-app/controllers/nl/tno/metagenomics/StudyController.groovy
r2 r3 18 18 // Filter studies for the ones the user is allowed to see 19 19 def studies = Study.findAll(); 20 [studies: studies.findAll { it.canRead( session.user ) }, gscfAddUrl: gscfService.urlAddStudy() ]20 [studies: studies.findAll { it.canRead( session.user ) }, gscfAddUrl: gscfService.urlAddStudy(), lastSynchronized: synchronizationService.lastFullSynchronization ] 21 21 } 22 22 23 } -
trunk/grails-app/controllers/nl/tno/metagenomics/auth/LogoutController.groovy
r2 r3 14 14 log.info("Session.sessionToken is now ${session.sessionToken}") 15 15 16 //logout on GSCF side, and do not redirect back to metagenomics (&spring-security-redirect=${ConfigurationHolder.config. metagenomics.baseURL}/study/list)16 //logout on GSCF side, and do not redirect back to metagenomics (&spring-security-redirect=${ConfigurationHolder.config.grails.serverURL}/study/list) 17 17 def redirectURL = "${ConfigurationHolder.config.gscf.baseURL}/logout/remote?consumer=${ConfigurationHolder.config.metagenomics.ConsumerID}&token=${session.sessionToken}" 18 18 log.info("Redirecting to: ${redirectURL}") -
trunk/grails-app/controllers/nl/tno/metagenomics/integration/RestController.groovy
r2 r3 339 339 } 340 340 341 def url = [ 'url' : ConfigurationHolder.config. metagenomics.baseURL + '/assay/show/' + assay.id.toString() ]341 def url = [ 'url' : ConfigurationHolder.config.grails.serverURL + '/assay/show/' + assay.id.toString() ] 342 342 343 343 render url as JSON 344 344 } 345 } 345 }.metagenomics.baseURL 346 346 347 347 /***************************************************/ -
trunk/grails-app/domain/nl/tno/metagenomics/AssaySample.groovy
r2 r3 11 11 private long _numSequences = -1; 12 12 private float _averageQuality = -1.0; 13 13 14 14 Integer numUniqueSequences // Number of unique sequences / OTUs. Is only available after preprocessing 15 15 … … 49 49 50 50 /** 51 * Returns the number of sequence files in the system, belonging to this 52 * assay-sample combination. 53 * 54 * @return 55 */ 56 public int numSequenceFiles() { 57 if( !sequenceData ) 58 return 0 59 60 int numFiles = 0; 61 sequenceData.each { 62 if( it.sequenceFile ) 63 numFiles++ 64 } 65 66 return numFiles; 67 } 68 69 /** 70 * Returns the number of quality files in the system, belonging to this 71 * assay-sample combination. 72 * 73 * @return 74 */ 75 public int numQualityFiles() { 76 if( !sequenceData ) 77 return 0 78 79 int numFiles = 0; 80 sequenceData.each { 81 if( it.qualityFile ) 82 numFiles++ 83 } 84 85 return numFiles; 86 } 87 88 /** 51 89 * Returns the number of sequences in the files on the system, belonging to this 52 90 * assay-sample combination. … … 57 95 if( _numSequences > -1 ) 58 96 return _numSequences; 59 97 60 98 if( !sequenceData ) 61 99 return 0 … … 93 131 // Save as cache 94 132 _averageQuality = averageQuality; 95 133 96 134 return averageQuality; 97 135 } 136 137 /** 138 * Reset the statistics to their default value, in order to ensure that the values are recomputed next time. 139 */ 140 public void resetStats() { 141 _numSequences = -1; 142 _averageQuality = -1; 143 } 98 144 } -
trunk/grails-app/domain/nl/tno/metagenomics/Run.groovy
r2 r3 11 11 class Run { 12 12 def fileService 13 13 14 14 String name 15 15 Date date … … 20 20 static hasMany = [sequenceData: SequenceData, assays: Assay] 21 21 static belongsTo = Assay // Only used to determine the owner of the many-to-many relationship assay-run 22 22 23 23 static mapping = { 24 24 columns { … … 33 33 parameterFile(nullable:true) 34 34 } 35 35 36 36 def beforeDelete = { 37 37 // Remove the file if the object is deleted 38 38 if( this.parameterFile ) 39 39 fileService.delete( this.parameterFile, fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir ) ) 40 41 40 } 41 42 42 /** 43 43 * Sets the properties of this run from a form … … 47 47 public setPropertiesFromForm( params ) { 48 48 this.properties = params.run 49 49 50 50 // Enter date or default NULL 51 51 if( params.run_date ) { … … 54 54 this.date = null 55 55 } 56 56 57 57 // Enter filename if needed 58 58 if( params.parameterFile ) { 59 59 this.parameterFile = fileService.handleUploadedFile( 60 fileService.get( params.parameterFile ),61 "",62 fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir )63 );60 fileService.get( params.parameterFile ), 61 "", 62 fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir ) 63 ); 64 64 } 65 65 } 66 67 /** 68 * Returns the number of files in the system, belonging to this run 69 * 70 * @return 71 */ 72 public int numFiles() { 73 if( !sequenceData ) 74 return 0 75 76 int numFiles = 0; 77 sequenceData.each { numFiles += it.numFiles() } 78 79 return numFiles; 80 } 81 82 /** 83 * Returns the number of sequences in the files in the system, belonging to this run 84 * 85 * @return 86 */ 87 public long numSequences() { 88 if( !sequenceData ) 89 return 0 90 91 long numSequences = 0; 92 sequenceData.each { numSequences += it.numSequences } 93 94 return numSequences; 95 } 96 97 /** 98 * Returns the assaySamples associated with this run 99 * 100 * @return 101 */ 102 public ArrayList samples( def assayId ) { 103 if( !sequenceData ) 104 return [] 105 106 def list = [] 107 sequenceData.each { 108 if( it.sample.assay.id == assayId ) 109 list << it.sample 110 } 111 112 return list.unique().toList(); 113 } 66 114 } -
trunk/grails-app/domain/nl/tno/metagenomics/SequenceData.groovy
r2 r3 1 1 package nl.tno.metagenomics 2 2 3 import org.codehaus.groovy.grails.commons.ConfigurationHolder 4 3 5 class SequenceData { 6 def fileService 7 4 8 String sequenceFile = '' 5 9 String qualityFile = '' … … 28 32 return number; 29 33 } 34 35 def beforeDelete = { 36 def permanentDir = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir.toString() ) 37 if( sequenceFile ) { 38 fileService.delete( sequenceFile, permanentDir ) 39 } 40 41 if( qualityFile ) 42 fileService.delete( qualityFile, permanentDir ) 43 44 // Reset statistics of the assay sample, to ensure the deleted files are removed from statistics 45 sample.resetStats(); 46 } 30 47 } -
trunk/grails-app/services/nl/tno/metagenomics/FastaService.groovy
r2 r3 1 1 package nl.tno.metagenomics 2 3 2 4 3 import java.io.File; … … 9 8 def fileService 10 9 def fuzzySearchService 11 12 10 11 static transactional = true 13 12 14 13 /** 15 14 * Parses uploaded files and checks them for FASTA and QUAL files 16 15 * @param filenames List of filenames currently existing in the upload directory 16 * @param onProgress Closure to execute when progress indicators should be updated. 17 * Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number 18 * of files and bytes that have been processed in total 17 19 * @param directory Directory to move the files to 18 20 * @return Structure with information about the parsed files. The 'success' files are … … 32 34 * 33 35 */ 34 def parseFiles( ArrayList filenames, File directory = null ) {36 def parseFiles( ArrayList filenames, Closure onProgress, File directory = null ) { 35 37 if( filenames.size() == 0 ) { 36 38 return [ success: [], failure: [] ]; … … 43 45 def success = []; 44 46 def failure = []; 47 48 long filesProcessed = 0; 49 long bytesProcessed = 0; 45 50 46 51 // Loop through all filenames … … 55 60 } else { 56 61 try { 57 def contents = parseFile( file, filetype ); 58 62 def contents = parseFile( file, filetype, { files, bytes -> 63 filesProcessed += files; 64 bytesProcessed += bytes; 65 66 onProgress( filesProcessed, bytesProcessed ); 67 } ); 68 59 69 contents.filename = file.getName(); 60 contents.originalfilename = fileService.originalFilename( contents.filename ) 61 70 contents.originalfilename = fileService.originalFilename( contents.filename ) 71 62 72 if( contents.success ) { 63 73 success << contents; … … 71 81 failure << [ filename: filename, originalfilename: fileService.originalFilename( filename ), type: filetype, message: e.getMessage() ]; 72 82 } 73 83 74 84 } 75 85 } … … 103 113 def quals = parsedFiles.findAll { it.type == "qual" } 104 114 samples = samples.toList() 105 115 106 116 def files = []; 107 117 108 118 fastas.each { fastaFile -> 109 119 // Remove extension 110 def matchWith = fastaFile.originalfilename.substring( 0, fastaFile. filename.lastIndexOf( '.' ) )111 120 def matchWith = fastaFile.originalfilename.substring( 0, fastaFile.originalfilename.lastIndexOf( '.' ) ) 121 112 122 // Determine feasible quals (based on number of sequences ) 113 123 def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences } 114 124 115 125 // Best matching qual file 116 126 def qualIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.qual', feasibleQuals.originalfilename ); … … 119 129 if( qualIdx != null ) 120 130 qual = feasibleQuals[ qualIdx ]; 121 131 122 132 // Best matching sample 123 133 def sampleIdx = fuzzySearchService.mostSimilarWithIndex( matchWith, samples.sample.name ); 124 134 def assaySample = null 125 if( sampleIdx != null ) { 135 if( sampleIdx != null ) { 126 136 assaySample = samples[ sampleIdx ]; 127 137 } 128 138 129 139 files << [ 130 fasta: fastaFile,131 feasibleQuals: feasibleQuals,132 qual: qual,133 assaySample: assaySample134 ]135 } 136 140 fasta: fastaFile, 141 feasibleQuals: feasibleQuals, 142 qual: qual, 143 assaySample: assaySample 144 ] 145 } 146 137 147 return files; 138 139 140 } 141 142 148 149 150 } 151 152 143 153 /** 144 154 * Determines the file type of a given file, based on the extension. … … 195 205 /** 196 206 * Parses the given file 197 * @param file File to parse 198 * @param filetype Type of the given file 199 * @return List structure. Examples: 207 * @param file File to parse 208 * @param filetype Type of the given file 209 * @param onProgress Closure to execute when progress indicators should be updated. 210 * Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number 211 * of files and bytes that have been processed in this file (so the first parameter should 212 * only be 1 when the file is finished) 213 * 214 * @return List structure. Examples: 200 215 * 201 216 * [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ] … … 203 218 * [ success: false, filename: 'abc.txt', type: 'txt', message: 'Filetype could not be parsed.' ] 204 219 */ 205 protected def parseFile( File file, String filetype ) {220 protected def parseFile( File file, String filetype, Closure onProgress ) { 206 221 switch( filetype ) { 207 222 case "fasta": 208 return parseFasta( file );223 return parseFasta( file, onProgress ); 209 224 case "qual": 210 return parseQual( file );225 return parseQual( file, onProgress ); 211 226 default: 227 onProgress( 1, file.length() ); 212 228 return [ success: false, type: filetype, message: 'Filetype could not be parsed.' ] 213 229 } … … 216 232 /** 217 233 * Parses the given FASTA file 218 * @param file File to parse 219 * @return List structure. Examples: 234 * @param file File to parse 235 * @param onProgress Closure to execute when progress indicators should be updated. 236 * Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number 237 * of files and bytes that have been processed in this file (so the first parameter should 238 * only be 1 when the file is finished) 239 * @return List structure. Examples: 220 240 * 221 241 * [ success: true, filename: 'abc.fasta', type: 'fasta', numSequences: 200 ] 222 242 * [ success: false, filename: 'def.fasta', type: 'fasta', message: 'File is not a valid FASTA file' ] 223 243 */ 224 protected def parseFasta( File file ) {225 244 protected def parseFasta( File file, Closure onProgress ) { 245 226 246 long startTime = System.nanoTime(); 227 log.trace "Start parsing FASTA " + file.getName() 228 247 log.trace "Start parsing FASTA " + file.getName() 248 229 249 // Count the number of lines, starting with '>' (and where the following line contains a character other than '>') 230 250 long numSequences = 0; 251 long bytesProcessed = 0; 231 252 boolean lookingForSequence = false; 232 253 … … 241 262 } 242 263 } 243 } 244 } 264 265 266 // Update progress every time 1MB is processed 267 bytesProcessed += line.size(); 268 if( bytesProcessed > 1000000 ) { 269 onProgress( 0, bytesProcessed ); 270 bytesProcessed = 0; 271 } 272 } 273 274 275 276 } 277 278 // Update progress and say we're finished 279 onProgress( 1, bytesProcessed ); 245 280 246 281 log.trace "Finished parsing FASTA " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L … … 251 286 /** 252 287 * Parses the given QUAL file 253 * @param file File to parse 254 * @return List structure. Examples: 288 * @param file File to parse 289 * @param onProgress Closure to execute when progress indicators should be updated. 290 * Has 2 parameters: numFilesProcessed and numBytesProcessed that indicate the number 291 * of files and bytes that have been processed in this file (so the first parameter should 292 * only be 1 when the file is finished) 293 * @return List structure. Examples: 255 294 * 256 295 * [ success: true, filename: 'abc.qual', type: 'qual', numSequences: 200, avgQuality: 31 ] 257 296 * [ success: false, filename: 'def.qual', type: 'qual', message: 'File is not a valid QUAL file' ] 258 297 */ 259 protected def parseQual( File file ) {298 protected def parseQual( File file, Closure onProgress ) { 260 299 long startTime = System.nanoTime(); 261 300 log.trace "Start parsing QUAL " + file.getName() 262 301 263 // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for 302 // Count the number of lines, starting with '>'. After we've found such a character, we continue looking for 264 303 // quality scores 265 304 long numSequences = 0; 305 long bytesProcessed = 0; 266 306 def quality = [ quality: 0.0, number: 0L ] 267 307 268 308 boolean lookingForFirstQualityScores = false; 269 309 file.eachLine { line -> … … 275 315 numSequences++; 276 316 lookingForFirstQualityScores = false; 277 317 278 318 quality = updateQuality( quality, line ); 279 319 } … … 281 321 quality = updateQuality( quality, line ); 282 322 } 283 } 284 } 323 324 // Update progress every time 1MB is processed 325 bytesProcessed += line.size(); 326 if( bytesProcessed > 1000000 ) { 327 onProgress( 0, bytesProcessed ); 328 bytesProcessed = 0; 329 } 330 } 331 332 } 333 334 // Update progress and say we're finished 335 onProgress( 1, bytesProcessed ); 285 336 286 337 log.trace "Finished parsing QUAL " + file.getName() + ": " + ( System.nanoTime() - startTime ) / 1000000L … … 288 339 return [ success: true, type: "qual", numSequences: numSequences, avgQuality: quality.quality ]; 289 340 } 290 341 291 342 /** 292 343 * Parses the given line and updates the average quality based on the scores … … 298 349 // Determine current average 299 350 List tokens = line.tokenize(); 300 Long total = 0; 301 351 Long total = 0; 352 302 353 tokens.each { 303 354 total += Integer.parseInt( it ); 304 355 } 305 356 306 357 int numTokens = tokens.size(); 307 358 308 359 // Update the given average 309 360 if( numTokens > 0 ) { … … 311 362 quality.quality = quality.quality + ( ( total / numTokens as double ) - quality.quality ) / quality.number * numTokens; 312 363 } 313 364 314 365 return quality 315 366 } 316 367 317 368 /** 318 369 * Moves a fasta and qual file to their permanent location, and returns information about these files … … 326 377 File permanentDirectory = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir ); 327 378 def returnStructure = [:]; 328 379 329 380 if( fileService.fileExists( fastaFile ) ) { 330 381 // Lookup the original filename … … 340 391 throw new Exception( "Fasta file to save doesn't exist on disk" ); 341 392 } 342 393 343 394 if( qualFile && fileService.fileExists( qualFile ) ) { 344 395 // Lookup the original filename 345 396 def qualData = processedFiles.parsed.success.find { it.filename == qualFile }; 346 397 347 398 if( qualData ) { 348 399 returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile ), qualData.originalfilename, permanentDirectory ); … … 358 409 returnStructure.avgQuality = 0; 359 410 } 360 411 361 412 return returnStructure; 362 413 } -
trunk/grails-app/services/nl/tno/metagenomics/integration/GscfService.groovy
r2 r3 27 27 */ 28 28 public String urlAuthRemote( def params, def token ) { 29 def redirectURL = "${config.gscf.baseURL}/login/auth_remote?moduleURL=${this.moduleURL()}&consumer=${this.consumerID()}&token=${token}&returnUrl=${config. metagenomics.baseURL}"29 def redirectURL = "${config.gscf.baseURL}/login/auth_remote?moduleURL=${this.moduleURL()}&consumer=${this.consumerID()}&token=${token}&returnUrl=${config.grails.serverURL}" 30 30 31 31 if (params.controller != null){ … … 330 330 */ 331 331 private String moduleURL() { 332 return config. metagenomics.baseURL332 return config.grails.serverURL 333 333 } 334 334 -
trunk/grails-app/services/nl/tno/metagenomics/integration/SynchronizationService.groovy
r2 r3 7 7 class SynchronizationService { 8 8 def gscfService 9 9 10 10 String sessionToken = "" // Session token to use for communication 11 11 User user = null // Currently logged in user. Must be set when synchronizing authorization 12 12 boolean eager = false // When set to true, this method fetches data about all studies from GSCF. Otherwise, it will only look at the 13 // studies marked as dirty in the database. Defaults to false. 13 // studies marked as dirty in the database. Defaults to false. 14 15 // Keeps track of the last time this module performed a full synchronization. 16 static Date lastFullSynchronization = null; 14 17 15 18 static transactional = true 16 19 17 20 /** 18 21 * Determines whether the synchronization should be performed or not. This can be entered … … 22 25 protected performSynchronization() { 23 26 def conf = ConfigurationHolder.config.metagenomics.synchronization; 24 27 25 28 // If nothing is entered in configuration, return true (default value) 26 29 if( conf == null ) 27 30 return true 31 32 return conf 33 } 34 35 /** 36 * Returns true iff a full synchronization should be performed 37 * @return 38 */ 39 public boolean timeForFullSynchronization() { 40 if( SynchronizationService.lastFullSynchronization == null ) 41 return true 28 42 29 return conf 43 // Compute the time since the last full synchronization in milliseconds 44 Date today = new Date(); 45 long difference = SynchronizationService.lastFullSynchronization.getTime() - today.getTime() 46 47 if( difference / 1000 > ConfigurationHolder.config.metagenomics.fullSynchronization ) 48 return true 49 else 50 return false 51 } 52 53 /** 54 * Redirects to a temporary page to give the user a 'waiting' page while synchronizing 55 * @return 56 */ 57 public String urlForFullSynchronization( def params ) { 58 def returnUrl = ConfigurationHolder.config.grails.serverURL 59 if (params.controller != null){ 60 returnUrl += "/${params.controller}" 61 if (params.action != null){ 62 returnUrl += "/${params.action}" 63 if (params.id != null){ 64 returnUrl += "/${params.id}" 65 } 66 } 67 } 68 if( timeForFullSynchronization() ) { 69 return ConfigurationHolder.config.grails.serverURL + "/synchronize/full?redirect=" + returnUrl.encodeAsURL() 70 } else { 71 return returnUrl 72 } 73 } 74 75 /** 76 * Performs a full synchronization in order to retrieve all studies 77 * @return 78 */ 79 public void fullSynchronization() { 80 if( !timeForFullSynchronization() ) 81 return 82 83 def previousEager = eager 84 eager = true 85 synchronizeStudies(); 86 eager = previousEager 87 88 SynchronizationService.lastFullSynchronization = new Date(); 30 89 } 31 90 … … 37 96 if( !performSynchronization() ) 38 97 return Study.findAll() 39 98 40 99 // When eager fetching is enabled, ask for all studies, otherwise only ask for studies marked dirty 41 100 // Synchronization is performed on all studies, not only the studies the user has access to. Otherwise … … 47 106 studies = Study.findAllWhere( [isDirty: true] ); 48 107 } 49 108 50 109 // Perform no synchronization if no studies have to be synchronized 51 110 // Perform synchronization on only one study directly, because otherwise … … 57 116 println "Study: " + studies[ 0] 58 117 def newStudy = synchronizeStudy( studies[0] ); 59 if( newStudy ) 118 if( newStudy ) 60 119 return [ newStudy ]; 61 else 120 else 62 121 return [] 63 122 } 64 123 65 124 // Fetch all studies from GSCF 66 125 def newStudies … … 68 127 if( !eager ) { 69 128 def studyTokens = studies.studyToken; 70 129 71 130 if( studyTokens instanceof String ) { 72 131 studyTokens = [studyTokens]; … … 86 145 synchronizeStudies( newStudies ); 87 146 studies = handleDeletedStudies( studies, newStudies ); 88 147 89 148 log.trace( "Returning " + studies.size() + " studies after synchronization" ) 90 149 91 150 return studies 92 151 } 93 152 94 153 /** 95 154 * Synchronizes all studies given by 'newStudies' with existing studies in the database, and adds them … … 107 166 108 167 Study studyFound = Study.findByStudyToken( gscfStudy.studyToken as String ) 109 168 110 169 if(studyFound) { 111 170 log.trace( "Study found with name " + studyFound.name ) 112 171 113 172 // Synchronize the study itself with the data retrieved 114 173 synchronizeStudy( studyFound, gscfStudy ); … … 123 182 synchronizeAuthorization(studyFound); 124 183 synchronizeStudyAssays(studyFound); 125 184 126 185 // Mark the study as clean 127 186 studyFound.isDirty = false … … 131 190 } 132 191 } 133 192 134 193 /** 135 194 * Removes studies from the database that are expected but not found in the list from GSCF … … 151 210 log.trace( "Study " + existingStudy.studyToken + " not found. Check whether it is removed or the user just can't see it." ) 152 211 153 // Study was not given to us by GSCF. This might be because the study is removed, or because the study is not visible (anymore) 212 // Study was not given to us by GSCF. This might be because the study is removed, or because the study is not visible (anymore) 154 213 // to the current user. 155 214 // Synchronize authorization and see what is the case (it returns null if the study has been deleted) … … 160 219 } 161 220 } 162 221 163 222 return studies 164 223 } 165 224 166 225 /** 167 226 * Synchronizes the given study with the data from GSCF … … 172 231 if( !performSynchronization() ) 173 232 return study 174 233 175 234 if( study == null ) 176 235 return null … … 179 238 if( !eager && !study.isDirty ) 180 239 return study; 181 240 182 241 // Retrieve the study from GSCF 183 242 def newStudy … … 218 277 if( !performSynchronization() ) 219 278 return study 220 279 221 280 if( study == null || newStudy == null) 222 281 return null … … 224 283 // If the study hasn't changed, don't update anything 225 284 if( !eager && !study.isDirty ) { 226 return study; 285 return study; 227 286 } 228 287 … … 231 290 return null; 232 291 } 233 292 234 293 // Mark study dirty to enable synchronization 235 294 study.isDirty = true; 236 295 synchronizeAuthorization( study ); 237 296 synchronizeStudyAssays( study ); 238 297 239 298 // Update properties and mark as clean 240 299 study.name = newStudy.title … … 254 313 return study.assays.toList() 255 314 256 if( !eager && !study.isDirty ) 257 return study.assays as List 258 315 if( !eager && !study.isDirty ) 316 return study.assays as List 317 259 318 // Also update all assays, belonging to this study 260 319 // Retrieve the assays from GSCF … … 287 346 return handleDeletedAssays( study, newAssays ); 288 347 } 289 348 290 349 /** 291 350 * Synchronizes the assays of a study with the given data from GSCF … … 323 382 } 324 383 } 325 384 326 385 /** 327 386 * Removes assays from the system that have been deleted from GSCF … … 354 413 } 355 414 } 356 415 357 416 return study.assays.toList() 358 417 } 359 418 360 419 /** 361 420 * Retrieves the authorization for the currently logged in user … … 374 433 throw new Exception( "Property user of SynchronizationService must be set to the currently logged in user" ); 375 434 } 376 435 377 436 // Only perform synchronization if needed 378 437 if( !eager && !study.isDirty ) 379 438 return Auth.findByUserAndStudy( user, study ) 380 439 381 440 if( !performSynchronization() ) 382 441 return Auth.findByUserAndStudy( user, study ) 383 442 384 443 def gscfAuthorization 385 444 try { … … 389 448 // TODO: handle deletion 390 449 log.trace( "Study " + study.studyToken + " has been deleted. Remove all authorization on that study") 391 450 392 451 Auth.findAllByStudy( study ).each { 393 452 it.delete() 394 453 } 395 396 return null 397 } 398 454 455 return null 456 } 457 399 458 // Update the authorization object, or create a new one 400 459 Auth a = Auth.authorization( study, user ) 401 460 402 461 if( !a ) { 403 462 log.trace( "Authorization not found for " + study.studyToken + " and " + user.username + ". Creating a new object" ); 404 463 405 464 a = Auth.createAuth( study, user ); 406 465 } 407 466 408 467 // Copy properties from gscf object 409 468 a.canRead = gscfAuthorization.canRead 410 469 a.canWrite = gscfAuthorization.canWrite 411 470 a.isOwner = gscfAuthorization.isOwner 412 471 413 472 a.save() 414 473 415 474 return a 416 475 } … … 431 490 if( !eager && !assay.study.isDirty ) 432 491 return assay 433 492 434 493 // Retrieve the assay from GSCF 435 494 def newAssay … … 477 536 if( !eager && !assay.study.isDirty ) 478 537 return assay 479 538 480 539 // If new assay is empty, something went wrong 481 540 if( newAssay.size() == 0 ) { … … 535 594 } 536 595 537 596 538 597 synchronizeAssaySamples( assay, newSamples ); 539 598 return handleDeletedSamples( assay, newSamples ); 540 599 } 541 600 542 601 /** 543 602 * Synchronize all samples for a given assay with the data from GSCF … … 597 656 } 598 657 } 599 658 600 659 /** 601 660 * Removes samples from the system that have been removed from an assay in GSCF … … 612 671 for( int i = numAssaySamples - 1; i >= 0; i-- ) { 613 672 def existingSample = assaySamples[i]; 614 673 615 674 AssaySample sampleFound = newSamples.find { it.name == existingSample.sample.sampleToken } 616 675 617 676 if( !sampleFound ) { 618 677 log.trace( "Sample " + existingSample.sample.sampleToken + " not found. Removing it." ) 619 678 620 679 // The sample has been removed 621 680 // TODO: What to do with the data associated with this AssaySample (e.g. sequences)? See also GSCF ticket #255 -
trunk/grails-app/views/assay/_addFilesDialog.gsp
r2 r3 2 2 <h2>Upload sequence files</h2> 3 3 4 <g:form name="addFiles" controller="assay", action="process" id="${assay.id}"> 5 <p>Select the run these files belong to.</p> 6 <p> 7 <g:select name="selectedRun" from="${assay.runs}" optionKey="id" optionValue="name" /> 8 </p> 4 <g:form name="addFiles" controller="fasta" action="showProcessScreen" id="${assay.id}"> 5 <p>Select the run these files belong to: <g:select name="selectedRun" from="${assay.runs}" optionKey="id" optionValue="name" /></p> 9 6 <p> 10 7 Select sequence and quality files to upload. It is possible to zip the files before upload. -
trunk/grails-app/views/assay/show.gsp
r2 r3 11 11 <g:javascript src="assay.show.enterTagsDialog.js" /> 12 12 <g:javascript src="assay.show.runDialogs.js" /> 13 <g:javascript src="assay.show.showSampleDialog.js" /> 14 <g:javascript src="assay.show.showRunDialog.js" /> 13 15 14 16 <g:javascript src="fileuploader.new.js" /> … … 66 68 <g:each in="${assaySamples}" var="assaySample"> 67 69 <tr> 68 <td> ${assaySample.sample.name}</td>70 <td><a href="#" onClick="showSample(${assaySample.id}); return false;">${assaySample.sample.name}</a></td> 69 71 <td>${assaySample.tagSequence}</td> 70 72 <td>${assaySample.numSequences()}</td> … … 95 97 <g:render template="addFilesDialog" model="[assay: assay]" /> 96 98 </g:if> 99 <g:render template="showSampleDialog" model="[assay: assay]" /> 97 100 </g:else> 98 101 … … 108 111 <th nowrap>name</th> 109 112 <th nowrap>date</th> 113 <th nowrap>supplier</th> 110 114 <th nowrap>machine</th> 111 <th nowrap>supplier</th> 112 <th nowrap>configuration</th> 115 <th nowrap>parameter file</th> 113 116 <th nowrap>other assays</th> 114 117 <th class="nonsortable"></th> … … 120 123 <g:each in="${runs}" var="run"> 121 124 <tr> 122 <td> ${run.name}</td>125 <td><a href="#" onClick="showRun(${run.id}); return false;">${run.name}</a></td> 123 126 <td><g:formatDate format="dd-MM-yyyy" date="${run.date}"/></td> 127 <td>${run.supplier}</td> 124 128 <td>${run.machine}</td> 125 <td>${run.supplier}</td>126 129 <td><g:uploadedFile value="${run.parameterFile}"/></td> 127 130 <td> … … 148 151 <g:render template="editRunDialog" model="[assay: assay]" /> 149 152 </g:if> 153 <g:render template="showRunDialog" model="[assay: assay]" /> 154 150 155 </body> 151 156 </html> -
trunk/grails-app/views/fasta/showProcessResult.gsp
r2 r3 2 2 <head> 3 3 <meta name="layout" content="main" /> 4 <title>Process files for assay ${assay.name} | Metagenomics | dbNP</title>4 <title>Processed files for assay ${assay.name} | Metagenomics | dbNP</title> 5 5 6 <g:javascript src="jquery.ui.tabbeddialog.js" />7 8 6 <script> 9 7 var assayId = ${assay.id}; … … 66 64 </td> 67 65 <td> 68 ${selectedRun}69 66 <g:if test="${sortedRuns.size()}"> 70 67 <select name="file.${i}.run"> … … 116 113 117 114 <p> 118 <g:link action="show" id="${assay.id}">Return to assay</g:link>115 <g:link controller="assay" action="show" id="${assay.id}">Return to assay</g:link> 119 116 </p> 120 117 </body> -
trunk/grails-app/views/layouts/main.gsp
r2 r3 34 34 35 35 <div id="content"> 36 <g:if test="${lastSynchronized}"> 37 <p>Last full synchronization: ${lastSynchronized}</p> 38 </g:if> 36 39 <g:if test="${flash.error}"> 37 40 <p class="error">${flash.error}</p> -
trunk/web-app/css/metagenomics.css
r2 r3 2 2 margin: 0; 3 3 padding: 0; 4 font: 1 0px normal Arial, Helvetica, sans-serif;4 font: 11px normal Arial, Helvetica, sans-serif; 5 5 background: #fff url(../images/metagenomics/body_bg.gif) repeat-x; 6 6 } … … 466 466 467 467 /* END :: special select option styles */ 468 469 .spinner { 470 background: url(../images/spinner.gif) no-repeat left top; 471 width: 16px; 472 height: 16px; 473 display: none; 474 } -
trunk/web-app/css/showAssay.css
r2 r3 2 2 3 3 #enterTagsDialog tr.example td { padding-top: 2px; padding-bottom: 2px; color: #666; } 4 5 #showSampleDialog ul, #showRunDialog ul { list-style-type: none; margin-left: 0; padding-left: 0; } 6 #showSampleDialog ul li, #showRunDialog ul li { margin: 4px 0; padding-left: 0; }
Note: See TracChangeset
for help on using the changeset viewer.