details: http://www.bx.psu.edu/hg/galaxy/rev/369f6b15f3f1 changeset: 3188:369f6b15f3f1 user: jeremy goecks <jeremy.goecks at emory.edu> date: Mon Dec 21 09:33:46 2009 -0500 description: Added functionality for public repositories of histories and pages. Features added include: (a) unique slugs for histories; (b) pretty URLs for histories; (c) public grids for histories and pages; (d) links between lists of public pages and histories and individual pages and histories. Also fixed some bugs in grid framework. diffstat: lib/galaxy/model/mapping.py | 3 +- lib/galaxy/model/migrate/versions/0030_history_slug_column.py | 38 + lib/galaxy/web/base/controller.py | 16 + lib/galaxy/web/buildapp.py | 1 + lib/galaxy/web/controllers/history.py | 106 +- lib/galaxy/web/controllers/page.py | 46 +- lib/galaxy/web/framework/helpers/grids.py | 9 +- templates/base_panels.mako | 4 +- templates/display_common.mako | 12 + templates/grid_base.mako | 16 +- templates/grid_common.mako | 29 +- templates/history/list_public.mako | 23 + templates/history/sharing.mako | 3 +- templates/history/view.mako | 615 +++++---- templates/page/display.mako | 31 +- templates/page/editor.mako | 17 +- 16 files changed, 628 insertions(+), 341 deletions(-) diffs (1332 lines): diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/model/mapping.py Mon Dec 21 09:33:46 2009 -0500 @@ -78,7 +78,8 @@ Column( "deleted", Boolean, index=True, default=False ), Column( "purged", Boolean, index=True, default=False ), Column( "genome_build", TrimmedString( 40 ) ), - Column( "importable", Boolean, default=False ) ) + Column( "importable", Boolean, default=False ), + Column( "slug", TEXT, index=True ) ) HistoryUserShareAssociation.table = Table( "history_user_share_association", metadata, Column( "id", Integer, primary_key=True ), diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/model/migrate/versions/0030_history_slug_column.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0030_history_slug_column.py Mon Dec 21 09:33:46 2009 -0500 @@ -0,0 +1,38 @@ +""" +Migration script to add column for a history slug. +""" + +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() + + History_table = Table( "history", metadata, autoload=True ) + + # Create slug column. + c = Column( "slug", TEXT, index=True ) + c.create( History_table ) + assert c is History_table.c.slug + + # Create slug index. + try: + i = Index( "ix_history_slug", History_table.c.slug ) + i.create() + except: + # Mysql doesn't have a named index, but alter should work + History_table.c.slug.alter( unique=False ) + +def downgrade(): + metadata.reflect() + + History_table = Table( "history", metadata, autoload=True ) + History_table.c.slug.drop() diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/web/base/controller.py Mon Dec 21 09:33:46 2009 -0500 @@ -8,11 +8,27 @@ from galaxy import config, tools, web, model, util from galaxy.web import error, form, url_for from galaxy.model.orm import * +from galaxy.web.framework.helpers import grids from Cheetah.Template import Template log = logging.getLogger( __name__ ) +# Useful columns in many grids used by controllers. + +# Item's user/owner. +class OwnerColumn( grids.TextColumn ): + def get_value( self, trans, grid, item ): + return item.user.username + +# Item's public URL based on username and slug. +class PublicURLColumn( grids.TextColumn ): + def get_link( self, trans, grid, item ): + if item.user.username: + 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. diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/web/buildapp.py --- a/lib/galaxy/web/buildapp.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/web/buildapp.py Mon Dec 21 09:33:46 2009 -0500 @@ -80,6 +80,7 @@ webapp.add_route( '/:action', controller='root', action='index' ) 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.finalize_config() # Wrap the webapp in some useful middleware if kwargs.get( 'middleware', True ): diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/web/controllers/history.py Mon Dec 21 09:33:46 2009 -0500 @@ -10,6 +10,7 @@ import webhelpers, logging, operator from datetime import datetime from cgi import escape +import re log = logging.getLogger( __name__ ) @@ -171,7 +172,33 @@ return session.query( self.model_class ).join( 'users_shared_with' ) def apply_default_filter( self, trans, query, **kwargs ): return query.filter( model.HistoryUserShareAssociation.user == trans.user ) - + +class PublicHistoryListGrid( grids.Grid ): + 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"), + OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), + 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 = [] + def build_initial_query( self, session ): + # Join so that searching history.user makes sense. + return session.query( self.model_class ).join( model.User.table ) + def apply_default_filter( self, trans, query, **kwargs ): + # A public history 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 HistoryController( BaseController ): @web.expose def index( self, trans ): @@ -183,6 +210,17 @@ stored_list_grid = HistoryListGrid() shared_list_grid = SharedHistoryListGrid() + 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: + return grid + else: + # Render grid wrapped in panels + return trans.fill_template( "history/list_public.mako", grid=grid ) @web.expose @web.require_login( "work with multiple histories" ) @@ -234,7 +272,7 @@ elif operation == "enable import via link": for history in histories: if not history.importable: - history.importable = True + self.make_history_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 ] @@ -372,26 +410,31 @@ trans.sa_session.flush() @web.expose - @web.require_login( "get history name" ) - def get_name_async( self, trans, id=None ): - """ Returns the name for a given history. """ + @web.require_login( "get history name, slug, and owner's username" ) + def get_name_slug_username_async( self, trans, id=None ): + """ Returns the name, slug, and owner's username for a given history. """ history = self.get_history( trans, id, False ) - # To get name: user must own history, history must be importable. - if history.user == trans.get_user() or history.importable or trans.get_user() in history.users_shared_with: - return history.name + # To get info: user must own history. + if history.user == trans.get_user(): + slug = iff( history.slug, history.slug, "" ) + username = iff ( history.user.username, history.user.username, "" ) + return history.name + "," + slug + "," + username return @web.expose @web.require_login( "set history's importable flag" ) def set_importable_async( self, trans, id=None, importable=False ): - """ Set history's importable attribute. """ + """ Set history's importable attribute and sets history's slug. """ history = self.get_history( trans, id, True ) # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. importable = importable in ['True', 'true', 't', 'T']; if history and history.importable != importable: - history.importable = importable + if importable: + self.make_history_importable( trans.sa_session, history ) + else: + history.importable = importable trans.sa_session.flush() return @@ -492,6 +535,31 @@ datasets = query.all(), user_owns_history = user_owns_history, show_deleted = False ) + + @web.expose + def display_by_username_and_slug( self, trans, username, slug ): + session = trans.sa_session + user = session.query( model.User ).filter_by( username=username ).first() + if user is None: + raise web.httpexceptions.HTTPNotFound() + 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" ) ) \ + .join( "dataset" ).filter( model.Dataset.purged == False ) \ + .options( eagerload_all( "dataset.actions" ) ) + # Do not show deleted datasets. + query = query.filter( model.HistoryDatasetAssociation.deleted == False ) + user_owns_history = ( trans.get_user() == history.user ) + return trans.stream_template_mako( "history/view.mako", + history = history, + datasets = query.all(), + user_owns_history = user_owns_history, + show_deleted = False ) + @web.expose @web.require_login( "share histories with other users" ) def share( self, trans, id=None, email="", **kwd ): @@ -780,7 +848,7 @@ for history in histories: trans.sa_session.add( history ) if params.get( 'enable_import_via_link', False ): - history.importable = True + self.make_history_importable( trans.sa_session, history ) trans.sa_session.flush() elif params.get( 'disable_import_via_link', False ): history.importable = False @@ -896,4 +964,18 @@ 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 ) \ No newline at end of file + 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 diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/web/controllers/page.py --- a/lib/galaxy/web/controllers/page.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/web/controllers/page.py Mon Dec 21 09:33:46 2009 -0500 @@ -13,20 +13,6 @@ else: return "" -class PublicURLColumn( grids.TextColumn ): - def get_value( self, trans, grid, item ): - username = item.user.username or "???" - return username + "/" + item.slug - def get_link( self, trans, grid, item ): - if item.user.username: - return dict( action='display_by_username_and_slug', username=item.user.username, slug=item.slug ) - else: - return None - -class OwnerColumn( grids.TextColumn ): - def get_value( self, trans, grid, item ): - return item.user.username - class PageListGrid( grids.Grid ): # Grid definition use_panels = True @@ -58,18 +44,28 @@ class PageAllPublishedGrid( grids.Grid ): # Grid definition use_panels = True - title = "Published Pages From All Users" + use_async = True + title = "Published Pages" model_class = model.Page - default_sort_key = "-create_time" + default_sort_key = "-update_time" + default_filter = dict( title="All", username="All" ) columns = [ - grids.TextColumn( "Title", model_class=model.Page, key="title", filterable="standard" ), - PublicURLColumn( "Public URL" ), - OwnerColumn( "Published by", model_class=model.User, key="username" ), + 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.GridColumn( "Last Updated", key="update_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" ) + ) + def build_initial_query( self, session ): + # Join so that searching history.user makes sense. + return session.query( self.model_class ).join( model.User.table ) def apply_default_filter( self, trans, query, **kwargs ): - return query.filter_by( deleted=False, published=True ) + return query.filter( self.model_class.deleted==False ).filter( self.model_class.published==True ) class HistorySelectionGrid( grids.Grid ): # Custom columns. @@ -169,11 +165,13 @@ return trans.fill_template( "page/index.mako", grid=grid ) @web.expose - @web.require_login() def list_published( self, trans, *args, **kwargs ): grid = self._all_published_list( trans, *args, **kwargs ) - # Render grid wrapped in panels - return trans.fill_template( "page/index.mako", grid=grid ) + if 'async' in kwargs: + return grid + else: + # Render grid wrapped in panels + return trans.fill_template( "page/index.mako", grid=grid ) @web.expose diff -r e7244f7d613b -r 369f6b15f3f1 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Fri Dec 18 14:18:56 2009 -0500 +++ b/lib/galaxy/web/framework/helpers/grids.py Mon Dec 21 09:33:46 2009 -0500 @@ -122,6 +122,9 @@ column_filter = from_json_string_recurse( column_filter ) if len( column_filter ) == 1: column_filter = column_filter[0] + # Interpret ',' as a separator for multiple terms. + if isinstance( column_filter, basestring ) and column_filter.find(',') != -1: + column_filter = column_filter.split(',') # If filter criterion is empty, do nothing. if column_filter == '': continue @@ -273,8 +276,8 @@ class GridColumn( object ): def __init__( self, label, key=None, model_class=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, - # Valid values for filterable are ['default', 'advanced', None] - filterable=None ): + # Valid values for filterable are ['standard', 'advanced', None] + filterable=None, sortable=True ): self.label = label self.key = key self.model_class = model_class @@ -287,7 +290,7 @@ self.filterable = filterable # Currently can only sort of columns that have a database # representation, not purely derived. - if self.key: + if self.key and sortable: self.sortable = True else: self.sortable = False diff -r e7244f7d613b -r 369f6b15f3f1 templates/base_panels.mako --- a/templates/base_panels.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/base_panels.mako Mon Dec 21 09:33:46 2009 -0500 @@ -245,8 +245,8 @@ %> <li><a target="${logout_target}" href="${logout_url}">Logout</a></li> <li><hr style="color: inherit; background-color: gray"/></li> - <li><a target="galaxy_main" href="${h.url_for( controller='history', action='list' )}">Histories</a></li> - <li><a target="galaxy_main" href="${h.url_for( controller='dataset', action='list' )}">Datasets</a></li> + <li><a target="galaxy_main" href="${h.url_for( controller='/history', action='list' )}">Histories</a></li> + <li><a target="galaxy_main" href="${h.url_for( controller='/dataset', action='list' )}">Datasets</a></li> %if app.config.get_bool( 'enable_pages', False ): <li><a href="${h.url_for( controller='page' )}">Pages</a></li> %endif diff -r e7244f7d613b -r 369f6b15f3f1 templates/display_common.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/display_common.mako Mon Dec 21 09:33:46 2009 -0500 @@ -0,0 +1,12 @@ +## +## A set of useful methods for displaying different items. +## + +## Return a link to view a history. +<%def name="get_history_link( history, qualify=False )"> + %if history.slug and history.user.username: + <% return h.url_for( controller='/history', action='display_by_username_and_slug', username=history.user.username, slug=history.slug, qualified=qualify ) %> + %else: + <% return h.url_for( controller='/history', action='view', id=trans.security.encode_id( history.id ), qualified=qualify ) %> + %endif +</%def> \ No newline at end of file diff -r e7244f7d613b -r 369f6b15f3f1 templates/grid_base.mako --- a/templates/grid_base.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/grid_base.mako Mon Dec 21 09:33:46 2009 -0500 @@ -34,6 +34,12 @@ $(document).ready(function() { init_grid_elements(); init_grid_controls(); + + // Initalize filter elements. + $('input[type=text]').each( function() + { + $(this).click( function() { $(this).attr('value', '') } ); + }); }); ## Can this be moved into base.mako? %if refresh_frames: @@ -69,11 +75,11 @@ // Code to handle grid operations: filtering, sorting, paging, and operations. // - // Operations that are not async (AJAX) compatible. - var no_async_ops = new Object(); + // Operations that are async (AJAX) compatible. + var async_ops = new Object(); %for operation in grid.operations: - %if not operation.async_compatible: - no_async_ops['${operation.label.lower()}'] = "True"; + %if operation.async_compatible: + async_ops['${operation.label.lower()}'] = "True"; %endif %endfor @@ -536,7 +542,7 @@ url_args['id'] = item_ids; // If operation cannot be performed asynchronously, redirect to location. Otherwise do operation. - var no_async = ( no_async_ops[operation] != undefined && no_async_ops[operation] != null); + var no_async = ( async_ops[operation] == undefined || async_ops[operation] == null); if (no_async) { go_to_URL(); diff -r e7244f7d613b -r 369f6b15f3f1 templates/grid_common.mako --- a/templates/grid_common.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/grid_common.mako Mon Dec 21 09:33:46 2009 -0500 @@ -1,4 +1,7 @@ -<%! from galaxy.web.framework.helpers.grids import TextColumn, GridColumnFilter %> +<%! + from galaxy.web.framework.helpers.grids import TextColumn, GridColumnFilter + from galaxy.web.framework.helpers import iff +%> ## Render a filter UI for a grid column. Filter is rendered as a table row. <%def name="render_grid_column_filter(column)"> @@ -8,7 +11,9 @@ if column.filterable == "advanced": column_label = column_label.lower() %> - <td align="left" style="padding-left: 10px">${column_label}:</td> + %if column.filterable == "advanced": + <td align="left" style="padding-left: 10px">${column_label}:</td> + %endif <td> %if isinstance(column, TextColumn): <form class="text-filter-form" column_key="${column.key}" action="${url( dict() )}" method="get" > @@ -43,13 +48,14 @@ %if i > 0: , %endif - <span style="font-style: italic">${filter}</span> - <% - new_filter = list( column_filter ) - del new_filter[ i ] - new_column_filter = GridColumnFilter( "", { column.key : h.to_json_string( new_filter ) } ) - %> - <a href="${url( new_column_filter.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a> + <span class='text-filter-val'>${filter} + <% + new_filter = list( column_filter ) + del new_filter[ i ] + new_column_filter = GridColumnFilter( "", { column.key : h.to_json_string( new_filter ) } ) + %> + <a href="${url( new_column_filter.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a> + </span> %endfor %endif @@ -57,7 +63,8 @@ </span> ## Print input field for column. <span> - <input class="no-padding-or-margin" id="input-${column.key}-filter" name="f-${column.key}" type="text" value="" size="15"/> + <% value = iff( column.filterable == "standard", column.label.lower(), "") %> + <input class="no-padding-or-margin" id="input-${column.key}-filter" name="f-${column.key}" type="text" value="${value}" size="15"/> <input class='submit-image' type='image' src='${h.url_for('/static/images/mag_glass.png')}' alt='Filter'/> </span> </form> @@ -131,7 +138,7 @@ if 'advanced-search' in kwargs and kwargs['advanced-search'] in ['True', 'true']: advanced_search_display = "block" - for column in grid.columns: + for column in grid.columns: if column.filterable == "advanced": ## Show div if current filter has value that is different from the default filter. if column.key in cur_filter_dict and column.key in default_filter_dict and \ diff -r e7244f7d613b -r 369f6b15f3f1 templates/history/list_public.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/history/list_public.mako Mon Dec 21 09:33:46 2009 -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;"> + ${grid} + </div> + </div> + + +</%def> diff -r e7244f7d613b -r 369f6b15f3f1 templates/history/sharing.mako --- a/templates/history/sharing.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/history/sharing.mako Mon Dec 21 09:33:46 2009 -0500 @@ -1,5 +1,6 @@ <%inherit file="/base.mako"/> <%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/display_common.mako" import="get_history_link" /> ##<h2>Import via link</h2> @@ -34,7 +35,7 @@ %endif %if history.importable: <div class="form-row"> - <% url = h.url_for( controller='history', action='view', id=trans.security.encode_id(history.id), qualified=True ) %> + <% url = get_history_link( history, True )%> <a href="${url}" target="_blank">${url}</a> <div class="toolParamHelp" style="clear: both;"> Send the above link to users as an easy way for them to view the history. diff -r e7244f7d613b -r 369f6b15f3f1 templates/history/view.mako --- a/templates/history/view.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/history/view.mako Mon Dec 21 09:33:46 2009 -0500 @@ -1,8 +1,271 @@ <%inherit file="/base_panels.mako"/> +<%namespace file="/display_common.mako" import="get_history_link" /> +<%namespace file="/root/history_common.mako" import="render_dataset" /> +<%namespace file="../tagging_common.mako" import="render_tagging_element, render_community_tagging_element" /> <%def name="javascripts()"> ${parent.javascripts()} ${h.js( "galaxy.base", "jquery", "json2", "jquery.jstore-all", "jquery.autocomplete", "autocomplete_tagging" )} + <script type="text/javascript"> + $(function() { + // Load jStore for local storage + $.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '/static/jStore.Flash.html' }) + $.jStore.load(); // Auto-select best storage + + $.jStore.ready(function(engine) { + engine.ready(function() { + // Init stuff that requires the local storage to be running + initShowHide(); + setupHistoryItem( $("div.historyItemWrapper") ); + }); + }); + + // Generate 'collapse all' link + $("#top-links").append( "| " ).append( $("<a href='#'>${_('collapse all')}</a>").click( function() { + $( "div.historyItemBody:visible" ).each( function() { + if ( $.browser.mozilla ) { + $(this).find( "pre.peek" ).css( "overflow", "hidden" ); + } + $(this).slideUp( "fast" ); + }); + $.jStore.remove("history_expand_state"); + })); + + $("#history-rename").click( function() { + var old_name = $("#history-name").text() + var t = $("<input type='text' value='" + old_name + "'></input>" ); + t.blur( function() { + $(this).remove(); + $("#history-name").show(); + }); + t.keyup( function( e ) { + if ( e.keyCode == 27 ) { + // Escape key + $(this).trigger( "blur" ); + } else if ( e.keyCode == 13 ) { + // Enter key + new_value = this.value; + $(this).trigger( "blur" ); + $.ajax({ + url: "${h.url_for( controller='history', action='rename_async', id=history.id )}", + data: { "_": true, new_name: new_value }, + error: function() { alert( "Rename failed" ) }, + success: function() { + $("#history-name").text( new_value ); + } + }); + } + }); + $("#history-name").hide(); + $("#history-name-area").append( t ); + t.focus(); + return false; + }); + // Updater + updater({ + <% updateable = [data for data in reversed( datasets ) if data.visible and data.state not in [ "deleted", "empty", "error", "ok" ]] %> + ${ ",".join( map(lambda data: "\"%s\" : \"%s\"" % (data.id, data.state), updateable) ) } + }); + }); + // Functionized so AJAX'd datasets can call them + function initShowHide() { + + // Load saved state and show as necessary + try { + var stored = $.jStore.store("history_expand_state"); + if (stored) { + var st = JSON.parse(stored); + for (var id in st) { + $("#" + id + " div.historyItemBody" ).show(); + } + } + } catch(err) { + // Something was wrong with values in storage, so clear storage + $.jStore.remove("history_expand_state"); + } + + // If Mozilla, hide scrollbars in hidden items since they cause animation bugs + if ( $.browser.mozilla ) { + $( "div.historyItemBody" ).each( function() { + if ( ! $(this).is( ":visible" ) ) $(this).find( "pre.peek" ).css( "overflow", "hidden" ); + }) + } + } + // Add show/hide link and delete link to a history item + function setupHistoryItem( query ) { + query.each( function() { + var id = this.id; + var body = $(this).children( "div.historyItemBody" ); + var peek = body.find( "pre.peek" ) + $(this).children( ".historyItemTitleBar" ).find( ".historyItemTitle" ).wrap( "<a href='#'></a>" ).click( function() { + if ( body.is(":visible") ) { + // Hiding stuff here + if ( $.browser.mozilla ) { peek.css( "overflow", "hidden" ) } + body.slideUp( "fast" ); + + // Save setting + var stored = $.jStore.store("history_expand_state") + var prefs = stored ? JSON.parse(stored) : null + if (prefs) { + delete prefs[id]; + $.jStore.store("history_expand_state", JSON.stringify(prefs)); + } + } else { + // Showing stuff here + body.slideDown( "fast", function() { + if ( $.browser.mozilla ) { peek.css( "overflow", "auto" ); } + }); + + // Save setting + var stored = $.jStore.store("history_expand_state") + var prefs = stored ? JSON.parse(stored) : new Object; + prefs[id] = true; + $.jStore.store("history_expand_state", JSON.stringify(prefs)); + } + return false; + }); + // Delete link + $(this).find( "div.historyItemButtons > .delete" ).each( function() { + var data_id = this.id.split( "-" )[1]; + $(this).click( function() { + $( '#historyItem-' + data_id + "> div.historyItemTitleBar" ).addClass( "spinner" ); + $.ajax({ + url: "${h.url_for( action='delete_async', id='XXX' )}".replace( 'XXX', data_id ), + error: function() { alert( "Delete failed" ) }, + success: function() { + %if show_deleted: + var to_update = {}; + to_update[data_id] = "none"; + updater( to_update ); + %else: + $( "#historyItem-" + data_id ).fadeOut( "fast", function() { + $( "#historyItemContainer-" + data_id ).remove(); + if ( $( "div.historyItemContainer" ).length < 1 ) { + $( "#emptyHistoryMessage" ).show(); + } + }); + %endif + } + }); + return false; + }); + }); + // Undelete link + $(this).find( "a.historyItemUndelete" ).each( function() { + var data_id = this.id.split( "-" )[1]; + $(this).click( function() { + $( '#historyItem-' + data_id + " > div.historyItemTitleBar" ).addClass( "spinner" ); + $.ajax({ + url: "${h.url_for( controller='dataset', action='undelete_async', id='XXX' )}".replace( 'XXX', data_id ), + error: function() { alert( "Undelete failed" ) }, + success: function() { + var to_update = {}; + to_update[data_id] = "none"; + updater( to_update ); + } + }); + return false; + }); + }); + }); + }; + // Looks for changes in dataset state using an async request. Keeps + // calling itself (via setTimeout) until all datasets are in a terminal + // state. + var updater = function ( tracked_datasets ) { + // Check if there are any items left to track + var empty = true; + for ( i in tracked_datasets ) { + empty = false; + break; + } + if ( ! empty ) { + // console.log( "Updater running in 3 seconds" ); + setTimeout( function() { updater_callback( tracked_datasets ) }, 3000 ); + } else { + // console.log( "Updater finished" ); + } + }; + var updater_callback = function ( tracked_datasets ) { + // Build request data + var ids = [] + var states = [] + var force_history_refresh = false + $.each( tracked_datasets, function ( id, state ) { + ids.push( id ); + states.push( state ); + }); + // Make ajax call + $.ajax( { + type: "POST", + url: "${h.url_for( controller='root', action='history_item_updates' )}", + dataType: "json", + data: { ids: ids.join( "," ), states: states.join( "," ) }, + success : function ( data ) { + $.each( data, function( id, val ) { + // Replace HTML + var container = $("#historyItemContainer-" + id); + container.html( val.html ); + setupHistoryItem( container.children( ".historyItemWrapper" ) ); + initShowHide(); + // If new state was terminal, stop tracking + if (( val.state == "ok") || ( val.state == "error") || ( val.state == "empty") || ( val.state == "deleted" ) || ( val.state == "discarded" )) { + if ( val.force_history_refresh ){ + force_history_refresh = true; + } + delete tracked_datasets[ parseInt(id) ]; + } else { + tracked_datasets[ parseInt(id) ] = val.state; + } + }); + if ( force_history_refresh ) { + parent.frames.galaxy_history.location.reload(); + } else { + // Keep going (if there are still any items to track) + updater( tracked_datasets ); + } + }, + error: function() { + // Just retry, like the old method, should try to be smarter + updater( tracked_datasets ); + } + }); + }; + + // + // 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; + }; + + // + // Handle click on community tag. + // + 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> <%def name="stylesheets()"> @@ -13,12 +276,40 @@ padding-right: 3px; border-right-style: solid; border-right-color: #66AA66; - }; + } + .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; + } </style> + + <style> + .historyItemBody { + display: none; + } + </style> + + <noscript> + <style> + .historyItemBody { + display: block; + } + </style> + </noscript> </%def> -<%namespace file="/root/history_common.mako" import="render_dataset" /> - <%def name="init()"> <% self.has_left_panel=False @@ -33,274 +324,28 @@ user_owns_history = False %> - <div style="overflow: auto; height: 100%;"> - <div style="padding: 10px;"> + ## Get URL to other histories owned by user that owns this history. + <% + ##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_histories = h.url_for( controller='/history', action='list_public', xxx=history.user.username) + href_to_user_histories = href_to_user_histories.replace( 'xxx', 'f-username') + %> + + <div class="unified-panel-header" unselectable="on"> + <div class="unified-panel-header-inner"> + <a href="${h.url_for ( controller='/history', action='list_public' )}">Public Histories</a> | + <a href="${href_to_user_histories}">${history.user.username}</a> | ${history.name} + </div> + </div> + + <div class="unified-panel-body"> + <div class="page-body"> ## Render view of history. - <script type="text/javascript"> - $(function() { - // Load jStore for local storage - $.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '/static/jStore.Flash.html' }) - $.jStore.load(); // Auto-select best storage - - $.jStore.ready(function(engine) { - engine.ready(function() { - // Init stuff that requires the local storage to be running - initShowHide(); - setupHistoryItem( $("div.historyItemWrapper") ); - }); - }); - - // Generate 'collapse all' link - $("#top-links").append( "| " ).append( $("<a href='#'>${_('collapse all')}</a>").click( function() { - $( "div.historyItemBody:visible" ).each( function() { - if ( $.browser.mozilla ) { - $(this).find( "pre.peek" ).css( "overflow", "hidden" ); - } - $(this).slideUp( "fast" ); - }); - $.jStore.remove("history_expand_state"); - })); - - $("#history-rename").click( function() { - var old_name = $("#history-name").text() - var t = $("<input type='text' value='" + old_name + "'></input>" ); - t.blur( function() { - $(this).remove(); - $("#history-name").show(); - }); - t.keyup( function( e ) { - if ( e.keyCode == 27 ) { - // Escape key - $(this).trigger( "blur" ); - } else if ( e.keyCode == 13 ) { - // Enter key - new_value = this.value; - $(this).trigger( "blur" ); - $.ajax({ - url: "${h.url_for( controller='history', action='rename_async', id=history.id )}", - data: { "_": true, new_name: new_value }, - error: function() { alert( "Rename failed" ) }, - success: function() { - $("#history-name").text( new_value ); - } - }); - } - }); - $("#history-name").hide(); - $("#history-name-area").append( t ); - t.focus(); - return false; - }); - // Updater - updater({ - <% updateable = [data for data in reversed( datasets ) if data.visible and data.state not in [ "deleted", "empty", "error", "ok" ]] %> - ${ ",".join( map(lambda data: "\"%s\" : \"%s\"" % (data.id, data.state), updateable) ) } - }); - }); - // Functionized so AJAX'd datasets can call them - function initShowHide() { - - // Load saved state and show as necessary - try { - var stored = $.jStore.store("history_expand_state"); - if (stored) { - var st = JSON.parse(stored); - for (var id in st) { - $("#" + id + " div.historyItemBody" ).show(); - } - } - } catch(err) { - // Something was wrong with values in storage, so clear storage - $.jStore.remove("history_expand_state"); - } - - // If Mozilla, hide scrollbars in hidden items since they cause animation bugs - if ( $.browser.mozilla ) { - $( "div.historyItemBody" ).each( function() { - if ( ! $(this).is( ":visible" ) ) $(this).find( "pre.peek" ).css( "overflow", "hidden" ); - }) - } - } - // Add show/hide link and delete link to a history item - function setupHistoryItem( query ) { - query.each( function() { - var id = this.id; - var body = $(this).children( "div.historyItemBody" ); - var peek = body.find( "pre.peek" ) - $(this).children( ".historyItemTitleBar" ).find( ".historyItemTitle" ).wrap( "<a href='#'></a>" ).click( function() { - if ( body.is(":visible") ) { - // Hiding stuff here - if ( $.browser.mozilla ) { peek.css( "overflow", "hidden" ) } - body.slideUp( "fast" ); - - // Save setting - var stored = $.jStore.store("history_expand_state") - var prefs = stored ? JSON.parse(stored) : null - if (prefs) { - delete prefs[id]; - $.jStore.store("history_expand_state", JSON.stringify(prefs)); - } - } else { - // Showing stuff here - body.slideDown( "fast", function() { - if ( $.browser.mozilla ) { peek.css( "overflow", "auto" ); } - }); - - // Save setting - var stored = $.jStore.store("history_expand_state") - var prefs = stored ? JSON.parse(stored) : new Object; - prefs[id] = true; - $.jStore.store("history_expand_state", JSON.stringify(prefs)); - } - return false; - }); - // Delete link - $(this).find( "div.historyItemButtons > .delete" ).each( function() { - var data_id = this.id.split( "-" )[1]; - $(this).click( function() { - $( '#historyItem-' + data_id + "> div.historyItemTitleBar" ).addClass( "spinner" ); - $.ajax({ - url: "${h.url_for( action='delete_async', id='XXX' )}".replace( 'XXX', data_id ), - error: function() { alert( "Delete failed" ) }, - success: function() { - %if show_deleted: - var to_update = {}; - to_update[data_id] = "none"; - updater( to_update ); - %else: - $( "#historyItem-" + data_id ).fadeOut( "fast", function() { - $( "#historyItemContainer-" + data_id ).remove(); - if ( $( "div.historyItemContainer" ).length < 1 ) { - $( "#emptyHistoryMessage" ).show(); - } - }); - %endif - } - }); - return false; - }); - }); - // Undelete link - $(this).find( "a.historyItemUndelete" ).each( function() { - var data_id = this.id.split( "-" )[1]; - $(this).click( function() { - $( '#historyItem-' + data_id + " > div.historyItemTitleBar" ).addClass( "spinner" ); - $.ajax({ - url: "${h.url_for( controller='dataset', action='undelete_async', id='XXX' )}".replace( 'XXX', data_id ), - error: function() { alert( "Undelete failed" ) }, - success: function() { - var to_update = {}; - to_update[data_id] = "none"; - updater( to_update ); - } - }); - return false; - }); - }); - }); - }; - // Looks for changes in dataset state using an async request. Keeps - // calling itself (via setTimeout) until all datasets are in a terminal - // state. - var updater = function ( tracked_datasets ) { - // Check if there are any items left to track - var empty = true; - for ( i in tracked_datasets ) { - empty = false; - break; - } - if ( ! empty ) { - // console.log( "Updater running in 3 seconds" ); - setTimeout( function() { updater_callback( tracked_datasets ) }, 3000 ); - } else { - // console.log( "Updater finished" ); - } - }; - var updater_callback = function ( tracked_datasets ) { - // Build request data - var ids = [] - var states = [] - var force_history_refresh = false - $.each( tracked_datasets, function ( id, state ) { - ids.push( id ); - states.push( state ); - }); - // Make ajax call - $.ajax( { - type: "POST", - url: "${h.url_for( controller='root', action='history_item_updates' )}", - dataType: "json", - data: { ids: ids.join( "," ), states: states.join( "," ) }, - success : function ( data ) { - $.each( data, function( id, val ) { - // Replace HTML - var container = $("#historyItemContainer-" + id); - container.html( val.html ); - setupHistoryItem( container.children( ".historyItemWrapper" ) ); - initShowHide(); - // If new state was terminal, stop tracking - if (( val.state == "ok") || ( val.state == "error") || ( val.state == "empty") || ( val.state == "deleted" ) || ( val.state == "discarded" )) { - if ( val.force_history_refresh ){ - force_history_refresh = true; - } - delete tracked_datasets[ parseInt(id) ]; - } else { - tracked_datasets[ parseInt(id) ] = val.state; - } - }); - if ( force_history_refresh ) { - parent.frames.galaxy_history.location.reload(); - } else { - // Keep going (if there are still any items to track) - updater( tracked_datasets ); - } - }, - error: function() { - // Just retry, like the old method, should try to be smarter - updater( tracked_datasets ); - } - }); - }; - - // - // 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> - .historyItemBody { - display: none; - } - </style> - - <noscript> - <style> - .historyItemBody { - display: block; - } - </style> - </noscript> - <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="${h.url_for( controller='history', action='view', id=trans.security.encode_id(history.id) )}">${_('refresh')}</a> + <a href="${get_history_link( history )}">${_('refresh')}</a> %if show_deleted: | <a href="${h.url_for('history', show_deleted=False)}">${_('hide deleted')}</a> %endif @@ -320,13 +365,6 @@ <p></p> %endif - <%namespace file="../tagging_common.mako" import="render_tagging_element" /> - - %if trans.get_user() is not None: - <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element(tagged_item=history, elt_context="history/view.mako", use_toggle_link=False, get_toggle_link_text_fn='get_toggle_link_text', editable=user_owns_history)} - %endif - %if not datasets: <div class="infomessagesmall" id="emptyHistoryMessage"> @@ -334,7 +372,7 @@ %else: ## Render requested datasets, ordered from newest to oldest - %for data in reversed( datasets ): + %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 )} @@ -347,7 +385,32 @@ ${_("Your history is empty. Click 'Get Data' on the left pane to start")} </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> + ## User tags. + <p> + <div> + ##Yours: + ##${render_tagging_element( tagged_item=history, elt_context='view.mako', use_toggle_link=False )} + </div> + </div> + </div> - - </%def> diff -r e7244f7d613b -r 369f6b15f3f1 templates/page/display.mako --- a/templates/page/display.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/page/display.mako Mon Dec 21 09:33:46 2009 -0500 @@ -150,7 +150,14 @@ // function community_tag_click(tag_name, tag_value) { - alert("community tag click: " + tag_name); + // 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> @@ -173,6 +180,7 @@ margin: 0.25em; vertical-align: text-top; border: 2px solid #DDDDDD; + border-top: 4px solid #DDDDDD; } </style> </%def> @@ -190,9 +198,17 @@ <%def name="center_panel()"> + ## Get URL to other pages owned by user that owns this page. + <% + ##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_pages = h.url_for( controller='/page', action='list_published', xxx=page.user.username) + href_to_user_pages = href_to_user_pages.replace( 'xxx', 'f-username') + %> + <div class="unified-panel-header" unselectable="on"> <div class="unified-panel-header-inner"> - ${page.user.username} :: ${page.title} + <a href="${h.url_for ( controller='/page', action='list_published' )}">Published Pages</a> | + <a href="${href_to_user_pages}">${page.user.username}</a> | ${page.title} </div> </div> @@ -202,6 +218,13 @@ ${page.latest_revision.content.decode( "utf-8" )} </div> <div class="page-meta"> + ## Pages. + <div><strong>Related Pages</strong></div> + <p> + <a href="${h.url_for ( controller='/page', action='list_published' )}">All published pages</a><br> + <a href="${href_to_user_pages}">Pages published by ${page.user.username}</a> + + ## Tags. <div><strong>Tags</strong></div> <p> ## Community tags. @@ -215,8 +238,8 @@ ## User tags. <p> <div> - Yours: - ${render_tagging_element( tagged_item=page, elt_context='display.mako', use_toggle_link=False )} + ##Yours: + ##${render_tagging_element( tagged_item=page, elt_context='display.mako', use_toggle_link=False )} </div> </div> </div> diff -r e7244f7d613b -r 369f6b15f3f1 templates/page/editor.mako --- a/templates/page/editor.mako Fri Dec 18 14:18:56 2009 -0500 +++ b/templates/page/editor.mako Mon Dec 21 09:33:46 2009 -0500 @@ -248,8 +248,21 @@ // User selected no text; create link from scratch and use default text. // Get history name. - $.get( '${h.url_for( controller='history', action='get_name_async' )}?id=' + item_id, function( history_name ) { - var href = '${h.url_for( controller='history', action='view' )}?id=' + item_id; + $.get( '${h.url_for( controller='history', action='get_name_slug_username_async' )}?id=' + item_id, + function( history_info ) { + // Parse history info. + history_info = history_info.split(","); + var + history_name = history_info[0], + history_slug = history_info[1], + history_user_username = history_info[2]; + + // Build href from history info. + var href; + if (history_slug != "" && history_user_username != "") + var href = "/u/" + history_user_username + "/h/" + history_slug; + else + var href = '${h.url_for( controller='/history', action='view' )}?id=' + item_id; wym.insert("<a href='" + href + "'>History '" + history_name + "'</a>"); }); }