[hg] galaxy 3226: Enhancements to personal and public grids for ...
details: http://www.bx.psu.edu/hg/galaxy/rev/a7deb5c197ad changeset: 3226:a7deb5c197ad user: jeremy goecks <jeremy.goecks@emory.edu> date: Tue Jan 12 11:37:45 2010 -0500 description: Enhancements to personal and public grids for pages, published histories, and workflows. Specific improvements: (a) added community tags to grids for published pages and published histories; (b) bug fixes for grids for published pages and published histories; © added individual tags for pages and workflows; (d) added user grid for stored/saved workflows (not yet included in default workflow view); (e) added grid for publicly available workflows; (f) added pretty URLs for public workflows and ability to view a public workflow. Fixes #232 diffstat: lib/galaxy/model/__init__.py | 1 + lib/galaxy/model/mapping.py | 1 + lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py | 38 + lib/galaxy/tags/tag_handler.py | 23 +- lib/galaxy/web/base/controller.py | 18 +- lib/galaxy/web/buildapp.py | 1 + lib/galaxy/web/controllers/history.py | 46 +- lib/galaxy/web/controllers/page.py | 24 +- lib/galaxy/web/controllers/tag.py | 18 +- lib/galaxy/web/controllers/workflow.py | 138 ++++++- lib/galaxy/web/framework/__init__.py | 6 + static/june_2007_style/autocomplete_tagging.css.tmpl | 6 +- static/june_2007_style/blue/autocomplete_tagging.css | 3 +- templates/history/list_public.mako | 2 +- templates/history/view.mako | 136 +++--- templates/page/display.mako | 2 - templates/page/editor.mako | 7 +- templates/root/history.mako | 21 +- templates/tagging_common.mako | 12 +- templates/workflow/display.mako | 195 ++++++++++ templates/workflow/list_public.mako | 23 + templates/workflow/sharing.mako | 7 + 22 files changed, 566 insertions(+), 162 deletions(-) diffs (1192 lines): diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/model/__init__.py Tue Jan 12 11:37:45 2010 -0500 @@ -1114,6 +1114,7 @@ self.id = None self.user = None self.name = None + self.slug = None self.latest_workflow_id = None self.workflows = [] diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/model/mapping.py Tue Jan 12 11:37:45 2010 -0500 @@ -513,6 +513,7 @@ Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), Column( "importable", Boolean, default=False ), + Column( "slug", TEXT, index=True ) ) Workflow.table = Table( "workflow", metadata, diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0032_stored_workflow_slug_column.py Tue Jan 12 11:37:45 2010 -0500 @@ -0,0 +1,38 @@ +""" +Migration script to add slug column for stored workflow. +""" + +from sqlalchemy import * +from migrate import * +from migrate.changeset import * + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +def upgrade(): + + print __doc__ + metadata.reflect() + + StoredWorkflow_table = Table( "stored_workflow", metadata, autoload=True ) + + # Create slug column. + c = Column( "slug", TEXT, index=True ) + c.create( StoredWorkflow_table ) + assert c is StoredWorkflow_table.c.slug + + # Create slug index. + try: + i = Index( "ix_stored_workflow_slug", StoredWorkflow_table.c.slug ) + i.create() + except: + # Mysql doesn't have a named index, but alter should work + StoredWorkflow_table.c.slug.alter( unique=False ) + +def downgrade(): + metadata.reflect() + + StoredWorkflow_table = Table( "stored_workflow", metadata, autoload=True ) + StoredWorkflow_table.c.slug.drop() diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/tags/tag_handler.py --- a/lib/galaxy/tags/tag_handler.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/tags/tag_handler.py Tue Jan 12 11:37:45 2010 -0500 @@ -1,9 +1,7 @@ -from galaxy.model import Tag +from galaxy import model import re from sqlalchemy.sql.expression import func, and_ from sqlalchemy.sql import select -from galaxy.model import History, HistoryTagAssociation, Dataset, DatasetTagAssociation, \ - HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation, Page, PageTagAssociation class TagHandler( object ): @@ -31,10 +29,11 @@ # Initialize with known classes. item_tag_assoc_info = {} - item_tag_assoc_info["History"] = ItemTagAssocInfo( History, HistoryTagAssociation, HistoryTagAssociation.table.c.history_id ) + item_tag_assoc_info["History"] = ItemTagAssocInfo( model.History, model.HistoryTagAssociation, model.HistoryTagAssociation.table.c.history_id ) item_tag_assoc_info["HistoryDatasetAssociation"] = \ - ItemTagAssocInfo( HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation, HistoryDatasetAssociationTagAssociation.table.c.history_dataset_association_id ) - item_tag_assoc_info["Page"] = ItemTagAssocInfo( Page, PageTagAssociation, PageTagAssociation.table.c.page_id ) + ItemTagAssocInfo( model.HistoryDatasetAssociation, model.HistoryDatasetAssociationTagAssociation, model.HistoryDatasetAssociationTagAssociation.table.c.history_dataset_association_id ) + item_tag_assoc_info["Page"] = ItemTagAssocInfo( model.Page, model.PageTagAssociation, model.PageTagAssociation.table.c.page_id ) + item_tag_assoc_info["StoredWorkflow"] = ItemTagAssocInfo( model.StoredWorkflow, model.StoredWorkflowTagAssociation, model.StoredWorkflowTagAssociation.table.c.stored_workflow_id ) def get_tag_assoc_class(self, item_class): """ Returns tag association class for item class. """ @@ -55,7 +54,7 @@ # Build select statement. cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count('*') ] - from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) + from_obj = item_tag_assoc_class.table.join( item_class.table ).join( model.Tag.table ) where_clause = ( self.get_id_col_in_item_tag_assoc_table(item_class) == item.id ) order_by = [ func.count("*").desc() ] group_by = item_tag_assoc_class.table.c.tag_id @@ -101,7 +100,7 @@ # Get tag name. if isinstance(tag, basestring): tag_name = tag - elif isinstance(tag, Tag): + elif isinstance(tag, model.Tag): tag_name = tag.name # Check for an item-tag association to see if item has a given tag. @@ -168,12 +167,12 @@ def get_tag_by_id(self, db_session, tag_id): """Get a Tag object from a tag id.""" - return db_session.query(Tag).filter(Tag.id==tag_id).first() + return db_session.query( model.Tag ).filter_by( id=tag_id) .first() def get_tag_by_name(self, db_session, tag_name): """Get a Tag object from a tag name (string).""" if tag_name: - return db_session.query( Tag ).filter( Tag.name==tag_name.lower() ).first() + return db_session.query( model.Tag ).filter_by( name=tag_name.lower() ).first() return None def _create_tag(self, db_session, tag_str): @@ -184,9 +183,9 @@ for sub_tag in tag_hierarchy: # Get or create subtag. tag_name = tag_prefix + self._scrub_tag_name(sub_tag) - tag = db_session.query(Tag).filter(Tag.name==tag_name).first() + tag = db_session.query( model.Tag ).filter_by( name=tag_name).first() if not tag: - tag = Tag(type=0, name=tag_name) + tag = model.Tag(type=0, name=tag_name) # Set tag parent. tag.parent = parent_tag diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/base/controller.py Tue Jan 12 11:37:45 2010 -0500 @@ -2,7 +2,7 @@ Contains functionality needed in every web interface """ -import os, time, logging +import os, time, logging, re # Pieces of Galaxy to make global in every controller from galaxy import config, tools, web, model, util @@ -28,7 +28,7 @@ return dict( action='display_by_username_and_slug', username=item.user.username, slug=item.slug ) else: return None - + class BaseController( object ): """ Base class for Galaxy web application controllers. @@ -57,6 +57,20 @@ if history.user != user: error( "History is not owned by current user" ) return history + + def make_item_importable( self, sa_session, item ): + """ Makes item importable and sets item's slug. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ + item.importable = True + + # Set history slug. Slug must be unique among user's importable pages. + slug_base = re.sub( "\s+", "-", item.name.lower() ) + slug = slug_base + count = 1 + while sa_session.query( item.__class__ ).filter_by( user=item.user, slug=slug, importable=True ).count() != 0: + # Slug taken; choose a new slug based on count. This approach can handle numerous histories with the same name gracefully. + slug = '%s-%i' % ( slug_base, count ) + count += 1 + item.slug = slug Root = BaseController """ diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/buildapp.py --- a/lib/galaxy/web/buildapp.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/buildapp.py Tue Jan 12 11:37:45 2010 -0500 @@ -81,6 +81,7 @@ webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) + webapp.add_route( '/u/:username/w/:slug', controller='workflow', action='display_by_username_and_slug' ) webapp.finalize_config() # Wrap the webapp in some useful middleware if kwargs.get( 'middleware', True ): diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/controllers/history.py Tue Jan 12 11:37:45 2010 -0500 @@ -10,19 +10,18 @@ import webhelpers, logging, operator from datetime import datetime from cgi import escape -import re log = logging.getLogger( __name__ ) # States for passing messages SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" +class NameColumn( grids.TextColumn ): + def get_value( self, trans, grid, history ): + return history.get_display_name() + class HistoryListGrid( grids.Grid ): # Custom column types - class NameColumn( grids.TextColumn ): - def get_value(self, trans, grid, history): - return history.get_display_name() - class DatasetsByStateColumn( grids.GridColumn ): def get_value( self, trans, grid, history ): rval = [] @@ -174,21 +173,24 @@ return query.filter( model.HistoryUserShareAssociation.user == trans.user ) class PublicHistoryListGrid( grids.Grid ): + class NameURLColumn( PublicURLColumn, NameColumn ): + pass + title = "Public Histories" model_class = model.History default_sort_key = "-update_time" default_filter = dict( public_url="All", username="All", tags="All" ) use_async = True columns = [ - PublicURLColumn( "Name", key="name", model_class=model.History, filterable="advanced"), + NameURLColumn( "Name", key="name", model_class=model.History, filterable="advanced" ), OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), - grids.GridColumn( "Created", key="create_time", format=time_ago ), + grids.CommunityTagsColumn( "Community Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced", grid_name="PublicHistoryListGrid" ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ) ] columns.append( grids.MulticolFilterColumn( "Search", - cols_to_filter=[ columns[0], columns[1] ], + cols_to_filter=[ columns[0], columns[1], columns[2] ], key="free-text-search", visible=False, filterable="standard" ) ) operations = [] @@ -213,7 +215,6 @@ public_list_grid = PublicHistoryListGrid() @web.expose - @web.require_login() def list_public( self, trans, **kwargs ): grid = self.public_list_grid( trans, **kwargs ) if 'async' in kwargs: @@ -272,7 +273,7 @@ elif operation == "enable import via link": for history in histories: if not history.importable: - self.make_history_importable( trans.sa_session, history ) + self.make_item_importable( trans.sa_session, history ) elif operation == "disable import via link": if history_ids: histories = [ self.get_history( trans, history_id ) for history_id in history_ids ] @@ -432,7 +433,7 @@ importable = importable in ['True', 'true', 't', 'T']; if history and history.importable != importable: if importable: - self.make_history_importable( trans.sa_session, history ) + self.make_item_importable( trans.sa_session, history ) else: history.importable = importable trans.sa_session.flush() @@ -536,8 +537,9 @@ user_owns_history = user_owns_history, show_deleted = False ) - @web.expose + @web.expose def display_by_username_and_slug( self, trans, username, slug ): + """ Display history based on a username and slug. """ session = trans.sa_session user = session.query( model.User ).filter_by( username=username ).first() if user is None: @@ -545,7 +547,7 @@ history = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False, importable=True ).first() if history is None: raise web.httpexceptions.HTTPNotFound() - + query = trans.sa_session.query( model.HistoryDatasetAssociation ) \ .filter( model.HistoryDatasetAssociation.history == history ) \ .options( eagerload( "children" ) ) \ @@ -848,7 +850,7 @@ for history in histories: trans.sa_session.add( history ) if params.get( 'enable_import_via_link', False ): - self.make_history_importable( trans.sa_session, history ) + self.make_item_importable( trans.sa_session, history ) trans.sa_session.flush() elif params.get( 'disable_import_via_link', False ): history.importable = False @@ -964,18 +966,4 @@ msg = 'Clone with name "%s" is now included in your previously stored histories.' % new_history.name else: msg = '%d cloned histories are now included in your previously stored histories.' % len( histories ) - return trans.show_ok_message( msg ) - - def make_history_importable( self, sa_session, history ): - """ Makes history importable and sets history's slug. Does not flush/commit changes, however. """ - history.importable = True - - # Set history slug. Slug must be unique among user's importable pages. - slug_base = re.sub( "\s+", "-", history.name.lower() ) - slug = slug_base - count = 1 - while sa_session.query( model.History ).filter_by( user=history.user, slug=slug, importable=True ).count() != 0: - # Slug taken; choose a new slug based on count. This approach can handle numerous histories with the same name gracefully. - slug = '%s-%i' % ( slug_base, count ) - count += 1 - history.slug = slug \ No newline at end of file + return trans.show_ok_message( msg ) \ No newline at end of file diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/controllers/page.py --- a/lib/galaxy/web/controllers/page.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/controllers/page.py Tue Jan 12 11:37:45 2010 -0500 @@ -14,19 +14,31 @@ return "" class PageListGrid( grids.Grid ): + # Custom column. + class URLColumn( PublicURLColumn ): + def get_value( self, trans, grid, item ): + return url_for( action='display_by_username_and_slug', username=item.user.username, slug=item.slug ) + # Grid definition use_panels = True title = "Pages" model_class = model.Page - default_filter = { "published" : "All"} + default_filter = { "published" : "All", "tags" : "All", "title" : "All"} default_sort_key = "-create_time" columns = [ - grids.TextColumn( "Title", key="title", model_class=model.Page, attach_popup=True, filterable="standard" ), - PublicURLColumn( "Public URL" ), - grids.GridColumn( "Published", key="published", format=format_bool, filterable="standard" ), + grids.TextColumn( "Title", key="title", model_class=model.Page, attach_popup=True, filterable="advanced" ), + URLColumn( "Public URL" ), + grids.IndividualTagsColumn( "Tags", "tags", model.Page, model.PageTagAssociation, filterable="advanced", grid_name="PageListGrid" ), + grids.GridColumn( "Published", key="published", format=format_bool, filterable="advanced" ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), ] + columns.append( + grids.MulticolFilterColumn( + "Search", + cols_to_filter=[ columns[0], columns[2] ], + key="free-text-search", visible=False, filterable="standard" ) + ) global_actions = [ grids.GridAction( "Add new page", dict( action='create' ) ) ] @@ -52,13 +64,13 @@ columns = [ PublicURLColumn( "Title", key="title", model_class=model.Page, filterable="advanced"), OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), - grids.GridColumn( "Created", key="create_time", format=time_ago ), + grids.CommunityTagsColumn( "Community Tags", "tags", model.Page, model.PageTagAssociation, filterable="advanced", grid_name="PageAllPublishedGrid" ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ) ] columns.append( grids.MulticolFilterColumn( "Search", - cols_to_filter=[ columns[0], columns[1] ], + cols_to_filter=[ columns[0], columns[1], columns[2] ], key="free-text-search", visible=False, filterable="standard" ) ) def build_initial_query( self, session ): diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/controllers/tag.py --- a/lib/galaxy/web/controllers/tag.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/controllers/tag.py Tue Jan 12 11:37:45 2010 -0500 @@ -2,8 +2,6 @@ Tags Controller: handles tagging/untagging of entities and provides autocomplete support. """ -from galaxy.model import History, HistoryTagAssociation, Dataset, DatasetTagAssociation, \ - HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation, Page, PageTagAssociation from galaxy.web.base.controller import * from galaxy.tags.tag_handler import * from sqlalchemy.sql.expression import func, and_ @@ -75,11 +73,13 @@ # Get item class. TODO: we should have a mapper that goes from class_name to class object. if item_class == 'History': - item_class = History + item_class = model.History elif item_class == 'HistoryDatasetAssociation': - item_class = HistoryDatasetAssociation + item_class = model.HistoryDatasetAssociation elif item_class == 'Page': - item_class = Page + item_class = model.Page + elif item_class == 'StoredWorkflow': + item_class = model.StoredWorkflow q = q.encode('utf-8') if q.find(":") == -1: @@ -104,9 +104,9 @@ # Build select statement. cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count('*') ] - from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) + from_obj = item_tag_assoc_class.table.join( item_class.table ).join( model.Tag.table ) where_clause = and_( - Tag.table.c.name.like(q + "%"), + model.Tag.table.c.name.like(q + "%"), item_tag_assoc_class.table.c.user_id == user.id ) order_by = [ func.count("*").desc() ] @@ -155,9 +155,9 @@ # Build select statement. cols_to_select = [ item_tag_assoc_class.table.c.value, func.count('*') ] - from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) + from_obj = item_tag_assoc_class.table.join( item_class.table ).join( model.Tag.table ) where_clause = and_( item_tag_assoc_class.table.c.user_id == user.id, - Tag.table.c.id==tag.id, + model.Tag.table.c.id==tag.id, item_tag_assoc_class.table.c.value.like(tag_value + "%") ) order_by = [ func.count("*").desc(), item_tag_assoc_class.table.c.value ] group_by = item_tag_assoc_class.table.c.value diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/controllers/workflow.py --- a/lib/galaxy/web/controllers/workflow.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/controllers/workflow.py Tue Jan 12 11:37:45 2010 -0500 @@ -4,6 +4,7 @@ pkg_resources.require( "simplejson" ) import simplejson +from galaxy.web.framework.helpers import time_ago, iff, grids from galaxy.tools.parameters import * from galaxy.tools import DefaultToolState from galaxy.tools.parameters.grouping import Repeat, Conditional @@ -15,11 +16,89 @@ from galaxy.model.mapping import desc from galaxy.model.orm import * +class StoredWorkflowListGrid( grids.Grid ): + class StepsColumn( grids.GridColumn ): + def get_value(self, trans, grid, workflow): + return len( workflow.latest_workflow.steps ) + + # Grid definition + use_panels = True + title = "Saved Workflows" + model_class = model.StoredWorkflow + 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" ), + StepsColumn( "Steps" ), + grids.GridColumn( "Created", key="create_time", format=time_ago ), + grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), + ] + columns.append( + grids.MulticolFilterColumn( + "Search", + cols_to_filter=[ columns[0], columns[1] ], + key="free-text-search", visible=False, filterable="standard" ) + ) + operations = [ + grids.GridOperation( "Edit", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Run", condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Clone", condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Rename", condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Sharing", condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Delete", condition=( lambda item: item.deleted ), async_compatible=True ), + ] + def apply_default_filter( self, trans, query, **kwargs ): + return query.filter_by( user=trans.user, deleted=False ) + +class PublicStoredWorkflowListGrid( grids.Grid ): + title = "Public Workflows" + model_class = model.StoredWorkflow + default_sort_key = "-update_time" + default_filter = dict( public_url="All", username="All", tags="All" ) + use_async = True + columns = [ + PublicURLColumn( "Name", key="name", model_class=model.StoredWorkflow, filterable="advanced" ), + OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), + grids.CommunityTagsColumn( "Community Tags", "tags", model.StoredWorkflow, model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ), + grids.GridColumn( "Last Updated", key="update_time", format=time_ago ) + ] + columns.append( + grids.MulticolFilterColumn( + "Search", + cols_to_filter=[ columns[0], columns[1], columns[2] ], + key="free-text-search", visible=False, filterable="standard" ) + ) + operations = [] + def build_initial_query( self, session ): + # Join so that searching stored_workflow.user makes sense. + return session.query( self.model_class ).join( model.User.table ) + def apply_default_filter( self, trans, query, **kwargs ): + # A public workflow is importable, has a slug, and is not deleted. + return query.filter( self.model_class.importable==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) + class WorkflowController( BaseController ): + stored_list_grid = StoredWorkflowListGrid() + public_list_grid = PublicStoredWorkflowListGrid() @web.expose def index( self, trans ): return trans.fill_template( "workflow/index.mako" ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def list_grid( self, trans, **kwargs ): + """ List user's stored workflows. """ + status = message = None + if 'operation' in kwargs: + operation = kwargs['operation'].lower() + print operation + if operation == "rename": + return self.rename( trans, **kwargs ) + history_ids = util.listify( kwargs.get( 'id', [] ) ) + if operation == "sharing": + return self.sharing( trans, id=history_ids ) + return self.stored_list_grid( trans, **kwargs ) @web.expose @web.require_login( "use Galaxy workflows" ) @@ -64,7 +143,52 @@ return trans.fill_template( "workflow/list_for_run.mako", workflows = workflows, shared_by_others = shared_by_others ) - + + @web.expose + def list_public( self, trans, **kwargs ): + grid = self.public_list_grid( trans, **kwargs ) + if 'async' in kwargs: + return grid + else: + # Render grid wrapped in panels + return trans.fill_template( "workflow/list_public.mako", grid=grid ) + + @web.expose + def display_by_username_and_slug( self, trans, username, slug ): + """ View workflow based on a username and slug. """ + session = trans.sa_session + + # Get and verify user and stored workflow. + user = session.query( model.User ).filter_by( username=username ).first() + if user is None: + raise web.httpexceptions.HTTPNotFound() + stored_workflow = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False, importable=True ).first() + if stored_workflow is None: + raise web.httpexceptions.HTTPNotFound() + + # Get data for workflow's steps. + for step in stored_workflow.latest_workflow.steps: + if step.type == 'tool' or step.type is None: + # Restore the tool state for the step + module = module_factory.from_workflow_step( trans, step ) + # Any connected input needs to have value DummyDataset (these + # are not persisted so we need to do it every time) + module.add_dummy_datasets( connections=step.input_connections ) + # Store state with the step + step.module = module + step.state = module.state + # Error dict + if step.tool_errors: + errors[step.id] = step.tool_errors + else: + ## Non-tool specific stuff? + step.module = module_factory.from_workflow_step( trans, step ) + step.state = step.module.get_runtime_state() + # Connections by input name + step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) + + return trans.fill_template_mako( "workflow/display.mako", workflow = stored_workflow, steps = stored_workflow.latest_workflow.steps ) + @web.expose @web.require_login( "use Galaxy workflows" ) def share( self, trans, id, email="" ): @@ -108,7 +232,7 @@ stored = get_stored_workflow( trans, id ) session.add( stored ) if 'enable_import_via_link' in kwargs: - stored.importable = True + self.make_item_importable( trans.sa_session, stored ) session.flush() elif 'disable_import_via_link' in kwargs: stored.importable = False @@ -150,15 +274,19 @@ @web.expose @web.require_login( "use Galaxy workflows" ) - def rename( self, trans, id, new_name=None ): + def rename( self, trans, id, new_name=None, **kwargs ): stored = get_stored_workflow( trans, id ) if new_name is not None: stored.name = new_name trans.sa_session.flush() - trans.set_message( "Workflow renamed to '%s'." % new_name ) + # For current workflows grid: + trans.set_message ( "Workflow renamed to '%s'." % new_name ) return self.list( trans ) + # For new workflows grid: + #message = "Workflow renamed to '%s'." % new_name + #return self.list_grid( trans, message=message, status='done' ) else: - return form( url_for( id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ + return form( url_for( action='rename', id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ .add_text( "new_name", "Workflow Name", value=stored.name ) @web.expose diff -r 56efe838b9af -r a7deb5c197ad lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py Mon Jan 11 17:04:45 2010 -0500 +++ b/lib/galaxy/web/framework/__init__.py Tue Jan 12 11:37:45 2010 -0500 @@ -553,6 +553,12 @@ context. """ self.template_context['message'] = message + def get_message( self ): + """ + Convenience method for getting the 'message' element of the template + context. + """ + return self.template_context['message'] def show_message( self, message, type='info', refresh_frames=[], cont=None ): """ Convenience method for displaying a simple page with a single message. diff -r 56efe838b9af -r a7deb5c197ad static/june_2007_style/autocomplete_tagging.css.tmpl --- a/static/june_2007_style/autocomplete_tagging.css.tmpl Mon Jan 11 17:04:45 2010 -0500 +++ b/static/june_2007_style/autocomplete_tagging.css.tmpl Tue Jan 12 11:37:45 2010 -0500 @@ -67,7 +67,11 @@ .tag-area { width: 100%; cursor: pointer; - border: solid 1px #eee; +} + +.individual-tags +{ + border:solid 1px #eee; } .active-tag-area { diff -r 56efe838b9af -r a7deb5c197ad static/june_2007_style/blue/autocomplete_tagging.css --- a/static/june_2007_style/blue/autocomplete_tagging.css Mon Jan 11 17:04:45 2010 -0500 +++ b/static/june_2007_style/blue/autocomplete_tagging.css Tue Jan 12 11:37:45 2010 -0500 @@ -6,7 +6,8 @@ .ac_even{margin-left:0.3em;} .ac_over{background-color:#0A246A;color:white;} .ac_header{font-style:normal;color:gray;border-bottom:0.1em solid gray;} -.tag-area{width:100%;cursor:pointer;border:solid 1px #eee;} +.tag-area{width:100%;cursor:pointer;} +.individual-tags{border:solid 1px #eee;} .active-tag-area{background-color:white;} .toggle-link{font-weight:normal;padding:0.3em;margin-bottom:1em;width:100%;padding:0.2em 0em 0.2em 0em;} .tag-button{width:auto;color:#444;text-decoration:none;display:inline-block;cursor:pointer;margin:0.2em;border:0;padding:0.1em 0.5em 0.1em 0.5em;-moz-border-radius:.5em;-webkit-border-radius:.5em;border-radius:.5em;background:#bbb;} diff -r 56efe838b9af -r a7deb5c197ad templates/history/list_public.mako --- a/templates/history/list_public.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/history/list_public.mako Tue Jan 12 11:37:45 2010 -0500 @@ -15,7 +15,7 @@ <div style="overflow: auto; height: 100%;"> <div class="page-container" style="padding: 10px;"> - ${grid} + ${unicode( grid, 'utf-8' )} </div> </div> diff -r 56efe838b9af -r a7deb5c197ad templates/history/view.mako --- a/templates/history/view.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/history/view.mako Tue Jan 12 11:37:45 2010 -0500 @@ -256,14 +256,11 @@ // function community_tag_click(tag_name, tag_value) { - // Do nothing until community tags are implemented in public histories grid. - /* var href = '${h.url_for( controller='/history', action='list_public')}'; href = href + "?f-tags=" + tag_name; if (tag_value != null && tag_value != "") href = href + ":" + tag_value; self.location = href; - */ } </script> </%def> @@ -339,78 +336,79 @@ </div> <div class="unified-panel-body"> - <div class="page-body"> - ## Render view of history. - <div id="top-links" class="historyLinks" style="padding: 0px 0px 5px 0px"> - %if not user_owns_history: - <a href="${h.url_for( controller='history', action='imp', id=trans.security.encode_id(history.id) )}">import and start using history</a> | + <div style="overflow: auto; height: 100%;"> + <div class="page-body"> + ## Render view of history. + <div id="top-links" class="historyLinks" style="padding: 0px 0px 5px 0px"> + %if not user_owns_history: + <a href="${h.url_for( controller='history', action='imp', id=trans.security.encode_id(history.id) )}">import and start using history</a> | + %endif + <a href="${get_history_link( history )}">${_('refresh')}</a> + %if show_deleted: + | <a href="${h.url_for('history', show_deleted=False)}">${_('hide deleted')}</a> + %endif + </div> + + <div id="history-name-area" class="historyLinks" style="color: gray; font-weight: bold; padding: 0px 0px 5px 0px"> + %if user_owns_history: + <div style="float: right"><a id="history-rename" title="Rename" class="icon-button edit" target="galaxy_main" href="${h.url_for( controller='history', action='rename' )}"></a></div> + %endif + <div id="history-name">${history.get_display_name()}</div> + </div> + + %if history.deleted: + <div class="warningmessagesmall"> + ${_('You are currently viewing a deleted history!')} + </div> + <p></p> %endif - <a href="${get_history_link( history )}">${_('refresh')}</a> - %if show_deleted: - | <a href="${h.url_for('history', show_deleted=False)}">${_('hide deleted')}</a> + + %if not datasets: + + <div class="infomessagesmall" id="emptyHistoryMessage"> + + %else: + + ## Render requested datasets, ordered from newest to oldest + %for data in datasets: + %if data.visible: + <div class="historyItemContainer visible-right-border" id="historyItemContainer-${data.id}"> + ${render_dataset( data, data.hid, show_deleted_on_refresh = show_deleted, user_owns_dataset=user_owns_history )} + </div> + %endif + %endfor + + <div class="infomessagesmall" id="emptyHistoryMessage" style="display:none;"> %endif + ${_("Your history is empty. Click 'Get Data' on the left pane to start")} + </div> </div> - - <div id="history-name-area" class="historyLinks" style="color: gray; font-weight: bold; padding: 0px 0px 5px 0px"> - %if user_owns_history: - <div style="float: right"><a id="history-rename" title="Rename" class="icon-button edit" target="galaxy_main" href="${h.url_for( controller='history', action='rename' )}"></a></div> - %endif - <div id="history-name">${history.get_display_name()}</div> - </div> - - %if history.deleted: - <div class="warningmessagesmall"> - ${_('You are currently viewing a deleted history!')} + + <div class="page-meta"> + ## Histories. + <div><strong>Related Histories</strong></div> + <p> + <a href="${h.url_for ( controller='/history', action='list_public' )}">All public histories</a><br> + <a href="${href_to_user_histories}">Histories owned by ${history.user.username}</a> + + ## Tags. + <div><strong>Tags</strong></div> + <p> + ## Community tags. + <div> + Community: + ${render_community_tagging_element( tagged_item=history, tag_click_fn='community_tag_click', use_toggle_link=False )} + %if len ( history.tags ) == 0: + none + %endif </div> - <p></p> - %endif - - %if not datasets: - - <div class="infomessagesmall" id="emptyHistoryMessage"> - - %else: - - ## Render requested datasets, ordered from newest to oldest - %for data in datasets: - %if data.visible: - <div class="historyItemContainer visible-right-border" id="historyItemContainer-${data.id}"> - ${render_dataset( data, data.hid, show_deleted_on_refresh = show_deleted, user_owns_dataset=user_owns_history )} - </div> - %endif - %endfor - - <div class="infomessagesmall" id="emptyHistoryMessage" style="display:none;"> - %endif - ${_("Your history is empty. Click 'Get Data' on the left pane to start")} + ## Individual tags. + <p> + <div> + Yours: + ${render_individual_tagging_element( user=trans.get_user(), tagged_item=history, elt_context='view.mako', use_toggle_link=False )} </div> - </div> - - <div class="page-meta"> - ## Histories. - <div><strong>Related Histories</strong></div> - <p> - <a href="${h.url_for ( controller='/history', action='list_public' )}">All public histories</a><br> - <a href="${href_to_user_histories}">Histories owned by ${history.user.username}</a> - - ## Tags. - <div><strong>Tags</strong></div> - <p> - ## Community tags. - <div> - Community: - ${render_community_tagging_element( tagged_item=history, tag_click_fn='community_tag_click', use_toggle_link=False )} - %if len ( history.tags ) == 0: - none - %endif - </div> - ## Individual tags. - <p> - <div> - Yours: - ${render_individual_tagging_element( user=trans.get_user(), tagged_item=history, elt_context='view.mako', use_toggle_link=False )} </div> </div> - </div> </%def> diff -r 56efe838b9af -r a7deb5c197ad templates/page/display.mako --- a/templates/page/display.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/page/display.mako Tue Jan 12 11:37:45 2010 -0500 @@ -151,13 +151,11 @@ function community_tag_click(tag_name, tag_value) { // Do nothing until community tags implemented in published pages grid. - /* var href = '${h.url_for( controller='/page', action='list_published')}'; href = href + "?f-tags=" + tag_name; if (tag_value != null && tag_value != "") href = href + ":" + tag_value; self.location = href; - */ } </script> </%def> diff -r 56efe838b9af -r a7deb5c197ad templates/page/editor.mako --- a/templates/page/editor.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/page/editor.mako Tue Jan 12 11:37:45 2010 -0500 @@ -260,7 +260,12 @@ // Build href from history info. var href; if (history_slug != "" && history_user_username != "") - var href = "/u/" + history_user_username + "/h/" + history_slug; + { + var href = + '${h.url_for( controller='/history', action='display_by_username_and_slug', username='USERNAME', slug='SLUG' )}'; + href = href.replace('USERNAME', history_user_username); + href = href.replace('SLUG', history_slug); + } else var href = '${h.url_for( controller='/history', action='view' )}?id=' + item_id; wym.insert("<a href='" + href + "'>History '" + history_name + "'</a>"); diff -r 56efe838b9af -r a7deb5c197ad templates/root/history.mako --- a/templates/root/history.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/root/history.mako Tue Jan 12 11:37:45 2010 -0500 @@ -260,25 +260,6 @@ count++; return count; }; - - // - // Function provides text for tagging toggle link. - // - var get_toggle_link_text = function(tags) - { - var text = ""; - var num_tags = array_length(tags); - if (num_tags != 0) - { - text = num_tags + (num_tags != 1 ? " Tags" : " Tag"); - } - else - { - // No tags. - text = "Add tags to history"; - } - return text; - }; </script> <style> @@ -327,7 +308,7 @@ margin-bottom: 0.5em; } </style> - ${render_individual_tagging_element( user=trans.get_user(), tagged_item=history, elt_context='history.mako', get_toggle_link_text_fn='get_toggle_link_text' )} + ${render_individual_tagging_element( user=trans.get_user(), tagged_item=history, elt_context='history.mako' )} %endif %if not datasets: diff -r 56efe838b9af -r a7deb5c197ad templates/tagging_common.mako --- a/templates/tagging_common.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/tagging_common.mako Tue Jan 12 11:37:45 2010 -0500 @@ -20,7 +20,7 @@ %endif ## Render HTML for a list of tags. -<%def name="render_tagging_element_html(elt_id=None, tags=None, editable=True, use_toggle_link=True, input_size='15', in_form=False)"> +<%def name="render_tagging_element_html(elt_id=None, tags=None, editable=True, use_toggle_link=True, input_size='15', in_form=False, tag_type='individual')"> ## Useful attributes. <% num_tags = len( tags ) @@ -35,9 +35,13 @@ %endif > %if use_toggle_link: - <a class="toggle-link" href="#">${num_tags} Tags</a> + <a class="toggle-link" href="#">${num_tags} Tag${iff( num_tags == 1, "", "s")}</a> %endif - <div class="tag-area"> + <div class="tag-area + %if tag_type == 'individual': + individual-tags + %endif + "> ## Build buttons for current tags. %for tag in tags: @@ -88,7 +92,7 @@ elt_id = int ( floor ( random()*maxint ) ) community_tags = tag_handler.get_community_tags(trans.sa_session, tagged_item, 10) %> - ${self.render_tagging_element_html(elt_id=elt_id, tags=community_tags, use_toggle_link=use_toggle_link, editable=False)} + ${self.render_tagging_element_html(elt_id=elt_id, tags=community_tags, use_toggle_link=use_toggle_link, editable=False, tag_type="community")} ## Set up tag click function. <script type="text/javascript"> diff -r 56efe838b9af -r a7deb5c197ad templates/workflow/display.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/workflow/display.mako Tue Jan 12 11:37:45 2010 -0500 @@ -0,0 +1,195 @@ +<%inherit file="/base_panels.mako"/> +<%namespace file="/display_common.mako" import="get_history_link" /> +<%namespace file="../tagging_common.mako" import="render_individual_tagging_element, render_community_tagging_element" /> + +<%! from galaxy.tools.parameters import DataToolParameter %> + +<%def name="javascripts()"> + ${parent.javascripts()} + ${h.js( "galaxy.base", "jquery", "json2", "jquery.autocomplete", "autocomplete_tagging" )} + + <script type="text/javascript"> + // + // Handle click on community tag. + // + function community_tag_click(tag_name, tag_value) + { + var href = '${h.url_for( controller='/workflow', action='list_public')}'; + href = href + "?f-tags=" + tag_name; + if (tag_value != null && tag_value != "") + href = href + ":" + tag_value; + self.location = href; + } + </script> +</%def> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + ${h.css( "workflow", "autocomplete_tagging" )} + <style type="text/css"> + .page-body + { + padding: 10px; + float: left; + width: 65%; + } + .page-meta + { + float: right; + width: 27%; + padding: 0.5em; + margin: 0.25em; + vertical-align: text-top; + border: 2px solid #DDDDDD; + border-top: 4px solid #DDDDDD; + } + div.toolForm{ + margin-top: 10px; + margin-bottom: 10px; + } + </style> + <noscript> + <style> + .historyItemBody { + display: block; + } + </style> + </noscript> +</%def> + +<%def name="init()"> +<% + self.has_left_panel=False + self.has_right_panel=False + self.message_box_visible=False +%> +</%def> + +<%def name="do_inputs( inputs, values, prefix, step, other_values=None )"> + %for input_index, input in enumerate( inputs.itervalues() ): + %if input.type == "repeat": + <div class="repeat-group"> + <div class="form-title-row"><b>${input.title_plural}</b></div> + <% repeat_values = values[input.name] %> + %for i in range( len( repeat_values ) ): + <div class="repeat-group-item"> + <% index = repeat_values[i]['__index__'] %> + <div class="form-title-row"><b>${input.title} ${i + 1}</b></div> + ${do_inputs( input.inputs, repeat_values[ i ], prefix + input.name + "_" + str(index) + "|", step, other_values )} + + </div> + %endfor + </div> + %elif input.type == "conditional": + <% group_values = values[input.name] %> + <% current_case = group_values['__current_case__'] %> + <% new_prefix = prefix + input.name + "|" %> + ${row_for_param( input.test_param, group_values[ input.test_param.name ], other_values, prefix, step )} + ${do_inputs( input.cases[ current_case ].inputs, group_values, new_prefix, step, other_values )} + %else: + ${row_for_param( input, values[ input.name ], other_values, prefix, step )} + %endif + %endfor +</%def> + +<%def name="row_for_param( param, value, other_values, prefix, step )"> + <% cls = "form-row" %> + <div class="${cls}"> + <label>${param.get_label()}</label> + <div> + %if isinstance( param, DataToolParameter ): + %if ( prefix + param.name ) in step.input_connections_by_name: + <% + conn = step.input_connections_by_name[ prefix + param.name ] + %> + Output dataset '${conn.output_name}' from step ${int(conn.output_step.order_index)+1} + %else: + ## FIXME: Initialize in the controller + <% + if value is None: + value = other_values[ param.name ] = param.get_initial_value( t, other_values ) + %> + ${param.get_html_field( t, value, other_values ).get_html( str(step.id) + "|" + prefix )} + %endif + %else: + ${param.value_to_display_text( value, app )} + %endif + </div> + </div> +</%def> + +<%def name="center_panel()"> + ## Get URL to other workflows owned by user that owns this workflow. + <% + ##TODO: is there a better way to create this URL? Can't use 'f-username' as a key b/c it's not a valid identifier. + href_to_user_workflows = h.url_for( action='list_public', xxx=workflow.user.username ) + href_to_user_workflows = href_to_user_workflows.replace( 'xxx', 'f-username' ) + %> + + <div class="unified-panel-header" unselectable="on"> + <div class="unified-panel-header-inner"> + <a href="${h.url_for ( action='list_public' )}">Public Workflows</a> | + <a href="${href_to_user_workflows}">${workflow.user.username}</a> | ${workflow.name} + </div> + </div> + + <div class="unified-panel-body"> + <div style="overflow: auto; height: 100%;"> + <div class="page-body"> + ## Render top links. + <div id="top-links" style="padding: 0px 0px 5px 0px"> + %if workflow.user != trans.get_user(): + <a href="${h.url_for( action='imp', id=trans.security.encode_id(workflow.id) )}">import and start using workflow</a> + %endif + </div> + + ## Render Workflow. + <h2>${workflow.name}</h2> + %for i, step in enumerate( steps ): + %if step.type == 'tool' or step.type is None: + <% tool = app.toolbox.tools_by_id[step.tool_id] %> + <div class="toolForm"> + <div class="toolFormTitle">Step ${int(step.order_index)+1}: ${tool.name}</div> + <div class="toolFormBody"> + ${do_inputs( tool.inputs, step.state.inputs, "", step )} + </div> + </div> + %else: + <% module = step.module %> + <div class="toolForm"> + <div class="toolFormTitle">Step ${int(step.order_index)+1}: ${module.name}</div> + <div class="toolFormBody"> + </div> + </div> + %endif + %endfor + </div> + + <div class="page-meta"> + ## Workflows. + <div><strong>Related Workflows</strong></div> + <p> + <a href="${h.url_for ( action='list_public' )}">All public workflows</a><br> + <a href="${href_to_user_workflows}">Workflows owned by ${workflow.user.username}</a> + + ## Tags. + <div><strong>Tags</strong></div> + <p> + ## Community tags. + <div> + Community: + ${render_community_tagging_element( tagged_item=workflow, tag_click_fn='community_tag_click', use_toggle_link=False )} + %if len ( workflow.tags ) == 0: + none + %endif + </div> + ## Individual tags. + <p> + <div> + Yours: + ${render_individual_tagging_element( user=trans.get_user(), tagged_item=workflow, elt_context='view.mako', use_toggle_link=False )} + </div> + </div> + </div> + </div> +</%def> diff -r 56efe838b9af -r a7deb5c197ad templates/workflow/list_public.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/workflow/list_public.mako Tue Jan 12 11:37:45 2010 -0500 @@ -0,0 +1,23 @@ +<%inherit file="/base_panels.mako"/> + +<%def name="init()"> +<% + self.has_left_panel=False + self.has_right_panel=False + self.active_view="page" + self.message_box_visible=False +%> +</%def> + +<%def name="center_panel()"> + + ## <iframe name="galaxy_main" id="galaxy_main" frameborder="0" style="position: absolute; width: 100%; height: 100%;" src="${h.url_for( controller="page", action="list" )}"> </iframe> + + <div style="overflow: auto; height: 100%;"> + <div class="page-container" style="padding: 10px;"> + ${unicode( grid, 'utf-8' )} + </div> + </div> + + +</%def> diff -r 56efe838b9af -r a7deb5c197ad templates/workflow/sharing.mako --- a/templates/workflow/sharing.mako Mon Jan 11 17:04:45 2010 -0500 +++ b/templates/workflow/sharing.mako Tue Jan 12 11:37:45 2010 -0500 @@ -4,7 +4,14 @@ <p> %if stored.importable: + <p> + Anyone can view this workflow by visiting the following URL: + <% url = h.url_for( action='display_by_username_and_slug', username=trans.get_user().username, slug=stored.slug, qualified=True ) %> + <blockquote> + <a href="${url}">${url}</a> + </blockquote> + <p> Anyone can import this workflow into their history via the following URL: <% url = h.url_for( action='imp', id=trans.security.encode_id(stored.id), qualified=True ) %> <blockquote>
participants (1)
-
Greg Von Kuster