Commits

Anonymous committed 5d3e57e

Enhance grid framework to enable custom column sorting. Sort criteria are now mapped to a column, and the column defines the sorting to be done on the grid query. Default sorting behavior has not changed. In addition, column's model_class attribute now defaults to the grid's model_class; this should make column definitions more intuitive and shorter.

Used custom sorting functionality to enable (a) case-insensitive sorting of text fields and (b) case-insensitive sorting of published item by username.

Comments (0)

Files changed (12)

lib/galaxy/web/controllers/dataset.py

     title = "Saved Datasets"
     model_class = model.HistoryDatasetAssociation
     template='/dataset/grid.mako'
-    default_sort_key = "-create_time"
+    default_sort_key = "-update_time"
     columns = [
-        grids.TextColumn( "Name", key="name", model_class=model.HistoryDatasetAssociation,
+        grids.TextColumn( "Name", key="name", 
                             # Link name to dataset's history.
-                              link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ), filterable="advanced", attach_popup=True ),
+                            link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ), filterable="advanced", attach_popup=True ),
         HistoryColumn( "History", key="history", 
                         link=( lambda item: iff( item.history.deleted, None, dict( operation="switch_history", id=item.id ) ) ) ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.HistoryDatasetAssociation, model_tag_association_class=model.HistoryDatasetAssociationTagAssociation, filterable="advanced", grid_name="HistoryDatasetAssocationListGrid" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.HistoryDatasetAssociationTagAssociation, filterable="advanced", grid_name="HistoryDatasetAssocationListGrid" ),
         StatusColumn( "Status", key="deleted", attach_popup=False ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
     ]

lib/galaxy/web/controllers/history.py

     title = "Saved Histories"
     model_class = model.History
     template='/history/grid.mako'
