Changeset 59


Ignore:
Timestamp:
May 23, 2011, 5:52:20 PM (8 years ago)
Author:
robert@…
Message:
  • Improved speed by using ajax calls in pagination
  • Importing taxonomy files now works by adding it to sequenceData objects
Location:
trunk
Files:
3 added
3 deleted
27 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/conf/Config.groovy

    r52 r59  
    117117        }
    118118       
    119         trace   'grails.app'
     119        trace   'grails.app', 'org.hibernate.SQL'
    120120       
    121121    error  'org.codehaus.groovy.grails.web.servlet',  //  controllers
  • trunk/grails-app/controllers/masssequencing/SandboxController.groovy

    r58 r59  
    33import nl.tno.massSequencing.*
    44import nl.tno.massSequencing.classification.*
     5import nl.tno.massSequencing.auth.*
    56import org.hibernate.StatelessSession
     7import grails.converters.*
    68
    79class SandboxController {
    810        def sessionFactory
     11        def classificationService
     12        def dataTablesService;
    913       
    1014    def importTax = {
     
    1216                hibernateSession.clear();
    1317               
    14                 def filename = "/home/robert/tmp/test.taxonomy"
    15                 def reader = new File( filename ).newReader();
    16                
    17                 def parts
    18                 def sequenceName
    19                 def classification
    20                 def taxa
    21                 def taxon
    22                 def line
    23                
    24                 def classificationSplitRegex = /(\(\d+\))?;/
    25                 def startLevel = 0    // The first level that is present in the file. Sometimes, the root element (level 0) is not mentioned in the file, in that case, this values should be set to 1
    26                
    27                 def i = 0;
    2818                def start = System.currentTimeMillis();
    29                 def lapTime = start;
    30                
     19
     20                def filename = "test.taxonomy"
    3121                def sequenceData = SequenceData.list();
    32                
    33                 // Create a stateless session in order to speed up inserts
    34                 StatelessSession statelessSession = sessionFactory.openStatelessSession();
    35                 statelessSession.beginTransaction();
    36                
    37                 println "Starting at " + start
    38                
    39                 while( ( line = reader.readLine() ) ) {
    40                         // Find the taxon for this line
    41                         parts = line.tokenize( "\t" );
    42                        
    43                         if( parts.size() != 2 ) {
    44                                 // Skip this line because it is incorrect
    45                                 continue;
    46                         }
    47                        
    48                         sequenceName = parts[ 0 ];
    49                         classification = parts[ 1 ];
    50                        
    51                         // Split classification to check whether the taxon already exists
    52                         taxa = classification.split( classificationSplitRegex )
    53                        
    54                         // Find taxon or create one if needed
    55                         taxon = Taxon.findOrCreateTaxonByPath( taxa, startLevel );
    56                        
    57                         // Determine assaySample
    58                         Collections.shuffle( sequenceData );
    59                        
    60                         // Create a new sequence record
    61                         def s = new Sequence()
    62                         s.name = sequenceName
    63                         s.classification = taxon
    64                         s.sequenceData = sequenceData[ 0 ]
    65                        
    66                         statelessSession.insert(s);
    67                          
    68                         if( i % 40 == 0 ) {
    69                         }
    70                        
    71                         if( i % 256 == 0 ) {
    72                                 print "" + ( System.currentTimeMillis() - lapTime ) + " "
    73                                 lapTime = System.currentTimeMillis();
    74                         }
    75                        
    76                         if( i % 2048 == 0 ) {
    77                                 println ""
    78                                 print i + ": "
    79                          }
    80                                
    81                         i++;
    82                 }
    83                
    84                 reader.close();
     22                Collections.shuffle( sequenceData );
     23
     24                classificationService.storeClassification( filename, sequenceData[ 0 ], { bytes -> println "Bytes processed: " + bytes } )
    8525               
    8626                def end = System.currentTimeMillis();
     
    8929                println "------------------------------------------"
    9030                println "Duration: " + ( end - start ) + " ms"
    91                 println "Lines: " + i
    92                 println "Average: " + ( ( end - start ) / i ) + "ms / line"
    9331               
    9432        }
     
    179117                writer.close();
    180118        }
     119
     120        def createSummary = {
     121                def sequenceData = SequenceData.get( 222 );
     122                def sequenceData2 = SequenceData.get( 221 );
     123                def assaySamples = [ sequenceData.sample, sequenceData2.sample ];
     124               
     125                classificationService.updateClassificationForAssaySamples( assaySamples );
     126               
     127                def classifications = Classification.executeQuery( "FROM Classification c WHERE c.assaySample = ? ORDER BY c.taxon.lft", [ assaySamples[0] ] );
     128               
     129                println classifications.size() + " classifications for " + assaySamples[0].sample.name + " (" + assaySamples[0].id + ")"
     130                classifications.each {
     131                        println "   ".multiply( it.taxon.level ) + it.taxon.level + " " + it.taxon.name + " - " + it.occurrences
     132                }
     133               
     134
     135                classifications = Classification.executeQuery( "FROM Classification c WHERE c.assaySample = ? ORDER BY c.taxon.lft", [ assaySamples[1] ] );
     136               
     137                println classifications.size() + " classifications for " + assaySamples[1].sample.name + " (" + assaySamples[1].id + ")"
     138                classifications.each {
     139                        println "   ".multiply( it.taxon.level ) + it.taxon.level + " " + it.taxon.name + " - " + it.occurrences
     140                }
     141
     142                render "";
     143        }
     144       
     145        def datatables = {
     146                long runId = 3;
     147                long userId = 2;
     148                println "writable assays: " +  Run.executeQuery( "SELECT COUNT(*) FROM Run r LEFT JOIN r.assays a WHERE r.id = ?", [runId])
     149                println "writable assays: " +  Run.executeQuery( "SELECT COUNT(*) FROM Run r LEFT JOIN r.assays a WHERE r.id = ? AND NOT EXISTS( FROM a.study.auth auth WHERE auth.canWrite = true AND auth.user.id = ?)", [runId, userId ])
     150
     151                //println "total assaysamples: " + Run.executeQuery( 'SELECT r.id, r.name, COUNT( a ), SUM( sd.numSequences ) FROM Run r LEFT JOIN r.assaySamples a LEFT JOIN a.sequenceData sd GROUP BY r.id, r.name' );
     152                // println "total assaysamples: " + Run.executeQuery( 'SELECT r.id, r.name, SUM( sd.numSequences ) FROM Run r  LEFT JOIN SequenceData sd WITH EXISTS( FROM r.assaySamples a WHERE a = sd.assaySample )' );
     153        }
     154       
     155        /**
     156         * Returns data for datatables as JSON
     157         * @see http://www.datatables.net/usage/server-side
     158         * Input:
     159         *     
     160                int             iDisplayStart           Display start point
     161                int             iDisplayLength          Number of records to display
     162                int             iColumns                        Number of columns being displayed (useful for getting individual column search info)
     163                string          sSearch                         Global search field
     164                boolean         bEscapeRegex            Global search is regex or not
     165                boolean         bSortable_(int)         Indicator for if a column is flagged as sortable or not on the client-side
     166                boolean         bSearchable_(int)       Indicator for if a column is flagged as searchable or not on the client-side
     167                string          sSearch_(int)           Individual column filter
     168                boolean         bEscapeRegex_(int)      Individual column filter is regex or not
     169                int             iSortingCols            Number of columns to sort on
     170                int             iSortCol_(int)          Column being sorted on (you will need to decode this number for your database)
     171                string          sSortDir_(int)          Direction to be sorted - "desc" or "asc". Note that the prefix for this variable is wrong in 1.5.x where iSortDir_(int) was used)
     172                string          sEcho                           Information for DataTables to use for rendering
     173         *
     174         *
     175         */
     176        def datatables_data = {
     177                def run = Run.get( 3 );
     178                def total = (int) Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.run.id = :runId", [ "runId": run.id ] )[ 0 ];
     179               
     180                def columns = [
     181                        null,
     182                        "s.sample.name",
     183                        "s.assay.study.name",
     184                        "s.assay.name",
     185                        "s.fwMidName",
     186                        "SUM( sd.numSequences )",
     187                        "SUM( CASE WHEN sd.qualityFile IS NULL THEN 0 ELSE sd.numSequences END )",
     188                        null,
     189                        null,
     190                        null,
     191                        's.run.id',
     192                        's.assay.id',
     193                        's.assay.study.id',
     194                ]
     195               
     196                def idColumn = 's.id';
     197                def groupColumns = [ idColumn ] + columns[1..4] + columns[10];
     198
     199                def from = "AssaySample s LEFT JOIN s.sequenceData as sd"
     200                def where = "s.run.id = :runId"
     201                def parameters = [ "runId": run.id ];
     202               
     203                render dataTablesService.retrieveData(
     204                        params,
     205                        AssaySample.class,
     206                        {
     207                                def sampleId = it[ 0 ];
     208                                def runId = it[ 7 ];
     209                                def assayId = it[ 8 ];
     210                                def studyId = it[ 9 ];
     211                                def numSequences = it[ 5 ];
     212                               
     213                                // TODO: Rewrite this authorization part in a more efficient way
     214                                def auth = Auth.executeQuery( "SELECT a.canWrite FROM Auth a WHERE a.study.id = :studyId AND a.user.id = :userId", [ 'studyId': studyId, 'userId': session.user?.id ] );
     215                                def canWrite = auth ? auth[ 0 ] : false;
     216                               
     217                                // How to convert a database row to a datatables row
     218                                def editButton = '';
     219                                def deleteButton = '';
     220                                def chartButton = '';
     221                               
     222                                if( canWrite ) {
     223                                        editButton = g.link( url: '#', title: "Edit sample", onClick: "showEditSampleDialog(" + sampleId + ", 'run', " + runId + ");"  ) { '<img src="' + fam.icon( name: 'pencil' ) + '" title="Edit sample" />' };
     224                                        deleteButton = g.link( controller: 'run', action: 'removeSample', id: it[ 7 ], params: [ 'assaySampleId': sampleId ], title: "Remove sample from run", onClick: "return confirm( 'Are you sure you want to remove the selected sample from this run?' );"  ) { '<img src="' + fam.icon( name: 'application_delete' ) + '" title="Remove sample from run" />' };
     225                                } else {
     226                                        editButton = '<img src="' + fam.icon( name: 'pencil' ) + '" title="You can\'t edit this sample because you don\'t have sufficient privileges." class="disabled" />';
     227                                        deleteButton = '<img src="' + fam.icon( name: 'application_delete' ) + '" title="You can\'t remove this sample because you don\'t have sufficient privileges." class="disabled" />';
     228                                }
     229                               
     230                                if( numSequences > 0 ) {
     231                                        chartButton = g.link( controller: "assaySample", action: "sequenceLengthHistogram", id: sampleId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
     232                                } else {
     233                                        chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
     234                                }
     235                               
     236                                [
     237                                        g.checkBox( name: "ids", value: sampleId, checked: false, onClick: "updateCheckAll(this);" ),
     238                                        it[ 1 ],        // it.sample.name
     239                                        it[ 2 ],        // it.assay.study.name
     240                                        it[ 3 ],        // it.assay.name
     241                                        it[ 4 ],        // it.fwMidName
     242                                        it[ 5 ],        // it.numSequences(),
     243                                        it[ 6 ],        // it.numQualScores(),
     244                                        editButton,
     245                                        deleteButton,
     246                                        chartButton
     247                                ]
     248                        },
     249                        columns,
     250                        groupColumns,
     251                        idColumn,
     252                        from,
     253                        total,
     254                        where,
     255                        parameters
     256                ) as JSON
     257        }
    181258}
  • trunk/grails-app/controllers/nl/tno/massSequencing/AssayController.groovy

    r52 r59  
    22
    33import java.util.List;
     4import grails.converters.JSON
    45
    56import org.codehaus.groovy.grails.commons.ConfigurationHolder
     
    910        def gscfService
    1011        def fuzzySearchService
     12        def dataTablesService
    1113
    1214        def fileService
     
    2123                        gscfAddUrl: gscfService.urlAddStudy() ]
    2224        }
     25       
     26        /**
     27        * Returns JSON data for the datatable with runs
     28        * @see http://www.datatables.net/usage/server-side
     29        * @see DataTablesService.retrieveData
     30        */
     31   def showAssayList = {
     32           // Determine the total number of assaysamples for this run
     33           int total = Assay.executeQuery( "SELECT COUNT(*) FROM Assay a WHERE a.study.trashcan = false")[ 0 ]
     34
     35           // Which columns are shown on screen and should be retrieved from the database
     36           def columns = [
     37                   null,
     38                   "a.name",
     39                   "a.study.name",
     40                   "COUNT( DISTINCT s )",
     41                   "SUM( sd.numSequences ) / COUNT( DISTINCT s )",
     42                   null,
     43                   null,
     44                   "a.study.studyToken"
     45           ]
     46           
     47           def idColumn = 'a.id';
     48           def groupColumns = [ idColumn ] + columns[1..2] + columns[ 7 ];
     49
     50           // Retrieve data from assaySample table
     51           def from = "Assay a LEFT JOIN a.assaySamples s LEFT JOIN s.sequenceData sd"
     52           def where = " EXISTS( FROM Auth auth WHERE auth.study = a.study AND auth.user = :user AND auth.canRead = true )"
     53           def parameters = [ "user": session.user ]
     54           
     55           // This closure determines what to do with a row that is retrieved from the database.
     56           def convertClosure = {
     57                   def assayId = it[ 0 ];
     58                   def assayName = it[ 1 ];
     59                   def studyName = it[ 2 ];
     60                   def numSamples = it[ 3 ];
     61                   def numSequences = it[ 4 ];
     62                   def studyToken = it[ 5 ];
     63                   
     64                   // Create buttons in the last three columns
     65                   def editButton = g.link( controller: "assay", action: "show", id: assayId, "title": "View assay"  ) { '<img src="' + fam.icon( name: 'application_form_magnify' ) + '" title="View assay" />' };
     66                   def chartButton = '';
     67                   
     68                   if( numSequences > 0 ) {
     69                           chartButton = g.link( controller: "assay", action: "sequenceLengthHistogram", id: assayId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
     70                   } else {
     71                           chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
     72                   }
     73                   
     74                   [
     75                           g.checkBox( name: "ids", value: assayId, checked: false, onClick: "updateCheckAll(this);" ),
     76                           g.link( title:"View assay", controller:"assay", action:"show", id: assayId ) { assayName },  // it.name
     77                           g.link( title:"View study", url: gscfService.urlViewStudy( studyToken )) { studyName },      // it.study.name
     78                           numSamples > 0 ? g.formatNumber( number: numSamples, format: "###,###,##0" ) : "-",  // it.numSequences(),
     79                           numSequences > 0 ? g.formatNumber( number: numSequences, format: "###,###,##0" ) : "-",      // it.numQualScores(),
     80                           editButton,
     81                           chartButton
     82                   ]
     83           }
     84           
     85           // Send the data to the user
     86           render dataTablesService.retrieveData(
     87                   params,
     88                   Assay.class,
     89                   convertClosure,
     90                   columns,
     91                   groupColumns,
     92                   idColumn,
     93                   from,
     94                   total,
     95                   where,
     96                   parameters
     97           ) as JSON
     98   }
     99
    23100
    24101        def show = {
     
    47124        }
    48125       
     126        /**
     127        * Returns JSON data for the datatable with assaysamples
     128        * @see http://www.datatables.net/usage/server-side
     129        * @see DataTablesService.retrieveData
     130        */
     131   def showSampleData = {
     132           // load run with id specified by params.id
     133           def assay = getAssay( params.id );
     134
     135           if (!assay) {
     136                   response.sendError(404, "Run not found" )
     137                   return
     138           }
     139           
     140           // Determine the total number of assaysamples for this run
     141           def total = (int) Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.assay.id = :assayId", [ "assayId": assay.id ] )[ 0 ];
     142
     143           // Which columns are shown on screen and should be retrieved from the database
     144           def columns = [
     145                   null,
     146                   "s.sample.name",
     147                   "run.name",
     148                   "s.fwMidName",
     149                   "SUM( sd.numSequences )",
     150                   "SUM( CASE WHEN sd.qualityFile IS NULL THEN 0 ELSE sd.numSequences END )",
     151                   null,
     152                   null,
     153                   's.assay.id',
     154                   's.assay.study.id',
     155           ]
     156           
     157           def idColumn = 's.id';
     158           def groupColumns = [ idColumn ] + columns[1..3] + columns[8..9];
     159
     160           // Retrieve data from assaySample table
     161           def from = "AssaySample s LEFT JOIN s.sequenceData as sd LEFT JOIN s.run as run"
     162
     163           // And filter by runId
     164           def where = "s.assay.id = :assayId"
     165           def parameters = [ "assayId": assay.id ];
     166           
     167           def canWrite = assay.study.canWrite( session.user );
     168
     169           // This closure determines what to do with a row that is retrieved from the database.
     170           def convertClosure = {
     171                   def sampleId = it[ 0 ];
     172                   def assayId = it[ 6 ];
     173                   def studyId = it[ 7 ];
     174                   def numSequences = it[ 4 ];
     175                   
     176                   // Create buttons in the last three columns
     177                   def editButton = '';
     178                   def chartButton = '';
     179                   
     180                   if( canWrite ) {
     181                           editButton = g.link( url: '#', title: "Edit sample", onClick: "showEditSampleDialog(" + sampleId + ", 'assay', " + assayId + ");"  ) { '<img src="' + fam.icon( name: 'pencil' ) + '" title="Edit sample" />' };
     182                   } else {
     183                           editButton = '<img src="' + fam.icon( name: 'pencil' ) + '" title="You can\'t edit this sample because you don\'t have sufficient privileges." class="disabled" />';
     184                   }
     185                   
     186                   if( numSequences > 0 ) {
     187                           chartButton = g.link( controller: "assaySample", action: "sequenceLengthHistogram", id: sampleId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
     188                   } else {
     189                           chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
     190                   }
     191                   
     192                   [
     193                           g.checkBox( name: "ids", value: sampleId, checked: false, onClick: "updateCheckAll(this);" ),
     194                           it[ 1 ],     // it.sample.name
     195                           it[ 2 ],     // it.run.name
     196                           it[ 3 ],     // it.fwMidName
     197                           it[ 4 ] > 0 ? g.formatNumber( number: it[ 4 ], format: "###,###,##0" ) : "-",        // it.numSequences(),
     198                           it[ 5 ] > 0 ? g.formatNumber( number: it[ 5 ], format: "###,###,##0" ) : "-",        // it.numQualScores(),
     199                           editButton,
     200                           chartButton
     201                   ]
     202           }
     203           
     204           // Send the data to the user
     205           render dataTablesService.retrieveData(
     206                   params,
     207                   AssaySample.class,
     208                   convertClosure,
     209                   columns,
     210                   groupColumns,
     211                   idColumn,
     212                   from,
     213                   total,
     214                   where,
     215                   parameters
     216           ) as JSON
     217   }
     218       
    49219        def sequenceLengthHistogram = {
    50                 def id = params.long( 'id' );
    51                 def assay = id ? Assay.get( id ) : null
    52                
    53                 if( !id || !assay ) {
    54                         flash.message = "No assay selected";
    55                         redirect( action: "index" );
    56                         return;
    57                 }
    58                
    59                 [ assay: assay, histogram: fastaService.sequenceLengthHistogram( assay.assaySamples.toList() ) ]
    60         }
     220                redirect( controller: "assaySample", action: "sequenceLengthHistogramForAssay", id: params.id );
     221        }
     222
    61223
    62224        def showByToken = {
  • trunk/grails-app/controllers/nl/tno/massSequencing/AssaySampleController.groovy

    r57 r59  
    3636                        return;
    3737                }
     38               
     39                def title = "sample " + assaySample.sample.name + " (" + assaySample.assay.study.name + " / " + assaySample.assay.name + ")"
     40                renderSequenceLengthHistogram( title, [assaySample] )
     41        }
     42       
     43        def sequenceLengthHistogramForRun = {
     44                def id = params.long( 'id' );
     45                def run = id ? Run.get( id ) : null
    3846
    39                 [ assaySample: assaySample, histogram: fastaService.sequenceLengthHistogram( [assaySample] ) ]
     47                if( !id || !run ) {
     48                        flash.message = "No run selected";
     49                        redirect( controller: 'run', action: "index" );
     50                        return;
     51                }
     52               
     53                def title = "run " + run.name
     54                renderSequenceLengthHistogram( title, run.assaySamples?.toList() )
     55        }
     56       
     57        def sequenceLengthHistogramForAssay = {
     58                def id = params.long( 'id' );
     59                def assay = id ? Assay.get( id ) : null
     60
     61                if( !id || !assay ) {
     62                        flash.message = "No assay selected";
     63                        redirect( controller: 'assay', action: "index" );
     64                        return;
     65                }
     66               
     67                def title = "assay " + assay.name
     68                renderSequenceLengthHistogram( title, assay.assaySamples?.toList() )
     69        }
     70       
     71        def sequenceLengthHistogramForStudy = {
     72                def id = params.long( 'id' );
     73                def assay = id ? Study.get( id ) : null
     74
     75                if( !id || !study ) {
     76                        flash.message = "No study selected";
     77                        redirect( controller: 'study', action: "index" );
     78                        return;
     79                }
     80               
     81                def title = "study " + study.name
     82                renderSequenceLengthHistogram( title, study.assays*.assaySamples.flatten().unique().findAll { it } )
     83        }
     84        /**
     85         * Renders a histogram for the given assaySamples
     86         * @param title
     87         * @param assaySamples
     88         */
     89        protected void renderSequenceLengthHistogram( String title, def assaySamples ) {
     90                def numSequences = assaySamples.collect { it.numSequences() }.sum()
     91                render( view: "sequenceLengthHistogram", model: [ title: title, numSequences: numSequences, histogram: fastaService.sequenceLengthHistogram( assaySamples ) ] );
    4092        }
    4193
  • trunk/grails-app/controllers/nl/tno/massSequencing/RunController.groovy

    r52 r59  
    22
    33import java.util.Date;
     4import grails.converters.JSON
     5import nl.tno.massSequencing.auth.*
    46
    57import org.codehaus.groovy.grails.commons.ConfigurationHolder
     
    1012        def sampleExcelService
    1113        def fastaService
     14        def dataTablesService
    1215
    1316        def index = {
    1417                [runs: Run.list(), user: session.user]
    1518        }
     19       
     20        /**
     21        * Returns JSON data for the datatable with runs
     22        * @see http://www.datatables.net/usage/server-side
     23        * @see DataTablesService.retrieveData
     24        */
     25   def showRunList = {
     26           // Determine the total number of assaysamples for this run
     27           def total = Run.count()
     28
     29           // Which columns are shown on screen and should be retrieved from the database
     30           def columns = [
     31                   null,
     32                   "r.name",
     33                   "COUNT( a )",
     34                   "SUM( sd.numSequences )",
     35                   null,
     36                   null,
     37                   null
     38           ]
     39           
     40           def idColumn = 'r.id';
     41           def groupColumns = [ idColumn ] + columns[1];
     42
     43           // Retrieve data from assaySample table
     44           def from = "Run r LEFT JOIN r.assaySamples a LEFT JOIN a.sequenceData sd"
     45
     46           // This closure determines what to do with a row that is retrieved from the database.
     47           def convertClosure = {
     48                   def runId = it[ 0 ];
     49                   def runName = it[ 1 ];
     50                   def numSamples = it[ 2 ];
     51                   def numSequences = it[ 3 ];
     52                   
     53                   // Create buttons in the last three columns
     54                   def editButton = g.link( controller: "run", action: "show", id: it[ 0 ], "title": "View run"  ) { '<img src="' + fam.icon( name: 'application_form_magnify' ) + '" title="View run" />' };
     55                   def deleteButton = '';
     56                   def chartButton = '';
     57                   
     58                   if( numSequences > 0 ) {
     59                           chartButton = g.link( controller: "run", action: "sequenceLengthHistogram", id: runId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
     60                   } else {
     61                           chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
     62                   }
     63                   
     64                   if( numSequences > 0 || Run.hasNonWritableAssays( runId, session.user.id ) ) {
     65                           deleteButton = '<img src="' + fam.icon( name: 'delete' ) + '" class="disabled" alt="Run can not be deleted because data is associated with it." title="Run can not be deleted because data is associated with it." />'
     66                   } else {
     67                                deleteButton = g.link( title:"Delete run", onClick:"return confirm( 'Are you sure you want to delete this run?' );", controller:"run", action:"deleteRun", id: runId ) { '<img src="' + fam.icon( name: 'delete' ) + '" alt="Delete run" title="Delete run" />' }
     68                   }
     69                   
     70                   [
     71                           g.checkBox( name: "ids", value: runId, checked: false, onClick: "updateCheckAll(this);" ),
     72                           g.link( title:"View run", controller:"run", action:"show", id: runId ) { runName },  // it.name
     73                           numSamples > 0 ? g.formatNumber( number: numSamples, format: "###,###,##0" ) : "-",  // it.numSequences(),
     74                           numSequences > 0 ? g.formatNumber( number: numSequences, format: "###,###,##0" ) : "-",      // it.numQualScores(),
     75                           editButton,
     76                           deleteButton,
     77                           chartButton
     78                   ]
     79           }
     80           
     81           // Send the data to the user
     82           render dataTablesService.retrieveData(
     83                   params,
     84                   Run.class,
     85                   convertClosure,
     86                   columns,
     87                   groupColumns,
     88                   idColumn,
     89                   from,
     90                   total,
     91           ) as JSON
     92   }
     93
    1694
    1795        def show = {
     
    40118                def otherAssays = Assay.list( sort: "name" ).findAll { !it.runs.contains( run ) && it.study.canRead( session.user ) }
    41119
     120                // Determine several parameters to show on screen
     121               
     122               
    42123                // Send the assay information to the view
    43124                [run: run, allRuns: Run.list(), otherAssays: otherAssays, editable: true]
     125        }
     126       
     127        /**
     128         * Returns JSON data for the datatable with assaysamples
     129         * @see http://www.datatables.net/usage/server-side
     130         * @see DataTablesService.retrieveData
     131         */
     132        def showSampleData = {
     133                // load run with id specified by params.id
     134                def run = getRun( params.id );
     135
     136                if (!run) {
     137                        response.sendError(404, "Run not found" )
     138                        return
     139                }
     140               
     141                // Determine the total number of assaysamples for this run
     142                def total = (int) Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.run.id = :runId AND EXISTS( FROM Auth a3 WHERE s.assay.study = a3.study AND a3.user = :user AND a3.canRead = true )", [ "runId": run.id, "user": session.user  ] )[ 0 ];
     143
     144                // Which columns are shown on screen and should be retrieved from the database
     145                def columns = [
     146                        null,
     147                        "s.sample.name",
     148                        "s.assay.study.name",
     149                        "s.assay.name",
     150                        "s.fwMidName",
     151                        "SUM( sd.numSequences )",
     152                        "SUM( CASE WHEN sd.qualityFile IS NULL THEN 0 ELSE sd.numSequences END )",
     153                        null,
     154                        null,
     155                        null,
     156                        's.run.id',
     157                        's.assay.id',
     158                        's.assay.study.id',
     159                ]
     160               
     161                def idColumn = 's.id';
     162                def groupColumns = [ idColumn ] + columns[1..4] + columns[10..12];
     163
     164                // Retrieve data from assaySample table
     165                def from = "AssaySample s LEFT JOIN s.sequenceData as sd"
     166
     167                // And filter by runId
     168                def where = "s.run.id = :runId AND EXISTS( FROM Auth a3 WHERE s.assay.study = a3.study AND a3.user = :user AND a3.canRead = true )"
     169                def parameters = [ "runId": run.id, "user": session.user ];
     170               
     171                // This closure determines what to do with a row that is retrieved from the database.
     172                def convertClosure = {
     173                        def sampleId = it[ 0 ];
     174                        def runId = it[ 7 ];
     175                        def assayId = it[ 8 ];
     176                        def studyId = it[ 9 ];
     177                        def numSequences = it[ 5 ];
     178                       
     179                        // TODO: Rewrite this authorization part in a more efficient way
     180                        def auth = Auth.executeQuery( "SELECT a.canWrite FROM Auth a WHERE a.study.id = :studyId AND a.user.id = :userId", [ 'studyId': studyId, 'userId': session.user?.id ] );
     181                        def canWrite = auth ? auth[ 0 ] : false;
     182                       
     183                        // Create buttons in the last three columns
     184                        def editButton = '';
     185                        def deleteButton = '';
     186                        def chartButton = '';
     187                       
     188                        if( canWrite ) {
     189                                editButton = g.link( url: '#', title: "Edit sample", onClick: "showEditSampleDialog(" + sampleId + ", 'run', " + runId + ");"  ) { '<img src="' + fam.icon( name: 'pencil' ) + '" title="Edit sample" />' };
     190                                deleteButton = g.link( controller: 'run', action: 'removeSample', id: it[ 7 ], params: [ 'assaySampleId': sampleId ], title: "Remove sample from run", onClick: "return confirm( 'Are you sure you want to remove the selected sample from this run?' );"  ) { '<img src="' + fam.icon( name: 'application_delete' ) + '" title="Remove sample from run" />' };
     191                        } else {
     192                                editButton = '<img src="' + fam.icon( name: 'pencil' ) + '" title="You can\'t edit this sample because you don\'t have sufficient privileges." class="disabled" />';
     193                                deleteButton = '<img src="' + fam.icon( name: 'application_delete' ) + '" title="You can\'t remove this sample because you don\'t have sufficient privileges." class="disabled" />';
     194                        }
     195                       
     196                        if( numSequences > 0 ) {
     197                                chartButton = g.link( controller: "assaySample", action: "sequenceLengthHistogram", id: sampleId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
     198                        } else {
     199                                chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
     200                        }
     201                       
     202                        [
     203                                g.checkBox( name: "ids", value: sampleId, checked: false, onClick: "updateCheckAll(this);" ),
     204                                it[ 1 ],        // it.sample.name
     205                                it[ 2 ],        // it.assay.study.name
     206                                it[ 3 ],        // it.assay.name
     207                                it[ 4 ],        // it.fwMidName
     208                                it[ 5 ] > 0 ? g.formatNumber( number: it[ 5 ], format: "###,###,##0" ) : "-",   // it.numSequences(),
     209                                it[ 6 ] > 0 ? g.formatNumber( number: it[ 6 ], format: "###,###,##0" ) : "-",   // it.numQualScores(),
     210                                editButton,
     211                                deleteButton,
     212                                chartButton
     213                        ]
     214                }
     215               
     216                // Send the data to the user
     217                render dataTablesService.retrieveData(
     218                        params,
     219                        AssaySample.class,
     220                        convertClosure,
     221                        columns,
     222                        groupColumns,
     223                        idColumn,
     224                        from,
     225                        total,
     226                        where,
     227                        parameters
     228                ) as JSON
    44229        }
    45230
     
    699884       
    700885        def sequenceLengthHistogram = {
    701                 def id = params.long( 'id' );
    702                 def run = id ? Run.get( id ) : null
    703                
    704                 if( !id || !run ) {
    705                         flash.message = "No run selected";
    706                         redirect( action: "index" );
    707                         return;
    708                 }
    709                
    710                 [ run: run, histogram: fastaService.sequenceLengthHistogram( run.assaySamples?.toList() ) ]
     886                redirect( controller: "assaySample", action: "sequenceLengthHistogramForRun", id: params.id );
    711887        }
    712888       
  • trunk/grails-app/controllers/nl/tno/massSequencing/StudyController.groovy

    r52 r59  
    2626       
    2727        def sequenceLengthHistogram = {
    28                 def id = params.long( 'id' );
    29                 def study = id ? Study.get( id ) : null
    30                
    31                 if( !id || !study ) {
    32                         flash.message = "No study selected";
    33                         redirect( action: "index" );
    34                         return;
    35                 }
    36                
    37                 [ study: study, histogram: fastaService.sequenceLengthHistogram( study.assays*.assaySamples.flatten().unique().findAll { it } ) ]
     28                redirect( controller: "assaySample", action: "sequenceLengthHistogramForStudy", id: params.id );
    3829        }
    3930
  • trunk/grails-app/controllers/nl/tno/massSequencing/files/ImportController.groovy

    r58 r59  
    1919         *************************************************************************/
    2020
    21         /** 
    22          * Shows a screen that processing is done
     21        /**
     22         * Shows a screen to indicate that files are being parsed
    2323         */
    24         def showProcessScreen = {
     24        def parseUploadedFiles = {
    2525                def entityType = params.entityType
    26 
     26               
    2727                // Check whether files are given
    2828                def names = params.list( 'sequencefiles' )
     
    3737                        return
    3838                }
    39                        
     39               
     40                // Create a unique process identifier
     41                String processId = UUID.randomUUID().toString();
     42                                       
    4043                // Save filenames in session
    41                 session.processFilenames = names;
     44                if( !session.process )
     45                        session.process = [:]
     46                       
     47                if( !session.process[ processId ] )
     48                        session.process[ processId ] = [:]
     49                         
     50                session.process[ processId ].filenames = names;
     51                session.process[ processId ].entityId = params.id;
     52                session.process[ processId ].entityType = entityType
    4253               
    4354                // Check for total size of the files in order to be able
     
    4758                        filesize += fileService.get( it )?.length()
    4859                }
    49 
    50                 session.processProgress = [
     60               
     61                if( !session.progress )
     62                        session.progress = [:]
     63               
     64                session.progress[ processId ] = [
    5165                        stepNum: 1,
    5266                        numSteps: 2,
     
    5670                        stepTotal: filesize
    5771                ]
    58                        
    59                 [entityId: params.id, entityType: params.entityType, filenames: names, url: createLink( action: 'showProcessResult', id: params.id, params: [entityType: entityType] ) ]
     72                                       
     73                render( view: 'showProcessScreen', model: [
     74                        processUrl: createLink( controller: "import", action: "processUploadedFiles" ),
     75                        processParameters: [ processId: processId, entityId: params.id, entityType: params.entityType ],
     76                        progressUrl: createLink( controller: "import", action: "getProgress", params: [ processId: processId ] ),
     77                        finishUrl: createLink( controller: "import", action: 'parseUploadResult', params: [ processId: processId, id: params.id, entityType: entityType] ),
     78                        errorUrl: createLink( controller: entityType, action: "show", id: params.id ),
     79                        entityId: params.id, entityType: params.entityType] );
    6080        }
    6181       
     
    6383         * Processes uploaded files and tries to combine them with samples
    6484         */
    65         def process = {
     85        def processUploadedFiles = {
     86                def processId = params.processId
    6687                def entity
    67                 def assaySamples
    6888               
    6989                switch( params.entityType ) {
    7090                        case "run":
    71                                 entity = getRun( params.id );
    72                                 assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
     91                                entity = getRun( params.entityId );
    7392                                break;
    7493                        case "assay":
    75                                 entity = getAssay( params.id );
    76                                 assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
     94                                entity = getAssay( params.entityId );
    7795                                break;
    7896                        default:
     
    8199                                return;
    82100                }
    83 
     101               
     102                def assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
     103               
    84104                if (!entity) {
    85105                        response.setStatus( 404, flash.error )
     
    89109
    90110                // Check whether files are given
    91                 def names = session.processFilenames
     111                def names = session.process[ processId ]?.filenames
    92112
    93113                if( !names ) {
     114                        println "Process ID: " + processId
     115                        session.process.each {
     116                                println it.key + " = " + it.value;
     117                        }
    94118                        response.setStatus( 500, "No files uploaded for processing" )
    95119                        render "";
     
    123147                def onProgress = { progress, total ->
    124148                        // Update progress
    125                         httpSession.processProgress.stepTotal = total;
    126                         httpSession.processProgress.stepProgress = progress;
     149                        httpSession.progress[ processId ].stepTotal = total;
     150                        httpSession.progress[ processId ].stepProgress = progress;
    127151                }
    128152                def newStep = { total, description ->
    129153                        // Start a new step
    130                         httpSession.processProgress.stepTotal = total;
    131                         httpSession.processProgress.stepProgress = 0;
    132                        
    133                         httpSession.processProgress.stepDescription = description;
    134                         httpSession.processProgress.stepNum++;
    135                 }
    136 
    137                 def parsedFiles = importService.parseFiles( filenames, onProgress, [progress: 0, total: httpSession.processProgress.stepTotal ], newStep );
    138                
    139                 println "Parsed files success: " + parsedFiles.success
     154                        httpSession.progress[ processId ].stepTotal = total;
     155                        httpSession.progress[ processId ].stepProgress = 0;
     156                       
     157                        httpSession.progress[ processId ].stepDescription = description;
     158                        httpSession.progress[ processId ].stepNum++;
     159                }
     160
     161                def parsedFiles = importService.parseFiles( filenames, onProgress, [progress: 0, total: httpSession.progress[ processId ].stepTotal ], newStep );
    140162               
    141163                // Determine excel matches from the uploaded files
    142164                parsedFiles.success = fastaService.inferExcelMatches( parsedFiles.success );
    143165               
    144                 // Now check whether a taxonomy and groups file are uploaded. If so send them to the classificationService for
    145                 // parsing
    146                 def classificationFiles = parsedFiles.success.findAll { it.type == 'groups' || it.type == 'taxonomy' };
    147                 def excelFiles = parsedFiles.success.findAll { it.type == 'excel' }
    148                 def excelMatches = [:]
    149                
    150                 // Find a match between input (mothur) samples and the samples in the system
    151                 excelFiles.each { excelFile ->
    152                         if( excelFile.matches && excelFile.matches[ 'mothursamples' ] ) {
    153                                 excelFile.matches[ 'mothursamples' ].each { excelMatch ->
    154                                         def foundSample = assaySamples.find { it.sample.name == excelMatch.samplename };
    155                                         if( foundSample )
    156                                                 excelMatches[ excelMatch.mothurname ] = foundSample
    157                                 }
    158                         }
    159                 }
    160                
    161                 if( classificationFiles ) {
    162                         parsedFiles.success -= classificationFiles;
    163 
    164                         // If no excel matches are found, no classifications can be stored
    165                         if( !excelMatches ) {
    166                                 classificationFiles.each {
    167                                         it.success = false
    168                                         it.message = "An excel file which maps the input samples with samples in the system must be provided."
    169                                        
    170                                         parsedFiles.failure << it;
    171                                 }
    172                         } else {
    173                                 // Set progress to a new step, so the user knows that it will take a while
    174                                 long totalSize = classificationFiles.collect { it.filesize }.sum();
    175                                 newStep( totalSize, "Storing classification" )
    176                                 println "Total size: " + totalSize;
    177                                
    178                                 classificationFiles = classificationService.storeClassification( classificationFiles, excelMatches, { bytesProcessed -> httpSession.processProgress.stepProgress += bytesProcessed } );
    179                                
    180                                 classificationFiles.each {
    181                                         if( it.success ) {
    182                                                 parsedFiles.success << it;
    183                                         } else {
    184                                                 parsedFiles.failure << it;
    185                                         }
    186                                 }
    187                         }
    188                 }
    189                
    190166                // Match files with samples in the database
    191167                def matchedFiles = fastaService.matchFiles( parsedFiles.success, assaySamples );
     
    194170                matchedFiles.sort { a,b -> a.fasta?.originalfilename <=> b.fasta?.originalfilename }
    195171
     172                // Retrieve all files that have not been matched
     173                def notMatchedFiles = parsedFiles.success.findAll {
     174                        switch( it.type ) {
     175                                case "fasta":
     176                                        return !matchedFiles*.fasta*.filename.contains( it.filename );
     177                                case "qual":
     178                                        return !matchedFiles*.feasibleQuals.flatten().filename.contains( it.filename );
     179                                case "taxonomy":
     180                                        return !matchedFiles*.feasibleClassifications.flatten().filename.contains( it.filename );
     181                        }
     182                        return false;
     183                }
     184               
    196185                // Saved file matches in session to use them later on
    197                 session.processedFiles = [ parsed: parsedFiles,  matched: matchedFiles ];
     186                session.process[ processId ].processedFiles = [ parsed: parsedFiles,  matched: matchedFiles, notMatched: notMatchedFiles ];
    198187
    199188                render ""
    200189        }
    201        
     190
    202191        def getProgress = {
    203                 if( !session.processProgress ) {
     192                def processId = params.processId;
     193                if( !processId || !session.progress?.getAt( processId ) ) {
    204194                        response.setStatus( 500, "No progress information found" );
    205195                        render ""
     
    207197                }
    208198               
    209                 render session.processProgress as JSON
     199                render session.progress[ processId ] as JSON
    210200        }
    211201       
    212202        /**
    213          * Show result of processing
     203         * Show result of processing uploaded files (step 1)
    214204         */
    215         def showProcessResult = {
     205        def parseUploadResult = {
     206                def processId = params.processId;
    216207                // load study with id specified by param.id
    217208                def entity
     
    229220                                return;
    230221                }
    231 
     222               
     223                def assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
     224               
    232225                if (!entity) {
    233226                        response.setStatus( 404, flash.error )
     
    236229                }
    237230               
    238                 if( !session.processedFiles ) {
     231                if( !session.process[ processId ].processedFiles ) {
    239232                        flash.error = "Processing of files failed. Maybe the session timed out."
    240                         redirect( controller: 'assay', action: 'show', 'id': params.id)
    241                         return
    242                 }
    243                
    244                 [entityType: params.entityType, entity: entity, id: params.id, parsedFiles: session.processedFiles.parsed, classificationFiles: session.processedFiles.parsed.success.findAll { it.type == 'taxonomy' }, matchedFiles: session.processedFiles.matched, selectedRun: params.selectedRun ]
     233                        redirect( controller: params.entityType, action: 'show', 'id': params.id)
     234                        return
     235                }
     236               
     237               
     238                // Find matching sequenceData objects for taxonomyfiles that have not been matched
     239                def notMatchedFiles = session.process[ processId ].processedFiles.notMatched;
     240                def extraClassifications = notMatchedFiles.findAll { it.type == "taxonomy" }
     241                extraClassifications.collect {
     242                        // Find all sequence files that have the correct number of sequences and are in the list of assaySamples
     243                        it[ 'feasibleSequenceData' ] = SequenceData.findAllByNumSequences( it.numLines ).findAll { assaySamples.contains( it.sample ) }
     244                        return it
     245                }
     246               
     247                [       entityType: params.entityType, processId: processId, entity: entity, id: params.id,
     248                        parsedFiles: session.process[ processId ].processedFiles.parsed,
     249                        matchedFiles: session.process[ processId ].processedFiles.matched,
     250                        remainingClassificationFiles: extraClassifications,
     251                        selectedRun: params.selectedRun ]
    245252        }
    246253
     
    249256         */
    250257        def returnWithoutSaving = {
     258                def processId = params.processId;
     259
    251260                // Delete all uploaded files from disk
    252                 session.processedFiles?.parsed?.success?.each {
     261                session.process[ processId ]?.processedFiles?.parsed?.success?.each {
    253262                        fileService.delete( it.filename );
    254263                }
     
    269278        }
    270279       
     280        /**
     281         * Shows a screen with the progress of saving matched files
     282         */
     283        def saveMatchedFiles = {
     284                def entityType = params.entityType
     285                def processId = params.processId
     286               
     287                session.process[ processId ].matchedFiles = params.file
     288                session.process[ processId ].matchedRemainingClassification = params.remainingClassification
     289               
     290                // Check for total size of the classification files in order to be able
     291                // to show a progress bar. The handling of classification files is orders
     292                // of magnitude bigger than the rest, so we only show progress of those files
     293                long filesize = 0;
     294
     295                // Loop through all files. Those are the numeric elements in the 'files' array
     296                def digitRE = ~/^\d+$/;
     297                params.file.findAll { it.key.matches( digitRE ) }.each { file ->
     298                        def filevalue = file.value;
     299                       
     300                        // Check if the file is selected
     301                        if( filevalue.include == "on" ) {
     302                                if( fileService.fileExists( filevalue.fasta ) ) {
     303                                        // Also save classification data for this file, if it is present
     304                                        if( filevalue.classification ) {
     305                                                filesize += fileService.get( filevalue.classification )?.size()
     306                                        }
     307                                }
     308                        }
     309                }
     310                params.remainingClassification.findAll { it.key.matches( digitRE ) }.each { file ->
     311                        def filevalue = file.value;
     312                       
     313                        // Check if the file is selected
     314                        if( filevalue.include == "on" ) {
     315                                if( fileService.fileExists( filevalue.filename ) ) {
     316                                        // Also save classification data for this file, if it is present
     317                                        filesize += fileService.get( filevalue.filename )?.size()
     318                                }
     319                        }
     320                }
     321
     322                if( !session.progress )
     323                        session.progress = [:]
     324                       
     325                session.progress[ processId ] = [
     326                        stepNum: 2,
     327                        numSteps: 2,
     328                        stepDescription: 'Store sequence data and classification',
     329                       
     330                        stepProgress: 0,
     331                        stepTotal: filesize
     332                ]
     333                                       
     334                render( view: 'showProcessScreen', model: [
     335                        processUrl: createLink( controller: "import", action: "processMatchedFiles" ),
     336                        processParameters: [ processId: processId, entityId: params.id, entityType: params.entityType ],
     337                        progressUrl: createLink( controller: "import", action: "getProgress", params: [ processId: processId ] ),
     338                        finishUrl: createLink( controller: "import", action: 'saveMatchedResult', params: [ processId: processId, id: params.id, entityType: entityType] ),
     339                        errorUrl: createLink( controller: entityType, action: "show", id: params.id ),
     340                        entityId: params.id, entityType: params.entityType] );
     341        }
     342       
    271343        /**
    272344         * Saves processed files to the database, based on the selections made by the user
    273345         */
    274         def saveProcessedFiles = {
     346        def processMatchedFiles = {
    275347                // load entity with id specified by param.id
    276                 def entity
    277                
    278                 switch( params.entityType ) {
    279                         case "run":
    280                                 entity = getRun( params.id );
    281                                 break;
    282                         case "assay":
    283                                 entity = getAssay( params.id );
    284                                 break;
    285                         default:
    286                                 response.setStatus( 404, "No entity found" );
    287                                 render "";
    288                                 return;
    289                 }
    290 
    291                 if (!entity) {
    292                         response.setStatus( 404, flash.error )
    293                         render "";
    294                         return
    295                 }
     348                def processId = params.processId;
    296349
    297350                // Check whether files are given
    298                 def files = params.file
    299 
    300                 if( !files ) {
    301                         flash.message = "No files were selected."
    302                         redirect( controller: params.entityType, action: 'show', 'id': params.id)
     351                def files = session.process[ processId ].matchedFiles 
     352                def remainingClassification = session.process[ processId ].matchedRemainingClassification;
     353               
     354                if( !files && !remainingClassification ) {
     355                        flash.message = "No files were selected for import."
     356                        redirect( controller: params.entityType, action: 'show', 'id': params.entityId)
    303357                        return
    304358                }
    305359
    306360                File permanentDir = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir )
     361               
     362                // This closure enables keeping track of the progress
     363                def httpSession = session;
     364                def onProgress = { progress ->
     365                        // Update progress
     366                        httpSession.progress[ processId ].stepProgress += progress;
     367                }
     368               
     369                // Loop through all FASTA files. Those are the numeric elements in the 'files' array
     370                def fastaReturn = saveMatchedFastaFiles( files, session.process[ processId ]?.processedFiles, onProgress );
     371                def classificationReturn = saveRemainingClassificationFiles( remainingClassification, onProgress );
     372               
     373                // Update classification (summary) for updated samples
     374                def samplesClassified = [] + fastaReturn.samplesClassified + classificationReturn.samplesClassified;
     375                classificationService.updateClassificationForAssaySamples( samplesClassified.findAll { it }.unique() )
     376               
     377                def returnStructure = [
     378                        numSequenceFiles: fastaReturn.numSequenceFiles,
     379                        numQualFiles: fastaReturn.numQualFiles,
     380                        numClassificationFiles: fastaReturn.numClassificationFiles,
     381                        numExtraClassificationFiles: classificationReturn.numExtraClassifications,
     382                        numTotal: fastaReturn.numSequenceFiles + classificationReturn.numExtraClassifications,
     383                        errors: [] + fastaReturn.errors + classificationReturn.errors
     384                ]
     385
     386                // Return all files that have not been moved
     387                session.process[ processId ]?.processedFiles?.parsed?.success?.each {
     388                        fileService.delete( it.filename );
     389                }
     390               
     391                session.process[ processId ].result = returnStructure;
     392               
     393                response.contentType = "text/plain"
     394                render "";
     395        }
     396       
     397        def saveMatchedFastaFiles( def files, processedFiles, Closure onProgress ) {
    307398                int numSuccesful = 0;
     399                int numQualFiles = 0;
     400                int numClassificationFiles = 0;
     401                def samplesClassified = [];
    308402                def errors = [];
    309                
    310                 // Loop through all files. Those are the numeric elements in the 'files' array
     403
    311404                def digitRE = ~/^\d+$/;
    312405                files.findAll { it.key.matches( digitRE ) }.each { file ->
     
    317410                                if( fileService.fileExists( filevalue.fasta ) ) {
    318411                                        try {
    319                                                 def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, session.processedFiles );
     412                                                def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, processedFiles );
    320413                                               
    321414                                                // Save the data into the database
     
    335428                                                } else {
    336429                                                        sd.save(flush:true);
     430                                                       
     431                                                        // Also save classification data for this file, if it is present
     432                                                        if( filevalue.classification ) {
     433                                                                classificationService.storeClassification( filevalue.classification, sd, onProgress )
     434                                                                samplesClassified << sample
     435                                                               
     436                                                                numClassificationFiles++;
     437                                                        }
     438                                                       
     439                                                        if( sd.qualityFile )
     440                                                                numQualFiles++;
     441                                                       
     442                                                        numSuccesful++;
    337443                                                }
    338                                                
    339                                                 numSuccesful++;
    340444                                        } catch( Exception e ) {
     445                                                e.printStackTrace();
    341446                                                errors << "an error occurred while saving " + filevalue.fasta + ": " + e.getMessage()
    342447                                        }
     
    347452                        }
    348453                }
    349 
    350                 // Return all files that have not been moved
    351                 session.processedFiles?.parsed?.success?.each {
    352                         fileService.delete( it.filename );
    353                 }
    354                
     454               
     455                return [ numSequenceFiles: numSuccesful, numQualFiles: numQualFiles, numClassificationFiles: numClassificationFiles, errors: errors, samplesClassified: samplesClassified.unique() ]
     456        }
     457       
     458        def saveRemainingClassificationFiles( def files, Closure onProgress ) {
     459                def digitRE = ~/^\d+$/;
     460                def errors = [];
     461                def samplesClassified = [];
     462                def numSuccesful = 0;
     463               
     464                files.findAll { it.key.matches( digitRE ) }.each { file ->
     465                        def filevalue = file.value;
     466                       
     467                        // Check if the file is selected
     468                        if( filevalue.include == "on" ) {
     469                                if( fileService.fileExists( filevalue.filename ) ) {
     470                                        def sequenceDataId = filevalue.sequenceData;
     471                                        try {
     472                                                if( sequenceDataId.toString().isLong() ) {
     473                                                        // Retrieve sequenceData and sample now, because the session will be cleared during import
     474                                                        def sequenceData = SequenceData.get( sequenceDataId.toString().toLong() );
     475                                                        def sample = sequenceData.sample;
     476                                                       
     477                                                        if( sequenceData ) {
     478                                                                classificationService.removeClassificationForSequenceData( sequenceData );
     479                                                                classificationService.storeClassification( filevalue.filename, sequenceData, onProgress )
     480                                                                samplesClassified << sample;
     481                                                        }
     482       
     483                                                        numSuccesful++;
     484                                                } else {
     485                                                        errors << "a wrong ID is entered for classification file " + filevalue.filename;
     486                                                }
     487                                        } catch( Exception e ) {
     488                                                e.printStackTrace();
     489                                                errors << "an error occurred while saving " + filevalue.filename + ": " + e.getMessage()
     490                                        }
     491                                }
     492                        }
     493                       
     494                        // File doesn't need to be included in the system. Delete it from disk.
     495                        fileService.delete( filevalue.filename );
     496                }
     497               
     498                return [ numExtraClassifications: numSuccesful, errors: errors, samplesClassified: samplesClassified.unique()  ]
     499               
     500        }
     501
     502        /**
     503         * Redirects the user back to the start screen with a message about how things went
     504         */
     505        def saveMatchedResult = {
     506                def processId = params.processId
     507               
     508                def result = session.process[ processId ].result
     509
    355510                // Return a message to the user
    356                 if( numSuccesful == 0 ) {
    357                         flash.error = "None of the files were imported, because "
    358                        
    359                         if( errors.size() > 0 ) {
    360                                 errors.each {
     511                if( result.numTotal == 0 ) {
     512                       
     513                        if( result.errors.size() > 0 ) {
     514                                flash.error = "None of the files were imported, because "
     515                                result.errors.each {
    361516                                        flash.error += "<br />- " + it
    362517                                }
    363518                        } else {
    364                                 flash.error = "none of the files were selected for import."
     519                                flash.message = "None of the files were imported, because none of the files were selected for import."
    365520                        }
    366521                } else {
    367                         flash.message = numSuccesful + " files have been added to the system. "
    368 
    369                         if( errors.size() > 0 ) {
    370                                 flash.error += errors.size() + " errors occurred during import: "
    371                                 errors.each {
     522                        flash.message = ""     
     523                        if( result.numSequenceFiles == 1 ) {
     524                                flash.message += result.numSequenceFiles + " sequence file has been added to the system"
     525                        } else if( result.numSequenceFiles > 1 ) {
     526                                flash.message += result.numSequenceFiles + " sequence files have been added to the system"
     527                        }
     528                       
     529                        if( result.numQualFiles > 0 || result.numClassificationFiles > 0 ) {
     530                                flash.message += ", with";
     531                        }
     532                       
     533                        if( result.numQualFiles == 1 ) {
     534                                flash.message += " 1 quality file"
     535                        } else if( result.numQualFiles > 1 ) {
     536                                flash.message += " " + result.numQualFiles + " quality files"
     537                        }
     538                       
     539                        if( result.numQualFiles > 0 && result.numClassificationFiles > 0 ) {
     540                                flash.message += " and";
     541                        }
     542                       
     543                        if( result.numClassificationFiles == 1 ) {
     544                                flash.message += " 1 classification file"
     545                        } else if( result.numClassificationFiles > 1 ) {
     546                                flash.message += " " + result.numClassificationFiles + " classification files"
     547                        }
     548                       
     549                        if( flash.message )
     550                                flash.message += "."
     551
     552                        if( result.numExtraClassificationFiles == 1 ) {
     553                                flash.message += result.numExtraClassificationFiles + " additional classification file has been read. ";
     554                        } else if( result.numExtraClassificationFiles > 1 ) {
     555                                flash.message += result.numExtraClassificationFiles + " additional classification files have been read. ";
     556                        }
     557
     558                        if( result.errors.size() > 0 ) {
     559                                flash.error = "However, " + result.errors.size() + " errors occurred during import: "
     560                                result.errors.each {
    372561                                        flash.error += "<br />- " + it
    373562                                }
     
    375564                }
    376565               
     566                // Clear session
     567                session.process?.remove( processId );
     568                session.progress?.remove( processId );
     569               
     570                // Redirect user
    377571                redirect( controller: params.entityType, action: "show", id: params.id )
    378572        }
  • trunk/grails-app/domain/nl/tno/massSequencing/AssaySample.groovy

    r58 r59  
    308308                                        if( sd )
    309309                                                otherAssaySample.addToSequenceData( sd );
    310                                                
     310
     311                                        // Copy all sequence classifications to the new sequenceData
     312                                        Sequence.executeUpdate( "UPDATE Sequence s SET s.sequenceData = :new WHERE s.sequenceData = :old ", [ 'old': dataList[ j ], 'new': sd ] )
     313                               
    311314                                        // Remove the old sequencedata object
    312315                                        this.removeFromSequenceData( dataList[ j ] );
     
    314317                        }
    315318                }
    316                
    317                 // Copy all sequence classifications to the new assaysample
    318                 Sequence.executeUpdate( "UPDATE Sequence s SET s.assaySample = :new WHERE s.assaySample = :old ", [ 'old': this, 'new': otherAssaySample ] )
    319319       
    320320                // Copy run properties
     
    344344               
    345345                def numFiles = 0;
    346                 sequenceData.each { sequenceData ->
     346                def data = [] + sequenceData
     347                data.each { sequenceData ->
    347348                        numFiles += sequenceData.numFiles();
    348349                 
     
    350351                        sequenceData.delete(flush:true);
    351352                }
    352 
    353                 // Remove all sequence objects referencing this sequenceData object
    354                 Sequence.executeUpdate( "DELETE FROM Sequence s WHERE s.assaySample = ?", [this])
    355353               
    356354                resetStats();
  • trunk/grails-app/domain/nl/tno/massSequencing/Run.groovy

    r52 r59  
    137137                return hasPrivileges
    138138        }
     139       
     140        public static hasNonWritableAssays( runId, userId ) {
     141                def queryNonWritableAssays = Run.executeQuery( "SELECT COUNT(*) FROM Run r LEFT JOIN r.assays a WHERE r.id = ? AND NOT EXISTS( FROM a.study.auth auth WHERE auth.canWrite = true AND auth.user.id = ?)", [runId, userId ])
     142                return queryNonWritableAssays ? queryNonWritableAssays[ 0 ] > 0 : false;
     143        }
     144       
     145        public int numAssays() {
     146                def num = Run.executeQuery( "SELECT COUNT(*) FROM Assay a WHERE EXISTS( FROM a.runs r WHERE r = ? )", [ this ] );
     147               
     148                if( num )
     149                        return num[ 0 ];
     150        }
     151       
     152        public int numAssaySamples() {
     153                def num = Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.run = ?", [ this ] );
     154               
     155                if( num )
     156                        return num[ 0 ];
     157        }
     158       
     159        public int numReadableAssays( User u ) {
     160                def num = Run.executeQuery( "SELECT COUNT(*) FROM Assay a WHERE EXISTS( FROM a.runs r WHERE r = ? ) AND EXISTS( FROM a.study.auth a WHERE a.user = ? AND a.canRead = true )", [ this, u ] );
     161               
     162                if( num )
     163                        return num[ 0 ];
     164        }
     165       
     166        public int numReadableAssaySamples( User u ) {
     167                def num = Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.run = ? AND EXISTS( FROM s.assay.study.auth a WHERE a.user = ? AND a.canRead = true )", [ this, u ] );
     168               
     169                if( num )
     170                        return num[ 0 ];
     171        }
     172
     173        public int numWritableAssays( User u ) {
     174                def num = Run.executeQuery( "SELECT COUNT(*) FROM Assay a WHERE EXISTS( FROM a.runs r WHERE r = ? ) AND EXISTS( FROM a.study.auth a WHERE a.user = ? AND a.canWrite = true )", [ this, u ] );
     175               
     176                if( num )
     177                        return num[ 0 ];
     178        }
     179
     180        public int numWritableAssaySamples( User u ) {
     181                def num = Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.run = ? AND EXISTS( FROM s.assay.study.auth a WHERE a.user = ? AND a.canWrite = true )", [ this, u ] );
     182               
     183                if( num )
     184                        return num[ 0 ];
     185        }
     186
    139187}
  • trunk/grails-app/domain/nl/tno/massSequencing/Sequence.groovy

    r58 r59  
    1111       
    1212        // Which sample contains this sequence
    13         AssaySample assaySample
     13        SequenceData sequenceData
    1414       
    1515        // Classification of this sequence
  • trunk/grails-app/domain/nl/tno/massSequencing/SequenceData.groovy

    r49 r59  
    5656                        fileService.delete( qualityFile, permanentDir )
    5757               
     58                       
     59                // Remove all sequence objects referencing this sequenceData object
     60                Sequence.executeUpdate( "DELETE FROM Sequence s WHERE s.sequenceData = ?", [this])
     61
    5862                // Reset statistics of the assay sample, to ensure the deleted files are removed from statistics
    5963                sample?.resetStats();
  • trunk/grails-app/domain/nl/tno/massSequencing/classification/Classification.groovy

    r58 r59  
    66        AssaySample     assaySample
    77        Taxon                   taxon
    8         int                             occurrences
     8       
     9        // Number of occurrences of this specific taxon (without more specific classification)
     10        long                    unclassified
     11       
     12        // Number of total occurrences of this taxon (including sequences classified at a more specific level)
     13        long                    total
     14       
     15        // Number of occurrences of this taxon with more specific classification
     16        public getClassified() {
     17                return total - unclassified;
     18        }
    919       
    1020    static constraints = {
     
    1626                assaySample index: 'AssaySample_Idx'
    1727                taxon index:'Search_Idx'
    18                 occurrences index:'Search_Idx'
     28                total index:'Search_Idx'
    1929                version false
    2030        }
  • trunk/grails-app/domain/nl/tno/massSequencing/classification/Taxon.groovy

    r58 r59  
    166166         */
    167167        public static Taxon findTaxonByPath( def path, int startLevel = 0 ) {
     168                if( path.size() == 0 )
     169                        return null;
     170                       
    168171                def leafLevel = path.size() - 1 + startLevel;
    169172                def leafName = path[ -1 ];
  • trunk/grails-app/services/nl/tno/massSequencing/ClassificationService.groovy

    r58 r59  
    88import org.codehaus.groovy.grails.commons.ConfigurationHolder
    99import org.hibernate.StatelessSession
     10import org.hibernate.Transaction
    1011import java.util.zip.*
    1112import nl.tno.massSequencing.classification.*
     
    8586                file.eachLine { line ->
    8687                        numLines++;
    87                        
     88
    8889                        // Update progress every time 100kB is processed
    8990                        bytesProcessed += line.size();
     
    101102                return [ numLines: numLines, fileSize: file.size() ];
    102103        }
    103        
    104         /**
    105          * Saves the classification of sequences into the database.
    106          * @param inputFiles    List of maps with groups- and taxonomy files.
    107          * @return                              List of processed files
    108          */
    109         public List storeClassification( List inputFiles, Map excelMatches, Closure onProgress ) {
    110                 def filesStored = [];
    111                
    112                 // In order to store classification, we need a .groups and a .taxonomy file with an
    113                 // equal number of lines
    114                 inputFiles.each { inputFile ->
    115                         // Check whether this file has already been processed as a matching file for another file
    116                         if( filesStored*.filename.contains( inputFile.filename ) )
    117                                 return;
    118                                
    119                         // Search for a matching file
    120                         def searchForType = inputFile.type == 'groups' ? 'taxonomy' : 'groups';
    121                         def matchingFile = findMatchingClassificationFile( inputFiles, searchForType, inputFile.numLines, filesStored*.filename );
    122                        
    123                         // If no matching file is found, give a message to the user
    124                         if( !matchingFile ) {
    125                                 inputFile[ 'success' ] = false
    126                                 inputFile[ 'message' ] = "No matching " + searchForType + " file could be found for this file (or a matching file is already processed)."
    127                                
    128                                 filesStored << inputFile;
    129                                 return;
    130                         }
    131                        
    132                         println "Handling files: " + inputFile.filename + " - " + matchingFile.filename
    133                         println excelMatches;
    134                        
    135                         // Now parse the two matching files
    136                         def classificationInfo = []
    137                         if( inputFile.type == 'groups' ) {
    138                                 classificationInfo = storeClassificationFromFiles( inputFile.filename, matchingFile.filename, excelMatches, onProgress );
    139                         } else {
    140                                 classificationInfo = storeClassificationFromFiles( matchingFile.filename, inputFile.filename, excelMatches, onProgress );
    141                         }
    142                         filesStored += classificationInfo;
    143                 }
    144                
    145                 return filesStored;
    146         }
    147        
     104
     105        /**
     106         * Removes all classification sequence objects for a given SequenceData object
     107         * @param sequenceData 
     108         * @return      True if the deletion has succeeded
     109         */
     110        public boolean removeClassificationForSequenceData( SequenceData sequenceData ) {
     111                if( sequenceData ) {
     112                        Sequence.executeUpdate( "DELETE FROM Sequence s WHERE s.sequenceData = ?", [sequenceData] );
     113                        return true;
     114                } else {
     115                        return false;
     116                }
     117        }
     118
    148119        /**
    149120         * Store the classification from the given files
     
    152123         * @return
    153124         */
    154         protected List storeClassificationFromFiles( String groupsFilename, String taxonomyFilename, Map excelMatches, Closure onProgress ) {
    155                 def returnList = []
     125        public Map storeClassification( String taxonomyFilename, SequenceData sequenceData, Closure onProgress ) {
     126                def hibernateSession = sessionFactory.getCurrentSession();
     127                hibernateSession.flush();
     128                hibernateSession.clear();
    156129               
    157                 File groupsFile = fileService.get( groupsFilename );
    158130                File taxonomyFile = fileService.get( taxonomyFilename );
     131
     132                if( !taxonomyFile ) {
     133                        return [ success: false, type: 'taxonomy', filename: groupsFilename, message: 'Taxonomy file doesn\'t exist.' ]
     134                }
    159135               
    160                 if( !groupsFile ) {
    161                         returnList << [ success: false, type: 'groups', filename: groupsFilename, message: 'Groups file doesn\'t exist.' ]
    162                 }
    163                 if( !taxonomyFile ) {
    164                         returnList << [ success: false, type: 'taxonomy', filename: groupsFilename, message: 'Taxonomy file doesn\'t exist.' ]
    165                 }
    166                
    167                 if( !groupsFile || !taxonomyFile )
    168                         return returnList;
    169                        
    170136                // Open files for reading and create temporary variables
    171137                def taxonomyReader = taxonomyFile.newReader();
    172                 def groupsReader = groupsFile.newReader();
    173                        
     138
    174139                def parts, sequenceName, classification, sampleName, taxa, taxon, line, groupsLine
    175                
    176                 def classificationScoreRegex = /(\(\d+\))/
     140
     141                def taxonRegex = /"?([^"(]+)"?(\(\d+\))?/
    177142                def startLevel = 0    // The first level that is present in the file. Sometimes, the root element (level 0) is not mentioned in the file, in that case, this values should be set to 1
    178                
     143                def unclassified = "unclassified"
     144
    179145                def i = 0;
    180146                def start = System.currentTimeMillis();
    181147                def lapTime = start;
    182                
     148
    183149                // Create a stateless session in order to speed up inserts
    184150                StatelessSession statelessSession = sessionFactory.openStatelessSession();
    185                 statelessSession.beginTransaction();
    186                
    187                 println "Starting at " + start
    188                        
    189                 // For each line in the taxonomy file, also read the groups file. If the sequenceNames don't match,
    190                 // discard the sequence
     151
     152                // For each line in the taxonomy file, read the sequence and save it
    191153                def discardedSequences = 0;
    192154                def importedSequences = 0;
    193155                long bytesProcessed = 0;
    194                 while( ( line = taxonomyReader.readLine() ) ) {
    195                         // Also read a line from the groupsFile
    196                         groupsLine = groupsReader.readLine();
    197                        
    198                         bytesProcessed += line.size() + groupsLine.size();
    199                        
    200                         // Find the taxon for this line
    201                         parts = line.tokenize( "\t" );
    202                        
    203                         if( parts.size() != 2 ) {
    204                                 // Skip this line because it is incorrect
    205                                 continue;
     156               
     157                try {
     158                        while( ( line = taxonomyReader.readLine() ) ) {
     159                                bytesProcessed += line.size();
     160       
     161                                // Find the taxon for this line
     162                                parts = line.tokenize( "\t" );
     163       
     164                                if( parts.size() != 2 ) {
     165                                        // Skip this line because it is incorrect
     166                                        continue;
     167                                }
     168       
     169                                sequenceName = parts[ 0 ];
     170                                classification = parts[ 1 ];
     171       
     172                                // Split classification to check whether the taxon already exists
     173                                taxa = classification.tokenize( ';' ).collect { it.replaceAll( taxonRegex, '$1' ) }.findAll { it && it != unclassified }
     174                               
     175                                // Find taxon or create one if needed
     176                                taxon = Taxon.findOrCreateTaxonByPath( taxa, startLevel );
     177       
     178                                if( taxon ) {
     179                                        // Create a new sequence record
     180                                        def s = new Sequence()
     181                                        s.name = sequenceName
     182                                        s.classification = taxon
     183                                        s.sequenceData = sequenceData
     184       
     185                                        statelessSession.insert(s);
     186                                } else {
     187                                        log.trace( "" + i + " Sequence " + sequenceName + " not imported because it is unclassified." )
     188                                }
     189       
     190                                if( i % 80 == 0 ) {
     191                                        onProgress( bytesProcessed );
     192                                        bytesProcessed = 0;
     193                                }
     194       
     195                                importedSequences++;
     196       
     197                                i++;
    206198                        }
    207199                       
    208                         sequenceName = parts[ 0 ];
    209                         classification = parts[ 1 ];
     200                } catch( Exception e ) {
     201                        throw e;
     202                } finally {
     203                        statelessSession.close();
     204                        taxonomyReader.close();
     205                }
     206
     207                onProgress( bytesProcessed );
     208
     209                return [ filename: taxonomyFilename, type: 'taxonomy', success: true, importedSequences: importedSequences, discardedSequences: discardedSequences ]
     210        }
     211
     212        /**
     213         * Updates the Classification table with data computed from the raw sequences for the given assay sample
     214         * @param assaySample
     215         */
     216        public void updateClassificationForAssaySample( AssaySample assaySample ) {
     217                if( !assaySample )
     218                        return;
    210219                       
    211                         if( !groupsLine ) {
    212                                 // Skip this line because the groups file doesn't contain a new line
    213                                 discardedSequences++;
    214                                 continue
    215                         }
    216 
    217                         // Find the sequence name and sample name in the groups file
    218                         parts = groupsLine.tokenize( "\t" );
    219                         if( parts.size() != 2 ) {
    220                                 // Skip this line because the groups file line is incorrect
    221                                 discardedSequences++;
    222                                 continue;
    223                         }
     220                updateClassificationForAssaySamples( [ assaySample ] );
     221        }
     222
     223        /**
     224         * Updates the Classification table with data computed from the raw sequences for the given assay sample
     225         * @param assaySample
     226         */
     227        public void updateClassificationForAssaySamples( List assaySamples ) {
     228                if( !assaySamples )
     229                        return;
    224230                       
    225                         if( parts[ 0 ] != sequenceName ) {
    226                                 log.trace( "Sequence names in taxonomy and groups files don't match: " + sequenceName + " " + parts[ 0 ] )
    227                                 discardedSequences++;
    228                                 continue;
    229                         }
    230                         sampleName = parts[ 1 ];
    231                        
    232                         // Split classification to check whether the taxon already exists
    233                         taxa = classification.replaceAll( classificationScoreRegex, '' ).tokenize( ';' ).findAll { it }
    234                        
    235                         // Find taxon or create one if needed
    236                         taxon = Taxon.findOrCreateTaxonByPath( taxa, startLevel );
    237                        
    238                         // Determine assaySample
    239                         def assaySample = excelMatches[ sampleName ]
    240                        
    241                         if( !assaySample ) {
    242                                 log.trace( "Sample name from GROUPS file is not found in excel file: " + sampleName )
    243                                 discardedSequences++;
    244                                 continue;
    245                         }
    246                        
    247                         // Create a new sequence record
    248                         def s = new Sequence()
    249                         s.name = sequenceName
    250                         s.classification = taxon
    251                         s.assaySample = assaySample
    252                        
    253                         statelessSession.insert(s);
    254                        
    255                         //println "Sequence " + sequenceName + " / classification " + classification + " / sample: " + assaySample
    256 
    257                         if( i % 100 == 0 ) {
    258                                 onProgress( bytesProcessed );
    259                                 bytesProcessed = 0;
    260                         }
    261                        
    262                         importedSequences++;
    263                        
    264                         i++;
    265                 }
    266                
    267                 onProgress( bytesProcessed );
    268                
    269                 taxonomyReader.close();
    270                 groupsReader.close();
    271                
    272                 returnList << [ filename: taxonomyFilename, type: 'taxonomy', success: true, importedSequences: importedSequences, discardedSequences: discardedSequences ]
    273                 returnList << [ filename: groupsFilename, type: 'groups', success: true, importedSequences: importedSequences, discardedSequences: discardedSequences ]
    274                
    275                 return returnList;
    276         }
    277        
     231                // Clear hibernate session for otherwise the session will be out of sync with the
     232                // database
     233                def hibernateSession = sessionFactory.getCurrentSession();
     234                hibernateSession.flush();
     235                hibernateSession.clear();
     236
     237                // Remove all Classifications for the given assaySample
     238                Classification.executeUpdate( "DELETE FROM Classification WHERE assaySample IN (:assaySamples)", [ 'assaySamples': assaySamples ] );
     239
     240                // Update classification table with data from sequences
     241
     242                // Unfortunately, Hibernate (through HQL) can't handle bulk insertion if a table has a
     243                // composite id (Classification). See http://opensource.atlassian.com/projects/hibernate/browse/HHH-3434
     244                // For that reason we execute plain SQL
     245                def connection = sessionFactory.currentSession.connection()
     246                def statement = connection.createStatement();
     247                statement.executeUpdate( connection.nativeSQL( """
     248                        INSERT INTO     classification (assay_sample_id, taxon_id, unclassified, total)
     249                        SELECT          sa.id, s.classification_id, count( * ),
     250                                                (
     251                                                        SELECT          count( * )
     252                                                        FROM            sequence s2
     253                                                        LEFT JOIN       taxonomy t2 ON s2.classification_id = t2.id
     254                                                        LEFT JOIN       sequence_data sd2 ON s2.sequence_data_id = sd2.id
     255                                                        LEFT JOIN       assay_sample sa2 ON sd2.sample_id = sa2.id
     256                                                        WHERE           sa2.id = sa.id AND t2.lft >= t.lft AND t2.rgt <= t.rgt
     257                                                )
     258                        FROM            sequence s
     259                        LEFT JOIN       taxonomy t ON s.classification_id = t.id
     260                        LEFT JOIN       sequence_data sd ON s.sequence_data_id = sd.id
     261                        LEFT JOIN       assay_sample sa ON sd.sample_id = sa.id
     262                        WHERE           sa.id IN (""" + assaySamples.id.join( ", " ) + """ )
     263                        GROUP BY        sa.id, s.classification_id
     264           """ ) );
     265        }
     266
     267
    278268        /**
    279269         * Find a file that matches the parameters and is not already stored
  • trunk/grails-app/services/nl/tno/massSequencing/FastaService.groovy

    r58 r59  
    1313        def fuzzySearchService
    1414        def sampleExcelService
     15        def classificationService
    1516        def excelService
    1617
     
    242243           def fastas = parsedFiles.findAll { it.type == "fasta" }
    243244           def quals = parsedFiles.findAll { it.type == "qual" }
     245           def classifications = parsedFiles.findAll { it.type == "taxonomy" }
    244246           def excels = parsedFiles.findAll { it.type == "excel" }
    245247           
     
    263265                   // Determine feasible quals (based on number of sequences )
    264266                   def feasibleQuals = quals.findAll { it.numSequences == fastaFile.numSequences }
     267                   def feasibleClassifications = classifications.findAll { it.numLines == fastaFile.numSequences }
    265268
    266269                   // Best matching qual file
     
    271274                           qual = feasibleQuals[ qualIdx ];
    272275
     276                   // Best matching classification file
     277                   def classificationIdx = fuzzySearchService.mostSimilarWithIndex( matchWith + '.taxonomy', feasibleClassifications.originalfilename );
     278                   
     279                   def classification = null
     280                   if( classificationIdx != null )
     281                           classification = feasibleClassifications[ classificationIdx ];
     282                           
    273283                   // Best matching sample
    274284                   def assaySample = null
     
    298308                                           fasta: fastaFile,
    299309                                           feasibleQuals: feasibleQuals,
     310                                           feasibleClassifications: feasibleClassifications,
    300311                                           qual: qual,
     312                                           classification: classification,
    301313                                           assaySample: assaySample,
    302314                                           checked: checked
     
    427439                if( fileService.fileExists( fastaFile ) ) {
    428440                        // Lookup the original filename
    429                         def fastaData = processedFiles.parsed.success.find { it.filename == fastaFile };
     441                        def fastaData = processedFiles?.parsed?.success?.find { it.filename == fastaFile };
    430442                        if( fastaData ) {
    431443                                returnStructure.fasta = fileService.moveFileToUploadDir( fileService.get( fastaFile ), fastaData.originalfilename, permanentDirectory );
     
    444456
    445457                        if( qualData ) {
    446                                 returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile ), qualData.originalfilename, permanentDirectory );
     458                                returnStructure.qual = fileService.moveFileToUploadDir( fileService.get(qualFile), qualData.originalfilename, permanentDirectory );
    447459                                returnStructure.avgQuality = qualData.avgQuality
    448460                        } else {
     
    689701                }
    690702               
     703                // Update classification summary
     704                classificationService.updateClassificationForAssaySamples( assaySamples );
     705               
    691706                return numFiles;
    692707        }
  • trunk/grails-app/views/assay/_addFilesDialog.gsp

    r58 r59  
    22        <h2>Upload sequence files</h2>
    33       
    4         <g:form name="addFiles" controller="import" action="showProcessScreen" id="${assay.id}">
     4        <g:form name="addFiles" controller="import" action="parseUploadedFiles" id="${assay.id}">
    55                <input type="hidden" name="entityType" value="assay" />
    66                <p>
  • trunk/grails-app/views/assay/index.gsp

    r52 r59  
    1414                        <form id="assayForm">
    1515                        </form>
    16                         <table id="assays" class="paginate">
     16                        <table id="assays" class="paginate_serverside" rel="<g:createLink controller="assay" action="showAssayList" />">
    1717                                <thead>
    1818                                        <tr>
     
    2727                                </thead>
    2828                                <tbody>
    29                                         <g:each in="${studies}" var="study">
    30                                                 <g:if test="${study.assays != null && study.assays.size() > 0}">
    31                                                         <g:each in="${study.assays}" var="assay">
    32                                                                 <tr>
    33                                                                         <td><g:checkBox name="ids" value="${assay.id}" checked="${false}" onClick="updateCheckAll(this);" /></td>
    34                                                                
    35                                                                         <td><g:link title="View assay" controller="assay" action="show" id="${assay.id}">${assay.name}</g:link></td>
    36                                                                         <td><a href="${study.viewUrl()}">${study.name}</a></td>
    37                                                                         <td>${assay.assaySamples?.size()}</td>
    38                                                                         <td>
    39                                                                                 <g:if test="${assay.assaySamples?.size()}">
    40                                                                                         <g:formatNumber number="${assay.numSequences() / assay.assaySamples?.size()}" format="###,###,##0"/>
    41                                                                                 </g:if>
    42                                                                                 <g:else>
    43                                                                                         -
    44                                                                                 </g:else>
    45                                                                         </td>
    46                                                                         <td><g:link title="View assay" controller="assay" action="show" id="${assay.id}"><img src="${fam.icon( name: 'application_form_magnify' )}" alt="View assay" title="View assay" /></g:link></td>
    47                                                                         <td>
    48                                                                                 <g:if test="${assay.numSequences() > 0}">
    49                                                                                         <g:link title="Sequence length histogram" controller="assay" action="sequenceLengthHistogram" id="${assay.id}"><img src="${fam.icon( name: 'chart_bar' )}" alt="Sequence length histogram" title="Sequence length histogram" /></g:link>
    50                                                                                 </g:if>
    51                                                                                 <g:else>
    52                                                                                         <img src="${fam.icon( name: 'chart_bar' )}" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />
    53                                                                                 </g:else>
    54                                                                         </td>                                                   
    55                                                                        
    56                                                                 </tr>
    57                                                         </g:each>
    58                                                 </g:if>
    59                                         </g:each>
     29                                        <tr>
     30                                                <td colspan="7">
     31                                                        Retrieving data from server.
     32                                                </td>
     33                                        </tr>
    6034                                </tbody>
    6135                        </table>
  • trunk/grails-app/views/assay/show.gsp

    r57 r59  
    6666        <g:else>
    6767                <form id="sampleForm"><input type="hidden" name="assayId" value="${assay.id}" /></form>
    68                 <table class="paginate" id="samples">
     68                <table class="paginate_serverside" rel="<g:createLink controller="assay" action="showSampleData" id="${assay.id}" />" id="samples">
    6969                        <thead>
    7070                                <tr>
     
    8080                        </thead>                       
    8181                        <tbody>
    82                                 <% def assaySamples = assay.assaySamples.toList().sort { it.sample.name } %>
    83                                 <g:each in="${assaySamples}" var="assaySample">
    84                                         <tr>
    85                                                 <td><g:checkBox name="ids" value="${assaySample.id}" checked="${false}" onClick="updateCheckAll(this);" /></td>
    86                                                 <td><a title="Show assay" href="#" onClick="showSample(${assaySample.id}, 'assay'); return false;">${assaySample.sample.name}</a></td>
    87                                                 <td>${assaySample.run?.name}</td>
    88                                                 <td>${assaySample.fwMidName}</td>
    89                                                 <td>
    90                                                         <g:if test="${assaySample.numSequenceFiles() > 0}">
    91                                                                 <g:formatNumber number="${assaySample.numSequences()}" format="###,###,##0" />
    92                                                         </g:if>
    93                                                         <g:else>
    94                                                                 -
    95                                                         </g:else>
    96                                                 </td>
    97                                                 <td>
    98                                                         <g:if test="${assaySample.numQualityFiles() > 0}">
    99                                                                 <g:formatNumber number="${assaySample.numQualScores()}" format="###,###,##0" />
    100                                                         </g:if>
    101                                                         <g:else>
    102                                                                 -
    103                                                         </g:else>
    104                                                 </td>
    105                                                 <td class="button">
    106                                                         <g:if test="${!assaySample.assay.study.canWrite(session.user)}">
    107                                                                 <img src="${fam.icon(name: 'pencil')}" class="disabled" title="You can't edit this sample because you don't have sufficient privileges." />
    108                                                         </g:if>
    109                                                         <g:else>
    110                                                                 <a onClick="showEditSampleDialog(${assaySample.id}, 'assay', ${assay.id}); return false;" href="#" title="Edit sample data"><img title="Edit sample data" src="${fam.icon(name: 'pencil')}" /></a>
    111                                                         </g:else>
    112                                                 </td>
    113                                                 <td class="button">
    114                                                         <g:if test="${assaySample.numSequences() > 0}">
    115                                                                 <g:link controller="assaySample" action="sequenceLengthHistogram" id="${assaySample.id}" title="Sequence length histogram"><img src="${fam.icon( name: 'chart_bar' )}" alt="Sequence length histogram" title="Sequence length histogram" /></g:link>
    116                                                         </g:if>
    117                                                         <g:else>
    118                                                                 <img src="${fam.icon( name: 'chart_bar' )}" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />
    119                                                         </g:else>
    120                                                 </td>
    121                                         </tr>
    122                                 </g:each>
     82                                <tr>
     83                                        <td colspan="8">
     84                                                Loading data from server
     85                                        </td>
     86                                </tr>
     87
    12388                        </tbody>
    12489                </table>
  • trunk/grails-app/views/assaySample/sequenceLengthHistogram.gsp

    r52 r59  
    22        <head>
    33                <meta name="layout" content="main" />
    4                 <title>Sequence length histogram for sample ${assaySample.sample.name} | Mass Sequencing | dbNP</title>
     4                <title>Sequence length histogram for ${title} | Mass Sequencing | dbNP</title>
    55        </head>
    66        <body>
    7                 <h1>Sequence length histogram for sample ${assaySample.sample.name} (${assaySample.assay.study.name} / ${assaySample.assay.name})</h1>
     7                <h1>Sequence length histogram for ${title}</h1>
    88                <p>
    9                         Total number of sequences: <g:formatNumber number="${assaySample.numSequences()}" format="###,###,##0" />
     9                        Total number of sequences: <g:formatNumber number="${numSequences}" format="###,###,##0" />
    1010                </p>
    1111                <div id="placeholder" style="width:600px;height:300px"></div>
  • trunk/grails-app/views/import/parseUploadResult.gsp

    r58 r59  
    2727       
    2828        <h2>Sequence files</h2>
    29         <g:if test="${matchedFiles.size() > 0}">
    30                 <p>
    31                         Match each sequencefile with the qualityfile and the sample it belongs to. Samples are already chosen for some files based
    32                         on the given excelsheet or the filenames.
    33                 </p>
    34                 <p>
    35                         By unchecking a sequence file you can exclude the file from being used.
    36                 </p>
    37                  
    38                 <%
    39                         def sortedSamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) }.sort() { a,b -> a.sample.name <=> b.sample.name }
    40                 %>
    41                 <g:form name="saveProcessedFiles" action="saveProcessedFiles" id="${entity.id}" params="${[controller: controller]}">
    42                 <input type="hidden" name="entityType" value="${entityType}" />
    43                 <table>
    44                         <thead>
    45                                 <tr>
    46                                         <th></th>
    47                                         <th>Fasta file</th>
    48                                         <th># sequences</th>
    49                                         <th>Qual file</th>
    50                                         <th>Sample</th>
    51                                 </tr>
    52                         </thead>
     29        <g:form name="saveProcessedFiles" action="saveMatchedFiles" id="${entity.id}" params="${[controller: controller]}">
     30        <input type="hidden" name="entityType" value="${entityType}" />
     31        <input type="hidden" name="processId" value="${processId}" />
     32       
     33                <g:if test="${matchedFiles.size() > 0}">
     34                        <p>
     35                                Match each sequencefile with the qualityfile, classification file and the sample it belongs to. Samples are already chosen for some files based
     36                                on the given excelsheet or the filenames.
     37                        </p>
     38                        <p>
     39                                By unchecking a sequence file you can exclude the file from being used.
     40                        </p>
     41                         
     42                        <%
     43                                def sortedSamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) }.sort() { a,b -> a.sample.name <=> b.sample.name }
     44                        %>
     45                        <table class="importProcessedFiles" cellspacing="0">
     46                                <thead>
     47                                        <tr>
     48                                                <th></th>
     49                                                <th>Fasta file</th>
     50                                                <th># sequences</th>
     51                                                <th>Sample</th>
     52                                        </tr>
     53                                </thead>
     54                               
     55                                <g:each in="${matchedFiles}" var="file" status="i">
     56                                        <tr class="${ i % 2 == 0 ? 'even' : 'odd' }">
     57                                                <td>
     58                                                        <g:hiddenField name="file.${i}.fasta" value="${file.fasta.filename}" />
     59                                                        <g:checkBox name="file.${i}.include" value="${file.checked}" />
     60                                                </td>
     61                                                <td class="name" valign="center">${file.fasta.originalfilename}</td>
     62                                                <td class="sequences">${file.fasta.numSequences}</td>
     63                                                <td nowrap>
     64                                                        <g:if test="${sortedSamples.size()}">
     65                                                                <select name="file.${i}.assaySample" onChange="showSampleDataWarning(this, ${i});">
     66                                                                        <g:each in="${sortedSamples}" var="assaySample">
     67                                                                                <option
     68                                                                                        <g:if test="${file.assaySample?.id == assaySample.id}">
     69                                                                                                <g:set var="selectedAssaySample" value="${assaySample}" />
     70                                                                                                selected="selected"
     71                                                                                        </g:if>
     72                                                                                        value="${assaySample.id}" class="<g:if test="${assaySample?.sequenceData?.size() > 0}">containsdata</g:if><g:else>nodata</g:else>">${assaySample.sample.name}</option>
     73                                                                        </g:each>
     74                                                                </select>
     75                                                                <span id="warningSampleContainsData_${i}" class="warningSampleContainsData" <g:if test="${selectedAssaySample?.sequenceData?.size() > 0}">style="display: inline;"</g:if>>
     76                                                                        <img src="${fam.icon( name: "exclamation" ) }" title="This sample already contains sequences. Adding sequences doesn't replace the old ones." />
     77                                                                </span>
     78                                                        </g:if>
     79                                                        <g:else>
     80                                                                No samples available.
     81                                                        </g:else>
     82                                                </td>
     83                                        </tr>
     84                                       
     85                                        <tr class="${ i % 2 == 0 ? 'even' : 'odd' }">
     86                                                <td></td>
     87                                                <td></td>
     88                                                <td>Quality file</td>
     89                                                <td>
     90                                                        <g:if test="${file.feasibleQuals?.size()}">
     91                                                                <g:select name="file.${i}.qual" from="${file.feasibleQuals}" optionKey="filename" optionValue="originalfilename" value="${file.qual?.filename}" noSelection="['': '- No qual file -']"/>
     92                                                        </g:if>
     93                                                        <g:else>
     94                                                                <select name="" disabled="disabled"><option value="">No qual file available</option></select>
     95                                                        </g:else>
     96                                                </td>
     97                                        </tr>
     98                                        <tr class="${ i % 2 == 0 ? 'even' : 'odd' }">
     99                                                <td></td>
     100                                                <td></td>
     101                                                <td>Classification file</td>
     102                                                <td>
     103                                                        <g:if test="${file.feasibleClassifications?.size()}">
     104                                                                <g:select name="file.${i}.classification" from="${file.feasibleClassifications}" optionKey="filename" optionValue="originalfilename" value="${file.classification?.filename}" noSelection="['': '- No classification -']"/>
     105                                                        </g:if>
     106                                                        <g:else>
     107                                                                <select name="" disabled="disabled"><option value="">No classification available</option></select>
     108                                                        </g:else>
     109                                                </td>
     110                                        </tr>
     111                                </g:each>
    53112                       
    54                         <g:each in="${matchedFiles}" var="file" status="i">
    55                                 <tr>
    56                                         <td>
    57                                                 <g:hiddenField name="file.${i}.fasta" value="${file.fasta.filename}" />
    58                                                 <g:checkBox name="file.${i}.include" value="${file.checked}" />
    59                                         </td>
    60                                         <td class="name">${file.fasta.originalfilename}</td>
    61                                         <td class="sequences">${file.fasta.numSequences}</td>
    62                                         <td>
    63                                                 <g:if test="${file.feasibleQuals?.size()}">
    64                                                         <g:select name="file.${i}.qual" from="${file.feasibleQuals}" optionKey="filename" optionValue="originalfilename" value="${file.qual?.filename}" noSelection="['': '- No qual file -']"/>
    65                                                 </g:if>
    66                                                 <g:else>
    67                                                         No qual file available
    68                                                 </g:else>
    69                                         </td>
    70                                         <td nowrap>
    71                                                 <g:if test="${sortedSamples.size()}">
    72                                                         <select name="file.${i}.assaySample" onChange="showSampleDataWarning(this, ${i});">
    73                                                                 <g:each in="${sortedSamples}" var="assaySample">
    74                                                                         <option
    75                                                                                 <g:if test="${file.assaySample?.id == assaySample.id}">
    76                                                                                         <g:set var="selectedAssaySample" value="${assaySample}" />
    77                                                                                         selected="selected"
    78                                                                                 </g:if>
    79                                                                                 value="${assaySample.id}" class="<g:if test="${assaySample?.sequenceData?.size() > 0}">containsdata</g:if><g:else>nodata</g:else>">${assaySample.sample.name}</option>
    80                                                                 </g:each>
    81                                                         </select>
    82                                                         <span id="warningSampleContainsData_${i}" class="warningSampleContainsData" <g:if test="${selectedAssaySample?.sequenceData?.size() > 0}">style="display: inline;"</g:if>>
    83                                                                 <img src="${fam.icon( name: "exclamation" ) }" title="This sample already contains sequences. Adding sequences doesn't replace the old ones." />
    84                                                         </span>
    85                                                 </g:if>
    86                                                 <g:else>
    87                                                         No samples available.
    88                                                 </g:else>
    89                                         </td>
    90                                 </tr>
    91                         </g:each>
     113                        </table>
    92114               
    93                 </table>
     115                </g:if>
     116                <g:else>
     117                        <p>
     118                                ${parsedFiles.success?.size()} files were successfully uploaded, but no FASTA file could be determined. Please
     119                                upload at least one FASTA file if sequences should be added.
     120                        </p>
     121                </g:else>
    94122               
    95                 <input type="submit" value="Continue">
    96                 </g:form>
    97        
    98         </g:if>
    99         <g:else>
    100                 <p>
    101                         ${parsedFiles.success?.size()} files were successfully uploaded, but no FASTA file could be determined. Please
    102                         upload at least one FASTA file if sequences should be added.
    103                 </p>
    104         </g:else>
    105        
    106         <h2>Classification files</h2>
    107         <g:if test="${classificationFiles.size() > 0}">
    108                 <p>
    109                         The following files containing sequence classification have been imported.
    110                 </p>
    111                 <table>
    112                         <thead>
    113                                 <tr>
    114                                         <th>File</th>
    115                                         <th># sequences imported</th>
    116                                         <th># sequences discarded</th>
    117                                 </tr>
    118                         </thead>
    119                         <g:each in="${classificationFiles}" var="file" status="i">
    120                                 <tr>
    121                                         <td>${file.filename}</td>
    122                                         <td>${file.importedSequences}</td>
    123                                         <td>${file.discardedSequences}</td>
    124                                 </tr>
    125                         </g:each>
    126                 </table>
    127         </g:if>
    128         <g:else>
    129                 <p>
    130                         No classification files found in uploaded data.
    131                 </p>
    132         </g:else>
    133        
    134        
     123                <g:if test="${remainingClassificationFiles}">
     124                        <h2>Clasification files</h2>
     125                        <p>
     126                                The following classification files could not be matched with an uploaded sequence file. Please choose which file
     127                                belongs to which sample and data file.
     128                        </p>
     129                        <p>
     130                                <b>N.B.</b> Existing classification data wil be removed for the selected file.
     131                        </p>
     132                        <table class="importProcessedFiles">
     133                                <thead>
     134                                        <tr>
     135                                                <th></th>
     136                                                <th>Classification file</th>
     137                                                <th># sequences</th>
     138                                                <th>Matching existing file</th>
     139                                        </tr>
     140                                </thead>
     141                               
     142                                <g:each in="${remainingClassificationFiles}" var="file" status="i">
     143                                        <tr class="${ i % 2 == 0 ? 'even' : 'odd' }">
     144                                                <td>
     145                                                        <g:hiddenField name="remainingClassification.${i}.filename" value="${file.filename}" />
     146                                                        <g:checkBox name="remainingClassification.${i}.include" value="${file.feasibleSequenceData as Boolean}" disabled="${!file.feasibleSequenceData}" />
     147                                                </td>
     148                                                <td class="name" valign="center">${file.originalfilename}</td>
     149                                                <td class="sequences">${file.numLines}</td>
     150                                                <td nowrap>
     151                                                        <g:if test="${file.feasibleSequenceData}">
     152                                                                <select name="remainingClassification.${i}.sequenceData">
     153                                                                        <g:each in="${file.feasibleSequenceData}" var="sequenceData">
     154                                                                                <option
     155                                                                                        <g:if test="${file.sequenceData?.id == sequenceData.id}">
     156                                                                                                selected="selected"
     157                                                                                        </g:if>
     158                                                                                        value="${sequenceData.id}">
     159                                                                                        ${sequenceData.sample.sample.name} - ${sequenceData.sequenceFile}</option>
     160                                                                        </g:each>
     161                                                                </select>
     162                                                        </g:if>
     163                                                        <g:else>
     164                                                                No samples available for this classification file
     165                                                        </g:else>
     166                                                </td>
     167                                        </tr>
     168                                </g:each>
     169                       
     170                        </table>
     171                </g:if>
     172
     173        <input type="submit" value="Continue">
     174        </g:form>
     175
    135176       
    136177        <g:if test="${parsedFiles.failure?.size()}">
     
    151192       
    152193        <p>
    153                 <g:link controller="import" action="returnWithoutSaving" params="[entityType: entityType, id: entity.id]">Return to ${entityType}</g:link>     
     194                <g:link controller="import" action="returnWithoutSaving" params="[processId: processId, entityType: entityType, id: entity.id]">Return to ${entityType}</g:link>       
    154195        </p>
    155196</body>
  • trunk/grails-app/views/import/showProcessScreen.gsp

    r58 r59  
    55
    66        <script type="text/javascript">
     7                var progressInterval;
     8               
    79                $(function() {
    810                        $( "#wait_dialog" ).dialog({
     
    2325                        });
    2426
    25                         var progressInterval = setInterval(updateProgress, 250);
     27                        progressInterval = setTimeout(updateProgress, 250);
    2628                                                       
    2729                        // Call processing URL
    2830                        $.ajax({
    2931                          type: 'POST',
    30                           url: "<g:createLink controller="import" action="process" id="${entityId}" />",
    31                           data: "entityType=${entityType}",
     32                          url: "${processUrl.encodeAsJavaScript()}",
     33                          data: "${processParameters*.toString().join("&")}",
    3234                          success: function(data) {
    3335                                 
    3436                                  // Stop update progress bar
    35                                   clearInterval( progressInterval );
     37                                  clearTimeout( progressInterval );
    3638                                   
    37                                   window.location.replace( "${url.encodeAsJavaScript()}" );
     39                                  window.location.replace( "${finishUrl.encodeAsJavaScript()}" );
    3840                          },
    3941                          error: function(xhr, textStatus, errorThrown) {
     
    4143                                  // Stop update progress bar (but update it for the last time)
    4244                                  updateProgress()
    43                                   clearInterval( progressInterval );
     45                                  clearTimeout( progressInterval );
    4446
    4547                                  alert( "Error " + xhr.getStatus() + ": " + xhr.getStatusText() );
    46                                   //window.location.replace( "<g:createLink controller="${entityType}" action="show" id="${entityId}" />" );
     48                                  window.location.replace( "${errorUrl?.encodeAsJavaScript()}" );
    4749                        }
    4850                        });
     
    5355                        $.ajax({
    5456                        type: "GET",
    55                                 url: "<g:createLink controller="import" action="getProgress" />",
     57                                url: "${progressUrl?.encodeAsJavaScript()}",
    5658                                dataType: "json",
    5759                                success: function(progress) {
     
    6365
    6466                                        $("#progressbar").progressbar("value", progress.stepProgress / progress.stepTotal * 100 );
     67
     68                                        // Make sure the next progress will be retrieved in 250 ms
     69                                        progressInterval = setTimeout(updateProgress, 250);                                     
    6570                                }
    6671                        });
  • trunk/grails-app/views/layouts/main.gsp

    r52 r59  
    4747                                        <!-- Last full synchronization: ${lastSynchronized} -->
    4848                                </g:if>
     49                                <g:if test="${flash.message}">
     50                                        <p class="message">${flash.message.toString()}</p>
     51                                </g:if>
    4952                                <g:if test="${flash.error}">
    50                                         <p class="error">${flash.error.toString().encodeAsHTML()}</p>
    51                                 </g:if>
    52                                 <g:if test="${flash.message}">
    53                                         <p class="message">${flash.message.toString().encodeAsHTML()}</p>
     53                                        <p class="error">${flash.error.toString()}</p>
    5454                                </g:if>
    5555                               
  • trunk/grails-app/views/run/_addFilesDialog.gsp

    r58 r59  
    22        <h2>Upload sequence files</h2>
    33       
    4         <g:form name="addFiles" controller="import" action="showProcessScreen" id="${run.id}">
     4        <g:form name="addFiles" controller="import" action="parseUploadedFiles" id="${run.id}">
    55                <input type="hidden" name="entityType" value="run" />
    66                <p>
  • trunk/grails-app/views/run/index.gsp

    r52 r59  
    1818        <body>
    1919                <h1>List of mass sequencing runs</h1>
    20                 <g:if test="${runs.size() == 0}">
     20                <g:if test="${numRuns == 0}">
    2121                        <p>
    2222                                Currently there are no runs available in the database. Click <a href="#" onClick="showAddRunDialog(); return false;">here</a> to add a new run.
     
    2626                        <form id="runForm">
    2727                        </form>
    28                         <table id="runs" class="paginate">
     28                        <table id="runs" class="paginate_serverside" rel="<g:createLink controller="run" action="showRunList" />">
    2929                                <thead>
    3030                                        <tr>
     
    3939                                </thead>
    4040                                <tbody>
    41                                         <g:each in="${runs}" var="run">
    42                                                 <tr>
    43                                                         <td><g:checkBox name="ids" value="${run.id}" checked="${false}" onClick="updateCheckAll(this);" /></td>
    44                                                         <td><g:link title="View run" controller="run" action="show" id="${run.id}">${run.name}</g:link></td>
    45                                                         <td>${run.assaySamples?.size()}</td>
    46                                                         <td><g:formatNumber number="${run.numSequences()}" format="###,###,##0" /></td>
    47                                                         <td><g:link title="View run" controller="run" action="show" id="${run.id}"><img src="${fam.icon( name: 'application_form_magnify' )}" alt="View run" title="View run" /></g:link></td>
    48                                                         <td>
    49                                                                 <g:if test="${run.deletable(user)}">
    50                                                                         <g:link title="Delete run" onClick="return confirm( 'Are you sure you want to delete this run?' );" controller="run" action="deleteRun" id="${run.id}"><img src="${fam.icon( name: 'delete' )}" alt="Delete run" title="Delete run" /></g:link>
    51                                                                 </g:if>
    52                                                                 <g:else>
    53                                                                         <img src="${fam.icon( name: 'delete' )}" class="disabled" alt="Run can not be deleted because data is associated with it." title="Run can not be deleted because data is associated with it." />
    54                                                                 </g:else>
    55                                                         </td>
    56                                                         <td>
    57                                                                 <g:if test="${run.numSequences() > 0}">
    58                                                                         <g:link controller="run" action="sequenceLengthHistogram" title="Sequence length histogram" id="${run.id}"><img src="${fam.icon( name: 'chart_bar' )}" alt="Sequence length histogram" title="Sequence length histogram" /></g:link>
    59                                                                 </g:if>
    60                                                                 <g:else>
    61                                                                         <img src="${fam.icon( name: 'chart_bar' )}" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />
    62                                                                 </g:else>
    63                                                         </td>                                                   
    64                                                 </tr>
    65                                         </g:each>
     41                                        <td colspan="7">
     42                                                Loading data from server
     43                                        </td>
    6644                                </tbody>
    6745                        </table>
  • trunk/grails-app/views/run/show.gsp

    r52 r59  
    6161        </div>
    6262        <div class="blok_data">
    63                 <label># assays</label>: ${run.assays?.size()}
    64                 <% def numHidden = run.assays?.findAll { !it.study.canRead( session.user ) }.size() ; %>
    65                 <g:if test="${numHidden}">
    66                         (${numHidden}
    67                         <a href="#" onClick="alert( '${numHidden} assay(s) from this run are hidden because you don\'t have the right permissions to view them.' ); return false;">
     63                <%
     64                        def numAssays = run.numAssays();
     65                        def numReadableAssays = run.numReadableAssays( session.user );
     66                        def numHiddenAssays = numAssays - numReadableAssays; 
     67                %>
     68                <label># assays</label>: ${numAssays}
     69                <g:if test="${numHiddenAssays}">
     70                        (${numHiddenAssays}
     71                        <a href="#" onClick="alert( '${numHiddenAssays} assay(s) from this run are hidden because you don\'t have the right permissions to view them.' ); return false;">
    6872                        hidden</a>)
    6973                </g:if>
    7074                <br />
    7175       
    72                 <label># samples</label>: ${run.assaySamples?.size()}
    73                 <% numHidden = run.assaySamples?.findAll { !it.assay?.study.canRead( session.user ) }.size() ; %>
    74                 <g:if test="${numHidden}">
    75                         (${numHidden}
    76                         <a href="#" onClick="alert( '${numHidden} sample(s) from this run are hidden because you don\'t have the right permissions to view them.' ); return false;">
     76                <%
     77                        def numAssaySamples = run.numAssaySamples();
     78                        def numReadableAssaySamples = run.numReadableAssaySamples( session.user );
     79                        def numWritableAssaySamples = run.numWritableAssaySamples( session.user );
     80                        def numHiddenAssaySamples = numAssaySamples - numReadableAssaySamples; 
     81                %>
     82                <label># samples</label>: ${numAssaySamples}
     83                <g:if test="${numHiddenAssaySamples}">
     84                        (${numHiddenAssaySamples}
     85                        <a href="#" onClick="alert( '${numHiddenAssaySamples} sample(s) from this run are hidden because you don\'t have the right permissions to view them.' ); return false;">
    7786                        hidden</a>)
    7887                </g:if>
     
    8796        <!-- Samples -->
    8897        <h2>Samples</h2>
    89         <% def assaySamples = run.assaySamples ? run.assaySamples.findAll { it.assay?.study.canRead( session.user ) }.toList().sort { it.sample.name } : []; %>
    90         <g:if test="${assaySamples.size() == 0}">
     98        <g:if test="${numReadableAssaySamples == 0}">
    9199                <p>No samples found in run.</p>
    92100        </g:if>
    93101        <g:else>
    94102                <form id="sampleForm"><input type="hidden" name="runId" value="${run.id}" /></form>
    95                 <table class="paginate" id="samples">
     103                <table class="paginate_serverside" rel="<g:createLink controller="run" action="showSampleData" id="${run.id}" />" id="samples">
    96104                        <thead>
    97105                                <tr>
     
    109117                        </thead>                       
    110118                        <tbody>
    111                                 <g:each in="${assaySamples}" var="assaySample">
    112                                         <tr>
    113                                                 <td><g:checkBox name="ids" value="${assaySample.id}" checked="${false}" onClick="updateCheckAll(this);" /></td>
    114                                                 <td><a title="Show sample details" href="#" onClick="showSample(${assaySample.id}, 'run'); return false;">${assaySample.sample.name}</a></td>
    115                                                 <td>${assaySample.assay.study.name}</td>
    116                                                 <td>${assaySample.assay.name}</td>
    117                                                 <td>${assaySample.fwMidName}</td>
    118                                                 <td>
    119                                                         <g:if test="${assaySample.numSequenceFiles() > 0}">
    120                                                                 <g:formatNumber number="${assaySample.numSequences()}" format="###,###,##0" />
    121                                                         </g:if>
    122                                                         <g:else>
    123                                                                 -
    124                                                         </g:else>
    125                                                 </td>
    126                                                 <td>
    127                                                         <g:if test="${assaySample.numQualityFiles() > 0}">
    128                                                                 <g:formatNumber number="${assaySample.numQualScores()}" format="###,###,##0" />
    129                                                         </g:if>
    130                                                         <g:else>
    131                                                                 -
    132                                                         </g:else>
    133                                                 </td>
    134                                                 <td class="button">
    135                                                         <g:if test="${!assaySample.assay.study.canWrite(session.user)}">
    136                                                                 <img src="${fam.icon(name: 'pencil')}" class="disabled" title="You can't edit this sample because you don't have sufficient privileges." />
    137                                                         </g:if>
    138                                                         <g:else>
    139                                                                 <a title="Edit sample" onClick="showEditSampleDialog(${assaySample.id}, 'run', ${run.id});" href="#"><img title="Edit sample" src="${fam.icon(name: 'pencil')}" /></a>
    140                                                         </g:else>
    141                                                 </td>
    142                                                 <td class="button">
    143                                                         <g:if test="${!assaySample.assay.study.canWrite(session.user)}">
    144                                                                 <img src="${fam.icon(name: 'application_delete')}" class="disabled" title="You can't remove this sample because you don't have sufficient privileges." />
    145                                                         </g:if>
    146                                                         <g:else>
    147                                                                 <g:link title="Remove sample from run" 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  title="Remove sample from run" src="${fam.icon(name: 'application_delete')}" /></g:link>
    148                                                         </g:else>
    149                                                 </td>
    150                                                 <td class="button">
    151                                                         <g:if test="${assaySample.numSequences() > 0}">
    152                                                                 <g:link controller="assaySample" action="sequenceLengthHistogram" id="${assaySample.id}" title="Sequence length histogram"><img src="${fam.icon( name: 'chart_bar' )}" alt="Sequence length histogram" title="Sequence length histogram" /></g:link>
    153                                                         </g:if>
    154                                                         <g:else>
    155                                                                 <img src="${fam.icon( name: 'chart_bar' )}" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />
    156                                                         </g:else>
    157                                                 </td>                                                   
    158                                         </tr>
    159                                 </g:each>
     119                                <tr>
     120                                        <td colspan="10" class="dataTables_empty">Loading data from server</td>
     121                                </tr>
    160122                        </tbody>
    161123                </table>
     
    164126        <g:if test="${editable}">
    165127                <p class="options multiple">
    166                         <% def writableAssaySamples = assaySamples.findAll { it.assay.study.canWrite( session.user ) } %>
    167128                        <a class="addAssociation" onClick="showAddSamplesDialog(); return false;" href="#">Add samples</a><br />
    168129
    169                         <g:if test="${writableAssaySamples.size() > 0}">
     130                        <g:if test="${numWritableAssaySamples > 0}">
    170131                                <a class="editAssociation" onClick="showEnterTagsDialog(); return false;" href="#">Edit sample data</a>
    171132                        </g:if>
     
    176137                        <br />
    177138
    178                         <g:if test="${writableAssaySamples.size() > 0}">
     139                        <g:if test="${numWritableAssaySamples > 0}">
    179140                                <a class="removeAssociation" href="#" onClick="if( confirm( 'Are you sure you want to remove selected samples from this run?' ) ) { submitPaginatedForm( $( '#sampleForm' ), '<g:createLink controller="run" action="removeSamples" />', '#samples', 'Please select one or more samples to remove from this run' ); } return false;">Remove selected samples</a>
    180141                        </g:if>
     
    184145                </p>
    185146                <p class="options multiple">
    186                         <g:if test="${writableAssaySamples.size() == 0 || !run.assays?.size()}">
     147                        <g:if test="${numWritableAssaySamples == 0 || numWritableAssays == 0 }">
    187148                                <a class="addSequences disabled" onClick="return false;" href="#">Add sequence files</a>
    188149                        </g:if>
     
    193154                        <br />
    194155
    195                         <g:if test="${writableAssaySamples.size() > 0 && run.numFiles() > 0 }">
     156                        <g:if test="${numWritableAssaySamples > 0 && run.numFiles() > 0 }">
    196157                                <a class="removeSequences" href="#" onClick="if( confirm( 'Are you sure you want to remove all sequence data from this run? Only sequences are removed from samples you have write access to.' ) ) { submitPaginatedForm( $( '#sampleForm' ), '<g:createLink controller="run" action="deleteSequenceData" />', '#samples', 'Please select one or more samples to remove sequences from.' ); } return false;">Delete selected sequences</a>
    197158                        </g:if>
     
    202163               
    203164                <p class="options multiple">
    204                         <g:if test="${assaySamples.size() > 0}">
     165                        <g:if test="${numReadableAssaySamples > 0}">
    205166                                <a class="fasta" href="#" onClick="submitPaginatedForm( $( '#sampleForm' ), '<g:createLink controller="assaySample" action="exportAsFasta" />', '#samples', 'Please select one or more samples to export' ); return false;">Export as fasta</a><br />
    206167                        </g:if>
     
    212173                <div style="clear: both;"></div>
    213174                       
    214                 <g:if test="${writableAssaySamples.size() > 0}">
     175                <g:if test="${numWritableAssaySamples > 0}">
    215176                        <g:render template="enterTagsDialog" model="[run: run, writableAssaySamples: writableAssaySamples]" />
    216177                        <g:render template="addFilesDialog" model="[run: run]" />
     
    225186        <!-- Runs -->
    226187        <h2>Assays</h2>
    227         <g:if test="${run.assays == null || run.assays.size() == 0}">
     188        <g:if test="${run.assays == null || numReadableAssays == 0}">
    228189                No assay found for this run
    229190        </g:if>
  • trunk/web-app/css/datatables/demo_table_jui.css

    r26 r59  
    141141        margin-left: -125px;
    142142        border: 1px solid #ddd;
     143        background-color: white;
    143144        text-align: center;
    144145        color: #999;
  • trunk/web-app/css/metagenomics.css

    r56 r59  
    508508/* Makes sure the filenames in the dialog don't exceed 200px */
    509509.dataTables_wrapper .uploadedFile { display: inline-block; zoom: 1; *display: inline; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 190px; height: 15px; }
    510 .blok_data { display: inline-block; zoom: 1; *display: inline; width: 400px; vertical-align: top;}                                     
     510.blok_data { display: inline-block; zoom: 1; *display: inline; width: 400px; vertical-align: top;}
     511
     512.importProcessedFiles td { vertical-align: middle; }
  • trunk/web-app/js/paginate.js

    r47 r59  
    44        }
    55       
     6        // Initialize default pagination
    67        $( selector + ' .paginate').each(function(idx, el) {
    78                var $el = $(el);
     
    1920                });
    2021        });
     22
     23        // Initialize serverside pagination
     24        $( selector + ' .paginate_serverside').each(function(idx, el) {
     25                var $el = $(el);
     26               
     27                // Determine data url from rel attribute
     28                var dataUrl = $el.attr('rel');
     29               
     30                $el.dataTable({
     31                        "bProcessing": true,
     32                        "bServerSide": true,
     33                        "sAjaxSource": dataUrl,
     34                        sDom: '<"H"lf>rt<"F"ip>',
     35
     36                        bJQueryUI: true,
     37                        bAutoWidth: false,
     38                        bFilter: false,
     39                        bLengthChange: false,
     40                        iCookieDuration: 86400,                         // Save cookie one day
     41                        sPaginationType: 'full_numbers',
     42                        iDisplayLength: 10,                                     // Number of items shown on one page.
     43                        aoColumnDefs: [
     44                                { "bSortable": false, "aTargets": ["nonsortable"] }     // Disable sorting on all columns with th.nonsortable
     45                        ],
     46                       
     47                        // Override the fnServerData in order to show/hide the paginated
     48                        // buttons if data is loaded
     49                        "fnServerData": function ( sSource, aoData, fnCallback ) {
     50                                $.ajax( {
     51                                        "dataType": 'json',
     52                                        "type": "POST",
     53                                        "url": sSource,
     54                                        "data": aoData,
     55                                        "success": function( data, textStatus, jqXHR ) {
     56                                                fnCallback( data, textStatus, jqXHR );
     57                                                showHidePaginatedButtonsForTableWrapper( $el.parent() );
     58                                        }
     59                                } );
     60                        }                       
     61                });
     62        });
    2163       
     64        // Show hide paginated buttons
     65        showHidePaginatedButtons( selector );
     66}
     67
     68function showHidePaginatedButtons( selector ) {
    2269        // Remove the top bar of the datatable and hide pagination with only one page
    2370        $( selector + " .dataTables_wrapper").each(function(idx, el) {
    2471                var $el = $(el);
     72                showHidePaginatedButtonsForTableWrapper( $el )
     73        });     
     74}
     75
     76function showHidePaginatedButtonsForTableWrapper( el ) {
     77        // Hide pagination if only one page is present (that is: if no buttons can be clicked)
     78        if(el.find('span span.ui-state-default:not(.ui-state-disabled)').size() == 0 ){
     79                el.find('div.fg-toolbar').css( 'display', 'none' );
     80        } else {
     81                el.find('div.fg-toolbar').css( 'display', 'block' );
     82                el.find( 'div.ui-toolbar' ).first().hide();
    2583               
    26                 // Hide pagination if only one page is present (that is: if no buttons can be clicked)
    27                 if($el.find('span span.ui-state-default:not(.ui-state-disabled)').size() == 0 ){
    28                         $el.find('div.fg-toolbar').css( 'display', 'none' );
    29                 } else {
    30                         $el.find('div.fg-toolbar').css( 'display', 'block' );
    31                         $el.find( 'div.ui-toolbar' ).first().hide();
    32                        
    33                         // Check whether a h1, h2 or h3 is present above the table, and move it into the table
    34                         /*
    35                         var $previousElement = $el.prev();
    36                         if( $previousElement != undefined && $previousElement.get(0) != undefined ) {
    37                                 var tagName = $previousElement.get(0).tagName.toLowerCase();
    38                                 if( tagName == "h1" || tagName == "h2" || tagName == "h3" ) {
    39                                         // Put the margin that was on the title onto the table
    40                                         $el.css( "margin-top", $previousElement.css( "margin-top" ) );
    41                                         $previousElement.css( "margin-top", '4px' );
    42                                         $previousElement.css( "marginBottom", '4px' );
     84                // Check whether a h1, h2 or h3 is present above the table, and move it into the table
     85                /*
     86                var $previousElement = $el.prev();
     87                if( $previousElement != undefined && $previousElement.get(0) != undefined ) {
     88                        var tagName = $previousElement.get(0).tagName.toLowerCase();
     89                        if( tagName == "h1" || tagName == "h2" || tagName == "h3" ) {
     90                                // Put the margin that was on the title onto the table
     91                                $el.css( "margin-top", $previousElement.css( "margin-top" ) );
     92                                $previousElement.css( "margin-top", '4px' );
     93                                $previousElement.css( "marginBottom", '4px' );
    4394
    44                                         // If so, move the element into the table
    45                                         $previousElement.remove();
    46                                         $el.find( 'div.ui-toolbar' ).first().append( $previousElement );
    47                                 }
     95                                // If so, move the element into the table
     96                                $previousElement.remove();
     97                                $el.find( 'div.ui-toolbar' ).first().append( $previousElement );
    4898                        }
    49                         */                                             
    5099                }
    51         });
     100                */                                             
     101        }       
    52102}
    53103
Note: See TracChangeset for help on using the changeset viewer.