root/galaxy-central/lib/galaxy/web/framework/helpers/grids.py

リビジョン 2, 36.9 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

行番号 
1from galaxy.model.orm import *
2from galaxy.web.base.controller import *
3from galaxy.web.framework.helpers import iff
4from galaxy.web import url_for
5from galaxy.util.json import from_json_string, to_json_string
6from galaxy.util.odict import odict
7from galaxy.web.framework.helpers import to_unicode
8from galaxy.model.item_attrs import *
9
10import sys, logging, math
11
12log = logging.getLogger( __name__ )
13
14class Grid( object ):
15    """
16    Specifies the content and format of a grid (data table).
17    """
18    webapp = None
19    title = ""
20    exposed = True
21    model_class = None
22    template = "grid_base.mako"
23    async_template = "grid_base_async.mako"
24    use_async = False
25    global_actions = []
26    columns = []
27    operations = []
28    standard_filters = []
29    # Any columns that are filterable (either standard or advanced) should have a default value set in the default filter.
30    default_filter = {}
31    default_sort_key = None
32    preserve_state = False
33    use_paging = False
34    num_rows_per_page = 25
35    # Set preference names.
36    cur_filter_pref_name = ".filter"
37    cur_sort_key_pref_name = ".sort_key"   
38    pass_through_operations = {}
39    def __init__( self ):
40        # Determine if any multiple row operations are defined
41        self.has_multiple_item_operations = False
42        for operation in self.operations:
43            if operation.allow_multiple:
44                self.has_multiple_item_operations = True
45                break
46               
47        # If a column does not have a model class, set the column's model class
48        # to be the grid's model class.
49        for column in self.columns:
50            if not column.model_class:
51                column.model_class = self.model_class
52       
53    def __call__( self, trans, **kwargs ):
54        # Get basics.
55        webapp = kwargs.get( 'webapp', 'galaxy' )
56        status = kwargs.get( 'status', None )
57        message = kwargs.get( 'message', None )
58        # Build a base filter and sort key that is the combination of the saved state and defaults.
59        # Saved state takes preference over defaults.
60        base_filter = {}
61        if self.default_filter:
62            # default_filter is a dictionary that provides a default set of filters based on the grid's columns.
63            base_filter = self.default_filter.copy()
64        base_sort_key = self.default_sort_key
65        if self.preserve_state:
66            pref_name = unicode( self.__class__.__name__ + self.cur_filter_pref_name )
67            if pref_name in trans.get_user().preferences:
68                saved_filter = from_json_string( trans.get_user().preferences[pref_name] )
69                base_filter.update( saved_filter )
70            pref_name = unicode( self.__class__.__name__ + self.cur_sort_key_pref_name )
71            if pref_name in trans.get_user().preferences:
72                base_sort_key = from_json_string( trans.get_user().preferences[pref_name] )
73        # Build initial query
74        query = self.build_initial_query( trans, **kwargs )
75        query = self.apply_query_filter( trans, query, **kwargs )
76        # Maintain sort state in generated urls
77        extra_url_args = {}
78        # Determine whether use_default_filter flag is set.
79        use_default_filter_str = kwargs.get( 'use_default_filter' )
80        use_default_filter = False
81        if use_default_filter_str:
82            use_default_filter = ( use_default_filter_str.lower() == 'true' )
83        # Process filtering arguments to (a) build a query that represents the filter and (b) build a
84        # dictionary that denotes the current filter.
85        cur_filter_dict = {}
86        for column in self.columns:
87            if column.key:
88                # Get the filter criterion for the column. Precedence is (a) if using default filter, only look there; otherwise,
89                # (b) look in kwargs; and (c) look in base filter.
90                column_filter = None
91                if use_default_filter:
92                    if self.default_filter:
93                        column_filter = self.default_filter.get( column.key )
94                elif "f-" + column.model_class.__name__ + ".%s" % column.key in kwargs:
95                    # Queries that include table joins cannot guarantee unique column names.  This problem is
96                    # handled by setting the column_filter value to <TableName>.<ColumnName>.
97                    column_filter = kwargs.get( "f-" + column.model_class.__name__ + ".%s" % column.key )
98                elif "f-" + column.key in kwargs:                   
99                    column_filter = kwargs.get( "f-" + column.key )
100                elif column.key in base_filter:
101                    column_filter = base_filter.get( column.key )
102                # Method (1) combines a mix of strings and lists of strings into a single string and (2) attempts to de-jsonify all strings.
103                def from_json_string_recurse(item):
104                    decoded_list = []
105                    if isinstance( item, basestring):
106                        try:
107                            # Not clear what we're decoding, so recurse to ensure that we catch everything.
108                             decoded_item = from_json_string( item )
109                             if isinstance( decoded_item, list):
110                                 decoded_list = from_json_string_recurse( decoded_item )
111                             else:
112                                 decoded_list = [ unicode( decoded_item ) ]
113                        except ValueError:
114                            decoded_list = [ unicode ( item ) ]
115                    elif isinstance( item, list):
116                        return_val = []
117                        for element in item:
118                            a_list = from_json_string_recurse( element )
119                            decoded_list = decoded_list + a_list
120                    return decoded_list
121                # If column filter found, apply it.
122                if column_filter is not None:
123                    # TextColumns may have a mix of json and strings.
124                    if isinstance( column, TextColumn ):
125                        column_filter = from_json_string_recurse( column_filter )
126                        if len( column_filter ) == 1:
127                            column_filter = column_filter[0]
128                    # Interpret ',' as a separator for multiple terms.
129                    if isinstance( column_filter, basestring ) and column_filter.find(',') != -1:
130                        column_filter = column_filter.split(',')
131                    # If filter criterion is empty, do nothing.
132                    if column_filter == '':
133                        continue
134                    # Update query.
135                    query = column.filter( trans, trans.user, query, column_filter )
136                    # Upate current filter dict.
137                    cur_filter_dict[ column.key ] = column_filter
138                    # Carry filter along to newly generated urls; make sure filter is a string so
139                    # that we can encode to UTF-8 and thus handle user input to filters.
140                    if isinstance( column_filter, list ):
141                        # Filter is a list; process each item.
142                        for filter in column_filter:
143                            if not isinstance( filter, basestring ):
144                                filter = unicode( filter ).encode("utf-8")
145                        extra_url_args[ "f-" + column.key ] = to_json_string( column_filter )
146                    else:
147                        # Process singleton filter.
148                        if not isinstance( column_filter, basestring ):
149                            column_filter = unicode(column_filter)
150                        extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8")
151        # Process sort arguments.
152        sort_key = None
153        if 'sort' in kwargs:
154            sort_key = kwargs['sort']
155        elif base_sort_key:
156            sort_key = base_sort_key
157        if sort_key:
158            ascending = not( sort_key.startswith( "-" ) )
159            # Queries that include table joins cannot guarantee unique column names.  This problem is
160            # handled by setting the column_filter value to <TableName>.<ColumnName>.
161            table_name = None
162            if sort_key.find( '.' ) > -1:
163                a_list = sort_key.split( '.' )
164                if ascending:
165                    table_name = a_list[0]
166                else:
167                    table_name = a_list[0][1:]
168                column_name = a_list[1]
169            elif ascending:
170                column_name = sort_key
171            else:
172                column_name = sort_key[1:]
173            # Sort key is a column key.
174            for column in self.columns:
175                if column.key and column.key.find( '.' ) > -1:
176                    column_key = column.key.split( '.' )[1]
177                else:
178                    column_key = column.key
179                if ( table_name is None or table_name == column.model_class.__name__ ) and column_key == column_name:
180                    query = column.sort( trans, query, ascending, column_name=column_name )
181                    break
182            extra_url_args['sort'] = sort_key
183        # There might be a current row
184        current_item = self.get_current_item( trans, **kwargs )
185        # Process page number.
186        if self.use_paging:
187            if 'page' in kwargs:
188                if kwargs['page'] == 'all':
189                    page_num = 0
190                else:
191                    page_num = int( kwargs['page'] )
192            else:
193                page_num = 1
194               
195            if page_num == 0:
196                # Show all rows in page.
197                total_num_rows = query.count()
198                page_num = 1
199                num_pages = 1
200            else:
201                # Show a limited number of rows. Before modifying query, get the total number of rows that query
202                # returns so that the total number of pages can be computed.
203                total_num_rows = query.count()
204                query = query.limit( self.num_rows_per_page ).offset( ( page_num-1 ) * self.num_rows_per_page )
205                num_pages = int ( math.ceil( float( total_num_rows ) / self.num_rows_per_page ) )
206        else:
207            # Defaults.
208            page_num = 1
209            num_pages = 1
210        # Preserve grid state: save current filter and sort key.
211        if self.preserve_state:
212            pref_name = unicode( self.__class__.__name__ + self.cur_filter_pref_name )
213            trans.get_user().preferences[pref_name] = unicode( to_json_string( cur_filter_dict ) )
214            if sort_key:
215                pref_name = unicode( self.__class__.__name__ + self.cur_sort_key_pref_name )
216                trans.get_user().preferences[pref_name] = unicode( to_json_string( sort_key ) )
217            trans.sa_session.flush()
218        # Log grid view.
219        context = unicode( self.__class__.__name__ )
220        params = cur_filter_dict.copy()
221        params['sort'] = sort_key
222        params['async'] = ( 'async' in kwargs )
223        params['webapp'] = webapp
224        trans.log_action( trans.get_user(), unicode( "grid.view" ), context, params )
225        # Render grid.
226        def url( *args, **kwargs ):
227            # Only include sort/filter arguments if not linking to another
228            # page. This is a bit of a hack.
229            if 'action' in kwargs:
230                new_kwargs = dict()
231            else:
232                new_kwargs = dict( extra_url_args )
233            # Extend new_kwargs with first argument if found
234            if len(args) > 0:
235                new_kwargs.update( args[0] )
236            new_kwargs.update( kwargs )
237            # We need to encode item ids
238            if 'id' in new_kwargs:
239                id = new_kwargs[ 'id' ]
240                if isinstance( id, list ):
241                    new_args[ 'id' ] = [ trans.security.encode_id( i ) for i in id ]
242                else:
243                    new_kwargs[ 'id' ] = trans.security.encode_id( id )
244            return url_for( **new_kwargs )
245        use_panels = ( kwargs.get( 'use_panels', False ) in [ True, 'True', 'true' ] )
246        async_request = ( ( self.use_async ) and ( kwargs.get( 'async', False ) in [ True, 'True', 'true'] ) )
247        # Currently, filling the template returns a str object; this requires decoding the string into a
248        # unicode object within mako templates. What probably should be done is to return the template as
249        # utf-8 unicode; however, this would require encoding the object as utf-8 before returning the grid
250        # results via a controller method, which is require substantial changes. Hence, for now, return grid
251        # as str.
252        return trans.fill_template( iff( async_request, self.async_template, self.template ),
253                                    grid=self,
254                                    query=query,
255                                    cur_page_num = page_num,
256                                    num_pages = num_pages,
257                                    default_filter_dict=self.default_filter,
258                                    cur_filter_dict=cur_filter_dict,
259                                    sort_key=sort_key,
260                                    current_item=current_item,
261                                    ids = kwargs.get( 'id', [] ),
262                                    url = url,
263                                    status = status,
264                                    message = message,
265                                    use_panels=use_panels,
266                                    webapp=webapp,
267                                    # Pass back kwargs so that grid template can set and use args without
268                                    # grid explicitly having to pass them.
269                                    kwargs=kwargs )
270    def get_ids( self, **kwargs ):
271        id = []
272        if 'id' in kwargs:
273            id = kwargs['id']
274            # Coerce ids to list
275            if not isinstance( id, list ):
276                id = id.split( "," )
277            # Ensure ids are integers
278            try:
279                id = map( int, id )
280            except:
281                error( "Invalid id" )
282        return id
283    # ---- Override these ----------------------------------------------------
284    def handle_operation( self, trans, operation, ids, **kwargs ):
285        pass
286    def get_current_item( self, trans, **kwargs ):
287        return None
288    def build_initial_query( self, trans, **kwargs ):
289        return trans.sa_session.query( self.model_class )
290    def apply_query_filter( self, trans, query, **kwargs ):
291        # Applies a database filter that holds for all items in the grid.
292        # (gvk) Is this method necessary?  Why not simply build the entire query,
293        # including applying filters in the build_initial_query() method?
294        return query
295   
296class GridColumn( object ):
297    def __init__( self, label, key=None, model_class=None, method=None, format=None, \
298                  link=None, attach_popup=False, visible=True, ncells=1, \
299                  # Valid values for filterable are ['standard', 'advanced', None]
300                  filterable=None, sortable=True ):
301        """Create a grid column."""
302        self.label = label
303        self.key = key
304        self.model_class = model_class
305        self.method = method
306        self.format = format
307        self.link = link
308        self.attach_popup = attach_popup
309        self.visible = visible
310        self.ncells = ncells
311        self.filterable = filterable
312        # Column must have a key to be sortable.
313        self.sortable = ( self.key is not None and sortable )
314    def get_value( self, trans, grid, item ):
315        if self.method:
316            value = getattr( grid, self.method )( trans, item )
317        elif self.key:
318            value = getattr( item, self.key )
319        else:
320            value = None
321        if self.format:
322            value = self.format( value )
323        return value
324    def get_link( self, trans, grid, item ):
325        if self.link and self.link( item ):
326            return self.link( item )
327        return None
328    def filter( self, trans, user, query, column_filter ):
329        """ Modify query to reflect the column filter. """
330        if column_filter == "All":
331            pass
332        if column_filter == "True":
333            query = query.filter_by( **{ self.key: True } )
334        elif column_filter == "False":
335            query = query.filter_by( **{ self.key: False } )
336        return query
337    def get_accepted_filters( self ):
338        """ Returns a list of accepted filters for this column. """
339        accepted_filters_vals = [ "False", "True", "All" ]
340        accepted_filters = []
341        for val in accepted_filters_vals:
342            args = { self.key: val }
343            accepted_filters.append( GridColumnFilter( val, args) )
344        return accepted_filters
345    def sort( self, trans, query, ascending, column_name=None ):
346        """Sort query using this column."""
347        if column_name is None:
348            column_name = self.key
349        if ascending:
350            query = query.order_by( self.model_class.table.c.get( column_name ).asc() )
351        else:
352            query = query.order_by( self.model_class.table.c.get( column_name ).desc() )
353        return query
354       
355class ReverseSortColumn( GridColumn ):
356    """ Column that reverses sorting; this is useful when the natural sort is descending. """
357    def sort( self, trans, query, ascending, column_name=None ):
358        return GridColumn.sort( self, trans, query, (not ascending), column_name=column_name )
359       
360class TextColumn( GridColumn ):
361    """ Generic column that employs freetext and, hence, supports freetext, case-independent filtering. """
362    def filter( self, trans, user, query, column_filter ):
363        """ Modify query to filter using free text, case independence. """
364        if column_filter == "All":
365            pass
366        elif column_filter:
367            query = query.filter( self.get_filter( trans, user, column_filter ) )
368        return query
369    def get_filter( self, trans, user, column_filter ):
370        """ Returns a SQLAlchemy criterion derived from column_filter. """
371        if isinstance( column_filter, basestring ):
372            return self.get_single_filter( user, column_filter )
373        elif isinstance( column_filter, list ):
374            clause_list = []
375            for filter in column_filter:
376                clause_list.append( self.get_single_filter( user, filter ) )
377            return and_( *clause_list )
378    def get_single_filter( self, user, a_filter ):
379        """
380        Returns a SQLAlchemy criterion derived for a single filter. Single filter
381        is the most basic filter--usually a string--and cannot be a list.
382        """
383        # Queries that include table joins cannot guarantee that table column names will be
384        # unique, so check to see if a_filter is of type <TableName>.<ColumnName>.
385        if self.key.find( '.' ) > -1:
386            a_key = self.key.split( '.' )[1]
387        else:
388            a_key = self.key
389        model_class_key_field = getattr( self.model_class, a_key )
390        return func.lower( model_class_key_field ).like( "%" + a_filter.lower() + "%" )
391    def sort( self, trans, query, ascending, column_name=None ):
392        """Sort column using case-insensitive alphabetical sorting."""
393        if column_name is None:
394            column_name = self.key
395        if ascending:
396            query = query.order_by( func.lower( self.model_class.table.c.get( column_name ) ).asc() )
397        else:
398            query = query.order_by( func.lower( self.model_class.table.c.get( column_name ) ).desc() )
399        return query
400
401class DateTimeColumn( TextColumn ):
402    def sort( self, trans, query, ascending, column_name=None ):
403        """Sort query using this column."""
404        return GridColumn.sort( self, trans, query, ascending, column_name=column_name )
405
406class BooleanColumn( TextColumn ):
407    def sort( self, trans, query, ascending, column_name=None ):
408        """Sort query using this column."""
409        return GridColumn.sort( self, trans, query, ascending, column_name=column_name )
410
411class IntegerColumn( TextColumn ):
412    """
413    Integer column that employs freetext, but checks that the text is an integer,
414    so support filtering on integer values.
415   
416    IMPORTANT NOTE: grids that use this column type should not include the column
417    in the cols_to_filter list of MulticolFilterColumn ( i.e., searching on this
418    column type should not be performed in the grid's standard search - it won't
419    throw exceptions, but it also will not find what you're looking for ).  Grids
420    that search on this column should use 'filterable="advanced"' so that searching
421    is only performed in the advanced search component, restricting the search to
422    the specific column.
423   
424    This is useful for searching on object ids or other integer columns.  See the
425    JobIdColumn column in the SpecifiedDateListGrid class in the jobs controller of
426    the reports webapp for an example.
427    """
428    def get_single_filter( self, user, a_filter ):
429        model_class_key_field = getattr( self.model_class, self.key )
430        assert int( a_filter ), "The search entry must be an integer"
431        return model_class_key_field == int( a_filter )
432    def sort( self, trans, query, ascending, column_name=None ):
433        """Sort query using this column."""
434        return GridColumn.sort( self, trans, query, ascending, column_name=column_name )
435       
436class CommunityRatingColumn( GridColumn, UsesItemRatings ):
437    """ Column that displays community ratings for an item. """
438    def get_value( self, trans, grid, item ):
439        ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, item, webapp_model=trans.model )
440        return trans.fill_template( "community_rating.mako",
441                                    ave_item_rating=ave_item_rating,
442                                    num_ratings=num_ratings,
443                                    item_id=trans.security.encode_id( item.id ) )
444    def sort( self, trans, query, ascending, column_name=None ):
445        def get_foreign_key( source_class, target_class ):
446            """ Returns foreign key in source class that references target class. """
447            target_fk = None
448            for fk in source_class.table.foreign_keys:
449                if fk.references( target_class.table ):
450                    target_fk = fk
451                    break
452            if not target_fk:
453                raise RuntimeException( "No foreign key found between objects: %s, %s" % source_class.table, target_class.table )
454            return target_fk
455        # Get the columns that connect item's table and item's rating association table.
456        item_rating_assoc_class = getattr( trans.model, '%sRatingAssociation' % self.model_class.__name__ )
457        foreign_key = get_foreign_key( item_rating_assoc_class, self.model_class )
458        fk_col = foreign_key.parent
459        referent_col = foreign_key.get_referent( self.model_class.table )
460        # Do sorting using a subquery.
461        # Subquery to get average rating for each item.
462        ave_rating_subquery = trans.sa_session.query( fk_col, \
463                                                      func.avg( item_rating_assoc_class.table.c.rating ).label('avg_rating') ) \
464                                              .group_by( fk_col ) \
465                                              .subquery()
466        # Integrate subquery into main query.
467        query = query.outerjoin( (ave_rating_subquery, referent_col==ave_rating_subquery.columns[fk_col.name]) )
468        # Sort using subquery results; use coalesce to avoid null values.
469        if not ascending: # TODO: for now, reverse sorting b/c first sort is ascending, and that should be the natural sort.
470            query = query.order_by( func.coalesce( ave_rating_subquery.c.avg_rating, 0 ).asc() )
471        else:
472            query = query.order_by( func.coalesce( ave_rating_subquery.c.avg_rating, 0 ).desc() )
473        return query
474
475class OwnerAnnotationColumn( TextColumn, UsesAnnotations ):
476    """ Column that displays and filters item owner's annotations. """
477    def __init__( self, col_name, key, model_class=None, model_annotation_association_class=None, filterable=None ):
478        GridColumn.__init__( self, col_name, key=key, model_class=model_class, filterable=filterable )
479        self.sortable = False
480        self.model_annotation_association_class = model_annotation_association_class
481    def get_value( self, trans, grid, item ):
482        """ Returns first 150 characters of annotation. """
483        annotation = self.get_item_annotation_str( trans.sa_session, item.user, item )
484        if annotation:
485            ann_snippet = annotation[:155]
486            if len( annotation ) > 155:
487                ann_snippet = ann_snippet[ :ann_snippet.rfind(' ') ]
488                ann_snippet += "..."
489        else:
490            ann_snippet = ""
491        return ann_snippet
492    def get_single_filter( self, user, a_filter ):
493        """ Filter by annotation and annotation owner. """
494        return self.model_class.annotations.any(
495            and_( func.lower( self.model_annotation_association_class.annotation ).like( "%" + a_filter.lower() + "%" ),
496                # TODO: not sure why, to filter by owner's annotations, we have to do this rather than
497                # 'self.model_class.user==self.model_annotation_association_class.user'
498                self.model_annotation_association_class.table.c.user_id==self.model_class.table.c.user_id ) )           
499                       
500class CommunityTagsColumn( TextColumn ):
501    """ Column that supports community tags. """
502    def __init__( self, col_name, key, model_class=None, model_tag_association_class=None, filterable=None, grid_name=None ):
503        GridColumn.__init__( self, col_name, key=key, model_class=model_class, filterable=filterable, sortable=False )
504        self.model_tag_association_class = model_tag_association_class
505        # Column-specific attributes.
506        self.grid_name = grid_name
507    def get_value( self, trans, grid, item ):
508        return trans.fill_template( "/tagging_common.mako", tag_type="community", trans=trans, user=trans.get_user(), tagged_item=item, elt_context=self.grid_name,
509                                    in_form=True, input_size="20", tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True )
510    def filter( self, trans, user, query, column_filter ):
511        """ Modify query to filter model_class by tag. Multiple filters are ANDed. """
512        if column_filter == "All":
513            pass
514        elif column_filter:
515            query = query.filter( self.get_filter( trans, user, column_filter ) )
516        return query
517    def get_filter( self, trans, user, column_filter ):
518            # Parse filter to extract multiple tags.
519            if isinstance( column_filter, list ):
520                # Collapse list of tags into a single string; this is redundant but effective. TODO: fix this by iterating over tags.
521                column_filter = ",".join( column_filter )
522            raw_tags = trans.app.tag_handler.parse_tags( column_filter.encode( "utf-8" ) )
523            clause_list = []
524            for name, value in raw_tags.items():
525                if name:
526                    # Filter by all tags.
527                    clause_list.append( self.model_class.tags.any( func.lower( self.model_tag_association_class.user_tname ).like( "%" + name.lower() + "%" ) ) )
528                    if value:
529                        # Filter by all values.
530                        clause_list.append( self.model_class.tags.any( func.lower( self.model_tag_association_class.user_value ).like( "%" + value.lower() + "%" ) ) )
531            return and_( *clause_list )
532           
533class IndividualTagsColumn( CommunityTagsColumn ):
534    """ Column that supports individual tags. """
535    def get_value( self, trans, grid, item ):
536        return trans.fill_template( "/tagging_common.mako",
537                                    tag_type="individual",
538                                    user=trans.user,
539                                    tagged_item=item,
540                                    elt_context=self.grid_name,
541                                    in_form=True,
542                                    input_size="20",
543                                    tag_click_fn="add_tag_to_grid_filter",
544                                    use_toggle_link=True )
545    def get_filter( self, trans, user, column_filter ):
546            # Parse filter to extract multiple tags.
547            if isinstance( column_filter, list ):
548                # Collapse list of tags into a single string; this is redundant but effective. TODO: fix this by iterating over tags.
549                column_filter = ",".join( column_filter )
550            raw_tags = trans.app.tag_handler.parse_tags( column_filter.encode( "utf-8" ) )
551            clause_list = []
552            for name, value in raw_tags.items():
553                if name:
554                    # Filter by individual's tag names.
555                    clause_list.append( self.model_class.tags.any( and_( func.lower( self.model_tag_association_class.user_tname ).like( "%" + name.lower() + "%" ), self.model_tag_association_class.user == user ) ) )
556                    if value:
557                        # Filter by individual's tag values.
558                        clause_list.append( self.model_class.tags.any( and_( func.lower( self.model_tag_association_class.user_value ).like( "%" + value.lower() + "%" ), self.model_tag_association_class.user == user ) ) )
559            return and_( *clause_list )
560           
561class MulticolFilterColumn( TextColumn ):
562    """ Column that performs multicolumn filtering. """
563    def __init__( self, col_name, cols_to_filter, key, visible, filterable="default" ):
564        GridColumn.__init__( self, col_name, key=key, visible=visible, filterable=filterable)
565        self.cols_to_filter = cols_to_filter
566    def filter( self, trans, user, query, column_filter ):
567        """ Modify query to filter model_class by tag. Multiple filters are ANDed. """
568        if column_filter == "All":
569            return query
570        if isinstance( column_filter, list):
571            clause_list = []
572            for filter in column_filter:
573                part_clause_list = []
574                for column in self.cols_to_filter:
575                    part_clause_list.append( column.get_filter( trans, user, filter ) )
576                clause_list.append( or_( *part_clause_list ) )
577            complete_filter = and_( *clause_list )
578        else:
579            clause_list = []
580            for column in self.cols_to_filter:
581                clause_list.append( column.get_filter( trans, user, column_filter ) )
582            complete_filter = or_( *clause_list )
583        return query.filter( complete_filter )
584               
585class OwnerColumn( TextColumn ):
586    """ Column that lists item's owner. """
587    def get_value( self, trans, grid, item ):
588        return item.user.username
589    def sort( self, trans, query, ascending, column_name=None ):
590        """ Sort column using case-insensitive alphabetical sorting on item's username. """
591        if ascending:
592            query = query.order_by( func.lower ( self.model_class.username ).asc() )
593        else:
594            query = query.order_by( func.lower( self.model_class.username ).desc() )
595        return query
596
597class PublicURLColumn( TextColumn ):
598    """ Column displays item's public URL based on username and slug. """
599    def get_link( self, trans, grid, item ):
600        if item.user.username and item.slug:
601            return dict( action='display_by_username_and_slug', username=item.user.username, slug=item.slug )
602        elif not item.user.username:
603            # TODO: provide link to set username.
604            return None
605        elif not item.user.slug:
606            # TODO: provide link to set slug.
607            return None
608
609class DeletedColumn( GridColumn ):
610    """ Column that tracks and filters for items with deleted attribute. """
611    def get_accepted_filters( self ):
612        """ Returns a list of accepted filters for this column. """
613        accepted_filter_labels_and_vals = { "active" : "False", "deleted" : "True", "all": "All" }
614        accepted_filters = []
615        for label, val in accepted_filter_labels_and_vals.items():
616           args = { self.key: val }
617           accepted_filters.append( GridColumnFilter( label, args) )
618        return accepted_filters
619
620class StateColumn( GridColumn ):
621    """
622    Column that tracks and filters for items with state attribute.
623
624    IMPORTANT NOTE: self.model_class must have a states Bunch or dict if
625    this column type is used in the grid.
626    """
627    def get_value( self, trans, grid, item ):
628        return item.state
629    def filter( self, trans, user, query, column_filter ):
630        """Modify query to filter self.model_class by state."""
631        if column_filter == "All":
632            pass
633        elif column_filter in [ v for k, v in self.model_class.states.items() ]:
634            query = query.filter( self.model_class.state == column_filter )
635        return query
636    def get_accepted_filters( self ):
637        """Returns a list of accepted filters for this column."""
638        all = GridColumnFilter( 'all', { self.key : 'All' } )
639        accepted_filters = [ all ]
640        for k, v in self.model_class.states.items():
641           args = { self.key: v }
642           accepted_filters.append( GridColumnFilter( v, args) )
643        return accepted_filters
644
645class SharingStatusColumn( GridColumn ):
646    """ Grid column to indicate sharing status. """
647    def get_value( self, trans, grid, item ):
648        # Delete items cannot be shared.
649        if item.deleted:
650            return ""
651        # Build a list of sharing for this item.
652        sharing_statuses = []
653        if item.users_shared_with:
654            sharing_statuses.append( "Shared" )
655        if item.importable:
656            sharing_statuses.append( "Accessible" )
657        if item.published:
658            sharing_statuses.append( "Published" )
659        return ", ".join( sharing_statuses )
660    def get_link( self, trans, grid, item ):
661        if not item.deleted and ( item.users_shared_with or item.importable or item.published ):
662            return dict( operation="share or publish", id=item.id )
663        return None
664    def filter( self, trans, user, query, column_filter ):
665        """ Modify query to filter histories by sharing status. """
666        if column_filter == "All":
667            pass
668        elif column_filter:
669            if column_filter == "private":
670                query = query.filter( self.model_class.users_shared_with == None )
671                query = query.filter( self.model_class.importable == False )
672            elif column_filter == "shared":
673                query = query.filter( self.model_class.users_shared_with != None )
674            elif column_filter == "accessible":
675                query = query.filter( self.model_class.importable == True )
676            elif column_filter == "published":
677                query = query.filter( self.model_class.published == True )
678        return query
679    def get_accepted_filters( self ):
680        """ Returns a list of accepted filters for this column. """
681        accepted_filter_labels_and_vals = odict()
682        accepted_filter_labels_and_vals["private"] = "private"
683        accepted_filter_labels_and_vals["shared"] = "shared"
684        accepted_filter_labels_and_vals["accessible"] = "accessible"
685        accepted_filter_labels_and_vals["published"] = "published"
686        accepted_filter_labels_and_vals["all"] = "All"
687        accepted_filters = []
688        for label, val in accepted_filter_labels_and_vals.items():
689            args = { self.key: val }
690            accepted_filters.append( GridColumnFilter( label, args) )
691        return accepted_filters
692
693class GridOperation( object ):
694    def __init__( self, label, key=None, condition=None, allow_multiple=True, allow_popup=True,
695                  target=None, url_args=None, async_compatible=False, confirm=None ):
696        self.label = label
697        self.key = key
698        self.allow_multiple = allow_multiple
699        self.allow_popup = allow_popup
700        self.condition = condition
701        self.target = target
702        self.url_args = url_args
703        self.async_compatible = async_compatible
704        # if 'confirm' is set, then ask before completing the operation
705        self.confirm = confirm
706    def get_url_args( self, item ):
707        if self.url_args:
708            temp = dict( self.url_args )
709            temp['id'] = item.id
710            return temp
711        else:
712            return dict( operation=self.label, id=item.id )
713    def allowed( self, item ):
714        if self.condition:
715            return self.condition( item )
716        else:
717            return True
718           
719class DisplayByUsernameAndSlugGridOperation( GridOperation ):
720    """ Operation to display an item by username and slug. """
721    def get_url_args( self, item ):
722        return { 'action' : 'display_by_username_and_slug', 'username' : item.user.username, 'slug' : item.slug }
723       
724class GridAction( object ):
725    def __init__( self, label=None, url_args=None ):
726        self.label = label
727        self.url_args = url_args
728       
729class GridColumnFilter( object ):
730    def __init__( self, label, args=None ):
731        self.label = label
732        self.args = args
733    def get_url_args( self ):
734        rval = {}
735        for k, v in self.args.items():
736            rval[ "f-" + k ] = v
737        return rval
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。