-    default_sort_key = "-create_time"
+    default_sort_key = "-update_time"
     columns = [
-        NameColumn( "Name", key="name", model_class=model.History,
+        NameColumn( "Name", key="name",
                           link=( lambda history: iff( history.deleted, None, dict( operation="Switch", id=history.id ) ) ),
                           attach_popup=True, filterable="advanced" ),
         DatasetsByStateColumn( "Datasets (by state)", ncells=4 ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.History, \
-                                    model_tag_association_class=model.HistoryTagAssociation, \
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, \
                                     filterable="advanced", grid_name="HistoryListGrid" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.History, filterable="advanced", sortable=False ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
         grids.GridColumn( "Created", key="create_time", format=time_ago ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
         # Columns that are valid for filtering but are not visible.
     default_filter = dict( public_url="All", username="All", tags="All" )
     use_async = True
     columns = [
-        NameURLColumn( "Name", key="name", model_class=model.History, filterable="advanced" ),
-        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.History, model_annotation_association_class=model.HistoryAnnotationAssociation, filterable="advanced" ),
-        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), 
-        grids.CommunityTagsColumn( "Community Tags", key="tags", model_class=model.History, model_tag_association_class=model.HistoryTagAssociation, filterable="advanced", grid_name="PublicHistoryListGrid" ),
+        NameURLColumn( "Name", key="name", filterable="advanced" ),
+        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.HistoryAnnotationAssociation, filterable="advanced" ),
+        grids.OwnerColumn( "Owner", key="owner", model_class=model.User, filterable="advanced" ), 
+        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, filterable="advanced", grid_name="PublicHistoryListGrid" ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago )
     ]
     columns.append( 

lib/galaxy/web/controllers/page.py

     default_filter = { "published" : "All", "tags" : "All", "title" : "All", "sharing" : "All" }
     default_sort_key = "-create_time"
     columns = [
-        grids.TextColumn( "Title", key="title", model_class=model.Page, attach_popup=True, filterable="advanced" ),
+        grids.TextColumn( "Title", key="title", attach_popup=True, filterable="advanced" ),
         URLColumn( "Public URL" ),
-        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.Page, model_annotation_association_class=model.PageAnnotationAssociation, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.Page, model_tag_association_class=model.PageTagAssociation, filterable="advanced", grid_name="PageListGrid" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.Page, filterable="advanced", sortable=False ),
+        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.PageAnnotationAssociation, filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.PageTagAssociation, filterable="advanced", grid_name="PageListGrid" ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
         grids.GridColumn( "Created", key="create_time", format=time_ago ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
     ]
     default_sort_key = "-update_time"
     default_filter = dict( title="All", username="All" )
     columns = [
-        grids.PublicURLColumn( "Title", key="title", model_class=model.Page, filterable="advanced" ),
-        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.Page, model_annotation_association_class=model.PageAnnotationAssociation, filterable="advanced" ),
-        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), 
-        grids.CommunityTagsColumn( "Community Tags", key="tags", model_class=model.Page, model_tag_association_class=model.PageTagAssociation, filterable="advanced", grid_name="PageAllPublishedGrid" ),
+        grids.PublicURLColumn( "Title", key="title", filterable="advanced" ),
+        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.PageAnnotationAssociation, filterable="advanced" ),
+        grids.OwnerColumn( "Owner", key="owner", model_class=model.User, filterable="advanced" ), 
+        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.PageTagAssociation, filterable="advanced", grid_name="PageAllPublishedGrid" ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago )
     ]
     columns.append( 
     title = "Saved Histories"
     model_class = model.History
     columns = [
-        ItemSelectionGrid.NameColumn( "Name", key="name", model_class=model.History, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.History, model_tag_association_class=model.HistoryTagAssociation, filterable="advanced"),
+        ItemSelectionGrid.NameColumn( "Name", key="name", filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, filterable="advanced"),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
         # Columns that are valid for filtering but are not visible.
         grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.History, filterable="advanced", sortable=False, visible=False ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False, visible=False ),
     ]
     columns.append(     
         grids.MulticolFilterColumn(  
     title = "Saved Datasets"
     model_class = model.HistoryDatasetAssociation
     columns = [
-        ItemSelectionGrid.NameColumn( "Name", key="name", model_class=model.HistoryDatasetAssociation, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.HistoryDatasetAssociation, model_tag_association_class=model.HistoryDatasetAssociationTagAssociation, filterable="advanced"),
+        ItemSelectionGrid.NameColumn( "Name", key="name", filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.HistoryDatasetAssociationTagAssociation, filterable="advanced"),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
         # Columns that are valid for filtering but are not visible.
         grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.HistoryDatasetAssociation, filterable="advanced", sortable=False, visible=False ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False, visible=False ),
     ]
     columns.append(     
         grids.MulticolFilterColumn(  
     title = "Saved Workflows"
     model_class = model.StoredWorkflow
     columns = [
-        ItemSelectionGrid.NameColumn( "Name", key="name", model_class=model.StoredWorkflow, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.StoredWorkflow, model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced"),
+        ItemSelectionGrid.NameColumn( "Name", key="name", filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced"),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
         # Columns that are valid for filtering but are not visible.
         grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.StoredWorkflow, filterable="advanced", sortable=False, visible=False ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False, visible=False ),
     ]
     columns.append(     
         grids.MulticolFilterColumn(  
     title = "Saved Pages"
     model_class = model.Page
     columns = [
-        grids.TextColumn( "Title", key="title", model_class=model.Page, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.Page, model_tag_association_class=model.PageTagAssociation, filterable="advanced"),
+        grids.TextColumn( "Title", key="title", filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.PageTagAssociation, filterable="advanced"),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
         # Columns that are valid for filtering but are not visible.
         grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.Page, filterable="advanced", sortable=False, visible=False ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False, visible=False ),
     ]
     columns.append(     
         grids.MulticolFilterColumn(  
     title = "Saved Visualizations"
     model_class = model.Visualization
     columns = [
-        grids.TextColumn( "Title", key="title", model_class=model.Visualization, filterable="advanced" ),
-        grids.TextColumn( "Type", key="type", model_class=model.Visualization ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.Visualization, model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.Visualization, filterable="advanced", sortable=False ),
+        grids.TextColumn( "Title", key="title", filterable="advanced" ),
+        grids.TextColumn( "Type", key="type" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
     ]    
     columns.append( 
                     return self.sharing( trans, **kwargs )
             session.flush()
             
-        # Build grid HTML and make sure to encode in utf-8 to support unicode characters.
-        grid = unicode( self._page_list( trans, *args, **kwargs ), 'utf-8' )
+        # Build grid HTML.
+        grid = self._page_list( trans, *args, **kwargs )
         
         # Build list of pages shared with user.
         shared_by_others = trans.sa_session \
              
     @web.expose
     def list_published( self, trans, *args, **kwargs ):
-        grid = unicode( self._all_published_list( trans, *args, **kwargs ), 'utf-8' )
+        grid = self._all_published_list( trans, *args, **kwargs )
         if 'async' in kwargs:
             return grid
         else:

lib/galaxy/web/controllers/visualization.py

     default_sort_key = "-update_time"
     default_filter = dict( title="All", deleted="False", tags="All", sharing="All" )
     columns = [
-        grids.TextColumn( "Title", key="title", model_class=model.Visualization, attach_popup=True,
+        grids.TextColumn( "Title", key="title", attach_popup=True,
                          link=( lambda item: dict( controller="tracks", action="browser", id=item.id ) ) ),
-        grids.TextColumn( "Dbkey", key="dbkey", model_class=model.Visualization ),
-        grids.IndividualTagsColumn( "Tags", key="tags", model_class=model.Visualization, model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
-        grids.SharingStatusColumn( "Sharing", key="sharing", model_class=model.Visualization, filterable="advanced", sortable=False ),
+        grids.TextColumn( "Dbkey", key="dbkey" ),
+        grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
+        grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
         grids.GridColumn( "Created", key="create_time", format=time_ago ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
     ]    
     default_sort_key = "-update_time"
     default_filter = dict( title="All", username="All" )
     columns = [
-        grids.PublicURLColumn( "Title", key="title", model_class=model.Visualization, filterable="advanced" ),
-        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.Visualization, model_annotation_association_class=model.VisualizationAnnotationAssociation, filterable="advanced" ),
-        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), 
-        grids.CommunityTagsColumn( "Community Tags", key="tags", model_class=model.Visualization, model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationAllPublishedGrid" ),
+        grids.PublicURLColumn( "Title", key="title", filterable="advanced" ),
+        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.VisualizationAnnotationAssociation, filterable="advanced" ),
+        grids.OwnerColumn( "Owner", key="owner", model_class=model.User, filterable="advanced" ), 
+        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationAllPublishedGrid" ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago )
     ]
     columns.append( 

lib/galaxy/web/controllers/workflow.py

     default_filter = { "name" : "All", "tags": "All" }
     default_sort_key = "-update_time"
     columns = [
-        grids.TextColumn( "Name", key="name", model_class=model.StoredWorkflow, attach_popup=True, filterable="advanced" ),
-        grids.IndividualTagsColumn( "Tags", "tags", model.StoredWorkflow, model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="StoredWorkflowListGrid" ),
+        grids.TextColumn( "Name", key="name", attach_popup=True, filterable="advanced" ),
+        grids.IndividualTagsColumn( "Tags", "tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="StoredWorkflowListGrid" ),
         StepsColumn( "Steps" ),
         grids.GridColumn( "Created", key="create_time", format=time_ago ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
     default_filter = dict( public_url="All", username="All", tags="All" )
     use_async = True
     columns = [
-        grids.PublicURLColumn( "Name", key="name", model_class=model.StoredWorkflow, filterable="advanced" ),
-        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.StoredWorkflow, model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, filterable="advanced" ),
-        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), 
-        grids.CommunityTagsColumn( "Community Tags", key="tags", model_class=model.StoredWorkflow, model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ),
+        grids.PublicURLColumn( "Name", key="name", filterable="advanced" ),
+        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, filterable="advanced" ),
+        grids.OwnerColumn( "Owner", key="owner", model_class=model.User, filterable="advanced" ), 
+        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ),
         grids.GridColumn( "Last Updated", key="update_time", format=time_ago )
     ]
     columns.append( 

lib/galaxy/web/framework/helpers/grids.py

 from galaxy.web import url_for
 from galaxy.util.json import from_json_string, to_json_string
 from galaxy.util.odict import odict
+from galaxy.web.framework.helpers import to_unicode
 
 import sys, logging, math
 
             if operation.allow_multiple:
                 self.has_multiple_item_operations = True
                 break
+                
+        # If a column does not have a model class, set the column's model class 
+        # to be the grid's model class.
+        for column in self.columns:
+            if not column.model_class:
+                column.model_class = self.model_class
+        
     def __call__( self, trans, **kwargs ):
+        #
+        # Get basics.
+        #
         webapp = kwargs.get( 'webapp', 'galaxy' )
         status = kwargs.get( 'status', None )
         message = kwargs.get( 'message', None )
+        
+        #
         # Build a base filter and sort key that is the combination of the saved state and defaults.
         # Saved state takes preference over defaults.
+        #
         base_filter = {}
         if self.default_filter:
             # default_filter is a dictionary that provides a default set of filters based on the grid's columns.
         use_default_filter = False
         if use_default_filter_str:
             use_default_filter = ( use_default_filter_str.lower() == 'true' )
+            
+        #
         # Process filtering arguments to (a) build a query that represents the filter and (b) builds a
-        # dictionary that denotes the current filter.        
+        # dictionary that denotes the current filter.
+        #
         cur_filter_dict = {}
         for column in self.columns:
             if column.key:
                         if not isinstance( column_filter, basestring ):
                             column_filter = unicode(column_filter)
                         extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8")
+        
+        #
         # Process sort arguments.
+        #
         sort_key = None
         if 'sort' in kwargs:
             sort_key = kwargs['sort']
         elif base_sort_key:
             sort_key = base_sort_key
-        if sort_key:
+
+        if sort_key:            
             if sort_key.startswith( "-" ):
-                # Can't use lower() on timestamp or integer objects, so func.lower() is not used here...
-                query = query.order_by( self.model_class.table.c.get( sort_key[1:] ).desc() ) 
+                ascending = False
+                column_key = sort_key[1:]
             else:
-                # See reason for not using lower() to do case-insensitive sorting.
-                query = query.order_by( self.model_class.table.c.get( sort_key ).asc() )
-        extra_url_args['sort'] = sort_key
+                ascending = True
+                column_key = sort_key
+            
+            # Sort key is a column key.
+            for column in self.columns:
+                if column.key == column_key:
+                    query = column.sort( query, ascending )
+                    break
+            extra_url_args['sort'] = sort_key
+        
+        #
         # There might be a current row
+        #
         current_item = self.get_current_item( trans, **kwargs )
+        
+        #
         # Process page number.
+        #
         if self.use_paging:
             if 'page' in kwargs:
                 if kwargs['page'] == 'all':
             # Defaults.
             page_num = 1
             num_pages = 1
+            
+        #
         # Preserve grid state: save current filter and sort key.
+        #
         if self.preserve_state:
             pref_name = unicode( self.__class__.__name__ + self.cur_filter_pref_name )
             trans.get_user().preferences[pref_name] = unicode( to_json_string( cur_filter_dict ) )
         params['async'] = ( 'async' in kwargs )
         params['webapp'] = webapp
         trans.log_action( trans.get_user(), unicode( "grid.view" ), context, params )
+        
+        #
         # Render grid.
+        #
         def url( *args, **kwargs ):
             # Only include sort/filter arguments if not linking to another
             # page. This is a bit of a hack.
                 else:
                     new_kwargs[ 'id' ] = trans.security.encode_id( id )
             return url_for( **new_kwargs )
-        use_panels = ( 'use_panels' in kwargs ) and ( kwargs['use_panels'] in [ True, 'True', 'true' ] )
-        async_request = ( ( self.use_async ) and ( 'async' in kwargs ) and ( kwargs['async'] in [ True, 'True', 'true'] ) )
-        return trans.fill_template( iff( async_request, self.async_template, self.template),
+        use_panels = ( kwargs.get( 'use_panels', False ) in [ True, 'True', 'true' ] )
+        async_request = ( ( self.use_async ) and ( kwargs.get( 'async', False ) in [ True, 'True', 'true'] ) )
+        # Currently, filling the template returns a str object; this requires decoding the string into a 
+        # unicode object within mako templates. What probably should be done is to return the template as 
+        # utf-8 unicode; however, this would require encoding the object as utf-8 before returning the grid
+        # results via a controller method, which is require substantial changes. Hence, for now, return grid 
+        # as str.
+        return trans.fill_template( iff( async_request, self.async_template, self.template ),
                                     grid=self,
                                     query=query,
                                     cur_page_num = page_num,
         return query
     
 class GridColumn( object ):
-    def __init__( self, label, grid=None, key=None, model_class=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, 
-                    # Valid values for filterable are ['standard', 'advanced', None]
-                    filterable=None, sortable=True ):
+    def __init__( self, label, key=None, model_class=None, method=None, format=None, \
+                  link=None, attach_popup=False, visible=True, ncells=1, \
+                  # Valid values for filterable are ['standard', 'advanced', None]
+                  filterable=None, sortable=True ):
+        """
+        Create a grid column. 
+        """
         self.label = label
-        self.grid = grid
         self.key = key
         self.model_class = model_class
         self.method = method
         self.visible = visible
         self.ncells = ncells
         self.filterable = filterable
-        # Currently can only sort of columns that have a database
-        # representation, not purely derived.
-        if self.key and sortable:
-            self.sortable = True
-        else:
-            self.sortable = False
+        # Column must have a key to be sortable.
+        self.sortable = ( self.key is not None and sortable )
+            
     def get_value( self, trans, grid, item ):
         if self.method:
             value = getattr( grid, self.method )( trans, item )
         if self.format:
             value = self.format( value )
         return value
+        
     def get_link( self, trans, grid, item ):
         if self.link and self.link( item ):
             return self.link( item )
         return None
+        
     def filter( self, trans, user, query, column_filter ):
         """ Modify query to reflect the column filter. """
         if column_filter == "All":
         elif column_filter == "False":
             query = query.filter_by( **{ self.key: False } )
         return query
+        
     def get_accepted_filters( self ):
         """ Returns a list of accepted filters for this column. """
         accepted_filters_vals = [ "False", "True", "All" ]
             args = { self.key: val }
             accepted_filters.append( GridColumnFilter( val, args) )
         return accepted_filters
+    
+    def sort( self, query, ascending ):
+        """ Sort query using this column. """
+        if ascending:
+            query = query.order_by( self.model_class.table.c.get( self.key ).asc() ) 
+        else:
+            query = query.order_by( self.model_class.table.c.get( self.key ).desc() )
+        return query
+
         
 class TextColumn( GridColumn ):
     """ Generic column that employs freetext and, hence, supports freetext, case-independent filtering. """
         """ Returns a SQLAlchemy criterion derived for a single filter. Single filter is the most basic filter--usually a string--and cannot be a list. """
         model_class_key_field = getattr( self.model_class, self.key )
         return func.lower( model_class_key_field ).like( "%" + a_filter.lower() + "%" )
+        
+    def sort( self, query, ascending ):
+        """ Sort column using case-insensitive alphabetical sorting. """
+        if ascending:
+            query = query.order_by( func.lower ( self.model_class.table.c.get( self.key ) ).asc() ) 
+        else:
+            query = query.order_by( func.lower( self.model_class.table.c.get( self.key ) ).desc() )
+        return query
 
 class IntegerColumn( TextColumn ):
     """
 
 class OwnerAnnotationColumn( TextColumn, UsesAnnotations ):
     """ Column that displays and filters item owner's annotations. """
-    def __init__( self, col_name, key, model_class, model_annotation_association_class, filterable ):
+    def __init__( self, col_name, key, model_class=None, model_annotation_association_class=None, filterable=None ):
         GridColumn.__init__( self, col_name, key=key, model_class=model_class, filterable=filterable )
         self.sortable = False
         self.model_annotation_association_class = model_annotation_association_class
                         
 class CommunityTagsColumn( TextColumn ):
     """ Column that supports community tags. """
-    def __init__( self, col_name, key, model_class, model_tag_association_class, filterable, grid_name=None ):
-        GridColumn.__init__( self, col_name, key=key, model_class=model_class, filterable=filterable )
+    def __init__( self, col_name, key, model_class=None, model_tag_association_class=None, filterable=None, grid_name=None ):
+        GridColumn.__init__( self, col_name, key=key, model_class=model_class, filterable=filterable, sortable=False )
         self.model_tag_association_class = model_tag_association_class
-        # Tags cannot be sorted.
-        self.sortable = False
         # Column-specific attributes.
         self.grid_name = grid_name
     def get_value( self, trans, grid, item ):
     """ Column that lists item's owner. """
     def get_value( self, trans, grid, item ):
         return item.user.username
+        
+    def sort( self, query, ascending ):
+        """ Sort column using case-insensitive alphabetical sorting on item's username. """
+        if ascending:
+            query = query.order_by( func.lower ( self.model_class.username ).asc() ) 
+        else:
+            query = query.order_by( func.lower( self.model_class.username ).desc() )
+        return query
+    
 
 class PublicURLColumn( TextColumn ):
     """ Column displays item's public URL based on username and slug. """
             # TODO: provide link to set username.
             return None
         elif not item.user.slug:
-            # TODO: provide link to set slg
+            # TODO: provide link to set slug.
             return None
 
 class DeletedColumn( GridColumn ):

templates/history/list_published.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${unicode( grid, 'utf-8' )}
+            ${h.to_unicode( grid )}
         </div>
     </div>
 

templates/page/index.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${grid}
+            ${h.to_unicode( grid )}
 
 			<br><br>
 			<h2>Pages shared with you by others</h2>

templates/page/list_published.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${grid}
-
+            ${h.to_unicode( grid )}
+        </div>
+    </div>
 </%def>

templates/visualization/list.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${grid}
+            ${h.to_unicode( grid )}
 
 			<br><br>
 			<h2>Visualizations shared with you by others</h2>

templates/visualization/list_published.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${unicode( grid, 'utf-8' )}
+            ${h.to_unicode( grid )}
         </div>
     </div>
 

templates/workflow/list_published.mako

 
     <div style="overflow: auto; height: 100%;">
         <div class="page-container" style="padding: 10px;">
-            ${unicode( grid, 'utf-8' )}
+            ${h.to_unicode( grid )}
         </div>
     </div>