/lib/galaxy/web/controllers/visualization.py

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 457 lines · 427 code · 14 blank · 16 comment · 13 complexity · 10d22404979008dff44e127c3831f4d4 MD5 · raw file

  1. from galaxy import model
  2. from galaxy.model.item_attrs import *
  3. from galaxy.web.base.controller import *
  4. from galaxy.web.framework.helpers import time_ago, grids, iff
  5. from galaxy.util.sanitize_html import sanitize_html
  6. class VisualizationListGrid( grids.Grid ):
  7. # Grid definition
  8. title = "Saved Visualizations"
  9. model_class = model.Visualization
  10. default_sort_key = "-update_time"
  11. default_filter = dict( title="All", deleted="False", tags="All", sharing="All" )
  12. columns = [
  13. grids.TextColumn( "Title", key="title", attach_popup=True,
  14. link=( lambda item: dict( controller="tracks", action="browser", id=item.id ) ) ),
  15. grids.TextColumn( "Dbkey", key="dbkey" ),
  16. grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationListGrid" ),
  17. grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
  18. grids.GridColumn( "Created", key="create_time", format=time_ago ),
  19. grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
  20. ]
  21. columns.append(
  22. grids.MulticolFilterColumn(
  23. "Search",
  24. cols_to_filter=[ columns[0], columns[2] ],
  25. key="free-text-search", visible=False, filterable="standard" )
  26. )
  27. global_actions = [
  28. grids.GridAction( "Create new visualization", dict( action='create' ) )
  29. ]
  30. operations = [
  31. grids.GridOperation( "View/Edit", allow_multiple=False, url_args=dict( controller='tracks', action='browser' ) ),
  32. grids.GridOperation( "Edit Attributes", allow_multiple=False, url_args=dict( action='edit') ),
  33. grids.GridOperation( "Clone", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False, url_args=dict( action='clone') ),
  34. grids.GridOperation( "Share or Publish", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
  35. grids.GridOperation( "Delete", condition=( lambda item: not item.deleted ), async_compatible=True, confirm="Are you sure you want to delete this visualization?" ),
  36. ]
  37. def apply_query_filter( self, trans, query, **kwargs ):
  38. return query.filter_by( user=trans.user, deleted=False )
  39. class VisualizationAllPublishedGrid( grids.Grid ):
  40. # Grid definition
  41. use_panels = True
  42. use_async = True
  43. title = "Published Visualizations"
  44. model_class = model.Visualization
  45. default_sort_key = "update_time"
  46. default_filter = dict( title="All", username="All" )
  47. columns = [
  48. grids.PublicURLColumn( "Title", key="title", filterable="advanced" ),
  49. grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.VisualizationAnnotationAssociation, filterable="advanced" ),
  50. grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced" ),
  51. grids.CommunityRatingColumn( "Community Rating", key="rating" ),
  52. grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.VisualizationTagAssociation, filterable="advanced", grid_name="VisualizationAllPublishedGrid" ),
  53. grids.ReverseSortColumn( "Last Updated", key="update_time", format=time_ago )
  54. ]
  55. columns.append(
  56. grids.MulticolFilterColumn(
  57. "Search title, annotation, owner, and tags",
  58. cols_to_filter=[ columns[0], columns[1], columns[2], columns[4] ],
  59. key="free-text-search", visible=False, filterable="standard" )
  60. )
  61. def build_initial_query( self, trans, **kwargs ):
  62. # Join so that searching history.user makes sense.
  63. return trans.sa_session.query( self.model_class ).join( model.User.table )
  64. def apply_query_filter( self, trans, query, **kwargs ):
  65. return query.filter( self.model_class.deleted==False ).filter( self.model_class.published==True )
  66. class VisualizationController( BaseUIController, Sharable, UsesAnnotations,
  67. UsesHistoryDatasetAssociation, UsesVisualization,
  68. UsesItemRatings ):
  69. _user_list_grid = VisualizationListGrid()
  70. _published_list_grid = VisualizationAllPublishedGrid()
  71. @web.expose
  72. def list_published( self, trans, *args, **kwargs ):
  73. grid = self._published_list_grid( trans, **kwargs )
  74. if 'async' in kwargs:
  75. return grid
  76. else:
  77. # Render grid wrapped in panels
  78. return trans.fill_template( "visualization/list_published.mako", grid=grid )
  79. @web.expose
  80. @web.require_login( "use Galaxy visualizations", use_panels=True )
  81. def index( self, trans, *args, **kwargs ):
  82. """ Lists user's saved visualizations. """
  83. return self.list( trans, *args, **kwargs )
  84. @web.expose
  85. @web.require_login()
  86. def clone(self, trans, id, *args, **kwargs):
  87. visualization = self.get_visualization( trans, id, check_ownership=False )
  88. user = trans.get_user()
  89. owner = ( visualization.user == user )
  90. new_title = "Copy of '%s'" % visualization.title
  91. if not owner:
  92. new_title += " shared by %s" % visualization.user.email
  93. cloned_visualization = visualization.copy( user=trans.user, title=new_title )
  94. # Persist
  95. session = trans.sa_session
  96. session.add( cloned_visualization )
  97. session.flush()
  98. # Display the management page
  99. trans.set_message( 'Copy created with name "%s"' % cloned_visualization.title )
  100. return self.list( trans )
  101. @web.expose
  102. @web.require_login( "use Galaxy visualizations", use_panels=True )
  103. def list( self, trans, *args, **kwargs ):
  104. # Handle operation
  105. if 'operation' in kwargs and 'id' in kwargs:
  106. session = trans.sa_session
  107. operation = kwargs['operation'].lower()
  108. ids = util.listify( kwargs['id'] )
  109. for id in ids:
  110. item = session.query( model.Visualization ).get( trans.security.decode_id( id ) )
  111. if operation == "delete":
  112. item.deleted = True
  113. if operation == "share or publish":
  114. return self.sharing( trans, **kwargs )
  115. session.flush()
  116. # Build list of visualizations shared with user.
  117. shared_by_others = trans.sa_session \
  118. .query( model.VisualizationUserShareAssociation ) \
  119. .filter_by( user=trans.get_user() ) \
  120. .join( model.Visualization.table ) \
  121. .filter( model.Visualization.deleted == False ) \
  122. .order_by( desc( model.Visualization.update_time ) ) \
  123. .all()
  124. return trans.fill_template( "visualization/list.mako", grid=self._user_list_grid( trans, *args, **kwargs ), shared_by_others=shared_by_others )
  125. @web.expose
  126. @web.require_login( "modify Galaxy visualizations" )
  127. def set_slug_async( self, trans, id, new_slug ):
  128. """ Set item slug asynchronously. """
  129. visualization = self.get_visualization( trans, id )
  130. if visualization:
  131. visualization.slug = new_slug
  132. trans.sa_session.flush()
  133. return visualization.slug
  134. @web.expose
  135. @web.require_login( "use Galaxy visualizations" )
  136. def set_accessible_async( self, trans, id=None, accessible=False ):
  137. """ Set visualization's importable attribute and slug. """
  138. visualization = self.get_visualization( trans, id )
  139. # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed.
  140. importable = accessible in ['True', 'true', 't', 'T'];
  141. if visualization and visualization.importable != importable:
  142. if importable:
  143. self._make_item_accessible( trans.sa_session, visualization )
  144. else:
  145. visualization.importable = importable
  146. trans.sa_session.flush()
  147. return
  148. @web.expose
  149. @web.require_login( "rate items" )
  150. @web.json
  151. def rate_async( self, trans, id, rating ):
  152. """ Rate a visualization asynchronously and return updated community data. """
  153. visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )
  154. if not visualization:
  155. return trans.show_error_message( "The specified visualization does not exist." )
  156. # Rate visualization.
  157. visualization_rating = self.rate_item( trans.sa_session, trans.get_user(), visualization, rating )
  158. return self.get_ave_item_rating_data( trans.sa_session, visualization )
  159. @web.expose
  160. @web.require_login( "share Galaxy visualizations" )
  161. def imp( self, trans, id ):
  162. """ Import a visualization into user's workspace. """
  163. # Set referer message.
  164. referer = trans.request.referer
  165. if referer is not "":
  166. referer_message = "<a href='%s'>return to the previous page</a>" % referer
  167. else:
  168. referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' )
  169. # Do import.
  170. session = trans.sa_session
  171. visualization = self.get_visualization( trans, id, check_ownership=False )
  172. if visualization.importable == False:
  173. return trans.show_error_message( "The owner of this visualization has disabled imports via this link.<br>You can %s" % referer_message, use_panels=True )
  174. elif visualization.deleted:
  175. return trans.show_error_message( "You can't import this visualization because it has been deleted.<br>You can %s" % referer_message, use_panels=True )
  176. else:
  177. # Create imported visualization via copy.
  178. # TODO: need to handle custom db keys.
  179. imported_visualization = visualization.copy( user=trans.user, title="imported: " + visualization.title )
  180. # Persist
  181. session = trans.sa_session
  182. session.add( imported_visualization )
  183. session.flush()
  184. # Redirect to load galaxy frames.
  185. return trans.show_ok_message(
  186. message="""Visualization "%s" has been imported. <br>You can <a href="%s">start using this visualization</a> or %s."""
  187. % ( visualization.title, web.url_for( controller='visualization' ), referer_message ), use_panels=True )
  188. @web.expose
  189. @web.require_login( "share Galaxy visualizations" )
  190. def sharing( self, trans, id, **kwargs ):
  191. """ Handle visualization sharing. """
  192. # Get session and visualization.
  193. session = trans.sa_session
  194. visualization = self.get_visualization( trans, id, check_ownership=True )
  195. # Do operation on visualization.
  196. if 'make_accessible_via_link' in kwargs:
  197. self._make_item_accessible( trans.sa_session, visualization )
  198. elif 'make_accessible_and_publish' in kwargs:
  199. self._make_item_accessible( trans.sa_session, visualization )
  200. visualization.published = True
  201. elif 'publish' in kwargs:
  202. visualization.published = True
  203. elif 'disable_link_access' in kwargs:
  204. visualization.importable = False
  205. elif 'unpublish' in kwargs:
  206. visualization.published = False
  207. elif 'disable_link_access_and_unpublish' in kwargs:
  208. visualization.importable = visualization.published = False
  209. elif 'unshare_user' in kwargs:
  210. user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) )
  211. if not user:
  212. error( "User not found for provided id" )
  213. association = session.query( model.VisualizationUserShareAssociation ) \
  214. .filter_by( user=user, visualization=visualization ).one()
  215. session.delete( association )
  216. session.flush()
  217. return trans.fill_template( "/sharing_base.mako", item=visualization, use_panels=True )
  218. @web.expose
  219. @web.require_login( "share Galaxy visualizations" )
  220. def share( self, trans, id=None, email="", use_panels=False ):
  221. """ Handle sharing a visualization with a particular user. """
  222. msg = mtype = None
  223. visualization = self.get_visualization( trans, id, check_ownership=True )
  224. if email:
  225. other = trans.sa_session.query( model.User ) \
  226. .filter( and_( model.User.table.c.email==email,
  227. model.User.table.c.deleted==False ) ) \
  228. .first()
  229. if not other:
  230. mtype = "error"
  231. msg = ( "User '%s' does not exist" % email )
  232. elif other == trans.get_user():
  233. mtype = "error"
  234. msg = ( "You cannot share a visualization with yourself" )
  235. elif trans.sa_session.query( model.VisualizationUserShareAssociation ) \
  236. .filter_by( user=other, visualization=visualization ).count() > 0:
  237. mtype = "error"
  238. msg = ( "Visualization already shared with '%s'" % email )
  239. else:
  240. share = model.VisualizationUserShareAssociation()
  241. share.visualization = visualization
  242. share.user = other
  243. session = trans.sa_session
  244. session.add( share )
  245. self.create_item_slug( session, visualization )
  246. session.flush()
  247. trans.set_message( "Visualization '%s' shared with user '%s'" % ( visualization.title, other.email ) )
  248. return trans.response.send_redirect( url_for( action='sharing', id=id ) )
  249. return trans.fill_template( "/ind_share_base.mako",
  250. message = msg,
  251. messagetype = mtype,
  252. item=visualization,
  253. email=email,
  254. use_panels=use_panels )
  255. @web.expose
  256. def display_by_username_and_slug( self, trans, username, slug ):
  257. """ Display visualization based on a username and slug. """
  258. # Get visualization.
  259. session = trans.sa_session
  260. user = session.query( model.User ).filter_by( username=username ).first()
  261. visualization = trans.sa_session.query( model.Visualization ).filter_by( user=user, slug=slug, deleted=False ).first()
  262. if visualization is None:
  263. raise web.httpexceptions.HTTPNotFound()
  264. # Security check raises error if user cannot access visualization.
  265. self.security_check( trans, visualization, False, True)
  266. # Get rating data.
  267. user_item_rating = 0
  268. if trans.get_user():
  269. user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), visualization )
  270. if user_item_rating:
  271. user_item_rating = user_item_rating.rating
  272. else:
  273. user_item_rating = 0
  274. ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, visualization )
  275. # Display.
  276. visualization_config = self.get_visualization_config( trans, visualization )
  277. return trans.stream_template_mako( "visualization/display.mako", item = visualization, item_data = visualization_config,
  278. user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings,
  279. content_only=True )
  280. @web.expose
  281. @web.json
  282. @web.require_login( "get item name and link" )
  283. def get_name_and_link_async( self, trans, id=None ):
  284. """ Returns visualization's name and link. """
  285. visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )
  286. if self.create_item_slug( trans.sa_session, visualization ):
  287. trans.sa_session.flush()
  288. return_dict = { "name" : visualization.title, "link" : url_for( action="display_by_username_and_slug", username=visualization.user.username, slug=visualization.slug ) }
  289. return return_dict
  290. @web.expose
  291. def get_item_content_async( self, trans, id ):
  292. """ Returns item content in HTML format. """
  293. # Get visualization, making sure it's accessible.
  294. visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True )
  295. if visualization is None:
  296. raise web.httpexceptions.HTTPNotFound()
  297. # Return content.
  298. visualization_config = self.get_visualization_config( trans, visualization )
  299. return trans.fill_template_mako( "visualization/item_content.mako", encoded_id=trans.security.encode_id(visualization.id),
  300. item=visualization, item_data=visualization_config, content_only=True )
  301. @web.expose
  302. @web.require_login( "create visualizations" )
  303. def create( self, trans, visualization_title="", visualization_slug="", visualization_annotation="", visualization_dbkey="" ):
  304. """
  305. Create a new visualization
  306. """
  307. user = trans.get_user()
  308. visualization_title_err = visualization_slug_err = visualization_annotation_err = ""
  309. if trans.request.method == "POST":
  310. if not visualization_title:
  311. visualization_title_err = "visualization name is required"
  312. elif not visualization_slug:
  313. visualization_slug_err = "visualization id is required"
  314. elif not VALID_SLUG_RE.match( visualization_slug ):
  315. visualization_slug_err = "visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
  316. elif trans.sa_session.query( model.Visualization ).filter_by( user=user, slug=visualization_slug, deleted=False ).first():
  317. visualization_slug_err = "visualization id must be unique"
  318. else:
  319. # Create the new stored visualization
  320. visualization = model.Visualization()
  321. visualization.title = visualization_title
  322. visualization.slug = visualization_slug
  323. visualization.dbkey = visualization_dbkey
  324. visualization.type = 'trackster' # HACK: set visualization type to trackster since it's the only viz
  325. visualization_annotation = sanitize_html( visualization_annotation, 'utf-8', 'text/html' )
  326. self.add_item_annotation( trans.sa_session, trans.get_user(), visualization, visualization_annotation )
  327. visualization.user = user
  328. # And the first (empty) visualization revision
  329. visualization_revision = model.VisualizationRevision()
  330. visualization_revision.title = visualization_title
  331. visualization_revision.config = {}
  332. visualization_revision.dbkey = visualization_dbkey
  333. visualization_revision.visualization = visualization
  334. visualization.latest_revision = visualization_revision
  335. # Persist
  336. session = trans.sa_session
  337. session.add(visualization)
  338. session.add(visualization_revision)
  339. session.flush()
  340. return trans.response.send_redirect( web.url_for( action='list' ) )
  341. return trans.show_form(
  342. web.FormBuilder( web.url_for(), "Create new visualization", submit_text="Submit" )
  343. .add_text( "visualization_title", "Visualization title", value=visualization_title, error=visualization_title_err )
  344. .add_text( "visualization_slug", "Visualization identifier", value=visualization_slug, error=visualization_slug_err,
  345. help="""A unique identifier that will be used for
  346. public links to this visualization. A default is generated
  347. from the visualization title, but can be edited. This field
  348. must contain only lowercase letters, numbers, and
  349. the '-' character.""" )
  350. .add_select( "visualization_dbkey", "Visualization DbKey/Build", value=visualization_dbkey, options=self._get_dbkeys( trans ), error=None)
  351. .add_text( "visualization_annotation", "Visualization annotation", value=visualization_annotation, error=visualization_annotation_err,
  352. help="A description of the visualization; annotation is shown alongside published visualizations."),
  353. template="visualization/create.mako" )
  354. @web.expose
  355. @web.require_login( "edit visualizations" )
  356. def edit( self, trans, id, visualization_title="", visualization_slug="", visualization_annotation="" ):
  357. """
  358. Edit a visualization's attributes.
  359. """
  360. visualization = self.get_visualization( trans, id, check_ownership=True )
  361. session = trans.sa_session
  362. visualization_title_err = visualization_slug_err = visualization_annotation_err = ""
  363. if trans.request.method == "POST":
  364. if not visualization_title:
  365. visualization_title_err = "Visualization name is required"
  366. elif not visualization_slug:
  367. visualization_slug_err = "Visualization id is required"
  368. elif not VALID_SLUG_RE.match( visualization_slug ):
  369. visualization_slug_err = "Visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
  370. elif visualization_slug != visualization.slug and trans.sa_session.query( model.Visualization ).filter_by( user=visualization.user, slug=visualization_slug, deleted=False ).first():
  371. visualization_slug_err = "Visualization id must be unique"
  372. else:
  373. visualization.title = visualization_title
  374. visualization.slug = visualization_slug
  375. if visualization_annotation != "":
  376. visualization_annotation = sanitize_html( visualization_annotation, 'utf-8', 'text/html' )
  377. self.add_item_annotation( trans.sa_session, trans.get_user(), visualization, visualization_annotation )
  378. session.flush()
  379. # Redirect to visualization list.
  380. return trans.response.send_redirect( web.url_for( action='list' ) )
  381. else:
  382. visualization_title = visualization.title
  383. # Create slug if it's not already set.
  384. if visualization.slug is None:
  385. self.create_item_slug( trans.sa_session, visualization )
  386. visualization_slug = visualization.slug
  387. visualization_annotation = self.get_item_annotation_str( trans.sa_session, trans.user, visualization )
  388. if not visualization_annotation:
  389. visualization_annotation = ""
  390. return trans.show_form(
  391. web.FormBuilder( web.url_for( id=id ), "Edit visualization attributes", submit_text="Submit" )
  392. .add_text( "visualization_title", "Visualization title", value=visualization_title, error=visualization_title_err )
  393. .add_text( "visualization_slug", "Visualization identifier", value=visualization_slug, error=visualization_slug_err,
  394. help="""A unique identifier that will be used for
  395. public links to this visualization. A default is generated
  396. from the visualization title, but can be edited. This field
  397. must contain only lowercase letters, numbers, and
  398. the '-' character.""" )
  399. .add_text( "visualization_annotation", "Visualization annotation", value=visualization_annotation, error=visualization_annotation_err,
  400. help="A description of the visualization; annotation is shown alongside published visualizations."),
  401. template="visualization/create.mako" )
  402. def get_item( self, trans, id ):
  403. return self.get_visualization( trans, id )