/lib/galaxy/webapps/community/controllers/common.py
Python | 552 lines | 538 code | 6 blank | 8 comment | 16 complexity | d903c33b6ade000b876f1095622a8d6a MD5 | raw file
1import tarfile 2from galaxy.web.base.controller import * 3from galaxy.webapps.community import model 4from galaxy.model.orm import * 5from galaxy.web.framework.helpers import time_ago, iff, grids 6from galaxy.web.form_builder import SelectField 7from galaxy.item_attrs.ratings import UsesItemRatings 8import logging 9log = logging.getLogger( __name__ ) 10 11# States for passing messages 12SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" 13 14class ToolListGrid( grids.Grid ): 15 class NameColumn( grids.TextColumn ): 16 def get_value( self, trans, grid, tool ): 17 return tool.name 18 class TypeColumn( grids.GridColumn ): 19 def get_value( self, trans, grid, tool ): 20 if tool.is_suite: 21 return 'Suite' 22 return 'Tool' 23 class VersionColumn( grids.TextColumn ): 24 def get_value( self, trans, grid, tool ): 25 return tool.version 26 class DescriptionColumn( grids.TextColumn ): 27 def get_value( self, trans, grid, tool ): 28 return tool.description 29 class CategoryColumn( grids.TextColumn ): 30 def get_value( self, trans, grid, tool ): 31 rval = '<ul>' 32 if tool.categories: 33 for tca in tool.categories: 34 rval += '<li><a href="browse_tools?operation=tools_by_category&id=%s&webapp=community">%s</a></li>' \ 35 % ( trans.security.encode_id( tca.category.id ), tca.category.name ) 36 else: 37 rval += '<li>not set</li>' 38 rval += '</ul>' 39 return rval 40 class ToolCategoryColumn( grids.GridColumn ): 41 def filter( self, trans, user, query, column_filter ): 42 """Modify query to filter by category.""" 43 if column_filter == "All": 44 pass 45 return query.filter( model.Category.name == column_filter ) 46 class UserColumn( grids.TextColumn ): 47 def get_value( self, trans, grid, tool ): 48 if tool.user: 49 return tool.user.username 50 return 'no user' 51 class EmailColumn( grids.GridColumn ): 52 def filter( self, trans, user, query, column_filter ): 53 if column_filter == 'All': 54 return query 55 return query.filter( and_( model.Tool.table.c.user_id == model.User.table.c.id, 56 model.User.table.c.email == column_filter ) ) 57 # Grid definition 58 title = "Tools" 59 model_class = model.Tool 60 template='/webapps/community/tool/grid.mako' 61 default_sort_key = "name" 62 columns = [ 63 NameColumn( "Name", 64 key="name", 65 link=( lambda item: dict( operation="view_tool", id=item.id, webapp="community" ) ), 66 model_class=model.Tool, 67 attach_popup=False 68 ), 69 TypeColumn( "Type", 70 key="suite", 71 model_class=model.Tool, 72 attach_popup=False ), 73 VersionColumn( "Version", 74 key="version", 75 model_class=model.Tool, 76 attach_popup=False, 77 filterable="advanced" ), 78 DescriptionColumn( "Description", 79 key="description", 80 model_class=model.Tool, 81 attach_popup=False 82 ), 83 CategoryColumn( "Category", 84 model_class=model.Category, 85 attach_popup=False, 86 filterable="advanced" ), 87 UserColumn( "Uploaded By", 88 model_class=model.User, 89 link=( lambda item: dict( operation="tools_by_user", id=item.id, webapp="community" ) ), 90 attach_popup=False, 91 filterable="advanced" ), 92 # Columns that are valid for filtering but are not visible. 93 EmailColumn( "Email", 94 key="email", 95 model_class=model.User, 96 visible=False ), 97 ToolCategoryColumn( "Category", 98 key="category", 99 model_class=model.Category, 100 visible=False ) 101 ] 102 columns.append( grids.MulticolFilterColumn( "Search", 103 cols_to_filter=[ columns[0], columns[1], columns[2] ], 104 key="free-text-search", 105 visible=False, 106 filterable="standard" ) ) 107 operations = [] 108 standard_filters = [] 109 default_filter = {} 110 num_rows_per_page = 50 111 preserve_state = False 112 use_paging = True 113 def build_initial_query( self, trans, **kwd ): 114 return trans.sa_session.query( self.model_class ) \ 115 .join( model.ToolEventAssociation.table ) \ 116 .join( model.Event.table ) \ 117 .outerjoin( model.ToolCategoryAssociation.table ) \ 118 .outerjoin( model.Category.table ) 119 120class CategoryListGrid( grids.Grid ): 121 class NameColumn( grids.TextColumn ): 122 def get_value( self, trans, grid, category ): 123 return category.name 124 class DescriptionColumn( grids.TextColumn ): 125 def get_value( self, trans, grid, category ): 126 return category.description 127 class ToolsColumn( grids.TextColumn ): 128 def get_value( self, trans, grid, category ): 129 if category.tools: 130 viewable_tools = 0 131 for tca in category.tools: 132 viewable_tools += 1 133 return viewable_tools 134 return 0 135 136 # Grid definition 137 webapp = "community" 138 title = "Categories" 139 model_class = model.Category 140 template='/webapps/community/category/grid.mako' 141 default_sort_key = "name" 142 columns = [ 143 NameColumn( "Name", 144 key="name", 145 model_class=model.Category, 146 link=( lambda item: dict( operation="tools_by_category", id=item.id, webapp="community" ) ), 147 attach_popup=False, 148 filterable="advanced" 149 ), 150 DescriptionColumn( "Description", 151 key="description", 152 model_class=model.Category, 153 attach_popup=False, 154 filterable="advanced" 155 ), 156 # Columns that are valid for filtering but are not visible. 157 grids.DeletedColumn( "Deleted", 158 key="deleted", 159 visible=False, 160 filterable="advanced" ), 161 ToolsColumn( "Tools", 162 model_class=model.Tool, 163 attach_popup=False ) 164 ] 165 columns.append( grids.MulticolFilterColumn( "Search", 166 cols_to_filter=[ columns[0], columns[1] ], 167 key="free-text-search", 168 visible=False, 169 filterable="standard" ) ) 170 171 # Override these 172 global_actions = [] 173 operations = [] 174 standard_filters = [] 175 num_rows_per_page = 50 176 preserve_state = False 177 use_paging = True 178 179class ItemRatings( UsesItemRatings ): 180 """Overrides rate_item method since we also allow for comments""" 181 def rate_item( self, trans, user, item, rating, comment='' ): 182 """ Rate an item. Return type is <item_class>RatingAssociation. """ 183 item_rating = self.get_user_item_rating( trans, user, item ) 184 if not item_rating: 185 # User has not yet rated item; create rating. 186 item_rating_assoc_class = self._get_item_rating_assoc_class( trans, item ) 187 item_rating = item_rating_assoc_class() 188 item_rating.user = trans.user 189 item_rating.set_item( item ) 190 item_rating.rating = rating 191 item_rating.comment = comment 192 trans.sa_session.add( item_rating ) 193 trans.sa_session.flush() 194 elif item_rating.rating != rating or item_rating.comment != comment: 195 # User has previously rated item; update rating. 196 item_rating.rating = rating 197 item_rating.comment = comment 198 trans.sa_session.flush() 199 return item_rating 200 201class CommonController( BaseController, ItemRatings ): 202 @web.expose 203 def edit_tool( self, trans, cntrller, **kwd ): 204 params = util.Params( kwd ) 205 message = util.restore_text( params.get( 'message', '' ) ) 206 status = params.get( 'status', 'done' ) 207 id = params.get( 'id', None ) 208 if not id: 209 return trans.response.send_redirect( web.url_for( controller=cntrller, 210 action='browse_tools', 211 message='Select a tool to edit', 212 status='error' ) ) 213 tool = get_tool( trans, id ) 214 can_edit = trans.app.security_agent.can_edit( trans.user, trans.user_is_admin(), cntrller, tool ) 215 if not can_edit: 216 return trans.response.send_redirect( web.url_for( controller=cntrller, 217 action='browse_tools', 218 message='You are not allowed to edit this tool', 219 status='error' ) ) 220 if params.get( 'edit_tool_button', False ): 221 if params.get( 'in_categories', False ): 222 in_categories = [ trans.sa_session.query( trans.app.model.Category ).get( x ) for x in util.listify( params.in_categories ) ] 223 trans.app.security_agent.set_entity_category_associations( tools=[ tool ], categories=in_categories ) 224 else: 225 # There must not be any categories associated with the tool 226 trans.app.security_agent.set_entity_category_associations( tools=[ tool ], categories=[] ) 227 user_description = util.restore_text( params.get( 'user_description', '' ) ) 228 if user_description: 229 tool.user_description = user_description 230 else: 231 tool.user_description = '' 232 trans.sa_session.add( tool ) 233 trans.sa_session.flush() 234 message = "Tool '%s' description and category associations have been saved" % tool.name 235 return trans.response.send_redirect( web.url_for( controller='common', 236 action='edit_tool', 237 cntrller=cntrller, 238 id=id, 239 message=message, 240 status='done' ) ) 241 elif params.get( 'approval_button', False ): 242 user_description = util.restore_text( params.get( 'user_description', '' ) ) 243 if user_description: 244 tool.user_description = user_description 245 if params.get( 'in_categories', False ): 246 in_categories = [ trans.sa_session.query( trans.app.model.Category ).get( x ) for x in util.listify( params.in_categories ) ] 247 trans.app.security_agent.set_entity_category_associations( tools=[ tool ], categories=in_categories ) 248 else: 249 # There must not be any categories associated with the tool 250 trans.app.security_agent.set_entity_category_associations( tools=[ tool ], categories=[] ) 251 trans.sa_session.add( tool ) 252 trans.sa_session.flush() 253 # Move the state from NEW to WAITING 254 event = trans.app.model.Event( state=trans.app.model.Tool.states.WAITING ) 255 tea = trans.app.model.ToolEventAssociation( tool, event ) 256 trans.sa_session.add_all( ( event, tea ) ) 257 trans.sa_session.flush() 258 message = "Tool '%s' has been submitted for approval and can no longer be modified" % ( tool.name ) 259 return trans.response.send_redirect( web.url_for( controller='common', 260 action='view_tool', 261 cntrller=cntrller, 262 id=id, 263 message=message, 264 status='done' ) ) 265 else: 266 # The user_description field is required when submitting for approval 267 message = 'A user description is required prior to approval.' 268 status = 'error' 269 in_categories = [] 270 out_categories = [] 271 for category in get_categories( trans ): 272 if category in [ x.category for x in tool.categories ]: 273 in_categories.append( ( category.id, category.name ) ) 274 else: 275 out_categories.append( ( category.id, category.name ) ) 276 if tool.is_rejected: 277 # Include the comments regarding the reason for rejection 278 reason_for_rejection = tool.latest_event.comment 279 else: 280 reason_for_rejection = '' 281 can_approve_or_reject = trans.app.security_agent.can_approve_or_reject( trans.user, trans.user_is_admin(), cntrller, tool ) 282 can_delete = trans.app.security_agent.can_delete( trans.user, trans.user_is_admin(), cntrller, tool ) 283 can_download = trans.app.security_agent.can_download( trans.user, trans.user_is_admin(), cntrller, tool ) 284 can_purge = trans.app.security_agent.can_purge( trans.user, trans.user_is_admin(), cntrller ) 285 can_upload_new_version = trans.app.security_agent.can_upload_new_version( trans.user, tool ) 286 can_view = trans.app.security_agent.can_view( trans.user, trans.user_is_admin(), cntrller, tool ) 287 return trans.fill_template( '/webapps/community/tool/edit_tool.mako', 288 cntrller=cntrller, 289 tool=tool, 290 id=id, 291 in_categories=in_categories, 292 out_categories=out_categories, 293 can_approve_or_reject=can_approve_or_reject, 294 can_delete=can_delete, 295 can_download=can_download, 296 can_edit=can_edit, 297 can_purge=can_purge, 298 can_upload_new_version=can_upload_new_version, 299 can_view=can_view, 300 reason_for_rejection=reason_for_rejection, 301 message=message, 302 status=status ) 303 @web.expose 304 def view_tool( self, trans, cntrller, **kwd ): 305 params = util.Params( kwd ) 306 message = util.restore_text( params.get( 'message', '' ) ) 307 status = params.get( 'status', 'done' ) 308 id = params.get( 'id', None ) 309 if not id: 310 return trans.response.send_redirect( web.url_for( controller=cntrller, 311 action='browse_tools', 312 message='Select a tool to view', 313 status='error' ) ) 314 tool = get_tool( trans, id ) 315 can_view = trans.app.security_agent.can_view( trans.user, trans.user_is_admin(), cntrller, tool ) 316 if not can_view: 317 return trans.response.send_redirect( web.url_for( controller=cntrller, 318 action='browse_tools', 319 message='You are not allowed to view this tool', 320 status='error' ) ) 321 can_approve_or_reject = trans.app.security_agent.can_approve_or_reject( trans.user, trans.user_is_admin(), cntrller, tool ) 322 can_delete = trans.app.security_agent.can_delete( trans.user, trans.user_is_admin(), cntrller, tool ) 323 can_download = trans.app.security_agent.can_download( trans.user, trans.user_is_admin(), cntrller, tool ) 324 can_edit = trans.app.security_agent.can_edit( trans.user, trans.user_is_admin(), cntrller, tool ) 325 can_purge = trans.app.security_agent.can_purge( trans.user, trans.user_is_admin(), cntrller ) 326 can_rate = trans.app.security_agent.can_rate( trans.user, trans.user_is_admin(), cntrller, tool ) 327 can_upload_new_version = trans.app.security_agent.can_upload_new_version( trans.user, tool ) 328 visible_versions = trans.app.security_agent.get_visible_versions( trans.user, trans.user_is_admin(), cntrller, tool ) 329 categories = [ tca.category for tca in tool.categories ] 330 tool_file_contents = tarfile.open( tool.file_name, 'r' ).getnames() 331 if tool.is_rejected: 332 # Include the comments regarding the reason for rejection 333 reason_for_rejection = tool.latest_event.comment 334 else: 335 reason_for_rejection = '' 336 return trans.fill_template( '/webapps/community/tool/view_tool.mako', 337 tool=tool, 338 tool_file_contents=tool_file_contents, 339 categories=categories, 340 cntrller=cntrller, 341 can_approve_or_reject=can_approve_or_reject, 342 can_delete=can_delete, 343 can_download=can_download, 344 can_edit=can_edit, 345 can_purge=can_purge, 346 can_rate=can_rate, 347 can_upload_new_version=can_upload_new_version, 348 can_view=can_view, 349 visible_versions=visible_versions, 350 reason_for_rejection=reason_for_rejection, 351 message=message, 352 status=status ) 353 @web.expose 354 def delete_tool( self, trans, cntrller, **kwd ): 355 params = util.Params( kwd ) 356 message = util.restore_text( params.get( 'message', '' ) ) 357 status = params.get( 'status', 'done' ) 358 id = params.get( 'id', None ) 359 if not id: 360 message='Select a tool to delete' 361 status='error' 362 else: 363 tool = get_tool( trans, id ) 364 if not trans.app.security_agent.can_delete( trans.user, trans.user_is_admin(), cntrller, tool ): 365 return trans.response.send_redirect( web.url_for( controller=cntrller, 366 action='browse_tools', 367 message='You are not allowed to delete this tool', 368 status='error' ) ) 369 # Create a new event 370 event = trans.model.Event( state=trans.model.Tool.states.DELETED ) 371 # Flush so we can get an event id 372 trans.sa_session.add( event ) 373 trans.sa_session.flush() 374 # Associate the tool with the event 375 tea = trans.model.ToolEventAssociation( tool=tool, event=event ) 376 # Delete the tool, keeping state for categories, events and versions 377 tool.deleted = True 378 trans.sa_session.add_all( ( tool, tea ) ) 379 trans.sa_session.flush() 380 # TODO: What if the tool has versions, should they all be deleted? 381 message = "Tool '%s' has been marked deleted" % tool.name 382 status = 'done' 383 return trans.response.send_redirect( web.url_for( controller=cntrller, 384 action='browse_tools', 385 message=message, 386 status=status ) ) 387 @web.expose 388 def download_tool( self, trans, cntrller, **kwd ): 389 params = util.Params( kwd ) 390 id = params.get( 'id', None ) 391 if not id: 392 return trans.response.send_redirect( web.url_for( controller='tool', 393 action='browse_tools', 394 message='Select a tool to download', 395 status='error' ) ) 396 tool = get_tool( trans, id ) 397 if not trans.app.security_agent.can_download( trans.user, trans.user_is_admin(), cntrller, tool ): 398 return trans.response.send_redirect( web.url_for( controller=cntrller, 399 action='browse_tools', 400 message='You are not allowed to download this tool', 401 status='error' ) ) 402 trans.response.set_content_type( tool.mimetype ) 403 trans.response.headers['Content-Length'] = int( os.stat( tool.file_name ).st_size ) 404 trans.response.headers['Content-Disposition'] = 'attachment; filename=%s' % tool.download_file_name 405 return open( tool.file_name ) 406 @web.expose 407 def upload_new_tool_version( self, trans, cntrller, **kwd ): 408 params = util.Params( kwd ) 409 message = util.restore_text( params.get( 'message', '' ) ) 410 status = params.get( 'status', 'done' ) 411 id = params.get( 'id', None ) 412 if not id: 413 return trans.response.send_redirect( web.url_for( controller=cntrller, 414 action='browse_tools', 415 message='Select a tool to upload a new version', 416 status='error' ) ) 417 tool = get_tool( trans, id ) 418 if not trans.app.security_agent.can_upload_new_version( trans.user, tool ): 419 return trans.response.send_redirect( web.url_for( controller=cntrller, 420 action='browse_tools', 421 message='You are not allowed to upload a new version of this tool', 422 status='error' ) ) 423 return trans.response.send_redirect( web.url_for( controller='upload', 424 action='upload', 425 message=message, 426 status=status, 427 replace_id=id ) ) 428 @web.expose 429 @web.require_login( "view tool history" ) 430 def view_tool_history( self, trans, cntrller, **kwd ): 431 params = util.Params( kwd ) 432 message = util.restore_text( params.get( 'message', '' ) ) 433 status = params.get( 'status', 'done' ) 434 id = params.get( 'id', None ) 435 if not id: 436 return trans.response.send_redirect( web.url_for( controller=cntrller, 437 action='browse_tools', 438 message='Select a tool to view its history', 439 status='error' ) ) 440 tool = get_tool( trans, id ) 441 can_view = trans.app.security_agent.can_view( trans.user, trans.user_is_admin(), cntrller, tool ) 442 if not can_view: 443 return trans.response.send_redirect( web.url_for( controller=cntrller, 444 action='browse_tools', 445 message="You are not allowed to view this tool's history", 446 status='error' ) ) 447 can_approve_or_reject = trans.app.security_agent.can_approve_or_reject( trans.user, trans.user_is_admin(), cntrller, tool ) 448 can_edit = trans.app.security_agent.can_edit( trans.user, trans.user_is_admin(), cntrller, tool ) 449 can_delete = trans.app.security_agent.can_delete( trans.user, trans.user_is_admin(), cntrller, tool ) 450 can_download = trans.app.security_agent.can_download( trans.user, trans.user_is_admin(), cntrller, tool ) 451 events = [ tea.event for tea in tool.events ] 452 events = [ ( event.state, time_ago( event.update_time ), event.comment ) for event in events ] 453 return trans.fill_template( '/webapps/community/common/view_tool_history.mako', 454 cntrller=cntrller, 455 events=events, 456 tool=tool, 457 can_approve_or_reject=can_approve_or_reject, 458 can_edit=can_edit, 459 can_delete=can_delete, 460 can_download=can_download, 461 can_view=can_view, 462 message=message, 463 status=status ) 464 @web.expose 465 @web.require_login( "rate tools" ) 466 def rate_tool( self, trans, cntrller, **kwd ): 467 """ Rate a tool and return updated rating data. """ 468 params = util.Params( kwd ) 469 message = util.restore_text( params.get( 'message', '' ) ) 470 status = params.get( 'status', 'done' ) 471 id = params.get( 'id', None ) 472 if not id: 473 return trans.response.send_redirect( web.url_for( controller=cntrller, 474 action='browse_tools', 475 message='Select a tool to rate', 476 status='error' ) ) 477 tool = get_tool( trans, id ) 478 can_rate = trans.app.security_agent.can_rate( trans.user, trans.user_is_admin(), cntrller, tool ) 479 if not can_rate: 480 return trans.response.send_redirect( web.url_for( controller=cntrller, 481 action='browse_tools', 482 message="You are not allowed to rate this tool", 483 status='error' ) ) 484 if params.get( 'rate_button', False ): 485 rating = int( params.get( 'rating', '0' ) ) 486 comment = util.restore_text( params.get( 'comment', '' ) ) 487 rating = self.rate_item( trans, trans.user, tool, rating, comment ) 488 avg_rating, num_ratings = self.get_ave_item_rating_data( trans, tool ) 489 can_approve_or_reject = trans.app.security_agent.can_approve_or_reject( trans.user, trans.user_is_admin(), cntrller, tool ) 490 can_edit = trans.app.security_agent.can_edit( trans.user, trans.user_is_admin(), cntrller, tool ) 491 can_delete = trans.app.security_agent.can_delete( trans.user, trans.user_is_admin(), cntrller, tool ) 492 can_download = trans.app.security_agent.can_download( trans.user, trans.user_is_admin(), cntrller, tool ) 493 display_reviews = util.string_as_bool( params.get( 'display_reviews', False ) ) 494 tra = self.get_user_item_rating( trans, trans.user, tool ) 495 return trans.fill_template( '/webapps/community/common/rate_tool.mako', 496 cntrller=cntrller, 497 tool=tool, 498 avg_rating=avg_rating, 499 can_approve_or_reject=can_approve_or_reject, 500 can_edit=can_edit, 501 can_delete=can_delete, 502 can_download=can_download, 503 can_rate=can_rate, 504 display_reviews=display_reviews, 505 num_ratings=num_ratings, 506 tra=tra, 507 message=message, 508 status=status ) 509 510## ---- Utility methods ------------------------------------------------------- 511 512def get_versions( item ): 513 """Get all versions of item""" 514 versions = [ item ] 515 this_item = item 516 while item.newer_version: 517 versions.insert( 0, item.newer_version ) 518 item = item.newer_version 519 item = this_item 520 while item.older_version: 521 versions.append( item.older_version[ 0 ] ) 522 item = item.older_version[ 0 ] 523 return versions 524def get_categories( trans ): 525 """Get all categories from the database""" 526 return trans.sa_session.query( trans.model.Category ) \ 527 .filter( trans.model.Category.table.c.deleted==False ) \ 528 .order_by( trans.model.Category.table.c.name ).all() 529def get_category( trans, id ): 530 """Get a category from the database""" 531 return trans.sa_session.query( trans.model.Category ).get( trans.security.decode_id( id ) ) 532def get_tool( trans, id ): 533 """Get a tool from the database""" 534 return trans.sa_session.query( trans.model.Tool ).get( trans.security.decode_id( id ) ) 535def get_latest_versions_of_tools( trans ): 536 """Get only the latest version of each tool from the database""" 537 return trans.sa_session.query( trans.model.Tool ) \ 538 .filter( trans.model.Tool.newer_version_id == None ) \ 539 .order_by( trans.model.Tool.name ) 540def get_approved_tools( trans ): 541 """Get the tools from the database whose state is APPROVED""" 542 approved_tools = [] 543 for tool in get_latest_versions_of_tools( trans ): 544 if tool.state == trans.model.Tool.states.APPROVED: 545 approved_tools.append( tool ) 546 return approved_tools 547def get_event( trans, id ): 548 """Get an event from the databse""" 549 return trans.sa_session.query( trans.model.Event ).get( trans.security.decode_id( id ) ) 550def get_user( trans, id ): 551 """Get a user from the database""" 552 return trans.sa_session.query( trans.model.User ).get( trans.security.decode_id( id ) )