Merge pull request #4476 from mvdbeek/workflow_view_tags

Show tags, import by drag-and-drop and avoid refreshed in workflow page
This commit is contained in:
Martin Cech
2017-09-01 12:47:39 -04:00
committed by GitHub
26 changed files with 575 additions and 227 deletions
+67 -10
View File
@@ -11,29 +11,40 @@ var TagsEditor = Backbone.View
.extend( baseMVC.LoggableMixin )
.extend( baseMVC.HiddenUntilActivatedViewMixin ).extend({
tagName : 'div',
className : 'tags-display',
tagName : 'div',
className : 'tags-display',
select_width : '100%',
events: {},
/** Set up listeners, parse options */
initialize : function( options ){
//console.debug( this, options );
// only listen to the model only for changes to tags - re-render
this.show_editor = false;
if (options.usePrompt === false) {
this.label = '';
} else {
this.label = '<label class="prompt">' + _l( 'Tags' ) + '</label>';
}
this.workflow_mode = options.workflow_mode || false;
if (this.workflow_mode) {
this.events.click = 'showEditor';
this.events.keydown = 'keydownHandler';
}
this.hiddenUntilActivated( options.$activator, options );
},
/** Build the DOM elements, call select to on the created input, and set up behaviors */
render : function(){
var self = this;
this.$el.html( this._template() );
if (this.workflow_mode) {
this.$el.html(this._workflowTemplate());
} else {
this.$el.html(this._defaultTemplate());
}
this.$input().select2({
placeholder : 'Add tags',
width : '100%',
width : this.workflow_mode ? this.width : this.select_width,
tags : function(){
// initialize possible tags in the dropdown based on all the tags the user has used so far
return self._getTagsUsed();
@@ -59,14 +70,61 @@ var TagsEditor = Backbone.View
},
/** @returns {String} the html text used to build the view's DOM */
_template : function(){
_defaultTemplate : function(){
return [
this.label,
// set up initial tags by adding as CSV to input vals (necc. to init select2)
'<input class="tags-input" value="', this.tagsToCSV(), '" />'
this._renderEditor()
].join( '' );
},
_workflowTemplate : function(){
// Shows labels by default, event handler controls whether we show tags or editor
return [
this.show_editor ? this._renderEditor() : this._renderTags(),
].join( ' ' );
},
keydownHandler : function (e) {
switch (e.which) {
// esc
case 27 :
// hide the tag editor when pressing escape
this.hideEditor();
break;
}
},
showEditor: function() {
this.show_editor = true;
this.render();
},
hideEditor: function() {
this.show_editor = false;
this.render();
},
_renderEditor: function(){
// set up initial tags by adding as CSV to input vals (necc. to init select2)
return '<input class="tags-input" value="' + this.tagsToCSV() + '"/>'
},
_renderTags : function(){
var tags = this.model.get('tags');
var addButton = 'static/images/fugue/tag--plus.png';
var renderedArray = [];
_.each(tags, function(tag) {
tag = tag.indexOf("name:") == 0 ? tag.slice(5) : tag ;
var renderString = '<span class="label label-info">' + tag + '</span>';
renderedArray.push( renderString );
});
if (renderedArray.length === 0) {
// If there are no tags to render we just show the add-tag-button
renderedArray.push('<img src=' + addButton + ' class="add-tag-button" title="Add tags"/>');
}
return renderedArray.join(" ");
},
/** @returns {String} the sorted, comma-separated tags from the model */
tagsToCSV : function(){
var self = this;
@@ -132,8 +190,7 @@ var TagsEditor = Backbone.View
toString : function(){ return [ 'TagsEditor(', this.model + '', ')' ].join(''); }
});
// =============================================================================
return {
TagsEditor : TagsEditor
TagsEditor : TagsEditor,
};
});
@@ -0,0 +1,44 @@
define([
"mvc/base-mvc",
], function( baseMVC ){
/* global Backbone */
// workflow model
var logNamespace = 'workflow';
//==============================================================================
/** @class model for a single workflow.
* @name WorkflowItem
* @augments Backbone.Model
*/
var WorkflowItem = Backbone.Model.extend( baseMVC.LoggableMixin ).extend({
_logNamespace : logNamespace,
urlRoot: '/api/workflows',
toJSON: function(){
// need to overwrite this as endpoint expects the 'workflow' key in payload
return {workflow : this.attributes};
},
});
//==============================================================================
/** @class collection for workflows.
* @name WorkflowCollection
* @augments Backbone.Collection
*/
var WorkflowCollection = Backbone.Collection.extend({
model: WorkflowItem,
url: '/api/workflows',
});
//==============================================================================
return {
WorkflowItem: WorkflowItem,
WorkflowCollection: WorkflowCollection,
};
});
+228 -123
View File
@@ -1,94 +1,241 @@
/** Workflow view */
define( [ 'utils/utils', 'mvc/ui/ui-misc' ], function( Utils, Ui ) {
define( [ "libs/toastr", "mvc/tag", "mvc/workflow/workflow-model" ], function( mod_toastr, TAGS, WORKFLOWS ) {
/** View of the individual workflows */
var WorkflowItemView = Backbone.View.extend({
tagName: 'tr', // name of (orphan) root tag in this.el
initialize: function(){
_.bindAll(this, 'render', '_rowTemplate', 'renderTagEditor', '_templateActions', 'removeWorkflow', 'copyWorkflow'); // every function that uses 'this' as the current object should be in here
mod_toastr.options.timeOut = 1500;
},
events: {
'click #show-in-tool-panel': 'showInToolPanel',
'click #delete-workflow' : 'removeWorkflow',
'click #rename-workflow' : 'renameWorkflow',
'click #copy-workflow' : 'copyWorkflow',
},
render: function(){
$(this.el).html(this._rowTemplate());
return this;
},
showInToolPanel: function(){
this.model.set('show_in_tool_panel', !this.model.get('show_in_tool_panel'));
this.model.save();
// This reloads the whole page, so that the workflow appears in the tool panel.
// Ideally we would notify only the tool panel of a change
window.location = Galaxy.root + 'workflow';
},
removeWorkflow: function(){
var wfName = this.model.get('name');
if (confirm( "Are you sure you want to delete workflow '" + wfName + "'?" )) {
this.model.destroy({
success: function() {
mod_toastr.success("Successfully deleted workflow '" + wfName + "'");
}
});
this.remove();
};
},
renameWorkflow: function(){
var oldName = this.model.get('name');
var newName = prompt("Enter a new Name for workflow '" + oldName + "'", oldName );
if (newName) {
this.model.save(
{ 'name': newName },
{ success: function() {
mod_toastr.success("Successfully renamed workflow '" + oldName + "' to '" + newName + "'")
}
});
this.render();
}
},
copyWorkflow: function(){
self = this;
var oldName = this.model.get('name');
$.getJSON(this.model.urlRoot + '/' + this.model.id + '/download', function(wfJson) {
var newName = 'Copy of ' + oldName;
var currentOwner = self.model.get('owner');
if (currentOwner != Galaxy.user.attributes.username) {
newName += ' shared by user ' + currentOwner;
}
wfJson.name = newName;
self.collection.create(wfJson, { at: 0,
wait: true,
success: function() {
mod_toastr.success("Successfully copied workflow '" + oldName + "' to '" + newName + "'")
},
error : function(model, resp, options) {
// signature seems to have changed over the course of backbone dev
// see https://github.com/jashkenas/backbone/issues/2606#issuecomment-19289483
mod_toastr.error(options.errorThrown);
}
});
}).error(function(jqXHR, textStatus, errorThrown) {
mod_toastr.error(jqXHR.responseJSON.err_msg);
})
},
_rowTemplate: function() {
var show = this.model.get("show_in_tool_panel");
var wfId = this.model.id;
var checkboxHtml = '<input id="show-in-tool-panel" type="checkbox" class="show-in-tool-panel" '+ ( show ? 'checked="' + show + '"' : "" ) +' value="' + wfId + '">';
var trHtml = '<td>' +
'<div class="dropdown">' +
'<button class="menubutton" type="button" data-toggle="dropdown">' +
_.escape( this.model.get("name") ) + '<span class="caret"></span>' +
'</button>' +
this._templateActions( ) +
'</div>' +
'</td>' +
'<td><span>' + '<div class="' + wfId + ' tags-display"></div>' + '</td>' +
'<td>' + ( this.model.get('owner') === Galaxy.user.attributes.username ? "You" : this.model.get('owner') ) +'</span></td>' +
'<td>' + this.model.get("number_of_steps") + '</td>' +
'<td>' + ( this.model.get("published") ? "Yes" : "No" ) + '</td>' +
'<td>'+ checkboxHtml + '</td>';
return trHtml;
},
renderTagEditor: function(){
var TagEditor = new TAGS.TagsEditor({
model : this.model,
el : $.find( '.' + this.model.id + '.tags-display' ),
workflow_mode : true });
TagEditor.toggle( true );
TagEditor.render();
},
/** Template for user actions for workflows */
_templateActions: function( ) {
if( this.model.get("owner") === Galaxy.user.attributes.username ) {
return '<ul class="dropdown-menu action-dpd">' +
'<li><a href="'+ Galaxy.root +'workflow/editor?id='+ this.model.id +'">Edit</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/run?id='+ this.model.id +'">Run</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/sharing?id='+ this.model.id +'">Share</a></li>' +
'<li><a href="'+ Galaxy.root +'api/workflows/'+ this.model.id +'/download?format=json-download">Download</a></li>' +
'<li><a id="copy-workflow" style="cursor: pointer;">Copy</a></li>' +
'<li><a id="rename-workflow" style="cursor: pointer;">Rename</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/display_by_id?id='+ this.model.id +'">View</a></li>' +
'<li><a id="delete-workflow" style="cursor: pointer;">Delete</a></li>' +
'</ul>';
}
else {
return '<ul class="dropdown-menu action-dpd">' +
'<li><a href="'+ Galaxy.root +'workflow/display_by_username_and_slug?username='+ workflow.owner +'&slug='+ workflow.slug +'">View</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/run?id='+ this.model.id +'">Run</a></li>' +
'<li><a id="copy-workflow" style="cursor: pointer;">Copy</a></li>' +
'<li><a class="link-confirm-shared-'+ this.model.id +'" href="'+ Galaxy.root +'workflow/sharing?unshare_me=True&id='+ this.model.id +'">Remove</a></li>' +
'</ul>';
}
},
});
/** Build messages after user action */
function build_messages() {
var $el_message = this.$( '.response-message' ),
response = {};
response = {
'status': Utils.getQueryString( 'status' ),
'message': _.escape( Utils.getQueryString( 'message' ) ),
'persistent': true,
'cls': Utils.getQueryString( 'status' ) + 'message'
};
$el_message.empty().html( new Ui.Message( response ).$el );
}
/** View of the main workflow list page */
var View = Backbone.View.extend({
var WorkflowListView = Backbone.View.extend({
initialize: function() {
this.setElement( '<div/>' );
this.render();
_.bindAll(this, 'adjustActiondropdown')
this.collection = new WORKFLOWS.WorkflowCollection();
this.collection.fetch().done(this.render());
this.collection.bind('add', this.appendItem);
this.collection.on('sync', this.render, this);
},
events: {
'dragleave' : 'unhighlightDropZone',
'drop' : 'drop',
'dragover': function(ev) {
$( '.hidden_description_layer' ).addClass( 'dragover' );
$('.menubutton').addClass('background-none');
ev.preventDefault();
}
},
unhighlightDropZone: function() {
$( '.hidden_description_layer' ).removeClass( 'dragover' );
$('.menubutton').removeClass('background-none');
},
drop: function(e) {
// TODO: check that file is valid galaxy workflow
this.unhighlightDropZone();
e.preventDefault();
var files = e.dataTransfer.files;
var self = this;
for (var i = 0, f; f = files[i]; i++) {
self.readWorkflowFiles(f);
}
},
readWorkflowFiles: function(f) {
var self = this;
var reader = new FileReader();
reader.onload = function(theFile) {
try {
var wf_json = JSON.parse(reader.result);
} catch(e) {
mod_toastr.error("Could not read file '" + f.name + "'. Verify it is a valid Galaxy workflow");
wf_json = null;
}
if (wf_json) {
self.collection.create(wf_json, {
at: 0,
wait: true,
success: function() {
mod_toastr.success("Successfully imported workflow '" + wf_json.name + "'")
},
error : function(model, resp, options) {
mod_toastr.error(options.errorThrown);
}
});
}
};
reader.readAsText(f, 'utf-8');
},
render: function() {
var self = this,
min_query_length = 3;
$.getJSON( Galaxy.root + 'api/workflows/', function( workflows ) {
var $el_workflow = null;
// Add workflow header
self.$el.empty().append( self._templateHeader() );
// Add user actions message if any
build_messages();
$el_workflow = self.$( '.user-workflows' );
// Add the actions buttons
$el_workflow.append( self._templateActionButtons() );
if( workflows.length > 0) {
$el_workflow.append( self._templateWorkflowTable( self, workflows) );
self.adjust_actiondropdown( $el_workflow );
// Register delete and run workflow events
_.each( workflows, function( wf ) {
self.confirm_delete( wf );
});
self.register_show_tool_menu();
// Register search workflow event
self.search_workflow( self.$( '.search-wf' ), self.$( '.workflow-search tr' ), min_query_length );
}
else {
$el_workflow.append( self._templateNoWorkflow() );
}
});
// Add workflow header
var header = this._templateHeader();
// Add the actions buttons
var templateActions = this._templateActionButtons();
var tableTemplate = this._templateWorkflowTable();
this.$el.html( header + templateActions + tableTemplate);
var self = this;
_(this.collection.models).each(function(item){ // in case collection is not empty
self.appendItem(item);
self.confirmDelete(item);
}, this);
var minQueryLength = 3;
this.searchWorkflow( this.$( '.search-wf' ), this.$( '.workflow-search tr' ), minQueryLength );
this.adjustActiondropdown();
return this;
},
// Save the workflow as an item in Tool panel
register_show_tool_menu: function() {
var $el_checkboxes = this.$( '.show-in-tool-panel' );
$el_checkboxes.on( 'click', function( e ) {
var ids = [];
// Look for all the checked checkboxes
for( var item = 0; item < $el_checkboxes.length; item++ ) {
var checkbox = $el_checkboxes[ item ];
if( checkbox.checked ) {
ids.push( checkbox.value );
}
}
// Save all the checked workflows
$.ajax({
type: 'PUT',
url: Galaxy.root + 'api/workflows/menu/',
data: JSON.stringify( { 'workflow_ids': ids } ),
contentType : 'application/json'
}).done( function( response ) {
window.location = Galaxy.root + 'workflow';
});
appendItem: function(item){
var workflowItemView = new WorkflowItemView({
model: item,
collection: this.collection,
});
$( '.workflow-search' ).append(workflowItemView.render().el);
workflowItemView.renderTagEditor();
},
/** Add confirm box before removing/unsharing workflow */
confirm_delete: function( workflow ) {
var $el_wf_link = this.$( '.link-confirm-' + workflow.id ),
$el_shared_wf_link = this.$( '.link-confirm-shared-' + workflow.id );
$el_wf_link.click( function() {
return confirm( "Are you sure you want to delete workflow '" + workflow.name + "'?" );
});
confirmDelete: function( workflow ) {
var $el_shared_wf_link = this.$( '.link-confirm-shared-' + workflow.id );
$el_shared_wf_link.click( function() {
return confirm( "Are you sure you want to remove the shared workflow '" + workflow.name + "'?" );
return confirm( "Are you sure you want to remove the shared workflow '" + workflow.attributes.name + "'?" );
});
},
/** Implement client side workflow search/filtering */
search_workflow: function( $el_searchinput, $el_tabletr, min_querylen ) {
searchWorkflow: function( $el_searchinput, $el_tabletr, min_querylen ) {
$el_searchinput.on( 'keyup', function () {
var query = $( this ).val();
// Filter when query is at least 3 characters
@@ -110,13 +257,12 @@ define( [ 'utils/utils', 'mvc/ui/ui-misc' ], function( Utils, Ui ) {
},
/** Ajust the position of dropdown with respect to table */
adjust_actiondropdown: function( $el ) {
$el.on( 'show.bs.dropdown', function () {
$el.css( "overflow", "inherit" );
adjustActiondropdown: function( ) {
$(this.el).on( 'show.bs.dropdown', function () {
$(this.el).css( "overflow", "inherit" );
});
$el.on( 'hide.bs.dropdown', function () {
$el.css( "overflow", "auto" );
$(this.el).on( 'hide.bs.dropdown', function () {
$(this.el).css( "overflow", "auto" );
});
},
@@ -143,57 +289,17 @@ define( [ 'utils/utils', 'mvc/ui/ui-misc' ], function( Utils, Ui ) {
},
/** Template for workflow table */
_templateWorkflowTable: function( self, workflows ) {
var tableHtml = "", trHtml = "";
tableHtml = tableHtml + '<table class="table colored"><thead>' +
_templateWorkflowTable: function( ) {
var tableHtml = '<table class="table colored"><thead>' +
'<tr class="header">' +
'<th>Name</th>' +
'<th>Tags</th>' +
'<th>Owner</th>' +
'<th># of Steps</th>' +
'<th>Published</th>' +
'<th>Show in tools panel</th>' +
'</tr></thead>';
_.each( workflows, function( wf ) {
var checkbox_html = '<input type="checkbox" class="show-in-tool-panel" '+ ( wf.show_in_tool_panel ? 'checked="' + wf.show_in_tool_panel + '"' : "" ) +' value="' + wf.id + '">';
trHtml = trHtml + '<tr>' +
'<td>' +
'<div class="dropdown">' +
'<button class="menubutton" type="button" data-toggle="dropdown">' +
_.escape( wf.name ) + '<span class="caret"></span>' +
'</button>' +
self._templateActions( wf ) +
'</div>' +
'</td>' +
'<td>' + ( wf.owner === Galaxy.user.attributes.username ? "You" : wf.owner ) +'</td>' +
'<td>' + wf.number_of_steps + '</td>' +
'<td>' + ( wf.published ? "Yes" : "No" ) + '</td>' +
'<td>'+ checkbox_html +'</td>' +
'</tr>';
});
return tableHtml + '<tbody class="workflow-search">' + trHtml + '</tbody></table>';
},
/** Template for user actions for workflows */
_templateActions: function( workflow ) {
if( workflow.owner === Galaxy.user.attributes.username ) {
return '<ul class="dropdown-menu action-dpd">' +
'<li><a href="'+ Galaxy.root +'workflow/editor?id='+ workflow.id +'">Edit</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/run?id='+ workflow.id +'">Run</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/sharing?id='+ workflow.id +'">Share or Download</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/copy?id='+ workflow.id +'">Copy</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/rename?id='+ workflow.id +'">Rename</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/display_by_id?id='+ workflow.id +'">View</a></li>' +
'<li><a class="link-confirm-'+ workflow.id +'" href="'+ Galaxy.root +'workflow/delete?id='+ workflow.id +'">Delete</a></li>' +
'</ul>';
}
else {
return '<ul class="dropdown-menu action-dpd">' +
'<li><a href="'+ Galaxy.root +'workflow/display_by_username_and_slug?username='+ workflow.owner +'&slug='+ workflow.slug +'">View</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/run?id='+ workflow.id +'">Run</a></li>' +
'<li><a href="'+ Galaxy.root +'workflow/copy?id='+ workflow.id +'">Copy</a></li>' +
'<li><a class="link-confirm-shared-'+ workflow.id +'" href="'+ Galaxy.root +'workflow/sharing?unshare_me=True&id='+ workflow.id +'">Remove</a></li>' +
'</ul>';
}
return tableHtml + '<tbody class="workflow-search "><div class="hidden_description_layer"><p>Drop workflow files here to import</p>' + '</tbody></table></div>';
},
/** Main template */
@@ -264,11 +370,10 @@ define( [ 'utils/utils', 'mvc/ui/ui-misc' ], function( Utils, Ui ) {
"</div>" +
"</div>";
},
});
return {
View : View,
View : WorkflowListView,
ImportWorkflowView : ImportWorkflowView
};
});
+27
View File
@@ -1271,6 +1271,10 @@ a.action-button {
}
}
.menubutton.background-none {
background: none;
}
// A split menu button, the main button has an action, the arrow causes the
// popup menu to appear
@@ -1824,3 +1828,26 @@ div.toolTitleNoSection
.other-options {
margin-bottom: 2%;
}
.hidden_description_layer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(200, 200, 200, 0.6);
visibility: hidden;
opacity: 0;
font-size: 2.0em;
display: flex;
align-items: center;
justify-content: center;
/* transition effect. not necessary */
transition: opacity .2s, visibility .2s;
}
.hidden_description_layer.dragover {
visibility: visible;
opacity: 1;
}
+5
View File
@@ -231,6 +231,8 @@ class WorkflowContentsManager(UsesAnnotations):
if data['annotation']:
annotation = sanitize_html(data['annotation'], 'utf-8', 'text/html')
self.add_item_annotation(trans.sa_session, stored.user, stored, annotation)
workflow_tags = data.get('tags', [])
trans.app.tag_handler.set_tags_from_list(user=trans.user, item=stored, new_tags_list=workflow_tags)
# Persist
trans.sa_session.add(stored)
@@ -546,14 +548,17 @@ class WorkflowContentsManager(UsesAnnotations):
workflow = stored.latest_workflow
annotation_str = ""
tag_str = ""
if stored is not None:
annotation_str = self.get_item_annotation_str(trans.sa_session, trans.user, stored) or ''
tag_str = stored.make_tag_string_list()
# Pack workflow data into a dictionary and return
data = {}
data['a_galaxy_workflow'] = 'true' # Placeholder for identifying galaxy workflow
data['format-version'] = "0.1"
data['name'] = workflow.name
data['annotation'] = annotation_str
data['tags'] = tag_str
if workflow.uuid is not None:
data['uuid'] = str(workflow.uuid)
data['steps'] = {}
+29 -17
View File
@@ -358,11 +358,13 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn
stored_workflow = self.__get_stored_accessible_workflow(trans, workflow_id)
style = kwd.get("style", "export")
download_format = kwd.get('format')
ret_dict = self.workflow_contents_manager.workflow_to_dict(trans, stored_workflow, style=style)
if not ret_dict:
# This workflow has a tool that's missing from the distribution
message = "Workflow cannot be exported due to missing tools."
raise exceptions.MessageException(message)
if download_format == 'json-download':
sname = stored_workflow.name
sname = ''.join(c in util.FILENAME_VALID_CHARS and c or '_' for c in sname)[0:150]
trans.response.headers["Content-Disposition"] = 'attachment; filename="Galaxy-Workflow-%s.ga"' % (sname)
trans.response.set_content_type('application/galaxy-archive')
return ret_dict
@expose_api
@@ -436,15 +438,18 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn
:returns: serialized version of the workflow
"""
stored_workflow = self.__get_stored_workflow(trans, id)
if 'workflow' in payload:
stored_workflow.name = sanitize_html(payload['name']) if ('name' in payload) else stored_workflow.name
workflow_dict = payload.get('workflow')
if workflow_dict:
new_workflow_name = payload.get('name') or workflow_dict.get('name')
if new_workflow_name:
stored_workflow.name = sanitize_html(new_workflow_name)
if 'annotation' in payload:
newAnnotation = sanitize_html(payload['annotation'])
self.add_item_annotation(trans.sa_session, trans.get_user(), stored_workflow, newAnnotation)
if 'menu_entry' in payload:
if payload['menu_entry']:
if 'menu_entry' in payload or 'show_in_tool_panel' in workflow_dict:
if payload.get('menu_entry') or workflow_dict.get('show_in_tool_panel'):
menuEntry = model.StoredWorkflowMenuEntry()
menuEntry.stored_workflow = stored_workflow
trans.get_user().stored_workflow_menu_entries.append(menuEntry)
@@ -453,15 +458,21 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn
entries = {x.stored_workflow_id: x for x in trans.get_user().stored_workflow_menu_entries}
if (trans.security.decode_id(id) in entries):
trans.get_user().stored_workflow_menu_entries.remove(entries[trans.security.decode_id(id)])
# set tags
trans.app.tag_handler.set_tags_from_list(user=trans.user, item=stored_workflow, new_tags_list=workflow_dict.get('tags', []))
try:
workflow, errors = self.workflow_contents_manager.update_workflow_from_dict(
trans,
stored_workflow,
payload['workflow'],
)
except workflows.MissingToolsException:
raise exceptions.MessageException("This workflow contains missing tools. It cannot be saved until they have been removed from the workflow or installed.")
if 'steps' in workflow_dict:
try:
workflow, errors = self.workflow_contents_manager.update_workflow_from_dict(
trans,
stored_workflow,
workflow_dict,
)
except workflows.MissingToolsException:
raise exceptions.MessageException("This workflow contains missing tools. It cannot be saved until they have been removed from the workflow or installed.")
else:
# We only adjusted tags and menu entry
return payload
else:
message = "Updating workflow requires dictionary containing 'workflow' attribute with new JSON description."
raise exceptions.RequestParameterInvalidException(message)
@@ -537,7 +548,8 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn
item = workflow.to_dict(value_mapper={'id': trans.security.encode_id})
item['url'] = url_for('workflow', id=encoded_id)
item['owner'] = workflow.user.username
item['number_of_steps'] = len(workflow.latest_workflow.steps)
rval.append(item)
#
@@ -20,7 +20,7 @@ from galaxy.managers import workflows
from galaxy.model.item_attrs import UsesItemRatings
from galaxy.model.mapping import desc
from galaxy.tools.parameters.basic import workflow_building_modes
from galaxy.util import unicodify, FILENAME_VALID_CHARS
from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
from galaxy.web import error, url_for
from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesStoredWorkflowMixin
@@ -713,32 +713,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
stored = self.get_stored_workflow(trans, id, check_ownership=False, check_accessible=True)
return self._workflow_to_dict(trans, stored)
@web.json_pretty
def export_to_file(self, trans, id):
"""
Get the latest Workflow for the StoredWorkflow identified by `id` and
encode it as a json string that can be imported back into Galaxy
This has slightly different information than the above. In particular,
it does not attempt to decode forms and build UIs, it just stores
the raw state.
"""
# Get workflow.
stored = self.get_stored_workflow(trans, id, check_ownership=False, check_accessible=True)
# Stream workflow to file.
stored_dict = self._workflow_to_dict(trans, stored)
if not stored_dict:
# This workflow has a tool that's missing from the distribution
trans.response.status = 400
return "Workflow cannot be exported due to missing tools."
sname = stored.name
sname = ''.join(c in FILENAME_VALID_CHARS and c or '_' for c in sname)[0:150]
trans.response.headers["Content-Disposition"] = 'attachment; filename="Galaxy-Workflow-%s.ga"' % (sname)
trans.response.set_content_type('application/galaxy-archive')
return stored_dict
@web.expose
@web.json
def upload_import_workflow(self, trans, cntrller='workflow', **kwd):
+3
View File
@@ -617,6 +617,9 @@ class ToolModule(WorkflowModule):
if message:
log.debug(message)
module.version_changes.append(message)
else:
# The tool is not installed
raise ToolMissingException("Tool %s missing. Cannot build workflow module." % tool_id)
return module
# ---- Saving in various forms ------------------------------------------
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"tag.js","sources":["../../src/mvc/tag.js"],"names":["define","baseMVC","_l","TagsEditor","Backbone","View","extend","LoggableMixin","HiddenUntilActivatedViewMixin","tagName","className","initialize","options","this","label","usePrompt","hiddenUntilActivated","$activator","render","self","$el","html","_template","$input","select2","placeholder","width","tags","_getTagsUsed","_setUpBehaviors","_hashToName","tag","startsWith","slice","_nameToHash","tagsToCSV","join","tagsArray","model","get","_","isArray","isEmpty","map","escape","sort","find","Galaxy","user","on","event","val","save","added","_addNewTagToTagsUsed","text","newTag","tagsUsed","contains","push","set","remove","off","stopListening","prototype","call","toString"],"mappings":"AAAAA,QACI,eACA,sBACD,SAAUC,EAASC,GAMtB,GAAIC,GAAaC,SAASC,KACjBC,OAAQL,EAAQM,eAChBD,OAAQL,EAAQO,+BAAgCF,QAErDG,QAAc,MACdC,UAAc,eAGdC,WAAa,SAAUC,GAIfC,KAAKC,MADLF,EAAQG,aAAc,EACT,GAEA,yBAA2Bb,EAAI,QAAW,WAE3DW,KAAKG,qBAAsBJ,EAAQK,WAAYL,IAInDM,OAAS,WACL,GAAIC,GAAON,IAaX,OAZAA,MAAKO,IAAIC,KAAMR,KAAKS,aAEpBT,KAAKU,SAASC,SACVC,YAAc,WACdC,MAAc,OACdC,KAAO,WAEH,MAAOR,GAAKS,kBAIpBf,KAAKgB,kBACEhB,MAGXiB,YAAa,SAASC,GAClB,MAAIA,GAAIC,WAAW,KACR,QAAUD,EAAIE,MAAM,GAExBF,GAGXG,YAAa,SAASH,GAIlB,MAHIA,GAAIC,WAAW,WACfD,EAAM,IAAMA,EAAIE,MAAM,IAEnBF,GAIXT,UAAY,WACR,OACIT,KAAKC,MAEL,oCAAqCD,KAAKsB,YAAa,QACzDC,KAAM,KAIZD,UAAY,WACR,GAAIhB,GAAON,KACPwB,EAAYxB,KAAKyB,MAAMC,IAAK,OAChC,QAAKC,EAAEC,QAASJ,IAAeG,EAAEE,QAASL,GAC/B,GAEJA,EAAUM,IAAK,SAAUZ,GAC5B,MAAOS,GAAEI,OAAQzB,EAAKe,YAAaH,MACpCc,OAAOT,KAAM,MAIpBb,OAAS,WACL,MAAOV,MAAKO,IAAI0B,KAAM,qBAI1BlB,aAAe,WAEX,GAAIT,GAAON,IACX,OAAO2B,GAAEG,IAAII,OAAOC,KAAKT,IAAK,aAAepB,EAAKe,cAItDL,gBAAkB,WACd,GAAIV,GAAON,IACXA,MAAKU,SAAS0B,GAAI,SAAU,SAAUC,GAElCA,EAAMC,IAAMX,EAAEG,IAAIO,EAAMC,IAAKhC,EAAKW,aAElCX,EAAKmB,MAAMc,MAAOzB,KAAMuB,EAAMC,MAE1BD,EAAMG,OAGNlC,EAAKmC,qBAAsBJ,EAAMG,MAAME,KAAO,OAQ1DD,qBAAuB,SAAUE,GAE7B,GAAIC,GAAWV,OAAOC,KAAKT,IAAK,YAC3BC,GAAEkB,SAAUD,EAAUD,KACvBC,EAASE,KAAMH,GACfC,EAASZ,OACTE,OAAOC,KAAKY,IAAK,YAAaH,KAKtCI,OAAS,WACLhD,KAAKU,OAAOuC,MACZjD,KAAKkD,cAAelD,KAAKyB,OACzBlC,SAASC,KAAK2D,UAAUH,OAAOI,KAAMpD,OAIzCqD,SAAW,WAAY,OAAS,cAAerD,KAAKyB,MAAQ,GAAI,KAAMF,KAAK,MAI/E,QACIjC,WAAaA"}
{"version":3,"file":"tag.js","sources":["../../src/mvc/tag.js"],"names":["define","baseMVC","_l","TagsEditor","Backbone","View","extend","LoggableMixin","HiddenUntilActivatedViewMixin","tagName","className","select_width","events","initialize","options","this","show_editor","label","usePrompt","workflow_mode","click","keydown","hiddenUntilActivated","$activator","render","self","$el","html","_workflowTemplate","_defaultTemplate","$input","select2","placeholder","width","tags","_getTagsUsed","_setUpBehaviors","_hashToName","tag","startsWith","slice","_nameToHash","_renderEditor","join","_renderTags","keydownHandler","e","which","hideEditor","showEditor","tagsToCSV","model","get","addButton","renderedArray","_","each","indexOf","renderString","push","length","tagsArray","isArray","isEmpty","map","escape","sort","find","Galaxy","user","on","event","val","save","added","_addNewTagToTagsUsed","text","newTag","tagsUsed","contains","set","remove","off","stopListening","prototype","call","toString"],"mappings":"AAAAA,QACI,eACA,sBACD,SAAUC,EAASC,GAMtB,GAAIC,GAAaC,SAASC,KACjBC,OAAQL,EAAQM,eAChBD,OAAQL,EAAQO,+BAAgCF,QAErDG,QAAe,MACfC,UAAe,eACfC,aAAe,OACfC,UAGAC,WAAa,SAAUC,GAGnBC,KAAKC,aAAc,EAEfD,KAAKE,MADLH,EAAQI,aAAc,EACT,GAEA,yBAA2BhB,EAAI,QAAW,WAE3Da,KAAKI,cAAgBL,EAAQK,gBAAiB,EAC1CJ,KAAKI,gBACLJ,KAAKH,OAAOQ,MAAQ,aACpBL,KAAKH,OAAOS,QAAU,kBAE1BN,KAAKO,qBAAsBR,EAAQS,WAAYT,IAInDU,OAAS,WACL,GAAIC,GAAOV,IAgBX,OAdGA,MAAKW,IAAIC,KADRZ,KAAKI,cACQJ,KAAKa,oBAEJb,KAAKc,oBAEvBd,KAAKe,SAASC,SACVC,YAAc,WACdC,MAAclB,KAAKI,cAAgBJ,KAAKkB,MAAQlB,KAAKJ,aACrDuB,KAAO,WAEH,MAAOT,GAAKU,kBAIpBpB,KAAKqB,kBACErB,MAGXsB,YAAa,SAASC,GAClB,MAAIA,GAAIC,WAAW,KACR,QAAUD,EAAIE,MAAM,GAExBF,GAGXG,YAAa,SAASH,GAIlB,MAHIA,GAAIC,WAAW,WACfD,EAAM,IAAMA,EAAIE,MAAM,IAEnBF,GAIXT,iBAAmB,WACf,OACId,KAAKE,MACLF,KAAK2B,iBACPC,KAAM,KAGZf,kBAAoB,WAEhB,OACIb,KAAKC,YAAcD,KAAK2B,gBAAkB3B,KAAK6B,eACjDD,KAAM,MAGZE,eAAiB,SAAUC,GACvB,OAAQA,EAAEC,OAEN,IAAK,IAEDhC,KAAKiC,eAKjBC,WAAY,WACRlC,KAAKC,aAAc,EACnBD,KAAKS,UAGTwB,WAAY,WACRjC,KAAKC,aAAc,EACnBD,KAAKS,UAGTkB,cAAe,WAEX,MAAO,oCAAsC3B,KAAKmC,YAAc,OAGpEN,YAAc,WACV,GAAIV,GAAOnB,KAAKoC,MAAMC,IAAI,QACtBC,EAAY,oCACZC,IAUJ,OATAC,GAAEC,KAAKtB,EAAM,SAASI,GAClBA,EAA8B,GAAxBA,EAAImB,QAAQ,SAAgBnB,EAAIE,MAAM,GAAKF,CACjD,IAAIoB,GAAe,kCAAoCpB,EAAM,SAC7DgB,GAAcK,KAAMD,KAEK,IAAzBJ,EAAcM,QAEdN,EAAcK,KAAK,YAAcN,EAAY,8CAE1CC,EAAcX,KAAK,MAI9BO,UAAY,WACR,GAAIzB,GAAOV,KACP8C,EAAY9C,KAAKoC,MAAMC,IAAK,OAChC,QAAKG,EAAEO,QAASD,IAAeN,EAAEQ,QAASF,GAC/B,GAEJA,EAAUG,IAAK,SAAU1B,GAC5B,MAAOiB,GAAEU,OAAQxC,EAAKgB,YAAaH,MACpC4B,OAAOvB,KAAM,MAIpBb,OAAS,WACL,MAAOf,MAAKW,IAAIyC,KAAM,qBAI1BhC,aAAe,WAEX,GAAIV,GAAOV,IACX,OAAOwC,GAAES,IAAII,OAAOC,KAAKjB,IAAK,aAAe3B,EAAKgB,cAItDL,gBAAkB,WACd,GAAIX,GAAOV,IACXA,MAAKe,SAASwC,GAAI,SAAU,SAAUC,GAElCA,EAAMC,IAAMjB,EAAES,IAAIO,EAAMC,IAAK/C,EAAKY,aAElCZ,EAAK0B,MAAMsB,MAAOvC,KAAMqC,EAAMC,MAE1BD,EAAMG,OAGNjD,EAAKkD,qBAAsBJ,EAAMG,MAAME,KAAO,OAQ1DD,qBAAuB,SAAUE,GAE7B,GAAIC,GAAWV,OAAOC,KAAKjB,IAAK,YAC3BG,GAAEwB,SAAUD,EAAUD,KACvBC,EAASnB,KAAMkB,GACfC,EAASZ,OACTE,OAAOC,KAAKW,IAAK,YAAaF,KAKtCG,OAAS,WACLlE,KAAKe,OAAOoD,MACZnE,KAAKoE,cAAepE,KAAKoC,OACzB/C,SAASC,KAAK+E,UAAUH,OAAOI,KAAMtE,OAIzCuE,SAAW,WAAY,OAAS,cAAevE,KAAKoC,MAAQ,GAAI,KAAMR,KAAK,MAG/E,QACIxC,WAAaA"}
@@ -0,0 +1 @@
{"version":3,"file":"workflow-model.js","sources":["../../../src/mvc/workflow/workflow-model.js"],"names":["define","baseMVC","logNamespace","WorkflowItem","Backbone","Model","extend","LoggableMixin","_logNamespace","urlRoot","toJSON","workflow","this","attributes","WorkflowCollection","Collection","model","url"],"mappings":"AAAAA,QACI,gBACD,SAAUC,GAIb,GAAIC,GAAe,WAMfC,EAAeC,SAASC,MAAMC,OAAQL,EAAQM,eAAgBD,QAC9DE,cAAgBN,EAEhBO,QAAS,iBAETC,OAAQ,WAER,OAAQC,SAAWC,KAAKC,eAUxBC,EAAqBV,SAASW,WAAWT,QACzCU,MAAOb,EACPc,IAAK,kBAMT,QACId,aAAcA,EACdW,mBAAoBA"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
webpackJsonp([5],[function(e,i,n){(function(e,i){var a=n(1),t=a,r=n(51).GalaxyApp,o=n(6),l=n(52);window.app=function(n,c){window.Galaxy=new r(n,c),Galaxy.debug("login app");var p=encodeURI(n.redirect);if(!n.show_welcome_with_login){var d=a.param({use_panels:"True",redirect:p});return void(window.location.href=Galaxy.root+"user/login?"+d)}var w=e.View.extend({initialize:function(i){this.page=i,this.model=new e.Model({title:o("Login required")}),this.setElement(this._template())},render:function(){this.page.$("#galaxy_main").prop("src",n.welcome_url)},_template:function(){var e=n.root+"user/login?"+t.param({redirect:p});return'<iframe src="'+e+'" frameborder="0" style="width: 100%; height: 100%;"/>'}});t(function(){Galaxy.page=new l.View(i.extend(n,{Right:w}))})}}).call(i,n(2),n(3))}]);
webpackJsonp([5],[function(e,i,n){(function(e,i){var a=n(1),t=a,r=n(52).GalaxyApp,o=n(6),l=n(53);window.app=function(n,c){window.Galaxy=new r(n,c),Galaxy.debug("login app");var p=encodeURI(n.redirect);if(!n.show_welcome_with_login){var d=a.param({use_panels:"True",redirect:p});return void(window.location.href=Galaxy.root+"user/login?"+d)}var w=e.View.extend({initialize:function(i){this.page=i,this.model=new e.Model({title:o("Login required")}),this.setElement(this._template())},render:function(){this.page.$("#galaxy_main").prop("src",n.welcome_url)},_template:function(){var e=n.root+"user/login?"+t.param({redirect:p});return'<iframe src="'+e+'" frameborder="0" style="width: 100%; height: 100%;"/>'}});t(function(){Galaxy.page=new l.View(i.extend(n,{Right:w}))})}}).call(i,n(2),n(3))}]);
//# sourceMappingURL=login.bundled.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"sources":["webpack:///login.bundled.js","webpack:///./galaxy/scripts/apps/login.js"],"names":["webpackJsonp","module","exports","__webpack_require__","Backbone","_","jQuery","$","GalaxyApp","_l","Page","window","app","options","bootstrapped","Galaxy","debug","redirect","encodeURI","show_welcome_with_login","params","param","use_panels","location","href","root","LoginPage","View","extend","initialize","page","this","model","Model","title","setElement","_template","render","prop","welcome_url","login_url","Right","call"],"mappings":"AAAAA,cAAc,IAER,SAAUC,EAAQC,EAASC,ICFjC,SAAAC,EAAAC,GAAA,GAAIC,GAASH,EAAS,GAClBI,EAAID,EACJE,EAAYL,EAAS,IAAWK,UAChCC,EAAKN,EAAS,GACdO,EAAOP,EAAS,GAEpBQ,QAAOC,IAAM,SAAcC,EAASC,GAChCH,OAAOI,OAAS,GAAIP,GAAWK,EAASC,GACxCC,OAAOC,MAAO,YACd,IAAIC,GAAWC,UAAWL,EAAQI,SAIlC,KAAKJ,EAAQM,wBAAyB,CAClC,GAAIC,GAASd,EAAOe,OAAQC,WAAa,OAAQL,SAAWA,GAE5D,aADAN,OAAOY,SAASC,KAAOT,OAAOU,KAAO,cAAgBL,GAIzD,GAAIM,GAAYtB,EAASuB,KAAKC,QAC1BC,WAAY,SAAUC,GAClBC,KAAKD,KAAOA,EACZC,KAAKC,MAAQ,GAAI5B,GAAS6B,OAAQC,MAAQzB,EAAI,oBAC9CsB,KAAKI,WAAYJ,KAAKK,cAE1BC,OAAQ,WACJN,KAAKD,KAAKvB,EAAG,gBAAiB+B,KAAM,MAAOzB,EAAQ0B,cAEvDH,UAAY,WACR,GAAII,GAAY3B,EAAQY,KAAO,cAAgBlB,EAAEc,OAASJ,SAAWA,GACrE,OAAO,gBAAkBuB,EAAY,2DAI7CjC,GAAE,WACEQ,OAAOe,KAAO,GAAIpB,GAAKiB,KAAMtB,EAAEuB,OAAQf,GACnC4B,MAAQf,UDQUgB,KAAKxC,EAASC,EAAoB,GAAIA,EAAoB","file":"login.bundled.js","sourcesContent":["webpackJsonp([5],[\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/* WEBPACK VAR INJECTION */(function(Backbone, _) {var jQuery = __webpack_require__(1),\n\t $ = jQuery,\n\t GalaxyApp = __webpack_require__(51).GalaxyApp,\n\t _l = __webpack_require__(6),\n\t Page = __webpack_require__(52);\n\t\n\twindow.app = function app(options, bootstrapped) {\n\t window.Galaxy = new GalaxyApp(options, bootstrapped);\n\t Galaxy.debug('login app');\n\t var redirect = encodeURI(options.redirect);\n\t\n\t // TODO: remove iframe for user login (at least) and render login page from here\n\t // then remove this redirect\n\t if (!options.show_welcome_with_login) {\n\t var params = jQuery.param({ use_panels: 'True', redirect: redirect });\n\t window.location.href = Galaxy.root + 'user/login?' + params;\n\t return;\n\t }\n\t\n\t var LoginPage = Backbone.View.extend({\n\t initialize: function (page) {\n\t this.page = page;\n\t this.model = new Backbone.Model({ title: _l('Login required') });\n\t this.setElement(this._template());\n\t },\n\t render: function () {\n\t this.page.$('#galaxy_main').prop('src', options.welcome_url);\n\t },\n\t _template: function () {\n\t var login_url = options.root + 'user/login?' + $.param({ redirect: redirect });\n\t return '<iframe src=\"' + login_url + '\" frameborder=\"0\" style=\"width: 100%; height: 100%;\"/>';\n\t }\n\t });\n\t\n\t $(function () {\n\t Galaxy.page = new Page.View(_.extend(options, {\n\t Right: LoginPage\n\t }));\n\t });\n\t};\n\t/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(2), __webpack_require__(3)))\n\n/***/ })\n]);\n\n\n// WEBPACK FOOTER //\n// login.bundled.js","var jQuery = require( 'jquery' ),\n $ = jQuery,\n GalaxyApp = require( 'galaxy' ).GalaxyApp,\n _l = require( 'utils/localization' ),\n Page = require( 'layout/page' );\n\nwindow.app = function app( options, bootstrapped ){\n window.Galaxy = new GalaxyApp( options, bootstrapped );\n Galaxy.debug( 'login app' );\n var redirect = encodeURI( options.redirect );\n\n // TODO: remove iframe for user login (at least) and render login page from here\n // then remove this redirect\n if( !options.show_welcome_with_login ){\n var params = jQuery.param({ use_panels : 'True', redirect : redirect });\n window.location.href = Galaxy.root + 'user/login?' + params;\n return;\n }\n\n var LoginPage = Backbone.View.extend({\n initialize: function( page ) {\n this.page = page;\n this.model = new Backbone.Model({ title : _l( 'Login required' ) } );\n this.setElement( this._template() );\n },\n render: function() {\n this.page.$( '#galaxy_main' ).prop( 'src', options.welcome_url );\n },\n _template : function() {\n var login_url = options.root + 'user/login?' + $.param( { redirect : redirect } );\n return '<iframe src=\"' + login_url + '\" frameborder=\"0\" style=\"width: 100%; height: 100%;\"/>';\n }\n });\n\n $(function(){\n Galaxy.page = new Page.View( _.extend( options, {\n Right : LoginPage\n } ) );\n });\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/apps/login.js"],"sourceRoot":""}
{"version":3,"sources":["webpack:///login.bundled.js","webpack:///./galaxy/scripts/apps/login.js"],"names":["webpackJsonp","module","exports","__webpack_require__","Backbone","_","jQuery","$","GalaxyApp","_l","Page","window","app","options","bootstrapped","Galaxy","debug","redirect","encodeURI","show_welcome_with_login","params","param","use_panels","location","href","root","LoginPage","View","extend","initialize","page","this","model","Model","title","setElement","_template","render","prop","welcome_url","login_url","Right","call"],"mappings":"AAAAA,cAAc,IAER,SAAUC,EAAQC,EAASC,ICFjC,SAAAC,EAAAC,GAAA,GAAIC,GAASH,EAAS,GAClBI,EAAID,EACJE,EAAYL,EAAS,IAAWK,UAChCC,EAAKN,EAAS,GACdO,EAAOP,EAAS,GAEpBQ,QAAOC,IAAM,SAAcC,EAASC,GAChCH,OAAOI,OAAS,GAAIP,GAAWK,EAASC,GACxCC,OAAOC,MAAO,YACd,IAAIC,GAAWC,UAAWL,EAAQI,SAIlC,KAAKJ,EAAQM,wBAAyB,CAClC,GAAIC,GAASd,EAAOe,OAAQC,WAAa,OAAQL,SAAWA,GAE5D,aADAN,OAAOY,SAASC,KAAOT,OAAOU,KAAO,cAAgBL,GAIzD,GAAIM,GAAYtB,EAASuB,KAAKC,QAC1BC,WAAY,SAAUC,GAClBC,KAAKD,KAAOA,EACZC,KAAKC,MAAQ,GAAI5B,GAAS6B,OAAQC,MAAQzB,EAAI,oBAC9CsB,KAAKI,WAAYJ,KAAKK,cAE1BC,OAAQ,WACJN,KAAKD,KAAKvB,EAAG,gBAAiB+B,KAAM,MAAOzB,EAAQ0B,cAEvDH,UAAY,WACR,GAAII,GAAY3B,EAAQY,KAAO,cAAgBlB,EAAEc,OAASJ,SAAWA,GACrE,OAAO,gBAAkBuB,EAAY,2DAI7CjC,GAAE,WACEQ,OAAOe,KAAO,GAAIpB,GAAKiB,KAAMtB,EAAEuB,OAAQf,GACnC4B,MAAQf,UDQUgB,KAAKxC,EAASC,EAAoB,GAAIA,EAAoB","file":"login.bundled.js","sourcesContent":["webpackJsonp([5],[\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\t/* WEBPACK VAR INJECTION */(function(Backbone, _) {var jQuery = __webpack_require__(1),\n\t $ = jQuery,\n\t GalaxyApp = __webpack_require__(52).GalaxyApp,\n\t _l = __webpack_require__(6),\n\t Page = __webpack_require__(53);\n\t\n\twindow.app = function app(options, bootstrapped) {\n\t window.Galaxy = new GalaxyApp(options, bootstrapped);\n\t Galaxy.debug('login app');\n\t var redirect = encodeURI(options.redirect);\n\t\n\t // TODO: remove iframe for user login (at least) and render login page from here\n\t // then remove this redirect\n\t if (!options.show_welcome_with_login) {\n\t var params = jQuery.param({ use_panels: 'True', redirect: redirect });\n\t window.location.href = Galaxy.root + 'user/login?' + params;\n\t return;\n\t }\n\t\n\t var LoginPage = Backbone.View.extend({\n\t initialize: function (page) {\n\t this.page = page;\n\t this.model = new Backbone.Model({ title: _l('Login required') });\n\t this.setElement(this._template());\n\t },\n\t render: function () {\n\t this.page.$('#galaxy_main').prop('src', options.welcome_url);\n\t },\n\t _template: function () {\n\t var login_url = options.root + 'user/login?' + $.param({ redirect: redirect });\n\t return '<iframe src=\"' + login_url + '\" frameborder=\"0\" style=\"width: 100%; height: 100%;\"/>';\n\t }\n\t });\n\t\n\t $(function () {\n\t Galaxy.page = new Page.View(_.extend(options, {\n\t Right: LoginPage\n\t }));\n\t });\n\t};\n\t/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(2), __webpack_require__(3)))\n\n/***/ })\n]);\n\n\n// WEBPACK FOOTER //\n// login.bundled.js","var jQuery = require( 'jquery' ),\n $ = jQuery,\n GalaxyApp = require( 'galaxy' ).GalaxyApp,\n _l = require( 'utils/localization' ),\n Page = require( 'layout/page' );\n\nwindow.app = function app( options, bootstrapped ){\n window.Galaxy = new GalaxyApp( options, bootstrapped );\n Galaxy.debug( 'login app' );\n var redirect = encodeURI( options.redirect );\n\n // TODO: remove iframe for user login (at least) and render login page from here\n // then remove this redirect\n if( !options.show_welcome_with_login ){\n var params = jQuery.param({ use_panels : 'True', redirect : redirect });\n window.location.href = Galaxy.root + 'user/login?' + params;\n return;\n }\n\n var LoginPage = Backbone.View.extend({\n initialize: function( page ) {\n this.page = page;\n this.model = new Backbone.Model({ title : _l( 'Login required' ) } );\n this.setElement( this._template() );\n },\n render: function() {\n this.page.$( '#galaxy_main' ).prop( 'src', options.welcome_url );\n },\n _template : function() {\n var login_url = options.root + 'user/login?' + $.param( { redirect : redirect } );\n return '<iframe src=\"' + login_url + '\" frameborder=\"0\" style=\"width: 100%; height: 100%;\"/>';\n }\n });\n\n $(function(){\n Galaxy.page = new Page.View( _.extend( options, {\n Right : LoginPage\n } ) );\n });\n};\n\n\n\n// WEBPACK FOOTER //\n// ./galaxy/scripts/apps/login.js"],"sourceRoot":""}
+1 -1
View File
@@ -1,2 +1,2 @@
define(["mvc/base-mvc","utils/localization"],function(a,b){var c=Backbone.View.extend(a.LoggableMixin).extend(a.HiddenUntilActivatedViewMixin).extend({tagName:"div",className:"tags-display",initialize:function(a){this.label=a.usePrompt===!1?"":'<label class="prompt">'+b("Tags")+"</label>",this.hiddenUntilActivated(a.$activator,a)},render:function(){var a=this;return this.$el.html(this._template()),this.$input().select2({placeholder:"Add tags",width:"100%",tags:function(){return a._getTagsUsed()}}),this._setUpBehaviors(),this},_hashToName:function(a){return a.startsWith("#")?"name:"+a.slice(1):a},_nameToHash:function(a){return a.startsWith("name:")&&(a="#"+a.slice(5)),a},_template:function(){return[this.label,'<input class="tags-input" value="',this.tagsToCSV(),'" />'].join("")},tagsToCSV:function(){var a=this,b=this.model.get("tags");return!_.isArray(b)||_.isEmpty(b)?"":b.map(function(b){return _.escape(a._nameToHash(b))}).sort().join(",")},$input:function(){return this.$el.find("input.tags-input")},_getTagsUsed:function(){var a=this;return _.map(Galaxy.user.get("tags_used"),a._nameToHash)},_setUpBehaviors:function(){var a=this;this.$input().on("change",function(b){b.val=_.map(b.val,a._hashToName),a.model.save({tags:b.val}),b.added&&a._addNewTagToTagsUsed(b.added.text+"")})},_addNewTagToTagsUsed:function(a){var b=Galaxy.user.get("tags_used");_.contains(b,a)||(b.push(a),b.sort(),Galaxy.user.set("tags_used",b))},remove:function(){this.$input.off(),this.stopListening(this.model),Backbone.View.prototype.remove.call(this)},toString:function(){return["TagsEditor(",this.model+"",")"].join("")}});return{TagsEditor:c}});
define(["mvc/base-mvc","utils/localization"],function(a,b){var c=Backbone.View.extend(a.LoggableMixin).extend(a.HiddenUntilActivatedViewMixin).extend({tagName:"div",className:"tags-display",select_width:"100%",events:{},initialize:function(a){this.show_editor=!1,this.label=a.usePrompt===!1?"":'<label class="prompt">'+b("Tags")+"</label>",this.workflow_mode=a.workflow_mode||!1,this.workflow_mode&&(this.events.click="showEditor",this.events.keydown="keydownHandler"),this.hiddenUntilActivated(a.$activator,a)},render:function(){var a=this;return this.$el.html(this.workflow_mode?this._workflowTemplate():this._defaultTemplate()),this.$input().select2({placeholder:"Add tags",width:this.workflow_mode?this.width:this.select_width,tags:function(){return a._getTagsUsed()}}),this._setUpBehaviors(),this},_hashToName:function(a){return a.startsWith("#")?"name:"+a.slice(1):a},_nameToHash:function(a){return a.startsWith("name:")&&(a="#"+a.slice(5)),a},_defaultTemplate:function(){return[this.label,this._renderEditor()].join("")},_workflowTemplate:function(){return[this.show_editor?this._renderEditor():this._renderTags()].join(" ")},keydownHandler:function(a){switch(a.which){case 27:this.hideEditor()}},showEditor:function(){this.show_editor=!0,this.render()},hideEditor:function(){this.show_editor=!1,this.render()},_renderEditor:function(){return'<input class="tags-input" value="'+this.tagsToCSV()+'"/>'},_renderTags:function(){var a=this.model.get("tags"),b="static/images/fugue/tag--plus.png",c=[];return _.each(a,function(a){a=0==a.indexOf("name:")?a.slice(5):a;var b='<span class="label label-info">'+a+"</span>";c.push(b)}),0===c.length&&c.push("<img src="+b+' class="add-tag-button" title="Add tags"/>'),c.join(" ")},tagsToCSV:function(){var a=this,b=this.model.get("tags");return!_.isArray(b)||_.isEmpty(b)?"":b.map(function(b){return _.escape(a._nameToHash(b))}).sort().join(",")},$input:function(){return this.$el.find("input.tags-input")},_getTagsUsed:function(){var a=this;return _.map(Galaxy.user.get("tags_used"),a._nameToHash)},_setUpBehaviors:function(){var a=this;this.$input().on("change",function(b){b.val=_.map(b.val,a._hashToName),a.model.save({tags:b.val}),b.added&&a._addNewTagToTagsUsed(b.added.text+"")})},_addNewTagToTagsUsed:function(a){var b=Galaxy.user.get("tags_used");_.contains(b,a)||(b.push(a),b.sort(),Galaxy.user.set("tags_used",b))},remove:function(){this.$input.off(),this.stopListening(this.model),Backbone.View.prototype.remove.call(this)},toString:function(){return["TagsEditor(",this.model+"",")"].join("")}});return{TagsEditor:c}});
//# sourceMappingURL=../../maps/mvc/tag.js.map
@@ -0,0 +1,2 @@
define(["mvc/base-mvc"],function(a){var b="workflow",c=Backbone.Model.extend(a.LoggableMixin).extend({_logNamespace:b,urlRoot:"/api/workflows",toJSON:function(){return{workflow:this.attributes}}}),d=Backbone.Collection.extend({model:c,url:"/api/workflows"});return{WorkflowItem:c,WorkflowCollection:d}});
//# sourceMappingURL=../../../maps/mvc/workflow/workflow-model.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+50 -7
View File
@@ -28,7 +28,7 @@ class NullTourCallback(object):
pass
def retry_call_during_transitions(f, attempts=5, sleep=.1):
def retry_call_during_transitions(f, attempts=5, sleep=.1, exception_check=exception_indicates_stale_element):
previous_attempts = 0
while True:
try:
@@ -37,18 +37,18 @@ def retry_call_during_transitions(f, attempts=5, sleep=.1):
if previous_attempts > attempts:
raise
if not exception_indicates_stale_element(e):
if not exception_check(e):
raise
time.sleep(sleep)
previous_attempts += 1
def retry_during_transitions(f, attempts=5, sleep=.1):
def retry_during_transitions(f, attempts=5, sleep=.1, exception_check=exception_indicates_stale_element):
@wraps(f)
def _retry(*args, **kwds):
retry_call_during_transitions(partial(f, *args, **kwds), attempts=attempts, sleep=sleep)
return retry_call_during_transitions(partial(f, *args, **kwds), attempts=attempts, sleep=sleep, exception_check=exception_check)
return _retry
@@ -395,12 +395,26 @@ class NavigatesGalaxy(HasDriver):
def workflow_index_table_elements(self):
self.wait_for_selector_visible("tbody.workflow-search")
table_elements = self.driver.find_elements_by_css_selector("tbody.workflow-search > tr")
table_elements = self.driver.find_elements_by_css_selector("tbody.workflow-search > tr:not([style*='display: none'])")
return table_elements
def workflow_index_table_row(self, workflow_index=0):
return self.workflow_index_table_elements()[workflow_index]
@retry_during_transitions
def workflow_index_click_search(self):
search_element = self.wait_for_selector_clickable("input.search-wf")
search_element.click()
return search_element
def workflow_index_rename(self, new_name, workflow_index=0):
self.workflow_index_click_option("Rename", workflow_index=workflow_index)
alert = self.driver.switch_to.alert
alert.send_keys(new_name)
alert.accept()
def workflow_index_click_option(self, option_title, workflow_index=0):
table_elements = self.workflow_index_table_elements()
workflow_row = table_elements[workflow_index]
workflow_row = self.workflow_index_table_row(workflow_index=workflow_index)
workflow_button = workflow_row.find_element_by_css_selector(".menubutton")
workflow_button.click()
menu_element = self.wait_for_selector_visible("ul.action-dpd")
@@ -415,6 +429,35 @@ class NavigatesGalaxy(HasDriver):
if not found_option:
raise AssertionError("Failed to find workflow action option with title [%s]" % option_title)
def workflow_index_click_tag_display(self, workflow_index=0):
workflow_row_element = self.workflow_index_table_row(workflow_index)
tag_display = workflow_row_element.find_element_by_css_selector(".tags-display")
tag_display.click()
def workflow_index_tags(self, workflow_index=0):
workflow_row_element = self.workflow_index_table_row(workflow_index)
tag_display = workflow_row_element.find_element_by_css_selector(".tags-display")
tag_spans = tag_display.find_elements_by_css_selector("span.label")
tags = []
for tag_span in tag_spans:
tags.append(tag_span.text)
return tags
def workflow_sharing_click_publish(self):
button = self.wait_for_selector_clickable("input[name='make_accessible_and_publish']")
button.click()
def tagging_add(self, tags, auto_closes=True, parent_selector=""):
for i, tag in enumerate(tags):
if auto_closes or i == 0:
tag_area = parent_selector + ".tags-input input[type='text']"
tag_area = self.wait_for_selector_clickable(tag_area)
tag_area.click()
tag_area.send_keys(tag)
self.send_enter(tag_area)
def workflow_run_submit(self):
button = self.wait_for_selector(".ui-form-header button")
button.click()
+9 -2
View File
@@ -6,16 +6,17 @@ import datetime
import json
import os
import time
import traceback
from functools import wraps
from functools import partial, wraps
import requests
from galaxy_selenium import (
driver_factory,
)
from galaxy_selenium.navigates_galaxy import NavigatesGalaxy
from galaxy_selenium.navigates_galaxy import NavigatesGalaxy, retry_during_transitions
try:
from pyvirtualdisplay import Display
@@ -112,6 +113,9 @@ def selenium_test(f):
return func_wrapper
retry_assertion_during_transitions = partial(retry_during_transitions, exception_check=lambda e: isinstance(e, AssertionError))
class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy):
framework_tool_and_types = True
@@ -149,6 +153,9 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy):
def setup_driver_and_session(self):
self.display = driver_factory.virtual_display_if_enabled(headless_selenium())
self.driver = get_driver()
# New workflow index page does not degrade well to smaller sizes, needed
# to increase this.
self.driver.set_window_size(1280, 900)
if self.ensure_registered:
self.register()
+77 -10
View File
@@ -1,6 +1,7 @@
from .framework import (
retry_assertion_during_transitions,
selenium_test,
SeleniumTestCase
SeleniumTestCase,
)
@@ -33,16 +34,82 @@ class WorkflowManagementTestCase(SeleniumTestCase):
def test_rename(self):
self.workflow_index_open()
self._workflow_import_from_url()
self.workflow_index_click_option("Rename")
rename_form_element = self.wait_for_selector("form")
self.fill(rename_form_element, {
"new_name": "CoolNewName"
})
self.click_submit(rename_form_element)
self.workflow_index_rename("CoolNewName")
table_elements = self.workflow_index_table_elements()
renamed_workflow_button = table_elements[0].find_element_by_css_selector(".menubutton")
assert 'CoolNewName' in renamed_workflow_button.text, renamed_workflow_button.text
@retry_assertion_during_transitions
def check_name():
row_element = self.workflow_index_table_row()
renamed_workflow_button = row_element.find_element_by_css_selector(".menubutton")
assert 'CoolNewName' in renamed_workflow_button.text, renamed_workflow_button.text
check_name()
@selenium_test
def test_download(self):
self.workflow_index_open()
self._workflow_import_from_url()
# TODO: fill this test out - getting downloaded files in general through Selenium is a bit tough,
# going through the motions though should catch a couple potential problems.
self.workflow_index_click_option("Download")
@selenium_test
def test_tagging(self):
self.workflow_index_open()
self._workflow_import_from_url()
self.workflow_index_click_tag_display()
self.tagging_add(["cooltag"])
@retry_assertion_during_transitions
def check_tags():
self.assertEqual(self.workflow_index_tags(), ["cooltag"])
check_tags()
@selenium_test
def test_index_search(self):
self.workflow_index_open()
self._workflow_import_from_url()
self.workflow_index_rename("searchforthis")
self._assert_showing_n_workflows(1)
search_box = self.workflow_index_click_search()
search_box.send_keys("doesnotmatch")
self._assert_showing_n_workflows(0)
# Prevent stale element textbox by re-fetching, seems to be
# needed but I don't understand why exactly. -John
search_box = self.workflow_index_click_search()
search_box.clear()
self.send_enter(search_box)
self._assert_showing_n_workflows(1)
search_box = self.workflow_index_click_search()
search_box.send_keys("searchforthis")
self.send_enter(search_box)
self._assert_showing_n_workflows(1)
@selenium_test
def test_publishing_display(self):
self.workflow_index_open()
self._workflow_import_from_url()
self.workflow_index_rename("managementesttopublish")
row_element = self.workflow_index_table_row()
columns = row_element.find_elements_by_css_selector("td")
assert columns[4].text == "No"
self.workflow_index_click_option("Share")
self.workflow_sharing_click_publish()
self.workflow_index_open()
row_element = self.workflow_index_table_row()
columns = row_element.find_elements_by_css_selector("td")
assert columns[4].text == "Yes"
@retry_assertion_during_transitions
def _assert_showing_n_workflows(self, n):
self.assertEqual(len(self.workflow_index_table_elements()), n)
def _workflow_import_from_url(self):
element = self.wait_for_selector_clickable(self.test_data["selectors"]["workflows"]["import_button"])