[2] | 1 | from galaxy.model.orm import * |
---|
| 2 | from galaxy.web.base.controller import * |
---|
| 3 | from galaxy.web.framework.helpers import iff |
---|
| 4 | from galaxy.web import url_for |
---|
| 5 | from galaxy.util.json import from_json_string, to_json_string |
---|
| 6 | from galaxy.util.odict import odict |
---|
| 7 | from galaxy.web.framework.helpers import to_unicode |
---|
| 8 | from galaxy.model.item_attrs import * |
---|
| 9 | |
---|
| 10 | import sys, logging, math |
---|
| 11 | |
---|
| 12 | log = logging.getLogger( __name__ ) |
---|
| 13 | |
---|
| 14 | class 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 | |
---|
| 296 | class 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 | |
---|
| 355 | class 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 | |
---|
| 360 | class 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 | |
---|
| 401 | class 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 | |
---|
| 406 | class 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 | |
---|
| 411 | class 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 | |
---|
| 436 | class 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 | |
---|
| 475 | class 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 | |
---|
| 500 | class 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 | |
---|
| 533 | class 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 | |
---|
| 561 | class 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 | |
---|
| 585 | class 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 | |
---|
| 597 | class 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 | |
---|
| 609 | class 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 | |
---|
| 620 | class 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 | |
---|
| 645 | class 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 | |
---|
| 693 | class 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 | |
---|
| 719 | class 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 | |
---|
| 724 | class GridAction( object ): |
---|
| 725 | def __init__( self, label=None, url_args=None ): |
---|
| 726 | self.label = label |
---|
| 727 | self.url_args = url_args |
---|
| 728 | |
---|
| 729 | class 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 |
---|