Changeset 1501


Ignore:
Timestamp:
Feb 7, 2011, 4:07:54 PM (12 years ago)
Author:
robert@…
Message:
  • Number of seconds for the rest controller to keep data in cache is now a configuration option
  • After searching, it is possible to choose which action to perform on the search results.
Location:
trunk
Files:
4 added
22 edited

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/conf/config-ci.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://ci.metagenomics.nmcdsp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-dbnpdemo.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://demo.metagenomics.dbnp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-dbnptest.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://test.metagenomics.dbnp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-development.properties

    r1459 r1501  
    3535modules.metagenomics.url=http://localhost:8184/metagenomics
    3636
     37# Number of seconds to keep rest results from modules in cache
     38modules.cacheDuration = 0
     39
    3740# default application users
    3841authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-nmcdsptest.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://test.metagenomics.nmcdsp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-production.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://metagenomics.nmcdsp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-test.properties

    r1459 r1501  
    3535modules.metagenomics.url=http://localhost:8184/metagenomics
    3636
     37# Number of seconds to keep rest results from modules in cache
     38modules.cacheDuration = 600
     39
    3740# default application users
    3841authentication.users.admin.username=admin
  • trunk/grails-app/conf/config-www.properties

    r1459 r1501  
    3636modules.metagenomics.url=http://metagenomics.nmcdsp.org
    3737
     38# Number of seconds to keep rest results from modules in cache
     39modules.cacheDuration = 600
     40
    3841# default application users
    3942authentication.users.admin.username=admin
  • trunk/grails-app/controllers/dbnp/query/AdvancedQueryController.groovy

    r1482 r1501  
    11package dbnp.query
     2
    23import dbnp.modules.*
    34import org.dbnp.gdt.*
     
    1516
    1617        def entitiesToSearchFor = [ 'Study': 'Studies', 'Sample': 'Samples']
    17        
     18
    1819        /**
    1920         * Shows search screen
     
    2526                        criteria = parseCriteria( params.criteria, false )
    2627                }
    27                 [entitiesToSearchFor: entitiesToSearchFor, searchableFields: getSearchableFields(), criteria: criteria]
     28                [searchModes: SearchMode.values(), entitiesToSearchFor: entitiesToSearchFor, searchableFields: getSearchableFields(), criteria: criteria]
    2829        }
    2930
     
    4748
    4849                // Create a search object and let it do the searching
    49                 Search search;
     50                Search search = determineSearch( params.entity );
    5051                String view = determineView( params.entity );
    51                 switch( params.entity ) {
    52                         case "Study":   search = new StudySearch();     break;
    53                         case "Sample":  search = new SampleSearch(); break;
    54 
    55                         // This exception will only be thrown if the entitiesToSearchFor contains more entities than
    56                         // mentioned in this switch structure.
    57                         default:                throw new Exception( "Can't search for entities of type " + params.entity );
    58                 }
     52
     53                // Choose between AND and OR search. Default is given by the Search class itself.
     54                switch( params.operator?.toString()?.toLowerCase() ) {
     55                        case "or":
     56                                search.searchMode = SearchMode.or;
     57                                break;
     58                        case "and":
     59                                search.searchMode = SearchMode.and;
     60                                break;
     61                }
     62
    5963                search.execute( parseCriteria( params.criteria ) );
    6064
    6165                // Save search in session
    6266                def queryId = saveSearch( search );
    63                 render( view: view, model: [search: search, queryId: queryId] );
     67                render( view: view, model: [search: search, queryId: queryId, actions: determineActions(search)] );
    6468        }
    6569
     
    6973         */
    7074        def discard = {
    71                 Integer queryId
    72                 try {
    73                         queryId = params.id as Integer
    74                 } catch( Exception e ) {
     75                def queryIds = params.list( 'id' );
     76                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
     77
     78                if( queryIds.size() == 0 ) {
    7579                        flash.error = "Incorrect search ID given to discard"
    7680                        redirect( action: "index" );
     
    7882                }
    7983
    80                 discardSearch( queryId );
    81                 flash.message = "Search has been discarded"
     84                queryIds.each { queryId ->
     85                        discardSearch( queryId );
     86                }
     87
     88                if( queryIds.size() > 1 ) {
     89                        flash.message = "Searches have been discarded"
     90                } else {
     91                        flash.message = "Search has been discarded"
     92                }
    8293                redirect( action: "list" );
    8394        }
     
    8899         */
    89100        def show = {
    90                 Integer queryId
    91                 try {
    92                         queryId = params.id as Integer
    93                 } catch( Exception e ) {
     101                def queryId = params.int( 'id' );
     102
     103                if( !queryId ) {
    94104                        flash.error = "Incorrect search ID given to show"
    95105                        redirect( action: "index" );
     
    107117                // Determine which view to show
    108118                def view = determineView( s.entity );
    109                 render( view: view, model: [search: s, queryId: queryId] );
    110         }
    111 
     119                render( view: view, model: [search: s, queryId: queryId, actions: determineActions(s)] );
     120        }
     121
     122        /**
     123         * Performs an action on specific searchResults
     124         * @param       queryId         queryId of the search to show
     125         * @param       id                      list with the ids of the results to perform the action on
     126         * @param       actionName      Name of the action to perform
     127         */
     128        def performAction = {
     129                def queryId = params.int( 'queryId' );
     130                def selectedIds = params.list( 'id' ).findAll { it.isLong() }.collect { Long.parseLong(it) }
     131                def actionName = params.actionName;
     132                def moduleName = params.moduleName;
     133
     134                if( !queryId ) {
     135                        flash.error = "Incorrect search ID given to show"
     136                        redirect( action: "index" );
     137                        return
     138                }
     139               
     140                // Retrieve the search from session
     141                Search s = retrieveSearch( queryId );
     142                if( !s ) {
     143                        flash.message = "Specified search could not be found"
     144                        redirect( action: "list" );
     145                        return;
     146                }
     147
     148                // Determine the possible actions
     149                def actions = determineActions(s, selectedIds );
     150
     151                // Find the right action to perform
     152                def redirectUrl;
     153                for( action in actions ) {
     154                        if( action.module == moduleName && action.name == actionName ) {
     155                                redirectUrl = action.url;
     156                                break;
     157                        }
     158                }
     159               
     160                if( !redirectUrl ) {
     161                        flash.error = "No valid action is given to perform";
     162                        redirect( action: "show", id: queryId );
     163                        return;
     164                }
     165               
     166                redirect( url: redirectUrl );
     167        }
     168       
    112169        /**
    113170         * Shows a list of searches that have been saved in session
     
    124181                [searches: searches]
    125182        }
    126        
     183
    127184        /**
    128185         * Shows a search screen where the user can search within the results of another search
     
    130187         */
    131188        def searchIn = {
    132                 Integer queryId
    133                 try {
    134                         queryId = params.id as Integer
    135                 } catch( Exception e ) {
     189                def queryIds = params.list( 'id' );
     190                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
     191
     192                if( queryIds.size() == 0 ) {
    136193                        flash.error = "Incorrect search ID given to show"
    137194                        redirect( action: "index" );
     
    139196                }
    140197
    141                 // Retrieve the search from session
    142                 Search s = retrieveSearch( queryId );
    143                 if( !s ) {
    144                         flash.message = "Specified search could not be found"
     198                // Retrieve the searches from session
     199                def params = [:]
     200                queryIds.eachWithIndex { queryId, idx ->
     201                        Search s = retrieveSearch( queryId );
     202                        if( !s ) {
     203                                flash.message = "Specified search " + queryId + " could not be found"
     204                                return;
     205                        } else {
     206                                params[ "criteria." + idx + ".entityfield" ] = s.entity;
     207                                params[ "criteria." + idx + ".operator" ] = "in";
     208                                params[ "criteria." + idx + ".value" ] = queryId;
     209                        }
     210                }
     211
     212                redirect( action: "index", params: params)
     213        }
     214
     215        /**
     216         * Combines the results of multiple searches
     217         * @param       id      queryIds of the searches to combine
     218         */
     219        def combine = {
     220                def queryIds = params.list( 'id' );
     221                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
     222
     223                if( queryIds.size() == 0 ) {
     224                        flash.error = "Incorrect search ID given to combine"
    145225                        redirect( action: "index" );
     226                        return
     227                }
     228
     229                // First determine whether the types match
     230                def searches = [];
     231                def type = "";
     232                flash.error = "";
     233                queryIds.eachWithIndex { queryId, idx ->
     234                        Search s = retrieveSearch( queryId );
     235                        if( !s ) {
     236                                return;
     237                        }
     238
     239                        if( type ) {
     240                                if( type != s.entity ) {
     241                                        flash.error = type + " and " + s.entity.toLowerCase() + " queries can't be combined. Selected queries of one type.";
     242                                        return
     243                                }
     244                        } else {
     245                                type = s.entity
     246                        }
     247                }
     248
     249                if( flash.error ) {
     250                        redirect( action: "list" );
    146251                        return;
    147252                }
    148253
    149                 redirect( action: "index", params: [ "criteria.0.entityfield": s.entity, "criteria.0.operator": "in", "criteria.0.value": queryId ])
     254                if( !type ) {
     255                        flash.error = "No correct query ids were given."
     256                        redirect( action: "list" );
     257                        return;
     258                }
     259
     260                // Retrieve the searches from session
     261                Search combined = determineSearch( type );
     262                combined.searchMode = SearchMode.or;
     263
     264                queryIds.eachWithIndex { queryId, idx ->
     265                        Search s = retrieveSearch( queryId );
     266                        if( s ) {
     267                                combined.addCriterion( new Criterion( entity: type, field: null, operator: Operator.insearch, value: s ) );
     268                        }
     269                }
     270
     271                // Execute search to combine the results
     272                combined.execute();
     273
     274                def queryId = saveSearch( combined );
     275                redirect( action: "show", id: queryId );
    150276        }
    151277
     
    155281                        case "Sample":  return "sampleresults"; break;
    156282                        default:                return "results"; break;
     283                }
     284        }
     285
     286        /**
     287         * Returns the search object used for searching
     288         */
     289        protected Search determineSearch( String entity ) {
     290                switch( entity ) {
     291                        case "Study":   return new StudySearch();
     292                        case "Sample":  return new SampleSearch();
     293
     294                        // This exception will only be thrown if the entitiesToSearchFor contains more entities than
     295                        // mentioned in this switch structure.
     296                        default:                throw new Exception( "Can't search for entities of type " + entity );
    157297                }
    158298        }
     
    230370                        if( c.key ==~ /[0-9]+/ ) {
    231371                                def formCriterion = c.value;
    232                                
     372
    233373                                Criterion criterion = new Criterion();
    234374
     
    251391                                        continue;
    252392                                }
    253                                
     393
    254394                                // Special case of the 'in' operator
    255395                                if( criterion.operator == Operator.insearch ) {
     
    258398                                                s = retrieveSearch( Integer.parseInt( formCriterion.value ) );
    259399                                        } catch( Exception e ) {}
    260                                        
     400
    261401                                        if( !s ) {
    262402                                                flash.error += "Can't search within previous query: query not found";
    263403                                                continue;
    264404                                        }
    265                                        
     405
    266406                                        if( parseSearchIds ) {
    267407                                                criterion.value = s
     
    273413                                        criterion.value = formCriterion.value;
    274414                                }
    275                                
     415
    276416                                list << criterion;
    277417                        }
     
    332472                // First check whether a search with the same criteria is already present
    333473                def previousSearch = retrieveSearchByCriteria( s.getCriteria() );
    334                
     474
    335475                def id
    336476                if( previousSearch ) {
     
    340480                        id = ( session.queries*.key.max() ?: 0 ) + 1;
    341481                }
    342                
     482
    343483                s.id = id;
    344484                session.queries[ id ] = s;
    345485
    346                 println "On saveSearch: " + session.queries;
    347486                return id;
    348487        }
     
    356495                if( !session.queries )
    357496                        return null
    358                
     497
    359498                if( !criteria )
    360499                        return null
    361                        
     500
    362501                for( query in session.queries ) {
    363502                        def key = query.key;
     
    385524                        return null;
    386525
    387                 println "On retrieveSearch: " + session.queries;
    388526                return (Search) session.queries[ id ]
    389527        }
     
    402540                session.queries.remove( id );
    403541
    404                 println "On discardSearch: " + session.queries;
    405542                if( !( sessionSearch instanceof Search ) )
    406543                        return null;
     
    419556                return session.queries*.value.toList()
    420557        }
     558
     559        /**
     560         * Determine a list of actions that can be performed on specific entities
     561         * @param entity                Name of the entity that the actions could be performed on
     562         * @param selectedIds   List with ids of the selected items to perform an action on
     563         * @return
     564         */
     565        protected List determineActions( Search s, def selectedIds = null ) {
     566                return gscfActions( s, selectedIds ) + moduleActions( s, selectedIds );
     567        }
     568
     569        /**
     570         * Determine a list of actions that can be performed on specific entities by GSCF
     571         * @param entity        Name of the entity that the actions could be performed on
     572         * @param selectedIds   List with ids of the selected items to perform an action on
     573         */
     574        protected List gscfActions(Search s, def selectedIds = null) {
     575                switch(s.entity) {
     576                        case "Study":
     577                                def exportParams = [:]
     578                                s.filterResults(selectedIds).each {
     579                                        exportParams[ it.code ] = it.id;
     580                                }
     581                                return [[
     582                                                module: "gscf",
     583                                                name:"simpletox",
     584                                                description: "Export as SimpleTox",
     585                                                url: createLink( controller: "exporter", action: "export", params: exportParams )
     586                                        ]]
     587                        case "Sample":
     588                                return []
     589                        default:
     590                                return [];
     591                }
     592        }
     593
     594        /**
     595         * Determine a list of actions that can be performed on specific entities by other modules
     596         * @param entity        Name of the entity that the actions could be performed on
     597         */
     598        protected List moduleActions(Search s, def selectedIds = null) {
     599                def actions = []
     600
     601                if( !s.getResults() || s.getResults().size() == 0 )
     602                        return []
     603
     604                // Loop through all modules and check which actions can be performed on the
     605                AssayModule.list().each { module ->
     606                        // Remove 'module' from module name
     607                        def moduleName = module.name.replace( 'module', '' ).trim()
     608                        try {
     609                                def callUrl = module.url + "/rest/getPossibleActions?entity=" + s.entity
     610                                def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
     611
     612                                // Check whether the entity is present in the return value
     613                                if( json[ s.entity ] ) {
     614                                        json[ s.entity ].each { action ->
     615                                                def url = module.url + "/action/" + action.name
     616                                                url += "?entity=" + s.entity
     617                                                url += "&" + s.filterResults(selectedIds).collect { "tokens=" + it.giveUUID() }.join( "&" )
     618                                                actions << [
     619                                                                        module: moduleName,
     620                                                                        name: action.name,
     621                                                                        description: action.description + " (" + moduleName + ")",
     622                                                                        url: url
     623                                                                ];
     624                                        }
     625                                }
     626                        } catch( Exception e ) {
     627                                // Exception is thrown when the call to the module fails. No problems though.
     628                                log.error "Error while fetching possible actions from " + module.name + ": " + e.getMessage()
     629                        }
     630                }
     631
     632                return actions;
     633        }
     634
    421635}
  • trunk/grails-app/services/dbnp/modules/ModuleCommunicationService.groovy

    r1482 r1501  
    1818import grails.converters.*
    1919import javax.servlet.http.HttpServletResponse
     20import org.codehaus.groovy.grails.commons.ConfigurationHolder
    2021
    2122class ModuleCommunicationService implements Serializable {
     
    3334         * Number of seconds to save the data in cache
    3435         */
    35         def numberOfSecondsInCache = 10 * 60;
     36        def numberOfSecondsInCache = Integer.valueOf( ConfigurationHolder.config.modules.cacheDuration )
    3637
    3738        /**
     
    147148                def user = authenticationService.getLoggedInUser();
    148149                def userId = user ? user.id : -1;
    149 
     150               
     151                println "Retrieve from cache: " + url
     152                println "Seconds in cache: " + numberOfSecondsInCache
     153               
    150154                if( cache[ userId ] && cache[ userId ][ url ] && ( System.currentTimeMillis() - cache[ userId ][ url ][ "timestamp" ] ) < numberOfSecondsInCache * 1000 ) {
    151155                        return cache[ userId ][ url ];
  • trunk/grails-app/views/advancedQuery/_criteria.gsp

    r1482 r1501  
     1<%@ page import="dbnp.query.*" %>
    12<ul id="criteria">
    2         <g:each in="${criteria}" var="criterion">
     3        <g:each in="${criteria}" var="criterion" status="j">
    34                <li>
    45                        <span class="entityfield">${criterion.entityField()}</span>
    56                        <span class="operator">${criterion.operator}</span>
    6                         <span class="value">${criterion.value}</span>
     7                        <span class="value">
     8                                <g:if test="${criterion.value != null && criterion.value instanceof Search}">
     9                                        <g:link action="show" id="${criterion.value.id}">${criterion.value}</g:link>
     10                                </g:if>
     11                                <g:else>
     12                                        ${criterion.value}
     13                                </g:else>
     14                        </span>
     15                        <g:if test="${j < criteria.size() -1}">
     16                                <g:if test="${search.searchMode == SearchMode.and}">and</g:if>
     17                                <g:if test="${search.searchMode == SearchMode.or}">or</g:if>
     18                        </g:if>
    719                </li>
    820        </g:each>
  • trunk/grails-app/views/advancedQuery/_resultbuttons.gsp

    r1482 r1501  
    1 <p>
    2         <g:link action="searchIn" id="${queryId}">Search within results</g:link><br />
    3         <g:link action="index">Search again</g:link><br />
    4         <g:link action="discard" id="${queryId}">Discard results</g:link><br />
    5         <g:link action="list">Previous searches</g:link>
     1<p class="options">
     2        <g:link class="searchIn" action="searchIn" id="${queryId}">Search within results</g:link><br />
     3        <g:link class="search" action="index">Search again</g:link><br />
     4        <g:link class="discard" action="discard" id="${queryId}">Discard results</g:link><br />
     5        <g:link class="listPrevious" action="list">Previous searches</g:link>
    66</p>
     7<p class="options">
     8        <g:each in="${actions}" var="action">
     9                <a class="performAction ${action.name}" href="${action.url}" onClick="performAction( $('form#results'), '${action.name}', '${action.module}' ); return false;">${action.description}</a><br />
     10        </g:each>
     11</p>
     12<br clear="all">
  • trunk/grails-app/views/advancedQuery/index.gsp

    r1487 r1501  
    1414                                        <g:if test="${j > 0}">,</g:if>
    1515                                        {
     16                                                label: "${entity.key.toString().encodeAsJavaScript()}.${field.toString().encodeAsJavaScript()} ${entity.key.toString().encodeAsJavaScript()} ${field.toString().encodeAsJavaScript()}",
     17                                                show: "${(field[0].toUpperCase() + field[1..-1]).encodeAsJavaScript()}",
    1618                                                value: "${entity.key.toString().encodeAsJavaScript()}.${field.toString().encodeAsJavaScript()}",
    17                                                 show: "${(field[0].toUpperCase() + field[1..-1]).encodeAsJavaScript()}",
    18                                                 label: "${entity.key.toString().encodeAsJavaScript()}.${field.toString().encodeAsJavaScript()}",
    1919                                                entity: "${entity.key.toString().encodeAsJavaScript()}"
    2020                                        }
     
    4949        </div>
    5050</g:if>
    51 
    52 <a href="<g:createLink action="list" />">View previous queries</a>
    5351
    5452<form id="input_criteria">
     
    9189        <g:form action="search" method="get">
    9290                <label for="entity">Search for</label><g:select from="${entitiesToSearchFor}" optionKey="key" optionValue="value" name="entity" /><br />
     91                <label for="entity">Searchtype</label><g:select from="${searchModes}" name="operator" /><br />
    9392                <label for="criteria">Criteria</label>
    9493                <ul id="criteria">
     
    9998        </g:form>
    10099</div>
    101 
     100<p class="options">
     101        <g:link class="listPrevious" action="list">Previous searches</g:link>
     102</p>
    102103<br  clear="all" />
    103104</body>
  • trunk/grails-app/views/advancedQuery/list.gsp

    r1482 r1501  
    66        <title>Previous queries</title>
    77        <link rel="stylesheet" href="<g:resource dir="css" file="advancedQuery.css" />" type="text/css"/>
     8        <g:javascript src="advancedQueryResults.js" />
     9        <script type="text/javascript">
     10                function searchWithinResults( form ) {
     11                        submitForm( form, '/advancedQuery/searchIn' );
     12                }
     13                function discardResults( form ) {
     14                        submitForm( form, '/advancedQuery/discard' );
     15                }       
     16                function combineResults( form ) {
     17                        submitForm( form, '/advancedQuery/combine' );
     18                }                               
     19        </script>
     20       
    821</head>
    922<body>
     
    1124<h1>Previous queries</h1>
    1225
     26<g:if test="${flash.error}">
     27        <div class="errormessage">
     28                ${flash.error.toString().encodeAsHTML()}
     29        </div>
     30</g:if>
     31<g:if test="${flash.message}">
     32        <div class="message">
     33                ${flash.message.toString().encodeAsHTML()}
     34        </div>
     35</g:if>
     36
    1337<g:if test="${searches.size() > 0}">
    14         <table id="searchresults">
     38        <form id="searchform" method="post">
     39        <table id="searchresults" class="paginate">
    1540                <thead>
    1641                        <tr>
    17                                 <th></th>
     42                                <th class="nonsortable"></th>
    1843                                <th>#</th>
    1944                                <th>Type</th>
     
    2146                                <th># results</th>
    2247                                <th>time</th>
    23                                 <th></th>
    24                                 <th></th>
     48                                <th class="nonsortable"></th>
     49                                <th class="nonsortable"></th>
    2550                        </tr>
    2651                </thead>
    2752                <g:each in="${searches}" var="search">
    2853                        <tr>
    29                                 <td><g:checkBox name="queryId" value="${search.id}" checked="${false}" /></td>
     54                                <td><g:checkBox name="id" value="${search.id}" checked="${false}" /></td>
    3055                                <td>${search.id}</td>
    3156                                <td>${search.entity}</td>
     
    5277                </g:each>
    5378        </table>
     79        </form>
    5480</g:if>
    55 <p>
    56         <g:link action="index">Search again</g:link>
     81
     82<p class="options">
     83        <a href="#" class="combine" onClick="combineResults( $( '#searchform' ) ); return false;">Combine results</a><br />
     84        <a href="#" class="searchIn" onClick="searchWithinResults( $( '#searchform' ) ); return false;">Search within results</a><br />
     85        <g:link class="search" action="index">Search again</g:link><br />
     86        <a href="#" class="discard" onClick="discardResults( $( '#searchform' ) ); return false;">Discard results</a><br />
    5787</p>
     88
    5889</body>
    5990</html>
  • trunk/grails-app/views/advancedQuery/results.gsp

    r1482 r1501  
    55        <title>Query results</title>
    66        <link rel="stylesheet" href="<g:resource dir="css" file="advancedQuery.css" />" type="text/css"/>
     7        <g:javascript src="advancedQueryResults.js" />
    78</head>
    89<body>
     
    1314        Your search for:
    1415</p>
    15 <ul id="criteria">
    16         <g:each in="${search.getCriteria()}" var="criterion">
    17                 <li>
    18                         <span class="entityfield">${criterion.entityField()}</span>
    19                         <span class="operator">${criterion.operator}</span>
    20                         <span class="value">${criterion.value}</span>
    21                 </li>
    22         </g:each>
    23 </ul>
     16<g:render template="criteria" model="[criteria: search.getCriteria()]" />
    2417<p>
    2518        resulted in ${search.getNumResults()} results.
     
    3124                def extraFields = resultFields[ search.getResults()[ 0 ].id ]?.keySet();
    3225        %>
    33         <table id="searchresults">
     26        <table id="searchresults" class="paginate">
    3427                <thead>
    3528                        <tr>
     29                                <th class="nonsortable"></th>
    3630                                <th>Type</th>
    3731                                <th>Id</th>
     
    4337                <g:each in="${search.getResults()}" var="result">
    4438                        <tr>
     39                                <td width="3%">
     40                                        <% /*
     41                                                The value of this checkbox will be moved to the form (under this table) with javascript. This
     42                                                way the user can select items from multiple pages of the paginated result list correctly. See
     43                                                also http://datatables.net/examples/api/form.html and advancedQueryResults.js
     44                                        */ %>
     45                                        <g:checkBox name="id" value="${result.id}" checked="${false}" />
     46                                </td>                   
    4547                                <td>${search.entity}</td>
    4648                                <td>${result.id}</td>
     
    5052                                                        def fieldValue = resultFields[ result.id ]?.get( fieldName );
    5153                                                        if( fieldValue ) {
    52                                                                 if( fieldValue instanceof Collection )
     54                                                                if( fieldValue instanceof Collection ) {
    5355                                                                        fieldValue = fieldValue.collect { it.toString() }.findAll { it }.join( ', ' );
    54                                                                 else
     56                                                                } else {
    5557                                                                        fieldValue = fieldValue.toString();
     58                                                                }
     59                                                        } else {
     60                                                                fieldValue = "";
    5661                                                        }
    5762                                                %>
  • trunk/grails-app/views/advancedQuery/sampleresults.gsp

    r1482 r1501  
    55        <title>Query results</title>
    66        <link rel="stylesheet" href="<g:resource dir="css" file="advancedQuery.css" />" type="text/css"/>
     7        <g:javascript src="advancedQueryResults.js" />
    78</head>
    89<body>
     
    2627                <thead>
    2728                <tr>
     29                        <th class="nonsortable"></th>                   
     30                        <th>Name</th>
    2831                        <th>Study</th>
    29                         <th>Name</th>
    3032                        <g:each in="${extraFields}" var="fieldName">
    3133                                <th>${fieldName}</th>
     
    3638                <g:each in="${search.getResults()}" var="sampleInstance" status="i">
    3739                        <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
     40                                <td width="3%">
     41                                        <% /*
     42                                                The value of this checkbox will be moved to the form (under this table) with javascript. This
     43                                                way the user can select items from multiple pages of the paginated result list correctly. See
     44                                                also http://datatables.net/examples/api/form.html and advancedQueryResults.js
     45                                        */ %>
     46                                        <g:checkBox name="id" value="${sampleInstance.id}" checked="${false}" />
     47                                </td>
     48                                <td>${fieldValue(bean: sampleInstance, field: "name")}</td>
    3849                                <td><g:link controller="study" action="show" id="${sampleInstance?.parent?.id}">${sampleInstance?.parent?.title}</g:link></td>
    39                                 <td>${fieldValue(bean: sampleInstance, field: "name")}</td>
    4050                                <g:each in="${extraFields}" var="fieldName">
    4151                                        <td>
     
    4353                                                        def fieldValue = resultFields[ sampleInstance.id ]?.get( fieldName );
    4454                                                        if( fieldValue ) {
    45                                                                 if( fieldValue instanceof Collection )
     55                                                                if( fieldValue instanceof Collection ) {
    4656                                                                        fieldValue = fieldValue.collect { it.toString() }.findAll { it }.join( ', ' );
    47                                                                 else
     57                                                                } else {
    4858                                                                        fieldValue = fieldValue.toString();
     59                                                                }
     60                                                        } else {
     61                                                                fieldValue = "";
    4962                                                        }
    5063                                                %>
     
    5669                </tbody>
    5770        </table>
     71        <g:render template="resultsform" />
    5872
    5973</g:if>
  • trunk/grails-app/views/advancedQuery/studyresults.gsp

    r1482 r1501  
    55        <title>Query results</title>
    66        <link rel="stylesheet" href="<g:resource dir="css" file="advancedQuery.css" />" type="text/css"/>
     7        <g:javascript src="advancedQueryResults.js" />
    78</head>
    89<body>
     
    2223                def extraFields = resultFields[ search.getResults()[ 0 ].id ]?.keySet();
    2324        %>
    24 
    2525        <table id="searchresults" class="paginate">
    2626                <thead>
    2727                <tr>
    28                         <th colspan="2"></th>
     28                        <th class="nonsortable"></th>
     29                        <th>Title</th>
    2930                        <th>Code</th>
    30                         <th>Title</th>
    3131                        <th>Subjects</th>
    3232                        <th>Events</th>
     
    4040                <g:each in="${search.getResults()}" var="studyInstance" status="i">
    4141                        <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
    42 
    43                                 <td><g:link controller="study" action="show" id="${studyInstance?.id}"><img src='${fam.icon(name: 'application_form_magnify')}' border="0" alt="view study" /></g:link></td>
    44                                 <td><g:if test="${studyInstance.canWrite(loggedInUser)}"><g:link class="edit" controller="studyWizard" params="[jump:'edit']" id="${studyInstance?.id}"><img src='${fam.icon(name: 'application_form_edit')}' border="0" alt="edit study" /></g:link></g:if><g:else><img src='${fam.icon(name: 'lock')}' border="0" alt="you have no write access to shis study" /></g:else> </td>
     42                                <td width="3%">
     43                                        <% /*
     44                                                The value of this checkbox will be moved to the form (under this table) with javascript. This
     45                                                way the user can select items from multiple pages of the paginated result list correctly. See
     46                                                also http://datatables.net/examples/api/form.html and advancedQueryResults.js
     47                                        */ %>
     48                                        <g:checkBox name="id" value="${studyInstance.id}" checked="${false}" />
     49                                </td>
     50                                <td>
     51                                        <g:link controller="study" action="show" id="${studyInstance?.id}">${fieldValue(bean: studyInstance, field: "title")}</g:link>
     52                                       
     53                                </td>
    4554                                <td>${fieldValue(bean: studyInstance, field: "code")}</td>
    46                                 <td>
    47                                         ${fieldValue(bean: studyInstance, field: "title")}
    48                                 </td>
    4955                                <td>
    5056                                        <g:if test="${studyInstance.subjects.species.size()==0}">
     
    8288                                                        def fieldValue = resultFields[ studyInstance.id ]?.get( fieldName );
    8389                                                        if( fieldValue ) {
    84                                                                 if( fieldValue instanceof Collection )
     90                                                                if( fieldValue instanceof Collection ) {
    8591                                                                        fieldValue = fieldValue.collect { it.toString() }.findAll { it }.join( ', ' );
    86                                                                 else
     92                                                                } else {
    8793                                                                        fieldValue = fieldValue.toString();
     94                                                                }
     95                                                        } else {
     96                                                                fieldValue = "";
    8897                                                        }
    8998                                                %>
     
    95104                </tbody>
    96105        </table>
     106        <g:render template="resultsform" />
    97107
    98108</g:if>
  • trunk/grails-app/views/layouts/main.gsp

    r1434 r1501  
    2121        <script type="text/javascript" src="${resource(dir: 'js', file: 'topnav.js')}"></script>
    2222        <g:if env="development"><script type="text/javascript" src="${resource(dir: 'js', file: 'development.js')}"></script></g:if>
     23
     24        <!--  Scripts for pagination using dataTables -->
     25        <link rel="stylesheet" href="${resource(dir: 'css/datatables', file: 'demo_table_jui.css')}"/>
     26        <script type="text/javascript" src="${resource(dir: 'js', file: 'jquery.dataTables.min.js')}"></script>
     27        <script type="text/javascript" src="${resource(dir: 'js', file: 'paginate.js')}"></script>
     28
    2329</head>
    2430<body>
  • trunk/src/groovy/dbnp/query/SampleSearch.groovy

    r1482 r1501  
    6262         */
    6363        @Override
    64         void execute() {
    65                 super.execute();
    66 
     64        void executeAnd() {
    6765                // If no criteria are found, return all samples
    6866                if( !criteria || criteria.size() == 0 ) {
     
    8078                        def studies = Study.findAll().findAll { it.canRead( this.user ) };
    8179
    82                         studies = filterOnStudyCriteria( studies );
     80                        studies = filterStudiesOnStudyCriteria( studies );
    8381
    8482                        if( studies.size() == 0 ) {
     
    116114
    117115        /**
    118          * Filters the given list of samples on the sample criteria
    119          * @param samples       Original list of samples
    120          * @return                      List with all samples that match the Sample-criteria
    121          */
    122         protected List filterOnStudyCriteria( List studies ) {
     116        * Searches for samples based on the given criteria. Only one of the criteria have to be satisfied and
     117        * criteria for the different entities are satisfied as follows:
     118        *
     119        *               Sample.title = 'abc'
     120        *                               Samples are returned from studies with title 'abc'
     121        *
     122        *               Subject.species = 'human'
     123        *                               Samples are returned from subjects with species = 'human'
     124        *
     125        *               Sample.name = 'sample 1'
     126        *                               Samples are returned with name = 'sample 1'
     127        *
     128        *               Event.startTime = '0s'
     129        *                               Samples are returned from subjects that have had an event with start time = '0s'
     130        *
     131        *               SamplingEvent.startTime = '0s'
     132        *                               Samples are returned that have originated from a sampling event with start time = '0s'
     133        *
     134        *               Assay.module = 'metagenomics'
     135        *                               Samples are returned that have been processed in an assay with module = metagenomics
     136        *
     137        * When searching for more than one criterion per entity, these are taken separately. Searching for
     138        *
     139        *               Subject.species = 'human'
     140        *               Subject.name = 'Jan'
     141        *
     142        *  will result in all samples from a human subject or a subject named 'Jan'. Samples from a mouse subject
     143        *  named 'Jan' or a human subject named 'Kees' will also satisfy the criteria.
     144        *
     145        */
     146   @Override
     147   void executeOr() {
     148           // We expect the sample criteria to be the most discriminative, and discard
     149           // the most samples. (e.g. by searching on sample title of sample type). For
     150           // that reason we first look through the list of studies. However, when the
     151           // user didn't enter any sample criteria, this will be an extra step, but doesn't
     152           // cost much time to process.
     153           def samples = []
     154           def allSamples = Sample.list().findAll { it.parent?.canRead( this.user ) }.toList();
     155
     156           // If no criteria are found, return all samples
     157           if( !criteria || criteria.size() == 0 ) {
     158                   results = allSamples
     159                   return;
     160           }
     161           
     162           samples = ( samples + filterSamplesOnStudyCriteria( allSamples - samples ) ).unique();
     163           samples = ( samples + filterOnSubjectCriteria( allSamples - samples ) ).unique();
     164           samples = ( samples + filterOnSampleCriteria( allSamples - samples ) ).unique();
     165           samples = ( samples + filterOnEventCriteria( allSamples - samples ) ).unique();
     166           samples = ( samples + filterOnSamplingEventCriteria( allSamples - samples ) ).unique();
     167           samples = ( samples + filterOnAssayCriteria( allSamples - samples ) ).unique();
     168           
     169           samples = ( samples + filterOnModuleCriteria( allSamples - samples ) ).unique();
     170           
     171           // Save matches
     172           results = samples;
     173   }
     174       
     175        /**
     176         * Filters the given list of studies on the study criteria
     177         * @param studies       Original list of studies
     178         * @return                      List with all samples that match the Study-criteria
     179         */
     180        protected List filterStudiesOnStudyCriteria( List studies ) {
    123181                return filterOnTemplateEntityCriteria(studies, "Study", { study, criterion -> return criterion.getFieldValue( study ) })
    124182        }
     183       
     184        /**
     185        * Filters the given list of samples on the sample criteria
     186        * @param samples        Original list of samples
     187        * @return                       List with all samples that match the Study-criteria
     188        */
     189   protected List filterSamplesOnStudyCriteria( List samples ) {
     190           return filterOnTemplateEntityCriteria(samples, "Study", { study, criterion ->
     191                   return criterion.getFieldValue( study.parent )
     192           })
     193   }
     194
    125195
    126196        /**
     
    180250                        return [];
    181251
    182                 if( getEntityCriteria( 'Assay' ).size() == 0 )
    183                         return samples
    184 
    185252                // There is no sample.assays property, so we have to look for assays another way: just find
    186253                // all assays that match the criteria
    187254                def criteria = getEntityCriteria( 'Assay' );
     255               
     256                if( getEntityCriteria( 'Assay' ).size() == 0 ) {
     257                        if( searchMode == SearchMode.and )
     258                                return samples
     259                        else if( searchMode == SearchMode.or )
     260                                return [];
     261                }
     262
    188263                def assays = filterEntityList( Assay.list(), criteria, { assay, criterion ->
    189264                        if( !assay )
    190265                                return false
    191266
    192                         return criterion.matchOne( assay );
     267                        return criterion.matchEntity( assay );
    193268                });
    194269
     
    197272                        return [];
    198273
    199                 // Save sample data for later use
    200                 saveResultFields( samples, criteria, { sample, criterion ->
    201                         def sampleAssays = Assay.findByStudy( sample.parent ).findAll { it.samples?.contains( sample ) };
    202                         if( sampleAssays && sampleAssays.size() > 0 )
    203                                 return sampleAssays.collect( criterion.getFieldValue( it ) )
    204                         else
    205                                 return null
    206                 });
    207 
    208274                // Now filter the samples on whether they are attached to the filtered assays
    209                 return samples.findAll { sample ->
     275                def matchingSamples = samples.findAll { sample ->
    210276                        if( !sample.parent )
    211277                                return false;
     
    222288                        return false;
    223289                }
     290               
     291                // Save sample data for later use
     292                println samples
     293                println "Find values for " + matchingSamples + " and " + criteria
     294                saveResultFields( matchingSamples, criteria, { sample, criterion ->
     295                        println "Find value for " + sample + " and " + criterion
     296                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
     297                        if( sampleAssays && sampleAssays.size() > 0 )
     298                                return sampleAssays.collect { criterion.getFieldValue( it ) }
     299                        else
     300                                return null
     301                });
     302       
     303                return matchingSamples;
    224304        }
    225305
  • trunk/src/groovy/dbnp/query/Search.groovy

    r1487 r1501  
    3030import org.dbnp.gdt.*
    3131
     32/**
     33* Available boolean operators for searches
     34* @author robert
     35*
     36*/
     37enum SearchMode {
     38   and, or
     39}
     40
    3241class Search {
    33         public String entity;
    3442        public SecUser user;
    3543        public Date executionDate;
    3644        public int id;  // Is only used when this query is saved in session
    3745
     46        public String entity;
     47        public SearchMode searchMode = SearchMode.and
     48       
    3849        protected List criteria;
    3950        protected List results;
     
    4253        public List getCriteria() { return criteria; }
    4354        public void setCriteria( List c ) { criteria = c; }
     55        public void addCriterion( Criterion c ) {
     56                if( criteria )
     57                        criteria << c;
     58                else
     59                        criteria = [c];
     60        }
    4461
    4562        public List getResults() { return results; }
    4663        public void setResults( List r ) { results = r; }
     64        public List filterResults( List selectedIds ) {
     65                if( !selectedIds || !results )
     66                        return results
     67                       
     68                return results.findAll {
     69                        selectedIds.contains( it.id )
     70                }
     71        }
     72       
    4773       
    4874        public Map getResultFields() { return resultFields; }
     
    83109
    84110        /**
    85          * Executes a search based on the given criteria. Should be filled in by
    86          * subclasses searching for a specific entity
     111         * Executes a search based on the given criteria.
    87112         */
    88113        public void execute() {
    89114                this.executionDate = new Date();
    90         }
     115               
     116                switch( searchMode ) {
     117                        case SearchMode.and:
     118                                executeAnd();
     119                                break;
     120                        case SearchMode.or:
     121                                executeOr();
     122                                break;
     123                }
     124        }
     125       
     126        /**
     127         * Executes an inclusive (AND) search based on the given criteria. Should be filled in by
     128         * subclasses searching for a specific entity
     129         */
     130        public void executeAnd() {
     131               
     132        }
     133       
     134        /**
     135        * Executes an exclusive (OR) search based on the given criteria. Should be filled in by
     136        * subclasses searching for a specific entity
     137        */
     138   public void executeOr() {
     139           
     140   }
    91141
    92142        /**
     
    112162        protected List filterEntityList( List entities, List<Criterion> criteria, Closure check ) {
    113163                if( !entities || !criteria || criteria.size() == 0 ) {
    114                         return entities;
    115                 }
    116 
     164                        if( searchMode == SearchMode.and )
     165                                return entities;
     166                        else if( searchMode == SearchMode.or )
     167                                return []
     168                }
     169               
    117170                return entities.findAll { entity ->
    118                         for( criterion in criteria ) {
    119                                 if( !check( entity, criterion ) ) {
    120                                         return false;
    121                                 }
     171                        if( searchMode == SearchMode.and ) {
     172                                for( criterion in criteria ) {
     173                                        if( !check( entity, criterion ) ) {
     174                                                return false;
     175                                        }
     176                                }
     177                                return true;
     178                        } else if( searchMode == SearchMode.or ) {
     179                                for( criterion in criteria ) {
     180                                        if( check( entity, criterion ) ) {
     181                                                return true;
     182                                        }
     183                                }
     184                                return false;
    122185                        }
    123                         return true;
    124186                }
    125187        }
     
    133195         */
    134196        public static def prepare( def value, TemplateFieldType type ) {
     197                if( value == null )
     198                        return value
     199                       
    135200                switch (type) {
    136201                        case TemplateFieldType.DATE:
     
    185250                }
    186251
    187         }
    188        
     252        }       
    189253       
    190254        /**
     
    201265                        def value = valueCallback( study, criterion );
    202266                       
    203                         if( value == null )
     267                        if( value == null ) {
    204268                                return false
     269                        }
     270
    205271                        if( value instanceof Collection ) {
    206272                                return criterion.matchAny( value )
     
    230296                def ctx = ApplicationHolder.getApplication().getMainContext();
    231297                def moduleCommunicationService = ctx.getBean("moduleCommunicationService");
    232                        
     298               
     299                def allEntities = []
     300                if( searchMode == SearchMode.or ) {
     301                        allEntities += entities;
     302                        entities = [];
     303                }
     304               
    233305                // Loop through all modules and check whether criteria have been given
    234306                // for that module
     
    250322                                        def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
    251323
    252                                         // The data has been retrieved. Now walk through all criteria to filter the samples
    253                                         entities = filterEntityList( entities, moduleCriteria, { entity, criterion ->
     324                                        Closure checkClosure = { entity, criterion ->
    254325                                                // Find the value of the field in this sample. That value is still in the
    255326                                                // JSON object
     
    268339                                                }
    269340                                               
    270                                                 // Loop through all values and match any
    271                                                 for( val in value ) {
    272                                                         // Convert numbers to a long or double in order to process them correctly
     341                                                // Convert numbers to a long or double in order to process them correctly
     342                                                def values = value.collect { val ->
    273343                                                        val = val.toString();
    274344                                                        if( val.isLong() ) {
     
    277347                                                                val = Double.parseDouble( val );
    278348                                                        }
    279                                                        
     349                                                        return val;
     350                                                }
     351
     352                                                // Loop through all values and match any
     353                                                for( val in values ) {
    280354                                                        if( criterion.match( val ) )
    281355                                                                return true;
     
    283357                                               
    284358                                                return false;
    285                                         });
     359                                        }
     360                                       
     361                                        // The data has been retrieved. Now walk through all criteria to filter the samples
     362                                        if( searchMode == SearchMode.and ) {
     363                                                entities = filterEntityList( entities, moduleCriteria, checkClosure );
     364                                        } else if( searchMode == SearchMode.or ) {
     365                                                entities += filterEntityList( allEntities - entities, moduleCriteria, checkClosure );
     366                                                entities = entities.unique();
     367                                        }
    286368                                                                               
    287369                                } catch( Exception e ) {
     
    321403                        resultFields[ id ] = [:]
    322404               
     405                // Handle special cases
     406                if( value == null )
     407                        value = "";
     408               
     409                if( value instanceof Collection ) {
     410                        value = value.findAll { it != null }
     411                }
     412               
    323413                resultFields[ id ][ fieldName ] = value;
    324414        }
  • trunk/src/groovy/dbnp/query/StudySearch.groovy

    r1487 r1501  
    2424class StudySearch extends Search {
    2525        private static final log = LogFactory.getLog(this);
    26        
     26
    2727        public StudySearch() {
    2828                super();
     
    6363         */
    6464        @Override
    65         void execute() {
    66                 super.execute();
    67 
     65        void executeAnd() {
    6866                def studies = Study.list().findAll { it.canRead( this.user ) };
    6967
     
    8179                studies = filterOnSamplingEventCriteria( studies );
    8280                studies = filterOnAssayCriteria( studies );
     81
     82                studies = filterOnModuleCriteria( studies );
     83
     84                // Save matches
     85                results = studies;
     86        }
     87
     88        /**
     89         * Searches for studies based on the given criteria. Only one criteria have to be satisfied and
     90         * criteria for the different entities are satisfied as follows:
     91         *
     92         *              Study.title = 'abc'
     93         *                              The returned study will have title 'abc'
     94         *
     95         *              Subject.species = 'human'
     96         *                              The returned study will have one or more subjects with species = 'human'
     97         *
     98         *              Sample.name = 'sample 1'
     99         *                              The returned study will have one or more samples with name = 'sample 1'
     100         *
     101         *              Event.startTime = '0s'
     102         *                              The returned study will have one or more events with start time = '0s'
     103         *
     104         *              Assay.module = 'metagenomics'
     105         *                              The returned study will have one or more assays with module = 'metagenomics'
     106         *
     107         * When searching the system doesn't look at the connections between different entities. This means,
     108         * the system doesn't look for human subjects having a sample with name 'sample 1'. The sample 1 might
     109         * as well belong to a mouse subject and still the study satisfies the criteria.
     110         *
     111         * When searching for more than one criterion per entity, these are taken separately. Searching for
     112         *
     113         *              Subject.species = 'human'
     114         *              Subject.name = 'Jan'
     115         *
     116         *  will result in all studies having a human subject or a subject named 'Jan'. Studies with only a
     117         *  mouse subject named 'Jan' or a human subject named 'Kees' will satisfy the criteria.
     118         *
     119         */
     120        @Override
     121        void executeOr() {
     122                def allStudies = Study.list().findAll { it.canRead( this.user ) };
     123
     124                // If no criteria are found, return all studies
     125                if( !criteria || criteria.size() == 0 ) {
     126                        results = allStudies;
     127                        return;
     128                }
     129
     130                // Perform filters
     131                def studies = []
     132                studies = ( studies + filterOnStudyCriteria( allStudies - studies ) ).unique();
     133                studies = ( studies + filterOnSubjectCriteria( allStudies - studies ) ).unique();
     134                studies = ( studies + filterOnSampleCriteria( allStudies - studies ) ).unique();
     135                studies = ( studies + filterOnEventCriteria( allStudies - studies ) ).unique();
     136                studies = ( studies + filterOnSamplingEventCriteria( allStudies - studies ) ).unique();
     137                studies = ( studies + filterOnAssayCriteria( allStudies - studies ) ).unique();
    83138               
    84                 studies = filterOnModuleCriteria( studies );
     139                studies = ( studies + filterOnModuleCriteria( allStudies - studies ) ).unique();
    85140               
    86141                // Save matches
  • trunk/web-app/css/advancedQuery.css

    r1482 r1501  
    2323
    2424.ui-menu-item .entity { color: #666; font-style: italic; }
     25
     26.options { float: left; width: 300px; }
     27.options a {
     28        font-size: 10px;
     29        font-weight: bold;
     30        margin-left: 3px;
     31        margin-right: 3px;
     32        padding-top: 2px;
     33        padding-bottom: 2px;
     34        line-height: 20px;
     35        padding-left: 28px;     
     36}
     37
     38.options a.performAction {
     39        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/brick_go.png) 5px 50% no-repeat;
     40}
     41.options a.excel {
     42        background-image: url(../plugins/famfamfam-1.0.1/images/icons/page_excel.png);
     43}
     44.options a.searchIn {
     45        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/arrow_branch.png) 5px 50% no-repeat;
     46}
     47.options a.search {
     48        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/arrow_undo.png) 5px 50% no-repeat;
     49}
     50.options a.discard {
     51        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/basket_remove.png) 5px 50% no-repeat;
     52}
     53.options a.listPrevious {
     54        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/basket.png) 5px 50% no-repeat;
     55}
     56.options a.combine {
     57        background: transparent url(../plugins/famfamfam-1.0.1/images/icons/arrow_join.png) 5px 50% no-repeat;
     58}
Note: See TracChangeset for help on using the changeset viewer.