commit/galaxy-central: 3 new changesets
3 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/c4950bc66225/ Changeset: c4950bc66225 User: carlfeberhard Date: 2014-03-25 20:30:51 Summary: History API: show: add importable, username_and_slug to returned attributes, update: 1) allow setting importable and published, 2) published automatically makes a history importable, 3) in both cases slug is automatically set set_as_current: now correctly uses the HTTP method PUT (not POST), HDA API: all: histories that are importable and/or published now allow GET'ing the contents properly, create: you can now copy HDAs into your histories if the HDA and it's history are accessible Affected #: 7 files diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -773,7 +773,7 @@ class History( object, Dictifiable, UsesAnnotations, HasName ): dict_collection_visible_keys = ( 'id', 'name', 'published', 'deleted' ) - dict_element_visible_keys = ( 'id', 'name', 'published', 'deleted', 'genome_build', 'purged' ) + dict_element_visible_keys = ( 'id', 'name', 'published', 'deleted', 'genome_build', 'purged', 'importable', 'slug' ) default_name = 'Unnamed history' def __init__( self, id=None, name=None, user=None ): diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -187,8 +187,11 @@ def validate_and_sanitize_basestring_list( self, key, val ): if not isinstance( val, list ): - raise ValueError( '%s must be a list: %s' %( key, str( type( val ) ) ) ) - return [ unicode( sanitize_html( t, 'utf-8', 'text/html' ), 'utf-8' ) for t in val ] + raise ValueError( '%s must be a list of strings: %s' %( key, str( type( val ) ) ) ) + try: + return [ unicode( sanitize_html( t, 'utf-8', 'text/html' ), 'utf-8' ) for t in val ] + except TypeError, type_err: + raise ValueError( '%s must be a list of strings: %s' %( key, str( type_err ) ) ) def validate_boolean( self, key, val ): if not isinstance( val, bool ): @@ -527,6 +530,10 @@ if not history_dict[ 'annotation' ]: history_dict[ 'annotation' ] = '' #TODO: item_slug url + if history_dict[ 'importable' ] and history_dict[ 'slug' ]: + #TODO: this should be in History (or a superclass of) + username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] )) + history_dict[ 'username_and_slug' ] = username_and_slug hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history ) #TODO remove the following in v2 @@ -541,26 +548,69 @@ """ Changes history data using the given dictionary new_data. """ - # precondition: access of the history has already been checked + #precondition: ownership of the history has already been checked + #precondition: user is not None (many of these attributes require a user to set properly) + user = trans.get_user() + # published histories should always be importable + if 'published' in new_data and new_data[ 'published' ] and not history.importable: + new_data[ 'importable' ] = True # send what we can down into the model changed = history.set_from_dict( new_data ) + # the rest (often involving the trans) - do here - if 'annotation' in new_data.keys() and trans.get_user(): - history.add_item_annotation( trans.sa_session, trans.get_user(), history, new_data[ 'annotation' ] ) + #TODO: the next two could be an aspect/mixin + #TODO: also need a way to check whether they've changed - assume they have for now + if 'annotation' in new_data: + history.add_item_annotation( trans.sa_session, user, history, new_data[ 'annotation' ] ) changed[ 'annotation' ] = new_data[ 'annotation' ] - if 'tags' in new_data.keys() and trans.get_user(): - self.set_tags_from_list( trans, history, new_data[ 'tags' ], user=trans.user ) - # importable (ctrl.history.set_accessible_async) - # sharing/permissions? - # slugs? - # purged - duh duh duhhhhhhnnnnnnnnnn + + if 'tags' in new_data: + self.set_tags_from_list( trans, history, new_data[ 'tags' ], user=user ) + changed[ 'tags' ] = new_data[ 'tags' ] + + #TODO: sharing with user/permissions? if changed.keys(): trans.sa_session.flush() + # create a slug if none exists (setting importable to false should not remove the slug) + if 'importable' in changed and changed[ 'importable' ] and not history.slug: + self._create_history_slug( trans, history ) + return changed + def _create_history_slug( self, trans, history ): + #TODO: mixins need to die a quick, horrible death + # (this is duplicate from SharableMixin which can't be added to UsesHistory without exposing various urls) + cur_slug = history.slug + + # Setup slug base. + if cur_slug is None or cur_slug == "": + # Item can have either a name or a title. + item_name = history.name + slug_base = util.ready_name_for_url( item_name.lower() ) + else: + slug_base = cur_slug + + # Using slug base, find a slug that is not taken. If slug is taken, + # add integer to end. + new_slug = slug_base + count = 1 + while ( trans.sa_session.query( trans.app.model.History ) + .filter_by( user=history.user, slug=new_slug, importable=True ) + .count() != 0 ): + # Slug taken; choose a new slug based on count. This approach can + # handle numerous items with the same name gracefully. + new_slug = '%s-%i' % ( slug_base, count ) + count += 1 + + # Set slug and return. + trans.sa_session.add( history ) + history.slug = new_slug + trans.sa_session.flush() + return history.slug == cur_slug + class ExportsHistoryMixin: @@ -685,9 +735,9 @@ else: #TODO: do we really need the history? history = self.get_history( trans, history_id, - check_ownership=True, check_accessible=True, deleted=False ) + check_ownership=False, check_accessible=True, deleted=False ) hda = self.get_history_dataset_association( trans, history, id, - check_ownership=True, check_accessible=True ) + check_ownership=False, check_accessible=True ) return hda def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ): diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -6,20 +6,26 @@ import pkg_resources pkg_resources.require( "Paste" ) -from paste.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPInternalServerError, HTTPException + +from paste.httpexceptions import HTTPBadRequest +from paste.httpexceptions import HTTPForbidden +from paste.httpexceptions import HTTPInternalServerError +from paste.httpexceptions import HTTPException from galaxy import exceptions from galaxy import web from galaxy.web import _future_expose_api as expose_api from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous from galaxy.web import _future_expose_api_raw as expose_api_raw -from galaxy.util import string_as_bool -from galaxy.util import restore_text + from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import UsesTagsMixin from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin + +from galaxy.util import string_as_bool +from galaxy.util import restore_text from galaxy.web import url_for import logging @@ -48,27 +54,22 @@ #TODO: query (by name, date, etc.) rval = [] deleted = string_as_bool( deleted ) - try: - if trans.user: - histories = self.get_user_histories( trans, user=trans.user, only_deleted=deleted ) - #for history in query: - for history in histories: - item = history.to_dict(value_mapper={'id': trans.security.encode_id}) - item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - rval.append( item ) - elif trans.galaxy_session.current_history: - #No user, this must be session authentication with an anonymous user. - history = trans.galaxy_session.current_history + if trans.user: + histories = self.get_user_histories( trans, user=trans.user, only_deleted=deleted ) + + for history in histories: item = history.to_dict(value_mapper={'id': trans.security.encode_id}) item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - rval.append(item) + rval.append( item ) - except Exception, e: - raise - rval = "Error in history API" - log.error( rval + ": %s" % str(e) ) - trans.response.status = 500 + elif trans.galaxy_session.current_history: + #No user, this must be session authentication with an anonymous user. + history = trans.galaxy_session.current_history + item = history.to_dict(value_mapper={'id': trans.security.encode_id}) + item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) + rval.append(item) + return rval @web.expose_api_anonymous @@ -92,7 +93,6 @@ :returns: detailed history information from :func:`galaxy.web.base.controller.UsesHistoryDatasetAssociationMixin.get_history_dict` """ - #TODO: GET /api/histories/{encoded_history_id}?as_archive=True #TODO: GET /api/histories/s/{username}/{slug} history_id = id deleted = string_as_bool( deleted ) @@ -116,7 +116,7 @@ except HTTPBadRequest, bad_req: trans.response.status = 400 - return str( bad_req ) + raise exceptions.MessageException( bad_req.detail ) except Exception, e: msg = "Error in history API at showing history detail: %s" % ( str( e ) ) @@ -444,7 +444,7 @@ continue if key in ( 'name', 'genome_build', 'annotation' ): validated_payload[ key ] = self.validate_and_sanitize_basestring( key, val ) - if key in ( 'deleted', 'published' ): + if key in ( 'deleted', 'published', 'importable' ): validated_payload[ key ] = self.validate_boolean( key, val ) elif key == 'tags': validated_payload[ key ] = self.validate_and_sanitize_basestring_list( key, val ) diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 lib/galaxy/webapps/galaxy/api/history_contents.py --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -2,20 +2,32 @@ API operations on the contents of a history. """ -import logging -from galaxy import exceptions, util, web -from galaxy.web.base.controller import ( BaseAPIController, url_for, - UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, UsesLibraryMixin, - UsesLibraryMixinItems, UsesTagsMixin ) +from galaxy import exceptions +from galaxy import util +from galaxy import web + +from galaxy.web import _future_expose_api as expose_api +from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous +from galaxy.web import _future_expose_api_raw as expose_api_raw + +from galaxy.web.base.controller import BaseAPIController +from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import UsesHistoryMixin +from galaxy.web.base.controller import UsesLibraryMixin +from galaxy.web.base.controller import UsesLibraryMixinItems +from galaxy.web.base.controller import UsesTagsMixin + +from galaxy.web.base.controller import url_for from galaxy.util.sanitize_html import sanitize_html +import logging log = logging.getLogger( __name__ ) class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, UsesLibraryMixin, UsesLibraryMixinItems, UsesTagsMixin ): - @web.expose_api_anonymous + @expose_api_anonymous def index( self, trans, history_id, ids=None, **kwd ): """ index( self, trans, history_id, ids=None, **kwd ) @@ -52,7 +64,7 @@ history = trans.history # otherwise, check permissions for the history first else: - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=True ) + history = self.get_history( trans, history_id, check_ownership=False, check_accessible=True ) # Allow passing in type or types - for continuity rest of methods # take in type - but this one can be passed multiple types and @@ -134,7 +146,7 @@ hda.history_id, hda.id, type( exc ), str( exc ) ) return self.get_hda_dict_with_error( trans, hda=hda, error_msg=str( exc ) ) - @web.expose_api_anonymous + @expose_api_anonymous def show( self, trans, id, history_id, **kwd ): """ show( self, trans, id, history_id, **kwd ) @@ -170,7 +182,7 @@ trans.response.status = 500 return msg - @web.expose_api + @expose_api def create( self, trans, history_id, payload, **kwd ): """ create( self, trans, history_id, payload, **kwd ) @@ -201,6 +213,8 @@ # retrieve history try: history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) + except exceptions.httpexceptions.HTTPException: + raise except Exception, e: # no way to tell if it failed bc of perms or other (all MessageExceptions) trans.response.status = 500 @@ -236,7 +250,8 @@ elif source == 'hda': try: #NOTE: user currently only capable of copying one of their own datasets - hda = self.get_dataset( trans, content ) + hda = self.get_dataset( trans, content, check_ownership=False, check_accessible=True ) + self.security_check( trans, hda.history, check_ownership=False, check_accessible=True ) except ( exceptions.httpexceptions.HTTPRequestRangeNotSatisfiable, exceptions.httpexceptions.HTTPBadRequest ), id_exc: # wot... @@ -259,7 +274,7 @@ trans.response.status = 501 return - @web.expose_api_anonymous + @expose_api_anonymous def update( self, trans, history_id, id, payload, **kwd ): """ update( self, trans, history_id, id, payload, **kwd ) @@ -306,6 +321,7 @@ if hda.history != trans.history: trans.response.status = 401 return { 'error': 'Anonymous users cannot edit datasets outside their current history' } + else: payload = self._validate_and_parse_update_payload( payload ) # only check_state if not deleting, otherwise cannot delete uploading files @@ -316,6 +332,7 @@ changed = self.set_hda_from_dict( trans, hda, payload ) if payload.get( 'deleted', False ): self.stop_hda_creating_job( hda ) + except Exception, exception: log.error( 'Update of history (%s), HDA (%s) failed: %s', history_id, id, str( exception ), exc_info=True ) @@ -330,7 +347,7 @@ return changed #TODO: allow anonymous del/purge and test security on this - @web.expose_api + @expose_api def delete( self, trans, history_id, id, purge=False, **kwd ): """ delete( self, trans, history_id, id, **kwd ) diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -165,8 +165,9 @@ parent_resources=dict( member_name='page', collection_name='pages' ) ) # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource + #webapp.mapper.connect( "set_as_current", "/api/histories/set_as_current/{id}", webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", - controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) + controller="histories", action="set_as_current", conditions=dict( method=["PUT"] ) ) webapp.mapper.connect( "history_archive_export", "/api/histories/{id}/exports", controller="histories", action="archive_export", conditions=dict( method=[ "PUT" ] ) ) diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 static/scripts/mvc/history/current-history-panel.js --- a/static/scripts/mvc/history/current-history-panel.js +++ b/static/scripts/mvc/history/current-history-panel.js @@ -84,7 +84,10 @@ var panel = this, historyFn = function(){ // make this current and get history data with one call - return jQuery.post( galaxy_config.root + 'api/histories/' + historyId + '/set_as_current' ); + return jQuery.ajax({ + url : galaxy_config.root + 'api/histories/' + historyId + '/set_as_current', + method : 'PUT' + }); }; return this.loadHistoryWithHDADetails( historyId, attributes, historyFn ) .then(function( historyData, hdaData ){ diff -r 4d2f3398ad6d6eebfe084a184bf860dd7c6f9071 -r c4950bc662256f013916b0aaabb73a47508213c5 static/scripts/packed/mvc/history/current-history-panel.js --- a/static/scripts/packed/mvc/history/current-history-panel.js +++ b/static/scripts/packed/mvc/history/current-history-panel.js @@ -1,1 +1,1 @@ -define(["mvc/dataset/hda-edit","mvc/history/history-panel","mvc/base-mvc"],function(b,f,c){var d=c.SessionStorageModel.extend({defaults:{searching:false,tagsEditorShown:false,annotationEditorShown:false},toString:function(){return"HistoryPanelPrefs("+JSON.stringify(this.toJSON())+")"}});d.storageKey=function e(){return("history-panel")};var a=f.HistoryPanel.extend({HDAViewClass:b.HDAEditView,emptyMsg:_l("This history is empty. Click 'Get Data' on the left tool menu to start"),noneFoundMsg:_l("No matching datasets found"),initialize:function(g){g=g||{};this.preferences=new d(_.extend({id:d.storageKey()},_.pick(g,_.keys(d.prototype.defaults))));f.HistoryPanel.prototype.initialize.call(this,g)},loadCurrentHistory:function(h){var g=this;return this.loadHistoryWithHDADetails("current",h).then(function(j,i){g.trigger("current-history",g)})},switchToHistory:function(j,i){var g=this,h=function(){return jQuery.post(galaxy_config.root+"api/histories/"+j+"/set_as_current")};return this.loadHistoryWithHDADetails(j,i,h).then(function(l,k){g.trigger("switched-history",g)})},createNewHistory:function(i){if(!Galaxy||!Galaxy.currUser||Galaxy.currUser.isAnonymous()){this.displayMessage("error",_l("You must be logged in to create histories"));return $.when()}var g=this,h=function(){return jQuery.post(galaxy_config.root+"api/histories",{current:true})};return this.loadHistory(undefined,i,h).then(function(k,j){g.trigger("new-history",g)})},setModel:function(h,g,i){f.HistoryPanel.prototype.setModel.call(this,h,g,i);if(this.model){this.log("checking for updates");this.model.checkForUpdates()}return this},_setUpModelEventHandlers:function(){f.HistoryPanel.prototype._setUpModelEventHandlers.call(this);if(Galaxy&&Galaxy.quotaMeter){this.listenTo(this.model,"change:nice_size",function(){Galaxy.quotaMeter.update()})}this.model.hdas.on("state:ready",function(h,i,g){if((!h.get("visible"))&&(!this.storage.get("show_hidden"))){this.removeHdaView(this.hdaViews[h.id])}},this)},render:function(i,j){i=(i===undefined)?(this.fxSpeed):(i);var g=this,h;if(this.model){h=this.renderModel()}else{h=this.renderWithoutModel()}$(g).queue("fx",[function(k){if(i&&g.$el.is(":visible")){g.$el.fadeOut(i,k)}else{k()}},function(k){g.$el.empty();if(h){g.$el.append(h.children());g.renderBasedOnPrefs()}k()},function(k){if(i&&!g.$el.is(":visible")){g.$el.fadeIn(i,k)}else{k()}},function(k){if(j){j.call(this)}g.trigger("rendered",this);k()}]);return this},renderBasedOnPrefs:function(){if(this.preferences.get("searching")){this.toggleSearchControls(0,true)}},_renderEmptyMsg:function(i){var h=this,g=h.$emptyMessage(i),j=$(".toolMenuContainer");if((_.isEmpty(h.hdaViews)&&!h.searchFor)&&(Galaxy&&Galaxy.upload&&j.size())){g.empty();g.html([_l("This history is empty. "),_l("You can "),'<a class="uploader-link" href="javascript:void(0)">',_l("load your own data"),"</a>",_l(" or "),'<a class="get-data-link" href="javascript:void(0)">',_l("get data from an external source"),"</a>"].join(""));g.find(".uploader-link").click(function(k){Galaxy.upload._eventShow(k)});g.find(".get-data-link").click(function(k){j.parent().scrollTop(0);j.find('span:contains("Get Data")').click()});g.show()}else{f.HistoryPanel.prototype._renderEmptyMsg.call(this,i)}return this},toggleSearchControls:function(h,g){var i=f.HistoryPanel.prototype.toggleSearchControls.call(this,h,g);this.preferences.set("searching",i)},_renderTags:function(g){var h=this;f.HistoryPanel.prototype._renderTags.call(this,g);if(this.preferences.get("tagsEditorShown")){this.tagsEditor.toggle(true)}this.tagsEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(i){h.preferences.set("tagsEditorShown",i.hidden)})},_renderAnnotation:function(g){var h=this;f.HistoryPanel.prototype._renderAnnotation.call(this,g);if(this.preferences.get("annotationEditorShown")){this.annotationEditor.toggle(true)}this.annotationEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(i){h.preferences.set("annotationEditorShown",i.hidden)})},connectToQuotaMeter:function(g){if(!g){return this}this.listenTo(g,"quota:over",this.showQuotaMessage);this.listenTo(g,"quota:under",this.hideQuotaMessage);this.on("rendered rendered:initial",function(){if(g&&g.isOverQuota()){this.showQuotaMessage()}});return this},showQuotaMessage:function(){var g=this.$el.find(".quota-message");if(g.is(":hidden")){g.slideDown(this.fxSpeed)}},hideQuotaMessage:function(){var g=this.$el.find(".quota-message");if(!g.is(":hidden")){g.slideUp(this.fxSpeed)}},connectToOptionsMenu:function(g){if(!g){return this}this.on("new-storage",function(i,h){if(g&&i){g.findItemByHtml(_l("Include Deleted Datasets")).checked=i.get("show_deleted");g.findItemByHtml(_l("Include Hidden Datasets")).checked=i.get("show_hidden")}});return this},toString:function(){return"CurrentHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{CurrentHistoryPanel:a}}); \ No newline at end of file +define(["mvc/dataset/hda-edit","mvc/history/history-panel","mvc/base-mvc"],function(b,f,c){var d=c.SessionStorageModel.extend({defaults:{searching:false,tagsEditorShown:false,annotationEditorShown:false},toString:function(){return"HistoryPanelPrefs("+JSON.stringify(this.toJSON())+")"}});d.storageKey=function e(){return("history-panel")};var a=f.HistoryPanel.extend({HDAViewClass:b.HDAEditView,emptyMsg:_l("This history is empty. Click 'Get Data' on the left tool menu to start"),noneFoundMsg:_l("No matching datasets found"),initialize:function(g){g=g||{};this.preferences=new d(_.extend({id:d.storageKey()},_.pick(g,_.keys(d.prototype.defaults))));f.HistoryPanel.prototype.initialize.call(this,g)},loadCurrentHistory:function(h){var g=this;return this.loadHistoryWithHDADetails("current",h).then(function(j,i){g.trigger("current-history",g)})},switchToHistory:function(j,i){var g=this,h=function(){return jQuery.ajax({url:galaxy_config.root+"api/histories/"+j+"/set_as_current",method:"PUT"})};return this.loadHistoryWithHDADetails(j,i,h).then(function(l,k){g.trigger("switched-history",g)})},createNewHistory:function(i){if(!Galaxy||!Galaxy.currUser||Galaxy.currUser.isAnonymous()){this.displayMessage("error",_l("You must be logged in to create histories"));return $.when()}var g=this,h=function(){return jQuery.post(galaxy_config.root+"api/histories",{current:true})};return this.loadHistory(undefined,i,h).then(function(k,j){g.trigger("new-history",g)})},setModel:function(h,g,i){f.HistoryPanel.prototype.setModel.call(this,h,g,i);if(this.model){this.log("checking for updates");this.model.checkForUpdates()}return this},_setUpModelEventHandlers:function(){f.HistoryPanel.prototype._setUpModelEventHandlers.call(this);if(Galaxy&&Galaxy.quotaMeter){this.listenTo(this.model,"change:nice_size",function(){Galaxy.quotaMeter.update()})}this.model.hdas.on("state:ready",function(h,i,g){if((!h.get("visible"))&&(!this.storage.get("show_hidden"))){this.removeHdaView(this.hdaViews[h.id])}},this)},render:function(i,j){i=(i===undefined)?(this.fxSpeed):(i);var g=this,h;if(this.model){h=this.renderModel()}else{h=this.renderWithoutModel()}$(g).queue("fx",[function(k){if(i&&g.$el.is(":visible")){g.$el.fadeOut(i,k)}else{k()}},function(k){g.$el.empty();if(h){g.$el.append(h.children());g.renderBasedOnPrefs()}k()},function(k){if(i&&!g.$el.is(":visible")){g.$el.fadeIn(i,k)}else{k()}},function(k){if(j){j.call(this)}g.trigger("rendered",this);k()}]);return this},renderBasedOnPrefs:function(){if(this.preferences.get("searching")){this.toggleSearchControls(0,true)}},_renderEmptyMsg:function(i){var h=this,g=h.$emptyMessage(i),j=$(".toolMenuContainer");if((_.isEmpty(h.hdaViews)&&!h.searchFor)&&(Galaxy&&Galaxy.upload&&j.size())){g.empty();g.html([_l("This history is empty. "),_l("You can "),'<a class="uploader-link" href="javascript:void(0)">',_l("load your own data"),"</a>",_l(" or "),'<a class="get-data-link" href="javascript:void(0)">',_l("get data from an external source"),"</a>"].join(""));g.find(".uploader-link").click(function(k){Galaxy.upload._eventShow(k)});g.find(".get-data-link").click(function(k){j.parent().scrollTop(0);j.find('span:contains("Get Data")').click()});g.show()}else{f.HistoryPanel.prototype._renderEmptyMsg.call(this,i)}return this},toggleSearchControls:function(h,g){var i=f.HistoryPanel.prototype.toggleSearchControls.call(this,h,g);this.preferences.set("searching",i)},_renderTags:function(g){var h=this;f.HistoryPanel.prototype._renderTags.call(this,g);if(this.preferences.get("tagsEditorShown")){this.tagsEditor.toggle(true)}this.tagsEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(i){h.preferences.set("tagsEditorShown",i.hidden)})},_renderAnnotation:function(g){var h=this;f.HistoryPanel.prototype._renderAnnotation.call(this,g);if(this.preferences.get("annotationEditorShown")){this.annotationEditor.toggle(true)}this.annotationEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(i){h.preferences.set("annotationEditorShown",i.hidden)})},connectToQuotaMeter:function(g){if(!g){return this}this.listenTo(g,"quota:over",this.showQuotaMessage);this.listenTo(g,"quota:under",this.hideQuotaMessage);this.on("rendered rendered:initial",function(){if(g&&g.isOverQuota()){this.showQuotaMessage()}});return this},showQuotaMessage:function(){var g=this.$el.find(".quota-message");if(g.is(":hidden")){g.slideDown(this.fxSpeed)}},hideQuotaMessage:function(){var g=this.$el.find(".quota-message");if(!g.is(":hidden")){g.slideUp(this.fxSpeed)}},connectToOptionsMenu:function(g){if(!g){return this}this.on("new-storage",function(i,h){if(g&&i){g.findItemByHtml(_l("Include Deleted Datasets")).checked=i.get("show_deleted");g.findItemByHtml(_l("Include Hidden Datasets")).checked=i.get("show_hidden")}});return this},toString:function(){return"CurrentHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{CurrentHistoryPanel:a}}); \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/79ab14af25e4/ Changeset: 79ab14af25e4 User: carlfeberhard Date: 2014-03-25 21:59:27 Summary: History/HDA API: begin normalizing exceptions and return codes; Browser testing: update and expand history/hda testing, add importable/published permissions testing, transfer upload functions to api module Affected #: 8 files diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -97,32 +97,32 @@ history_id = id deleted = string_as_bool( deleted ) - try: - if history_id == "most_recently_used": - if not trans.user or len( trans.user.galaxy_sessions ) <= 0: - return None - # Most recent active history for user sessions, not deleted - history = trans.user.galaxy_sessions[0].histories[-1].history + #try: + if history_id == "most_recently_used": + if not trans.user or len( trans.user.galaxy_sessions ) <= 0: + return None + # Most recent active history for user sessions, not deleted + history = trans.user.galaxy_sessions[0].histories[-1].history - elif history_id == "current": - history = trans.get_history( create=True ) + elif history_id == "current": + history = trans.get_history( create=True ) - else: - history = self.get_history( trans, history_id, check_ownership=False, - check_accessible=True, deleted=deleted ) + else: + history = self.get_history( trans, history_id, check_ownership=False, + check_accessible=True, deleted=deleted ) - history_data = self.get_history_dict( trans, history ) - history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) + history_data = self.get_history_dict( trans, history ) + history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) - except HTTPBadRequest, bad_req: - trans.response.status = 400 - raise exceptions.MessageException( bad_req.detail ) - - except Exception, e: - msg = "Error in history API at showing history detail: %s" % ( str( e ) ) - log.exception( msg ) - trans.response.status = 500 - return msg + #except HTTPBadRequest, bad_req: + # trans.response.status = 400 + # raise exceptions.MessageException( bad_req.detail ) + # + #except Exception, e: + # msg = "Error in history API at showing history detail: %s" % ( str( e ) ) + # log.exception( msg ) + # trans.response.status = 500 + # return msg return history_data diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/api-hda-tests.js --- a/test/casperjs/api-hda-tests.js +++ b/test/casperjs/api-hda-tests.js @@ -32,33 +32,12 @@ } spaceghost.user.loginOrRegisterUser( email, password ); -var uploadFilename = '1.sam', - uploadFilepath = '../../test-data/' + uploadFilename, - upload = {}; -spaceghost.thenOpen( spaceghost.baseUrl ).tools.uploadFile( uploadFilepath, function( uploadInfo ){ - upload = uploadInfo; +spaceghost.thenOpen( spaceghost.baseUrl, function(){ + this.api.tools.thenUpload( spaceghost.api.histories.show( 'current' ).id, { + filepath: this.options.scriptDir + '/../../test-data/1.sam' + }); }); -function hasKeys( object, keysArray ){ - if( !utils.isObject( object ) ){ return false; } - for( var i=0; i<keysArray.length; i += 1 ){ - if( !object.hasOwnProperty( keysArray[i] ) ){ - spaceghost.debug( 'object missing key: ' + keysArray[i] ); - return false; - } - } - return true; -} - -function countKeys( object ){ - if( !utils.isObject( object ) ){ return 0; } - var count = 0; - for( var key in object ){ - if( object.hasOwnProperty( key ) ){ count += 1; } - } - return count; -} - // =================================================================== TESTS var summaryKeys = [ 'id', 'name', 'type', 'url' ], detailKeys = [ @@ -76,11 +55,8 @@ 'metadata_comment_lines', 'metadata_data_lines' ]; -spaceghost.historypanel.waitForHdas().then( function(){ +spaceghost.then( function(){ - var uploaded = this.historypanel.hdaElementInfoByTitle( uploadFilename ); - this.info( 'found uploaded hda: ' + uploaded.attributes.id ); - this.debug( 'uploaded hda: ' + this.jsonStr( uploaded ) ); // ------------------------------------------------------------------------------------------- INDEX this.test.comment( 'index should return a list of summary data for each hda' ); var histories = this.api.histories.index(), @@ -92,18 +68,17 @@ this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); var firstHda = hdaIndex[0]; - this.test.assert( hasKeys( firstHda, summaryKeys ), 'Has the proper keys' ); + this.test.assert( this.hasKeys( firstHda, summaryKeys ), 'Has the proper keys' ); this.test.assert( this.api.isEncodedId( firstHda.id ), 'Id appears well-formed: ' + firstHda.id ); - this.test.assert( uploaded.text.indexOf( firstHda.name ) !== -1, 'Title matches: ' + firstHda.name ); - // not caring about type or url here + this.test.assert( firstHda.name === '1.sam', 'Title matches: ' + firstHda.name ); // ------------------------------------------------------------------------------------------- SHOW this.test.comment( 'show should get an HDA details object' ); var hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); //this.debug( this.jsonStr( hdaShow ) ); - this.test.assert( hasKeys( hdaShow, detailKeys ), 'Has the proper keys' ); + this.test.assert( this.hasKeys( hdaShow, detailKeys ), 'Has the proper keys' ); //TODO: validate data in each hdaShow attribute... @@ -117,7 +92,7 @@ this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); firstHda = hdaIndex[0]; - this.test.assert( hasKeys( firstHda, detailKeys ), 'Has the proper keys' ); + this.test.assert( this.hasKeys( firstHda, detailKeys ), 'Has the proper keys' ); //TODO??: validate data in firstHda attribute? we ASSUME it's from a common method as show... @@ -133,29 +108,21 @@ var returned = this.api.hdas.update( lastHistory.id, firstHda.id, { name : hdaShow.name }); - this.test.assert( countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); + this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); this.test.comment( 'updating using a nonsense key should NOT fail with an error' ); returned = this.api.hdas.update( lastHistory.id, firstHda.id, { konamiCode : 'uuddlrlrba' }); - this.test.assert( countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); + this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); this.test.comment( 'updating by attempting to change type should cause an error' ); - err = {}; - try { + this.api.assertRaises( function(){ returned = this.api.hdas.update( lastHistory.id, firstHda.id, { //name : false deleted : 'sure why not' }); - } catch( error ){ - err = error; - //this.debug( this.jsonStr( err ) ); - } - this.test.assert( !!err.message, "Error occurred: " + err.message ); - this.test.assert( err.status === 400, "Error status is 400: " + err.status ); - //TODO??: other type checks? - + }, 400, 'deleted must be a boolean', 'changing deleted type failed' ); // ........................................................................................... name this.test.comment( 'update should allow changing the name' ); @@ -330,25 +297,68 @@ // ------------------------------------------------------------------------------------------- ERRORS this.test.comment( 'create should error with "Please define the source" when the param "from_ld_id" is not used' ); - var errored = false; - try { - // sending an empty object won't work - var created = this.api.hdas.create( lastHistory.id, { bler: 'bler' } ); + this.api.assertRaises( function(){ + this.api.hdas.create( lastHistory.id, { bler: 'bler' } ); + }, 400, 'Please define the source', 'create with no source failed' ); - } catch( err ){ - errored = true; - this.test.assert( err.message.indexOf( 'Please define the source' ) !== -1, - 'Error has the proper message: ' + err.message ); - this.test.assert( err.status === 400, 'Error has the proper status code: ' + err.status ); - } - if( !errored ){ - this.test.fail( 'create without "from_ld_id" did not cause error' ); - } + this.test.comment( 'updating using a nonsense key should fail silently' ); + returned = this.api.hdas.update( lastHistory.id, hdaShow.id, { + konamiCode : 'uuddlrlrba' + }); + this.test.assert( returned.konamiCode === undefined, 'key was not set: ' + returned.konamiCode ); - //var returned = this.api.hdas.update( lastHistory.id, hdaIndex[0].id, { deleted: true, blerp: 'blerp' }); - //var returned = this.api.hdas.update( lastHistory.id, { deleted: true, blerp: 'blerp' }); - //this.debug( 'returned:' + this.jsonStr( returned ) ); - //this.debug( 'page:' + this.jsonStr( this.page ) ); + spaceghost.test.comment( 'A bad id should throw an error when using show' ); + this.api.assertRaises( function(){ + this.api.hdas.show( lastHistory.id, '1234123412341234' ); + }, 500, 'unable to decode', 'Bad Request with invalid id: show' ); + spaceghost.test.comment( 'A bad id should throw an error when using update' ); + this.api.assertRaises( function(){ + this.api.hdas.update( lastHistory.id, '1234123412341234', {} ); + }, 400, 'invalid literal for int', 'Bad Request with invalid id: update' ); + spaceghost.test.comment( 'A bad id should throw an error when using delete' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( lastHistory.id, '1234123412341234' ); + }, 500, 'invalid literal for int', 'Bad Request with invalid id: delete' ); + spaceghost.test.comment( 'A bad id should throw an error when using undelete' ); + + this.test.comment( 'updating by attempting to change type should cause an error' ); + [ 'name', 'annotation', 'genome_build', 'misc_info' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = false; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); + }, 400, key + ' must be a string or unicode', 'type validation error' ); + }); + [ 'deleted', 'visible' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = 'straaang'; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); + }, 400, key + ' must be a boolean', 'type validation error' ); + }); + [ 'you\'re it', [ true ] ].forEach( function( badVal ){ + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, { tags: badVal }); + }, 400, 'tags must be a list', 'type validation error' ); + }); + + // ------------------------------------------------------------------------------------------- DELETE + this.test.comment( 'calling delete on an hda should mark it as deleted but not change the history size' ); + lastHistory = this.api.histories.show( lastHistory.id ); + var sizeBeforeDelete = lastHistory.nice_size; + + returned = this.api.hdas.delete_( lastHistory.id, firstHda.id ); + //this.debug( this.jsonStr( returned ) ); + + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.deleted, 'hda is marked deleted' ); + lastHistory = this.api.histories.show( lastHistory.id ); + this.test.assert( lastHistory.nice_size === sizeBeforeDelete, 'history size has not changed' ); + + // by default, purging fails bc uni.ini:allow_user_dataset_purge=False + this.api.assertRaises( function(){ + returned = this.api.hdas.delete_( lastHistory.id, firstHda.id, { purge : true }); + }, 403, 'This instance does not allow user dataset purging', 'Purge failed' ); /* */ }); diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/api-history-permission-tests.js --- /dev/null +++ b/test/casperjs/api-history-permission-tests.js @@ -0,0 +1,303 @@ +/* Utility to load a specific page and output html, page text, or a screenshot + * Optionally wait for some time, text, or dom selector + */ +try { + //...if there's a better way - please let me know, universe + var scriptDir = require( 'system' ).args[3] + // remove the script filename + .replace( /[\w|\.|\-|_]*$/, '' ) + // if given rel. path, prepend the curr dir + .replace( /^(?!\/)/, './' ), + spaceghost = require( scriptDir + 'spaceghost' ).create({ + // script options here (can be overridden by CLI) + //verbose: true, + //logLevel: debug, + scriptDir: scriptDir + }); + +} catch( error ){ + console.debug( error ); + phantom.exit( 1 ); +} +spaceghost.start(); + + +// =================================================================== SET UP +var utils = require( 'utils' ); + +var email = spaceghost.user.getRandomEmail(), + password = '123456'; +if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; +} +var email2 = spaceghost.user.getRandomEmail(), + password2 = '123456'; +if( spaceghost.fixtureData.testUser2 ){ + email2 = spaceghost.fixtureData.testUser2.email; + password2 = spaceghost.fixtureData.testUser2.password; +} + +var inaccessibleHistory, accessibleHistory, publishedHistory, + inaccessibleHdas, accessibleHdas, publishedHdas, + accessibleLink; + +// =================================================================== TESTS +//// ------------------------------------------------------------------------------------------- create 3 histories +spaceghost.user.loginOrRegisterUser( email, password ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + // create three histories: make the 2nd importable (via the API), and the third published + + this.test.comment( 'importable, slug, and published should all be returned by show and initially off' ); + // make the current the inaccessible one + inaccessibleHistory = this.api.histories.show( 'current' ); + this.test.assert( this.hasKeys( inaccessibleHistory, [ 'id', 'name', 'slug', 'importable', 'published' ] ), + 'Has the proper keys' ); + this.test.assert( inaccessibleHistory.slug === null, + 'initial slug is null: ' + inaccessibleHistory.slug ); + this.test.assert( inaccessibleHistory.importable === false, + 'initial importable is false: ' + inaccessibleHistory.importable ); + this.test.assert( inaccessibleHistory.published === false, + 'initial published is false: ' + inaccessibleHistory.published ); + this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); + + this.test.comment( 'Setting importable to true should create a slug, username_and_slug, and importable === true' ); + accessibleHistory = this.api.histories.create({ name: 'accessible' }); + var returned = this.api.histories.update( accessibleHistory.id, { + importable : true + }); + this.debug( this.jsonStr( returned ) ); + accessibleHistory = this.api.histories.show( accessibleHistory.id ); + this.test.assert( this.hasKeys( accessibleHistory, [ 'username_and_slug' ] ), + 'Has the proper keys' ); + this.test.assert( accessibleHistory.slug === 'accessible', + 'slug is not null: ' + accessibleHistory.slug ); + this.test.assert( accessibleHistory.importable, + 'importable is true: ' + accessibleHistory.importable ); + accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/accessible'; + this.test.assert( accessibleHistory.username_and_slug === accessibleLink, + 'username_and_slug is proper: ' + accessibleHistory.username_and_slug ); + + this.test.comment( 'Setting published to true should create make accessible and published === true' ); + publishedHistory = this.api.histories.create({ name: 'published' }); + returned = this.api.histories.update( publishedHistory.id, { + published : true + }); + this.debug( this.jsonStr( returned ) ); + publishedHistory = this.api.histories.show( publishedHistory.id ); + this.test.assert( this.hasKeys( publishedHistory, [ 'username_and_slug' ] ), + 'Has the proper keys' ); + this.test.assert( publishedHistory.published, + 'published is true: ' + publishedHistory.published ); + this.test.assert( publishedHistory.importable, + 'importable is true: ' + publishedHistory.importable ); + this.test.assert( publishedHistory.slug === 'published', + 'slug is not null: ' + publishedHistory.slug ); + accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/published'; + this.test.assert( publishedHistory.username_and_slug === accessibleLink, + 'username_and_slug is proper: ' + publishedHistory.username_and_slug ); + +}); + +//// ------------------------------------------------------------------------------------------- upload some files +spaceghost.then( function(){ + this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); + this.api.tools.thenUpload( accessibleHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); + this.api.tools.thenUpload( publishedHistory.id, { filepath: this.options.scriptDir + '/../../test-data/1.bed' }); +}); +spaceghost.then( function(){ + // check that they're there + inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), + accessibleHdas = this.api.hdas.index( accessibleHistory.id ), + publishedHdas = this.api.hdas.index( publishedHistory.id ); + this.test.assert( inaccessibleHdas.length === 1, + 'uploaded file to inaccessible: ' + inaccessibleHdas.length ); + this.test.assert( accessibleHdas.length === 1, + 'uploaded file to accessible: ' + accessibleHdas.length ); + this.test.assert( publishedHdas.length === 1, + 'uploaded file to published: ' + publishedHdas.length ); +}); +spaceghost.user.logout(); + +//// ------------------------------------------------------------------------------------------- log in user2 +function ensureInaccessibility( history, hdas ){ + + this.test.comment( 'all four CRUD API calls should fail for user2 with history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.show( history.id ); + }, 400, 'History is not accessible to the current user', 'show failed with error' ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'History is not accessible to the current user', 'copy failed with error' ); + this.api.assertRaises( function(){ + this.api.histories.update( history.id, { deleted: true }); + }, 500, 'History is not owned by the current user', 'update failed with error' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( history.id ); + }, 400, 'History is not owned by the current user', 'delete failed with error' ); + + this.test.comment( 'all four HDA CRUD API calls should fail for user2 with history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.index( history.id ); + }, 500, 'History is not accessible to the current user', 'index failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.show( history.id, hdas[0].id ); + }, 500, 'History is not accessible to the current user', 'show failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( history.id, hdas[0].id ); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); + + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + 'from an inaccessible history should fail' ); + this.api.assertRaises( function(){ + var returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : hdas[0].id + }); + this.debug( this.jsonStr( returned ) ); + }, 403, 'History is not accessible to the current user', 'copy failed with error' ); + +} +spaceghost.user.loginOrRegisterUser( email2, password2 ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + + // ----------------------------------------------------------------------------------------- user2 + inaccessible + ensureInaccessibility.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); + + // ----------------------------------------------------------------------------------------- user2 + accessible + this.test.comment( 'show should work for the importable history' ); + this.test.assert( this.api.histories.show( accessibleHistory.id ).id === accessibleHistory.id, + 'show worked' ); + + this.test.comment( 'create/copy should work for the importable history' ); + var returned = this.api.histories.create({ history_id : accessibleHistory.id }); + this.test.assert( returned.name === "Copy of '" + accessibleHistory.name + "'", + 'copied name matches: ' + returned.name ); + + this.test.comment( 'update should fail for the importable history' ); + this.api.assertRaises( function(){ + this.api.histories.update( accessibleHistory.id, { deleted: true }); + }, 500, 'History is not owned by the current user', 'update failed with error' ); + this.test.comment( 'delete should fail for the importable history' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( accessibleHistory.id ); + }, 400, 'History is not owned by the current user', 'delete failed with error' ); + + this.test.comment( 'indexing should work for the contents of the importable history' ); + this.test.assert( this.api.hdas.index( accessibleHistory.id ).length === 1, + 'index worked' ); + this.test.comment( 'showing should work for the contents of the importable history' ); + this.test.assert( this.api.hdas.show( accessibleHistory.id, accessibleHdas[0].id ).id === accessibleHdas[0].id, + 'show worked' ); + this.test.comment( 'updating should fail for the contents of the importable history' ); + this.api.assertRaises( function(){ + this.api.hdas.update( accessibleHistory.id, accessibleHdas[0].id, { deleted: true }); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); + this.test.comment( 'deleting should fail for the contents of the importable history' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( accessibleHistory.id, accessibleHdas[0].id ); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); + this.test.comment( 'copying a dataset from the importable history should work' ); + returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : accessibleHdas[0].id + }); + this.test.assert( returned.name === accessibleHdas[0].name, 'successful copy from: ' + returned.name ); + + this.test.comment( 'copying a dataset into the importable history should fail' ); + this.api.assertRaises( function(){ + this.api.hdas.create( accessibleHistory.id, { + source : 'hda', + // should error before it checks the id + content : 'bler' + }); + }, 400, 'History is not owned by the current user', 'copy to failed' ); + + //// ----------------------------------------------------------------------------------------- user2 + published + this.test.comment( 'show should work for the published history' ); + this.test.assert( this.api.histories.show( publishedHistory.id ).id === publishedHistory.id, + 'show worked' ); + this.test.comment( 'create/copy should work for the published history' ); + returned = this.api.histories.create({ history_id : publishedHistory.id }); + this.test.assert( returned.name === "Copy of '" + publishedHistory.name + "'", + 'copied name matches: ' + returned.name ); + this.test.comment( 'update should fail for the published history' ); + this.api.assertRaises( function(){ + this.api.histories.update( publishedHistory.id, { deleted: true }); + }, 500, 'History is not owned by the current user', 'update failed with error' ); + this.test.comment( 'delete should fail for the published history' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( publishedHistory.id ); + }, 400, 'History is not owned by the current user', 'delete failed with error' ); + + this.test.comment( 'indexing should work for the contents of the published history' ); + this.test.assert( this.api.hdas.index( publishedHistory.id ).length === 1, + 'index worked' ); + this.test.comment( 'showing should work for the contents of the published history' ); + this.test.assert( this.api.hdas.show( publishedHistory.id, publishedHdas[0].id ).id === publishedHdas[0].id, + 'show worked' ); + this.test.comment( 'updating should fail for the contents of the published history' ); + this.api.assertRaises( function(){ + this.api.hdas.update( publishedHistory.id, publishedHdas[0].id, { deleted: true }); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'update failed with error' ); + this.test.comment( 'deleting should fail for the contents of the published history' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( publishedHistory.id, publishedHdas[0].id ); + }, 500, 'HistoryDatasetAssociation is not owned by current user', 'delete failed with error' ); + + this.test.comment( 'copying a dataset from the published history should work' ); + returned = this.api.hdas.create( this.api.histories.show( 'current' ).id, { + source : 'hda', + content : publishedHdas[0].id + }); + this.test.assert( returned.name === publishedHdas[0].name, 'successful copy from: ' + returned.name ); + + this.test.comment( 'copying a dataset into the published history should fail' ); + this.api.assertRaises( function(){ + this.api.hdas.create( publishedHistory.id, { + source : 'hda', + // should error before it checks the id + content : 'bler' + }); + }, 400, 'History is not owned by the current user', 'copy to failed' ); +}); +spaceghost.user.logout(); + + +//// ------------------------------------------------------------------------------------------- user1 revoke perms +spaceghost.user.loginOrRegisterUser( email, password ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + this.test.comment( 'revoking perms should prevent access' ); + this.api.histories.update( accessibleHistory.id, { + importable : false + }); + var returned = this.api.histories.show( accessibleHistory.id ); + this.test.assert( !returned.importable, 'now not importable' ); + this.test.assert( !returned.published, '(still not published)' ); + this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); + + this.api.histories.update( publishedHistory.id, { + importable : false, + published : false + }); + returned = this.api.histories.show( publishedHistory.id ); + this.test.assert( !returned.importable, 'now not importable' ); + this.test.assert( !returned.published, 'now not published' ); + this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); +}); +spaceghost.user.logout(); + + +//// ------------------------------------------------------------------------------------------- user2 retry perms +spaceghost.user.loginOrRegisterUser( email2, password2 ); +spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + ensureInaccessibility.call( spaceghost, accessibleHistory, accessibleHdas ); + ensureInaccessibility.call( spaceghost, publishedHistory, publishedHdas ); +}); + +// =================================================================== +spaceghost.run( function(){ +}); diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/api-history-tests.js --- a/test/casperjs/api-history-tests.js +++ b/test/casperjs/api-history-tests.js @@ -33,25 +33,6 @@ } spaceghost.user.loginOrRegisterUser( email, password ); -function hasKeys( object, keysArray ){ - if( !utils.isObject( object ) ){ return false; } - for( var i=0; i<keysArray.length; i += 1 ){ - if( !object.hasOwnProperty( keysArray[i] ) ){ - spaceghost.debug( 'object missing key: ' + keysArray[i] ); - return false; - } - } - return true; -} - -function countKeys( object ){ - if( !utils.isObject( object ) ){ return 0; } - var count = 0; - for( var key in object ){ - if( object.hasOwnProperty( key ) ){ count += 1; } - } - return count; -} // =================================================================== TESTS spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ @@ -64,7 +45,7 @@ this.test.assert( historyIndex.length >= 1, 'Has at least one history' ); var firstHistory = historyIndex[0]; - this.test.assert( hasKeys( firstHistory, [ 'id', 'name', 'url' ] ), 'Has the proper keys' ); + this.test.assert( this.hasKeys( firstHistory, [ 'id', 'name', 'url' ] ), 'Has the proper keys' ); this.test.assert( this.api.isEncodedId( firstHistory.id ), 'Id appears well-formed' ); @@ -72,7 +53,7 @@ this.test.comment( 'show should get a history details object' ); var historyShow = this.api.histories.show( firstHistory.id ); //this.debug( this.jsonStr( historyShow ) ); - this.test.assert( hasKeys( historyShow, [ + this.test.assert( this.hasKeys( historyShow, [ 'id', 'name', 'annotation', 'nice_size', 'contents_url', 'state', 'state_details', 'state_ids' ]), 'Has the proper keys' ); @@ -83,8 +64,8 @@ 'ok', 'paused', 'queued', 'running', 'setting_metadata', 'upload' ], state_details = historyShow.state_details, state_ids = historyShow.state_ids; - this.test.assert( hasKeys( state_details, states ), 'state_details has the proper keys' ); - this.test.assert( hasKeys( state_ids, states ), 'state_ids has the proper keys' ); + this.test.assert( this.hasKeys( state_details, states ), 'state_details has the proper keys' ); + this.test.assert( this.hasKeys( state_ids, states ), 'state_ids has the proper keys' ); var state_detailsAreNumbers = true; state_idsAreArrays = true; states.forEach( function( state ){ @@ -103,20 +84,19 @@ this.test.assert( this.api.histories.show( this.api.histories.index()[0].id ).id === firstHistory.id, 'combining function calls works' ); - // test server bad id protection - this.test.comment( 'A bad id to show should throw an error' ); - this.assertRaises( function(){ - this.api.histories.show( '1234123412341234' ); - }, '400 Bad Request', 'Raises an exception' ); + this.test.comment( 'Calling show with "current" should return the current history' ); + this.test.assert( this.api.histories.show( 'current' ).id === historyShow.id, 'current returned same id' ); // ------------------------------------------------------------------------------------------- CREATE - this.test.comment( 'Calling create should create a new history and allow setting the name' ); + this.test.comment( 'Calling create should create a new history and allow setting the name and making it current' ); var newHistoryName = 'Created History', - createdHistory = this.api.histories.create({ name: newHistoryName }); + createdHistory = this.api.histories.create({ name: newHistoryName, current: true }); //this.debug( 'returned from create:\n' + this.jsonStr( createdHistory ) ); this.test.assert( createdHistory.name === newHistoryName, "Name of created history (from create) is correct: " + createdHistory.name ); + this.test.assert( this.api.histories.show( 'current' ).id === createdHistory.id, + "was made current" ); // check the index var newFirstHistory = this.api.histories.index()[0]; @@ -156,50 +136,25 @@ "Undeletion returned 'OK' - even though that's not a great, informative response: " + undeletedHistory ); newFirstHistory = this.api.histories.index()[0]; - this.debug( 'newFirstHistory:\n' + this.jsonStr( newFirstHistory ) ); + //this.debug( 'newFirstHistory:\n' + this.jsonStr( newFirstHistory ) ); this.test.assert( newFirstHistory.id === createdHistory.id, "Id of last history (from index) DOES appear after undeletion: " + newFirstHistory.id ); //TODO: show, deleted flag //TODO: delete, purge flag + + + // ------------------------------------------------------------------------------------------- set_as_current + this.test.comment( 'calling set_as_current on a non-current history will make it current' ); + this.test.assert( this.api.histories.show( 'current' ).id !== historyShow.id, historyShow.id + ' is not current' ); + var returned = this.api.histories.set_as_current( historyShow.id ); + this.debug( this.jsonStr( returned ) ); + this.test.assert( this.api.histories.show( 'current' ).id === historyShow.id, 'made current' ); + this.api.histories.set_as_current( newFirstHistory.id ); + + // ------------------------------------------------------------------------------------------- UPDATE - // ........................................................................................... idiot proofing - this.test.comment( 'updating to the current value should return no value (no change)' ); - historyShow = this.api.histories.show( newFirstHistory.id ); - var returned = this.api.histories.update( newFirstHistory.id, { - name : historyShow.name - }); - this.test.assert( countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); - - this.test.comment( 'updating using a nonsense key should fail silently' ); - var err = null; - try { - returned = this.api.histories.update( newFirstHistory.id, { - konamiCode : 'uuddlrlrba' - }); - } catch( error ){ - err = error; - //this.debug( this.jsonStr( err ) ); - } - this.test.assert( err === null, "No error occurred: " + this.jsonStr( err ) ); - - this.test.comment( 'updating by attempting to change type should cause an error' ); - err = {}; - try { - returned = this.api.histories.update( newFirstHistory.id, { - //name : false - deleted : 'sure why not' - }); - } catch( error ){ - err = error; - //this.debug( this.jsonStr( err ) ); - } - this.test.assert( !!err.message, "Error occurred: " + err.message ); - this.test.assert( err.status === 400, "Error status is 400: " + err.status ); - //TODO??: other type checks? - - // ........................................................................................... name this.test.comment( 'update should allow changing the name' ); returned = this.api.histories.update( newFirstHistory.id, { @@ -339,8 +294,58 @@ // ------------------------------------------------------------------------------------------- ERRORS - //TODO: make sure expected errors are being passed back (but no permissions checks here - different suite) - // bad ids: index, show, update, delete, undelete + // ........................................................................................... idiot proofing + this.test.comment( 'updating to the current value should return no value (no change)' ); + historyShow = this.api.histories.show( newFirstHistory.id ); + returned = this.api.histories.update( newFirstHistory.id, { + name : historyShow.name + }); + this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); + + this.test.comment( 'updating using a nonsense key should fail silently' ); + returned = this.api.histories.update( newFirstHistory.id, { + konamiCode : 'uuddlrlrba' + }); + this.test.assert( returned.konamiCode === undefined, 'key was not set: ' + returned.konamiCode ); + + // test server bad id protection + spaceghost.test.comment( 'A bad id should throw an error' ); + this.api.assertRaises( function(){ + this.api.histories.show( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); + spaceghost.test.comment( 'A bad id should throw an error when using update' ); + this.api.assertRaises( function(){ + this.api.histories.update( '1234123412341234', {} ); + }, 500, 'unable to decode', 'Bad Request with invalid id: update' ); + spaceghost.test.comment( 'A bad id should throw an error when using delete' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: delete' ); + spaceghost.test.comment( 'A bad id should throw an error when using undelete' ); + this.api.assertRaises( function(){ + this.api.histories.undelete( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: undelete' ); + + this.test.comment( 'updating by attempting to change type should cause an error' ); + [ 'name', 'annotation' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = false; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.histories.update( newFirstHistory.id, updatedAttrs ); + }, 400, key + ' must be a string or unicode', 'type validation error' ); + }); + [ 'deleted', 'importable', 'published' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = 'straaang'; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.histories.update( newFirstHistory.id, updatedAttrs ); + }, 400, key + ' must be a boolean', 'type validation error' ); + }); + [ 'you\'re it', [ true ] ].forEach( function( badVal ){ + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.histories.update( newFirstHistory.id, { tags: badVal }); + }, 400, 'tags must be a list', 'type validation error' ); + }); /* */ //this.debug( this.jsonStr( historyShow ) ); diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/casperjs_runner.py --- a/test/casperjs/casperjs_runner.py +++ b/test/casperjs/casperjs_runner.py @@ -81,6 +81,7 @@ """ # debugging flag - set to true to have casperjs tests output with --verbose=true and --logLevel=debug + #debug = True debug = False # bit of a hack - this is the beginning of the last string when capserjs --verbose=true --logLevel=debug # use this to get subprocess to stop waiting for output @@ -191,6 +192,7 @@ except ValueError, val_err: if str( val_err ) == 'No JSON object could be decoded': log.debug( '(error parsing returned JSON from casperjs, dumping stdout...)\n:%s', stdout_output ) + return HeadlessJSJavascriptError( 'see log for details' ) else: raise @@ -375,19 +377,29 @@ class Test_04_HDAs( CasperJSTestCase ): """Tests for HistoryDatasetAssociation fetching, rendering, and modeling. """ - def test_00_HDA_states( self ): - """Test structure rendering of HDAs in all the possible HDA states - """ - self.run_js_script( 'hda-state-tests.js' ) + #def test_00_HDA_states( self ): + # """Test structure rendering of HDAs in all the possible HDA states + # """ + # self.run_js_script( 'hda-state-tests.js' ) class Test_05_API( CasperJSTestCase ): """Tests for API functionality and security. """ - def test_00_history_api( self ): - """Test history API. + #def test_00_history_api( self ): + # """Test history API. + # """ + # self.run_js_script( 'api-history-tests.js' ) + + def test_01_hda_api( self ): + """Test HDA API. """ - self.run_js_script( 'api-history-tests.js' ) + self.run_js_script( 'api-hda-tests.js' ) + + def test_02_history_permissions_api( self ): + """Test API permissions for importable, published histories. + """ + self.run_js_script( 'api-history-permission-tests.js' ) # ==================================================================== MAIN diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/modules/api.js --- a/test/casperjs/modules/api.js +++ b/test/casperjs/modules/api.js @@ -75,12 +75,63 @@ if( resp.status !== 200 ){ // grrr... this doesn't lose the \n\r\t //throw new APIError( resp.responseText.replace( /[\s\n\r\t]+/gm, ' ' ).replace( /"/, '' ) ); - this.spaceghost.debug( 'api error response status code: ' + resp.status ); + this.spaceghost.debug( 'API error: code: ' + resp.status + ', responseText: ' + resp.responseText ); + this.spaceghost.debug( '\t responseJSON: ' + this.spaceghost.jsonStr( resp.responseJSON ) ); throw new APIError( resp.responseText, resp.status ); } return JSON.parse( resp.responseText ); }; +// =================================================================== TESTING +/** Checks whether fn raises an error with a message that contains a status and given string. + * NOTE: DOES NOT work with steps. @see SpaceGhost#assertStepsRaise + * @param {Function} testFn a function that may throw an error + * @param {Integer} statusExpected the HTTP status code expected + * @param {String} errMsgContains some portion of the correct error msg + * @private + */ +API.prototype._APIRaises = function _APIRaises( testFn, statusExpected, errMsgContains ){ + var failed = false; + try { + testFn.call( this.spaceghost ); + } catch( err ){ + this.spaceghost.debug( err.name + ': ' + err.status ); + this.spaceghost.debug( err.message ); + if( ( err.name === 'APIError' ) + && ( err.status && err.status === statusExpected ) + && ( err.message.indexOf( errMsgContains ) !== -1 ) ){ + failed = true; + + // re-raise other, non-searched-for errors + } else { + throw err; + } + } + return failed; +}; + +/** Simple assert raises. + * NOTE: DOES NOT work with steps. @see SpaceGhost#assertStepsRaise + * @param {Function} testFn a function that may throw an error + * @param {Integer} statusExpected the HTTP status code expected + * @param {String} errMsgContains some portion of the correct error msg + * @param {String} msg assertion message to display + */ +API.prototype.assertRaises = function assertRaises( testFn, statusExpected, errMsgContains, msg ){ + return this.spaceghost.test.assert( this._APIRaises( testFn, statusExpected, errMsgContains ), msg ); +}; + +/** Simple assert does not raise. + * NOTE: DOES NOT work with steps. @see SpaceGhost#assertStepsRaise + * @param {Function} testFn a function that may throw an error + * @param {Integer} statusExpected the HTTP status code expected + * @param {String} errMsgContains some portion of the correct error msg + * @param {String} msg assertion message to display + */ +API.prototype.assertDoesntRaise = function assertDoesntRaise( testFn, statusExpected, errMsgContains, msg ){ + return this.spaceghost.test.assert( !this._APIRaises( testFn, statusExpected, errMsgContains ), msg ); +}; + // =================================================================== MISC API.prototype.isEncodedId = function isEncodedId( id ){ if( typeof id !== 'string' ){ return false; } @@ -164,7 +215,8 @@ create : 'api/histories', delete_ : 'api/histories/%s', undelete: 'api/histories/deleted/%s/undelete', - update : 'api/histories/%s' + update : 'api/histories/%s', + set_as_current : 'api/histories/%s/set_as_current' }; HistoriesAPI.prototype.index = function index( deleted ){ @@ -179,7 +231,7 @@ HistoriesAPI.prototype.show = function show( id, deleted ){ this.api.spaceghost.info( 'histories.show: ' + [ id, (( deleted )?( 'w deleted' ):( '' )) ] ); - id = ( id === 'most_recently_used' )?( id ):( this.api.ensureId( id ) ); + id = ( id === 'most_recently_used' || id === 'current' )?( id ):( this.api.ensureId( id ) ); deleted = deleted || false; return this.api._ajax( utils.format( this.urlTpls.show, id ), { data : { deleted: deleted } @@ -217,7 +269,7 @@ }); }; -HistoriesAPI.prototype.update = function create( id, payload ){ +HistoriesAPI.prototype.update = function update( id, payload ){ this.api.spaceghost.info( 'histories.update: ' + id + ',' + this.api.spaceghost.jsonStr( payload ) ); // py.payload <-> ajax.data @@ -231,6 +283,15 @@ }); }; +HistoriesAPI.prototype.set_as_current = function set_as_current( id ){ + this.api.spaceghost.info( 'histories.set_as_current: ' + id ); + id = this.api.ensureId( id ); + + return this.api._ajax( utils.format( this.urlTpls.set_as_current, id ), { + type : 'PUT' + }); +}; + // =================================================================== HDAS var HDAAPI = function HDAAPI( api ){ @@ -297,6 +358,23 @@ }); }; +HDAAPI.prototype.delete_ = function create( historyId, id, purge ){ + this.api.spaceghost.info( 'hdas.delete_: ' + [ historyId, id ] ); + historyId = this.api.ensureId( historyId ); + id = this.api.ensureId( id ); + + // have to attach like GET param - due to body loss in jq + url = utils.format( this.urlTpls.update, historyId, id ); + if( purge ){ + url += '?purge=True'; + } + return this.api._ajax( url, { + type : 'DELETE' + }); +}; + +//TODO: delete_ + // =================================================================== TOOLS var ToolsAPI = function HDAAPI( api ){ @@ -350,6 +428,218 @@ }); }; +//ToolsAPI.prototype.uploadByForm = function upload( historyId, options ){ +// this.api.spaceghost.debug( '-------------------------------------------------' ); +// this.api.spaceghost.info( 'tools.upload: ' + [ historyId, '(contents)', this.api.spaceghost.jsonStr( options ) ] ); +// this.api.ensureId( historyId ); +// options = options || {}; +// +// this.api.spaceghost.evaluate( function( url ){ +// var html = [ +// '<form action="', url, '" method="post" enctype="multipart/form-data">', +// '<input type="file" name="files_0|file_data">', +// '<input type="hidden" name="tool_id" />', +// '<input type="hidden" name="history_id" />', +// '<input type="hidden" name="inputs" />', +// '<button type="submit">Submit</button>', +// '</form>' +// ]; +// document.write( html.join('') ); +// //document.getElementsByTagName( 'body' )[0].innerHTML = html.join(''); +// }, utils.format( this.urlTpls.create ) ); +// +// this.api.spaceghost.fill( 'form', { +// 'files_0|file_data' : '1.txt', +// 'tool_id' : 'upload1', +// 'history_id' : historyId, +// 'inputs' : JSON.stringify({ +// 'file_type' : 'auto', +// 'files_0|type' : 'upload_dataset', +// 'to_posix_lines' : true, +// 'space_to_tabs' : false, +// 'dbkey' : '?' +// }) +// }, true ); +// // this causes the page to switch...I think +//}; + +/** paste a file - either from a string (options.paste) or from a filesystem file (options.filepath) */ +ToolsAPI.prototype.uploadByPaste = function upload( historyId, options ){ + this.api.spaceghost.info( 'tools.upload: ' + [ historyId, this.api.spaceghost.jsonStr( options ) ] ); + this.api.ensureId( historyId ); + options = options || {}; + + var inputs = { + 'files_0|NAME' : options.name || 'Test Dataset', + 'dbkey' : options.dbkey || '?', + 'file_type' : options.ext || 'auto' + }; + if( options.filepath ){ + var fs = require( 'fs' ); + inputs[ 'files_0|url_paste' ] = fs.read( options.filepath ); + + } else if( options.paste ){ + inputs[ 'files_0|url_paste' ] = options.paste; + } + if( options.posix ){ + inputs[ 'files_0|to_posix_lines' ] = 'Yes'; + } + if( options.tabs ){ + inputs[ 'files_0|space_to_tab' ] = 'Yes'; + } + return this.api._ajax( utils.format( this.urlTpls.create ), { + type : 'POST', + data : { + tool_id : 'upload1', + upload_type : 'upload_dataset', + history_id : historyId, + inputs : inputs + } + }); +}; + +/** post a file to the upload1 tool over ajax */ +ToolsAPI.prototype.upload = function upload( historyId, options ){ + this.api.spaceghost.info( 'tools.upload: ' + [ historyId, this.api.spaceghost.jsonStr( options ) ] ); + this.api.ensureId( historyId ); + options = options || {}; + + // We can post an upload using jquery and formdata (see below), the more + // difficult part is attaching the file without user intervention. + // To do this we need to (unfortunately) create a form phantom can attach the file to first. + this.api.spaceghost.evaluate( function(){ + $( 'body' ).append( '<input type="file" name="casperjs-upload-file" />' ); + }); + this.api.spaceghost.page.uploadFile( 'input[name="casperjs-upload-file"]', options.filepath ); + + var inputs = { + 'file_type' : options.ext || 'auto', + 'files_0|type' : 'upload_dataset', + 'dbkey' : options.dbkey || '?' + }; + if( options.posix ){ + inputs[ 'files_0|to_posix_lines' ] = 'Yes'; + } + if( options.tabs ){ + inputs[ 'files_0|space_to_tab' ] = 'Yes'; + } + var response = this.api.spaceghost.evaluate( function( url, historyId, inputs ){ + var file = $( 'input[name="casperjs-upload-file"]' )[0].files[0], + formData = new FormData(), + response; + + formData.append( 'files_0|file_data', file ); + formData.append( 'history_id', historyId ); + formData.append( 'tool_id', 'upload1' ); + formData.append( 'inputs', JSON.stringify( inputs ) ); + $.ajax({ + url : url, + async : false, + type : 'POST', + data : formData, + // when sending FormData don't have jq process or cache the data + cache : false, + contentType : false, + processData : false, + // if we don't add this, payload isn't processed as JSON + headers : { 'Accept': 'application/json' } + }).done(function( resp ){ + response = resp; + }); + // this works only bc jq is async + return response; + }, utils.format( this.urlTpls.create ), historyId, inputs ); + + if( !response ){ + throw new APIError( 'Failed to upload: ' + options.filepath, 0 ); + } + + return response; +}; + +/** amount of time allowed to upload a file (before erroring) */ +ToolsAPI.prototype.DEFAULT_UPLOAD_TIMEOUT = 12000; + +/** add two casperjs steps - upload a file, wait for the job to complete, and run 'then' when they are */ +ToolsAPI.prototype.thenUpload = function thenUpload( historyId, options, then ){ + var spaceghost = this.api.spaceghost, + uploadedId; + + spaceghost.then( function(){ + var returned = this.api.tools.upload( historyId, options ); + this.debug( 'returned: ' + this.jsonStr( returned ) ); + uploadedId = returned.outputs[0].id; + this.debug( 'uploadedId: ' + uploadedId ); + }); + + spaceghost.then( function(){ + this.waitFor( + function testHdaState(){ + var hda = spaceghost.api.hdas.show( historyId, uploadedId ); + spaceghost.debug( spaceghost.jsonStr( hda.state ) ); + return !( hda.state === 'upload' || hda.state === 'queued' || hda.state === 'running' ); + }, + function _then(){ + spaceghost.info( 'upload finished: ' + uploadedId ); + if( then ){ + then.call( spaceghost, uploadedId ); + } + //var hda = spaceghost.api.hdas.show( historyId, uploadedId ); + //spaceghost.debug( spaceghost.jsonStr( hda ) ); + }, + function timeout(){ + throw new APIError( 'timeout uploading file', 408 ); + }, + options.timeout || spaceghost.api.tools.DEFAULT_UPLOAD_TIMEOUT + ); + }); + return spaceghost; +}; + +/** add two casperjs steps - upload multiple files (described in optionsArray) and wait for all jobs to complete */ +ToolsAPI.prototype.thenUploadMultiple = function thenUploadMultiple( historyId, optionsArray, then ){ + var spaceghost = this.api.spaceghost, + uploadedIds = []; + + this.api.spaceghost.then( function(){ + var spaceghost = this; + optionsArray.forEach( function( options ){ + var returned = spaceghost.api.tools.upload( historyId, options ); + spaceghost.debug( 'uploaded:' + spaceghost.jsonStr( returned ) ); + uploadedIds.push( returned.outputs[0].id ); + }); + }); + + // wait for every hda to finish running - IOW, don't use uploadedIds + this.api.spaceghost.then( function(){ + this.debug( this.jsonStr( uploadedIds ) ); + this.waitFor( + function testHdaStates(){ + var hdas = spaceghost.api.hdas.index( historyId ), + running = hdas.filter( function( hda ){ + return ( hda.state === 'upload' || hda.state === 'queued' || hda.state === 'running' ); + }).map( function( hda ){ + return hda.id; + }); + //spaceghost.debug( 'still uploading: ' + spaceghost.jsonStr( running ) ); + return running.length === 0; + }, + function _then(){ + var hdas = spaceghost.api.hdas.index( historyId ); + spaceghost.debug( spaceghost.jsonStr( hdas ) ); + if( then ){ + then.call( spaceghost, uploadedIds ); + } + }, + function timeout(){ + throw new APIError( 'timeout uploading files', 408 ); + }, + ( options.timeout || spaceghost.api.tools.DEFAULT_UPLOAD_TIMEOUT ) * optionsArray.length + ); + }); + return spaceghost; +}; + // =================================================================== WORKFLOWS var WorkflowsAPI = function WorkflowsAPI( api ){ @@ -495,7 +785,7 @@ }; -// =================================================================== HISTORIES +// =================================================================== VISUALIZATIONS var VisualizationsAPI = function VisualizationsAPI( api ){ this.api = api; }; diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/modules/user.js --- a/test/casperjs/modules/user.js +++ b/test/casperjs/modules/user.js @@ -280,6 +280,9 @@ User.prototype.getRandomEmail = function getRandomEmail( username, domain ){ username = username || 'test'; domain = domain || 'test.test'; - return username + Date.now() + '@' + domain; + var number = Math.ceil( Math.random() * 10000000000000 ); + // doesn't work so well when creating two users at once + //var number = Date.now(); + return username + number + '@' + domain; }; diff -r c4950bc662256f013916b0aaabb73a47508213c5 -r 79ab14af25e4182dac99ecad3918ed653c50f272 test/casperjs/spaceghost.js --- a/test/casperjs/spaceghost.js +++ b/test/casperjs/spaceghost.js @@ -886,6 +886,30 @@ this.test.assert( classes.indexOf( className ) === -1, msg ); }; +/** Return true if object has all keys in keysArray (useful in API testing of return values). + * @param {Object} object the object to test + * @param {String[]} keysArray an array of expected keys + */ +SpaceGhost.prototype.hasKeys = function hasKeys( object, keysArray ){ + if( !utils.isObject( object ) ){ return false; } + for( var i=0; i<keysArray.length; i += 1 ){ + if( !object.hasOwnProperty( keysArray[i] ) ){ + return false; + } + } + return true; +}; + +/** Returns count of keys in object. */ +SpaceGhost.prototype.countKeys = function countKeys( object ){ + if( !utils.isObject( object ) ){ return 0; } + var count = 0; + for( var key in object ){ + if( object.hasOwnProperty( key ) ){ count += 1; } + } + return count; +}; + // =================================================================== CONVENIENCE /** Wraps casper.getElementInfo in try, returning null if element not found instead of erroring. https://bitbucket.org/galaxy/galaxy-central/commits/561929fb2d7e/ Changeset: 561929fb2d7e User: carlfeberhard Date: 2014-03-25 22:00:03 Summary: merge Affected #: 3 files diff -r 79ab14af25e4182dac99ecad3918ed653c50f272 -r 561929fb2d7e827f20ea078904aeebe35173f3f9 static/scripts/viz/trackster/tracks.js --- a/static/scripts/viz/trackster/tracks.js +++ b/static/scripts/viz/trackster/tracks.js @@ -1106,6 +1106,10 @@ }); // For vertical alignment, track mouse with simple line. + // Fixes needed for this to work: + // (a) make work with embedded visualizations; + // (b) seems to get stuck on tile overlaps. + /* var mouse_tracker_div = $('<div/>').addClass('mouse-pos').appendTo(parent_element); // Show tracker only when hovering over view. @@ -1125,6 +1129,7 @@ mouse_tracker_div.hide(); } ); + */ this.add_label_track( new LabelTrack( this, { content_div: this.top_labeltrack } ) ); this.add_label_track( new LabelTrack( this, { content_div: this.nav_labeltrack } ) ); @@ -3053,14 +3058,21 @@ var tile_drawn = $.Deferred(); track.tile_cache.set_elt(key, tile_drawn); $.when.apply($, get_tile_data()).then( function() { - // If deferred objects ever show up in tile data, that is likely because a - // Deferred-subsetting interaction failed. Specifically, a Deferred for a superset - // was returned but then couldn't be used). It's not clear whether this will happen - // in practice, and currently the code doesn't handle it. It could probably handle it - // by recursively calling draw_helper. var tile_data = get_tile_data(), tracks_data = tile_data, seq_data; + + // Deferreds may show up here if trying to fetch a subset of data from a superset data chunk + // that cannot be subsetted. This may occur if the superset has a message. If there is a + // Deferred, try again from the top. NOTE: this condition could (should?) be handled by the + // GenomeDataManager in visualization module. + if (_.find(tile_data, function(d) { return util.is_deferred(d); })) { + track.tile_cache.set_elt(key, undefined); + $.when(track.draw_helper(region, w_scale, options)).then(function(tile) { + tile_drawn.resolve(tile); + }); + return; + } // If sequence data is available, subset to get only data in region. if (view.reference_track) { diff -r 79ab14af25e4182dac99ecad3918ed653c50f272 -r 561929fb2d7e827f20ea078904aeebe35173f3f9 static/scripts/viz/trackster/util.js --- a/static/scripts/viz/trackster/util.js +++ b/static/scripts/viz/trackster/util.js @@ -1,6 +1,13 @@ define(function(){ /** + * Helper to determine if object is jQuery deferred. + */ +var is_deferred = function ( d ) { + return ('promise' in d); +}; + +/** * Implementation of a server-state based deferred. Server is repeatedly polled, and when * condition is met, deferred is resolved. */ @@ -109,6 +116,7 @@ }; return { + is_deferred: is_deferred, ServerStateDeferred : ServerStateDeferred, get_random_color : get_random_color }; diff -r 79ab14af25e4182dac99ecad3918ed653c50f272 -r 561929fb2d7e827f20ea078904aeebe35173f3f9 static/scripts/viz/visualization.js --- a/static/scripts/viz/visualization.js +++ b/static/scripts/viz/visualization.js @@ -86,14 +86,6 @@ }); }; - -/** - * Helper to determine if object is jQuery deferred. - */ -var is_deferred = function ( d ) { - return ('promise' in d); -}; - // --------- Models --------- /** @@ -380,11 +372,10 @@ * Get data from dataset. */ get_data: function(region, mode, resolution, extra_params) { - // Look for entry and return if it's a deferred or if data available is compatible with mode. var entry = this.get_elt(region); if ( entry && - ( is_deferred(entry) || this.get('data_mode_compatible')(entry, mode) ) ) { + ( util_mod.is_deferred(entry) || this.get('data_mode_compatible')(entry, mode) ) ) { return entry; } @@ -405,12 +396,12 @@ // This entry has data in the requested range. Return if data // is compatible and can be subsetted. entry = obj_cache[entry_region.toString()]; - if ( is_deferred(entry) || + if ( util_mod.is_deferred(entry) || ( this.get('data_mode_compatible')(entry, mode) && this.get('can_subset')(entry) ) ) { this.move_key_to_end(entry_region, i); // If there's data, subset it. - if ( !is_deferred(entry) ) { + if ( !util_mod.is_deferred(entry) ) { var subset_entry = this.subset_entry(entry, region); this.set(region, subset_entry); entry = subset_entry; Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org