mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' into remove_sample_tracking
This commit is contained in:
+1
-1
@@ -124,7 +124,7 @@ sprite-*.less
|
||||
# JS, Local node_modules, and bower_components directories
|
||||
static/scripts/bundled/[0-9]*
|
||||
static/maps
|
||||
static/scripts/bundled/*.js.map
|
||||
static/scripts/bundled/*.map
|
||||
node_modules
|
||||
bower_components
|
||||
client/galaxy/scripts/libs/*
|
||||
|
||||
@@ -17,6 +17,7 @@ DOC_SOURCE_DIR=$(DOCS_DIR)/source
|
||||
SLIDESHOW_DIR=$(DOC_SOURCE_DIR)/slideshow
|
||||
OPEN_RESOURCE=bash -c 'open $$0 || xdg-open $$0'
|
||||
SLIDESHOW_TO_PDF?=bash -c 'docker run --rm -v `pwd`:/cwd astefanutti/decktape /cwd/$$0 /cwd/`dirname $$0`/`basename -s .html $$0`.pdf'
|
||||
CLIENT_COMMIT_WARNING="Please remember to 'make client-production' when finished developing, before a commit!"
|
||||
|
||||
all: help
|
||||
@echo "This makefile is used for building Galaxy's JS client, documentation, and drive the release process. A sensible all target is not implemented."
|
||||
@@ -128,15 +129,22 @@ update-and-commit-dependencies: ## update and commit linting + dev dependencies
|
||||
node-deps: ## Install NodeJS dependencies.
|
||||
cd client && yarn install --check-files
|
||||
|
||||
client: node-deps ## Rebuild all client-side artifacts
|
||||
client: node-deps ## Rebuild client-side artifacts for local development.
|
||||
cd client && yarn run build
|
||||
@echo $(CLIENT_COMMIT_WARNING)
|
||||
|
||||
client-production: node-deps ## Rebuild client-side artifacts for a production deployment (or committing to the repository).
|
||||
cd client && yarn run build-production
|
||||
|
||||
client-production-maps: node-deps ## Rebuild client-side artifacts for a production deployment, and include sourcemaps to aid in debugging efforts.
|
||||
cd client && yarn run build-production-maps
|
||||
|
||||
client-format: node-deps ## Reformat client code
|
||||
cd client && yarn run prettier
|
||||
|
||||
client-watch: node-deps ## A useful target for parallel development building.
|
||||
cd client && yarn run watch
|
||||
@echo "Remember to 'make client' when finished developing!"
|
||||
@echo $(CLIENT_COMMIT_WARNING)
|
||||
|
||||
client-test: client ## Run qunit tests via Karma
|
||||
cd client && yarn run test
|
||||
|
||||
+18
-2
@@ -16,11 +16,27 @@ Complete Client Build
|
||||
|
||||
There are many moving parts to the client build system, but the entrypoint for
|
||||
most people is the 'client' rule in the Makefile at the root of the Galaxy
|
||||
repository. Execute the following to perform a complete build including
|
||||
dependency staging, style building, script processing and bundling.
|
||||
repository. Execute the following to perform a complete build suitable for
|
||||
local development, including dependency staging, style building, script
|
||||
processing and bundling.
|
||||
|
||||
make client
|
||||
|
||||
Note: Don't commit the files that 'make client' creates. This is a
|
||||
development-specific build which includes extra debugging features, and
|
||||
excludes several production optimizations made during the build process.
|
||||
|
||||
For a production build, suitable for committing to the repository, or deploying
|
||||
to a live server, use the following:
|
||||
|
||||
make client-production
|
||||
|
||||
And, lastly, if you want a production build that includes sourcemaps to allow
|
||||
for perusal of the page-level javascript to facilitate live-server debugging,
|
||||
use:
|
||||
|
||||
make client-production-maps
|
||||
|
||||
|
||||
Automatic Rebuilding (Watch Mode)
|
||||
=================================
|
||||
|
||||
@@ -10,7 +10,7 @@ import Router from "layout/router";
|
||||
import Utils from "utils/utils";
|
||||
import Page from "layout/page";
|
||||
import Vue from "libs/vue";
|
||||
import UserAPIKeys from "components/user_api_keys.vue";
|
||||
import UserAPIKeys from "components/UserAPIKeys.vue";
|
||||
|
||||
window.app = function app(options, bootstrapped) {
|
||||
window.Galaxy = new GalaxyApp.GalaxyApp(options, bootstrapped);
|
||||
|
||||
@@ -13,7 +13,7 @@ import Tours from "mvc/tours";
|
||||
import GridView from "mvc/grid/grid-view";
|
||||
import GridShared from "mvc/grid/grid-shared";
|
||||
import Workflows from "mvc/workflow/workflow";
|
||||
import HistoryImport from "components/history-import.vue";
|
||||
import HistoryImport from "components/HistoryImport.vue";
|
||||
import HistoryList from "mvc/history/history-list";
|
||||
import ToolFormComposite from "mvc/tool/tool-form-composite";
|
||||
import QueryStringParsing from "utils/query-string-parsing";
|
||||
@@ -21,7 +21,7 @@ import Utils from "utils/utils";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
import DatasetError from "mvc/dataset/dataset-error";
|
||||
import DatasetEditAttributes from "mvc/dataset/dataset-edit-attributes";
|
||||
import Citations from "components/citations.vue";
|
||||
import Citations from "components/Citations.vue";
|
||||
import Vue from "libs/vue";
|
||||
|
||||
/** define the 'Analyze Data'/analysis/main/home page for Galaxy
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
<div class="toolForm">
|
||||
<div class="toolFormTitle">
|
||||
Citations
|
||||
<button v-if="viewRender" v-on:click="toggleViewRender" type="button" class="btn btn-xs" title="Show all in BibTeX format.">
|
||||
<button v-if="viewRender" v-on:click="toggleViewRender" type="button" class="btn btn-xs citations-to-bibtex" title="Show all in BibTeX format.">
|
||||
<i class="fa fa-pencil-square-o"></i>
|
||||
Show BibTeX
|
||||
</button>
|
||||
<button v-else type="button" v-on:click="toggleViewRender" class="btn btn-xs" title="Return to formatted citation list.">
|
||||
<button v-else type="button" v-on:click="toggleViewRender" class="btn btn-xs citations-to-formatted" title="Return to formatted citation list.">
|
||||
<i class="fa fa-times"></i>
|
||||
Hide BibTeX
|
||||
</button>
|
||||
@@ -90,7 +90,10 @@ var DatasetCollectionElement = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
.extend({ _logNamespace: "collections" });
|
||||
|
||||
//==============================================================================
|
||||
/** @class Base/Abstract Backbone collection for Generic DCEs. */
|
||||
/** @class Base/Abstract Backbone collection for Generic DCEs -
|
||||
current may be associated with a dataset (DatasetDCECollection)
|
||||
or another collection (NestedDCDCECollection).
|
||||
*/
|
||||
var DCECollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
|
||||
/** @lends DCECollection.prototype */ {
|
||||
_logNamespace: "collections",
|
||||
@@ -197,7 +200,13 @@ var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
},
|
||||
|
||||
/** Which class to use for elements */
|
||||
collectionClass: DCECollection,
|
||||
collectionClass: function() {
|
||||
if (this.attributes.collection_type.indexOf(":") > 0) {
|
||||
return NestedDCDCECollection;
|
||||
} else {
|
||||
return DatasetDCECollection;
|
||||
}
|
||||
},
|
||||
|
||||
/** set up: create elements instance var and (on changes to elements) update them */
|
||||
initialize: function(model, options) {
|
||||
@@ -212,7 +221,8 @@ var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
|
||||
/** move elements model attribute to full collection */
|
||||
_createElementsModel: function() {
|
||||
this.debug(`${this}._createElementsModel`, this.collectionClass, this.get("elements"), this.elements);
|
||||
var collectionClass = this.collectionClass();
|
||||
this.debug(`${this}._createElementsModel`, collectionClass, this.get("elements"), this.elements);
|
||||
//TODO: same patterns as DatasetCollectionElement _createObjectModel - refactor to BASE_MVC.hasSubModel?
|
||||
var elements = this.get("elements") || [];
|
||||
this.unset("elements", { silent: true });
|
||||
@@ -222,7 +232,7 @@ var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
parent_hdca_id: self.get("id")
|
||||
});
|
||||
});
|
||||
this.elements = new this.collectionClass(elements);
|
||||
this.elements = new collectionClass(elements);
|
||||
//this.debug( 'collectionClass:', this.collectionClass + '', this.elements );
|
||||
return this.elements;
|
||||
},
|
||||
@@ -303,39 +313,12 @@ var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** Model for a DatasetCollection containing datasets (non-nested).
|
||||
*/
|
||||
var ListDatasetCollection = DatasetCollection.extend(
|
||||
/** @lends ListDatasetCollection.prototype */ {
|
||||
/** override since we know the collection will only contain datasets */
|
||||
collectionClass: DatasetDCECollection,
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `List${DatasetCollection.prototype.toString.call(this)}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** Model for a DatasetCollection containing fwd/rev datasets (a list of 2).
|
||||
*/
|
||||
var PairDatasetCollection = ListDatasetCollection.extend(
|
||||
/** @lends PairDatasetCollection.prototype */ {
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `Pair${DatasetCollection.prototype.toString.call(this)}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//_________________________________________________________________________________________________ NESTED COLLECTIONS
|
||||
// this is where things get weird, man. Weird.
|
||||
//TODO: it might be possible to compact all the following...I think.
|
||||
//==============================================================================
|
||||
/** @class Backbone model for a Generic DatasetCollectionElement that is also a DatasetCollection
|
||||
* (a nested collection). Currently only list:paired.
|
||||
* (a nested collection).
|
||||
*/
|
||||
var NestedDCDCE = DatasetCollection.extend(
|
||||
BASE_MVC.mixin(
|
||||
@@ -374,113 +357,7 @@ var NestedDCDCECollection = DCECollection.extend(
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for a paired dataset collection within a list:paired dataset collection.
|
||||
*/
|
||||
var NestedPairDCDCE = PairDatasetCollection.extend(
|
||||
BASE_MVC.mixin(
|
||||
DatasetCollectionElementMixin,
|
||||
/** @lends NestedPairDCDCE.prototype */ {
|
||||
//TODO:?? possibly rename to NestedDatasetCollection?
|
||||
|
||||
// because all objects have constructors (as this hashmap would even if this next line wasn't present)
|
||||
// the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
|
||||
// - re-apply manually it now
|
||||
/** This is both a collection and a collection element - call the constructor */
|
||||
constructor: function(attributes, options) {
|
||||
this.debug("\t NestedPairDCDCE.constructor:", attributes, options);
|
||||
//DatasetCollection.constructor.call( this, attributes, options );
|
||||
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
var objStr = this.object ? `${this.object}` : this.get("element_identifier");
|
||||
return ["NestedPairDCDCE(", objStr, ")"].join("");
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone collection for a backbone collection containing paired dataset collections.
|
||||
*/
|
||||
var NestedPairDCDCECollection = NestedDCDCECollection.extend(
|
||||
/** @lends PairDCDCECollection.prototype */ {
|
||||
/** We know this collection is composed of only nested pair collections */
|
||||
model: NestedPairDCDCE,
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return ["NestedPairDCDCECollection(", this.length, ")"].join("");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone Model for a DatasetCollection (list) that contains DatasetCollections (pairs).
|
||||
*/
|
||||
var ListPairedDatasetCollection = DatasetCollection.extend(
|
||||
/** @lends ListPairedDatasetCollection.prototype */ {
|
||||
/** list:paired is the only collection that itself contains collections */
|
||||
collectionClass: NestedPairDCDCECollection,
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return ["ListPairedDatasetCollection(", this.get("name"), ")"].join("");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for a list dataset collection within a list:list dataset collection. */
|
||||
var NestedListDCDCE = ListDatasetCollection.extend(
|
||||
BASE_MVC.mixin(
|
||||
DatasetCollectionElementMixin,
|
||||
/** @lends NestedListDCDCE.prototype */ {
|
||||
/** This is both a collection and a collection element - call the constructor */
|
||||
constructor: function(attributes, options) {
|
||||
this.debug("\t NestedListDCDCE.constructor:", attributes, options);
|
||||
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
var objStr = this.object ? `${this.object}` : this.get("element_identifier");
|
||||
return ["NestedListDCDCE(", objStr, ")"].join("");
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone collection containing list dataset collections. */
|
||||
var NestedListDCDCECollection = NestedDCDCECollection.extend({
|
||||
/** We know this collection is composed of only nested pair collections */
|
||||
model: NestedListDCDCE,
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return ["NestedListDCDCECollection(", this.length, ")"].join("");
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone Model for a DatasetCollection (list) that contains other lists. */
|
||||
var ListOfListsDatasetCollection = DatasetCollection.extend({
|
||||
/** list:paired is the only collection that itself contains collections */
|
||||
collectionClass: NestedListDCDCECollection,
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return ["ListOfListsDatasetCollection(", this.get("name"), ")"].join("");
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
ListDatasetCollection: ListDatasetCollection,
|
||||
PairDatasetCollection: PairDatasetCollection,
|
||||
ListPairedDatasetCollection: ListPairedDatasetCollection,
|
||||
ListOfListsDatasetCollection: ListOfListsDatasetCollection
|
||||
DatasetCollection: DatasetCollection
|
||||
};
|
||||
|
||||
@@ -26,6 +26,12 @@ var CollectionViewEdit = _super.extend(
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_EDIT.NestedDCDCEListItemEdit,
|
||||
|
||||
getNestedDCDCEViewClass: function() {
|
||||
return DC_EDIT.NestedDCDCEListItemEdit.extend({
|
||||
foldoutPanelClass: CollectionViewEdit
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... SET UP
|
||||
/** Set up the view, set up storage, bind listeners to HistoryContents events
|
||||
* @param {Object} attributes optional settings for the panel
|
||||
@@ -90,94 +96,7 @@ var CollectionViewEdit = _super.extend(
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class non-editable, read-only View/Controller for a dataset collection. */
|
||||
var ListCollectionViewEdit = CollectionViewEdit.extend(
|
||||
/** @lends ListCollectionView.prototype */ {
|
||||
//TODO: not strictly needed - due to switch in CollectionView._getContentClass
|
||||
/** sub view class used for datasets */
|
||||
DatasetDCEViewClass: DC_EDIT.DatasetDCEListItemEdit,
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListCollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class Editable, read-only View/Controller for a dataset collection. */
|
||||
var PairCollectionViewEdit = ListCollectionViewEdit.extend(
|
||||
/** @lends PairCollectionViewEdit.prototype */ {
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `PairCollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class Editable (roughly since these collections are immutable),
|
||||
* View/Controller for a dataset collection.
|
||||
*/
|
||||
var NestedPairCollectionViewEdit = PairCollectionViewEdit.extend(
|
||||
/** @lends NestedPairCollectionViewEdit.prototype */ {
|
||||
/** Override to remove the editable text from the name/identifier - these collections are considered immutable */
|
||||
_setUpBehaviors: function($where) {
|
||||
_super.prototype._setUpBehaviors.call(this, $where);
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `NestedPairCollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class editable, View/Controller for a list of pairs dataset collection. */
|
||||
var ListOfPairsCollectionViewEdit = CollectionViewEdit.extend(
|
||||
/** @lends ListOfPairsCollectionView.prototype */ {
|
||||
//TODO: not strictly needed - due to switch in CollectionView._getContentClass
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_EDIT.NestedDCDCEListItemEdit.extend({
|
||||
foldoutPanelClass: NestedPairCollectionViewEdit
|
||||
}),
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListOfPairsCollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class View/Controller for a list of lists dataset collection. */
|
||||
var ListOfListsCollectionViewEdit = CollectionViewEdit.extend(
|
||||
/** @lends ListOfListsCollectionView.prototype */ {
|
||||
//TODO: not strictly needed - due to switch in CollectionView._getContentClass
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_EDIT.NestedDCDCEListItemEdit.extend({
|
||||
foldoutPanelClass: NestedPairCollectionViewEdit
|
||||
}),
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListOfListsCollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
CollectionViewEdit: CollectionViewEdit,
|
||||
ListCollectionViewEdit: ListCollectionViewEdit,
|
||||
PairCollectionViewEdit: PairCollectionViewEdit,
|
||||
ListOfPairsCollectionViewEdit: ListOfPairsCollectionViewEdit,
|
||||
ListOfListsCollectionViewEdit: ListOfListsCollectionViewEdit
|
||||
CollectionViewEdit: CollectionViewEdit
|
||||
};
|
||||
|
||||
@@ -21,8 +21,7 @@ var CollectionView = _super.extend(
|
||||
|
||||
/** sub view class used for datasets */
|
||||
DatasetDCEViewClass: DC_LI.DatasetDCEListItemView,
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_LI.NestedDCDCEListItemView,
|
||||
|
||||
/** key of attribute in model to assign to this.collection */
|
||||
modelCollectionKey: "elements",
|
||||
|
||||
@@ -44,9 +43,16 @@ var CollectionView = _super.extend(
|
||||
this.downloadUrl = `${Galaxy.root}api/dataset_collections/${this.model.attributes.id}/download`;
|
||||
},
|
||||
|
||||
getNestedDCDCEViewClass: function() {
|
||||
return DC_LI.NestedDCDCEListItemView.extend({
|
||||
foldoutPanelClass: CollectionView
|
||||
});
|
||||
},
|
||||
|
||||
_queueNewRender: function($newRender, speed) {
|
||||
speed = speed === undefined ? this.fxSpeed : speed;
|
||||
var panel = this;
|
||||
this.handleWarning($newRender);
|
||||
panel.log("_queueNewRender:", $newRender, speed);
|
||||
|
||||
// TODO: jquery@1.12 doesn't change display when the elem has display: flex
|
||||
@@ -56,6 +62,16 @@ var CollectionView = _super.extend(
|
||||
panel.trigger("rendered", panel);
|
||||
},
|
||||
|
||||
handleWarning: function($newRender) {
|
||||
var viewLength = this.views.length;
|
||||
var elementCount = this.model.get("element_count");
|
||||
if (elementCount && elementCount !== viewLength) {
|
||||
var warning = _l(`displaying only ${viewLength} of ${elementCount} items`);
|
||||
var $warns = $newRender.find(".elements-warning");
|
||||
$warns.html(`<div class="warningmessagesmall">${warning}</div>`);
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ sub-views
|
||||
/** In this override, use model.getVisibleContents */
|
||||
_filterCollection: function() {
|
||||
@@ -71,7 +87,7 @@ var CollectionView = _super.extend(
|
||||
case "hda":
|
||||
return this.DatasetDCEViewClass;
|
||||
case "dataset_collection":
|
||||
return this.NestedDCDCEViewClass;
|
||||
return this.getNestedDCDCEViewClass();
|
||||
}
|
||||
throw new TypeError("Unknown element type:", model.get("element_type"));
|
||||
},
|
||||
@@ -144,116 +160,74 @@ var CollectionView = _super.extend(
|
||||
|
||||
//------------------------------------------------------------------------------ TEMPLATES
|
||||
CollectionView.prototype.templates = (() => {
|
||||
var controlsTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="controls">',
|
||||
'<div class="navigation">',
|
||||
'<a class="back" href="javascript:void(0)">',
|
||||
'<span class="fa fa-icon fa-angle-left"></span>',
|
||||
_l("Back to "),
|
||||
"<%- view.parentName %>",
|
||||
"</a>",
|
||||
"</div>",
|
||||
|
||||
'<div class="title">',
|
||||
'<div class="name"><%- collection.name || collection.element_identifier %></div>',
|
||||
'<div class="subtitle">',
|
||||
'<% if( collection.collection_type === "list" ){ %>',
|
||||
_l("a list of datasets"),
|
||||
'<% } else if( collection.collection_type === "paired" ){ %>',
|
||||
_l("a pair of datasets"),
|
||||
'<% } else if( collection.collection_type === "list:paired" ){ %>',
|
||||
_l("a list of paired datasets"),
|
||||
'<% } else if( collection.collection_type === "list:list" ){ %>',
|
||||
_l("a list of dataset lists"),
|
||||
"<% } %>",
|
||||
"</div>",
|
||||
"</div>",
|
||||
|
||||
'<div class="tags-display"></div>',
|
||||
|
||||
'<div class="actions">',
|
||||
'<a class="download-btn icon-btn" ',
|
||||
'href="<%- view.downloadUrl %>',
|
||||
'" title="" download="" data-original-title="Download Collection">',
|
||||
'<span class="fa fa-floppy-o"></span>',
|
||||
"</a>",
|
||||
"</div>",
|
||||
"</div>"
|
||||
],
|
||||
"collection"
|
||||
);
|
||||
var controlsTemplate = (collection, view) => {
|
||||
var subtitle = collectionDescription(view.model);
|
||||
return `
|
||||
<div class="controls">
|
||||
<div class="navigation">
|
||||
<a class="back" href="javascript:void(0)">
|
||||
<span class="fa fa-icon fa-angle-left"></span>
|
||||
${_l("Back to ")}
|
||||
${_.escape(view.parentName)}
|
||||
</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="name">${_.escape(collection.name) || _.escape(collection.element_identifier)}</div>
|
||||
<div class="subtitle">
|
||||
${subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div class="elements-warning">
|
||||
</div>
|
||||
<div class="tags-display"></div>
|
||||
<div class="actions">
|
||||
<a class="download-btn icon-btn" href="${view.downloadUrl}"
|
||||
title="" download="" data-original-title="Download Collection">
|
||||
<span class="fa fa-floppy-o"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
return _.extend(_.clone(_super.prototype.templates), {
|
||||
controls: controlsTemplate
|
||||
});
|
||||
})();
|
||||
|
||||
// =============================================================================
|
||||
/** @class non-editable, read-only View/Controller for a dataset collection. */
|
||||
var ListCollectionView = CollectionView.extend(
|
||||
/** @lends ListCollectionView.prototype */ {
|
||||
//TODO: not strictly needed - due to switch in CollectionView._getContentClass
|
||||
/** sub view class used for datasets */
|
||||
DatasetDCEViewClass: DC_LI.DatasetDCEListItemView,
|
||||
function collectionTypeDescription(collection) {
|
||||
var collectionType = collection.get("collection_type");
|
||||
var collectionTypeDescription;
|
||||
if (collectionType == "list") {
|
||||
collectionTypeDescription = _l("list");
|
||||
} else if (collectionType == "paired") {
|
||||
collectionTypeDescription = _l("dataset pair");
|
||||
} else if (collectionType == "list:paired") {
|
||||
collectionTypeDescription = _l("list of pairs");
|
||||
} else {
|
||||
collectionTypeDescription = _l("nested list");
|
||||
}
|
||||
return collectionTypeDescription;
|
||||
}
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListCollectionView(${this.model ? this.model.get("name") : ""})`;
|
||||
function collectionDescription(collection) {
|
||||
var elementCount = collection.get("element_count");
|
||||
|
||||
var itemsDescription = `a ${collectionTypeDescription(collection)}`;
|
||||
if (elementCount) {
|
||||
var countDescription;
|
||||
if (elementCount == 1) {
|
||||
countDescription = "with 1 item";
|
||||
} else if (elementCount) {
|
||||
countDescription = `with ${elementCount} items`;
|
||||
}
|
||||
itemsDescription = `${itemsDescription} ${_l(countDescription)}`;
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class non-editable, read-only View/Controller for a dataset collection. */
|
||||
var PairCollectionView = ListCollectionView.extend(
|
||||
/** @lends PairCollectionView.prototype */ {
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `PairCollectionView(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class non-editable, read-only View/Controller for a dataset collection. */
|
||||
var ListOfPairsCollectionView = CollectionView.extend(
|
||||
/** @lends ListOfPairsCollectionView.prototype */ {
|
||||
//TODO: not strictly needed - due to switch in CollectionView._getContentClass
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_LI.NestedDCDCEListItemView.extend({
|
||||
foldoutPanelClass: PairCollectionView
|
||||
}),
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListOfPairsCollectionView(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
/** @class non-editable, read-only View/Controller for a list of lists dataset collection. */
|
||||
var ListOfListsCollectionView = CollectionView.extend({
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_LI.NestedDCDCEListItemView.extend({
|
||||
foldoutPanelClass: PairCollectionView
|
||||
}),
|
||||
|
||||
/** string rep */
|
||||
toString: function() {
|
||||
return `ListOfListsCollectionView(${this.model ? this.model.get("name") : ""})`;
|
||||
}
|
||||
});
|
||||
return itemsDescription;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
CollectionView: CollectionView,
|
||||
ListCollectionView: ListCollectionView,
|
||||
PairCollectionView: PairCollectionView,
|
||||
ListOfPairsCollectionView: ListOfPairsCollectionView,
|
||||
ListOfListsCollectionView: ListOfListsCollectionView
|
||||
collectionTypeDescription: collectionTypeDescription,
|
||||
collectionDescription: collectionDescription,
|
||||
CollectionView: CollectionView
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ var ListCollectionCreator = Backbone.View.extend(BASE_MVC.LoggableMixin)
|
||||
/** the class used to display individual elements */
|
||||
elementViewClass: DatasetCollectionElementView,
|
||||
/** the class this creator will create and save */
|
||||
collectionClass: HDCA.HistoryListDatasetCollection,
|
||||
collectionClass: HDCA.HistoryDatasetCollection,
|
||||
className: "list-collection-creator collection-creator flex-row-container",
|
||||
|
||||
/** minimum number of valid elements to start with in order to build a collection of this type */
|
||||
|
||||
@@ -623,7 +623,6 @@ var PairedCollectionCreator = Backbone.View.extend(baseMVC.LoggableMixin)
|
||||
|
||||
var url = `${Galaxy.root}api/histories/${this.historyId}/contents/dataset_collections`;
|
||||
|
||||
//TODO: use ListPairedCollection.create()
|
||||
var ajaxData = {
|
||||
type: "dataset_collection",
|
||||
collection_type: "list:paired",
|
||||
|
||||
@@ -61,7 +61,7 @@ var PairCollectionCreator = _super.extend({
|
||||
/** the class used to display individual elements */
|
||||
elementViewClass: PairedDatasetCollectionElementView,
|
||||
/** the class this creator will create and save */
|
||||
collectionClass: HDCA.HistoryPairDatasetCollection,
|
||||
collectionClass: HDCA.HistoryDatasetCollection,
|
||||
className: "pair-collection-creator collection-creator flex-row-container",
|
||||
|
||||
/** override to no-op */
|
||||
|
||||
@@ -66,7 +66,7 @@ var View = Backbone.View.extend({
|
||||
message: response.message,
|
||||
status: "success",
|
||||
persistent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
if (self.redirect) {
|
||||
window.location = `${Galaxy.root + self.redirect}?${$.param(params)}`;
|
||||
|
||||
@@ -14,18 +14,7 @@ var HDCAListItemEdit = _super.extend(
|
||||
|
||||
/** Override to return editable versions of the collection panels */
|
||||
_getFoldoutPanelClass: function() {
|
||||
var collectionType = this.model.get("collection_type");
|
||||
switch (collectionType) {
|
||||
case "list":
|
||||
return DC_VIEW_EDIT.ListCollectionViewEdit;
|
||||
case "paired":
|
||||
return DC_VIEW_EDIT.PairCollectionViewEdit;
|
||||
case "list:paired":
|
||||
return DC_VIEW_EDIT.ListOfPairsCollectionViewEdit;
|
||||
case "list:list":
|
||||
return DC_VIEW_EDIT.ListOfListsCollectionViewEdit;
|
||||
}
|
||||
throw new TypeError(`Unknown collection_type: ${collectionType}`);
|
||||
return DC_VIEW_EDIT.CollectionViewEdit;
|
||||
},
|
||||
|
||||
// ......................................................................... delete
|
||||
|
||||
@@ -16,40 +16,102 @@ var HDCAListItemView = _super.extend(
|
||||
/** event listeners */
|
||||
_setUpListeners: function() {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
var renderListen = (model, options) => {
|
||||
this.render();
|
||||
};
|
||||
if (this.model.jobStatesSummary) {
|
||||
this.listenTo(this.model.jobStatesSummary, "change", renderListen);
|
||||
}
|
||||
this.listenTo(this.model, {
|
||||
"change:tags change:populated change:visible": function(model, options) {
|
||||
this.render();
|
||||
}
|
||||
"change:tags change:visible change:state": renderListen
|
||||
});
|
||||
},
|
||||
|
||||
/** Override to provide the proper collections panels as the foldout */
|
||||
_getFoldoutPanelClass: function() {
|
||||
var collectionType = this.model.get("collection_type");
|
||||
switch (collectionType) {
|
||||
case "list":
|
||||
return DC_VIEW.ListCollectionView;
|
||||
case "paired":
|
||||
return DC_VIEW.PairCollectionView;
|
||||
case "list:paired":
|
||||
return DC_VIEW.ListOfPairsCollectionView;
|
||||
case "list:list":
|
||||
return DC_VIEW.ListOfListsCollectionView;
|
||||
}
|
||||
throw new TypeError(`Unknown collection_type: ${collectionType}`);
|
||||
return DC_VIEW.CollectionView;
|
||||
},
|
||||
|
||||
/** In this override, add the state as a class for use with state-based CSS */
|
||||
_swapNewRender: function($newRender) {
|
||||
_super.prototype._swapNewRender.call(this, $newRender);
|
||||
//TODO: model currently has no state
|
||||
var state = !this.model.get("populated") ? STATES.RUNNING : STATES.OK;
|
||||
//if( this.model.has( 'state' ) ){
|
||||
var state;
|
||||
var jobStatesSummary = this.model.jobStatesSummary;
|
||||
if (jobStatesSummary) {
|
||||
if (jobStatesSummary.new()) {
|
||||
state = "loading";
|
||||
} else if (jobStatesSummary.errored()) {
|
||||
state = "error";
|
||||
} else if (jobStatesSummary.terminal()) {
|
||||
state = "ok";
|
||||
} else if (jobStatesSummary.running()) {
|
||||
state = "running";
|
||||
} else {
|
||||
state = "queued";
|
||||
}
|
||||
} else if (this.model.get("job_source_id")) {
|
||||
// Initial rendering - polling will fill in more details in a bit.
|
||||
state = "loading";
|
||||
} else {
|
||||
state = this.model.get("populated_state") ? STATES.OK : STATES.RUNNING;
|
||||
}
|
||||
this.$el.addClass(`state-${state}`);
|
||||
//}
|
||||
var stateDescription = this.stateDescription();
|
||||
this.$(".state-description").html(stateDescription);
|
||||
return this.$el;
|
||||
},
|
||||
|
||||
stateDescription: function() {
|
||||
var collection = this.model;
|
||||
var elementCount = collection.get("element_count");
|
||||
var jobStateSource = collection.get("job_source_type");
|
||||
var collectionType = this.model.get("collection_type");
|
||||
var collectionTypeDescription = DC_VIEW.collectionTypeDescription(collection);
|
||||
var simpleDescription = DC_VIEW.collectionDescription(collection);
|
||||
var jobStatesSummary = collection.jobStatesSummary;
|
||||
if (!jobStateSource || jobStateSource == "Job") {
|
||||
return simpleDescription;
|
||||
} else if (!jobStatesSummary || !jobStatesSummary.hasDetails()) {
|
||||
return `
|
||||
<div class="progress state-progress">
|
||||
<span class="note">Loading job data for ${
|
||||
collectionTypeDescription
|
||||
}.<span class="blinking">..</span></span>
|
||||
<div class="progress-bar info" style="width:100%">
|
||||
</div>`;
|
||||
} else {
|
||||
var isNew = jobStatesSummary.new();
|
||||
var jobCount = isNew ? null : jobStatesSummary.jobCount();
|
||||
if (isNew) {
|
||||
return `
|
||||
<div class="progress state-progress">
|
||||
<span class="note">Creating jobs.<span class="blinking">..</span></span>
|
||||
<div class="progress-bar info" style="width:100%">
|
||||
</div>`;
|
||||
} else if (jobStatesSummary.errored()) {
|
||||
var errorCount = jobStatesSummary.numInError();
|
||||
return `a ${collectionTypeDescription} with ${errorCount} / ${jobCount} jobs in error`;
|
||||
} else if (jobStatesSummary.terminal()) {
|
||||
return simpleDescription;
|
||||
} else {
|
||||
var running = jobStatesSummary.states()["running"] || 0;
|
||||
var ok = jobStatesSummary.states()["ok"] || 0;
|
||||
var okPercent = ok / (jobCount * 1.0);
|
||||
var runningPercent = running / (jobCount * 1.0);
|
||||
var otherPercent = 1.0 - okPercent - runningPercent;
|
||||
var jobsStr = jobCount && jobCount > 1 ? `${jobCount} jobs` : `a job`;
|
||||
return `
|
||||
<div class="progress state-progress">
|
||||
<span class="note">${jobsStr} generating a ${collectionTypeDescription}</span>
|
||||
<div class="progress-bar ok" style="width:${okPercent * 100.0}%"></div>
|
||||
<div class="progress-bar running" style="width:${runningPercent * 100.0}%"></div>
|
||||
<div class="progress-bar new" style="width:${otherPercent * 100.0}%">
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function() {
|
||||
@@ -69,7 +131,6 @@ HDCAListItemView.prototype.templates = (() => {
|
||||
}
|
||||
});
|
||||
|
||||
// could steal this from hda-base (or use mixed content)
|
||||
var titleBarTemplate = collection => `
|
||||
<div class="title-bar clear" tabindex="0">
|
||||
<span class="state-icon"></span>
|
||||
@@ -77,7 +138,8 @@ HDCAListItemView.prototype.templates = (() => {
|
||||
<span class="hid">${collection.hid}</span>
|
||||
<span class="name">${_.escape(collection.name)}</span>
|
||||
</div>
|
||||
<div class="subtitle"></div>
|
||||
<div class="state-description">
|
||||
</div>
|
||||
${HISTORY_ITEM_LI.nametagTemplate(collection)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -6,115 +6,41 @@ import _l from "utils/localization";
|
||||
|
||||
Models for DatasetCollections contained within a history.
|
||||
|
||||
TODO:
|
||||
these might be compactable to one class if some duplication with
|
||||
collection-model is used.
|
||||
|
||||
==============================================================================*/
|
||||
var hcontentMixin = HISTORY_CONTENT.HistoryContentMixin;
|
||||
|
||||
var ListDC = DC_MODEL.ListDatasetCollection;
|
||||
var PairDC = DC_MODEL.PairDatasetCollection;
|
||||
var ListPairedDC = DC_MODEL.ListPairedDatasetCollection;
|
||||
var ListOfListsDC = DC_MODEL.ListOfListsDatasetCollection;
|
||||
|
||||
//==============================================================================
|
||||
/** Override to post to contents route w/o id. */
|
||||
function buildHDCASave(_super) {
|
||||
return function _save(attributes, options) {
|
||||
if (this.isNew()) {
|
||||
options = options || {};
|
||||
options.url = `${this.urlRoot + this.get("history_id")}/contents`;
|
||||
attributes = attributes || {};
|
||||
attributes.type = "dataset_collection";
|
||||
}
|
||||
return _super.call(this, attributes, options);
|
||||
};
|
||||
}
|
||||
var DC = DC_MODEL.DatasetCollection;
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for List Dataset Collection within a History.
|
||||
*/
|
||||
var HistoryListDatasetCollection = ListDC.extend(hcontentMixin).extend(
|
||||
/** @lends HistoryListDatasetCollection.prototype */ {
|
||||
defaults: _.extend(_.clone(ListDC.prototype.defaults), {
|
||||
var HistoryDatasetCollection = DC.extend(hcontentMixin).extend(
|
||||
/** @lends HistoryDatasetCollection.prototype */ {
|
||||
defaults: _.extend(_.clone(DC.prototype.defaults), {
|
||||
history_content_type: "dataset_collection",
|
||||
collection_type: "list",
|
||||
model_class: "HistoryDatasetCollectionAssociation"
|
||||
}),
|
||||
|
||||
//==============================================================================
|
||||
/** Override to post to contents route w/o id. */
|
||||
save: buildHDCASave(ListDC.prototype.save),
|
||||
save: function(attributes, options) {
|
||||
if (this.isNew()) {
|
||||
options = options || {};
|
||||
options.url = `${this.urlRoot + this.get("history_id")}/contents`;
|
||||
attributes = attributes || {};
|
||||
attributes.type = "dataset_collection";
|
||||
}
|
||||
return DC.prototype.save.call(this, attributes, options);
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `History${ListDC.prototype.toString.call(this)}`;
|
||||
return `History${DC.prototype.toString.call(this)}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for Pair Dataset Collection within a History.
|
||||
* @constructs
|
||||
*/
|
||||
var HistoryPairDatasetCollection = PairDC.extend(hcontentMixin).extend(
|
||||
/** @lends HistoryPairDatasetCollection.prototype */ {
|
||||
defaults: _.extend(_.clone(PairDC.prototype.defaults), {
|
||||
history_content_type: "dataset_collection",
|
||||
collection_type: "paired",
|
||||
model_class: "HistoryDatasetCollectionAssociation"
|
||||
}),
|
||||
|
||||
/** Override to post to contents route w/o id. */
|
||||
save: buildHDCASave(PairDC.prototype.save),
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `History${PairDC.prototype.toString.call(this)}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for List of Pairs Dataset Collection within a History. */
|
||||
var HistoryListPairedDatasetCollection = ListPairedDC.extend(hcontentMixin).extend({
|
||||
defaults: _.extend(_.clone(ListPairedDC.prototype.defaults), {
|
||||
history_content_type: "dataset_collection",
|
||||
collection_type: "list:paired",
|
||||
model_class: "HistoryDatasetCollectionAssociation"
|
||||
}),
|
||||
|
||||
/** Override to post to contents route w/o id. */
|
||||
save: buildHDCASave(ListPairedDC.prototype.save),
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `History${ListPairedDC.prototype.toString.call(this)}`;
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for List of Lists Dataset Collection within a History. */
|
||||
var HistoryListOfListsDatasetCollection = ListOfListsDC.extend(hcontentMixin).extend({
|
||||
defaults: _.extend(_.clone(ListOfListsDC.prototype.defaults), {
|
||||
history_content_type: "dataset_collection",
|
||||
collection_type: "list:list",
|
||||
model_class: "HistoryDatasetCollectionAssociation"
|
||||
}),
|
||||
|
||||
/** Override to post to contents route w/o id. */
|
||||
save: buildHDCASave(ListOfListsDC.prototype.save),
|
||||
|
||||
/** String representation. */
|
||||
toString: function() {
|
||||
return `HistoryListOfListsDatasetCollection(${this.get("name")})`;
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
HistoryListDatasetCollection: HistoryListDatasetCollection,
|
||||
HistoryPairDatasetCollection: HistoryPairDatasetCollection,
|
||||
HistoryListPairedDatasetCollection: HistoryListPairedDatasetCollection,
|
||||
HistoryListOfListsDatasetCollection: HistoryListOfListsDatasetCollection
|
||||
HistoryDatasetCollection: HistoryDatasetCollection
|
||||
};
|
||||
|
||||
@@ -2,6 +2,11 @@ import STATES from "mvc/dataset/states";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
|
||||
var collectionFuzzyCountDefault = 1000;
|
||||
try {
|
||||
collectionFuzzyCountDefault = localStorage.getItem("collectionFuzzyCountDefault") || collectionFuzzyCountDefault;
|
||||
} catch (err) {}
|
||||
|
||||
//==============================================================================
|
||||
/** @class Mixin for HistoryContents content (HDAs, HDCAs).
|
||||
*/
|
||||
@@ -58,6 +63,11 @@ var HistoryContentMixin = {
|
||||
var historyId = this.get("history_id");
|
||||
var historyContentId = this.get("id");
|
||||
var url = `${this.urlRoot}${historyId}/contents/${historyContentType}s/${historyContentId}`;
|
||||
if (historyContentType == "dataset_collection") {
|
||||
// Don't fetch whole collection - just enought to render outline. Backbone will
|
||||
// make a detailed request if any datasets are expanded beyond that point.
|
||||
url = `${url}?view=element-reference&fuzzy_count=${collectionFuzzyCountDefault}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@ import CONTROLLED_FETCH_COLLECTION from "mvc/base/controlled-fetch-collection";
|
||||
import HDA_MODEL from "mvc/history/hda-model";
|
||||
import HDCA_MODEL from "mvc/history/hdca-model";
|
||||
import HISTORY_PREFS from "mvc/history/history-preferences";
|
||||
import JOB_STATES_MODEL from "mvc/history/job-states-model";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import AJAX_QUEUE from "utils/ajax-queue";
|
||||
|
||||
var limitPerPageDefault = 500;
|
||||
try {
|
||||
limitPerPageDefault = localStorage.getItem("historyContentsLimitPerPageDefault") || limitPerPageDefault;
|
||||
} catch (err) {}
|
||||
|
||||
//==============================================================================
|
||||
var _super = CONTROLLED_FETCH_COLLECTION.PaginatedCollection;
|
||||
/** @class Backbone collection for history content.
|
||||
@@ -19,10 +25,10 @@ var HistoryContents = _super.extend(BASE_MVC.LoggableMixin).extend({
|
||||
_logNamespace: "history",
|
||||
|
||||
// ........................................................................ set up
|
||||
limitPerPage: 500,
|
||||
limitPerPage: limitPerPageDefault,
|
||||
|
||||
/** @type {Integer} how many contents per call to fetch when using progressivelyFetchDetails */
|
||||
limitPerProgressiveFetch: 500,
|
||||
limitPerProgressiveFetch: limitPerPageDefault,
|
||||
|
||||
/** @type {String} order used here and when fetching from server */
|
||||
order: "hid",
|
||||
@@ -37,6 +43,10 @@ var HistoryContents = _super.extend(BASE_MVC.LoggableMixin).extend({
|
||||
|
||||
/** Set up */
|
||||
initialize: function(models, options) {
|
||||
this.on({
|
||||
"sync add": this.trackJobStates
|
||||
});
|
||||
|
||||
options = options || {};
|
||||
_super.prototype.initialize.call(this, models, options);
|
||||
|
||||
@@ -53,46 +63,67 @@ var HistoryContents = _super.extend(BASE_MVC.LoggableMixin).extend({
|
||||
this.model.prototype.idAttribute = "type_id";
|
||||
},
|
||||
|
||||
trackJobStates: function() {
|
||||
this.each(historyContent => {
|
||||
if (historyContent.has("job_states_summary")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (historyContent.attributes.history_content_type === "dataset_collection") {
|
||||
var jobSourceType = historyContent.attributes.job_source_type;
|
||||
var jobSourceId = historyContent.attributes.job_source_id;
|
||||
if (jobSourceType) {
|
||||
this.jobStateSummariesCollection.add({
|
||||
id: jobSourceId,
|
||||
model: jobSourceType,
|
||||
history_id: this.history_id,
|
||||
collection_id: historyContent.attributes.id
|
||||
});
|
||||
var jobStatesSummary = this.jobStateSummariesCollection.get(jobSourceId);
|
||||
historyContent.jobStatesSummary = jobStatesSummary;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ........................................................................ composite collection
|
||||
/** since history content is a mix, override model fn into a factory, creating based on history_content_type */
|
||||
model: function(attrs, options) {
|
||||
if (attrs.history_content_type === "dataset") {
|
||||
return new HDA_MODEL.HistoryDatasetAssociation(attrs, options);
|
||||
} else if (attrs.history_content_type === "dataset_collection") {
|
||||
switch (attrs.collection_type) {
|
||||
case "list":
|
||||
return new HDCA_MODEL.HistoryListDatasetCollection(attrs, options);
|
||||
case "paired":
|
||||
return new HDCA_MODEL.HistoryPairDatasetCollection(attrs, options);
|
||||
case "list:paired":
|
||||
return new HDCA_MODEL.HistoryListPairedDatasetCollection(attrs, options);
|
||||
case "list:list":
|
||||
return new HDCA_MODEL.HistoryListOfListsDatasetCollection(attrs, options);
|
||||
}
|
||||
// This is a hack inside a hack:
|
||||
// Raise a plain object with validationError to fake a model.validationError
|
||||
// (since we don't have a model to use validate with)
|
||||
// (the outer hack being the mixed content/model function in this collection)
|
||||
var msg = `Unknown collection_type: ${attrs.collection_type}`;
|
||||
console.warn(msg, attrs);
|
||||
return { validationError: msg };
|
||||
return new HDCA_MODEL.HistoryDatasetCollection(attrs, options);
|
||||
} else {
|
||||
return {
|
||||
validationError: `Unknown history_content_type: ${attrs.history_content_type}`
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
stopPolling: function() {
|
||||
if (this.jobStateSummariesCollection) {
|
||||
this.jobStateSummariesCollection.active = false;
|
||||
this.jobStateSummariesCollection.clearUpdateTimeout();
|
||||
}
|
||||
return {
|
||||
validationError: `Unknown history_content_type: ${attrs.history_content_type}`
|
||||
};
|
||||
},
|
||||
|
||||
setHistoryId: function(newId) {
|
||||
this.stopPolling();
|
||||
this.historyId = newId;
|
||||
this._setUpWebStorage();
|
||||
if (newId) {
|
||||
// If actually reflecting a history - setup storage and monitor jobs.
|
||||
|
||||
this._setUpWebStorage();
|
||||
|
||||
this.jobStateSummariesCollection = new JOB_STATES_MODEL.JobStatesSummaryCollection();
|
||||
this.jobStateSummariesCollection.historyId = newId;
|
||||
this.jobStateSummariesCollection.monitor();
|
||||
}
|
||||
},
|
||||
|
||||
/** Set up client side storage. Currently PersistanStorage keyed under 'history:<id>' */
|
||||
_setUpWebStorage: function(initialSettings) {
|
||||
// TODO: use initialSettings
|
||||
if (!this.historyId) {
|
||||
return;
|
||||
}
|
||||
this.storage = new HISTORY_PREFS.HistoryPrefs({
|
||||
id: HISTORY_PREFS.HistoryPrefs.historyStorageKey(this.historyId)
|
||||
});
|
||||
@@ -303,17 +334,6 @@ var HistoryContents = _super.extend(BASE_MVC.LoggableMixin).extend({
|
||||
return this.fetch(options);
|
||||
},
|
||||
|
||||
/** specialty fetch method for retrieving the element_counts of all hdcas in the history */
|
||||
fetchCollectionCounts: function(options) {
|
||||
options = options || {};
|
||||
options.keys = ["type_id", "element_count"].join(",");
|
||||
options.filters = _.extend(options.filters || {}, {
|
||||
history_content_type: "dataset_collection"
|
||||
});
|
||||
options.remove = false;
|
||||
return this.fetch(options);
|
||||
},
|
||||
|
||||
// ............. quasi-batch ops
|
||||
// TODO: to batch
|
||||
/** helper that fetches using filterParams then calls save on each fetched using updateWhat as the save params */
|
||||
|
||||
@@ -237,6 +237,13 @@ var History = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend(
|
||||
}
|
||||
},
|
||||
|
||||
stopPolling: function() {
|
||||
this.clearUpdateTimeout();
|
||||
if (this.contents) {
|
||||
this.contents.stopPolling();
|
||||
}
|
||||
},
|
||||
|
||||
// ........................................................................ ajax
|
||||
/** override to use actual Dates objects for create/update times */
|
||||
parse: function(response, options) {
|
||||
|
||||
@@ -47,9 +47,6 @@ var HistoryView = _super.extend(
|
||||
/** string used for search placeholder */
|
||||
searchPlaceholder: _l("search datasets"),
|
||||
|
||||
/** @type {Number} ms to wait after history load to fetch/decorate hdcas with element_count */
|
||||
FETCH_COLLECTION_COUNTS_DELAY: 2000,
|
||||
|
||||
// ......................................................................... SET UP
|
||||
/** Set up the view, bind listeners.
|
||||
* @param {Object} attributes optional settings for the panel
|
||||
@@ -60,9 +57,6 @@ var HistoryView = _super.extend(
|
||||
// control contents/behavior based on where (and in what context) the panel is being used
|
||||
/** where should pages from links be displayed? (default to new tab/window) */
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
|
||||
/** timeout id for detailed fetch of collection counts, etc... */
|
||||
this.detailedFetchTimeoutId = null;
|
||||
},
|
||||
|
||||
/** create and return a collection for when none is initially passed */
|
||||
@@ -77,20 +71,11 @@ var HistoryView = _super.extend(
|
||||
freeModel: function() {
|
||||
_super.prototype.freeModel.call(this);
|
||||
if (this.model) {
|
||||
this.model.clearUpdateTimeout();
|
||||
this.model.stopPolling();
|
||||
}
|
||||
this._clearDetailedFetchTimeout();
|
||||
return this;
|
||||
},
|
||||
|
||||
/** clear the timeout and the cached timeout id */
|
||||
_clearDetailedFetchTimeout: function() {
|
||||
if (this.detailedFetchTimeoutId) {
|
||||
clearTimeout(this.detailedFetchTimeoutId);
|
||||
this.detailedFetchTimeoutId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/** create any event listeners for the panel
|
||||
* @fires: rendered:initial on the first render
|
||||
* @fires: empty-history when switching to a history with no contents or creating a new history
|
||||
@@ -101,13 +86,6 @@ var HistoryView = _super.extend(
|
||||
error: function(model, xhr, options, msg, details) {
|
||||
this.errorHandler(model, xhr, options, msg, details);
|
||||
},
|
||||
"loading-done": () => {
|
||||
// after the initial load, decorate with more time consuming fields (like HDCA element_counts)
|
||||
this.detailedFetchTimeoutId = _.delay(() => {
|
||||
this.detailedFetchTimeoutId = null;
|
||||
this.model.contents.fetchCollectionCounts();
|
||||
}, this.FETCH_COLLECTION_COUNTS_DELAY);
|
||||
},
|
||||
"views:ready view:attached view:removed": function(view) {
|
||||
this._renderSelectButton();
|
||||
},
|
||||
@@ -378,17 +356,17 @@ var HistoryView = _super.extend(
|
||||
}),
|
||||
|
||||
_clickPrevPage: function(ev) {
|
||||
this.model.clearUpdateTimeout();
|
||||
this.model.stopPolling();
|
||||
this.model.contents.fetchPrevPage();
|
||||
},
|
||||
|
||||
_clickNextPage: function(ev) {
|
||||
this.model.clearUpdateTimeout();
|
||||
this.model.stopPolling();
|
||||
this.model.contents.fetchNextPage();
|
||||
},
|
||||
|
||||
_changePageSelect: function(ev) {
|
||||
this.model.clearUpdateTimeout();
|
||||
this.model.stopPolling();
|
||||
var page = $(ev.currentTarget).val();
|
||||
this.model.contents.fetchPage(page);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import * as Backbone from "libs/backbone";
|
||||
import AJAX_QUEUE from "utils/ajax-queue";
|
||||
|
||||
/** ms between fetches when checking running jobs/datasets for updates */
|
||||
var UPDATE_DELAY = 2000;
|
||||
var NON_TERMINAL_STATES = ["new", "queued", "running"];
|
||||
var ERROR_STATES = ["error", "deleted"];
|
||||
/** Fetch state on add or just wait for polling to start. */
|
||||
var FETCH_STATE_ON_ADD = false;
|
||||
var BATCH_FETCH_STATE = true;
|
||||
|
||||
var JobStatesSummary = Backbone.Model.extend({
|
||||
url: function() {
|
||||
return `${Galaxy.root}api/histories/${this.attributes.history_id}/contents/dataset_collections/${
|
||||
this.attributes.collection_id
|
||||
}/jobs_summary`;
|
||||
},
|
||||
|
||||
hasDetails: function() {
|
||||
return this.has("populated_state");
|
||||
},
|
||||
|
||||
new: function() {
|
||||
return !this.hasDetails() || this.get("populated_state") == "new";
|
||||
},
|
||||
|
||||
errored: function() {
|
||||
return this.get("populated_state") === "error" || this.anyWithStates(ERROR_STATES);
|
||||
},
|
||||
|
||||
states: function() {
|
||||
return this.get("states") || {};
|
||||
},
|
||||
|
||||
anyWithState: function(queryState) {
|
||||
return (this.states()[queryState] || 0) > 0;
|
||||
},
|
||||
|
||||
anyWithStates: function(queryStates) {
|
||||
var states = this.states();
|
||||
for (var index in queryStates) {
|
||||
if ((states[queryStates[index]] || 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
numWithStates: function(queryStates) {
|
||||
var states = this.states();
|
||||
var count = 0;
|
||||
for (var index in queryStates) {
|
||||
count += states[queryStates[index]] || 0;
|
||||
}
|
||||
return count;
|
||||
},
|
||||
|
||||
numInError: function() {
|
||||
return this.numWithStates(ERROR_STATES);
|
||||
},
|
||||
|
||||
running: function() {
|
||||
return this.anyWithState("running");
|
||||
},
|
||||
|
||||
terminal: function() {
|
||||
if (this.new()) {
|
||||
return false;
|
||||
} else {
|
||||
var anyNonTerminal = this.anyWithStates(NON_TERMINAL_STATES);
|
||||
return !anyNonTerminal;
|
||||
}
|
||||
},
|
||||
|
||||
jobCount: function() {
|
||||
var states = this.states();
|
||||
var count = 0;
|
||||
for (var index in states) {
|
||||
count += states[index];
|
||||
}
|
||||
return count;
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return `JobStatesSummary(id=${this.get("id")})`;
|
||||
}
|
||||
});
|
||||
|
||||
var JobStatesSummaryCollection = Backbone.Collection.extend({
|
||||
model: JobStatesSummary,
|
||||
|
||||
initialize: function() {
|
||||
if (FETCH_STATE_ON_ADD) {
|
||||
this.on({
|
||||
add: model => model.fetch()
|
||||
});
|
||||
}
|
||||
|
||||
/** cached timeout id for the dataset updater */
|
||||
this.updateTimeoutId = null;
|
||||
// this.checkForUpdates();
|
||||
this.active = true;
|
||||
},
|
||||
|
||||
url: function() {
|
||||
var nonTerminalModels = this.models.filter(model => {
|
||||
return !model.terminal();
|
||||
});
|
||||
var ids = nonTerminalModels
|
||||
.map(summary => {
|
||||
return summary.get("id");
|
||||
})
|
||||
.join(",");
|
||||
var types = nonTerminalModels
|
||||
.map(summary => {
|
||||
return summary.get("model");
|
||||
})
|
||||
.join(",");
|
||||
return `${Galaxy.root}api/histories/${this.historyId}/jobs_summary?ids=${ids}&types=${types}`;
|
||||
},
|
||||
|
||||
monitor: function() {
|
||||
this.clearUpdateTimeout();
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var _delayThenMonitorAgain = () => {
|
||||
this.updateTimeoutId = setTimeout(() => {
|
||||
this.monitor();
|
||||
}, UPDATE_DELAY);
|
||||
};
|
||||
|
||||
var nonTerminalModels = this.models.filter(model => {
|
||||
return !model.terminal();
|
||||
});
|
||||
|
||||
if (nonTerminalModels.length > 0 && !BATCH_FETCH_STATE) {
|
||||
// Allow models to fetch their own details.
|
||||
var updateFunctions = nonTerminalModels.map(summary => {
|
||||
return () => {
|
||||
return summary.fetch();
|
||||
};
|
||||
});
|
||||
|
||||
return new AJAX_QUEUE.AjaxQueue(updateFunctions).done(_delayThenMonitorAgain);
|
||||
} else if (nonTerminalModels.length > 0) {
|
||||
// Batch fetch updated state...
|
||||
this.fetch({ remove: false }).done(_delayThenMonitorAgain);
|
||||
} else {
|
||||
_delayThenMonitorAgain();
|
||||
}
|
||||
},
|
||||
|
||||
/** clear the timeout and the cached timeout id */
|
||||
clearUpdateTimeout: function() {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
this.updateTimeoutId = null;
|
||||
}
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return `JobStatesSummaryCollection()`;
|
||||
}
|
||||
});
|
||||
|
||||
export default { JobStatesSummary, JobStatesSummaryCollection, FETCH_STATE_ON_ADD };
|
||||
@@ -1,6 +1,7 @@
|
||||
import _l from "utils/localization";
|
||||
import HISTORY_MODEL from "mvc/history/history-model";
|
||||
import HISTORY_VIEW_EDIT from "mvc/history/history-view-edit";
|
||||
import JOB_STATES_MODEL from "mvc/history/job-states-model";
|
||||
import historyCopyDialog from "mvc/history/copy-dialog";
|
||||
import ERROR_MODAL from "mvc/ui/error-modal";
|
||||
import baseMVC from "mvc/base-mvc";
|
||||
@@ -733,9 +734,16 @@ var MultiPanelColumns = Backbone.View.extend(baseMVC.LoggableMixin).extend({
|
||||
this.hdaQueue.add({
|
||||
name: column.model.id,
|
||||
fn: function() {
|
||||
return contents.fetchCurrentPage(fetchOptions).done(() => {
|
||||
column.panel.renderItems();
|
||||
});
|
||||
return contents
|
||||
.fetchCurrentPage(fetchOptions)
|
||||
.done(() => {
|
||||
column.panel.renderItems();
|
||||
})
|
||||
.done(() => {
|
||||
if (!JOB_STATES_MODEL.FETCH_STATE_ON_ADD) {
|
||||
contents.jobStateSummariesCollection.fetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// the queue is re-used, so if it's not processing requests - start it again
|
||||
|
||||
@@ -5,7 +5,7 @@ import Utils from "utils/utils";
|
||||
import Deferred from "utils/deferred";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
import FormBase from "mvc/form/form-view";
|
||||
import Citations from "components/citations.vue";
|
||||
import Citations from "components/Citations.vue";
|
||||
import Vue from "libs/vue";
|
||||
export default FormBase.extend({
|
||||
initialize: function(options) {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// Load all qunit tests into a single bundle.
|
||||
var testsContext = require.context(".", true, /_tests$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
//var testsContext = require.context(".", true, /_tests$/);
|
||||
//testsContext.keys().forEach(testsContext);
|
||||
|
||||
import gab from "qunit/tests/galaxy_app_base_tests";
|
||||
import ml from "qunit/tests/metrics_logger_tests";
|
||||
// form_tests seems to need to come before something - maybe multiple things
|
||||
import form from "qunit/tests/form_tests";
|
||||
import l from "qunit/tests/list_of_pairs_collection_creator_tests";
|
||||
import upd from "qunit/tests/upload_dialog_tests";
|
||||
import we from "qunit/tests/workflow_editor_tests";
|
||||
import mast from "qunit/tests/masthead_tests";
|
||||
import graph from "qunit/tests/graph_tests";
|
||||
import jd from "qunit/tests/job_dag_tests";
|
||||
import hc from "qunit/tests/history_contents_model_tests";
|
||||
import hda from "qunit/tests/hda_base_tests";
|
||||
import modal from "qunit/tests/modal_tests";
|
||||
import page from "qunit/tests/page_tests";
|
||||
import popover from "qunit/tests/popover_tests";
|
||||
import utils from "qunit/tests/utils_tests";
|
||||
import ui from "qunit/tests/ui_tests";
|
||||
|
||||
@@ -1,182 +1,167 @@
|
||||
/* global define */
|
||||
define(
|
||||
[
|
||||
"qunit/test-app",
|
||||
"sinon",
|
||||
"mvc/form/form-input",
|
||||
"mvc/ui/ui-misc",
|
||||
"mvc/form/form-data",
|
||||
"mvc/tool/tool-form",
|
||||
"utils/utils"
|
||||
],
|
||||
function(testApp, sinon, InputElement, Ui, FormData, ToolForm, Utils) {
|
||||
"use strict";
|
||||
InputElement = InputElement.default;
|
||||
Ui = Ui.default;
|
||||
FormData = FormData.default;
|
||||
ToolForm = ToolForm.default;
|
||||
Utils = Utils.default;
|
||||
QUnit.module("Form test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.clock = sinon.useFakeTimers();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
this.clock.restore();
|
||||
}
|
||||
});
|
||||
import testApp from "qunit/test-app";
|
||||
import InputElement from "mvc/form/form-input";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
import FormData from "mvc/form/form-data";
|
||||
import ToolForm from "mvc/tool/tool-form";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
QUnit.test("tool-form", function(assert) {
|
||||
var toolform = new ToolForm.View({ id: "test" });
|
||||
var form = toolform.form;
|
||||
$("body").prepend(toolform.$el);
|
||||
window.fakeserver.respond();
|
||||
var output = "";
|
||||
for (var property in assert) {
|
||||
output += property + ": ; ";
|
||||
}
|
||||
assert.ok(true, "MooCow");
|
||||
assert.ok(
|
||||
form.$(".portlet-title-text").html() == "<b>_name</b> _description (Galaxy Version _version)",
|
||||
"Title correct"
|
||||
);
|
||||
var tour_ids = [];
|
||||
$("[tour_id]").each(function() {
|
||||
tour_ids.push($(this).attr("tour_id"));
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(tour_ids) == '["a","b|c","b|i","b|j","k_0|l","k_0|m|n","k_0|m|s","k_0|m|t"]',
|
||||
"Tour ids correct"
|
||||
);
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"r","k_0|m|s":"s","k_0|m|t":"t"}',
|
||||
"Created data correct"
|
||||
);
|
||||
var mapped_ids = [];
|
||||
form.data.matchModel(form.model.attributes, function(input, id) {
|
||||
mapped_ids.push($("#" + id).attr("tour_id"));
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(mapped_ids) == '["a","b|c","b|i","b|j","k_0|l","k_0|m|n","k_0|m|s","k_0|m|t"]',
|
||||
"Remapped tour ids correct"
|
||||
);
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
var dropdown = form.$("#menu > .dropdown-menu");
|
||||
assert.ok(dropdown.children().length == 2, "Found two menu items");
|
||||
dropdown
|
||||
.find(".fa-info-circle")
|
||||
.parent()
|
||||
.click();
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
assert.ok(
|
||||
form.$(".ui-message").html() ===
|
||||
'<span>This tool requires req_name_a (Version req_version_a) and req_name_b (Version req_version_b). Click <a target="_blank" href="https://galaxyproject.org/tools/requirements/">here</a> for more information.</span>',
|
||||
"Check requirements message"
|
||||
);
|
||||
assert.ok(form.$(".form-repeat-delete").css("display") == "none", "Delete button disabled");
|
||||
var $add = form.$(".form-repeat-add");
|
||||
assert.ok(!$add.attr("disabled"), "Adding new repeat possible");
|
||||
$add.click();
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
assert.ok($add.attr("disabled"), "Adding new repeat has been disabled");
|
||||
form.$(".form-repeat-delete").each(function(i, d) {
|
||||
assert.ok($(d).css("display") == "block", "Delete buttons " + i + " enabled");
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"r","k_0|m|s":"s","k_0|m|t":"t","k_1|l":"l","k_1|m|n":"o","k_1|m|p":"p","k_1|m|q":"q"}',
|
||||
"Created data correct, after adding repeat"
|
||||
);
|
||||
form.$(".form-repeat-delete:first").click();
|
||||
assert.ok(form.$(".form-repeat-delete").css("display") == "none", "Delete button disabled");
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"o","k_0|m|p":"p","k_0|m|q":"q"}',
|
||||
"Created data correct, after removing first repeat"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("data", function(assert) {
|
||||
var visits = [];
|
||||
Utils.get({
|
||||
url: Galaxy.root + "api/tools/test/build",
|
||||
success: function(response) {
|
||||
FormData.visitInputs(response.inputs, function(node, name, context) {
|
||||
visits.push({ name: name, node: node });
|
||||
});
|
||||
}
|
||||
});
|
||||
window.fakeserver.respond();
|
||||
assert.ok(
|
||||
JSON.stringify(visits) ==
|
||||
'[{"name":"a","node":{"name":"a","type":"text"}},{"name":"b|c","node":{"name":"c","type":"select","value":"h","options":[["d","d",false],["h","h",false]]}},{"name":"b|i","node":{"name":"i","type":"text","value":"i"}},{"name":"b|j","node":{"name":"j","type":"text","value":"j"}},{"name":"k_0|l","node":{"name":"l","type":"text","value":"l"}},{"name":"k_0|m|n","node":{"name":"n","type":"select","value":"r","options":[["o","o",false],["r","r",false]]}},{"name":"k_0|m|s","node":{"name":"s","type":"text","value":"s"}},{"name":"k_0|m|t","node":{"name":"t","type":"text","value":"t"}}]',
|
||||
"Testing value visitor"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("input", function(assert) {
|
||||
var input = new InputElement(
|
||||
{},
|
||||
{
|
||||
field: new Ui.Input({})
|
||||
}
|
||||
);
|
||||
$("body").prepend(input.$el);
|
||||
assert.ok(input.$field.css("display") == "block", "Input field shown");
|
||||
assert.ok(input.$preview.css("display") == "none", "Preview hidden");
|
||||
assert.ok(input.$collapsible.css("display") == "none", "Collapsible hidden");
|
||||
assert.ok(input.$title_text.css("display") == "inline", "Title visible");
|
||||
assert.ok(input.$title_text.html() == "", "Title content unavailable");
|
||||
input.model.set("label", "_label");
|
||||
assert.ok(input.$title_text.html() == "_label", "Title content available");
|
||||
assert.ok(input.$error.css("display") == "none", "Error hidden");
|
||||
input.model.set("error_text", "_error_text");
|
||||
assert.ok(input.$error.css("display") == "block", "Error visible");
|
||||
assert.ok(input.$error_text.html() == "_error_text", "Error text correct");
|
||||
input.model.set("error_text", null);
|
||||
assert.ok(input.$error.css("display") == "none", "Error hidden, again");
|
||||
assert.ok(input.$backdrop.css("display") == "none", "Backdrop hidden");
|
||||
input.model.set("backdrop", true);
|
||||
assert.ok(input.$backdrop.css("display") == "block", "Backdrop shown");
|
||||
assert.ok(input.$backdrop.css("opacity") == 0, "Backdrop transparent");
|
||||
assert.ok(input.$backdrop.css("cursor") == "default", "Backdrop regular cursor");
|
||||
input.model.set("backdrop", false);
|
||||
assert.ok(input.$backdrop.css("display") == "none", "Backdrop hidden, again");
|
||||
input.model.set("disabled", true);
|
||||
assert.ok(input.$field.css("display") == "none", "Input field hidden");
|
||||
input.model.set("disabled", false);
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
assert.ok(input.$field.css("display") == "block", "Input field shown, again");
|
||||
this.clock.tick(window.WAIT_FADE);
|
||||
input.model.set("color", "red");
|
||||
assert.ok(
|
||||
input.$field
|
||||
.children()
|
||||
.first()
|
||||
.css("color") == "rgb(255, 0, 0)",
|
||||
"Shows correct new color"
|
||||
);
|
||||
input.model.set("color", null);
|
||||
assert.ok(
|
||||
input.$field
|
||||
.children()
|
||||
.first()
|
||||
.css("color") == "rgb(85, 85, 85)",
|
||||
"Shows correct old color"
|
||||
);
|
||||
input.model.set("collapsible_value", "_collapsible_value");
|
||||
assert.ok(input.$collapsible.css("display") == "block", "Collapsible field");
|
||||
assert.ok(input.$collapsible_text.html() == "_label", "Title content available");
|
||||
assert.ok(input.$title_text.css("display") == "none", "Regular title not visible");
|
||||
input.model.set("help", "_help");
|
||||
assert.ok(input.$info.html() == "_help", "Correct help text");
|
||||
input.model.set("argument", "_argument");
|
||||
assert.ok(input.$info.html() == "_help (_argument)", "Correct help text with argument");
|
||||
input.model.set("help", "_help (_argument)");
|
||||
assert.ok(input.$info.html() == "_help (_argument)", "Correct help text with argument from help");
|
||||
});
|
||||
QUnit.module("Form test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
$.fx.off = true;
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
$.fx.off = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("tool-form", function(assert) {
|
||||
// Huh? The following seems to be needed by tool-form.js - once the global usage
|
||||
// is cleaned up in that module this can be deleted I assume.
|
||||
window.parent.Galaxy = window.Galaxy;
|
||||
|
||||
var toolform = new ToolForm.View({ id: "test" });
|
||||
var form = toolform.form;
|
||||
$("body").prepend(toolform.$el);
|
||||
window.fakeserver.respond();
|
||||
var output = "";
|
||||
for (var property in assert) {
|
||||
output += property + ": ; ";
|
||||
}
|
||||
assert.ok(
|
||||
form.$(".portlet-title-text").html() == "<b>_name</b> _description (Galaxy Version _version)",
|
||||
"Title correct"
|
||||
);
|
||||
var tour_ids = [];
|
||||
$("[tour_id]").each(function() {
|
||||
tour_ids.push($(this).attr("tour_id"));
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(tour_ids) == '["a","b|c","b|i","b|j","k_0|l","k_0|m|n","k_0|m|s","k_0|m|t"]',
|
||||
"Tour ids correct"
|
||||
);
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"r","k_0|m|s":"s","k_0|m|t":"t"}',
|
||||
"Created data correct"
|
||||
);
|
||||
var mapped_ids = [];
|
||||
form.data.matchModel(form.model.attributes, function(input, id) {
|
||||
mapped_ids.push($("#" + id).attr("tour_id"));
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(mapped_ids) == '["a","b|c","b|i","b|j","k_0|l","k_0|m|n","k_0|m|s","k_0|m|t"]',
|
||||
"Remapped tour ids correct"
|
||||
);
|
||||
var dropdown = form.$("#menu > .dropdown-menu");
|
||||
assert.ok(dropdown.children().length == 2, "Found two menu items");
|
||||
dropdown
|
||||
.find(".fa-info-circle")
|
||||
.parent()
|
||||
.click();
|
||||
assert.ok(
|
||||
form.$(".ui-message").html() ===
|
||||
'<span>This tool requires req_name_a (Version req_version_a) and req_name_b (Version req_version_b). Click <a target="_blank" href="https://galaxyproject.org/tools/requirements/">here</a> for more information.</span>',
|
||||
"Check requirements message"
|
||||
);
|
||||
assert.ok(form.$(".form-repeat-delete").css("display") == "none", "Delete button disabled");
|
||||
var $add = form.$(".form-repeat-add");
|
||||
assert.ok(!$add.attr("disabled"), "Adding new repeat possible");
|
||||
$add.click();
|
||||
assert.ok($add.attr("disabled"), "Adding new repeat has been disabled");
|
||||
form.$(".form-repeat-delete").each(function(i, d) {
|
||||
assert.ok($(d).css("display") == "block", "Delete buttons " + i + " enabled");
|
||||
});
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"r","k_0|m|s":"s","k_0|m|t":"t","k_1|l":"l","k_1|m|n":"o","k_1|m|p":"p","k_1|m|q":"q"}',
|
||||
"Created data correct, after adding repeat"
|
||||
);
|
||||
form.$(".form-repeat-delete:first").click();
|
||||
assert.ok(form.$(".form-repeat-delete").css("display") == "none", "Delete button disabled");
|
||||
assert.ok(
|
||||
JSON.stringify(form.data.create()) ==
|
||||
'{"a":"","b|c":"h","b|i":"i","b|j":"j","k_0|l":"l","k_0|m|n":"o","k_0|m|p":"p","k_0|m|q":"q"}',
|
||||
"Created data correct, after removing first repeat"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("data", function(assert) {
|
||||
var visits = [];
|
||||
Utils.get({
|
||||
url: Galaxy.root + "api/tools/test/build",
|
||||
success: function(response) {
|
||||
FormData.visitInputs(response.inputs, function(node, name, context) {
|
||||
visits.push({ name: name, node: node });
|
||||
});
|
||||
}
|
||||
});
|
||||
window.fakeserver.respond();
|
||||
assert.ok(
|
||||
JSON.stringify(visits) ==
|
||||
'[{"name":"a","node":{"name":"a","type":"text"}},{"name":"b|c","node":{"name":"c","type":"select","value":"h","options":[["d","d",false],["h","h",false]]}},{"name":"b|i","node":{"name":"i","type":"text","value":"i"}},{"name":"b|j","node":{"name":"j","type":"text","value":"j"}},{"name":"k_0|l","node":{"name":"l","type":"text","value":"l"}},{"name":"k_0|m|n","node":{"name":"n","type":"select","value":"r","options":[["o","o",false],["r","r",false]]}},{"name":"k_0|m|s","node":{"name":"s","type":"text","value":"s"}},{"name":"k_0|m|t","node":{"name":"t","type":"text","value":"t"}}]',
|
||||
"Testing value visitor"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("input", function(assert) {
|
||||
var input = new InputElement(
|
||||
{},
|
||||
{
|
||||
field: new Ui.Input({})
|
||||
}
|
||||
);
|
||||
$("body").prepend(input.$el);
|
||||
assert.ok(input.$field.css("display") == "block", "Input field shown");
|
||||
assert.ok(input.$preview.css("display") == "none", "Preview hidden");
|
||||
assert.ok(input.$collapsible.css("display") == "none", "Collapsible hidden");
|
||||
assert.ok(input.$title_text.css("display") == "inline", "Title visible");
|
||||
assert.ok(input.$title_text.html() == "", "Title content unavailable");
|
||||
input.model.set("label", "_label");
|
||||
assert.ok(input.$title_text.html() == "_label", "Title content available");
|
||||
assert.ok(input.$error.css("display") == "none", "Error hidden");
|
||||
input.model.set("error_text", "_error_text");
|
||||
assert.ok(input.$error.css("display") == "block", "Error visible");
|
||||
assert.ok(input.$error_text.html() == "_error_text", "Error text correct");
|
||||
input.model.set("error_text", null);
|
||||
assert.ok(input.$error.css("display") == "none", "Error hidden, again");
|
||||
assert.ok(input.$backdrop.css("display") == "none", "Backdrop hidden");
|
||||
input.model.set("backdrop", true);
|
||||
assert.ok(input.$backdrop.css("display") == "block", "Backdrop shown");
|
||||
assert.ok(input.$backdrop.css("opacity") == 0, "Backdrop transparent");
|
||||
assert.ok(input.$backdrop.css("cursor") == "default", "Backdrop regular cursor");
|
||||
input.model.set("backdrop", false);
|
||||
assert.ok(input.$backdrop.css("display") == "none", "Backdrop hidden, again");
|
||||
input.model.set("disabled", true);
|
||||
assert.ok(input.$field.css("display") == "none", "Input field hidden");
|
||||
input.model.set("disabled", false);
|
||||
assert.ok(input.$field.css("display") == "block", "Input field shown, again");
|
||||
input.model.set("color", "red");
|
||||
assert.ok(
|
||||
input.$field
|
||||
.children()
|
||||
.first()
|
||||
.css("color") == "rgb(255, 0, 0)",
|
||||
"Shows correct new color"
|
||||
);
|
||||
input.model.set("color", null);
|
||||
assert.ok(
|
||||
input.$field
|
||||
.children()
|
||||
.first()
|
||||
.css("color") == "rgb(85, 85, 85)",
|
||||
"Shows correct old color"
|
||||
);
|
||||
input.model.set("collapsible_value", "_collapsible_value");
|
||||
assert.ok(input.$collapsible.css("display") == "block", "Collapsible field");
|
||||
assert.ok(input.$collapsible_text.html() == "_label", "Title content available");
|
||||
assert.ok(input.$title_text.css("display") == "none", "Regular title not visible");
|
||||
input.model.set("help", "_help");
|
||||
assert.ok(input.$info.html() == "_help", "Correct help text");
|
||||
input.model.set("argument", "_argument");
|
||||
assert.ok(input.$info.html() == "_help (_argument)", "Correct help text with argument");
|
||||
input.model.set("help", "_help (_argument)");
|
||||
assert.ok(input.$info.html() == "_help (_argument)", "Correct help text with argument from help");
|
||||
});
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "galaxy", "jquery"], function(testApp, appBase, $) {
|
||||
"use strict";
|
||||
appBase = appBase.default;
|
||||
QUnit.module("Galaxy client app tests");
|
||||
|
||||
var options = {
|
||||
config: {
|
||||
allow_user_deletion: false,
|
||||
allow_user_creation: true,
|
||||
wiki_url: "https://galaxyproject.org/",
|
||||
ftp_upload_site: null,
|
||||
support_url: "https://galaxyproject.org/support/",
|
||||
allow_user_dataset_purge: false,
|
||||
allow_library_path_paste: false,
|
||||
user_library_import_dir: null,
|
||||
terms_url: null,
|
||||
ftp_upload_dir: null,
|
||||
library_import_dir: null,
|
||||
logo_url: null,
|
||||
enable_unique_workflow_defaults: false
|
||||
},
|
||||
user: {
|
||||
username: "test",
|
||||
quota_percent: null,
|
||||
total_disk_usage: 61815527,
|
||||
nice_total_disk_usage: "59.0 MB",
|
||||
email: "test@test.test",
|
||||
tags_used: ["test"],
|
||||
model_class: "User",
|
||||
id: "f2db41e1fa331b3e"
|
||||
}
|
||||
};
|
||||
|
||||
QUnit.test("App base construction/initializiation defaults", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("options") && typeof app.options === "object");
|
||||
assert.ok(app.hasOwnProperty("logger") && typeof app.logger === "object");
|
||||
assert.ok(app.hasOwnProperty("localize") && typeof app.localize === "function");
|
||||
assert.ok(app.hasOwnProperty("config") && typeof app.config === "object");
|
||||
assert.ok(app.hasOwnProperty("user") && typeof app.config === "object");
|
||||
assert.equal(app.localize, window._l);
|
||||
});
|
||||
|
||||
QUnit.test("App base default options", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("options") && typeof app.options === "object");
|
||||
assert.equal(app.options.root, "/");
|
||||
assert.equal(app.options.patchExisting, true);
|
||||
});
|
||||
|
||||
QUnit.test("App base extends from Backbone.Events", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
["on", "off", "trigger", "listenTo", "stopListening"].forEach(function(fn) {
|
||||
assert.ok(app.hasOwnProperty(fn) && typeof app[fn] === "function");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("App base has logging methods from utils/add-logging.js", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
["debug", "info", "warn", "error", "metric"].forEach(function(fn) {
|
||||
assert.ok(typeof app[fn] === "function");
|
||||
});
|
||||
assert.ok(app._logNamespace === "GalaxyApp");
|
||||
});
|
||||
|
||||
QUnit.test("App base will patch in attributes from existing Galaxy objects", function(assert) {
|
||||
window.Galaxy = {
|
||||
attribute: {
|
||||
subattr: 1
|
||||
}
|
||||
};
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(typeof app.attribute === "object" && app.attribute.subattr === 1);
|
||||
});
|
||||
|
||||
QUnit.test("App base logger", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("logger") && typeof app.config === "object");
|
||||
});
|
||||
|
||||
QUnit.test("App base config", function(assert) {
|
||||
var app = new appBase.GalaxyApp(options);
|
||||
assert.ok(app.hasOwnProperty("config") && typeof app.config === "object");
|
||||
assert.equal(app.config.allow_user_deletion, false);
|
||||
assert.equal(app.config.allow_user_creation, true);
|
||||
assert.equal(app.config.wiki_url, "https://galaxyproject.org/");
|
||||
assert.equal(app.config.ftp_upload_site, null);
|
||||
});
|
||||
|
||||
QUnit.test("App base user", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("user") && typeof app.user === "object");
|
||||
assert.ok(app.user.isAdmin() === false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import appBase from "galaxy";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.module("Galaxy client app tests");
|
||||
|
||||
var options = {
|
||||
config: {
|
||||
allow_user_deletion: false,
|
||||
allow_user_creation: true,
|
||||
wiki_url: "https://galaxyproject.org/",
|
||||
ftp_upload_site: null,
|
||||
support_url: "https://galaxyproject.org/support/",
|
||||
allow_user_dataset_purge: false,
|
||||
allow_library_path_paste: false,
|
||||
user_library_import_dir: null,
|
||||
terms_url: null,
|
||||
ftp_upload_dir: null,
|
||||
library_import_dir: null,
|
||||
logo_url: null,
|
||||
enable_unique_workflow_defaults: false
|
||||
},
|
||||
user: {
|
||||
username: "test",
|
||||
quota_percent: null,
|
||||
total_disk_usage: 61815527,
|
||||
nice_total_disk_usage: "59.0 MB",
|
||||
email: "test@test.test",
|
||||
tags_used: ["test"],
|
||||
model_class: "User",
|
||||
id: "f2db41e1fa331b3e"
|
||||
}
|
||||
};
|
||||
|
||||
QUnit.test("App base construction/initializiation defaults", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("options") && typeof app.options === "object");
|
||||
assert.ok(app.hasOwnProperty("logger") && typeof app.logger === "object");
|
||||
assert.ok(app.hasOwnProperty("localize") && typeof app.localize === "function");
|
||||
assert.ok(app.hasOwnProperty("config") && typeof app.config === "object");
|
||||
assert.ok(app.hasOwnProperty("user") && typeof app.config === "object");
|
||||
assert.equal(app.localize, window._l);
|
||||
});
|
||||
|
||||
QUnit.test("App base default options", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("options") && typeof app.options === "object");
|
||||
assert.equal(app.options.root, "/");
|
||||
assert.equal(app.options.patchExisting, true);
|
||||
});
|
||||
|
||||
QUnit.test("App base extends from Backbone.Events", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
["on", "off", "trigger", "listenTo", "stopListening"].forEach(function(fn) {
|
||||
assert.ok(app.hasOwnProperty(fn) && typeof app[fn] === "function");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("App base has logging methods from utils/add-logging.js", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
["debug", "info", "warn", "error", "metric"].forEach(function(fn) {
|
||||
assert.ok(typeof app[fn] === "function");
|
||||
});
|
||||
assert.ok(app._logNamespace === "GalaxyApp");
|
||||
});
|
||||
|
||||
QUnit.test("App base will patch in attributes from existing Galaxy objects", function(assert) {
|
||||
window.Galaxy = {
|
||||
attribute: {
|
||||
subattr: 1
|
||||
}
|
||||
};
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(typeof app.attribute === "object" && app.attribute.subattr === 1);
|
||||
});
|
||||
|
||||
QUnit.test("App base logger", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("logger") && typeof app.config === "object");
|
||||
});
|
||||
|
||||
QUnit.test("App base config", function(assert) {
|
||||
var app = new appBase.GalaxyApp(options);
|
||||
assert.ok(app.hasOwnProperty("config") && typeof app.config === "object");
|
||||
assert.equal(app.config.allow_user_deletion, false);
|
||||
assert.equal(app.config.allow_user_creation, true);
|
||||
assert.equal(app.config.wiki_url, "https://galaxyproject.org/");
|
||||
assert.equal(app.config.ftp_upload_site, null);
|
||||
});
|
||||
|
||||
QUnit.test("App base user", function(assert) {
|
||||
var app = new appBase.GalaxyApp({});
|
||||
assert.ok(app.hasOwnProperty("user") && typeof app.user === "object");
|
||||
assert.ok(app.user.isAdmin() === false);
|
||||
});
|
||||
@@ -1,515 +0,0 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "utils/graph", "jquery"], function(testApp, GRAPH, $) {
|
||||
"use strict";
|
||||
GRAPH = GRAPH.default;
|
||||
QUnit.module("utils/graph.js library tests");
|
||||
///*
|
||||
function testEmptyObject(assert, o) {
|
||||
assert.ok(typeof o === "object");
|
||||
assert.ok(Object.keys(o).length === 0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------ vertices
|
||||
QUnit.test("Empty vertex construction", function(assert) {
|
||||
var vert = new GRAPH.Vertex();
|
||||
assert.ok(vert instanceof GRAPH.Vertex);
|
||||
assert.ok(vert.name === "(unnamed)");
|
||||
assert.ok(vert.data === null);
|
||||
testEmptyObject(assert, vert.edges);
|
||||
assert.ok(vert.degree === 0);
|
||||
assert.ok(vert + "" === "Vertex((unnamed))");
|
||||
assert.deepEqual(vert.toJSON(), { name: "(unnamed)", data: null });
|
||||
});
|
||||
|
||||
QUnit.test("Vertex construction", function(assert) {
|
||||
var vert = new GRAPH.Vertex("blah", { blorp: 1, bleep: 2 });
|
||||
assert.ok(vert instanceof GRAPH.Vertex);
|
||||
assert.ok(vert.name === "blah");
|
||||
assert.deepEqual(vert.data, { blorp: 1, bleep: 2 });
|
||||
testEmptyObject(assert, vert.edges);
|
||||
assert.ok(vert.degree === 0);
|
||||
assert.ok(vert + "" === "Vertex(blah)");
|
||||
assert.deepEqual(vert.toJSON(), { name: "blah", data: { blorp: 1, bleep: 2 } });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ edges
|
||||
QUnit.test("Empty edge construction", function(assert) {
|
||||
var edge = new GRAPH.Edge();
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(edge.source === null);
|
||||
assert.ok(edge.target === null);
|
||||
assert.ok(edge.data === null);
|
||||
assert.ok(edge + "" === "null->null");
|
||||
assert.deepEqual(edge.toJSON(), { source: null, target: null });
|
||||
});
|
||||
|
||||
QUnit.test("Edge construction", function(assert) {
|
||||
var edge = new GRAPH.Edge("A", "B", { one: 1, two: 2 });
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(edge.source === "A");
|
||||
assert.ok(edge.target === "B");
|
||||
assert.deepEqual(edge.data, { one: 1, two: 2 });
|
||||
assert.ok(edge + "" === "A->B");
|
||||
assert.deepEqual(edge.toJSON(), { source: "A", target: "B", data: { one: 1, two: 2 } });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ graphs
|
||||
function testEmptyGraph(assert, graph) {
|
||||
assert.ok(graph instanceof GRAPH.Graph);
|
||||
testEmptyObject(assert, graph.vertices);
|
||||
assert.ok(graph.numEdges === 0);
|
||||
}
|
||||
|
||||
QUnit.test("Empty graph construction", function(assert) {
|
||||
var graph = new GRAPH.Graph();
|
||||
|
||||
assert.ok(graph.directed === false);
|
||||
assert.ok(graph.allowReflexiveEdges === false);
|
||||
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Bad data graph construction", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, {});
|
||||
testEmptyGraph(assert, graph);
|
||||
|
||||
graph = new GRAPH.Graph(false, null);
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test directed and options", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, {}, { allowReflexiveEdges: true });
|
||||
|
||||
assert.ok(graph.directed);
|
||||
assert.ok(graph.allowReflexiveEdges);
|
||||
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
function testSampleDirectedGraph(assert, graph) {
|
||||
assert.ok(!graph.directed);
|
||||
|
||||
assert.ok(Object.keys(graph.vertices).length === 3);
|
||||
assert.ok(graph.vertices.A instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.B instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.C instanceof GRAPH.Vertex);
|
||||
|
||||
assert.deepEqual(Object.keys(graph.vertices.A.edges), ["B", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.B.edges), ["A", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.C.edges), ["A", "B"]);
|
||||
|
||||
assert.ok(graph.vertices.A.degree === 2);
|
||||
assert.ok(graph.vertices.B.degree === 2);
|
||||
assert.ok(graph.vertices.C.degree === 2);
|
||||
|
||||
assert.deepEqual(graph.vertices.A.edges.B.toJSON(), { source: "A", target: "B" });
|
||||
assert.deepEqual(graph.vertices.A.edges.C.toJSON(), { source: "A", target: "C" });
|
||||
assert.deepEqual(graph.vertices.B.edges.A.toJSON(), { source: "B", target: "A" });
|
||||
assert.deepEqual(graph.vertices.B.edges.C.toJSON(), { source: "B", target: "C" });
|
||||
assert.deepEqual(graph.vertices.C.edges.A.toJSON(), { source: "C", target: "A" });
|
||||
assert.deepEqual(graph.vertices.C.edges.B.toJSON(), { source: "C", target: "B" });
|
||||
|
||||
assert.ok(graph.numEdges === 6);
|
||||
}
|
||||
|
||||
function testSampleNonDirectedGraph(assert, graph) {
|
||||
assert.ok(graph.directed);
|
||||
|
||||
assert.ok(Object.keys(graph.vertices).length === 3);
|
||||
assert.ok(graph.vertices.A instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.B instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.C instanceof GRAPH.Vertex);
|
||||
|
||||
assert.deepEqual(Object.keys(graph.vertices.A.edges), ["B", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.B.edges), ["C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.C.edges), []);
|
||||
|
||||
assert.ok(graph.vertices.A.degree === 2);
|
||||
assert.ok(graph.vertices.B.degree === 1);
|
||||
assert.ok(graph.vertices.C.degree === 0);
|
||||
|
||||
assert.deepEqual(graph.vertices.A.edges.B.toJSON(), { source: "A", target: "B" });
|
||||
assert.deepEqual(graph.vertices.A.edges.C.toJSON(), { source: "A", target: "C" });
|
||||
assert.deepEqual(graph.vertices.B.edges.C.toJSON(), { source: "B", target: "C" });
|
||||
|
||||
assert.ok(graph.numEdges === 3);
|
||||
}
|
||||
|
||||
var nodeLinkData = {
|
||||
nodes: [{ name: "A", data: 100 }, { name: "B", data: 200 }, { name: "C", data: 300 }],
|
||||
links: [{ source: 0, target: 1 }, { source: 0, target: 2 }, { source: 1, target: 2 }]
|
||||
};
|
||||
|
||||
QUnit.test("Test nodes and links data input on *non-directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
testSampleDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test nodes and links data input on *directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
testSampleNonDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
var vertexEdgeData = {
|
||||
vertices: [{ name: "A", data: 100 }, { name: "B", data: 200 }, { name: "C", data: 300 }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }, { source: "B", target: "C" }]
|
||||
};
|
||||
|
||||
QUnit.test("Test vertex and edge data input on *non-directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, vertexEdgeData);
|
||||
testSampleDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test vertex and edge data input on *directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, vertexEdgeData);
|
||||
testSampleNonDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test vertex eachEdge", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
assert.ok(typeof graph.vertices.A.eachEdge === "function");
|
||||
assert.deepEqual(
|
||||
graph.vertices.A.eachEdge(function(e) {
|
||||
return e.target;
|
||||
}),
|
||||
["B", "C"]
|
||||
);
|
||||
assert.ok(graph.vertices.A.eachEdge({ target: "B" }).length === 1);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph eachVertex", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.ok(typeof graph.eachVertex === "function");
|
||||
assert.deepEqual(
|
||||
graph.eachVertex(function(v) {
|
||||
return { n: v.name, d: v.degree };
|
||||
}),
|
||||
[{ n: "A", d: 2 }, { n: "B", d: 1 }, { n: "C", d: 0 }]
|
||||
);
|
||||
assert.ok(graph.eachVertex({ degree: 2 })[0] === graph.vertices.A);
|
||||
});
|
||||
|
||||
QUnit.test("Test createVertex", function(assert) {
|
||||
var graph = new GRAPH.Graph();
|
||||
var vert1 = graph.createVertex("A", { blah: 1 });
|
||||
assert.ok(vert1 instanceof GRAPH.Vertex);
|
||||
assert.ok(vert1 === graph.vertices.A);
|
||||
assert.ok(graph.createVertex("A", { blah: 1 }) === vert1);
|
||||
});
|
||||
|
||||
QUnit.test("Test createEdge", function(assert) {
|
||||
var graph, A, B, edge;
|
||||
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "B");
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(A.degree === 1);
|
||||
assert.ok(B.degree === 1);
|
||||
assert.ok(A.edges.B);
|
||||
assert.ok(B.edges.A);
|
||||
assert.ok(graph.numEdges === 2);
|
||||
|
||||
// bad target
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "C");
|
||||
assert.ok(edge === null);
|
||||
assert.ok(A.degree === 0);
|
||||
assert.ok(B.degree === 0);
|
||||
assert.ok(!A.edges.B);
|
||||
assert.ok(!B.edges.A);
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// bad source
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("C", "A");
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// reflexive
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "A");
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// reflexive (allowed)
|
||||
graph = new GRAPH.Graph(false, {}, { allowReflexiveEdges: true });
|
||||
A = graph.createVertex("A");
|
||||
edge = graph.createEdge("A", "A");
|
||||
// reflexive edges shouldn't mirror
|
||||
assert.ok(graph.numEdges === 1);
|
||||
assert.ok(A.edges.A);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.edges", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
assert.ok(graph.edges().length === 6);
|
||||
assert.deepEqual(
|
||||
graph.edges(function(e) {
|
||||
return e.source;
|
||||
}),
|
||||
["A", "A", "B", "B", "C", "C"]
|
||||
);
|
||||
assert.deepEqual(graph.edges({ source: "A" }), [graph.vertices.A.edges.B, graph.vertices.A.edges.C]);
|
||||
|
||||
graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.ok(graph.edges().length === 3);
|
||||
assert.deepEqual(
|
||||
graph.edges(function(e) {
|
||||
return e.source;
|
||||
}),
|
||||
["A", "A", "B"]
|
||||
);
|
||||
assert.deepEqual(graph.edges({ source: "A" }), [graph.vertices.A.edges.B, graph.vertices.A.edges.C]);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.adjacent", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.A), [graph.vertices.B, graph.vertices.C]);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.B), [graph.vertices.C]);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.C), []);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.eachAdjacent", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.deepEqual(
|
||||
graph.eachAdjacent(graph.vertices.A, function(v, e) {
|
||||
return v;
|
||||
}),
|
||||
[graph.vertices.B, graph.vertices.C]
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ breadth first search
|
||||
QUnit.test("Empty BreadthFirstSearch", function(assert) {
|
||||
var search = new GRAPH.BreadthFirstSearch();
|
||||
assert.ok(search instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(search.graph === undefined);
|
||||
assert.ok(typeof search.processFns === "object");
|
||||
assert.ok(typeof search.processFns.vertexEarly === "function");
|
||||
assert.ok(typeof search.processFns.edge === "function");
|
||||
assert.ok(typeof search.processFns.vertexLate === "function");
|
||||
assert.ok(typeof search._cache === "object");
|
||||
});
|
||||
|
||||
QUnit.test("BreadthFirstSearch on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData),
|
||||
bfs = new GRAPH.BreadthFirstSearch(graph);
|
||||
assert.ok(bfs instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(bfs.graph === graph);
|
||||
|
||||
var search = bfs.search("A"),
|
||||
tree = bfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true },
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }]
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 0, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(bfs.search("B").edges, [{ source: "B", target: "A" }, { source: "B", target: "C" }]);
|
||||
assert.deepEqual(bfs.search("C").edges, [{ source: "C", target: "A" }, { source: "C", target: "B" }]);
|
||||
assert.ok(typeof bfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(bfs._cache), ["A", "B", "C"]);
|
||||
});
|
||||
|
||||
QUnit.test("BreadthFirstSearch on directed graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData),
|
||||
bfs = new GRAPH.BreadthFirstSearch(graph);
|
||||
assert.ok(bfs instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(bfs.graph === graph);
|
||||
|
||||
var search = bfs.search("A"),
|
||||
tree = bfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true },
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }]
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 0, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(bfs.search("B").edges, [{ source: "B", target: "C" }]);
|
||||
assert.deepEqual(bfs.search("C").edges, []);
|
||||
assert.ok(typeof bfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(bfs._cache), ["A", "B", "C"]);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ depth first search
|
||||
var DFSData = {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }, { name: "F" }],
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
// confound it
|
||||
{ source: "A", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" },
|
||||
// confound it
|
||||
{ source: "F", target: "A" }
|
||||
]
|
||||
};
|
||||
|
||||
QUnit.test("Empty DepthFirstSearch", function(assert) {
|
||||
var search = new GRAPH.DepthFirstSearch();
|
||||
assert.ok(search instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(search.graph === undefined);
|
||||
assert.ok(typeof search.processFns === "object");
|
||||
assert.ok(typeof search.processFns.vertexEarly === "function");
|
||||
assert.ok(typeof search.processFns.edge === "function");
|
||||
assert.ok(typeof search.processFns.vertexLate === "function");
|
||||
assert.ok(typeof search._cache === "object");
|
||||
});
|
||||
|
||||
QUnit.test("DepthFirstSearch on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, DFSData),
|
||||
dfs = new GRAPH.DepthFirstSearch(graph);
|
||||
assert.ok(dfs instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(dfs.graph === graph);
|
||||
|
||||
var search = dfs.search("A"),
|
||||
tree = dfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true, D: true, E: true, F: true },
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
],
|
||||
entryTimes: { A: 0, B: 1, C: 2, D: 3, E: 7, F: 8 },
|
||||
exitTimes: { A: 11, B: 6, C: 5, D: 4, E: 10, F: 9 }
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 1, 1, 0, 1, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(dfs.search("B").edges, [
|
||||
{ source: "B", target: "A" },
|
||||
{ source: "A", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
]);
|
||||
|
||||
assert.ok(typeof dfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(dfs._cache), ["A", "B"]);
|
||||
});
|
||||
|
||||
QUnit.test("DepthFirstSearch on directed graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, DFSData),
|
||||
dfs = new GRAPH.DepthFirstSearch(graph);
|
||||
assert.ok(dfs instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(dfs.graph === graph);
|
||||
|
||||
var search = dfs.search("A"),
|
||||
tree = dfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true, D: true, E: true, F: true },
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
],
|
||||
entryTimes: { A: 0, B: 1, C: 2, D: 3, E: 7, F: 8 },
|
||||
exitTimes: { A: 11, B: 6, C: 5, D: 4, E: 10, F: 9 }
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 1, 1, 0, 1, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(dfs.search("B").edges, [{ source: "B", target: "C" }, { source: "C", target: "D" }]);
|
||||
|
||||
assert.ok(typeof dfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(dfs._cache), ["A", "B"]);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ components
|
||||
//*/
|
||||
QUnit.test("weakComponents on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "C", target: "D" }]
|
||||
});
|
||||
assert.equal(graph.numEdges, 4);
|
||||
var components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
});
|
||||
|
||||
QUnit.test("weakComponents on directed graph", function(assert) {
|
||||
var graph, components;
|
||||
graph = new GRAPH.Graph(true, {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "D", target: "C" }]
|
||||
});
|
||||
assert.equal(graph.numEdges, 2);
|
||||
|
||||
components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
|
||||
graph = new GRAPH.Graph(true, {
|
||||
vertices: [
|
||||
{ name: "A", data: 100 },
|
||||
{ name: "B", data: 200 },
|
||||
{ name: "C", data: 30 },
|
||||
{ name: "D", data: 40 },
|
||||
{ name: "E", data: 500 },
|
||||
{ name: "F", data: 600 },
|
||||
{ name: "G", data: 7 }
|
||||
],
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "D", target: "C" },
|
||||
{ source: "E", target: "A" },
|
||||
{ source: "F", target: "E" }
|
||||
]
|
||||
});
|
||||
components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
// data retained
|
||||
assert.equal(components[0].vertices[0].data, 100);
|
||||
|
||||
assert.equal(components[0].vertices.length, 4);
|
||||
assert.deepEqual(components[0].edges, [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "E", target: "A" },
|
||||
{ source: "F", target: "E" }
|
||||
]);
|
||||
|
||||
assert.equal(components[1].vertices.length, 2);
|
||||
assert.deepEqual(components[1].edges, [{ source: "D", target: "C" }]);
|
||||
|
||||
assert.deepEqual(components[2].vertices, [{ name: "G", data: 7 }]);
|
||||
assert.deepEqual(components[2].edges.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,515 @@
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import GRAPH from "utils/graph";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.module("utils/graph.js library tests");
|
||||
///*
|
||||
function testEmptyObject(assert, o) {
|
||||
assert.ok(typeof o === "object");
|
||||
assert.ok(Object.keys(o).length === 0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------ vertices
|
||||
QUnit.test("Empty vertex construction", function(assert) {
|
||||
var vert = new GRAPH.Vertex();
|
||||
assert.ok(vert instanceof GRAPH.Vertex);
|
||||
assert.ok(vert.name === "(unnamed)");
|
||||
assert.ok(vert.data === null);
|
||||
testEmptyObject(assert, vert.edges);
|
||||
assert.ok(vert.degree === 0);
|
||||
assert.ok(vert + "" === "Vertex((unnamed))");
|
||||
assert.deepEqual(vert.toJSON(), { name: "(unnamed)", data: null });
|
||||
});
|
||||
|
||||
QUnit.test("Vertex construction", function(assert) {
|
||||
var vert = new GRAPH.Vertex("blah", { blorp: 1, bleep: 2 });
|
||||
assert.ok(vert instanceof GRAPH.Vertex);
|
||||
assert.ok(vert.name === "blah");
|
||||
assert.deepEqual(vert.data, { blorp: 1, bleep: 2 });
|
||||
testEmptyObject(assert, vert.edges);
|
||||
assert.ok(vert.degree === 0);
|
||||
assert.ok(vert + "" === "Vertex(blah)");
|
||||
assert.deepEqual(vert.toJSON(), { name: "blah", data: { blorp: 1, bleep: 2 } });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ edges
|
||||
QUnit.test("Empty edge construction", function(assert) {
|
||||
var edge = new GRAPH.Edge();
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(edge.source === null);
|
||||
assert.ok(edge.target === null);
|
||||
assert.ok(edge.data === null);
|
||||
assert.ok(edge + "" === "null->null");
|
||||
assert.deepEqual(edge.toJSON(), { source: null, target: null });
|
||||
});
|
||||
|
||||
QUnit.test("Edge construction", function(assert) {
|
||||
var edge = new GRAPH.Edge("A", "B", { one: 1, two: 2 });
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(edge.source === "A");
|
||||
assert.ok(edge.target === "B");
|
||||
assert.deepEqual(edge.data, { one: 1, two: 2 });
|
||||
assert.ok(edge + "" === "A->B");
|
||||
assert.deepEqual(edge.toJSON(), { source: "A", target: "B", data: { one: 1, two: 2 } });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ graphs
|
||||
function testEmptyGraph(assert, graph) {
|
||||
assert.ok(graph instanceof GRAPH.Graph);
|
||||
testEmptyObject(assert, graph.vertices);
|
||||
assert.ok(graph.numEdges === 0);
|
||||
}
|
||||
|
||||
QUnit.test("Empty graph construction", function(assert) {
|
||||
var graph = new GRAPH.Graph();
|
||||
|
||||
assert.ok(graph.directed === false);
|
||||
assert.ok(graph.allowReflexiveEdges === false);
|
||||
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Bad data graph construction", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, {});
|
||||
testEmptyGraph(assert, graph);
|
||||
|
||||
graph = new GRAPH.Graph(false, null);
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test directed and options", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, {}, { allowReflexiveEdges: true });
|
||||
|
||||
assert.ok(graph.directed);
|
||||
assert.ok(graph.allowReflexiveEdges);
|
||||
|
||||
testEmptyGraph(assert, graph);
|
||||
});
|
||||
|
||||
function testSampleDirectedGraph(assert, graph) {
|
||||
assert.ok(!graph.directed);
|
||||
|
||||
assert.ok(Object.keys(graph.vertices).length === 3);
|
||||
assert.ok(graph.vertices.A instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.B instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.C instanceof GRAPH.Vertex);
|
||||
|
||||
assert.deepEqual(Object.keys(graph.vertices.A.edges), ["B", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.B.edges), ["A", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.C.edges), ["A", "B"]);
|
||||
|
||||
assert.ok(graph.vertices.A.degree === 2);
|
||||
assert.ok(graph.vertices.B.degree === 2);
|
||||
assert.ok(graph.vertices.C.degree === 2);
|
||||
|
||||
assert.deepEqual(graph.vertices.A.edges.B.toJSON(), { source: "A", target: "B" });
|
||||
assert.deepEqual(graph.vertices.A.edges.C.toJSON(), { source: "A", target: "C" });
|
||||
assert.deepEqual(graph.vertices.B.edges.A.toJSON(), { source: "B", target: "A" });
|
||||
assert.deepEqual(graph.vertices.B.edges.C.toJSON(), { source: "B", target: "C" });
|
||||
assert.deepEqual(graph.vertices.C.edges.A.toJSON(), { source: "C", target: "A" });
|
||||
assert.deepEqual(graph.vertices.C.edges.B.toJSON(), { source: "C", target: "B" });
|
||||
|
||||
assert.ok(graph.numEdges === 6);
|
||||
}
|
||||
|
||||
function testSampleNonDirectedGraph(assert, graph) {
|
||||
assert.ok(graph.directed);
|
||||
|
||||
assert.ok(Object.keys(graph.vertices).length === 3);
|
||||
assert.ok(graph.vertices.A instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.B instanceof GRAPH.Vertex);
|
||||
assert.ok(graph.vertices.C instanceof GRAPH.Vertex);
|
||||
|
||||
assert.deepEqual(Object.keys(graph.vertices.A.edges), ["B", "C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.B.edges), ["C"]);
|
||||
assert.deepEqual(Object.keys(graph.vertices.C.edges), []);
|
||||
|
||||
assert.ok(graph.vertices.A.degree === 2);
|
||||
assert.ok(graph.vertices.B.degree === 1);
|
||||
assert.ok(graph.vertices.C.degree === 0);
|
||||
|
||||
assert.deepEqual(graph.vertices.A.edges.B.toJSON(), { source: "A", target: "B" });
|
||||
assert.deepEqual(graph.vertices.A.edges.C.toJSON(), { source: "A", target: "C" });
|
||||
assert.deepEqual(graph.vertices.B.edges.C.toJSON(), { source: "B", target: "C" });
|
||||
|
||||
assert.ok(graph.numEdges === 3);
|
||||
}
|
||||
|
||||
var nodeLinkData = {
|
||||
nodes: [{ name: "A", data: 100 }, { name: "B", data: 200 }, { name: "C", data: 300 }],
|
||||
links: [{ source: 0, target: 1 }, { source: 0, target: 2 }, { source: 1, target: 2 }]
|
||||
};
|
||||
|
||||
QUnit.test("Test nodes and links data input on *non-directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
testSampleDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test nodes and links data input on *directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
testSampleNonDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
var vertexEdgeData = {
|
||||
vertices: [{ name: "A", data: 100 }, { name: "B", data: 200 }, { name: "C", data: 300 }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }, { source: "B", target: "C" }]
|
||||
};
|
||||
|
||||
QUnit.test("Test vertex and edge data input on *non-directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, vertexEdgeData);
|
||||
testSampleDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test vertex and edge data input on *directed* graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, vertexEdgeData);
|
||||
testSampleNonDirectedGraph(assert, graph);
|
||||
});
|
||||
|
||||
QUnit.test("Test vertex eachEdge", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
assert.ok(typeof graph.vertices.A.eachEdge === "function");
|
||||
assert.deepEqual(
|
||||
graph.vertices.A.eachEdge(function(e) {
|
||||
return e.target;
|
||||
}),
|
||||
["B", "C"]
|
||||
);
|
||||
assert.ok(graph.vertices.A.eachEdge({ target: "B" }).length === 1);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph eachVertex", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.ok(typeof graph.eachVertex === "function");
|
||||
assert.deepEqual(
|
||||
graph.eachVertex(function(v) {
|
||||
return { n: v.name, d: v.degree };
|
||||
}),
|
||||
[{ n: "A", d: 2 }, { n: "B", d: 1 }, { n: "C", d: 0 }]
|
||||
);
|
||||
assert.ok(graph.eachVertex({ degree: 2 })[0] === graph.vertices.A);
|
||||
});
|
||||
|
||||
QUnit.test("Test createVertex", function(assert) {
|
||||
var graph = new GRAPH.Graph();
|
||||
var vert1 = graph.createVertex("A", { blah: 1 });
|
||||
assert.ok(vert1 instanceof GRAPH.Vertex);
|
||||
assert.ok(vert1 === graph.vertices.A);
|
||||
assert.ok(graph.createVertex("A", { blah: 1 }) === vert1);
|
||||
});
|
||||
|
||||
QUnit.test("Test createEdge", function(assert) {
|
||||
var graph, A, B, edge;
|
||||
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "B");
|
||||
assert.ok(edge instanceof GRAPH.Edge);
|
||||
assert.ok(A.degree === 1);
|
||||
assert.ok(B.degree === 1);
|
||||
assert.ok(A.edges.B);
|
||||
assert.ok(B.edges.A);
|
||||
assert.ok(graph.numEdges === 2);
|
||||
|
||||
// bad target
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "C");
|
||||
assert.ok(edge === null);
|
||||
assert.ok(A.degree === 0);
|
||||
assert.ok(B.degree === 0);
|
||||
assert.ok(!A.edges.B);
|
||||
assert.ok(!B.edges.A);
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// bad source
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("C", "A");
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// reflexive
|
||||
graph = new GRAPH.Graph();
|
||||
A = graph.createVertex("A");
|
||||
B = graph.createVertex("B");
|
||||
edge = graph.createEdge("A", "A");
|
||||
assert.ok(graph.numEdges === 0);
|
||||
|
||||
// reflexive (allowed)
|
||||
graph = new GRAPH.Graph(false, {}, { allowReflexiveEdges: true });
|
||||
A = graph.createVertex("A");
|
||||
edge = graph.createEdge("A", "A");
|
||||
// reflexive edges shouldn't mirror
|
||||
assert.ok(graph.numEdges === 1);
|
||||
assert.ok(A.edges.A);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.edges", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData);
|
||||
assert.ok(graph.edges().length === 6);
|
||||
assert.deepEqual(
|
||||
graph.edges(function(e) {
|
||||
return e.source;
|
||||
}),
|
||||
["A", "A", "B", "B", "C", "C"]
|
||||
);
|
||||
assert.deepEqual(graph.edges({ source: "A" }), [graph.vertices.A.edges.B, graph.vertices.A.edges.C]);
|
||||
|
||||
graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.ok(graph.edges().length === 3);
|
||||
assert.deepEqual(
|
||||
graph.edges(function(e) {
|
||||
return e.source;
|
||||
}),
|
||||
["A", "A", "B"]
|
||||
);
|
||||
assert.deepEqual(graph.edges({ source: "A" }), [graph.vertices.A.edges.B, graph.vertices.A.edges.C]);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.adjacent", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.A), [graph.vertices.B, graph.vertices.C]);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.B), [graph.vertices.C]);
|
||||
assert.deepEqual(graph.adjacent(graph.vertices.C), []);
|
||||
});
|
||||
|
||||
QUnit.test("Test graph.eachAdjacent", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData);
|
||||
assert.deepEqual(
|
||||
graph.eachAdjacent(graph.vertices.A, function(v, e) {
|
||||
return v;
|
||||
}),
|
||||
[graph.vertices.B, graph.vertices.C]
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ breadth first search
|
||||
QUnit.test("Empty BreadthFirstSearch", function(assert) {
|
||||
var search = new GRAPH.BreadthFirstSearch();
|
||||
assert.ok(search instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(search.graph === undefined);
|
||||
assert.ok(typeof search.processFns === "object");
|
||||
assert.ok(typeof search.processFns.vertexEarly === "function");
|
||||
assert.ok(typeof search.processFns.edge === "function");
|
||||
assert.ok(typeof search.processFns.vertexLate === "function");
|
||||
assert.ok(typeof search._cache === "object");
|
||||
});
|
||||
|
||||
QUnit.test("BreadthFirstSearch on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, nodeLinkData),
|
||||
bfs = new GRAPH.BreadthFirstSearch(graph);
|
||||
assert.ok(bfs instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(bfs.graph === graph);
|
||||
|
||||
var search = bfs.search("A"),
|
||||
tree = bfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true },
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }]
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 0, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(bfs.search("B").edges, [{ source: "B", target: "A" }, { source: "B", target: "C" }]);
|
||||
assert.deepEqual(bfs.search("C").edges, [{ source: "C", target: "A" }, { source: "C", target: "B" }]);
|
||||
assert.ok(typeof bfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(bfs._cache), ["A", "B", "C"]);
|
||||
});
|
||||
|
||||
QUnit.test("BreadthFirstSearch on directed graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, nodeLinkData),
|
||||
bfs = new GRAPH.BreadthFirstSearch(graph);
|
||||
assert.ok(bfs instanceof GRAPH.BreadthFirstSearch);
|
||||
assert.ok(bfs.graph === graph);
|
||||
|
||||
var search = bfs.search("A"),
|
||||
tree = bfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true },
|
||||
edges: [{ source: "A", target: "B" }, { source: "A", target: "C" }]
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 0, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(bfs.search("B").edges, [{ source: "B", target: "C" }]);
|
||||
assert.deepEqual(bfs.search("C").edges, []);
|
||||
assert.ok(typeof bfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(bfs._cache), ["A", "B", "C"]);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ depth first search
|
||||
var DFSData = {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }, { name: "F" }],
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
// confound it
|
||||
{ source: "A", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" },
|
||||
// confound it
|
||||
{ source: "F", target: "A" }
|
||||
]
|
||||
};
|
||||
|
||||
QUnit.test("Empty DepthFirstSearch", function(assert) {
|
||||
var search = new GRAPH.DepthFirstSearch();
|
||||
assert.ok(search instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(search.graph === undefined);
|
||||
assert.ok(typeof search.processFns === "object");
|
||||
assert.ok(typeof search.processFns.vertexEarly === "function");
|
||||
assert.ok(typeof search.processFns.edge === "function");
|
||||
assert.ok(typeof search.processFns.vertexLate === "function");
|
||||
assert.ok(typeof search._cache === "object");
|
||||
});
|
||||
|
||||
QUnit.test("DepthFirstSearch on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, DFSData),
|
||||
dfs = new GRAPH.DepthFirstSearch(graph);
|
||||
assert.ok(dfs instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(dfs.graph === graph);
|
||||
|
||||
var search = dfs.search("A"),
|
||||
tree = dfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true, D: true, E: true, F: true },
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
],
|
||||
entryTimes: { A: 0, B: 1, C: 2, D: 3, E: 7, F: 8 },
|
||||
exitTimes: { A: 11, B: 6, C: 5, D: 4, E: 10, F: 9 }
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 1, 1, 0, 1, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(dfs.search("B").edges, [
|
||||
{ source: "B", target: "A" },
|
||||
{ source: "A", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
]);
|
||||
|
||||
assert.ok(typeof dfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(dfs._cache), ["A", "B"]);
|
||||
});
|
||||
|
||||
QUnit.test("DepthFirstSearch on directed graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(true, DFSData),
|
||||
dfs = new GRAPH.DepthFirstSearch(graph);
|
||||
assert.ok(dfs instanceof GRAPH.DepthFirstSearch);
|
||||
assert.ok(dfs.graph === graph);
|
||||
|
||||
var search = dfs.search("A"),
|
||||
tree = dfs.searchTree("A");
|
||||
assert.deepEqual(search, {
|
||||
discovered: { A: true, B: true, C: true, D: true, E: true, F: true },
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "B", target: "C" },
|
||||
{ source: "C", target: "D" },
|
||||
{ source: "A", target: "E" },
|
||||
{ source: "E", target: "F" }
|
||||
],
|
||||
entryTimes: { A: 0, B: 1, C: 2, D: 3, E: 7, F: 8 },
|
||||
exitTimes: { A: 11, B: 6, C: 5, D: 4, E: 10, F: 9 }
|
||||
});
|
||||
assert.ok(tree instanceof GRAPH.Graph);
|
||||
assert.deepEqual(tree.vertices.A.toJSON(), graph.vertices.A.toJSON());
|
||||
assert.deepEqual(
|
||||
tree.eachVertex(function(v) {
|
||||
return v.degree;
|
||||
}),
|
||||
[2, 1, 1, 0, 1, 0]
|
||||
);
|
||||
|
||||
assert.deepEqual(dfs.search("B").edges, [{ source: "B", target: "C" }, { source: "C", target: "D" }]);
|
||||
|
||||
assert.ok(typeof dfs._cache.A === "object");
|
||||
assert.deepEqual(Object.keys(dfs._cache), ["A", "B"]);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ components
|
||||
//*/
|
||||
QUnit.test("weakComponents on undirected graph", function(assert) {
|
||||
var graph = new GRAPH.Graph(false, {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "C", target: "D" }]
|
||||
});
|
||||
assert.equal(graph.numEdges, 4);
|
||||
var components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
});
|
||||
|
||||
QUnit.test("weakComponents on directed graph", function(assert) {
|
||||
var graph, components;
|
||||
graph = new GRAPH.Graph(true, {
|
||||
vertices: [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }, { name: "E" }],
|
||||
edges: [{ source: "A", target: "B" }, { source: "D", target: "C" }]
|
||||
});
|
||||
assert.equal(graph.numEdges, 2);
|
||||
|
||||
components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
|
||||
graph = new GRAPH.Graph(true, {
|
||||
vertices: [
|
||||
{ name: "A", data: 100 },
|
||||
{ name: "B", data: 200 },
|
||||
{ name: "C", data: 30 },
|
||||
{ name: "D", data: 40 },
|
||||
{ name: "E", data: 500 },
|
||||
{ name: "F", data: 600 },
|
||||
{ name: "G", data: 7 }
|
||||
],
|
||||
edges: [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "D", target: "C" },
|
||||
{ source: "E", target: "A" },
|
||||
{ source: "F", target: "E" }
|
||||
]
|
||||
});
|
||||
components = graph.weakComponents();
|
||||
assert.equal(components.length, 3);
|
||||
// data retained
|
||||
assert.equal(components[0].vertices[0].data, 100);
|
||||
|
||||
assert.equal(components[0].vertices.length, 4);
|
||||
assert.deepEqual(components[0].edges, [
|
||||
{ source: "A", target: "B" },
|
||||
{ source: "E", target: "A" },
|
||||
{ source: "F", target: "E" }
|
||||
]);
|
||||
|
||||
assert.equal(components[1].vertices.length, 2);
|
||||
assert.deepEqual(components[1].edges, [{ source: "D", target: "C" }]);
|
||||
|
||||
assert.deepEqual(components[2].vertices, [{ name: "G", data: 7 }]);
|
||||
assert.deepEqual(components[2].edges.length, 0);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
// This file isn't really testing anything useful yet, it is just testing
|
||||
// (or demonstrating) qunit+backbone interactions.
|
||||
/* global define */
|
||||
define(["qunit/test-app", "mvc/history/hda-model", "mvc/history/hda-li", "jquery"], function(
|
||||
testApp,
|
||||
HDA_MODEL,
|
||||
HDA_BASE,
|
||||
$
|
||||
) {
|
||||
"use strict";
|
||||
HDA_MODEL = HDA_MODEL.default;
|
||||
HDA_BASE = HDA_BASE.default;
|
||||
QUnit.module("HDA base backbone view tests");
|
||||
|
||||
QUnit.test("Base HDA view default construction, initialize", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
id: "123"
|
||||
}),
|
||||
view = new HDA_BASE.HDAListItemView({ model: hda });
|
||||
|
||||
assert.strictEqual(view.model, hda);
|
||||
|
||||
assert.equal(view.linkTarget, "_blank");
|
||||
assert.equal(view.selectable, false);
|
||||
assert.equal(view.selected, false);
|
||||
assert.equal(view.expanded, false);
|
||||
assert.equal(view.draggable, false);
|
||||
assert.equal(view.id(), "dataset-123");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file isn't really testing anything useful yet, it is just testing
|
||||
// (or demonstrating) qunit+backbone interactions.
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import HDA_MODEL from "mvc/history/hda-model";
|
||||
import HDA_BASE from "mvc/history/hda-li";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.module("HDA base backbone view tests");
|
||||
|
||||
QUnit.test("Base HDA view default construction, initialize", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
id: "123"
|
||||
}),
|
||||
view = new HDA_BASE.HDAListItemView({ model: hda });
|
||||
|
||||
assert.strictEqual(view.model, hda);
|
||||
|
||||
assert.equal(view.linkTarget, "_blank");
|
||||
assert.equal(view.selectable, false);
|
||||
assert.equal(view.selected, false);
|
||||
assert.equal(view.expanded, false);
|
||||
assert.equal(view.draggable, false);
|
||||
assert.equal(view.id(), "dataset-123");
|
||||
});
|
||||
@@ -1,38 +1,40 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "mvc/history/hda-model", "jquery", "sinon"], function(testApp, HDA_MODEL, $, sinon) {
|
||||
HDA_MODEL = HDA_MODEL.default;
|
||||
QUnit.module("History Contents Model QUnit.Tests");
|
||||
import testApp from "qunit/test-app";
|
||||
import HDA_MODEL from "mvc/history/hda-model";
|
||||
import sinon from "sinon";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.test("HDA Constructions with Default Attributes", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({});
|
||||
assert.equal(hda.get("name"), "(unnamed dataset)");
|
||||
assert.equal(hda.get("state"), "new");
|
||||
});
|
||||
QUnit.module("History Contents Model QUnit.Tests");
|
||||
|
||||
QUnit.test("HDA Construction with Supplied Attributes", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
history_content_type: "dataset",
|
||||
name: "my dataset",
|
||||
state: "ok"
|
||||
});
|
||||
assert.equal(hda.get("name"), "my dataset");
|
||||
assert.equal(hda.get("state"), "ok");
|
||||
QUnit.test("HDA Constructions with Default Attributes", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({});
|
||||
assert.equal(hda.get("name"), "(unnamed dataset)");
|
||||
assert.equal(hda.get("state"), "new");
|
||||
});
|
||||
|
||||
QUnit.test("HDA Construction with Supplied Attributes", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
history_content_type: "dataset",
|
||||
name: "my dataset",
|
||||
state: "ok"
|
||||
});
|
||||
assert.equal(hda.get("name"), "my dataset");
|
||||
assert.equal(hda.get("state"), "ok");
|
||||
});
|
||||
|
||||
QUnit.test("HDA Deletion", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
history_content_type: "dataset",
|
||||
id: "hda1",
|
||||
history_id: "h1",
|
||||
deleted: false
|
||||
});
|
||||
assert.equal(hda.get("deleted"), false);
|
||||
QUnit.test("HDA Deletion", function(assert) {
|
||||
var hda = new HDA_MODEL.HistoryDatasetAssociation({
|
||||
history_content_type: "dataset",
|
||||
id: "hda1",
|
||||
history_id: "h1",
|
||||
deleted: false
|
||||
});
|
||||
assert.equal(hda.get("deleted"), false);
|
||||
|
||||
sinon.stub($, "ajax").yieldsTo("success", { deleted: true });
|
||||
hda["delete"]();
|
||||
// to get the url sinon used:
|
||||
//console.debug( $.ajax.lastCall.args[0].url )
|
||||
assert.ok($.ajax.calledWithMatch({ url: "/api/histories/h1/contents/datasets/hda1" }));
|
||||
assert.equal(hda.get("deleted"), true);
|
||||
});
|
||||
sinon.stub($, "ajax").yieldsTo("success", { deleted: true });
|
||||
hda["delete"]();
|
||||
// to get the url sinon used:
|
||||
//console.debug( $.ajax.lastCall.args[0].url )
|
||||
assert.ok($.ajax.calledWithMatch({ url: "/api/histories/h1/contents/datasets/hda1" }));
|
||||
assert.equal(hda.get("deleted"), true);
|
||||
});
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "mvc/history/job-dag", "jquery", "qunit/test-data/job-dag-1"], function(
|
||||
testApp,
|
||||
JobDAG,
|
||||
$,
|
||||
testData
|
||||
) {
|
||||
"use strict";
|
||||
JobDAG = JobDAG.default;
|
||||
QUnit.module("mvc/history/job-dag.js tests");
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
QUnit.test("Empty JobDAG construction", function(assert) {
|
||||
var dag = new JobDAG();
|
||||
assert.ok(dag instanceof JobDAG);
|
||||
|
||||
// default options
|
||||
assert.deepEqual(dag.filters, []);
|
||||
assert.deepEqual(dag.options, {
|
||||
excludeSetMetadata: false
|
||||
});
|
||||
|
||||
// test (empty) instance vars
|
||||
assert.deepEqual(dag._jobsData, []);
|
||||
assert.deepEqual(dag._historyContentsMap, {});
|
||||
assert.deepEqual(dag._toolMap, {});
|
||||
assert.equal(dag.noInputJobs.length, 0);
|
||||
assert.equal(dag.noOutputJobs.length, 0);
|
||||
|
||||
// logging
|
||||
assert.equal(typeof dag.debug, "function");
|
||||
assert.equal(typeof dag.info, "function");
|
||||
assert.equal(typeof dag.warn, "function");
|
||||
assert.equal(typeof dag.error, "function");
|
||||
});
|
||||
|
||||
QUnit.test("Empty JobDAG construction - changing options", function(assert) {
|
||||
var dag;
|
||||
dag = new JobDAG({
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
|
||||
// excludeSetMetadata
|
||||
assert.deepEqual(dag.options, {
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
assert.equal(dag.filters.length, 1);
|
||||
assert.equal(typeof dag.filters[0], "function");
|
||||
|
||||
// filters
|
||||
function testFilter(job, index, jobData) {
|
||||
return true;
|
||||
}
|
||||
dag = new JobDAG({
|
||||
filters: [testFilter]
|
||||
});
|
||||
assert.equal(dag.filters[0], testFilter);
|
||||
});
|
||||
|
||||
QUnit.test("JobDAG construction with history and jobs", function(assert) {
|
||||
assert.equal(testData.jobs1.length, 3);
|
||||
assert.equal(testData.historyContents1.length, 3);
|
||||
|
||||
var history = testData.historyContents1,
|
||||
jobs = testData.jobs1,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
assert.deepEqual(dag._outputIdToJobMap, {
|
||||
"8c959c9304a2bc4b": "8a81cf6f989c4467",
|
||||
"132016f833b57406": "6505e875ddb66fd2",
|
||||
"846fb0a2a64137c0": "77f74776fd03cbc5"
|
||||
});
|
||||
assert.deepEqual(dag._jobsData, [
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "8a81cf6f989c4467" }),
|
||||
inputs: {},
|
||||
outputs: {
|
||||
"8c959c9304a2bc4b": {
|
||||
src: "hda",
|
||||
id: "8c959c9304a2bc4b",
|
||||
name: "output0",
|
||||
content: _.findWhere(history, { id: "8c959c9304a2bc4b" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
},
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "6505e875ddb66fd2" }),
|
||||
inputs: {
|
||||
"8c959c9304a2bc4b": {
|
||||
src: "hda",
|
||||
id: "8c959c9304a2bc4b",
|
||||
name: "input",
|
||||
content: _.findWhere(history, { id: "8c959c9304a2bc4b" })
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
"132016f833b57406": {
|
||||
src: "hda",
|
||||
id: "132016f833b57406",
|
||||
name: "out_file1",
|
||||
content: _.findWhere(history, { id: "132016f833b57406" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
},
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "77f74776fd03cbc5" }),
|
||||
inputs: {
|
||||
"132016f833b57406": {
|
||||
src: "hda",
|
||||
id: "132016f833b57406",
|
||||
name: "input",
|
||||
content: _.findWhere(history, { id: "132016f833b57406" })
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
"846fb0a2a64137c0": {
|
||||
src: "hda",
|
||||
id: "846fb0a2a64137c0",
|
||||
name: "out_file1",
|
||||
content: _.findWhere(history, { id: "846fb0a2a64137c0" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
}
|
||||
]);
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toNodesAndLinks(), {
|
||||
nodes: [
|
||||
{ name: "8a81cf6f989c4467", data: jobsDataMap["8a81cf6f989c4467"] },
|
||||
{ name: "6505e875ddb66fd2", data: jobsDataMap["6505e875ddb66fd2"] },
|
||||
{ name: "77f74776fd03cbc5", data: jobsDataMap["77f74776fd03cbc5"] }
|
||||
],
|
||||
links: [
|
||||
{ source: 0, target: 1, data: { dataset: "8c959c9304a2bc4b" } },
|
||||
{ source: 1, target: 2, data: { dataset: "132016f833b57406" } }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "8a81cf6f989c4467", data: jobsDataMap["8a81cf6f989c4467"] },
|
||||
{ name: "6505e875ddb66fd2", data: jobsDataMap["6505e875ddb66fd2"] },
|
||||
{ name: "77f74776fd03cbc5", data: jobsDataMap["77f74776fd03cbc5"] }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "8a81cf6f989c4467",
|
||||
target: "6505e875ddb66fd2",
|
||||
data: { dataset: "8c959c9304a2bc4b" }
|
||||
},
|
||||
{
|
||||
source: "6505e875ddb66fd2",
|
||||
target: "77f74776fd03cbc5",
|
||||
data: { dataset: "132016f833b57406" }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// test cloning
|
||||
});
|
||||
|
||||
QUnit.test("JobDAG removal of __SET_METADATA__ jobs", function(assert) {
|
||||
assert.equal(testData.jobs2.length, 3);
|
||||
assert.equal(testData.historyContents2.length, 2);
|
||||
|
||||
var history = testData.historyContents2,
|
||||
jobs = testData.jobs2,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs,
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toNodesAndLinks(), {
|
||||
nodes: [
|
||||
{ name: "bf60fd5f5f7f44bf", data: jobsDataMap["bf60fd5f5f7f44bf"] },
|
||||
{ name: "90240358ebde1489", data: jobsDataMap["90240358ebde1489"] }
|
||||
],
|
||||
links: [{ source: 0, target: 1, data: { dataset: "eca0af6fb47bf90c" } }]
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: test filtering out errored jobs
|
||||
QUnit.test("JobDAG construction with history and jobs", function(assert) {
|
||||
assert.equal(testData.jobs3.length, 5);
|
||||
assert.equal(testData.historyContents3.length, 5);
|
||||
|
||||
var history = testData.historyContents3,
|
||||
jobs = testData.jobs3,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "8c959c9304a2bc4b", data: jobsDataMap["8c959c9304a2bc4b"] },
|
||||
{ name: "132016f833b57406", data: jobsDataMap["132016f833b57406"] },
|
||||
{ name: "846fb0a2a64137c0", data: jobsDataMap["846fb0a2a64137c0"] },
|
||||
{ name: "eca0af6fb47bf90c", data: jobsDataMap["eca0af6fb47bf90c"] },
|
||||
{ name: "6fc9fbb81c497f69", data: jobsDataMap["6fc9fbb81c497f69"] }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "8c959c9304a2bc4b",
|
||||
target: "846fb0a2a64137c0",
|
||||
data: { dataset: "6fb17d0cc6e8fae5" }
|
||||
},
|
||||
{
|
||||
source: "132016f833b57406",
|
||||
target: "eca0af6fb47bf90c",
|
||||
data: { dataset: "5114a2a207b7caff" }
|
||||
},
|
||||
{
|
||||
source: "eca0af6fb47bf90c",
|
||||
target: "6fc9fbb81c497f69",
|
||||
data: { dataset: "b8a0d6158b9961df" }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
var components = dag.weakComponents();
|
||||
assert.deepEqual(components, [
|
||||
{
|
||||
vertices: [
|
||||
{ name: "8c959c9304a2bc4b", data: jobsDataMap["8c959c9304a2bc4b"] },
|
||||
{ name: "846fb0a2a64137c0", data: jobsDataMap["846fb0a2a64137c0"] }
|
||||
],
|
||||
edges: [{ source: "8c959c9304a2bc4b", target: "846fb0a2a64137c0" }]
|
||||
},
|
||||
{
|
||||
vertices: [
|
||||
{ name: "132016f833b57406", data: jobsDataMap["132016f833b57406"] },
|
||||
{ name: "eca0af6fb47bf90c", data: jobsDataMap["eca0af6fb47bf90c"] },
|
||||
{ name: "6fc9fbb81c497f69", data: jobsDataMap["6fc9fbb81c497f69"] }
|
||||
],
|
||||
edges: [
|
||||
{ source: "132016f833b57406", target: "eca0af6fb47bf90c" },
|
||||
{ source: "eca0af6fb47bf90c", target: "6fc9fbb81c497f69" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
//TODO: test filtering out errored jobs
|
||||
QUnit.test("JobDAG construction with copied history contents", function(assert) {
|
||||
assert.equal(testData.jobs4.length, 1);
|
||||
assert.equal(testData.historyContents4.length, 3);
|
||||
|
||||
var history = testData.historyContents4,
|
||||
jobs = testData.jobs4,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "92b83968e0b52980", data: jobsDataMap["92b83968e0b52980"] },
|
||||
{ name: "copy-422eef6b1b545329", data: _.findWhere(history, { id: "422eef6b1b545329" }) },
|
||||
{ name: "copy-c86c1b73aa7102dd", data: _.findWhere(history, { id: "c86c1b73aa7102dd" }) }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "copy-422eef6b1b545329",
|
||||
target: "92b83968e0b52980",
|
||||
data: { dataset: "422eef6b1b545329" }
|
||||
},
|
||||
{
|
||||
source: "copy-c86c1b73aa7102dd",
|
||||
target: "92b83968e0b52980",
|
||||
data: { dataset: "c86c1b73aa7102dd" }
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import JobDAG from "mvc/history/job-dag";
|
||||
import testData from "qunit/test-data/job-dag-1";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.module("mvc/history/job-dag.js tests");
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
QUnit.test("Empty JobDAG construction", function(assert) {
|
||||
var dag = new JobDAG();
|
||||
assert.ok(dag instanceof JobDAG);
|
||||
|
||||
// default options
|
||||
assert.deepEqual(dag.filters, []);
|
||||
assert.deepEqual(dag.options, {
|
||||
excludeSetMetadata: false
|
||||
});
|
||||
|
||||
// test (empty) instance vars
|
||||
assert.deepEqual(dag._jobsData, []);
|
||||
assert.deepEqual(dag._historyContentsMap, {});
|
||||
assert.deepEqual(dag._toolMap, {});
|
||||
assert.equal(dag.noInputJobs.length, 0);
|
||||
assert.equal(dag.noOutputJobs.length, 0);
|
||||
|
||||
// logging
|
||||
assert.equal(typeof dag.debug, "function");
|
||||
assert.equal(typeof dag.info, "function");
|
||||
assert.equal(typeof dag.warn, "function");
|
||||
assert.equal(typeof dag.error, "function");
|
||||
});
|
||||
|
||||
QUnit.test("Empty JobDAG construction - changing options", function(assert) {
|
||||
var dag;
|
||||
dag = new JobDAG({
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
|
||||
// excludeSetMetadata
|
||||
assert.deepEqual(dag.options, {
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
assert.equal(dag.filters.length, 1);
|
||||
assert.equal(typeof dag.filters[0], "function");
|
||||
|
||||
// filters
|
||||
function testFilter(job, index, jobData) {
|
||||
return true;
|
||||
}
|
||||
dag = new JobDAG({
|
||||
filters: [testFilter]
|
||||
});
|
||||
assert.equal(dag.filters[0], testFilter);
|
||||
});
|
||||
|
||||
QUnit.test("JobDAG construction with history and jobs", function(assert) {
|
||||
assert.equal(testData.jobs1.length, 3);
|
||||
assert.equal(testData.historyContents1.length, 3);
|
||||
|
||||
var history = testData.historyContents1,
|
||||
jobs = testData.jobs1,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
assert.deepEqual(dag._outputIdToJobMap, {
|
||||
"8c959c9304a2bc4b": "8a81cf6f989c4467",
|
||||
"132016f833b57406": "6505e875ddb66fd2",
|
||||
"846fb0a2a64137c0": "77f74776fd03cbc5"
|
||||
});
|
||||
assert.deepEqual(dag._jobsData, [
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "8a81cf6f989c4467" }),
|
||||
inputs: {},
|
||||
outputs: {
|
||||
"8c959c9304a2bc4b": {
|
||||
src: "hda",
|
||||
id: "8c959c9304a2bc4b",
|
||||
name: "output0",
|
||||
content: _.findWhere(history, { id: "8c959c9304a2bc4b" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
},
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "6505e875ddb66fd2" }),
|
||||
inputs: {
|
||||
"8c959c9304a2bc4b": {
|
||||
src: "hda",
|
||||
id: "8c959c9304a2bc4b",
|
||||
name: "input",
|
||||
content: _.findWhere(history, { id: "8c959c9304a2bc4b" })
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
"132016f833b57406": {
|
||||
src: "hda",
|
||||
id: "132016f833b57406",
|
||||
name: "out_file1",
|
||||
content: _.findWhere(history, { id: "132016f833b57406" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
},
|
||||
{
|
||||
job: _.findWhere(jobs, { id: "77f74776fd03cbc5" }),
|
||||
inputs: {
|
||||
"132016f833b57406": {
|
||||
src: "hda",
|
||||
id: "132016f833b57406",
|
||||
name: "input",
|
||||
content: _.findWhere(history, { id: "132016f833b57406" })
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
"846fb0a2a64137c0": {
|
||||
src: "hda",
|
||||
id: "846fb0a2a64137c0",
|
||||
name: "out_file1",
|
||||
content: _.findWhere(history, { id: "846fb0a2a64137c0" })
|
||||
}
|
||||
},
|
||||
tool: {}
|
||||
}
|
||||
]);
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toNodesAndLinks(), {
|
||||
nodes: [
|
||||
{ name: "8a81cf6f989c4467", data: jobsDataMap["8a81cf6f989c4467"] },
|
||||
{ name: "6505e875ddb66fd2", data: jobsDataMap["6505e875ddb66fd2"] },
|
||||
{ name: "77f74776fd03cbc5", data: jobsDataMap["77f74776fd03cbc5"] }
|
||||
],
|
||||
links: [
|
||||
{ source: 0, target: 1, data: { dataset: "8c959c9304a2bc4b" } },
|
||||
{ source: 1, target: 2, data: { dataset: "132016f833b57406" } }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "8a81cf6f989c4467", data: jobsDataMap["8a81cf6f989c4467"] },
|
||||
{ name: "6505e875ddb66fd2", data: jobsDataMap["6505e875ddb66fd2"] },
|
||||
{ name: "77f74776fd03cbc5", data: jobsDataMap["77f74776fd03cbc5"] }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "8a81cf6f989c4467",
|
||||
target: "6505e875ddb66fd2",
|
||||
data: { dataset: "8c959c9304a2bc4b" }
|
||||
},
|
||||
{
|
||||
source: "6505e875ddb66fd2",
|
||||
target: "77f74776fd03cbc5",
|
||||
data: { dataset: "132016f833b57406" }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// test cloning
|
||||
});
|
||||
|
||||
QUnit.test("JobDAG removal of __SET_METADATA__ jobs", function(assert) {
|
||||
assert.equal(testData.jobs2.length, 3);
|
||||
assert.equal(testData.historyContents2.length, 2);
|
||||
|
||||
var history = testData.historyContents2,
|
||||
jobs = testData.jobs2,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs,
|
||||
excludeSetMetadata: true
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toNodesAndLinks(), {
|
||||
nodes: [
|
||||
{ name: "bf60fd5f5f7f44bf", data: jobsDataMap["bf60fd5f5f7f44bf"] },
|
||||
{ name: "90240358ebde1489", data: jobsDataMap["90240358ebde1489"] }
|
||||
],
|
||||
links: [{ source: 0, target: 1, data: { dataset: "eca0af6fb47bf90c" } }]
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: test filtering out errored jobs
|
||||
QUnit.test("JobDAG construction with history and jobs", function(assert) {
|
||||
assert.equal(testData.jobs3.length, 5);
|
||||
assert.equal(testData.historyContents3.length, 5);
|
||||
|
||||
var history = testData.historyContents3,
|
||||
jobs = testData.jobs3,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "8c959c9304a2bc4b", data: jobsDataMap["8c959c9304a2bc4b"] },
|
||||
{ name: "132016f833b57406", data: jobsDataMap["132016f833b57406"] },
|
||||
{ name: "846fb0a2a64137c0", data: jobsDataMap["846fb0a2a64137c0"] },
|
||||
{ name: "eca0af6fb47bf90c", data: jobsDataMap["eca0af6fb47bf90c"] },
|
||||
{ name: "6fc9fbb81c497f69", data: jobsDataMap["6fc9fbb81c497f69"] }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "8c959c9304a2bc4b",
|
||||
target: "846fb0a2a64137c0",
|
||||
data: { dataset: "6fb17d0cc6e8fae5" }
|
||||
},
|
||||
{
|
||||
source: "132016f833b57406",
|
||||
target: "eca0af6fb47bf90c",
|
||||
data: { dataset: "5114a2a207b7caff" }
|
||||
},
|
||||
{
|
||||
source: "eca0af6fb47bf90c",
|
||||
target: "6fc9fbb81c497f69",
|
||||
data: { dataset: "b8a0d6158b9961df" }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
var components = dag.weakComponents();
|
||||
assert.deepEqual(components, [
|
||||
{
|
||||
vertices: [
|
||||
{ name: "8c959c9304a2bc4b", data: jobsDataMap["8c959c9304a2bc4b"] },
|
||||
{ name: "846fb0a2a64137c0", data: jobsDataMap["846fb0a2a64137c0"] }
|
||||
],
|
||||
edges: [{ source: "8c959c9304a2bc4b", target: "846fb0a2a64137c0" }]
|
||||
},
|
||||
{
|
||||
vertices: [
|
||||
{ name: "132016f833b57406", data: jobsDataMap["132016f833b57406"] },
|
||||
{ name: "eca0af6fb47bf90c", data: jobsDataMap["eca0af6fb47bf90c"] },
|
||||
{ name: "6fc9fbb81c497f69", data: jobsDataMap["6fc9fbb81c497f69"] }
|
||||
],
|
||||
edges: [
|
||||
{ source: "132016f833b57406", target: "eca0af6fb47bf90c" },
|
||||
{ source: "eca0af6fb47bf90c", target: "6fc9fbb81c497f69" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
//TODO: test filtering out errored jobs
|
||||
QUnit.test("JobDAG construction with copied history contents", function(assert) {
|
||||
assert.equal(testData.jobs4.length, 1);
|
||||
assert.equal(testData.historyContents4.length, 3);
|
||||
|
||||
var history = testData.historyContents4,
|
||||
jobs = testData.jobs4,
|
||||
dag;
|
||||
dag = new JobDAG({
|
||||
historyContents: history,
|
||||
tools: testData.tools,
|
||||
jobs: jobs
|
||||
});
|
||||
|
||||
var jobsDataMap = dag._jobsDataMap();
|
||||
assert.deepEqual(dag.toVerticesAndEdges(), {
|
||||
vertices: [
|
||||
{ name: "92b83968e0b52980", data: jobsDataMap["92b83968e0b52980"] },
|
||||
{ name: "copy-422eef6b1b545329", data: _.findWhere(history, { id: "422eef6b1b545329" }) },
|
||||
{ name: "copy-c86c1b73aa7102dd", data: _.findWhere(history, { id: "c86c1b73aa7102dd" }) }
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: "copy-422eef6b1b545329",
|
||||
target: "92b83968e0b52980",
|
||||
data: { dataset: "422eef6b1b545329" }
|
||||
},
|
||||
{
|
||||
source: "copy-c86c1b73aa7102dd",
|
||||
target: "92b83968e0b52980",
|
||||
data: { dataset: "c86c1b73aa7102dd" }
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
/* global define */
|
||||
define(
|
||||
[
|
||||
"qunit/test-app",
|
||||
"mvc/collection/list-of-pairs-collection-creator",
|
||||
"qunit/test-data/paired-collection-creator.data",
|
||||
"jquery",
|
||||
"sinon"
|
||||
],
|
||||
function(testApp, PAIRED_COLLECTION_CREATOR, DATA, $, sinon) {
|
||||
"use strict";
|
||||
PAIRED_COLLECTION_CREATOR = PAIRED_COLLECTION_CREATOR.default;
|
||||
var PCC = PAIRED_COLLECTION_CREATOR.PairedCollectionCreator;
|
||||
|
||||
QUnit.module("Galaxy client app tests");
|
||||
|
||||
QUnit.test("Collection creation", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
historyId: "fakeHistoryId"
|
||||
}),
|
||||
server = sinon.fakeServer.create();
|
||||
|
||||
var requestJSON;
|
||||
server.respondWith("POST", "/api/histories/fakeHistoryId/contents/dataset_collections", function(request) {
|
||||
requestJSON = JSON.parse(request.requestBody);
|
||||
request.respond(
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
fakeResponse: "yes"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
//console.debug( 'requestBody:', JSON.stringify( requestJSON, null, ' ' ) );
|
||||
pcc.createList("Heres a collection");
|
||||
server.respond();
|
||||
assert.deepEqual(requestJSON, DATA._1requestJSON);
|
||||
});
|
||||
|
||||
QUnit.test("Creator base/empty construction/initializiation defaults", function(assert) {
|
||||
var pcc = new PCC([]);
|
||||
assert.ok(pcc instanceof PCC);
|
||||
assert.deepEqual(pcc.filters, pcc.commonFilters[pcc.DEFAULT_FILTERS]);
|
||||
assert.ok(pcc.automaticallyPair);
|
||||
assert.equal(pcc.matchPercentage, 0.9);
|
||||
assert.equal(pcc.strategy, "autopairLCS");
|
||||
});
|
||||
|
||||
QUnit.test("Creator construction/initializiation with datasets", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1
|
||||
});
|
||||
//pcc.initialList.forEach( function( dataset, i ){
|
||||
// console.log( i + ':\n' + JSON.stringify( dataset ) );
|
||||
//});
|
||||
// pcc maintains the original list - which, in this case, is already sorted
|
||||
assert.deepEqual(pcc.initialList, DATA._1);
|
||||
// datasets 1 has no ids, so the pcc will create them
|
||||
assert.ok(
|
||||
_.every(pcc.initialList, function(dataset) {
|
||||
return dataset.id;
|
||||
})
|
||||
);
|
||||
// datasets 1 is very easy to auto pair
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with simple exact matching", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "simple",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with LCS", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "lcs",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with Levenshtein", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "levenshtein",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
//TODO:
|
||||
// filters: clearing, setting via popover, regex
|
||||
// partition: maximize paired, maximize unpaired, split evenly
|
||||
// pairing: manually pairing and unpairing
|
||||
// misc: renaming pairs, removing file extensions
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,108 @@
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import PAIRED_COLLECTION_CREATOR from "mvc/collection/list-of-pairs-collection-creator";
|
||||
import DATA from "qunit/test-data/paired-collection-creator.data";
|
||||
import $ from "jquery";
|
||||
import sinon from "sinon";
|
||||
|
||||
var PCC = PAIRED_COLLECTION_CREATOR.PairedCollectionCreator;
|
||||
|
||||
QUnit.module("Galaxy client app tests");
|
||||
|
||||
// Following test no longer passes - something is wrong setting up Galaxy.root - but we have multiple Selenium tests that cover verifying
|
||||
// actual such lists are created as a result of this form. The matching stuff later on in the
|
||||
// file is better suited for unit testing I think anyway and is not covered so well by
|
||||
// functional tests.
|
||||
|
||||
/*
|
||||
QUnit.test("Collection creation", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
historyId: "fakeHistoryId"
|
||||
}),
|
||||
server = sinon.fakeServer.create();
|
||||
|
||||
var requestJSON;
|
||||
server.respondWith("POST", "/api/histories/fakeHistoryId/contents/dataset_collections", function(request) {
|
||||
requestJSON = JSON.parse(request.requestBody);
|
||||
request.respond(
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
fakeResponse: "yes"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
//console.debug( 'requestBody:', JSON.stringify( requestJSON, null, ' ' ) );
|
||||
pcc.createList("Heres a collection");
|
||||
server.respond();
|
||||
assert.deepEqual(requestJSON, DATA._1requestJSON);
|
||||
server.restore();
|
||||
});
|
||||
*/
|
||||
|
||||
QUnit.test("Creator base/empty construction/initializiation defaults", function(assert) {
|
||||
var pcc = new PCC([]);
|
||||
assert.ok(pcc instanceof PCC);
|
||||
assert.deepEqual(pcc.filters, pcc.commonFilters[pcc.DEFAULT_FILTERS]);
|
||||
assert.ok(pcc.automaticallyPair);
|
||||
assert.equal(pcc.matchPercentage, 0.9);
|
||||
assert.equal(pcc.strategy, "autopairLCS");
|
||||
});
|
||||
|
||||
QUnit.test("Creator construction/initializiation with datasets", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1
|
||||
});
|
||||
//pcc.initialList.forEach( function( dataset, i ){
|
||||
// console.log( i + ':\n' + JSON.stringify( dataset ) );
|
||||
//});
|
||||
// pcc maintains the original list - which, in this case, is already sorted
|
||||
assert.deepEqual(pcc.initialList, DATA._1);
|
||||
// datasets 1 has no ids, so the pcc will create them
|
||||
assert.ok(
|
||||
_.every(pcc.initialList, function(dataset) {
|
||||
return dataset.id;
|
||||
})
|
||||
);
|
||||
// datasets 1 is very easy to auto pair
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with simple exact matching", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "simple",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with LCS", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "lcs",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
QUnit.test("Try easy autopairing with Levenshtein", function(assert) {
|
||||
var pcc = new PCC({
|
||||
datasets: DATA._1,
|
||||
strategy: "levenshtein",
|
||||
twoPassAutopairing: false
|
||||
});
|
||||
assert.equal(pcc.unpaired.length, 0);
|
||||
assert.equal(pcc.paired.length, pcc.initialList.length / 2);
|
||||
});
|
||||
|
||||
//TODO:
|
||||
// filters: clearing, setting via popover, regex
|
||||
// partition: maximize paired, maximize unpaired, split evenly
|
||||
// pairing: manually pairing and unpairing
|
||||
// misc: renaming pairs, removing file extensions
|
||||
@@ -1,124 +1,126 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "layout/masthead", "galaxy"], function(testApp, Masthead, Galaxy) {
|
||||
"use strict";
|
||||
Masthead = Masthead.default;
|
||||
QUnit.module("Masthead test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
var self = this;
|
||||
this.masthead = new Masthead.View({
|
||||
brand: "brand",
|
||||
use_remote_user: "use_remote_user",
|
||||
remote_user_logout_href: "remote_user_logout_href",
|
||||
lims_doc_url: "lims_doc_url",
|
||||
biostar_url: "biostar_url",
|
||||
biostar_url_redirect: "biostar_url_redirect",
|
||||
support_url: "support_url",
|
||||
search_url: "search_url",
|
||||
mailing_lists: "mailing_lists",
|
||||
screencasts_url: "screencasts_url",
|
||||
wiki_url: "wiki_url",
|
||||
citation_url: "citation_url",
|
||||
terms_url: "terms_url",
|
||||
logo_url: "logo_url",
|
||||
logo_src: "../../../static/images/galaxyIcon_noText.png",
|
||||
is_admin_user: "is_admin_user",
|
||||
active_view: "analysis",
|
||||
ftp_upload_dir: "ftp_upload_dir",
|
||||
ftp_upload_site: "ftp_upload_site",
|
||||
datatypes_disable_auto: true,
|
||||
allow_user_creation: true,
|
||||
enable_cloud_launch: true,
|
||||
user_requests: true
|
||||
});
|
||||
$("body").append(this.masthead.render().$el);
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
import testApp from "qunit/test-app";
|
||||
import Masthead from "layout/masthead";
|
||||
import Galaxy from "galaxy";
|
||||
|
||||
QUnit.test("tabs", function(assert) {
|
||||
var tab = this.masthead.collection.findWhere({ id: "analysis" });
|
||||
var $tab = $("#analysis").find(".dropdown");
|
||||
var $toggle = $tab.find(".dropdown-toggle");
|
||||
var $note = $tab.find(".dropdown-note");
|
||||
var $menu = $tab.find("ul");
|
||||
assert.ok(tab && $tab.length == 1, "Found analysis tab");
|
||||
tab.set("title", "Analyze");
|
||||
assert.ok($toggle.html() == "Analyze", "Correct title");
|
||||
assert.ok(tab.get("target") == "_parent", "Correct initial target");
|
||||
tab.set("target", "_target");
|
||||
assert.ok($toggle.attr("target") == "_target", "Correct test target");
|
||||
assert.ok($tab.css("visibility") == "visible", "Tab visible");
|
||||
tab.set("visible", false);
|
||||
assert.ok($tab.css("visibility") == "hidden", "Tab hidden");
|
||||
tab.set("visible", true);
|
||||
assert.ok($tab.css("visibility") == "visible", "Tab visible, again");
|
||||
assert.ok($toggle.attr("href") == Galaxy.root, "Correct initial url");
|
||||
tab.set("url", "_url");
|
||||
assert.ok($toggle.attr("href") == "/_url", "Correct test url");
|
||||
tab.set("url", "http://_url");
|
||||
assert.ok($toggle.attr("href") == "http://_url", "Correct http url");
|
||||
tab.set("tooltip", "_tooltip");
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").html() == "_tooltip", "Correct tooltip");
|
||||
tab.set("tooltip", null);
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").length == 0, "Tooltip removed");
|
||||
tab.set("tooltip", "_tooltip_new");
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").html() == "_tooltip_new", "Correct new tooltip");
|
||||
tab.set("cls", "_cls");
|
||||
assert.ok($toggle.hasClass("_cls"), "Correct extra class");
|
||||
tab.set("cls", "_cls_new");
|
||||
assert.ok($toggle.hasClass("_cls_new") && !$toggle.hasClass("_cls"), "Correct new extra class");
|
||||
assert.ok($note.html() == "", "Correct empty note");
|
||||
tab.set({ note: "_note", show_note: true });
|
||||
assert.ok($note.html() == "_note", "Correct new note");
|
||||
tab.set("toggle", true);
|
||||
assert.ok($toggle.hasClass("toggle"), "Toggled");
|
||||
tab.set("toggle", false);
|
||||
assert.ok(!$toggle.hasClass("toggle"), "Untoggled");
|
||||
tab.set("disabled", true);
|
||||
assert.ok($tab.hasClass("disabled"), "Correctly disabled");
|
||||
tab.set("disabled", false);
|
||||
assert.ok(!$tab.hasClass("disabled"), "Correctly enabled");
|
||||
assert.ok($tab.hasClass("active"), "Highlighted");
|
||||
tab.set("active", false);
|
||||
assert.ok(!$tab.hasClass("active"), "Not highlighted");
|
||||
tab.set("active", true);
|
||||
assert.ok($tab.hasClass("active"), "Highlighted, again");
|
||||
tab.set("menu", [{ title: "_menu_title", url: "_menu_url", target: "_menu_target" }]);
|
||||
assert.ok($menu.hasClass("dropdown-menu"), "Menu has correct class");
|
||||
assert.ok($menu.css("display") == "none", "Menu hidden");
|
||||
$toggle.trigger("click");
|
||||
assert.ok($menu.css("display") == "block", "Menu shown");
|
||||
var $item = $menu.find("a");
|
||||
assert.ok($item.length == 1, "Added one menu item");
|
||||
assert.ok($item.html() == "_menu_title", "Menu item has correct title");
|
||||
assert.ok($item.attr("href") == "/_menu_url", "Menu item has correct url");
|
||||
assert.ok($item.attr("target") == "_menu_target", "Menu item has correct target");
|
||||
tab.set("menu", null);
|
||||
$item = $menu.find("a");
|
||||
assert.ok($item.length == 0, "All menu items removed");
|
||||
tab.set("menu", [
|
||||
{ title: "_menu_title_0", url: "_menu_url_0", target: "_menu_target_0" },
|
||||
{ title: "_menu_title_1", url: "_menu_url_1", target: "_menu_target_1" }
|
||||
]);
|
||||
$item = $menu.find("a");
|
||||
assert.ok($item.length == 2, "Two menu items added");
|
||||
tab.set("show_menu", false);
|
||||
assert.ok($menu.css("display", "none"), "Menu manually hidden");
|
||||
tab.set("show_menu", true);
|
||||
assert.ok($menu.css("display", "block"), "Menu manually shown, again");
|
||||
var tab = this.masthead.collection.findWhere({ id: "enable-scratchbook" });
|
||||
var $tab = $("#enable-scratchbook").find(".dropdown");
|
||||
assert.ok(tab && $tab.length == 1, "Found tab to enable scratchbook");
|
||||
var $toggle = $tab.find(".dropdown-toggle");
|
||||
assert.ok(!$toggle.hasClass("toggle"), "Untoggled before click");
|
||||
$toggle.trigger("click");
|
||||
assert.ok($toggle.hasClass("toggle"), "Toggled after click");
|
||||
assert.ok(Galaxy.frame.active, "Scratchbook is active");
|
||||
});
|
||||
QUnit.module("Masthead test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
var self = this;
|
||||
this.masthead = new Masthead.View({
|
||||
brand: "brand",
|
||||
use_remote_user: "use_remote_user",
|
||||
remote_user_logout_href: "remote_user_logout_href",
|
||||
lims_doc_url: "lims_doc_url",
|
||||
biostar_url: "biostar_url",
|
||||
biostar_url_redirect: "biostar_url_redirect",
|
||||
support_url: "support_url",
|
||||
search_url: "search_url",
|
||||
mailing_lists: "mailing_lists",
|
||||
screencasts_url: "screencasts_url",
|
||||
wiki_url: "wiki_url",
|
||||
citation_url: "citation_url",
|
||||
terms_url: "terms_url",
|
||||
logo_url: "logo_url",
|
||||
logo_src: "../../../static/images/galaxyIcon_noText.png",
|
||||
is_admin_user: "is_admin_user",
|
||||
active_view: "analysis",
|
||||
ftp_upload_dir: "ftp_upload_dir",
|
||||
ftp_upload_site: "ftp_upload_site",
|
||||
datatypes_disable_auto: true,
|
||||
allow_user_creation: true,
|
||||
enable_cloud_launch: true,
|
||||
user_requests: true
|
||||
});
|
||||
$("body").append(this.masthead.render().$el);
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("tabs", function(assert) {
|
||||
var tab = this.masthead.collection.findWhere({ id: "analysis" });
|
||||
var $tab = $("#analysis").find(".dropdown");
|
||||
var $toggle = $tab.find(".dropdown-toggle");
|
||||
var $note = $tab.find(".dropdown-note");
|
||||
var $menu = $tab.find("ul");
|
||||
assert.ok(tab && $tab.length == 1, "Found analysis tab");
|
||||
tab.set("title", "Analyze");
|
||||
assert.ok($toggle.html() == "Analyze", "Correct title");
|
||||
assert.ok(tab.get("target") == "_parent", "Correct initial target");
|
||||
tab.set("target", "_target");
|
||||
assert.ok($toggle.attr("target") == "_target", "Correct test target");
|
||||
assert.ok($tab.css("visibility") == "visible", "Tab visible");
|
||||
tab.set("visible", false);
|
||||
assert.ok($tab.css("visibility") == "hidden", "Tab hidden");
|
||||
tab.set("visible", true);
|
||||
assert.ok($tab.css("visibility") == "visible", "Tab visible, again");
|
||||
// TODO: cleanup global usage so window.Galaxy isn't needed here.
|
||||
assert.ok($toggle.attr("href") == window.Galaxy.root, "Correct initial url");
|
||||
tab.set("url", "_url");
|
||||
assert.ok($toggle.attr("href") == "/_url", "Correct test url");
|
||||
tab.set("url", "http://_url");
|
||||
assert.ok($toggle.attr("href") == "http://_url", "Correct http url");
|
||||
tab.set("tooltip", "_tooltip");
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").html() == "_tooltip", "Correct tooltip");
|
||||
tab.set("tooltip", null);
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").length == 0, "Tooltip removed");
|
||||
tab.set("tooltip", "_tooltip_new");
|
||||
$toggle.trigger("mouseover");
|
||||
assert.ok($(".tooltip-inner").html() == "_tooltip_new", "Correct new tooltip");
|
||||
tab.set("cls", "_cls");
|
||||
assert.ok($toggle.hasClass("_cls"), "Correct extra class");
|
||||
tab.set("cls", "_cls_new");
|
||||
assert.ok($toggle.hasClass("_cls_new") && !$toggle.hasClass("_cls"), "Correct new extra class");
|
||||
assert.ok($note.html() == "", "Correct empty note");
|
||||
tab.set({ note: "_note", show_note: true });
|
||||
assert.ok($note.html() == "_note", "Correct new note");
|
||||
tab.set("toggle", true);
|
||||
assert.ok($toggle.hasClass("toggle"), "Toggled");
|
||||
tab.set("toggle", false);
|
||||
assert.ok(!$toggle.hasClass("toggle"), "Untoggled");
|
||||
tab.set("disabled", true);
|
||||
assert.ok($tab.hasClass("disabled"), "Correctly disabled");
|
||||
tab.set("disabled", false);
|
||||
assert.ok(!$tab.hasClass("disabled"), "Correctly enabled");
|
||||
assert.ok($tab.hasClass("active"), "Highlighted");
|
||||
tab.set("active", false);
|
||||
assert.ok(!$tab.hasClass("active"), "Not highlighted");
|
||||
tab.set("active", true);
|
||||
assert.ok($tab.hasClass("active"), "Highlighted, again");
|
||||
tab.set("menu", [{ title: "_menu_title", url: "_menu_url", target: "_menu_target" }]);
|
||||
assert.ok($menu.hasClass("dropdown-menu"), "Menu has correct class");
|
||||
assert.ok($menu.css("display") == "none", "Menu hidden");
|
||||
$toggle.trigger("click");
|
||||
assert.ok($menu.css("display") == "block", "Menu shown");
|
||||
var $item = $menu.find("a");
|
||||
assert.ok($item.length == 1, "Added one menu item");
|
||||
assert.ok($item.html() == "_menu_title", "Menu item has correct title");
|
||||
assert.ok($item.attr("href") == "/_menu_url", "Menu item has correct url");
|
||||
assert.ok($item.attr("target") == "_menu_target", "Menu item has correct target");
|
||||
tab.set("menu", null);
|
||||
$item = $menu.find("a");
|
||||
assert.ok($item.length == 0, "All menu items removed");
|
||||
tab.set("menu", [
|
||||
{ title: "_menu_title_0", url: "_menu_url_0", target: "_menu_target_0" },
|
||||
{ title: "_menu_title_1", url: "_menu_url_1", target: "_menu_target_1" }
|
||||
]);
|
||||
$item = $menu.find("a");
|
||||
assert.ok($item.length == 2, "Two menu items added");
|
||||
tab.set("show_menu", false);
|
||||
assert.ok($menu.css("display", "none"), "Menu manually hidden");
|
||||
tab.set("show_menu", true);
|
||||
assert.ok($menu.css("display", "block"), "Menu manually shown, again");
|
||||
var tab = this.masthead.collection.findWhere({ id: "enable-scratchbook" });
|
||||
var $tab = $("#enable-scratchbook").find(".dropdown");
|
||||
assert.ok(tab && $tab.length == 1, "Found tab to enable scratchbook");
|
||||
var $toggle = $tab.find(".dropdown-toggle");
|
||||
assert.ok(!$toggle.hasClass("toggle"), "Untoggled before click");
|
||||
$toggle.trigger("click");
|
||||
assert.ok($toggle.hasClass("toggle"), "Toggled after click");
|
||||
// TODO: cleanup global usage so window.Galaxy isn't needed here.
|
||||
assert.ok(window.Galaxy.frame.active, "Scratchbook is active");
|
||||
});
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
// This file isn't really testing anything useful yet, it is just testing
|
||||
// (or demonstrating) qunit+backbone interactions.
|
||||
/* global define */
|
||||
define(["qunit/test-app", "utils/metrics-logger", "jquery", "sinon"], function(testApp, metrics, $, sinon) {
|
||||
"use strict";
|
||||
metrics = metrics.default;
|
||||
|
||||
var MockConsole = function() {
|
||||
var self = this;
|
||||
self.lastMessage = null;
|
||||
["log", "debug", "info", "warn", "error"].forEach(function(fnName) {
|
||||
self[fnName] = function() {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
self.lastMessage = { level: fnName, args: args };
|
||||
};
|
||||
});
|
||||
return self;
|
||||
};
|
||||
|
||||
(window.bootstrapped = {}).user = {
|
||||
id: "test"
|
||||
};
|
||||
|
||||
QUnit.module("Metrics logger tests");
|
||||
// ======================================================================== MetricsLogger
|
||||
QUnit.test("logger construction/initializiation defaults", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({});
|
||||
assert.equal(logger.consoleLogger, null);
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger.options.consoleLevel, metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger.options.defaultNamespace, "Galaxy");
|
||||
assert.equal(logger.options.clientPrefix, "client.");
|
||||
assert.equal(logger.options.postSize, 1000);
|
||||
assert.equal(logger.options.maxCacheSize, 3000);
|
||||
assert.equal(logger.options.addTime, true);
|
||||
assert.equal(logger.options.postUrl, "/api/metrics");
|
||||
assert.equal(logger.options.getPingData, undefined);
|
||||
assert.equal(logger.options.onServerResponse, undefined);
|
||||
|
||||
assert.equal(logger._postSize, 1000);
|
||||
assert.equal(logger.cache.constructor, metrics.LoggingCache);
|
||||
});
|
||||
|
||||
QUnit.test("_parseLevel", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({});
|
||||
assert.equal(logger._parseLevel("all"), metrics.MetricsLogger.ALL);
|
||||
assert.equal(logger._parseLevel("debug"), metrics.MetricsLogger.DEBUG);
|
||||
assert.equal(logger._parseLevel("info"), metrics.MetricsLogger.INFO);
|
||||
assert.equal(logger._parseLevel("warn"), metrics.MetricsLogger.WARN);
|
||||
assert.equal(logger._parseLevel("error"), metrics.MetricsLogger.ERROR);
|
||||
assert.equal(logger._parseLevel("metric"), metrics.MetricsLogger.METRIC);
|
||||
assert.equal(logger._parseLevel("none"), metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger._parseLevel(15), 15);
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
logger._parseLevel(undefined);
|
||||
},
|
||||
/Unknown log level/,
|
||||
"Unknown log level throws error"
|
||||
);
|
||||
assert.throws(
|
||||
function() {
|
||||
logger._parseLevel("nope");
|
||||
},
|
||||
/Unknown log level/,
|
||||
"Unknown log level throws error"
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Emit to cache
|
||||
QUnit.test("emit to cache at level", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.METRIC);
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 1);
|
||||
|
||||
var cached = logger.cache.get(1)[0];
|
||||
//console.debug( 'cached:', JSON.stringify( cached ) );
|
||||
assert.equal(cached.level, metrics.MetricsLogger.METRIC);
|
||||
assert.equal(cached.namespace, "client.test");
|
||||
assert.equal(cached.args.length, 3);
|
||||
assert.equal(cached.args[2].three, 3);
|
||||
assert.ok(typeof cached.time === "string");
|
||||
assert.ok(cached.time === new Date(cached.time).toISOString());
|
||||
});
|
||||
|
||||
QUnit.test("emit to cache below does not cache", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
logger.emit("error", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 0);
|
||||
});
|
||||
|
||||
QUnit.test("emit to cache (silently) drops non-parsable", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
logger.emit("metric", "test", [{ window: window }]);
|
||||
assert.equal(logger.cache.length(), 0);
|
||||
});
|
||||
|
||||
function metricsFromRequestBody(request) {
|
||||
// assumes 'metrics' is only entry in requestBody
|
||||
return JSON.parse(decodeURIComponent(request.requestBody.replace("metrics=", "")));
|
||||
}
|
||||
|
||||
QUnit.test("_postCache success", function(assert) {
|
||||
var callback = sinon.spy(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric",
|
||||
onServerResponse: function(response) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
var server = sinon.fakeServer.create(),
|
||||
metricsOnServer;
|
||||
server.respondWith("POST", "/api/metrics", function(request) {
|
||||
metricsOnServer = metricsFromRequestBody(request);
|
||||
//console.debug( 'requestBody:', request.requestBody );
|
||||
//console.debug( 'metricsOnServer:', JSON.stringify( metricsOnServer, null, ' ' ) );
|
||||
request.respond(
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
fakeResponse: "yes"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
logger._postCache();
|
||||
server.respond();
|
||||
|
||||
assert.ok(callback.calledOnce, "onServerResponse was called");
|
||||
assert.equal(logger.cache.length(), 0, "should have emptied cache (on success)");
|
||||
assert.equal(logger._postSize, 1000, "_postSize still at default");
|
||||
|
||||
// metrics were in proper form on server
|
||||
assert.equal(metricsOnServer.length, 1);
|
||||
var metric = metricsOnServer[0];
|
||||
assert.equal(metric.level, metrics.MetricsLogger.METRIC);
|
||||
assert.equal(metric.namespace, "client.test");
|
||||
assert.equal(metric.args.length, 3);
|
||||
assert.equal(metric.args[2].three, 3);
|
||||
assert.ok(typeof metric.time === "string");
|
||||
assert.ok(metric.time === new Date(metric.time).toISOString());
|
||||
|
||||
server.restore();
|
||||
});
|
||||
|
||||
QUnit.test("_postCache failure", function(assert) {
|
||||
var callback = sinon.spy(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric",
|
||||
onServerResponse: function(response) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
var server = sinon.fakeServer.create();
|
||||
server.respondWith("POST", "/api/metrics", function(request) {
|
||||
request.respond(
|
||||
500,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
err_msg: "NoooOPE!"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
logger._postCache();
|
||||
server.respond();
|
||||
//TODO: is the following what we want?
|
||||
assert.ok(!callback.calledOnce, "onServerResponse was NOT called");
|
||||
assert.equal(logger.cache.length(), 1, "should NOT have emptied cache");
|
||||
assert.equal(logger._postSize, logger.options.maxCacheSize, "_postSize changed to max");
|
||||
|
||||
server.restore();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Emit to console
|
||||
QUnit.test("emit to console at level", function(assert) {
|
||||
var mockConsole = new MockConsole(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
consoleLevel: "debug",
|
||||
consoleLogger: mockConsole
|
||||
});
|
||||
assert.equal(logger.options.consoleLevel, metrics.MetricsLogger.DEBUG);
|
||||
assert.equal(logger.consoleLogger.constructor, MockConsole);
|
||||
|
||||
logger.emit("debug", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 1);
|
||||
//console.debug( JSON.stringify( mockConsole.lastMessage ) );
|
||||
assert.equal(mockConsole.lastMessage.level, "debug");
|
||||
assert.equal(mockConsole.lastMessage.args.length, 4);
|
||||
assert.equal(mockConsole.lastMessage.args[0], "test");
|
||||
assert.equal(mockConsole.lastMessage.args[3].three, 3);
|
||||
});
|
||||
|
||||
QUnit.test("emit to console below does not output", function(assert) {
|
||||
var mockConsole = new MockConsole(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
consoleLevel: "error",
|
||||
consoleLogger: mockConsole
|
||||
});
|
||||
logger.emit("debug", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(mockConsole.lastMessage, null);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Shortcuts
|
||||
QUnit.test("logger shortcuts emit to default namespace properly", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "all"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.ALL);
|
||||
logger.log(0);
|
||||
logger.debug(1);
|
||||
logger.info(2);
|
||||
logger.warn(3);
|
||||
logger.error(4);
|
||||
logger.metric(5);
|
||||
|
||||
assert.equal(logger.cache.length(), 6);
|
||||
var cached = logger.cache.remove(6),
|
||||
entry;
|
||||
|
||||
cached.forEach(function(entry) {
|
||||
assert.ok(entry.namespace === logger.options.clientPrefix + logger.options.defaultNamespace);
|
||||
assert.ok(jQuery.type(entry.args) === "array");
|
||||
assert.ok(typeof entry.time === "string");
|
||||
});
|
||||
|
||||
// log is different
|
||||
entry = cached[0];
|
||||
assert.ok(entry.level === 1);
|
||||
assert.ok(entry.args[0] === 0);
|
||||
|
||||
["debug", "info", "warn", "error", "metric"].forEach(function(level, i) {
|
||||
entry = cached[i + 1];
|
||||
assert.ok(entry.level === logger._parseLevel(level));
|
||||
assert.ok(entry.args[0] === i + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================== LoggingCache
|
||||
QUnit.test("cache construction/initializiation defaults", function(assert) {
|
||||
// use empty to prevent tests stepping on one another due to persistence
|
||||
var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
|
||||
assert.equal(cache.maxSize, 5000);
|
||||
assert.equal(window.localStorage.getItem("logs-test"), "[]");
|
||||
});
|
||||
|
||||
QUnit.test("cache construction/initializiation failure", function(assert) {
|
||||
////TODO: doesn't work - readonly
|
||||
//window.localStorage = null;
|
||||
//console.debug( 'localStorage:', window.localStorage );
|
||||
var oldFn = metrics.LoggingCache.prototype._hasStorage;
|
||||
metrics.LoggingCache.prototype._hasStorage = function() {
|
||||
return false;
|
||||
};
|
||||
assert.throws(
|
||||
function() {
|
||||
return new metrics.LoggingCache({ key: "logs-test" });
|
||||
},
|
||||
/LoggingCache needs localStorage/,
|
||||
"lack of localStorage throws error"
|
||||
);
|
||||
metrics.LoggingCache.prototype._hasStorage = oldFn;
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
return new metrics.LoggingCache();
|
||||
},
|
||||
/LoggingCache needs key for localStorage/,
|
||||
"lack of key throws error"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cache construction/initializiation setting max cache size", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
assert.equal(cache.maxSize, 5);
|
||||
});
|
||||
|
||||
QUnit.test("cache plays well with no data", function(assert) {
|
||||
var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
|
||||
|
||||
assert.equal(cache.length(), 0);
|
||||
var get = cache.get(10);
|
||||
assert.ok(jQuery.type(get) === "array" && get.length === 0);
|
||||
var remove = cache.remove(10);
|
||||
assert.ok(jQuery.type(remove) === "array" && remove.length === 0);
|
||||
assert.equal(cache.length(), 0);
|
||||
});
|
||||
|
||||
QUnit.test("cache add properly adds and removes data", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
|
||||
var entry1 = [{ one: 1 }, "two"];
|
||||
cache.add(entry1);
|
||||
|
||||
assert.equal(cache.length(), 1);
|
||||
assert.equal(JSON.stringify(cache.get(1)[0]), JSON.stringify(entry1));
|
||||
|
||||
var entry2 = { blah: { one: 1 }, bler: ["three", { two: 2 }] };
|
||||
cache.add(entry2);
|
||||
assert.equal(cache.length(), 2);
|
||||
assert.equal(cache.stringify(2), "[" + JSON.stringify(entry1) + "," + JSON.stringify(entry2) + "]");
|
||||
|
||||
// FIFO
|
||||
var returned = cache.remove(1);
|
||||
assert.equal(cache.length(), 1);
|
||||
assert.ok(jQuery.type(returned) === "array" && returned.length === 1);
|
||||
var returned0 = returned[0];
|
||||
assert.ok(jQuery.type(returned0) === "array" && JSON.stringify(returned0) === JSON.stringify(entry1));
|
||||
});
|
||||
|
||||
QUnit.test("cache past max loses oldest", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
|
||||
for (var i = 0; i < 10; i += 1) {
|
||||
cache.add({ index: i });
|
||||
}
|
||||
assert.equal(cache.length(), 5);
|
||||
var get = cache.get(5);
|
||||
assert.ok(get[0].index === 5);
|
||||
assert.ok(get[1].index === 6);
|
||||
assert.ok(get[2].index === 7);
|
||||
assert.ok(get[3].index === 8);
|
||||
assert.ok(get[4].index === 9);
|
||||
});
|
||||
|
||||
QUnit.test("cache is properly persistent", function(assert) {
|
||||
var cache1 = new metrics.LoggingCache({ key: "logs-test" }).empty(),
|
||||
entry = [{ one: 1 }, "two"];
|
||||
cache1.add(entry);
|
||||
assert.equal(cache1.length(), 1);
|
||||
|
||||
var cache2 = new metrics.LoggingCache({ key: "logs-test" });
|
||||
assert.equal(cache2.length(), 1, "old key gets previously stored");
|
||||
assert.equal(JSON.stringify(cache2.get(1)[0]), JSON.stringify(entry));
|
||||
|
||||
var cache3 = new metrics.LoggingCache({ key: "logs-bler" });
|
||||
assert.equal(cache3.length(), 0, "new key causes new storage");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,371 @@
|
||||
// This file isn't really testing anything useful yet, it is just testing
|
||||
// (or demonstrating) qunit+backbone interactions.
|
||||
/* global define */
|
||||
|
||||
import testApp from "qunit/test-app";
|
||||
import metrics from "utils/metrics-logger";
|
||||
import $ from "jquery";
|
||||
import sinon from "sinon";
|
||||
|
||||
var MockConsole = function() {
|
||||
var self = this;
|
||||
self.lastMessage = null;
|
||||
["log", "debug", "info", "warn", "error"].forEach(function(fnName) {
|
||||
self[fnName] = function() {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
self.lastMessage = { level: fnName, args: args };
|
||||
};
|
||||
});
|
||||
return self;
|
||||
};
|
||||
|
||||
(window.bootstrapped = {}).user = {
|
||||
id: "test"
|
||||
};
|
||||
|
||||
QUnit.module("Metrics logger tests");
|
||||
// ======================================================================== MetricsLogger
|
||||
QUnit.test("logger construction/initializiation defaults", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({});
|
||||
assert.equal(logger.consoleLogger, null);
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger.options.consoleLevel, metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger.options.defaultNamespace, "Galaxy");
|
||||
assert.equal(logger.options.clientPrefix, "client.");
|
||||
assert.equal(logger.options.postSize, 1000);
|
||||
assert.equal(logger.options.maxCacheSize, 3000);
|
||||
assert.equal(logger.options.addTime, true);
|
||||
assert.equal(logger.options.postUrl, "/api/metrics");
|
||||
assert.equal(logger.options.getPingData, undefined);
|
||||
assert.equal(logger.options.onServerResponse, undefined);
|
||||
|
||||
assert.equal(logger._postSize, 1000);
|
||||
assert.equal(logger.cache.constructor, metrics.LoggingCache);
|
||||
});
|
||||
|
||||
QUnit.test("_parseLevel", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({});
|
||||
assert.equal(logger._parseLevel("all"), metrics.MetricsLogger.ALL);
|
||||
assert.equal(logger._parseLevel("debug"), metrics.MetricsLogger.DEBUG);
|
||||
assert.equal(logger._parseLevel("info"), metrics.MetricsLogger.INFO);
|
||||
assert.equal(logger._parseLevel("warn"), metrics.MetricsLogger.WARN);
|
||||
assert.equal(logger._parseLevel("error"), metrics.MetricsLogger.ERROR);
|
||||
assert.equal(logger._parseLevel("metric"), metrics.MetricsLogger.METRIC);
|
||||
assert.equal(logger._parseLevel("none"), metrics.MetricsLogger.NONE);
|
||||
assert.equal(logger._parseLevel(15), 15);
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
logger._parseLevel(undefined);
|
||||
},
|
||||
/Unknown log level/,
|
||||
"Unknown log level throws error"
|
||||
);
|
||||
assert.throws(
|
||||
function() {
|
||||
logger._parseLevel("nope");
|
||||
},
|
||||
/Unknown log level/,
|
||||
"Unknown log level throws error"
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Emit to cache
|
||||
QUnit.test("emit to cache at level", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.METRIC);
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 1);
|
||||
|
||||
var cached = logger.cache.get(1)[0];
|
||||
//console.debug( 'cached:', JSON.stringify( cached ) );
|
||||
assert.equal(cached.level, metrics.MetricsLogger.METRIC);
|
||||
assert.equal(cached.namespace, "client.test");
|
||||
assert.equal(cached.args.length, 3);
|
||||
assert.equal(cached.args[2].three, 3);
|
||||
assert.ok(typeof cached.time === "string");
|
||||
assert.ok(cached.time === new Date(cached.time).toISOString());
|
||||
});
|
||||
|
||||
QUnit.test("emit to cache below does not cache", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
logger.emit("error", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 0);
|
||||
});
|
||||
|
||||
QUnit.test("emit to cache (silently) drops non-parsable", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
logger.emit("metric", "test", [{ window: window }]);
|
||||
assert.equal(logger.cache.length(), 0);
|
||||
});
|
||||
|
||||
function metricsFromRequestBody(request) {
|
||||
// assumes 'metrics' is only entry in requestBody
|
||||
return JSON.parse(decodeURIComponent(request.requestBody.replace("metrics=", "")));
|
||||
}
|
||||
|
||||
QUnit.test("_postCache success", function(assert) {
|
||||
var callback = sinon.spy(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric",
|
||||
onServerResponse: function(response) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
var server = sinon.fakeServer.create(),
|
||||
metricsOnServer;
|
||||
server.respondWith("POST", "/api/metrics", function(request) {
|
||||
metricsOnServer = metricsFromRequestBody(request);
|
||||
//console.debug( 'requestBody:', request.requestBody );
|
||||
//console.debug( 'metricsOnServer:', JSON.stringify( metricsOnServer, null, ' ' ) );
|
||||
request.respond(
|
||||
200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
fakeResponse: "yes"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
logger._postCache();
|
||||
server.respond();
|
||||
|
||||
assert.ok(callback.calledOnce, "onServerResponse was called");
|
||||
assert.equal(logger.cache.length(), 0, "should have emptied cache (on success)");
|
||||
assert.equal(logger._postSize, 1000, "_postSize still at default");
|
||||
|
||||
// metrics were in proper form on server
|
||||
assert.equal(metricsOnServer.length, 1);
|
||||
var metric = metricsOnServer[0];
|
||||
assert.equal(metric.level, metrics.MetricsLogger.METRIC);
|
||||
assert.equal(metric.namespace, "client.test");
|
||||
assert.equal(metric.args.length, 3);
|
||||
assert.equal(metric.args[2].three, 3);
|
||||
assert.ok(typeof metric.time === "string");
|
||||
assert.ok(metric.time === new Date(metric.time).toISOString());
|
||||
|
||||
server.restore();
|
||||
});
|
||||
|
||||
QUnit.test("_postCache failure", function(assert) {
|
||||
var callback = sinon.spy(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
logLevel: "metric",
|
||||
onServerResponse: function(response) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
var server = sinon.fakeServer.create();
|
||||
server.respondWith("POST", "/api/metrics", function(request) {
|
||||
request.respond(
|
||||
500,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({
|
||||
err_msg: "NoooOPE!"
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
logger.emit("metric", "test", [1, 2, { three: 3 }]);
|
||||
logger._postCache();
|
||||
server.respond();
|
||||
//TODO: is the following what we want?
|
||||
assert.ok(!callback.calledOnce, "onServerResponse was NOT called");
|
||||
assert.equal(logger.cache.length(), 1, "should NOT have emptied cache");
|
||||
assert.equal(logger._postSize, logger.options.maxCacheSize, "_postSize changed to max");
|
||||
|
||||
server.restore();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Emit to console
|
||||
QUnit.test("emit to console at level", function(assert) {
|
||||
var mockConsole = new MockConsole(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
consoleLevel: "debug",
|
||||
consoleLogger: mockConsole
|
||||
});
|
||||
assert.equal(logger.options.consoleLevel, metrics.MetricsLogger.DEBUG);
|
||||
assert.equal(logger.consoleLogger.constructor, MockConsole);
|
||||
|
||||
logger.emit("debug", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(logger.cache.length(), 1);
|
||||
//console.debug( JSON.stringify( mockConsole.lastMessage ) );
|
||||
assert.equal(mockConsole.lastMessage.level, "debug");
|
||||
assert.equal(mockConsole.lastMessage.args.length, 4);
|
||||
assert.equal(mockConsole.lastMessage.args[0], "test");
|
||||
assert.equal(mockConsole.lastMessage.args[3].three, 3);
|
||||
});
|
||||
|
||||
QUnit.test("emit to console below does not output", function(assert) {
|
||||
var mockConsole = new MockConsole(),
|
||||
logger = new metrics.MetricsLogger({
|
||||
consoleLevel: "error",
|
||||
consoleLogger: mockConsole
|
||||
});
|
||||
logger.emit("debug", "test", [1, 2, { three: 3 }]);
|
||||
assert.equal(mockConsole.lastMessage, null);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------ Shortcuts
|
||||
QUnit.test("logger shortcuts emit to default namespace properly", function(assert) {
|
||||
var logger = new metrics.MetricsLogger({
|
||||
logLevel: "all"
|
||||
});
|
||||
logger.cache.empty();
|
||||
|
||||
assert.equal(logger.options.logLevel, metrics.MetricsLogger.ALL);
|
||||
logger.log(0);
|
||||
logger.debug(1);
|
||||
logger.info(2);
|
||||
logger.warn(3);
|
||||
logger.error(4);
|
||||
logger.metric(5);
|
||||
|
||||
assert.equal(logger.cache.length(), 6);
|
||||
var cached = logger.cache.remove(6),
|
||||
entry;
|
||||
|
||||
cached.forEach(function(entry) {
|
||||
assert.ok(entry.namespace === logger.options.clientPrefix + logger.options.defaultNamespace);
|
||||
assert.ok(jQuery.type(entry.args) === "array");
|
||||
assert.ok(typeof entry.time === "string");
|
||||
});
|
||||
|
||||
// log is different
|
||||
entry = cached[0];
|
||||
assert.ok(entry.level === 1);
|
||||
assert.ok(entry.args[0] === 0);
|
||||
|
||||
["debug", "info", "warn", "error", "metric"].forEach(function(level, i) {
|
||||
entry = cached[i + 1];
|
||||
assert.ok(entry.level === logger._parseLevel(level));
|
||||
assert.ok(entry.args[0] === i + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================== LoggingCache
|
||||
QUnit.test("cache construction/initializiation defaults", function(assert) {
|
||||
// use empty to prevent tests stepping on one another due to persistence
|
||||
var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
|
||||
assert.equal(cache.maxSize, 5000);
|
||||
assert.equal(window.localStorage.getItem("logs-test"), "[]");
|
||||
});
|
||||
|
||||
QUnit.test("cache construction/initializiation failure", function(assert) {
|
||||
////TODO: doesn't work - readonly
|
||||
//window.localStorage = null;
|
||||
//console.debug( 'localStorage:', window.localStorage );
|
||||
var oldFn = metrics.LoggingCache.prototype._hasStorage;
|
||||
metrics.LoggingCache.prototype._hasStorage = function() {
|
||||
return false;
|
||||
};
|
||||
assert.throws(
|
||||
function() {
|
||||
return new metrics.LoggingCache({ key: "logs-test" });
|
||||
},
|
||||
/LoggingCache needs localStorage/,
|
||||
"lack of localStorage throws error"
|
||||
);
|
||||
metrics.LoggingCache.prototype._hasStorage = oldFn;
|
||||
|
||||
assert.throws(
|
||||
function() {
|
||||
return new metrics.LoggingCache();
|
||||
},
|
||||
/LoggingCache needs key for localStorage/,
|
||||
"lack of key throws error"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cache construction/initializiation setting max cache size", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
assert.equal(cache.maxSize, 5);
|
||||
});
|
||||
|
||||
QUnit.test("cache plays well with no data", function(assert) {
|
||||
var cache = new metrics.LoggingCache({ key: "logs-test" }).empty();
|
||||
|
||||
assert.equal(cache.length(), 0);
|
||||
var get = cache.get(10);
|
||||
assert.ok(jQuery.type(get) === "array" && get.length === 0);
|
||||
var remove = cache.remove(10);
|
||||
assert.ok(jQuery.type(remove) === "array" && remove.length === 0);
|
||||
assert.equal(cache.length(), 0);
|
||||
});
|
||||
|
||||
QUnit.test("cache add properly adds and removes data", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
|
||||
var entry1 = [{ one: 1 }, "two"];
|
||||
cache.add(entry1);
|
||||
|
||||
assert.equal(cache.length(), 1);
|
||||
assert.equal(JSON.stringify(cache.get(1)[0]), JSON.stringify(entry1));
|
||||
|
||||
var entry2 = { blah: { one: 1 }, bler: ["three", { two: 2 }] };
|
||||
cache.add(entry2);
|
||||
assert.equal(cache.length(), 2);
|
||||
assert.equal(cache.stringify(2), "[" + JSON.stringify(entry1) + "," + JSON.stringify(entry2) + "]");
|
||||
|
||||
// FIFO
|
||||
var returned = cache.remove(1);
|
||||
assert.equal(cache.length(), 1);
|
||||
assert.ok(jQuery.type(returned) === "array" && returned.length === 1);
|
||||
var returned0 = returned[0];
|
||||
assert.ok(jQuery.type(returned0) === "array" && JSON.stringify(returned0) === JSON.stringify(entry1));
|
||||
});
|
||||
|
||||
QUnit.test("cache past max loses oldest", function(assert) {
|
||||
var cache = new metrics.LoggingCache({
|
||||
key: "logs-test",
|
||||
maxSize: 5
|
||||
}).empty();
|
||||
|
||||
for (var i = 0; i < 10; i += 1) {
|
||||
cache.add({ index: i });
|
||||
}
|
||||
assert.equal(cache.length(), 5);
|
||||
var get = cache.get(5);
|
||||
assert.ok(get[0].index === 5);
|
||||
assert.ok(get[1].index === 6);
|
||||
assert.ok(get[2].index === 7);
|
||||
assert.ok(get[3].index === 8);
|
||||
assert.ok(get[4].index === 9);
|
||||
});
|
||||
|
||||
QUnit.test("cache is properly persistent", function(assert) {
|
||||
var cache1 = new metrics.LoggingCache({ key: "logs-test" }).empty(),
|
||||
entry = [{ one: 1 }, "two"];
|
||||
cache1.add(entry);
|
||||
assert.equal(cache1.length(), 1);
|
||||
|
||||
var cache2 = new metrics.LoggingCache({ key: "logs-test" });
|
||||
assert.equal(cache2.length(), 1, "old key gets previously stored");
|
||||
assert.equal(JSON.stringify(cache2.get(1)[0]), JSON.stringify(entry));
|
||||
|
||||
var cache3 = new metrics.LoggingCache({ key: "logs-bler" });
|
||||
assert.equal(cache3.length(), 0, "new key causes new storage");
|
||||
});
|
||||
@@ -1,92 +1,86 @@
|
||||
/* global define */
|
||||
|
||||
define(["qunit/test-app", "mvc/ui/ui-modal", "sinon"], function(testApp, GalaxyModal, sinon) {
|
||||
"use strict";
|
||||
GalaxyModal = GalaxyModal.default;
|
||||
QUnit.module("Modal dialog test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.clock = sinon.useFakeTimers();
|
||||
var self = this;
|
||||
this.app = new GalaxyModal.View({
|
||||
title: "Test title",
|
||||
body: "Test body",
|
||||
buttons: {
|
||||
Ok: function() {},
|
||||
Cancel: function() {
|
||||
self.app.hide();
|
||||
}
|
||||
import testApp from "qunit/test-app";
|
||||
import GalaxyModal from "mvc/ui/ui-modal";
|
||||
import $ from "jquery";
|
||||
|
||||
QUnit.module("Modal dialog test", {
|
||||
beforeEach: function() {
|
||||
$.fx.off = true;
|
||||
testApp.create();
|
||||
var self = this;
|
||||
this.app = new GalaxyModal.View({
|
||||
title: "Test title",
|
||||
body: "Test body",
|
||||
buttons: {
|
||||
Ok: function() {},
|
||||
Cancel: function() {
|
||||
self.app.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
this.clock.restore();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
$.fx.off = false;
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("test dialog attributes", function(assert) {
|
||||
assert.ok(this.app.$header.find(".title").html() == "Test title", "Modal header has correct title.");
|
||||
assert.ok(this.app.$body.html() == "Test body", "Modal header has correct body.");
|
||||
});
|
||||
QUnit.test("test dialog attributes", function(assert) {
|
||||
assert.ok(this.app.$header.find(".title").html() == "Test title", "Modal header has correct title.");
|
||||
assert.ok(this.app.$body.html() == "Test body", "Modal header has correct body.");
|
||||
});
|
||||
|
||||
QUnit.test("test dialog visibility", function(assert) {
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal is initially visible");
|
||||
this.app.hide();
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden manually");
|
||||
this.app.show();
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown manually");
|
||||
this.app.getButton("Ok").trigger("click");
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal still visible after clicking Ok");
|
||||
this.app.getButton("Cancel").trigger("click");
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden after clicking Cancel");
|
||||
this.app.show();
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal manually shown again");
|
||||
assert.ok(!this.app.$header.hasClass("no-separator"), "Title separator tagged as visible.");
|
||||
this.app.show({ title_separator: false });
|
||||
assert.ok(this.app.$header.hasClass("no-separator"), "Title separator tagged as hidden.");
|
||||
assert.ok(this.app.$backdrop.hasClass("in"), "Backdrop tagged as shown.");
|
||||
this.app.show({ backdrop: false });
|
||||
assert.ok(!this.app.$backdrop.hasClass("in"), "Backdrop tagged as hidden.");
|
||||
});
|
||||
QUnit.test("test dialog visibility", function(assert) {
|
||||
this.app.hide();
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden manually");
|
||||
this.app.show();
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown manually");
|
||||
this.app.getButton("Ok").trigger("click");
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal still visible after clicking Ok");
|
||||
this.app.getButton("Cancel").trigger("click");
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden after clicking Cancel");
|
||||
this.app.show();
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal manually shown again");
|
||||
assert.ok(!this.app.$header.hasClass("no-separator"), "Title separator tagged as visible.");
|
||||
this.app.show({ title_separator: false });
|
||||
assert.ok(this.app.$header.hasClass("no-separator"), "Title separator tagged as hidden.");
|
||||
assert.ok(this.app.$backdrop.hasClass("in"), "Backdrop tagged as shown.");
|
||||
this.app.show({ backdrop: false });
|
||||
assert.ok(!this.app.$backdrop.hasClass("in"), "Backdrop tagged as hidden.");
|
||||
});
|
||||
|
||||
QUnit.test("test dialog closing events", function(assert) {
|
||||
this.app.$backdrop.trigger("click");
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown after backdrop click");
|
||||
this.app.show({ closing_events: true });
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown with closing events");
|
||||
this.app.$backdrop.trigger("click");
|
||||
this.clock.tick(WAIT_FADE);
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden after backdrop click");
|
||||
});
|
||||
QUnit.test("test dialog closing events", function(assert) {
|
||||
this.app.show();
|
||||
this.app.$backdrop.trigger("click");
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown after backdrop click");
|
||||
this.app.hide();
|
||||
this.app.show({ closing_events: true });
|
||||
assert.ok(this.app.$el.css("display") == "block", "Modal shown with closing events");
|
||||
this.app.$backdrop.trigger("click");
|
||||
assert.ok(this.app.$el.css("display") == "none", "Modal hidden after backdrop click");
|
||||
});
|
||||
|
||||
QUnit.test("test dialog rendering", function(assert) {
|
||||
var before = this.app.$el.html();
|
||||
this.app.render();
|
||||
assert.ok(before == this.app.$el.html(), "Re-rendering successful");
|
||||
this.app.options.title = "New Title";
|
||||
this.app.render();
|
||||
assert.ok(this.app.$header.find(".title").html() == "New Title", "Modal header has correct new title.");
|
||||
});
|
||||
QUnit.test("test dialog rendering", function(assert) {
|
||||
var before = this.app.$el.html();
|
||||
this.app.render();
|
||||
assert.ok(before == this.app.$el.html(), "Re-rendering successful");
|
||||
this.app.options.title = "New Title";
|
||||
this.app.render();
|
||||
assert.ok(this.app.$header.find(".title").html() == "New Title", "Modal header has correct new title.");
|
||||
});
|
||||
|
||||
QUnit.test("test button states", function(assert) {
|
||||
assert.ok(this.app.getButton("Ok").html() === "Ok", "Ok has correct label");
|
||||
assert.ok(!this.app.getButton("Ok").prop("disabled"), "Ok is active");
|
||||
assert.ok(!this.app.getButton("Cancel").prop("disabled"), "Cancel is active");
|
||||
this.app.disableButton("Ok");
|
||||
assert.ok(this.app.getButton("Ok").prop("disabled"), "Ok is disabled");
|
||||
assert.ok(!this.app.getButton("Cancel").prop("disabled"), "Cancel is still active");
|
||||
this.app.disableButton("Cancel");
|
||||
assert.ok(this.app.getButton("Cancel").prop("disabled"), "Cancel is also disabled");
|
||||
assert.ok(this.app.getButton("Ok").prop("disabled"), "Ok is still disabled");
|
||||
this.app.enableButton("Ok");
|
||||
assert.ok(this.app.getButton("Cancel").prop("disabled"), "Cancel is still disabled");
|
||||
assert.ok(!this.app.getButton("Ok").prop("disabled"), "Ok is active again");
|
||||
});
|
||||
QUnit.test("test button states", function(assert) {
|
||||
assert.ok(this.app.getButton("Ok").html() === "Ok", "Ok has correct label");
|
||||
assert.ok(!this.app.getButton("Ok").prop("disabled"), "Ok is active");
|
||||
assert.ok(!this.app.getButton("Cancel").prop("disabled"), "Cancel is active");
|
||||
this.app.disableButton("Ok");
|
||||
assert.ok(this.app.getButton("Ok").prop("disabled"), "Ok is disabled");
|
||||
assert.ok(!this.app.getButton("Cancel").prop("disabled"), "Cancel is still active");
|
||||
this.app.disableButton("Cancel");
|
||||
assert.ok(this.app.getButton("Cancel").prop("disabled"), "Cancel is also disabled");
|
||||
assert.ok(this.app.getButton("Ok").prop("disabled"), "Ok is still disabled");
|
||||
this.app.enableButton("Ok");
|
||||
assert.ok(this.app.getButton("Cancel").prop("disabled"), "Cancel is still disabled");
|
||||
assert.ok(!this.app.getButton("Ok").prop("disabled"), "Ok is active again");
|
||||
});
|
||||
|
||||
@@ -1,56 +1,55 @@
|
||||
/* global define */
|
||||
|
||||
define(["qunit/test-app", "layout/page", "layout/panel"], function(testApp, Page, Panel) {
|
||||
"use strict";
|
||||
Page = Page.default;
|
||||
Panel = Panel.default;
|
||||
QUnit.module("Page test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
$("body").append((this.$container = $("<div/>").css("display", "none")));
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
import testApp from "qunit/test-app";
|
||||
import Page from "layout/page";
|
||||
import Panel from "layout/panel";
|
||||
|
||||
function _check(assert, page, sidePanels) {
|
||||
assert.ok(page.$("#center").length == 1, "Center panel found.");
|
||||
_.each(sidePanels, function(panelVisible, panelId) {
|
||||
assert.ok(
|
||||
page.$("#" + panelId).css("display") == panelVisible,
|
||||
(panelVisible ? "" : "No") + " " + panelId + " panel found."
|
||||
);
|
||||
});
|
||||
QUnit.module("Page test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
$("body").append((this.$container = $("<div/>").css("display", "none")));
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("test center/right", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({
|
||||
Right: Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.setElement($("<div/>"));
|
||||
this.model = new Backbone.Model({});
|
||||
}
|
||||
})
|
||||
}).render();
|
||||
_check(assert, page, { left: "none", right: "block" });
|
||||
function _check(assert, page, sidePanels) {
|
||||
assert.ok(page.$("#center").length == 1, "Center panel found.");
|
||||
_.each(sidePanels, function(panelVisible, panelId) {
|
||||
assert.ok(
|
||||
page.$("#" + panelId).css("display") == panelVisible,
|
||||
(panelVisible ? "" : "No") + " " + panelId + " panel found."
|
||||
);
|
||||
});
|
||||
QUnit.test("test center", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({}).render();
|
||||
_check(assert, page, { left: "none", right: "none" });
|
||||
});
|
||||
QUnit.test("test left/center", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({
|
||||
Left: Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.setElement($("<div/>"));
|
||||
this.model = new Backbone.Model({});
|
||||
}
|
||||
})
|
||||
}).render();
|
||||
_check(assert, page, { left: "block", right: "none" });
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.test("test center/right", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({
|
||||
Right: Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.setElement($("<div/>"));
|
||||
this.model = new Backbone.Model({});
|
||||
}
|
||||
})
|
||||
}).render();
|
||||
_check(assert, page, { left: "none", right: "block" });
|
||||
});
|
||||
QUnit.test("test center", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({}).render();
|
||||
_check(assert, page, { left: "none", right: "none" });
|
||||
});
|
||||
QUnit.test("test left/center", function(assert) {
|
||||
this.$container.empty();
|
||||
var page = new Page.View({
|
||||
Left: Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.setElement($("<div/>"));
|
||||
this.model = new Backbone.Model({});
|
||||
}
|
||||
})
|
||||
}).render();
|
||||
_check(assert, page, { left: "block", right: "none" });
|
||||
});
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "mvc/ui/ui-misc", "mvc/ui/ui-popover"], function(testApp, Ui, Popover) {
|
||||
"use strict";
|
||||
Ui = Ui.default;
|
||||
Popover = Popover.default;
|
||||
QUnit.module("Popover test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
var self = this;
|
||||
this.button = new Ui.Button({
|
||||
title: "Test button",
|
||||
onclick: function() {
|
||||
self.popover.show();
|
||||
}
|
||||
});
|
||||
this.$parent = $("<div/>").append(this.button.$el);
|
||||
this.popover = new Popover.View({
|
||||
title: "Test Title",
|
||||
body: "Test Body",
|
||||
placement: "bottom",
|
||||
container: this.button.$el
|
||||
});
|
||||
$("body").append(this.$parent);
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
import testApp from "qunit/test-app";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
import Popover from "mvc/ui/ui-popover";
|
||||
|
||||
QUnit.test("test popover visibility", function(assert) {
|
||||
assert.ok(this.popover.$el.css("display") == "none", "Popover is hidden.");
|
||||
this.button.$el.trigger("click");
|
||||
assert.ok(this.popover.$el.css("display") == "block", "Popover is shown.");
|
||||
assert.ok(this.popover.$el.hasClass("bottom"), "Popover at bottom.");
|
||||
this.popover.hide();
|
||||
assert.ok(this.popover.$el.css("display") == "none", "Popover is hidden manually.");
|
||||
assert.ok(this.popover.$title.html() == "Test Title", "Initial title correct.");
|
||||
this.popover.title("New Title");
|
||||
assert.ok(this.popover.$title.html() == "New Title", "New title correct.");
|
||||
});
|
||||
QUnit.module("Popover test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
var self = this;
|
||||
this.button = new Ui.Button({
|
||||
title: "Test button",
|
||||
onclick: function() {
|
||||
self.popover.show();
|
||||
}
|
||||
});
|
||||
this.$parent = $("<div/>").append(this.button.$el);
|
||||
this.popover = new Popover.View({
|
||||
title: "Test Title",
|
||||
body: "Test Body",
|
||||
placement: "bottom",
|
||||
container: this.button.$el
|
||||
});
|
||||
$("body").append(this.$parent);
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("test popover visibility", function(assert) {
|
||||
assert.ok(this.popover.$el.css("display") == "none", "Popover is hidden.");
|
||||
this.button.$el.trigger("click");
|
||||
assert.ok(this.popover.$el.css("display") == "block", "Popover is shown.");
|
||||
assert.ok(this.popover.$el.hasClass("bottom"), "Popover at bottom.");
|
||||
this.popover.hide();
|
||||
assert.ok(this.popover.$el.css("display") == "none", "Popover is hidden manually.");
|
||||
assert.ok(this.popover.$title.html() == "Test Title", "Initial title correct.");
|
||||
this.popover.title("New Title");
|
||||
assert.ok(this.popover.$title.html() == "New Title", "New title correct.");
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,87 +1,83 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "mvc/upload/upload-view"], function(testApp, GalaxyUpload) {
|
||||
"use strict";
|
||||
GalaxyUpload = GalaxyUpload.default;
|
||||
QUnit.module("Upload dialog test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.app = new GalaxyUpload();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
import testApp from "qunit/test-app";
|
||||
import GalaxyUpload from "mvc/upload/upload-view";
|
||||
|
||||
QUnit.test("test initial dialog state", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
assert.ok(this.app.default_view.collection.length == 0, "Invalid initial upload item collection.");
|
||||
assert.ok($("#btn-start").hasClass("disabled"), "Start button should be disabled.");
|
||||
assert.ok($("#btn-stop").hasClass("disabled"), "Stop button should be disabled.");
|
||||
assert.ok($("#btn-reset").hasClass("disabled"), "Reset button should be disabled.");
|
||||
});
|
||||
QUnit.module("Upload dialog test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.app = new GalaxyUpload();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("test adding/removing paste/fetch upload item", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
$("#btn-new").trigger("click");
|
||||
assert.ok(!$("#btn-start").hasClass("disabled"), "Start button should be enabled.");
|
||||
assert.ok($("#btn-stop").hasClass("disabled"), "Stop button should (still) be disabled.");
|
||||
assert.ok(!$("#btn-reset").hasClass("disabled"), "Reset button should be enabled.");
|
||||
assert.ok(
|
||||
this.app.default_view.collection.length == 1,
|
||||
"Invalid upload item collection length after adding item."
|
||||
);
|
||||
assert.ok(
|
||||
$("#btn-new")
|
||||
.find("i")
|
||||
.hasClass("fa-edit"),
|
||||
"Paste/fetch icon changed"
|
||||
);
|
||||
assert.ok($("#btn-start").hasClass("btn-primary"), "Start button should be enabled/highlighted.");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-symbol")
|
||||
.hasClass("fa-trash-o"),
|
||||
"Should show regular trash icon."
|
||||
);
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings")
|
||||
.trigger("click");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings-cover")
|
||||
.css("display") == "none",
|
||||
"Settings should be enabled."
|
||||
);
|
||||
$("#upload-row-0")
|
||||
.find(".popover-close")
|
||||
.trigger("click");
|
||||
$("#btn-start").trigger("click");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-symbol")
|
||||
.hasClass("fa-exclamation-triangle"),
|
||||
"Upload attempt should have failed."
|
||||
);
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings")
|
||||
.trigger("click")
|
||||
);
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings-cover")
|
||||
.css("display") == "block",
|
||||
"Settings should be disabled."
|
||||
);
|
||||
QUnit.test("test initial dialog state", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
assert.ok(this.app.default_view.collection.length == 0, "Invalid initial upload item collection.");
|
||||
assert.ok($("#btn-start").hasClass("disabled"), "Start button should be disabled.");
|
||||
assert.ok($("#btn-stop").hasClass("disabled"), "Stop button should be disabled.");
|
||||
assert.ok($("#btn-reset").hasClass("disabled"), "Reset button should be disabled.");
|
||||
});
|
||||
|
||||
QUnit.test("test adding/removing paste/fetch upload item", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
$("#btn-new").trigger("click");
|
||||
assert.ok(!$("#btn-start").hasClass("disabled"), "Start button should be enabled.");
|
||||
assert.ok($("#btn-stop").hasClass("disabled"), "Stop button should (still) be disabled.");
|
||||
assert.ok(!$("#btn-reset").hasClass("disabled"), "Reset button should be enabled.");
|
||||
assert.ok(this.app.default_view.collection.length == 1, "Invalid upload item collection length after adding item.");
|
||||
assert.ok(
|
||||
$("#btn-new")
|
||||
.find("i")
|
||||
.hasClass("fa-edit"),
|
||||
"Paste/fetch icon changed"
|
||||
);
|
||||
assert.ok($("#btn-start").hasClass("btn-primary"), "Start button should be enabled/highlighted.");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-symbol")
|
||||
.trigger("click");
|
||||
assert.ok(this.app.default_view.collection.length == 0, "Removing item from collection failed.");
|
||||
});
|
||||
.hasClass("fa-trash-o"),
|
||||
"Should show regular trash icon."
|
||||
);
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings")
|
||||
.trigger("click");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings-cover")
|
||||
.css("display") == "none",
|
||||
"Settings should be enabled."
|
||||
);
|
||||
$("#upload-row-0")
|
||||
.find(".popover-close")
|
||||
.trigger("click");
|
||||
$("#btn-start").trigger("click");
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-symbol")
|
||||
.hasClass("fa-exclamation-triangle"),
|
||||
"Upload attempt should have failed."
|
||||
);
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings")
|
||||
.trigger("click")
|
||||
);
|
||||
assert.ok(
|
||||
$("#upload-row-0")
|
||||
.find(".upload-settings-cover")
|
||||
.css("display") == "block",
|
||||
"Settings should be disabled."
|
||||
);
|
||||
$("#upload-row-0")
|
||||
.find(".upload-symbol")
|
||||
.trigger("click");
|
||||
assert.ok(this.app.default_view.collection.length == 0, "Removing item from collection failed.");
|
||||
});
|
||||
|
||||
QUnit.test("test ftp popup", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
$("#btn-ftp").trigger("click");
|
||||
assert.ok($(".upload-ftp").length == 1, "Should show ftp popover.");
|
||||
});
|
||||
QUnit.test("test ftp popup", function(assert) {
|
||||
$(this.app.ui_button.$el).trigger("click");
|
||||
$("#btn-ftp").trigger("click");
|
||||
assert.ok($(".upload-ftp").length == 1, "Should show ftp popover.");
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/* global define */
|
||||
define(["qunit/test-app", "utils/utils"], function(testApp, Utils) {
|
||||
"use strict";
|
||||
Utils = Utils.default;
|
||||
QUnit.module("Utils test", {});
|
||||
|
||||
QUnit.test("isEmpty", function(assert) {
|
||||
assert.ok(Utils.isEmpty([]), "Empty array");
|
||||
assert.ok(Utils.isEmpty(["data", undefined]), "Array contains `undefined`");
|
||||
assert.ok(Utils.isEmpty(["data", null]), "Array contains `null`");
|
||||
assert.ok(Utils.isEmpty(["data", "__null__"]), "Array contains `__null__`");
|
||||
assert.ok(Utils.isEmpty(["data", "__undefined__"]), "Array contains `__undefined__`");
|
||||
assert.ok(Utils.isEmpty(null), "Array is null");
|
||||
assert.ok(Utils.isEmpty("__null__"), "Array is __null__");
|
||||
assert.ok(Utils.isEmpty("__undefined__"), "Array is __undefined__");
|
||||
assert.ok(!Utils.isEmpty(["data"]), "Array contains `data`");
|
||||
assert.ok(!Utils.isEmpty(1), "Value is int");
|
||||
assert.ok(!Utils.isEmpty(0), "Value is zero");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/* global define */
|
||||
import testApp from "qunit/test-app";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
QUnit.module("Utils test", {});
|
||||
|
||||
QUnit.test("isEmpty", function(assert) {
|
||||
assert.ok(Utils.isEmpty([]), "Empty array");
|
||||
assert.ok(Utils.isEmpty(["data", undefined]), "Array contains `undefined`");
|
||||
assert.ok(Utils.isEmpty(["data", null]), "Array contains `null`");
|
||||
assert.ok(Utils.isEmpty(["data", "__null__"]), "Array contains `__null__`");
|
||||
assert.ok(Utils.isEmpty(["data", "__undefined__"]), "Array contains `__undefined__`");
|
||||
assert.ok(Utils.isEmpty(null), "Array is null");
|
||||
assert.ok(Utils.isEmpty("__null__"), "Array is __null__");
|
||||
assert.ok(Utils.isEmpty("__undefined__"), "Array is __undefined__");
|
||||
assert.ok(!Utils.isEmpty(["data"]), "Array contains `data`");
|
||||
assert.ok(!Utils.isEmpty(1), "Value is int");
|
||||
assert.ok(!Utils.isEmpty(0), "Value is zero");
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -572,7 +572,7 @@ var TracksterView = Backbone.View.extend({
|
||||
// close selection/finalize template
|
||||
html += `</select></div><div style="clear: both;"></div></div><div class="form-row">Is the build not listed here? <a href="${
|
||||
Galaxy.root
|
||||
}user/dbkeys?use_panels=True">Add a Custom Build</a></div></form>`;
|
||||
}custom_builds">Add a Custom Build</a></div></form>`;
|
||||
|
||||
// return
|
||||
return html;
|
||||
|
||||
@@ -1108,6 +1108,37 @@ ul.manage-table-actions li {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.state-progress {
|
||||
border: 1px solid gray;
|
||||
position: relative;
|
||||
|
||||
margin-top: 2px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
.info {
|
||||
color: @black;
|
||||
background: @white;
|
||||
}
|
||||
|
||||
.new {
|
||||
background: @state-default-bg;
|
||||
}
|
||||
|
||||
.running {
|
||||
background: @state-running-bg;
|
||||
}
|
||||
|
||||
.ok {
|
||||
background: @state-success-bg;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-left: 1em;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// State colors
|
||||
|
||||
.state-color-new {
|
||||
|
||||
@@ -176,6 +176,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.state-loading {
|
||||
background: @state-default-bg;
|
||||
.state-icon {
|
||||
.state-icon-running;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.blinking {
|
||||
animation: blinker 500ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------- datasets as list-items
|
||||
|
||||
@@ -86,6 +86,13 @@
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
.state-description {
|
||||
color: #777;
|
||||
font-size: 90%;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary-actions {
|
||||
|
||||
+9
-5
@@ -40,20 +40,24 @@ var paths = {
|
||||
libs: ['galaxy/scripts/libs/**/*.js']
|
||||
};
|
||||
|
||||
var nopack_mode = function(){
|
||||
return process.env.GXY_NOPACK || false;
|
||||
var dev_mode = function(){
|
||||
return process.env.NODE_ENV != "production";
|
||||
};
|
||||
|
||||
var source_maps = function(){
|
||||
return dev_mode() || process.env.GXY_BUILD_SOURCEMAPS !== undefined;
|
||||
};
|
||||
|
||||
gulp.task('scripts', function() {
|
||||
return gulp.src(paths.scripts)
|
||||
.pipe(plumber())
|
||||
.pipe(cached('scripts'))
|
||||
//.pipe(sourcemaps.init())
|
||||
.pipe(gulpif(source_maps, sourcemaps.init()))
|
||||
.pipe(babel({
|
||||
plugins: ['transform-es2015-modules-amd']
|
||||
}))
|
||||
.pipe(gulpif(nopack_mode, beautify(), uglify()))
|
||||
//.pipe(sourcemaps.write('../maps/'))
|
||||
.pipe(gulpif(dev_mode, beautify(), uglify()))
|
||||
.pipe(gulpif(source_maps, sourcemaps.write('../maps/')))
|
||||
.pipe(gulp.dest('../static/scripts/'));
|
||||
});
|
||||
|
||||
|
||||
+16
-15
@@ -15,29 +15,30 @@ var single_pack_mode = function(){
|
||||
};
|
||||
|
||||
var TESTS_SEPARATE_PACKS = [
|
||||
{pattern: 'galaxy/scripts/qunit/tests/list-of-pairs-collection-creator.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/galaxy-app-base.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/graph.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/hda-base.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/galaxy_app_base_tests.js', watched: false},
|
||||
// Something is funky with form_tests.js - needs to come before all other tests.
|
||||
{pattern: 'galaxy/scripts/qunit/tests/form_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/upload_dialog_tests.js', watched: false},
|
||||
// Something is funky with masthead_tests - needs to come before one of the other
|
||||
// tests - not sure which though...
|
||||
{pattern: 'galaxy/scripts/qunit/tests/masthead_tests.js', watched: false},
|
||||
//{pattern: 'galaxy/scripts/qunit/tests/list_of_pairs_collection_creator_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/graph_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/hda_base_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/history_contents_model_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/job-dag.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/metrics-logger.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/job_dag_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/metrics_logger_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/popover_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/utils_test.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/utils_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/page_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/workflow_editor_tests.js', watched: false},
|
||||
{pattern: 'galaxy/scripts/qunit/tests/modal_tests.js', watched: false},
|
||||
// The following tests don't work for state reasons:
|
||||
|
||||
// Error: Following test works on its own or with rest but not with
|
||||
// list-of-pairs-collection-creator in the same suite. Not as much isolation
|
||||
// list_of_pairs_collection_creator in the same suite. Not as much isolation
|
||||
// as seperate page setup of previous runner.
|
||||
// 'galaxy/scripts/qunit/tests/ui_tests.js',
|
||||
// Error: things displayed wrong I guess cause assertions fail on CSS stuff
|
||||
// 'galaxy/scripts/qunit/tests/modal_tests.js',
|
||||
// 'galaxy/scripts/qunit/tests/upload_dialog_tests.js',
|
||||
// Error: Cannot find module "libs/bibtexParse"
|
||||
// 'galaxy/scripts/qunit/tests/form_tests.js',
|
||||
// 'galaxy/scripts/qunit/tests/masthead_tests.js',
|
||||
// {pattern: 'galaxy/scripts/qunit/tests/ui_tests.js', watched: false},
|
||||
];
|
||||
|
||||
var TESTS_AS_SINGLE_PACK = [
|
||||
|
||||
+11
-7
@@ -30,16 +30,20 @@
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "gulp stage-libs && concurrently \"yarn run webpack-watch\" \"yarn run gulp watch\" \"yarn run style-watch\"",
|
||||
"build": "gulp stage-libs && concurrently \"yarn run style\" \"yarn run webpack\" \"yarn run gulp clean && yarn run gulp\"",
|
||||
"webpack": "webpack -p",
|
||||
"webpack-watch": "webpack -p --watch",
|
||||
"webpack-dev": "webpack -d",
|
||||
"webpack-dev-watch": "webpack -d --watch",
|
||||
"build": "NODE_ENV=development gulp stage-libs && concurrently \"yarn run style\" \"yarn run webpack\" \"yarn run gulp clean && yarn run gulp\"",
|
||||
"build-production": "NODE_ENV=production gulp stage-libs && concurrently \"yarn run style\" \"yarn run webpack-production\" \"yarn run gulp clean && yarn run gulp-production\"",
|
||||
"build-production-maps": "NODE_ENV=production gulp stage-libs && concurrently \"yarn run style\" \"yarn run webpack-production-maps\" \"yarn run gulp clean && yarn run gulp-production-maps\"",
|
||||
"webpack": "webpack -d",
|
||||
"webpack-watch": "webpack -d --watch",
|
||||
"webpack-production": "webpack -p",
|
||||
"webpack-production-maps": "GXY_BUILD_SOURCEMAPS=1 webpack -p",
|
||||
"style": "grunt style",
|
||||
"style-watch": "grunt watch-style",
|
||||
"test": "karma start --verbose --single-run --browsers PhantomJS karma.config.js",
|
||||
"test-watch": "karma start --verbose --browsers PhantomJS karma.config.js",
|
||||
"test": "GALAXY_TEST_AS_SINGLE_PACK=true karma start --verbose --single-run --browsers PhantomJS karma.config.js",
|
||||
"test-watch": "GALAXY_TEST_AS_SINGLE_PACK=true karma start --verbose --browsers PhantomJS karma.config.js",
|
||||
"gulp": "gulp",
|
||||
"gulp-production": "NODE_ENV=production gulp",
|
||||
"gulp-production-maps": "GXY_BUILD_SOURCEMAPS=1 NODE_ENV=production gulp",
|
||||
"build-toolshed": "grunt --app=toolshed",
|
||||
"jshint": "jshint --exclude='galaxy/scripts/libs/**' galaxy/scripts/**/*.js",
|
||||
"build-charts": "webpack -p --config ../config/plugins/visualizations/charts/webpack.config.js",
|
||||
|
||||
@@ -37,8 +37,7 @@ var commonLibs = [
|
||||
"onload"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
//devtool: "source-map",
|
||||
let buildconfig = {
|
||||
entry: {
|
||||
libs: commonLibs,
|
||||
login: "./galaxy/scripts/apps/login.js",
|
||||
@@ -104,6 +103,9 @@ module.exports = {
|
||||
name: "libs",
|
||||
filename: "libs.bundled.js"
|
||||
}),
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
filename: '[name].js.map',
|
||||
}),
|
||||
// this plugin allows using the following keys/globals in scripts (w/o req'ing them first)
|
||||
// and webpack will automagically require them in the bundle for you
|
||||
new webpack.ProvidePlugin({
|
||||
@@ -116,3 +118,9 @@ module.exports = {
|
||||
// new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
|
||||
]
|
||||
};
|
||||
|
||||
if (process.env.GXY_BUILD_SOURCEMAPS){
|
||||
buildconfig.devtool = 'source-map';
|
||||
}
|
||||
|
||||
module.exports = buildconfig;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<datatype extension="len" type="galaxy.datatypes.chrominfo:ChromInfo" display_in_upload="true">
|
||||
<converter file="len_to_linecount.xml" target_datatype="linecount" />
|
||||
</datatype>
|
||||
<datatype extension="dmnd" type="galaxy.datatypes.binary:Binary:DMND" display_in_upload="false"/>
|
||||
<datatype extension="dmnd" type="galaxy.datatypes.binary:DMND" display_in_upload="false"/>
|
||||
<datatype extension="bigbed" type="galaxy.datatypes.binary:BigBed" mimetype="application/octet-stream" display_in_upload="true">
|
||||
<display file="ucsc/bigbed.xml" />
|
||||
<display file="igb/bb.xml" />
|
||||
@@ -146,7 +146,7 @@
|
||||
<datatype extension="toolshed.gz" type="galaxy.datatypes.binary:Binary" mimetype="multipart/x-gzip" subclass="true" />
|
||||
<datatype extension="h5" type="galaxy.datatypes.binary:H5" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
<datatype extension="mz5" type="galaxy.datatypes.binary:H5" subclass="true" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
<datatype extension="cool" type="galaxy.datatypes.binary:Cool" subclass="true" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
<datatype extension="cool" type="galaxy.datatypes.binary:Cool" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
<datatype extension="html" type="galaxy.datatypes.text:Html" mimetype="text/html"/>
|
||||
<datatype extension="interval" type="galaxy.datatypes.interval:Interval" display_in_upload="true" description="File must start with definition line in the following format (columns may be in any order)." >
|
||||
<converter file="interval_to_bed_converter.xml" target_datatype="bed"/>
|
||||
@@ -237,7 +237,7 @@
|
||||
<datatype extension="sf3" type="galaxy.datatypes.proteomics:Sf3" display_in_upload="true" />
|
||||
<datatype extension="cps" type="galaxy.datatypes.binary:Binary" subclass="true" display_in_upload="true" />
|
||||
<datatype extension="ct" type="galaxy.datatypes.tabular:ConnectivityTable" display_in_upload="true"/>
|
||||
<datatype extension="postgresql" type="galaxy.datatypes.binary:PostgresqlArchive" subclass="True" display_in_upload="True"/>
|
||||
<datatype extension="postgresql" type="galaxy.datatypes.binary:PostgresqlArchive" display_in_upload="True"/>
|
||||
<datatype extension="searchgui_archive" type="galaxy.datatypes.binary:SearchGuiArchive" display_in_upload="true"/>
|
||||
<datatype extension="fast5.tar" type="galaxy.datatypes.binary:Fast5Archive" display_in_upload="true"/>
|
||||
<datatype extension="fast5.tar.gz" type="galaxy.datatypes.binary:Fast5ArchiveGz" display_in_upload="true"/>
|
||||
@@ -282,13 +282,13 @@
|
||||
<datatype extension="qualillumina" type="galaxy.datatypes.qualityscore:QualityScoreIllumina" display_in_upload="true"/>
|
||||
<datatype extension="qualsolid" type="galaxy.datatypes.qualityscore:QualityScoreSOLiD" display_in_upload="true"/>
|
||||
<datatype extension="qual454" type="galaxy.datatypes.qualityscore:QualityScore454" display_in_upload="true"/>
|
||||
<datatype extension="Roadmaps" type="galaxy.datatypes.assembly:Roadmaps" display_in_upload="false"/>
|
||||
<datatype extension="roadmaps" type="galaxy.datatypes.assembly:Roadmaps" display_in_upload="false"/>
|
||||
<datatype extension="sam" type="galaxy.datatypes.tabular:Sam" display_in_upload="true">
|
||||
<converter file="sam_to_bam.xml" target_datatype="bam"/>
|
||||
<converter file="sam_to_bigwig_converter.xml" target_datatype="bigwig"/>
|
||||
</datatype>
|
||||
<datatype extension="scf" type="galaxy.datatypes.binary:Scf" mimetype="application/octet-stream" display_in_upload="true" description="A binary sequence file in 'scf' format with a '.scf' file extension. You must manually select this 'File Format' when uploading the file." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Scf"/>
|
||||
<datatype extension="Sequences" type="galaxy.datatypes.assembly:Sequences" display_in_upload="false"/>
|
||||
<datatype extension="sequences" type="galaxy.datatypes.assembly:Sequences" display_in_upload="false"/>
|
||||
<datatype extension="snpeffdb" type="galaxy.datatypes.text:SnpEffDb" display_in_upload="true"/>
|
||||
<datatype extension="snpsiftdbnsfp" type="galaxy.datatypes.text:SnpSiftDbNSFP" display_in_upload="true"/>
|
||||
<datatype extension="dbnsfp.tabular" type="galaxy.datatypes.tabular:Tabular" subclass="true" display_in_upload="true">
|
||||
@@ -518,7 +518,7 @@
|
||||
<datatype extension="vtkascii" type="galaxy.datatypes.constructive_solid_geometry:VtkAscii" display_in_upload="true" />
|
||||
<datatype extension="vtkbinary" type="galaxy.datatypes.constructive_solid_geometry:VtkBinary" display_in_upload="true" />
|
||||
<!-- Metagenomic Datatypes -->
|
||||
<datatype extension="biom1" type="galaxy.datatypes.text:Biom1" display_in_upload="true" subclass="true" mimetype="application/json">
|
||||
<datatype extension="biom1" type="galaxy.datatypes.text:Biom1" display_in_upload="true" mimetype="application/json">
|
||||
<display file="biom/biom_simple.xml" />
|
||||
<converter file="biom1_to_biom2.xml" target_datatype="biom2"/>
|
||||
</datatype>
|
||||
|
||||
@@ -1122,6 +1122,14 @@ use_interactive = True
|
||||
# invocation to schedule indefinitely. The default corresponds to 1 month.
|
||||
#maximum_workflow_invocation_duration = 2678400
|
||||
|
||||
# Specify a maximum number of jobs that any given workflow scheduling iteration can create.
|
||||
# Set this to a positive integer to prevent large collection jobs in a workflow from
|
||||
# preventing other jobs from executing. This may also mitigate memory issues associated with
|
||||
# scheduling workflows at the expense of increased total DB traffic because model objects
|
||||
# are expunged from the SQL alchemy session between workflow invocation scheduling iterations.
|
||||
# Set to -1 to disable any such maximum (the default).
|
||||
#maximum_workflow_jobs_per_scheduling_iteration = -1
|
||||
|
||||
# Force serial scheduling of workflows within the context of a particular history
|
||||
#history_local_serial_workflow_scheduling=False
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# appropriate `apt-get/pip install` statements.
|
||||
---
|
||||
-
|
||||
image: quay.io/bgruening/docker-jupyter-notebook:17.05
|
||||
image: quay.io/bgruening/docker-jupyter-notebook:17.09
|
||||
description: |
|
||||
The Jupyter notebook is the next iteration of IPython, allowing
|
||||
analysis in many different languages. This image features the Python,
|
||||
|
||||
@@ -385,6 +385,7 @@ class Configuration(object):
|
||||
self.history_local_serial_workflow_scheduling = string_as_bool(kwargs.get('history_local_serial_workflow_scheduling', 'False'))
|
||||
self.parallelize_workflow_scheduling_within_histories = string_as_bool(kwargs.get('parallelize_workflow_scheduling_within_histories', 'False'))
|
||||
self.maximum_workflow_invocation_duration = int(kwargs.get("maximum_workflow_invocation_duration", 2678400))
|
||||
self.maximum_workflow_jobs_per_scheduling_iteration = int(kwargs.get("maximum_workflow_jobs_per_scheduling_iteration", -1))
|
||||
|
||||
self.cache_user_job_count = string_as_bool(kwargs.get('cache_user_job_count', False))
|
||||
self.pbs_application_server = kwargs.get('pbs_application_server', "")
|
||||
|
||||
@@ -24,6 +24,7 @@ def set_collection_elements(dataset_collection, type, dataset_instances):
|
||||
element_index += 1
|
||||
|
||||
dataset_collection.elements = elements
|
||||
dataset_collection.element_count = element_index
|
||||
return dataset_collection
|
||||
|
||||
|
||||
|
||||
@@ -44,27 +44,35 @@ class MatchingCollections(object):
|
||||
self.linked_structure = None
|
||||
self.unlinked_structures = []
|
||||
self.collections = {}
|
||||
self.subcollection_types = {}
|
||||
|
||||
def __attempt_add_to_linked_match(self, input_name, hdca, collection_type_description, subcollection_type):
|
||||
structure = get_structure(hdca, collection_type_description, leaf_subcollection_type=subcollection_type)
|
||||
if not self.linked_structure:
|
||||
self.linked_structure = structure
|
||||
self.collections[input_name] = hdca
|
||||
self.subcollection_types[input_name] = subcollection_type
|
||||
else:
|
||||
if not self.linked_structure.can_match(structure):
|
||||
raise exceptions.MessageException(CANNOT_MATCH_ERROR_MESSAGE)
|
||||
self.collections[input_name] = hdca
|
||||
self.subcollection_types[input_name] = subcollection_type
|
||||
|
||||
def slice_collections(self):
|
||||
return self.linked_structure.walk_collections(self.collections)
|
||||
|
||||
def subcollection_mapping_type(self, input_name):
|
||||
return self.subcollection_types[input_name]
|
||||
|
||||
@property
|
||||
def structure(self):
|
||||
"""Yield cross product of all unlinked datasets to linked dataset."""
|
||||
"""Yield cross product of all unlinked collections structures to linked collection structure."""
|
||||
effective_structure = leaf
|
||||
for unlinked_structure in self.unlinked_structures:
|
||||
effective_structure = effective_structure.multiply(unlinked_structure)
|
||||
linked_structure = self.linked_structure or leaf
|
||||
linked_structure = self.linked_structure
|
||||
if linked_structure is None:
|
||||
linked_structure = leaf
|
||||
effective_structure = effective_structure.multiply(linked_structure)
|
||||
return None if effective_structure.is_leaf else effective_structure
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
""" Module for reasoning about structure of and matching hierarchical collections of data.
|
||||
"""
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import six
|
||||
|
||||
from .type_description import map_over_collection_type
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class Leaf(object):
|
||||
children_known = True
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
@@ -18,18 +23,60 @@ class Leaf(object):
|
||||
def clone(self):
|
||||
return self
|
||||
|
||||
def multiply(self, other_structure):
|
||||
return other_structure.clone()
|
||||
def multiply(self, other_structure, uninitialized=False):
|
||||
if not uninitialized:
|
||||
return other_structure.clone()
|
||||
else:
|
||||
return UnitializedTree(other_structure.collection_type_description)
|
||||
|
||||
def sliced_collection_type(self, collection):
|
||||
return input
|
||||
|
||||
def __str__(self):
|
||||
return "Leaf[]"
|
||||
|
||||
|
||||
leaf = Leaf()
|
||||
|
||||
|
||||
class Tree(object):
|
||||
class BaseTree(object):
|
||||
|
||||
def __init__(self, collection_type_description):
|
||||
self.collection_type_description = collection_type_description
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class UnitializedTree(BaseTree):
|
||||
children_known = False
|
||||
|
||||
def clone(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def is_leaf(self):
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
raise Exception("Unknown length")
|
||||
|
||||
def multiply(self, other_structure, uninitialized=False):
|
||||
if other_structure.is_leaf:
|
||||
return self.clone()
|
||||
|
||||
new_collection_type = self.collection_type_description.multiply(other_structure.collection_type_description)
|
||||
return UnitializedTree(new_collection_type)
|
||||
|
||||
def __str__(self):
|
||||
return "UnitializedTree[collection_type=%s]" % self.collection_type_description
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class Tree(BaseTree):
|
||||
children_known = True
|
||||
|
||||
def __init__(self, children, collection_type_description):
|
||||
super(Tree, self).__init__(collection_type_description)
|
||||
self.children = children
|
||||
self.collection_type_description = collection_type_description
|
||||
|
||||
@staticmethod
|
||||
def for_dataset_collection(dataset_collection, collection_type_description):
|
||||
@@ -107,14 +154,14 @@ class Tree(object):
|
||||
element_identifiers=element_identifiers,
|
||||
)
|
||||
|
||||
def multiply(self, other_structure):
|
||||
def multiply(self, other_structure, uninitialized=False):
|
||||
if other_structure.is_leaf:
|
||||
return self.clone()
|
||||
|
||||
new_collection_type = self.collection_type_description.multiply(other_structure.collection_type_description)
|
||||
new_children = []
|
||||
for (identifier, structure) in self.children:
|
||||
new_children.append((identifier, structure.multiply(other_structure)))
|
||||
new_children.append((identifier, structure.multiply(other_structure, uninitialized=uninitialized)))
|
||||
|
||||
return Tree(new_children, new_collection_type)
|
||||
|
||||
@@ -122,6 +169,30 @@ class Tree(object):
|
||||
cloned_children = [(_[0], _[1].clone()) for _ in self.children]
|
||||
return Tree(cloned_children, self.collection_type_description)
|
||||
|
||||
def __str__(self):
|
||||
return "Tree[collection_type=%s,children=%s]" % (self.collection_type_description, ",".join(map(lambda identifier_and_element: "%s=%s" % (identifier_and_element[0], identifier_and_element[1]), self.children)))
|
||||
|
||||
|
||||
def tool_output_to_structure(get_sliced_input_collection_type, tool_output, collections_manager):
|
||||
if not tool_output.collection:
|
||||
tree = leaf
|
||||
else:
|
||||
collection_type_descriptions = collections_manager.collection_type_descriptions
|
||||
# Okay this is ToolCollectionOutputStructure not a Structure - different
|
||||
# concepts of structure.
|
||||
if tool_output.dynamic_structure:
|
||||
# Two cases collection_type_source and collection_type right?
|
||||
tree = UnitializedTree(collection_type_descriptions.for_type_description("list")) # list is obviously wrong...
|
||||
else:
|
||||
structured_like = tool_output.structure.structured_like
|
||||
if structured_like:
|
||||
collection_type = get_sliced_input_collection_type(structured_like)
|
||||
else:
|
||||
collection_type = tool_output.structure.collection_type
|
||||
tree = UnitializedTree(collection_type)
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
def dict_map(func, input_dict):
|
||||
return dict((k, func(v)) for k, v in input_dict.items())
|
||||
@@ -131,4 +202,5 @@ def get_structure(dataset_collection_instance, collection_type_description, leaf
|
||||
if leaf_subcollection_type:
|
||||
collection_type_description = collection_type_description.effective_collection_type_description(leaf_subcollection_type)
|
||||
|
||||
return Tree.for_dataset_collection(dataset_collection_instance.collection, collection_type_description)
|
||||
collection = dataset_collection_instance.collection
|
||||
return Tree.for_dataset_collection(collection, collection_type_description)
|
||||
|
||||
@@ -8,6 +8,7 @@ class CollectionTypeDescriptionFactory(object):
|
||||
self.type_registry = type_registry
|
||||
|
||||
def for_collection_type(self, collection_type):
|
||||
assert collection_type is not None
|
||||
return CollectionTypeDescription(collection_type, self)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import tarfile
|
||||
|
||||
from galaxy.datatypes.binary import Binary, CompressedArchive
|
||||
from galaxy.datatypes.binary import CompressedArchive
|
||||
from galaxy.datatypes.data import get_file_peek, Text
|
||||
from galaxy.util import nice_size
|
||||
|
||||
@@ -82,6 +82,3 @@ class Augustus(CompressedArchive):
|
||||
except Exception as e:
|
||||
log.warning('%s, sniff Exception: %s', self, e)
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("augustus", "augustus", Augustus)
|
||||
|
||||
@@ -71,6 +71,7 @@ class Amos(data.Text):
|
||||
class Sequences(sequence.Fasta):
|
||||
"""Class describing the Sequences file generated by velveth """
|
||||
edam_data = "data_0925"
|
||||
file_ext = 'sequences'
|
||||
|
||||
def sniff(self, filename):
|
||||
"""
|
||||
@@ -110,6 +111,7 @@ class Sequences(sequence.Fasta):
|
||||
class Roadmaps(data.Text):
|
||||
"""Class describing the Sequences file generated by velveth """
|
||||
edam_format = "format_2561"
|
||||
file_ext = 'roadmaps'
|
||||
|
||||
def sniff(self, filename):
|
||||
"""
|
||||
|
||||
+11
-116
@@ -33,33 +33,17 @@ log = logging.getLogger(__name__)
|
||||
class Binary(data.Data):
|
||||
"""Binary data"""
|
||||
edam_format = "format_2333"
|
||||
sniffable_binary_formats = []
|
||||
unsniffable_binary_formats = []
|
||||
|
||||
@staticmethod
|
||||
def register_sniffable_binary_format(data_type, ext, type_class):
|
||||
Binary.sniffable_binary_formats.append({"type": data_type, "ext": ext.lower(), "class": type_class})
|
||||
"""Deprecated method."""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def register_unsniffable_binary_ext(ext):
|
||||
Binary.unsniffable_binary_formats.append(ext.lower())
|
||||
|
||||
@staticmethod
|
||||
def is_sniffable_binary(filename):
|
||||
format_information = None
|
||||
for format in Binary.sniffable_binary_formats:
|
||||
format_instance = format["class"]()
|
||||
try:
|
||||
if format_instance.sniff(filename):
|
||||
format_information = (format["type"], format["ext"])
|
||||
break
|
||||
except Exception:
|
||||
# Sniffer raised exception, could be any number of
|
||||
# reasons for this so there is not much to do besides
|
||||
# trying next sniffer.
|
||||
pass
|
||||
return format_information
|
||||
|
||||
@staticmethod
|
||||
def is_ext_unsniffable(ext):
|
||||
return ext in Binary.unsniffable_binary_formats
|
||||
@@ -128,9 +112,6 @@ class Idat(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("idat", "idat", Idat)
|
||||
|
||||
|
||||
class Cel(Binary):
|
||||
|
||||
"""Binary data in CEL format."""
|
||||
@@ -160,9 +141,6 @@ class Cel(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("cel", "cel", Cel)
|
||||
|
||||
|
||||
class CompressedArchive(Binary):
|
||||
"""
|
||||
Class describing an compressed binary file
|
||||
@@ -595,9 +573,6 @@ class Bam(Binary):
|
||||
return dataproviders.dataset.SamtoolsDataProvider(dataset_source, **settings)
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("bam", "bam", Bam)
|
||||
|
||||
|
||||
class CRAM(Binary):
|
||||
file_ext = "cram"
|
||||
edam_format = "format_3462"
|
||||
@@ -667,13 +642,9 @@ class CRAM(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format('cram', 'cram', CRAM)
|
||||
|
||||
|
||||
class BaseBcf(Binary):
|
||||
edam_format = "format_3020"
|
||||
edam_data = "data_3498"
|
||||
file_ext = "bcf"
|
||||
|
||||
|
||||
class Bcf(BaseBcf):
|
||||
@@ -688,6 +659,7 @@ class Bcf(BaseBcf):
|
||||
>>> Bcf().sniff(fname)
|
||||
False
|
||||
"""
|
||||
file_ext = "bcf"
|
||||
|
||||
MetadataElement(name="bcf_index", desc="BCF Index File", param=metadata.FileParameter, file_ext="csi", readonly=True, no_value=None, visible=False, optional=True)
|
||||
|
||||
@@ -731,9 +703,6 @@ class Bcf(BaseBcf):
|
||||
dataset.metadata.bcf_index = index_file
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("bcf", "bcf", Bcf)
|
||||
|
||||
|
||||
class BcfUncompressed(Bcf):
|
||||
"""
|
||||
Class describing an uncompressed BCF file
|
||||
@@ -759,9 +728,6 @@ class BcfUncompressed(Bcf):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("bcf_uncompressed", "bcf_uncompressed", BcfUncompressed)
|
||||
|
||||
|
||||
class H5(Binary):
|
||||
"""
|
||||
Class describing an HDF5 file
|
||||
@@ -942,11 +908,6 @@ class Cool(H5):
|
||||
return "Cool (HDF5) file (%s)." % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("cool", "cool", Cool)
|
||||
Binary.register_sniffable_binary_format("biom2", "biom2", Biom2)
|
||||
Binary.register_sniffable_binary_format("h5", "h5", H5)
|
||||
|
||||
|
||||
class Scf(Binary):
|
||||
"""Class describing an scf binary sequence file"""
|
||||
edam_format = "format_1632"
|
||||
@@ -1003,9 +964,6 @@ class Sff(Binary):
|
||||
return "Binary sff file (%s)" % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("sff", "sff", Sff)
|
||||
|
||||
|
||||
class BigWig(Binary):
|
||||
"""
|
||||
Accessing binary BigWig files from UCSC.
|
||||
@@ -1014,6 +972,7 @@ class BigWig(Binary):
|
||||
"""
|
||||
edam_format = "format_3006"
|
||||
edam_data = "data_3002"
|
||||
file_ext = "bigwig"
|
||||
track_type = "LineTrack"
|
||||
data_sources = {"data_standalone": "bigwig"}
|
||||
|
||||
@@ -1047,13 +1006,11 @@ class BigWig(Binary):
|
||||
return "Binary UCSC %s file (%s)" % (self._name, nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("bigwig", "bigwig", BigWig)
|
||||
|
||||
|
||||
class BigBed(BigWig):
|
||||
"""BigBed support from UCSC."""
|
||||
edam_format = "format_3004"
|
||||
edam_data = "data_3002"
|
||||
file_ext = "bigbed"
|
||||
data_sources = {"data_standalone": "bigbed"}
|
||||
|
||||
def __init__(self, **kwd):
|
||||
@@ -1062,9 +1019,6 @@ class BigBed(BigWig):
|
||||
self._name = "BigBed"
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("bigbed", "bigbed", BigBed)
|
||||
|
||||
|
||||
class TwoBit(Binary):
|
||||
"""Class describing a TwoBit format nucleotide file"""
|
||||
edam_format = "format_3009"
|
||||
@@ -1097,9 +1051,6 @@ class TwoBit(Binary):
|
||||
return "Binary TwoBit format nucleotide file (%s)" % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("twobit", "twobit", TwoBit)
|
||||
|
||||
|
||||
@dataproviders.decorators.has_dataproviders
|
||||
class SQlite(Binary):
|
||||
"""Class describing a Sqlite database """
|
||||
@@ -1191,9 +1142,6 @@ class SQlite(Binary):
|
||||
return dataproviders.dataset.SQliteDataDictProvider(dataset_source, **settings)
|
||||
|
||||
|
||||
# Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
|
||||
|
||||
|
||||
class GeminiSQLite(SQlite):
|
||||
"""Class describing a Gemini Sqlite database """
|
||||
MetadataElement(name="gemini_version", default='0.10.0', param=MetadataParameter, desc="Gemini Version",
|
||||
@@ -1322,14 +1270,6 @@ class IdpDB(SQlite):
|
||||
return "IDPickerDB SQLite file (%s)" % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("gemini.sqlite", "gemini.sqlite", GeminiSQLite)
|
||||
Binary.register_sniffable_binary_format("idpdb", "idpdb", IdpDB)
|
||||
Binary.register_sniffable_binary_format("mz.sqlite", "mz.sqlite", MzSQlite)
|
||||
# FIXME: We need to register specialized sqlite formats before sqlite, since register_sniffable_binary_format and is_sniffable_binary called in upload.py
|
||||
# ignores sniff order declared in datatypes_conf.xml
|
||||
Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
|
||||
|
||||
|
||||
class Xlsx(Binary):
|
||||
"""Class for Excel 2007 (xlsx) files"""
|
||||
file_ext = "xlsx"
|
||||
@@ -1346,9 +1286,6 @@ class Xlsx(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("xlsx", "xlsx", Xlsx)
|
||||
|
||||
|
||||
class ExcelXls(Binary):
|
||||
"""Class describing an Excel (xls) file"""
|
||||
file_ext = "excel.xls"
|
||||
@@ -1377,9 +1314,6 @@ class ExcelXls(Binary):
|
||||
return "Microsoft Excel XLS file (%s)" % (data.nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("excel.xls", "excel.xls", ExcelXls)
|
||||
|
||||
|
||||
class Sra(Binary):
|
||||
""" Sequence Read Archive (SRA) datatype originally from mdshw5/sra-tools-galaxy"""
|
||||
file_ext = 'sra'
|
||||
@@ -1412,9 +1346,6 @@ class Sra(Binary):
|
||||
return 'Binary sra file (%s)' % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format('sra', 'sra', Sra)
|
||||
|
||||
|
||||
class RData(Binary):
|
||||
"""Generic R Data file datatype implementation"""
|
||||
file_ext = 'rdata'
|
||||
@@ -1433,9 +1364,6 @@ class RData(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format('RData', 'RData', RData)
|
||||
|
||||
|
||||
class OxliBinary(Binary):
|
||||
|
||||
@staticmethod
|
||||
@@ -1472,15 +1400,12 @@ class OxliCountGraph(OxliBinary):
|
||||
>>> OxliCountGraph().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxlicg'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"01")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.countgraph", "oxlicg",
|
||||
OxliCountGraph)
|
||||
|
||||
|
||||
class OxliNodeGraph(OxliBinary):
|
||||
"""
|
||||
OxliNodeGraph starts with "OXLI" + one byte version number +
|
||||
@@ -1500,15 +1425,12 @@ class OxliNodeGraph(OxliBinary):
|
||||
>>> OxliNodeGraph().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxling'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"02")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.nodegraph", "oxling",
|
||||
OxliNodeGraph)
|
||||
|
||||
|
||||
class OxliTagSet(OxliBinary):
|
||||
"""
|
||||
OxliTagSet starts with "OXLI" + one byte version number +
|
||||
@@ -1529,14 +1451,12 @@ class OxliTagSet(OxliBinary):
|
||||
>>> OxliTagSet().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxlits'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"03")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.tagset", "oxlits", OxliTagSet)
|
||||
|
||||
|
||||
class OxliStopTags(OxliBinary):
|
||||
"""
|
||||
OxliStopTags starts with "OXLI" + one byte version number +
|
||||
@@ -1552,15 +1472,12 @@ class OxliStopTags(OxliBinary):
|
||||
>>> OxliStopTags().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxlist'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"04")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.stoptags", "oxlist",
|
||||
OxliStopTags)
|
||||
|
||||
|
||||
class OxliSubset(OxliBinary):
|
||||
"""
|
||||
OxliSubset starts with "OXLI" + one byte version number +
|
||||
@@ -1581,14 +1498,12 @@ class OxliSubset(OxliBinary):
|
||||
>>> OxliSubset().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxliss'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"05")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.subset", "oxliss", OxliSubset)
|
||||
|
||||
|
||||
class OxliGraphLabels(OxliBinary):
|
||||
"""
|
||||
OxliGraphLabels starts with "OXLI" + one byte version number +
|
||||
@@ -1610,15 +1525,12 @@ class OxliGraphLabels(OxliBinary):
|
||||
>>> OxliGraphLabels().sniff( fname )
|
||||
True
|
||||
"""
|
||||
file_ext = 'oxligl'
|
||||
|
||||
def sniff(self, filename):
|
||||
return OxliBinary._sniff(filename, b"06")
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("oxli.graphlabels", "oxligl",
|
||||
OxliGraphLabels)
|
||||
|
||||
|
||||
class PostgresqlArchive(CompressedArchive):
|
||||
"""
|
||||
Class describing a Postgresql database packed into a tar archive
|
||||
@@ -1669,9 +1581,6 @@ class PostgresqlArchive(CompressedArchive):
|
||||
return "PostgreSQL Archive (%s)" % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("postgresql_archiv", "postgresql", PostgresqlArchive)
|
||||
|
||||
|
||||
class Fast5Archive(CompressedArchive):
|
||||
"""
|
||||
Class describing a FAST5 archive
|
||||
@@ -1772,11 +1681,6 @@ class Fast5ArchiveBz2(Fast5Archive):
|
||||
return Fast5Archive.sniff(self, filename)
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("fast5_archive_bz2", "fast5.tar.bz2", Fast5ArchiveBz2)
|
||||
Binary.register_sniffable_binary_format("fast5_archive_gz", "fast5.tar.gz", Fast5ArchiveGz)
|
||||
Binary.register_sniffable_binary_format("fast5_archive", "fast5.tar", Fast5Archive)
|
||||
|
||||
|
||||
class SearchGuiArchive(CompressedArchive):
|
||||
"""Class describing a SearchGUI archive """
|
||||
MetadataElement(name="searchgui_version", default='1.28.0', param=MetadataParameter, desc="SearchGui Version",
|
||||
@@ -1828,9 +1732,6 @@ class SearchGuiArchive(CompressedArchive):
|
||||
return "SearchGUI Archive, version %s" % (dataset.metadata.searchgui_version or 'unknown')
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("searchgui_archive", "searchgui_archive", SearchGuiArchive)
|
||||
|
||||
|
||||
class NetCDF(Binary):
|
||||
"""Binary data in netCDF format"""
|
||||
file_ext = "netcdf"
|
||||
@@ -1862,9 +1763,6 @@ class NetCDF(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("netcdf", "netcdf", NetCDF)
|
||||
|
||||
|
||||
class DMND(Binary):
|
||||
"""
|
||||
Class describing an DMND file
|
||||
@@ -1895,9 +1793,6 @@ class DMND(Binary):
|
||||
return False
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("dmnd", "dmnd", DMND)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
doctest.testmod(sys.modules[__name__])
|
||||
|
||||
@@ -131,9 +131,6 @@ class PlyBinary(Ply, Binary):
|
||||
Binary.__init__(self, **kwd)
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("plybinary", "plybinary", PlyBinary)
|
||||
|
||||
|
||||
class Vtk(object):
|
||||
r"""
|
||||
The Visualization Toolkit provides a number of source and writer objects to
|
||||
@@ -458,9 +455,6 @@ class VtkBinary(Vtk, Binary):
|
||||
Binary.__init__(self, **kwd)
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("vtkbinary", "vtkbinary", VtkBinary)
|
||||
|
||||
|
||||
class STL(data.Data):
|
||||
file_ext = "stl"
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import zipfile
|
||||
|
||||
from six.moves.urllib.parse import quote_plus
|
||||
|
||||
from galaxy.datatypes.binary import Binary
|
||||
from galaxy.datatypes.text import Html as HtmlFromText
|
||||
from galaxy.util import nice_size
|
||||
from galaxy.util.image_util import check_image_type
|
||||
@@ -163,9 +162,6 @@ class Pdf(Image):
|
||||
return fh.read(4) == b"%PDF"
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("pdf", "pdf", Pdf)
|
||||
|
||||
|
||||
def create_applet_tag_peek(class_name, archive, params):
|
||||
text = """
|
||||
<object classid="java:%s"
|
||||
|
||||
@@ -52,9 +52,6 @@ class Wiff(Binary):
|
||||
return "\n".join(rval)
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("wiff", "wiff", Wiff)
|
||||
|
||||
|
||||
class PepXmlReport(Tabular):
|
||||
"""pepxml converted to tabular report"""
|
||||
edam_data = "data_2536"
|
||||
@@ -303,9 +300,6 @@ class ThermoRAW(Binary):
|
||||
return "Thermo Finnigan RAW file (%s)" % (nice_size(dataset.get_size()))
|
||||
|
||||
|
||||
Binary.register_sniffable_binary_format("thermo.raw", "raw", ThermoRAW)
|
||||
|
||||
|
||||
class Msp(Text):
|
||||
""" Output of NIST MS Search Program chemdata.nist.gov/mass-spc/ftp/mass-spc/PepLib.pdf """
|
||||
file_ext = "msp"
|
||||
|
||||
@@ -249,6 +249,7 @@ class Registry(object):
|
||||
datatype_class.edam_format = edam_format
|
||||
if edam_data:
|
||||
datatype_class.edam_data = edam_data
|
||||
datatype_class.is_subclass = make_subclass
|
||||
self.datatypes_by_extension[extension] = datatype_class()
|
||||
if mimetype is None:
|
||||
# Use default mimetype per datatype specification.
|
||||
@@ -312,16 +313,14 @@ class Registry(object):
|
||||
self.set_default_values()
|
||||
|
||||
def append_to_sniff_order():
|
||||
# Just in case any supported data types are not included in the config's sniff_order section.
|
||||
for ext, datatype in self.datatypes_by_extension.items():
|
||||
if hasattr(datatype, 'sniff'):
|
||||
included = False
|
||||
for atype in self.sniff_order:
|
||||
if isinstance(atype, datatype.__class__):
|
||||
included = True
|
||||
break
|
||||
if not included:
|
||||
self.sniff_order.append(datatype)
|
||||
sniff_order_classes = set(type(_) for _ in self.sniff_order)
|
||||
for datatype in self.datatypes_by_extension.values():
|
||||
# Add a datatype only if it is not already in sniff_order, it
|
||||
# has a sniff() method and was not defined with subclass="true"
|
||||
if type(datatype) not in sniff_order_classes and \
|
||||
hasattr(datatype, 'sniff') and not datatype.is_subclass:
|
||||
self.sniff_order.append(datatype)
|
||||
|
||||
append_to_sniff_order()
|
||||
|
||||
def _load_build_sites(self, root):
|
||||
|
||||
@@ -734,7 +734,7 @@ class FastqCSSanger(Fastq):
|
||||
file_ext = "fastqcssanger"
|
||||
|
||||
|
||||
class FastqGz (BaseFastq, Binary):
|
||||
class FastqGz(BaseFastq, Binary):
|
||||
"""Class representing a generic compressed FASTQ sequence"""
|
||||
edam_format = "format_1930"
|
||||
file_ext = "fastq.gz"
|
||||
@@ -742,6 +742,8 @@ class FastqGz (BaseFastq, Binary):
|
||||
|
||||
def sniff(self, filename):
|
||||
"""Determines whether the file is in gzip-compressed FASTQ format"""
|
||||
if not SNIFF_COMPRESSED_FASTQS:
|
||||
return False
|
||||
if not is_gzip(filename):
|
||||
return False
|
||||
return BaseFastq.sniff(self, filename)
|
||||
@@ -759,11 +761,6 @@ class FastqSolexaGz(FastqGz):
|
||||
file_ext = "fastqsolexa.gz"
|
||||
|
||||
|
||||
if SNIFF_COMPRESSED_FASTQS:
|
||||
Binary.register_sniffable_binary_format("fastqsanger.gz", "fastqsanger.gz", FastqSangerGz)
|
||||
Binary.register_sniffable_binary_format("fastq.gz", "fastq.gz", FastqGz)
|
||||
|
||||
|
||||
class FastqIlluminaGz(FastqGz):
|
||||
"""Class representing a compressed FASTQ sequence ( the Illumina 1.3+ variant )"""
|
||||
edam_format = "format_1931"
|
||||
@@ -783,6 +780,8 @@ class FastqBz2 (BaseFastq, Binary):
|
||||
|
||||
def sniff(self, filename):
|
||||
"""Determine whether the file is in bzip2-compressed FASTQ format"""
|
||||
if not SNIFF_COMPRESSED_FASTQS:
|
||||
return False
|
||||
if not is_bz2(filename):
|
||||
return False
|
||||
return BaseFastq.sniff(self, filename)
|
||||
@@ -794,11 +793,6 @@ class FastqSangerBz2(FastqBz2):
|
||||
file_ext = "fastqsanger.bz2"
|
||||
|
||||
|
||||
if SNIFF_COMPRESSED_FASTQS:
|
||||
Binary.register_sniffable_binary_format("fastqsanger.bz2", "fastqsanger.bz2", FastqSangerBz2)
|
||||
Binary.register_sniffable_binary_format("fastq.bz2", "fastq.bz2", FastqBz2)
|
||||
|
||||
|
||||
class FastqSolexaBz2(FastqBz2):
|
||||
"""Class representing a compressed FASTQ sequence ( the Solexa variant )"""
|
||||
edam_format = "format_1933"
|
||||
|
||||
@@ -17,10 +17,7 @@ from six import text_type
|
||||
|
||||
from galaxy import util
|
||||
from galaxy.datatypes.binary import Binary
|
||||
from galaxy.util import (
|
||||
compression_utils,
|
||||
multi_byte,
|
||||
)
|
||||
from galaxy.util import compression_utils
|
||||
from galaxy.util.checkers import (
|
||||
check_binary,
|
||||
check_html,
|
||||
@@ -44,13 +41,12 @@ def get_test_fname(fname):
|
||||
|
||||
|
||||
def stream_to_open_named_file(stream, fd, filename, source_encoding=None, source_error='strict', target_encoding=None, target_error='strict'):
|
||||
"""Writes a stream to the provided file descriptor, returns the file's name and bool( is_multi_byte ). Closes file descriptor"""
|
||||
"""Writes a stream to the provided file descriptor, returns the file name. Closes file descriptor"""
|
||||
# signature and behavor is somewhat odd, due to backwards compatibility, but this can/should be done better
|
||||
CHUNK_SIZE = 1048576
|
||||
data_checked = False
|
||||
is_compressed = False
|
||||
is_binary = False
|
||||
is_multi_byte = False
|
||||
try:
|
||||
codecs.lookup(target_encoding)
|
||||
except Exception:
|
||||
@@ -72,11 +68,7 @@ def stream_to_open_named_file(stream, fd, filename, source_encoding=None, source
|
||||
except Exception:
|
||||
pass
|
||||
if not is_compressed:
|
||||
# See if we have a multi-byte character file
|
||||
chars = chunk[:100]
|
||||
is_multi_byte = multi_byte.is_multi_byte(chars)
|
||||
if not is_multi_byte:
|
||||
is_binary = util.is_binary(chunk)
|
||||
is_binary = util.is_binary(chunk)
|
||||
data_checked = True
|
||||
if not is_compressed and not is_binary:
|
||||
if not isinstance(chunk, text_type):
|
||||
@@ -87,7 +79,7 @@ def stream_to_open_named_file(stream, fd, filename, source_encoding=None, source
|
||||
# while binary files should not be encoded at all.
|
||||
os.write(fd, chunk)
|
||||
os.close(fd)
|
||||
return filename, is_multi_byte
|
||||
return filename
|
||||
|
||||
|
||||
def stream_to_file(stream, suffix='', prefix='', dir=None, text=False, **kwd):
|
||||
@@ -396,9 +388,9 @@ def guess_ext(fname, sniff_order):
|
||||
file_ext = None
|
||||
for datatype in sniff_order:
|
||||
"""
|
||||
Some classes may not have a sniff function, which is ok. In fact, the
|
||||
Tabular and Text classes are 2 examples of classes that should never have
|
||||
a sniff function. Since these classes are default classes, they contain
|
||||
Some classes may not have a sniff function, which is ok. In fact,
|
||||
Binary, Data, Tabular and Text are examples of classes that should never
|
||||
have a sniff function. Since these classes are default classes, they contain
|
||||
few rules to filter out data of other formats, so they should be called
|
||||
from this function after all other datatypes in sniff_order have not been
|
||||
successfully discovered.
|
||||
|
||||
@@ -186,6 +186,3 @@ class HDT(binary.Binary, Triples):
|
||||
else:
|
||||
dataset.peek = 'file does not exist'
|
||||
dataset.blurb = 'file purged from disk'
|
||||
|
||||
|
||||
binary.Binary.register_sniffable_binary_format('HDT', 'HDT', HDT)
|
||||
|
||||
@@ -34,7 +34,6 @@ pycrypto
|
||||
Paste
|
||||
PasteDeploy
|
||||
docutils
|
||||
#wchartype
|
||||
repoze.lru
|
||||
Routes
|
||||
WebOb
|
||||
|
||||
@@ -84,7 +84,6 @@ pycrypto==2.6.1 --hash:sha256=44c20785a1057b8d5e190d6a1847f7c62a8180b9b27a3aca7c
|
||||
Paste==2.0.2 --hash:sha256=4e7bb2d8cbdc47ab46e4af0cb61d2af91fccc620a08415d9c4da9e499f0455fd
|
||||
PasteDeploy==1.5.2 --hash:sha256=3922127d3acc6e274a800978b9293c874b3ef4ac2eb8bf485e2c85b22d1f260a
|
||||
docutils==0.12 --hash:sha256=732dfc2d706ea390c264bc69110d3d6c7c3a520628a052ccc5998466f95d5c29
|
||||
wchartype==0.1 --hash:sha256=2932471fe3a4e5cac4539c1b49bbc2bbd24ec3f00532c7f27c80b7756fdc177d
|
||||
repoze.lru==0.6 --hash:sha256=731cc0b0a184c9fd270a4f29d9635099bb0398823e9a1a39bf4f488d1ead57b3
|
||||
Routes==2.4.1 --hash:sha256=cc3a4b9a34bfe6b64b1d10d18b845bb2b6cb3ee1fbf6107295dfafe629bb84aa
|
||||
WebOb==1.4.1 --hash:sha256=dc3c45ac0b56a3c65f47f1da6c23760dfe0139e8a84caaf1cfc9276b3e10875a
|
||||
|
||||
@@ -24,7 +24,6 @@ boltons==17.1.0
|
||||
Paste==2.0.2
|
||||
PasteDeploy==1.5.2
|
||||
docutils==0.14
|
||||
wchartype==0.1
|
||||
repoze.lru==0.6
|
||||
Routes==2.4.1
|
||||
WebOb==1.4.1
|
||||
|
||||
@@ -21,7 +21,6 @@ boltons
|
||||
Paste
|
||||
PasteDeploy
|
||||
docutils
|
||||
wchartype
|
||||
repoze.lru
|
||||
Routes
|
||||
WebOb
|
||||
|
||||
@@ -1040,7 +1040,7 @@ class JobWrapper(object, HasResourceParameters):
|
||||
destination_params = job.destination_params
|
||||
if "__resubmit_delay_seconds" in destination_params:
|
||||
delay = float(destination_params["__resubmit_delay_seconds"])
|
||||
if job.seconds_since_update < delay:
|
||||
if job.seconds_since_updated < delay:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -46,6 +46,40 @@ class DatasetCollectionManager(object):
|
||||
self.tag_manager = tags.GalaxyTagManager(app.model.context)
|
||||
self.ldda_manager = lddas.LDDAManager(app)
|
||||
|
||||
def precreate_dataset_collection_instance(self, trans, parent, name, implicit_inputs, implicit_output_name, structure):
|
||||
# TODO: prebuild all required HIDs and send them in so no need to flush in between.
|
||||
dataset_collection = self.precreate_dataset_collection(structure)
|
||||
instance = self._create_instance_for_collection(
|
||||
trans, parent, name, dataset_collection, implicit_inputs=implicit_inputs, implicit_output_name=implicit_output_name, flush=False
|
||||
)
|
||||
return instance
|
||||
|
||||
def precreate_dataset_collection(self, structure):
|
||||
if structure.is_leaf or not structure.children_known:
|
||||
return model.DatasetCollectionElement.UNINITIALIZED_ELEMENT
|
||||
else:
|
||||
collection_type_description = structure.collection_type_description
|
||||
dataset_collection = model.DatasetCollection(populated=False)
|
||||
dataset_collection.collection_type = collection_type_description.collection_type
|
||||
elements = []
|
||||
for index, (identifier, substructure) in enumerate(structure.children):
|
||||
# TODO: Open question - populate these now or later?
|
||||
if substructure.is_leaf:
|
||||
element = model.DatasetCollectionElement.UNINITIALIZED_ELEMENT
|
||||
else:
|
||||
element = self.precreate_dataset_collection(substructure)
|
||||
|
||||
element = model.DatasetCollectionElement(
|
||||
element=element,
|
||||
element_identifier=identifier,
|
||||
element_index=index,
|
||||
)
|
||||
elements.append(element)
|
||||
dataset_collection.elements = elements
|
||||
dataset_collection.element_count = len(elements)
|
||||
|
||||
return dataset_collection
|
||||
|
||||
def create(self, trans, parent, name, collection_type, element_identifiers=None,
|
||||
elements=None, implicit_collection_info=None, trusted_identifiers=None,
|
||||
hide_source_items=False, tags=None):
|
||||
@@ -68,27 +102,30 @@ class DatasetCollectionManager(object):
|
||||
hide_source_items=hide_source_items,
|
||||
)
|
||||
|
||||
implicit_inputs = []
|
||||
if implicit_collection_info:
|
||||
implicit_inputs = implicit_collection_info.get('implicit_inputs', [])
|
||||
|
||||
implicit_output_name = None
|
||||
if implicit_collection_info:
|
||||
implicit_output_name = implicit_collection_info["implicit_output_name"]
|
||||
|
||||
return self._create_instance_for_collection(
|
||||
trans, parent, name, dataset_collection, implicit_inputs=implicit_inputs, implicit_output_name=implicit_output_name, tags=tags
|
||||
)
|
||||
|
||||
def _create_instance_for_collection(self, trans, parent, name, dataset_collection, implicit_output_name=None, implicit_inputs=None, tags=None, flush=True):
|
||||
if isinstance(parent, model.History):
|
||||
dataset_collection_instance = self.model.HistoryDatasetCollectionAssociation(
|
||||
collection=dataset_collection,
|
||||
name=name,
|
||||
)
|
||||
if implicit_collection_info:
|
||||
for input_name, input_collection in implicit_collection_info["implicit_inputs"]:
|
||||
if implicit_inputs:
|
||||
for input_name, input_collection in implicit_inputs:
|
||||
dataset_collection_instance.add_implicit_input_collection(input_name, input_collection)
|
||||
for output_dataset in implicit_collection_info.get("outputs"):
|
||||
if output_dataset not in trans.sa_session:
|
||||
output_dataset = trans.sa_session.query(type(output_dataset)).get(output_dataset.id)
|
||||
if isinstance(output_dataset, model.HistoryDatasetAssociation):
|
||||
output_dataset.hidden_beneath_collection_instance = dataset_collection_instance
|
||||
elif isinstance(output_dataset, model.HistoryDatasetCollectionAssociation):
|
||||
dataset_collection_instance.add_implicit_input_collection(input_name, input_collection)
|
||||
else:
|
||||
# dataset collection, don't need to do anything...
|
||||
pass
|
||||
trans.sa_session.add(output_dataset)
|
||||
|
||||
dataset_collection_instance.implicit_output_name = implicit_collection_info["implicit_output_name"]
|
||||
if implicit_output_name:
|
||||
dataset_collection_instance.implicit_output_name = implicit_output_name
|
||||
|
||||
log.debug("Created collection with %d elements" % (len(dataset_collection_instance.collection.elements)))
|
||||
# Handle setting hid
|
||||
@@ -105,37 +142,26 @@ class DatasetCollectionManager(object):
|
||||
message = "Internal logic error - create called with unknown parent type %s" % type(parent)
|
||||
log.exception(message)
|
||||
raise MessageException(message)
|
||||
tags = tags or {}
|
||||
if implicit_collection_info:
|
||||
for _, v in implicit_collection_info.get('implicit_inputs', []):
|
||||
for tag in [t for t in v.tags if t.user_tname == 'name']:
|
||||
tags[tag.value] = tag
|
||||
for _, tag in tags.items():
|
||||
dataset_collection_instance.tags.append(tag.copy(cls=model.HistoryDatasetCollectionTagAssociation))
|
||||
|
||||
return self.__persist(dataset_collection_instance)
|
||||
tags = self._append_tags(dataset_collection_instance, implicit_inputs, tags)
|
||||
return self.__persist(dataset_collection_instance, flush=flush)
|
||||
|
||||
def create_dataset_collection(self, trans, collection_type, element_identifiers=None, elements=None,
|
||||
hide_source_items=None):
|
||||
# Make sure at least one of these is None.
|
||||
assert element_identifiers is None or elements is None
|
||||
|
||||
if element_identifiers is None and elements is None:
|
||||
raise RequestParameterInvalidException(ERROR_INVALID_ELEMENTS_SPECIFICATION)
|
||||
if not collection_type:
|
||||
raise RequestParameterInvalidException(ERROR_NO_COLLECTION_TYPE)
|
||||
|
||||
collection_type_description = self.collection_type_descriptions.for_collection_type(collection_type)
|
||||
|
||||
# If we have elements, this is an internal request, don't need to load
|
||||
# objects from identifiers.
|
||||
if elements is None:
|
||||
if collection_type_description.has_subcollections():
|
||||
# Nested collection - recursively create collections and update identifiers.
|
||||
self.__recursively_create_collections(trans, element_identifiers)
|
||||
new_collection = False
|
||||
for element_identifier in element_identifiers:
|
||||
if element_identifier.get("src") == "new_collection" and element_identifier.get('collection_type') == '':
|
||||
new_collection = True
|
||||
elements = self.__load_elements(trans, element_identifier['element_identifiers'])
|
||||
if not new_collection:
|
||||
elements = self.__load_elements(trans, element_identifiers)
|
||||
|
||||
elements = self._element_identifiers_to_elements(trans, collection_type_description, element_identifiers)
|
||||
# else if elements is set, it better be an ordered dict!
|
||||
|
||||
if elements is not self.ELEMENTS_UNINITIALIZED:
|
||||
@@ -150,6 +176,28 @@ class DatasetCollectionManager(object):
|
||||
dataset_collection.collection_type = collection_type
|
||||
return dataset_collection
|
||||
|
||||
def _element_identifiers_to_elements(self, trans, collection_type_description, element_identifiers):
|
||||
if collection_type_description.has_subcollections():
|
||||
# Nested collection - recursively create collections and update identifiers.
|
||||
self.__recursively_create_collections(trans, element_identifiers)
|
||||
new_collection = False
|
||||
for element_identifier in element_identifiers:
|
||||
if element_identifier.get("src") == "new_collection" and element_identifier.get('collection_type') == '':
|
||||
new_collection = True
|
||||
elements = self.__load_elements(trans, element_identifier['element_identifiers'])
|
||||
if not new_collection:
|
||||
elements = self.__load_elements(trans, element_identifiers)
|
||||
return elements
|
||||
|
||||
def _append_tags(self, dataset_collection_instance, implicit_inputs=None, tags=None):
|
||||
tags = tags or {}
|
||||
implicit_inputs = implicit_inputs or []
|
||||
for _, v in implicit_inputs:
|
||||
for tag in [t for t in v.tags if t.user_tname == 'name']:
|
||||
tags[tag.value] = tag
|
||||
for _, tag in tags.items():
|
||||
dataset_collection_instance.tags.append(tag.copy(cls=model.HistoryDatasetCollectionTagAssociation))
|
||||
|
||||
def set_collection_elements(self, dataset_collection, dataset_instances):
|
||||
if dataset_collection.populated:
|
||||
raise Exception("Cannot reset elements of an already populated dataset collection.")
|
||||
@@ -240,10 +288,11 @@ class DatasetCollectionManager(object):
|
||||
collections = list(filter(query.direct_match, collections))
|
||||
return collections
|
||||
|
||||
def __persist(self, dataset_collection_instance):
|
||||
def __persist(self, dataset_collection_instance, flush=True):
|
||||
context = self.model.context
|
||||
context.add(dataset_collection_instance)
|
||||
context.flush()
|
||||
if flush:
|
||||
context.flush()
|
||||
return dataset_collection_instance
|
||||
|
||||
def __recursively_create_collections(self, trans, element_identifiers):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from galaxy import exceptions, model, web
|
||||
from galaxy.util import string_as_bool
|
||||
@@ -101,8 +102,9 @@ def get_collection_elements(collection, name=""):
|
||||
return names, hdas
|
||||
|
||||
|
||||
def dictify_dataset_collection_instance(dataset_collection_instance, parent, security, view="element"):
|
||||
dict_value = dataset_collection_instance.to_dict(view=view)
|
||||
def dictify_dataset_collection_instance(dataset_collection_instance, parent, security, view="element", fuzzy_count=None):
|
||||
hdca_view = "element" if view in ["element", "element-reference"] else "collection"
|
||||
dict_value = dataset_collection_instance.to_dict(view=hdca_view)
|
||||
encoded_id = security.encode_id(dataset_collection_instance.id)
|
||||
if isinstance(parent, model.History):
|
||||
encoded_history_id = security.encode_id(parent.id)
|
||||
@@ -112,25 +114,160 @@ def dictify_dataset_collection_instance(dataset_collection_instance, parent, sec
|
||||
encoded_folder_id = security.encode_id(parent.id)
|
||||
# TODO: Work in progress - this end-point is not right yet...
|
||||
dict_value['url'] = web.url_for('library_content', library_id=encoded_library_id, id=encoded_id, folder_id=encoded_folder_id)
|
||||
if view == "element":
|
||||
|
||||
if view in ["element", "element-reference"]:
|
||||
collection = dataset_collection_instance.collection
|
||||
dict_value['elements'] = [dictify_element(_) for _ in collection.elements]
|
||||
dict_value['populated'] = collection.populated
|
||||
rank_fuzzy_counts = gen_rank_fuzzy_counts(collection.collection_type, fuzzy_count)
|
||||
elements, rest_fuzzy_counts = get_fuzzy_count_elements(collection, rank_fuzzy_counts)
|
||||
if view == "element":
|
||||
dict_value['populated'] = collection.populated
|
||||
element_func = dictify_element
|
||||
else:
|
||||
element_func = dictify_element_reference
|
||||
dict_value['elements'] = [element_func(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
|
||||
|
||||
security.encode_all_ids(dict_value, recursive=True) # TODO: Use Kyle's recursive formulation of this.
|
||||
return dict_value
|
||||
|
||||
|
||||
def dictify_element(element):
|
||||
def dictify_element_reference(element, rank_fuzzy_counts=None):
|
||||
"""Load minimal details of elements required to show outline of contents in history panel.
|
||||
|
||||
History panel can use this reference to expand to full details if individual dataset elements
|
||||
are clicked.
|
||||
"""
|
||||
dictified = element.to_dict(view="element")
|
||||
object_detials = element.element_object.to_dict()
|
||||
if element.child_collection:
|
||||
# Recursively yield elements for each nested collection...
|
||||
child_collection = element.child_collection
|
||||
object_detials["elements"] = [dictify_element(_) for _ in child_collection.elements]
|
||||
object_detials["populated"] = child_collection.populated
|
||||
element_object = element.element_object
|
||||
if element_object is not None:
|
||||
object_detials = dict(
|
||||
id=element_object.id,
|
||||
model_class=element_object.__class__.__name__,
|
||||
)
|
||||
if element.child_collection:
|
||||
object_detials["collection_type"] = element_object.collection_type
|
||||
child_collection = element.child_collection
|
||||
elements, rest_fuzzy_counts = get_fuzzy_count_elements(child_collection, rank_fuzzy_counts)
|
||||
# Recursively yield elements for each nested collection...
|
||||
object_detials["elements"] = [dictify_element_reference(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
|
||||
object_detials["element_count"] = child_collection.element_count
|
||||
else:
|
||||
object_detials["state"] = element_object.state
|
||||
object_detials["hda_ldda"] = 'hda'
|
||||
object_detials["history_id"] = element_object.history_id
|
||||
|
||||
else:
|
||||
object_detials = None
|
||||
|
||||
dictified["object"] = object_detials
|
||||
return dictified
|
||||
|
||||
|
||||
def dictify_element(element, rank_fuzzy_counts=None):
|
||||
dictified = element.to_dict(view="element")
|
||||
element_object = element.element_object
|
||||
if element_object is not None:
|
||||
object_detials = element.element_object.to_dict()
|
||||
if element.child_collection:
|
||||
child_collection = element.child_collection
|
||||
elements, rest_fuzzy_counts = get_fuzzy_count_elements(child_collection, rank_fuzzy_counts)
|
||||
|
||||
# Recursively yield elements for each nested collection...
|
||||
child_collection = element.child_collection
|
||||
object_detials["elements"] = [dictify_element(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
|
||||
object_detials["populated"] = child_collection.populated
|
||||
object_detials["element_count"] = child_collection.element_count
|
||||
else:
|
||||
object_detials = None
|
||||
|
||||
dictified["object"] = object_detials
|
||||
return dictified
|
||||
|
||||
|
||||
def get_fuzzy_count_elements(collection, rank_fuzzy_counts):
|
||||
if rank_fuzzy_counts and rank_fuzzy_counts[0]:
|
||||
rank_fuzzy_count = rank_fuzzy_counts[0]
|
||||
elements = collection.elements[0:rank_fuzzy_count]
|
||||
else:
|
||||
elements = collection.elements
|
||||
|
||||
if rank_fuzzy_counts is not None:
|
||||
rest_fuzzy_counts = rank_fuzzy_counts[1:]
|
||||
else:
|
||||
rest_fuzzy_counts = None
|
||||
|
||||
return elements, rest_fuzzy_counts
|
||||
|
||||
|
||||
def gen_rank_fuzzy_counts(collection_type, fuzzy_count=None):
|
||||
"""Turn a global estimate on elements to return to per nested level based on collection type.
|
||||
|
||||
This takes an arbitrary constant and generates an arbitrary constant and is quite messy.
|
||||
None of this should be relied on as a stable API - it is more of a general guideline to
|
||||
restrict within broad ranges the amount of objects returned.
|
||||
|
||||
>>> def is_around(x, y):
|
||||
... return y - 1 < x and y + 1 > y
|
||||
...
|
||||
>>> gen_rank_fuzzy_counts("list", None)
|
||||
[None]
|
||||
>>> gen_rank_fuzzy_counts("list", 500)
|
||||
[500]
|
||||
>>> gen_rank_fuzzy_counts("paired", 500)
|
||||
[2]
|
||||
>>> gen_rank_fuzzy_counts("list:paired", None)
|
||||
[None, None]
|
||||
>>> gen_rank_fuzzy_counts("list:list", 101) # 100 would be edge case at 10 so bump to ensure 11
|
||||
[11, 11]
|
||||
>>> ll, pl = gen_rank_fuzzy_counts("list:paired", 100)
|
||||
>>> pl
|
||||
2
|
||||
>>> is_around(ll, 50)
|
||||
True
|
||||
>>> pl, ll = gen_rank_fuzzy_counts("paired:list", 100)
|
||||
>>> pl
|
||||
2
|
||||
>>> is_around(ll, 50)
|
||||
True
|
||||
>>> gen_rank_fuzzy_counts("list:list:list", 1001)
|
||||
[11, 11, 11]
|
||||
>>> l1l, l2l, l3l, pl = gen_rank_fuzzy_counts("list:list:list:paired", 2000)
|
||||
>>> pl
|
||||
2
|
||||
>>> is_around(10, l1l)
|
||||
True
|
||||
>>> gen_rank_fuzzy_counts("list:list:list", 1)
|
||||
[1, 1, 1]
|
||||
>>> gen_rank_fuzzy_counts("list:list:list", 2)
|
||||
[2, 2, 2]
|
||||
>>> gen_rank_fuzzy_counts("paired:paired", 400)
|
||||
[2, 2]
|
||||
>>> gen_rank_fuzzy_counts("paired:paired", 5)
|
||||
[2, 2]
|
||||
>>> gen_rank_fuzzy_counts("paired:paired", 3)
|
||||
[2, 2]
|
||||
>>> gen_rank_fuzzy_counts("paired:paired", 1)
|
||||
[1, 1]
|
||||
>>> gen_rank_fuzzy_counts("paired:paired", 2)
|
||||
[2, 2]
|
||||
"""
|
||||
rank_collection_types = collection_type.split(":")
|
||||
if fuzzy_count is None:
|
||||
return [None for rt in rank_collection_types]
|
||||
else:
|
||||
# This is a list...
|
||||
paired_count = sum([1 if rt == "paired" else 0 for rt in rank_collection_types])
|
||||
list_count = len(rank_collection_types) - paired_count
|
||||
paired_fuzzy_count_mult = 1 if paired_count == 0 else 2 << (paired_count - 1)
|
||||
list_fuzzy_count_mult = math.floor((fuzzy_count * 1.0) / paired_fuzzy_count_mult)
|
||||
list_rank_fuzzy_count = int(math.floor(math.pow(list_fuzzy_count_mult, 1.0 / list_count)) + 1) if list_count > 0 else 1.0
|
||||
pair_rank_fuzzy_count = 2
|
||||
if list_rank_fuzzy_count > fuzzy_count:
|
||||
list_rank_fuzzy_count = fuzzy_count
|
||||
if pair_rank_fuzzy_count > fuzzy_count:
|
||||
pair_rank_fuzzy_count = fuzzy_count
|
||||
rank_fuzzy_counts = [pair_rank_fuzzy_count if rt == "paired" else list_rank_fuzzy_count for rt in rank_collection_types]
|
||||
|
||||
return rank_fuzzy_counts
|
||||
|
||||
|
||||
__all__ = ('api_payload_to_create_params', 'dictify_dataset_collection_instance')
|
||||
|
||||
@@ -117,12 +117,13 @@ class DCSerializer(base.ModelSerializer):
|
||||
'create_time',
|
||||
'update_time',
|
||||
'collection_type',
|
||||
'populated',
|
||||
'populated_state',
|
||||
'populated_state_message',
|
||||
'element_count',
|
||||
])
|
||||
self.add_view('detailed', [
|
||||
'elements'
|
||||
'populated',
|
||||
'elements',
|
||||
], include_keys_from='summary')
|
||||
|
||||
def add_serializers(self):
|
||||
@@ -130,7 +131,6 @@ class DCSerializer(base.ModelSerializer):
|
||||
self.serializers.update({
|
||||
'model_class' : lambda *a, **c: 'DatasetCollection',
|
||||
'elements' : self.serialize_elements,
|
||||
'element_count' : self.serialize_element_count
|
||||
})
|
||||
|
||||
def serialize_elements(self, item, key, **context):
|
||||
@@ -140,14 +140,6 @@ class DCSerializer(base.ModelSerializer):
|
||||
returned.append(serialized)
|
||||
return returned
|
||||
|
||||
def serialize_element_count(self, item, key, **context):
|
||||
"""Return the count of elements for this collection."""
|
||||
# TODO: app.model.context -> session
|
||||
# TODO: to the container interface (dataset_collection_contents)
|
||||
return (self.app.model.context.query(model.DatasetCollectionElement)
|
||||
.filter(model.DatasetCollectionElement.dataset_collection_id == item.id)
|
||||
.count())
|
||||
|
||||
|
||||
class DCASerializer(base.ModelSerializer):
|
||||
"""
|
||||
@@ -163,12 +155,13 @@ class DCASerializer(base.ModelSerializer):
|
||||
'id',
|
||||
'create_time', 'update_time',
|
||||
'collection_type',
|
||||
'populated',
|
||||
'populated_state',
|
||||
'populated_state_message',
|
||||
'element_count',
|
||||
])
|
||||
self.add_view('detailed', [
|
||||
'elements'
|
||||
'populated',
|
||||
'elements',
|
||||
], include_keys_from='summary')
|
||||
|
||||
def add_serializers(self):
|
||||
@@ -184,7 +177,7 @@ class DCASerializer(base.ModelSerializer):
|
||||
'populated_state',
|
||||
'populated_state_message',
|
||||
'elements',
|
||||
'element_count'
|
||||
'element_count',
|
||||
]
|
||||
for key in collection_keys:
|
||||
self.serializers[key] = self._proxy_to_dataset_collection(key=key)
|
||||
@@ -221,15 +214,15 @@ class HDCASerializer(
|
||||
'history_content_type',
|
||||
|
||||
'collection_type',
|
||||
'populated',
|
||||
'populated_state',
|
||||
'populated_state_message',
|
||||
'element_count',
|
||||
|
||||
'job_source_id',
|
||||
'job_source_type',
|
||||
|
||||
'name',
|
||||
'type_id',
|
||||
'history_id',
|
||||
'hid',
|
||||
'history_content_type',
|
||||
'deleted',
|
||||
# 'purged',
|
||||
'visible',
|
||||
@@ -238,6 +231,7 @@ class HDCASerializer(
|
||||
'tags', # TODO: detail view only (maybe)
|
||||
])
|
||||
self.add_view('detailed', [
|
||||
'populated',
|
||||
'elements'
|
||||
], include_keys_from='summary')
|
||||
|
||||
@@ -254,6 +248,7 @@ class HDCASerializer(
|
||||
'history_id' : self.serialize_id,
|
||||
'history_content_type' : lambda *a, **c: self.hdca_manager.model_class.content_type,
|
||||
'type_id' : self.serialize_type_id,
|
||||
'job_source_id' : self.serialize_id,
|
||||
|
||||
'url' : lambda i, k, **c: self.url_for('history_content_typed',
|
||||
history_id=self.app.security.encode_id(i.history_id),
|
||||
|
||||
@@ -3,10 +3,12 @@ import logging
|
||||
|
||||
from boltons.iterutils import remap
|
||||
from six import string_types
|
||||
from sqlalchemy import and_, false, or_
|
||||
from sqlalchemy import and_, false, func, or_
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.sql import select
|
||||
|
||||
from galaxy import model
|
||||
from galaxy.exceptions import RequestParameterInvalidException
|
||||
from galaxy.managers.collections import DatasetCollectionManager
|
||||
from galaxy.managers.hdas import HDAManager
|
||||
from galaxy.managers.lddas import LDDAManager
|
||||
@@ -243,3 +245,75 @@ class JobSearch(object):
|
||||
log.info("Searching jobs finished %s", search_timer)
|
||||
return job
|
||||
return None
|
||||
|
||||
|
||||
def fetch_job_states(app, sa_session, job_source_ids, job_source_types):
|
||||
decode = app.security.decode_id
|
||||
assert len(job_source_ids) == len(job_source_types)
|
||||
job_ids = set()
|
||||
implicit_collection_job_ids = set()
|
||||
|
||||
for job_source_id, job_source_type in zip(job_source_ids, job_source_types):
|
||||
if job_source_type == "Job":
|
||||
job_ids.add(job_source_id)
|
||||
elif job_source_type == "ImplicitCollectionJobs":
|
||||
implicit_collection_job_ids.add(job_source_id)
|
||||
else:
|
||||
raise RequestParameterInvalidException("Invalid job source type %s found." % job_source_type)
|
||||
|
||||
# TODO: use above sets and optimize queries on second pass.
|
||||
rval = []
|
||||
for job_source_id, job_source_type in zip(job_source_ids, job_source_types):
|
||||
if job_source_type == "Job":
|
||||
rval.append(summarize_jobs_to_dict(sa_session, sa_session.query(model.Job).get(decode(job_source_id))))
|
||||
else:
|
||||
rval.append(summarize_jobs_to_dict(sa_session, sa_session.query(model.ImplicitCollectionJobs).get(decode(job_source_id))))
|
||||
|
||||
return rval
|
||||
|
||||
|
||||
def summarize_jobs_to_dict(sa_session, jobs_source):
|
||||
"""Proudce a summary of jobs for job summary endpoints.
|
||||
|
||||
:type jobs_source: a Job or ImplicitCollectionJobs or None
|
||||
:param jobs_source: the object to summarize
|
||||
|
||||
:rtype: dict
|
||||
:returns: dictionary containing job summary information
|
||||
"""
|
||||
rval = None
|
||||
if jobs_source is None:
|
||||
pass
|
||||
elif isinstance(jobs_source, model.Job):
|
||||
rval = {
|
||||
"populated_state": "ok",
|
||||
"states": {jobs_source.state: 1},
|
||||
"model": "Job",
|
||||
"id": jobs_source.id,
|
||||
}
|
||||
else:
|
||||
populated_state = jobs_source.populated_state
|
||||
rval = {
|
||||
"id": jobs_source.id,
|
||||
"populated_state": populated_state,
|
||||
"model": "ImplicitCollectionJobs",
|
||||
}
|
||||
if populated_state == "ok":
|
||||
# produce state summary...
|
||||
states = {}
|
||||
join = model.ImplicitCollectionJobs.table.join(
|
||||
model.ImplicitCollectionJobsJobAssociation.table.join(model.Job)
|
||||
)
|
||||
statement = select(
|
||||
[model.Job.state, func.count("*")]
|
||||
).select_from(
|
||||
join
|
||||
).where(
|
||||
model.ImplicitCollectionJobs.id == jobs_source.id
|
||||
).group_by(
|
||||
model.Job.state
|
||||
)
|
||||
for row in sa_session.execute(statement):
|
||||
states[row[0]] = row[1]
|
||||
rval["states"] = states
|
||||
return rval
|
||||
|
||||
+222
-63
@@ -4,7 +4,6 @@ Galaxy data model classes
|
||||
Naming: try to use class names that have a distinct plural form so that
|
||||
the relationship cardinalities are obvious (e.g. prefer Dataset to Data)
|
||||
"""
|
||||
import codecs
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
@@ -36,7 +35,6 @@ from galaxy.util import (directory_hash_id, Params, ready_name_for_url,
|
||||
from galaxy.util.bunch import Bunch
|
||||
from galaxy.util.dictifiable import Dictifiable
|
||||
from galaxy.util.hash_util import new_secure_hash
|
||||
from galaxy.util.multi_byte import is_multi_byte
|
||||
from galaxy.util.sanitize_html import sanitize_html
|
||||
from galaxy.web.form_builder import (AddressField, CheckboxField, HistoryField,
|
||||
PasswordField, SelectField, TextArea, TextField, WorkflowField,
|
||||
@@ -116,6 +114,19 @@ class HasName:
|
||||
return name
|
||||
|
||||
|
||||
class UsesCreateAndUpdateTime:
|
||||
|
||||
@property
|
||||
def seconds_since_updated(self):
|
||||
update_time = self.update_time or galaxy.model.orm.now.now() # In case not yet flushed
|
||||
return (galaxy.model.orm.now.now() - update_time).total_seconds()
|
||||
|
||||
@property
|
||||
def seconds_since_created(self):
|
||||
create_time = self.create_time or galaxy.model.orm.now.now() # In case not yet flushed
|
||||
return (galaxy.model.orm.now.now() - create_time).total_seconds()
|
||||
|
||||
|
||||
class JobLike:
|
||||
|
||||
def _init_metrics(self):
|
||||
@@ -423,7 +434,7 @@ class TaskMetricNumeric(BaseJobMetric):
|
||||
pass
|
||||
|
||||
|
||||
class Job(object, JobLike, Dictifiable):
|
||||
class Job(object, JobLike, UsesCreateAndUpdateTime, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'state', 'exit_code', 'update_time', 'create_time']
|
||||
dict_element_visible_keys = ['id', 'state', 'exit_code', 'update_time', 'create_time']
|
||||
|
||||
@@ -804,10 +815,6 @@ class Job(object, JobLike, Dictifiable):
|
||||
config_value = default
|
||||
return config_value
|
||||
|
||||
@property
|
||||
def seconds_since_update(self):
|
||||
return (galaxy.model.orm.now.now() - self.update_time).total_seconds()
|
||||
|
||||
|
||||
class Task(object, JobLike):
|
||||
"""
|
||||
@@ -1043,6 +1050,33 @@ class ImplicitlyCreatedDatasetCollectionInput(object):
|
||||
self.input_dataset_collection = input_dataset_collection
|
||||
|
||||
|
||||
class ImplicitCollectionJobs(object):
|
||||
|
||||
populated_states = Bunch(
|
||||
NEW='new', # New implicit jobs object, unpopulated job associations
|
||||
OK='ok', # Job associations are set and fixed.
|
||||
FAILED='failed', # There were issues populating job associations, object is in error.
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id=None,
|
||||
populated_state=None,
|
||||
):
|
||||
self.id = id
|
||||
self.populated_state = populated_state or ImplicitCollectionJobs.populated_states.NEW
|
||||
|
||||
@property
|
||||
def job_list(self):
|
||||
return [icjja.job for icjja in self.jobs]
|
||||
|
||||
|
||||
class ImplicitCollectionJobsJobAssociation(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class PostJobAction(object):
|
||||
def __init__(self, action_type, workflow_step, output_name=None, action_arguments=None):
|
||||
self.action_type = action_type
|
||||
@@ -1857,15 +1891,7 @@ class Dataset(StorableObject):
|
||||
def mark_deleted(self):
|
||||
self.deleted = True
|
||||
|
||||
def is_multi_byte(self):
|
||||
if not self.has_data():
|
||||
return False
|
||||
try:
|
||||
return is_multi_byte(codecs.open(self.file_name, 'r', 'utf-8').read(100))
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
# FIXME: sqlalchemy will replace this
|
||||
|
||||
def _delete(self):
|
||||
"""Remove the file that corresponds to this data"""
|
||||
self.object_store.delete(self)
|
||||
@@ -2066,10 +2092,6 @@ class DatasetInstance(object):
|
||||
# extension is None
|
||||
return 'data'
|
||||
|
||||
def is_multi_byte(self):
|
||||
"""Data consists of multi-byte characters"""
|
||||
return self.dataset.is_multi_byte()
|
||||
|
||||
def set_peek(self):
|
||||
return self.datatype.set_peek(self)
|
||||
|
||||
@@ -3207,6 +3229,15 @@ class DatasetCollection(object, Dictifiable, UsesAnnotations):
|
||||
self.populated_state = DatasetCollection.populated_states.FAILED
|
||||
self.populated_state_message = message
|
||||
|
||||
def finalize(self):
|
||||
# All jobs have written out their elements - everything should be populated
|
||||
# but might not be - check that second case! (TODO)
|
||||
self.mark_as_populated()
|
||||
if self.has_subcollections:
|
||||
# THIS IS WRONG - SHOULD ONLY BE TO THE DEPTH OF THE MAP OVER.
|
||||
for element in self.elements:
|
||||
element.child_collection.finalize()
|
||||
|
||||
@property
|
||||
def dataset_instances(self):
|
||||
instances = []
|
||||
@@ -3307,6 +3338,7 @@ class DatasetCollectionInstance(object, HasName):
|
||||
populated=self.populated,
|
||||
populated_state=self.collection.populated_state,
|
||||
populated_state_message=self.collection.populated_state_message,
|
||||
element_count=self.collection.element_count,
|
||||
type="collection", # contents type (distinguished from file or folder (in case of library))
|
||||
)
|
||||
|
||||
@@ -3382,6 +3414,19 @@ class HistoryDatasetCollectionAssociation(DatasetCollectionInstance,
|
||||
return ((type_coerce(cls.content_type, types.Unicode) + u'-' +
|
||||
type_coerce(cls.id, types.Unicode)).label('type_id'))
|
||||
|
||||
@property
|
||||
def job_source_type(self):
|
||||
if self.implicit_collection_jobs_id:
|
||||
return "ImplicitCollectionJobs"
|
||||
elif self.job_id:
|
||||
return "Job"
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def job_source_id(self):
|
||||
return self.implicit_collection_jobs_id or self.job_id
|
||||
|
||||
def to_hda_representative(self, multiple=False):
|
||||
rval = []
|
||||
for dataset in self.collection.dataset_elements:
|
||||
@@ -3399,6 +3444,8 @@ class HistoryDatasetCollectionAssociation(DatasetCollectionInstance,
|
||||
history_content_type=self.history_content_type,
|
||||
visible=self.visible,
|
||||
deleted=self.deleted,
|
||||
job_source_id=self.job_source_id,
|
||||
job_source_type=self.job_source_type,
|
||||
**self._base_to_dict(view=view)
|
||||
)
|
||||
|
||||
@@ -3430,6 +3477,11 @@ class HistoryDatasetCollectionAssociation(DatasetCollectionInstance,
|
||||
name=self.name,
|
||||
copied_from_history_dataset_collection_association=self,
|
||||
)
|
||||
if self.implicit_collection_jobs_id:
|
||||
hdca.implicit_collection_jobs_id = self.implicit_collection_jobs_id
|
||||
elif self.job_id:
|
||||
hdca.job_id = self.job_id
|
||||
|
||||
collection_copy = self.collection.copy(
|
||||
destination=hdca,
|
||||
element_destination=element_destination,
|
||||
@@ -3474,6 +3526,8 @@ class DatasetCollectionElement(object, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'element_type', 'element_index', 'element_identifier']
|
||||
dict_element_visible_keys = ['id', 'element_type', 'element_index', 'element_identifier']
|
||||
|
||||
UNINITIALIZED_ELEMENT = object()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id=None,
|
||||
@@ -3488,7 +3542,7 @@ class DatasetCollectionElement(object, Dictifiable):
|
||||
self.ldda = element
|
||||
elif isinstance(element, DatasetCollection):
|
||||
self.child_collection = element
|
||||
else:
|
||||
elif element != self.UNINITIALIZED_ELEMENT:
|
||||
raise AttributeError('Unknown element type provided: %s' % type(element))
|
||||
|
||||
self.id = id
|
||||
@@ -3506,7 +3560,7 @@ class DatasetCollectionElement(object, Dictifiable):
|
||||
# TOOD: Rename element_type to element_type.
|
||||
return "dataset_collection"
|
||||
else:
|
||||
raise Exception("Unknown element instance type")
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_collection(self):
|
||||
@@ -3521,7 +3575,7 @@ class DatasetCollectionElement(object, Dictifiable):
|
||||
elif self.child_collection:
|
||||
return self.child_collection
|
||||
else:
|
||||
raise Exception("Unknown element instance type")
|
||||
return None
|
||||
|
||||
@property
|
||||
def dataset_instance(self):
|
||||
@@ -3951,7 +4005,7 @@ class StoredWorkflowMenuEntry(object):
|
||||
self.order_index = None
|
||||
|
||||
|
||||
class WorkflowInvocation(object, Dictifiable):
|
||||
class WorkflowInvocation(object, UsesCreateAndUpdateTime, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'update_time', 'workflow_id', 'history_id', 'uuid', 'state']
|
||||
dict_element_visible_keys = ['id', 'update_time', 'workflow_id', 'history_id', 'uuid', 'state']
|
||||
states = Bunch(
|
||||
@@ -4026,17 +4080,16 @@ class WorkflowInvocation(object, Dictifiable):
|
||||
step_invocations = {}
|
||||
for invocation_step in self.steps:
|
||||
step_id = invocation_step.workflow_step_id
|
||||
if step_id not in step_invocations:
|
||||
step_invocations[step_id] = []
|
||||
step_invocations[step_id].append(invocation_step)
|
||||
assert step_id not in step_invocations
|
||||
step_invocations[step_id] = invocation_step
|
||||
return step_invocations
|
||||
|
||||
def step_invocations_for_step_id(self, step_id):
|
||||
step_invocations = []
|
||||
def step_invocation_for_step_id(self, step_id):
|
||||
target_invocation_step = None
|
||||
for invocation_step in self.steps:
|
||||
if step_id == invocation_step.workflow_step_id:
|
||||
step_invocations.append(invocation_step)
|
||||
return step_invocations
|
||||
target_invocation_step = invocation_step
|
||||
return target_invocation_step
|
||||
|
||||
@staticmethod
|
||||
def poll_active_workflow_ids(
|
||||
@@ -4062,6 +4115,24 @@ class WorkflowInvocation(object, Dictifiable):
|
||||
# is relatively intutitive.
|
||||
return [wid for wid in query.all()]
|
||||
|
||||
def add_output(self, workflow_output, step, output_object):
|
||||
if output_object.history_content_type == "dataset":
|
||||
output_assoc = WorkflowInvocationOutputDatasetAssociation()
|
||||
output_assoc.workflow_invocation = self
|
||||
output_assoc.workflow_output = workflow_output
|
||||
output_assoc.workflow_step = step
|
||||
output_assoc.dataset = output_object
|
||||
self.output_datasets.append(output_assoc)
|
||||
elif output_object.history_content_type == "dataset_collection":
|
||||
output_assoc = WorkflowInvocationOutputDatasetCollectionAssociation()
|
||||
output_assoc.workflow_invocation = self
|
||||
output_assoc.workflow_output = workflow_output
|
||||
output_assoc.workflow_step = step
|
||||
output_assoc.dataset_collection = output_object
|
||||
self.output_dataset_collections.append(output_assoc)
|
||||
else:
|
||||
raise Exception("Uknown output type encountered")
|
||||
|
||||
def to_dict(self, view='collection', value_mapper=None, step_details=False):
|
||||
rval = super(WorkflowInvocation, self).to_dict(view=view, value_mapper=value_mapper)
|
||||
if view == 'element':
|
||||
@@ -4077,17 +4148,43 @@ class WorkflowInvocation(object, Dictifiable):
|
||||
inputs = {}
|
||||
for step in self.steps:
|
||||
if step.workflow_step.type == 'tool':
|
||||
for step_input in step.workflow_step.input_connections:
|
||||
output_step_type = step_input.output_step.type
|
||||
if output_step_type in ['data_input', 'data_collection_input']:
|
||||
src = "hda" if output_step_type == 'data_input' else 'hdca'
|
||||
for job_input in step.job.input_datasets:
|
||||
if job_input.name == step_input.input_name:
|
||||
inputs[str(step_input.output_step.order_index)] = {
|
||||
"id": job_input.dataset_id, "src": src,
|
||||
"uuid" : str(job_input.dataset.dataset.uuid) if job_input.dataset.dataset.uuid is not None else None
|
||||
}
|
||||
for job in step.jobs:
|
||||
for step_input in step.workflow_step.input_connections:
|
||||
output_step_type = step_input.output_step.type
|
||||
if output_step_type in ['data_input', 'data_collection_input']:
|
||||
src = "hda" if output_step_type == 'data_input' else 'hdca'
|
||||
for job_input in job.input_datasets:
|
||||
if job_input.name == step_input.input_name:
|
||||
inputs[str(step_input.output_step.order_index)] = {
|
||||
"id": job_input.dataset_id, "src": src,
|
||||
"uuid" : str(job_input.dataset.dataset.uuid) if job_input.dataset.dataset.uuid is not None else None
|
||||
}
|
||||
rval['inputs'] = inputs
|
||||
|
||||
outputs = {}
|
||||
for output_assoc in self.output_datasets:
|
||||
label = output_assoc.workflow_output.label
|
||||
if not label:
|
||||
continue
|
||||
|
||||
outputs[label] = {
|
||||
'src': 'hda',
|
||||
'id': output_assoc.dataset_id,
|
||||
}
|
||||
|
||||
output_collections = {}
|
||||
for output_assoc in self.output_dataset_collections:
|
||||
label = output_assoc.workflow_output.label
|
||||
if not label:
|
||||
continue
|
||||
|
||||
output_collections[label] = {
|
||||
'src': 'hdca',
|
||||
'id': output_assoc.dataset_collection_id,
|
||||
}
|
||||
|
||||
rval['outputs'] = outputs
|
||||
rval['output_collections'] = output_collections
|
||||
return rval
|
||||
|
||||
def update(self):
|
||||
@@ -4120,11 +4217,6 @@ class WorkflowInvocation(object, Dictifiable):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def seconds_since_created(self):
|
||||
create_time = self.create_time or galaxy.model.orm.now.now() # In case not flushed yet
|
||||
return (galaxy.model.orm.now.now() - create_time).total_seconds()
|
||||
|
||||
|
||||
class WorkflowInvocationToSubworkflowInvocationAssociation(object, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'workflow_step_id', 'workflow_invocation_id', 'subworkflow_invocation_id']
|
||||
@@ -4132,36 +4224,83 @@ class WorkflowInvocationToSubworkflowInvocationAssociation(object, Dictifiable):
|
||||
|
||||
|
||||
class WorkflowInvocationStep(object, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'update_time', 'job_id', 'workflow_step_id', 'action']
|
||||
dict_element_visible_keys = ['id', 'update_time', 'job_id', 'workflow_step_id', 'action']
|
||||
dict_collection_visible_keys = ['id', 'update_time', 'job_id', 'workflow_step_id', 'state', 'action']
|
||||
dict_element_visible_keys = ['id', 'update_time', 'job_id', 'workflow_step_id', 'state', 'action']
|
||||
states = Bunch(
|
||||
NEW='new', # Brand new workflow invocation step
|
||||
READY='ready', # Workflow invocation step ready for another iteration of scheduling.
|
||||
SCHEDULED='scheduled', # Workflow invocation step has been scheduled.
|
||||
# CANCELLED='cancelled', TODO: implement and expose
|
||||
# FAILED='failed', TODO: implement and expose
|
||||
)
|
||||
|
||||
def update(self):
|
||||
self.workflow_invocation.update()
|
||||
|
||||
@property
|
||||
def is_new(self):
|
||||
return self.state == self.states.NEW
|
||||
|
||||
def add_output(self, output_name, output_object):
|
||||
if output_object.history_content_type == "dataset":
|
||||
output_assoc = WorkflowInvocationStepOutputDatasetAssociation()
|
||||
output_assoc.workflow_invocation_step = self
|
||||
output_assoc.dataset = output_object
|
||||
output_assoc.output_name = output_name
|
||||
self.output_datasets.append(output_assoc)
|
||||
elif output_object.history_content_type == "dataset_collection":
|
||||
output_assoc = WorkflowInvocationStepOutputDatasetCollectionAssociation()
|
||||
output_assoc.workflow_invocation_step = self
|
||||
output_assoc.dataset_collection = output_object
|
||||
output_assoc.output_name = output_name
|
||||
self.output_dataset_collections.append(output_assoc)
|
||||
else:
|
||||
raise Exception("Uknown output type encountered")
|
||||
|
||||
@property
|
||||
def jobs(self):
|
||||
if self.job:
|
||||
return [self.job]
|
||||
elif self.implicit_collection_jobs:
|
||||
return self.implicit_collection_jobs.job_list
|
||||
else:
|
||||
return []
|
||||
|
||||
def to_dict(self, view='collection', value_mapper=None):
|
||||
rval = super(WorkflowInvocationStep, self).to_dict(view=view, value_mapper=value_mapper)
|
||||
rval['order_index'] = self.workflow_step.order_index
|
||||
rval['workflow_step_label'] = self.workflow_step.label
|
||||
rval['workflow_step_uuid'] = str(self.workflow_step.uuid)
|
||||
rval['state'] = self.job.state if self.job is not None else None
|
||||
if self.job is not None and view == 'element':
|
||||
output_dict = {}
|
||||
for i in self.job.output_datasets:
|
||||
if i.dataset is not None:
|
||||
output_dict[i.name] = {
|
||||
"id" : i.dataset.id, "src" : "hda",
|
||||
"uuid" : str(i.dataset.dataset.uuid) if i.dataset.dataset.uuid is not None else None
|
||||
}
|
||||
for i in self.job.output_library_datasets:
|
||||
if i.dataset is not None:
|
||||
output_dict[i.name] = {
|
||||
"id" : i.dataset.id, "src" : "ldda",
|
||||
"uuid" : str(i.dataset.dataset.uuid) if i.dataset.dataset.uuid is not None else None
|
||||
}
|
||||
rval['outputs'] = output_dict
|
||||
# Following no longer makes sense...
|
||||
# rval['state'] = self.job.state if self.job is not None else None
|
||||
if view == 'element':
|
||||
outputs = {}
|
||||
for output_assoc in self.output_datasets:
|
||||
name = output_assoc.output_name
|
||||
outputs[name] = {
|
||||
'src': 'hda',
|
||||
'id': output_assoc.dataset.id,
|
||||
'uuid': str(output_assoc.dataset.dataset.uuid) if output_assoc.dataset.dataset.uuid is not None else None
|
||||
}
|
||||
|
||||
output_collections = {}
|
||||
for output_assoc in self.output_dataset_collections:
|
||||
name = output_assoc.output_name
|
||||
output_collections[name] = {
|
||||
'src': 'hdca',
|
||||
'id': output_assoc.dataset_collection.id,
|
||||
}
|
||||
|
||||
rval['outputs'] = outputs
|
||||
rval['output_collections'] = output_collections
|
||||
return rval
|
||||
|
||||
|
||||
class WorkflowInvocationStepJobAssociation(object, Dictifiable):
|
||||
dict_collection_visible_keys = ('id', 'job_id', 'workflow_invocation_step_id')
|
||||
dict_element_visible_keys = ('id', 'job_id', 'workflow_invocation_step_id')
|
||||
|
||||
|
||||
class WorkflowRequest(object, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'name', 'type', 'state', 'history_id', 'workflow_id']
|
||||
dict_element_visible_keys = ['id', 'name', 'type', 'state', 'history_id', 'workflow_id']
|
||||
@@ -4216,6 +4355,26 @@ class WorkflowRequestInputStepParmeter(object, Dictifiable):
|
||||
dict_collection_visible_keys = ['id', 'workflow_invocation_id', 'workflow_step_id', 'parameter_value']
|
||||
|
||||
|
||||
class WorkflowInvocationOutputDatasetAssociation(object, Dictifiable):
|
||||
"""Represents links to output datasets for the workflow."""
|
||||
dict_collection_visible_keys = ['id', 'workflow_invocation_id', 'workflow_step_id', 'dataset_id', 'name']
|
||||
|
||||
|
||||
class WorkflowInvocationOutputDatasetCollectionAssociation(object, Dictifiable):
|
||||
"""Represents links to output dataset collections for the workflow."""
|
||||
dict_collection_visible_keys = ['id', 'workflow_invocation_id', 'workflow_step_id', 'dataset_collection_id', 'name']
|
||||
|
||||
|
||||
class WorkflowInvocationStepOutputDatasetAssociation(object, Dictifiable):
|
||||
"""Represents links to output datasets for the workflow."""
|
||||
dict_collection_visible_keys = ['id', 'workflow_invocation_step_id', 'dataset_id', 'output_name']
|
||||
|
||||
|
||||
class WorkflowInvocationStepOutputDatasetCollectionAssociation(object, Dictifiable):
|
||||
"""Represents links to output dataset collections for the workflow."""
|
||||
dict_collection_visible_keys = ['id', 'workflow_invocation_step_id', 'dataset_collection_id', 'output_name']
|
||||
|
||||
|
||||
class MetadataFile(StorableObject):
|
||||
|
||||
def __init__(self, dataset=None, name=None):
|
||||
|
||||
+129
-7
@@ -557,6 +557,20 @@ model.ImplicitlyCreatedDatasetCollectionInput.table = Table(
|
||||
ForeignKey("history_dataset_collection_association.id"), index=True),
|
||||
Column("name", Unicode(255)))
|
||||
|
||||
model.ImplicitCollectionJobs.table = Table(
|
||||
"implicit_collection_jobs", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("populated_state", TrimmedString(64), default='new', nullable=False),
|
||||
)
|
||||
|
||||
model.ImplicitCollectionJobsJobAssociation.table = Table(
|
||||
"implicit_collection_jobs_job_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), index=True),
|
||||
Column("job_id", Integer, ForeignKey("job.id"), index=True), # Consider making this nullable...
|
||||
Column("order_index", Integer, nullable=False),
|
||||
)
|
||||
|
||||
model.JobExternalOutputMetadata.table = Table(
|
||||
"job_external_output_metadata", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
@@ -702,6 +716,7 @@ model.DatasetCollection.table = Table(
|
||||
Column("collection_type", Unicode(255), nullable=False),
|
||||
Column("populated_state", TrimmedString(64), default='ok', nullable=False),
|
||||
Column("populated_state_message", TEXT),
|
||||
Column("element_count", Integer, nullable=True),
|
||||
Column("create_time", DateTime, default=now),
|
||||
Column("update_time", DateTime, default=now, onupdate=now))
|
||||
|
||||
@@ -716,7 +731,10 @@ model.HistoryDatasetCollectionAssociation.table = Table(
|
||||
Column("deleted", Boolean, default=False),
|
||||
Column("copied_from_history_dataset_collection_association_id", Integer,
|
||||
ForeignKey("history_dataset_collection_association.id"), nullable=True),
|
||||
Column("implicit_output_name", Unicode(255), nullable=True))
|
||||
Column("implicit_output_name", Unicode(255), nullable=True),
|
||||
Column("job_id", ForeignKey("job.id"), index=True, nullable=True),
|
||||
Column("implicit_collection_jobs_id", ForeignKey("implicit_collection_jobs.id"), index=True, nullable=True),
|
||||
)
|
||||
|
||||
model.LibraryDatasetCollectionAssociation.table = Table(
|
||||
"library_dataset_collection_association", metadata,
|
||||
@@ -901,9 +919,46 @@ model.WorkflowInvocationStep.table = Table(
|
||||
Column("update_time", DateTime, default=now, onupdate=now),
|
||||
Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True, nullable=False),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False),
|
||||
Column("state", TrimmedString(64), index=True),
|
||||
Column("job_id", Integer, ForeignKey("job.id"), index=True, nullable=True),
|
||||
Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), index=True, nullable=True),
|
||||
Column("action", JSONType, nullable=True))
|
||||
|
||||
model.WorkflowInvocationOutputDatasetAssociation.table = Table(
|
||||
"workflow_invocation_output_dataset_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True),
|
||||
Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True),
|
||||
Column("workflow_output_id", Integer, ForeignKey("workflow_output.id"), index=True),
|
||||
)
|
||||
|
||||
model.WorkflowInvocationOutputDatasetCollectionAssociation.table = Table(
|
||||
"workflow_invocation_output_dataset_collection_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True),
|
||||
Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True),
|
||||
Column("workflow_output_id", Integer, ForeignKey("workflow_output.id"), index=True),
|
||||
)
|
||||
|
||||
model.WorkflowInvocationStepOutputDatasetAssociation.table = Table(
|
||||
"workflow_invocation_step_output_dataset_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_step_id", Integer, ForeignKey("workflow_invocation_step.id"), index=True),
|
||||
Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True),
|
||||
Column("output_name", String(255), nullable=True),
|
||||
)
|
||||
|
||||
model.WorkflowInvocationStepOutputDatasetCollectionAssociation.table = Table(
|
||||
"workflow_invocation_step_output_dataset_collection_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_step_id", Integer, ForeignKey("workflow_invocation_step.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True),
|
||||
Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True),
|
||||
Column("output_name", String(255), nullable=True),
|
||||
)
|
||||
|
||||
model.WorkflowInvocationToSubworkflowInvocationAssociation.table = Table(
|
||||
"workflow_invocation_to_subworkflow_invocation_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
@@ -1837,6 +1892,31 @@ simple_mapping(model.ImplicitlyCreatedDatasetCollectionInput,
|
||||
),
|
||||
)
|
||||
|
||||
simple_mapping(model.ImplicitCollectionJobs)
|
||||
|
||||
# simple_mapping(
|
||||
# model.ImplicitCollectionJobsHistoryDatasetCollectionAssociation,
|
||||
# history_dataset_collection_associations=relation(
|
||||
# model.HistoryDatasetCollectionAssociation,
|
||||
# backref=backref("implicit_collection_jobs_association", uselist=False),
|
||||
# uselist=True,
|
||||
# ),
|
||||
# )
|
||||
|
||||
simple_mapping(
|
||||
model.ImplicitCollectionJobsJobAssociation,
|
||||
implicit_collection_jobs=relation(
|
||||
model.ImplicitCollectionJobs,
|
||||
backref=backref("jobs", uselist=True),
|
||||
uselist=False,
|
||||
),
|
||||
job=relation(
|
||||
model.Job,
|
||||
backref=backref("implicit_collection_jobs_association", uselist=False),
|
||||
uselist=False,
|
||||
),
|
||||
)
|
||||
|
||||
mapper(model.JobParameter, model.JobParameter.table)
|
||||
|
||||
mapper(model.JobExternalOutputMetadata, model.JobExternalOutputMetadata.table, properties=dict(
|
||||
@@ -1928,6 +2008,16 @@ simple_mapping(model.HistoryDatasetCollectionAssociation,
|
||||
model.ImplicitlyCreatedDatasetCollectionInput.table.c.dataset_collection_id)),
|
||||
backref="dataset_collection",
|
||||
),
|
||||
implicit_collection_jobs=relation(
|
||||
model.ImplicitCollectionJobs,
|
||||
backref=backref("history_dataset_collection_associations", uselist=True),
|
||||
uselist=False,
|
||||
),
|
||||
job=relation(
|
||||
model.Job,
|
||||
backref=backref("history_dataset_collection_associations", uselist=True),
|
||||
uselist=False,
|
||||
),
|
||||
tags=relation(model.HistoryDatasetCollectionTagAssociation,
|
||||
order_by=model.HistoryDatasetCollectionTagAssociation.table.c.id,
|
||||
backref='dataset_collections'),
|
||||
@@ -2081,7 +2171,7 @@ mapper(model.WorkflowInvocation, model.WorkflowInvocation.table, properties=dict
|
||||
uselist=True,
|
||||
),
|
||||
steps=relation(model.WorkflowInvocationStep,
|
||||
backref='workflow_invocation'),
|
||||
backref="workflow_invocation"),
|
||||
workflow=relation(model.Workflow)
|
||||
))
|
||||
|
||||
@@ -2094,12 +2184,11 @@ mapper(model.WorkflowInvocationToSubworkflowInvocationAssociation, model.Workflo
|
||||
workflow_step=relation(model.WorkflowStep),
|
||||
))
|
||||
|
||||
mapper(model.WorkflowInvocationStep, model.WorkflowInvocationStep.table, properties=dict(
|
||||
simple_mapping(model.WorkflowInvocationStep,
|
||||
workflow_step=relation(model.WorkflowStep),
|
||||
job=relation(model.Job,
|
||||
backref=backref('workflow_invocation_step',
|
||||
uselist=False))
|
||||
))
|
||||
job=relation(model.Job, backref=backref('workflow_invocation_step', uselist=False), uselist=False),
|
||||
implicit_collection_jobs=relation(model.ImplicitCollectionJobs, backref=backref('workflow_invocation_step', uselist=False), uselist=False),)
|
||||
|
||||
|
||||
simple_mapping(model.WorkflowRequestInputParameter,
|
||||
workflow_invocation=relation(model.WorkflowInvocation))
|
||||
@@ -2129,6 +2218,39 @@ mapper(model.MetadataFile, model.MetadataFile.table, properties=dict(
|
||||
library_dataset=relation(model.LibraryDatasetDatasetAssociation)
|
||||
))
|
||||
|
||||
|
||||
simple_mapping(
|
||||
model.WorkflowInvocationOutputDatasetAssociation,
|
||||
workflow_invocation=relation(model.WorkflowInvocation, backref="output_datasets"),
|
||||
workflow_step=relation(model.WorkflowStep),
|
||||
dataset=relation(model.HistoryDatasetAssociation),
|
||||
workflow_output=relation(model.WorkflowOutput),
|
||||
)
|
||||
|
||||
|
||||
simple_mapping(
|
||||
model.WorkflowInvocationOutputDatasetCollectionAssociation,
|
||||
workflow_invocation=relation(model.WorkflowInvocation, backref="output_dataset_collections"),
|
||||
workflow_step=relation(model.WorkflowStep),
|
||||
dataset_collection=relation(model.HistoryDatasetCollectionAssociation),
|
||||
workflow_output=relation(model.WorkflowOutput),
|
||||
)
|
||||
|
||||
|
||||
simple_mapping(
|
||||
model.WorkflowInvocationStepOutputDatasetAssociation,
|
||||
workflow_invocation_step=relation(model.WorkflowInvocationStep, backref="output_datasets"),
|
||||
dataset=relation(model.HistoryDatasetAssociation),
|
||||
)
|
||||
|
||||
|
||||
simple_mapping(
|
||||
model.WorkflowInvocationStepOutputDatasetCollectionAssociation,
|
||||
workflow_invocation_step=relation(model.WorkflowInvocationStep, backref="output_dataset_collections"),
|
||||
dataset_collection=relation(model.HistoryDatasetCollectionAssociation),
|
||||
)
|
||||
|
||||
|
||||
mapper(model.PageRevision, model.PageRevision.table)
|
||||
|
||||
mapper(model.Page, model.Page.table, properties=dict(
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Migration script for collections and workflows connections.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Table
|
||||
|
||||
from galaxy.model.custom_types import TrimmedString
|
||||
|
||||
|
||||
now = datetime.datetime.utcnow
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
metadata = MetaData()
|
||||
|
||||
|
||||
workflow_invocation_output_dataset_association_table = Table(
|
||||
"workflow_invocation_output_dataset_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")),
|
||||
Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True),
|
||||
Column("workflow_output_id", Integer, ForeignKey("workflow_output.id")),
|
||||
)
|
||||
|
||||
workflow_invocation_output_dataset_collection_association_table = Table(
|
||||
"workflow_invocation_output_dataset_collection_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_id", Integer, ForeignKey("workflow_invocation.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")),
|
||||
Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True),
|
||||
Column("workflow_output_id", Integer, ForeignKey("workflow_output.id")),
|
||||
)
|
||||
|
||||
workflow_invocation_step_output_dataset_association_table = Table(
|
||||
"workflow_invocation_step_output_dataset_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_step_id", Integer, ForeignKey("workflow_invocation_step.id"), index=True),
|
||||
Column("dataset_id", Integer, ForeignKey("history_dataset_association.id"), index=True),
|
||||
Column("output_name", String(255), nullable=True),
|
||||
)
|
||||
|
||||
workflow_invocation_step_output_dataset_collection_association_table = Table(
|
||||
"workflow_invocation_step_output_dataset_collection_association", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("workflow_invocation_step_id", Integer, ForeignKey("workflow_invocation_step.id"), index=True),
|
||||
Column("workflow_step_id", Integer, ForeignKey("workflow_step.id")),
|
||||
Column("dataset_collection_id", Integer, ForeignKey("history_dataset_collection_association.id"), index=True),
|
||||
Column("output_name", String(255), nullable=True),
|
||||
)
|
||||
|
||||
implicit_collection_jobs_table = Table(
|
||||
"implicit_collection_jobs", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("populated_state", TrimmedString(64), default='new', nullable=False),
|
||||
)
|
||||
|
||||
implicit_collection_jobs_job_association_table = Table(
|
||||
"implicit_collection_jobs_job_association", metadata,
|
||||
Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), index=True),
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("job_id", Integer, ForeignKey("job.id"), index=True), # Consider making this nullable...
|
||||
Column("order_index", Integer, nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def get_new_tables():
|
||||
# Normally we define this globally in the file, but we need to delay the
|
||||
# reading of existing tables because an existing workflow_invocation_step
|
||||
# table exists that we want to recreate.
|
||||
|
||||
tables = OrderedDict()
|
||||
tables["workflow_invocation_output_dataset_association"] = workflow_invocation_output_dataset_association_table
|
||||
tables["workflow_invocation_output_dataset_collection_association"] = workflow_invocation_output_dataset_collection_association_table
|
||||
tables["workflow_invocation_step_output_dataset_association"] = workflow_invocation_step_output_dataset_association_table
|
||||
tables["workflow_invocation_step_output_dataset_collection_association"] = workflow_invocation_step_output_dataset_collection_association_table
|
||||
tables["implicit_collection_jobs"] = implicit_collection_jobs_table
|
||||
tables["implicit_collection_jobs_job_association"] = implicit_collection_jobs_job_association_table
|
||||
|
||||
return tables
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
metadata.bind = migrate_engine
|
||||
print(__doc__)
|
||||
|
||||
metadata.reflect()
|
||||
tables = get_new_tables()
|
||||
for table in tables.values():
|
||||
__create(table)
|
||||
|
||||
def nextval(table, col='id'):
|
||||
if migrate_engine.name in ['postgres', 'postgresql']:
|
||||
return "nextval('%s_%s_seq')" % (table, col)
|
||||
elif migrate_engine.name in ['mysql', 'sqlite']:
|
||||
return "null"
|
||||
else:
|
||||
raise Exception("Unhandled database type")
|
||||
|
||||
# Set default for creation to scheduled, actual mapping has new as default.
|
||||
workflow_invocation_step_state_column = Column("state", TrimmedString(64), default="scheduled")
|
||||
if migrate_engine.name in ['postgres', 'postgresql']:
|
||||
implicit_collection_jobs_id_column = Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), nullable=True)
|
||||
job_id_column = Column("job_id", Integer, ForeignKey("job.id"), nullable=True)
|
||||
else:
|
||||
implicit_collection_jobs_id_column = Column("implicit_collection_jobs_id", Integer, nullable=True)
|
||||
job_id_column = Column("job_id", Integer, nullable=True)
|
||||
dataset_collection_element_count_column = Column("element_count", Integer, nullable=True)
|
||||
|
||||
__add_column(implicit_collection_jobs_id_column, "history_dataset_collection_association", metadata)
|
||||
__add_column(job_id_column, "history_dataset_collection_association", metadata)
|
||||
__add_column(dataset_collection_element_count_column, "dataset_collection", metadata)
|
||||
|
||||
implicit_collection_jobs_id_column = Column("implicit_collection_jobs_id", Integer, ForeignKey("implicit_collection_jobs.id"), nullable=True)
|
||||
__add_column(implicit_collection_jobs_id_column, "workflow_invocation_step", metadata)
|
||||
__add_column(workflow_invocation_step_state_column, "workflow_invocation_step", metadata)
|
||||
|
||||
cmd = \
|
||||
"UPDATE dataset_collection SET element_count = " + \
|
||||
"(SELECT (CASE WHEN count(*) > 0 THEN count(*) ELSE 0 END) FROM dataset_collection_element WHERE " + \
|
||||
"dataset_collection_element.dataset_collection_id = dataset_collection.id)"
|
||||
migrate_engine.execute(cmd)
|
||||
|
||||
|
||||
def __add_column(column, table_name, metadata, **kwds):
|
||||
try:
|
||||
table = Table(table_name, metadata, autoload=True)
|
||||
column.create(table, **kwds)
|
||||
except Exception:
|
||||
log.exception("Adding column %s failed.", column)
|
||||
|
||||
|
||||
def __drop_column(column_name, table_name, metadata):
|
||||
try:
|
||||
table = Table(table_name, metadata, autoload=True)
|
||||
getattr(table.c, column_name).drop()
|
||||
except Exception:
|
||||
log.exception("Dropping column %s failed.", column_name)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
metadata.bind = migrate_engine
|
||||
metadata.reflect()
|
||||
|
||||
__drop_column("implicit_collection_jobs_id", "history_dataset_collection_association", metadata)
|
||||
__drop_column("job_id", "history_dataset_collection_association", metadata)
|
||||
__drop_column("implicit_collection_jobs_id", "workflow_invocation_step", metadata)
|
||||
__drop_column("state", "workflow_invocation_step", metadata)
|
||||
__drop_column("element_count", "dataset_collection", metadata)
|
||||
|
||||
tables = get_new_tables()
|
||||
for table in reversed(tables.values()):
|
||||
__drop(table)
|
||||
|
||||
|
||||
def __create(table):
|
||||
try:
|
||||
table.create()
|
||||
except Exception:
|
||||
log.exception("Creating %s table failed.", table.name)
|
||||
|
||||
|
||||
def __drop(table):
|
||||
try:
|
||||
table.drop()
|
||||
except Exception:
|
||||
log.exception("Dropping %s table failed.", table.name)
|
||||
@@ -84,7 +84,10 @@ from galaxy.web import url_for
|
||||
from galaxy.web.form_builder import SelectField
|
||||
from galaxy.work.context import WorkRequestContext
|
||||
from tool_shed.util import common_util
|
||||
from .execute import execute as execute_job
|
||||
from .execute import (
|
||||
execute as execute_job,
|
||||
MappingParameters,
|
||||
)
|
||||
from .loader import (
|
||||
imported_macro_paths,
|
||||
raw_tool_xml_tree,
|
||||
@@ -1244,8 +1247,6 @@ class Tool(object, Dictifiable):
|
||||
# Fixed set of input parameters may correspond to any number of jobs.
|
||||
# Expand these out to individual parameters for given jobs (tool executions).
|
||||
expanded_incomings, collection_info = expand_meta_parameters(trans, self, incoming)
|
||||
if not expanded_incomings:
|
||||
raise exceptions.MessageException('Tool execution failed, trying to run a tool over an empty collection.')
|
||||
|
||||
# Remapping a single job to many jobs doesn't make sense, so disable
|
||||
# remap if multi-runs of tools are being used.
|
||||
@@ -1295,24 +1296,38 @@ class Tool(object, Dictifiable):
|
||||
err_data = {key: value for d in all_errors for (key, value) in d.items()}
|
||||
raise exceptions.MessageException(', '.join(msg for msg in err_data.values()), err_data=err_data)
|
||||
else:
|
||||
execution_tracker = execute_job(trans, self, all_params, history=request_context.history, rerun_remap_job_id=rerun_remap_job_id, collection_info=collection_info)
|
||||
if execution_tracker.successful_jobs:
|
||||
return dict(out_data=execution_tracker.output_datasets,
|
||||
num_jobs=len(execution_tracker.successful_jobs),
|
||||
job_errors=execution_tracker.execution_errors,
|
||||
jobs=execution_tracker.successful_jobs,
|
||||
output_collections=execution_tracker.output_collections,
|
||||
implicit_collections=execution_tracker.implicit_collections)
|
||||
else:
|
||||
mapping_params = MappingParameters(incoming, all_params)
|
||||
execution_tracker = execute_job(trans, self, mapping_params, history=request_context.history, rerun_remap_job_id=rerun_remap_job_id, collection_info=collection_info)
|
||||
# Raise an exception if there were jobs to execute and none of them were submitted,
|
||||
# if at least one is submitted or there are no jobs to execute - return aggregate
|
||||
# information including per-job errors. Arguably we should just always return the
|
||||
# aggregate information - we just haven't done that historically.
|
||||
raise_execution_exception = not execution_tracker.successful_jobs and len(all_params) > 0
|
||||
|
||||
if raise_execution_exception:
|
||||
raise exceptions.MessageException(execution_tracker.execution_errors[0])
|
||||
|
||||
def handle_single_execution(self, trans, rerun_remap_job_id, params, history, mapping_over_collection, execution_cache=None):
|
||||
return dict(out_data=execution_tracker.output_datasets,
|
||||
num_jobs=len(execution_tracker.successful_jobs),
|
||||
job_errors=execution_tracker.execution_errors,
|
||||
jobs=execution_tracker.successful_jobs,
|
||||
output_collections=execution_tracker.output_collections,
|
||||
implicit_collections=execution_tracker.implicit_collections)
|
||||
|
||||
def handle_single_execution(self, trans, rerun_remap_job_id, execution_slice, history, execution_cache=None):
|
||||
"""
|
||||
Return a pair with whether execution is successful as well as either
|
||||
resulting output data or an error message indicating the problem.
|
||||
"""
|
||||
try:
|
||||
job, out_data = self.execute(trans, incoming=params, history=history, rerun_remap_job_id=rerun_remap_job_id, mapping_over_collection=mapping_over_collection, execution_cache=execution_cache)
|
||||
job, out_data = self.execute(
|
||||
trans,
|
||||
incoming=execution_slice.param_combination,
|
||||
history=history,
|
||||
rerun_remap_job_id=rerun_remap_job_id,
|
||||
execution_cache=execution_cache,
|
||||
dataset_collection_elements=execution_slice.dataset_collection_elements,
|
||||
)
|
||||
except httpexceptions.HTTPFound as e:
|
||||
# if it's a paste redirect exception, pass it up the stack
|
||||
raise e
|
||||
@@ -2255,7 +2270,7 @@ class DatabaseOperationTool(Tool):
|
||||
def check_inputs_ready(self, input_datasets, input_dataset_collections):
|
||||
def check_dataset_instance(input_dataset):
|
||||
if input_dataset.is_pending:
|
||||
raise ToolInputsNotReadyException()
|
||||
raise ToolInputsNotReadyException("An input dataset is pending.")
|
||||
|
||||
if self.require_dataset_ok:
|
||||
if input_dataset.state != input_dataset.dataset.states.OK:
|
||||
@@ -2267,7 +2282,7 @@ class DatabaseOperationTool(Tool):
|
||||
for input_dataset_collection_pairs in input_dataset_collections.values():
|
||||
for input_dataset_collection, is_mapped in input_dataset_collection_pairs:
|
||||
if not input_dataset_collection.collection.populated:
|
||||
raise ToolInputsNotReadyException()
|
||||
raise ToolInputsNotReadyException("An input collection is not populated.")
|
||||
|
||||
map(check_dataset_instance, input_dataset_collection.dataset_instances)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ class DefaultToolAction(object):
|
||||
|
||||
return history, inp_data, inp_dataset_collections
|
||||
|
||||
def execute(self, tool, trans, incoming={}, return_job=False, set_output_hid=True, history=None, job_params=None, rerun_remap_job_id=None, mapping_over_collection=False, execution_cache=None):
|
||||
def execute(self, tool, trans, incoming={}, return_job=False, set_output_hid=True, history=None, job_params=None, rerun_remap_job_id=None, execution_cache=None, dataset_collection_elements=None):
|
||||
"""
|
||||
Executes a tool, creating job and tool outputs, associating them, and
|
||||
submitting the job to the job queue. If history is not specified, use
|
||||
@@ -267,7 +267,7 @@ class DefaultToolAction(object):
|
||||
tool=tool,
|
||||
tool_action=self,
|
||||
input_collections=input_collections,
|
||||
mapping_over_collection=mapping_over_collection,
|
||||
dataset_collection_elements=dataset_collection_elements,
|
||||
on_text=on_text,
|
||||
incoming=incoming,
|
||||
params=wrapped_params.params,
|
||||
@@ -304,8 +304,12 @@ class DefaultToolAction(object):
|
||||
data = app.model.HistoryDatasetAssociation(extension=ext, create_dataset=True, flush=False)
|
||||
if hidden is None:
|
||||
hidden = output.hidden
|
||||
if not hidden and dataset_collection_elements is not None: # Mapping over a collection - hide datasets
|
||||
hidden = True
|
||||
if hidden:
|
||||
data.visible = False
|
||||
if dataset_collection_elements is not None and name in dataset_collection_elements:
|
||||
dataset_collection_elements[name].hda = data
|
||||
trans.sa_session.add(data)
|
||||
trans.app.security_agent.set_all_dataset_permissions(data.dataset, output_permissions, new=True)
|
||||
for _, tag in preserved_tags.items():
|
||||
@@ -351,6 +355,7 @@ class DefaultToolAction(object):
|
||||
|
||||
for name, output in tool.outputs.items():
|
||||
if not filter_output(output, incoming):
|
||||
handle_output_timer = ExecutionTimer()
|
||||
if output.collection:
|
||||
collections_manager = app.dataset_collections_service
|
||||
element_identifiers = []
|
||||
@@ -402,15 +407,14 @@ class DefaultToolAction(object):
|
||||
element_kwds = dict(elements=collections_manager.ELEMENTS_UNINITIALIZED)
|
||||
else:
|
||||
element_kwds = dict(element_identifiers=element_identifiers)
|
||||
|
||||
output_collections.create_collection(
|
||||
output=output,
|
||||
name=name,
|
||||
tags=preserved_tags,
|
||||
**element_kwds
|
||||
)
|
||||
log.info("Handled collection output named %s for tool %s %s" % (name, tool.id, handle_output_timer))
|
||||
else:
|
||||
handle_output_timer = ExecutionTimer()
|
||||
handle_output(name, output)
|
||||
log.info("Handled output named %s for tool %s %s" % (name, tool.id, handle_output_timer))
|
||||
|
||||
@@ -594,6 +598,7 @@ class DefaultToolAction(object):
|
||||
job.add_implicit_output_dataset_collection(name, dataset_collection)
|
||||
for name, dataset_collection_instance in out_collection_instances.items():
|
||||
job.add_output_dataset_collection(name, dataset_collection_instance)
|
||||
dataset_collection_instance.job = job
|
||||
|
||||
def _check_input_data_access(self, trans, job, inp_data, current_user_roles):
|
||||
access_timer = ExecutionTimer()
|
||||
@@ -670,13 +675,13 @@ class OutputCollections(object):
|
||||
parameter).
|
||||
"""
|
||||
|
||||
def __init__(self, trans, history, tool, tool_action, input_collections, mapping_over_collection, on_text, incoming, params, job_params):
|
||||
def __init__(self, trans, history, tool, tool_action, input_collections, dataset_collection_elements, on_text, incoming, params, job_params):
|
||||
self.trans = trans
|
||||
self.history = history
|
||||
self.tool = tool
|
||||
self.tool_action = tool_action
|
||||
self.input_collections = input_collections
|
||||
self.mapping_over_collection = mapping_over_collection
|
||||
self.dataset_collection_elements = dataset_collection_elements
|
||||
self.on_text = on_text
|
||||
self.incoming = incoming
|
||||
self.params = params
|
||||
@@ -710,12 +715,15 @@ class OutputCollections(object):
|
||||
for dataset in value.dataset_instances:
|
||||
assert dataset.history is not None
|
||||
|
||||
if self.mapping_over_collection:
|
||||
if self.dataset_collection_elements is not None:
|
||||
dc = collections_manager.create_dataset_collection(
|
||||
self.trans,
|
||||
collection_type=collection_type,
|
||||
**element_kwds
|
||||
)
|
||||
if name in self.dataset_collection_elements:
|
||||
self.dataset_collection_elements[name].child_collection = dc
|
||||
# self.trans.sa_session.add(self.dataset_collection_elements[name])
|
||||
self.out_collections[name] = dc
|
||||
else:
|
||||
hdca_name = self.tool_action.get_output_name(
|
||||
|
||||
@@ -21,7 +21,7 @@ class ModelOperationToolAction(DefaultToolAction):
|
||||
|
||||
tool.check_inputs_ready(inp_data, inp_dataset_collections)
|
||||
|
||||
def execute(self, tool, trans, incoming={}, set_output_hid=False, overwrite=True, history=None, job_params=None, mapping_over_collection=False, execution_cache=None, **kwargs):
|
||||
def execute(self, tool, trans, incoming={}, set_output_hid=False, overwrite=True, history=None, job_params=None, execution_cache=None, **kwargs):
|
||||
if execution_cache is None:
|
||||
execution_cache = ToolExecutionCache(trans)
|
||||
|
||||
@@ -42,7 +42,7 @@ class ModelOperationToolAction(DefaultToolAction):
|
||||
tool=tool,
|
||||
tool_action=self,
|
||||
input_collections=input_collections,
|
||||
mapping_over_collection=mapping_over_collection,
|
||||
dataset_collection_elements=kwargs.get("dataset_collection_elements", None),
|
||||
on_text=on_text,
|
||||
incoming=incoming,
|
||||
params=wrapped_params.params,
|
||||
|
||||
@@ -124,7 +124,7 @@ def persist_uploads(params, trans):
|
||||
elif type(f) == dict and 'local_filename' not in f:
|
||||
raise Exception('Uploaded file was encoded in a way not understood by Galaxy.')
|
||||
if upload_dataset['url_paste'] and upload_dataset['url_paste'].strip() != '':
|
||||
upload_dataset['url_paste'], is_multi_byte = datatypes.sniff.stream_to_file(
|
||||
upload_dataset['url_paste'] = datatypes.sniff.stream_to_file(
|
||||
StringIO(validate_url(upload_dataset['url_paste'], trans.app.config.fetch_url_whitelist_ips)),
|
||||
prefix="strio_url_paste_"
|
||||
)
|
||||
|
||||
+336
-116
@@ -4,12 +4,17 @@ from various states, tracking results, and building implicit dataset
|
||||
collections from matched collections.
|
||||
"""
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
import six
|
||||
|
||||
from six.moves.queue import Queue
|
||||
|
||||
from galaxy.tools.actions import on_text_for_names, ToolExecutionCache
|
||||
from galaxy import model
|
||||
from galaxy.dataset_collections.structure import tool_output_to_structure
|
||||
from galaxy.tools.actions import filter_output, on_text_for_names, ToolExecutionCache
|
||||
from galaxy.tools.parser import ToolOutputCollectionPart
|
||||
from galaxy.util import ExecutionTimer
|
||||
|
||||
@@ -18,36 +23,51 @@ log = logging.getLogger(__name__)
|
||||
EXECUTION_SUCCESS_MESSAGE = "Tool [%s] created job [%s] %s"
|
||||
|
||||
|
||||
def execute(trans, tool, param_combinations, history, rerun_remap_job_id=None, collection_info=None, workflow_invocation_uuid=None):
|
||||
class PartialJobExecution(Exception):
|
||||
|
||||
def __init__(self, execution_tracker):
|
||||
self.execution_tracker = execution_tracker
|
||||
|
||||
|
||||
MappingParameters = collections.namedtuple("MappingParameters", ["param_template", "param_combinations"])
|
||||
|
||||
|
||||
def execute(trans, tool, mapping_params, history, rerun_remap_job_id=None, collection_info=None, workflow_invocation_uuid=None, invocation_step=None, max_num_jobs=None, job_callback=None):
|
||||
"""
|
||||
Execute a tool and return object containing summary (output data, number of
|
||||
failures, etc...).
|
||||
"""
|
||||
if max_num_jobs:
|
||||
assert invocation_step is not None
|
||||
if rerun_remap_job_id:
|
||||
assert invocation_step is None
|
||||
|
||||
all_jobs_timer = ExecutionTimer()
|
||||
execution_tracker = ToolExecutionTracker(tool, param_combinations, collection_info)
|
||||
if invocation_step is None:
|
||||
execution_tracker = ToolExecutionTracker(trans, tool, mapping_params, collection_info)
|
||||
else:
|
||||
execution_tracker = WorkflowStepExecutionTracker(trans, tool, mapping_params, collection_info, invocation_step, job_callback=job_callback)
|
||||
app = trans.app
|
||||
execution_cache = ToolExecutionCache(trans)
|
||||
|
||||
def execute_single_job(params):
|
||||
def execute_single_job(execution_slice):
|
||||
job_timer = ExecutionTimer()
|
||||
params = execution_slice.param_combination
|
||||
if workflow_invocation_uuid:
|
||||
params['__workflow_invocation_uuid__'] = workflow_invocation_uuid
|
||||
elif '__workflow_invocation_uuid__' in params:
|
||||
# Only workflow invocation code gets to set this, ignore user supplied
|
||||
# values or rerun parameters.
|
||||
del params['__workflow_invocation_uuid__']
|
||||
job, result = tool.handle_single_execution(trans, rerun_remap_job_id, params, history, collection_info, execution_cache)
|
||||
|
||||
job, result = tool.handle_single_execution(trans, rerun_remap_job_id, execution_slice, history, execution_cache)
|
||||
if job:
|
||||
message = EXECUTION_SUCCESS_MESSAGE % (tool.id, job.id, job_timer)
|
||||
log.debug(message)
|
||||
execution_tracker.record_success(job, result)
|
||||
execution_tracker.record_success(execution_slice, job, result)
|
||||
else:
|
||||
execution_tracker.record_error(result)
|
||||
|
||||
config = app.config
|
||||
burst_at = getattr(config, 'tool_submission_burst_at', 10)
|
||||
burst_threads = getattr(config, 'tool_submission_burst_threads', 1)
|
||||
|
||||
tool_action = tool.tool_action
|
||||
if hasattr(tool_action, "check_inputs_ready"):
|
||||
for params in execution_tracker.param_combinations:
|
||||
@@ -59,11 +79,26 @@ def execute(trans, tool, param_combinations, history, rerun_remap_job_id=None, c
|
||||
history
|
||||
)
|
||||
|
||||
execution_tracker.ensure_implicit_collections_populated(history, mapping_params.param_template)
|
||||
config = app.config
|
||||
burst_at = getattr(config, 'tool_submission_burst_at', 10)
|
||||
burst_threads = getattr(config, 'tool_submission_burst_threads', 1)
|
||||
|
||||
job_count = len(execution_tracker.param_combinations)
|
||||
if job_count < burst_at or burst_threads < 2:
|
||||
for params in execution_tracker.param_combinations:
|
||||
execute_single_job(params)
|
||||
|
||||
jobs_executed = 0
|
||||
has_remaining_jobs = False
|
||||
|
||||
if (job_count < burst_at or burst_threads < 2):
|
||||
for execution_slice in execution_tracker.new_execution_slices():
|
||||
if max_num_jobs and jobs_executed >= max_num_jobs:
|
||||
has_remaining_jobs = True
|
||||
break
|
||||
else:
|
||||
execute_single_job(execution_slice)
|
||||
jobs_executed += 1
|
||||
else:
|
||||
# TODO: re-record success...
|
||||
q = Queue()
|
||||
|
||||
def worker():
|
||||
@@ -77,42 +112,270 @@ def execute(trans, tool, param_combinations, history, rerun_remap_job_id=None, c
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
for params in execution_tracker.param_combinations:
|
||||
q.put(params)
|
||||
for execution_slice in execution_tracker.new_execution_slices():
|
||||
if max_num_jobs and jobs_executed >= max_num_jobs:
|
||||
has_remaining_jobs = True
|
||||
break
|
||||
else:
|
||||
q.put(execution_slice)
|
||||
jobs_executed += 1
|
||||
|
||||
q.join()
|
||||
|
||||
log.debug("Executed %d job(s) for tool %s request: %s" % (job_count, tool.id, all_jobs_timer))
|
||||
if collection_info:
|
||||
history = history or tool.get_default_history_by_trans(trans)
|
||||
if len(param_combinations) == 0:
|
||||
template = "Attempting to map over an empty collection, this is not yet implemented. collection_info is [%s]"
|
||||
message = template % collection_info
|
||||
log.warning(message)
|
||||
raise Exception(message)
|
||||
params = param_combinations[0]
|
||||
execution_tracker.create_output_collections(trans, history, params)
|
||||
if has_remaining_jobs:
|
||||
raise PartialJobExecution(execution_tracker)
|
||||
else:
|
||||
execution_tracker.finalize_dataset_collections(trans)
|
||||
|
||||
log.debug("Executed %d job(s) for tool %s request: %s" % (job_count, tool.id, all_jobs_timer))
|
||||
return execution_tracker
|
||||
|
||||
|
||||
class ToolExecutionTracker(object):
|
||||
class ExecutionSlice(object):
|
||||
|
||||
def __init__(self, tool, param_combinations, collection_info):
|
||||
def __init__(self, job_index, param_combination, dataset_collection_elements=None):
|
||||
self.job_index = job_index
|
||||
self.param_combination = param_combination
|
||||
self.dataset_collection_elements = dataset_collection_elements
|
||||
|
||||
|
||||
class ExecutionTracker(object):
|
||||
|
||||
def __init__(self, trans, tool, mapping_params, collection_info):
|
||||
# Known ahead of time...
|
||||
self.trans = trans
|
||||
self.tool = tool
|
||||
self.param_combinations = param_combinations
|
||||
self.mapping_params = mapping_params
|
||||
self.collection_info = collection_info
|
||||
self.successful_jobs = []
|
||||
|
||||
self._on_text = None
|
||||
|
||||
# Populated as we go...
|
||||
self.failed_jobs = 0
|
||||
self.execution_errors = []
|
||||
|
||||
self.successful_jobs = []
|
||||
self.output_datasets = []
|
||||
self.output_collections = []
|
||||
self.outputs_by_output_name = collections.defaultdict(list)
|
||||
|
||||
self.implicit_collections = {}
|
||||
|
||||
def record_success(self, job, outputs):
|
||||
@property
|
||||
def param_combinations(self):
|
||||
return self.mapping_params.param_combinations
|
||||
|
||||
@property
|
||||
def example_params(self):
|
||||
if self.mapping_params.param_combinations:
|
||||
return self.mapping_params.param_combinations[0]
|
||||
else:
|
||||
# TODO: This isn't quite right - what we want is something like param_template wrapped,
|
||||
# need a test case with an output filter applied to an empty list, still this is
|
||||
# an improvement over not allowing mapping of empty lists.
|
||||
return self.mapping_params.param_template
|
||||
|
||||
@property
|
||||
def job_count(self):
|
||||
return len(self.param_combinations)
|
||||
|
||||
def record_error(self, error):
|
||||
self.failed_jobs += 1
|
||||
message = "There was a failure executing a job for tool [%s] - %s"
|
||||
log.warning(message, self.tool.id, error)
|
||||
self.execution_errors.append(error)
|
||||
|
||||
@property
|
||||
def on_text(self):
|
||||
if self._on_text is None:
|
||||
collection_names = ["collection %d" % c.hid for c in self.collection_info.collections.values()]
|
||||
self._on_text = on_text_for_names(collection_names)
|
||||
|
||||
return self._on_text
|
||||
|
||||
def output_name(self, trans, history, params, output):
|
||||
on_text = self.on_text
|
||||
|
||||
try:
|
||||
output_collection_name = self.tool.tool_action.get_output_name(
|
||||
output,
|
||||
dataset=None,
|
||||
tool=self.tool,
|
||||
on_text=on_text,
|
||||
trans=trans,
|
||||
history=history,
|
||||
params=params,
|
||||
incoming=None,
|
||||
job_params=None,
|
||||
)
|
||||
except Exception:
|
||||
output_collection_name = "%s across %s" % (self.tool.name, on_text)
|
||||
|
||||
return output_collection_name
|
||||
|
||||
def sliced_input_collection_type(self, input_name):
|
||||
if self.is_implicit_input(input_name):
|
||||
subcollection_mapping_type = self.collection_info.subcollection_mapping_type(input_name)
|
||||
return subcollection_mapping_type
|
||||
# return self.collection_info.structure.sliced_input_collection_type(self.implicit_inputs[input_name])
|
||||
else:
|
||||
return self.mapping_params.param_template[input_name].collection.collection_type
|
||||
|
||||
def _structure_for_output(self, trans, tool_output):
|
||||
structure = self.collection_info.structure
|
||||
if hasattr(tool_output, "default_identifier_source"):
|
||||
# Switch the structure for outputs if the output specified a default_identifier_source
|
||||
collection_type_descriptions = trans.app.dataset_collections_service.collection_type_descriptions
|
||||
|
||||
source_collection = self.collection_info.collections.get(tool_output.default_identifier_source)
|
||||
if source_collection:
|
||||
collection_type_description = collection_type_descriptions.for_collection_type(source_collection.collection.collection_type)
|
||||
_structure = structure.for_dataset_collection(source_collection.collection, collection_type_description=collection_type_description)
|
||||
if structure.can_match(_structure):
|
||||
structure = _structure
|
||||
|
||||
return structure
|
||||
|
||||
def _element_identifiers_for_output(self, trans, tool_output, outputs):
|
||||
output_structure = self._structure_for_output(trans, tool_output)
|
||||
element_identifiers = output_structure.element_identifiers_for_outputs(trans, outputs)
|
||||
return element_identifiers
|
||||
|
||||
def _mapped_output_structure(self, trans, tool_output):
|
||||
collections_manager = trans.app.dataset_collections_service
|
||||
output_structure = tool_output_to_structure(self.sliced_input_collection_type, tool_output, collections_manager)
|
||||
mapping_structure = self._structure_for_output(trans, tool_output)
|
||||
# Output structure may not be known, but input structure must be,
|
||||
# otherwise this step of the workflow shouldn't have been scheduled
|
||||
# or the tool should not have been executable on this input.
|
||||
mapped_output_structure = mapping_structure.multiply(output_structure, uninitialized=True)
|
||||
return mapped_output_structure
|
||||
|
||||
def ensure_implicit_collections_populated(self, history, params):
|
||||
if not self.collection_info:
|
||||
return
|
||||
|
||||
history = history or self.tool.get_default_history_by_trans(self.trans)
|
||||
# params = param_combinations[0] if param_combinations else mapping_params.param_template
|
||||
self.precreate_output_collections(history, params)
|
||||
|
||||
def precreate_output_collections(self, history, params):
|
||||
# params is just one sample tool param execution with parallelized
|
||||
# collection replaced with a specific dataset. Need to replace this
|
||||
# with the collection and wrap everything up so can evaluate output
|
||||
# label.
|
||||
trans = self.trans
|
||||
params.update(self.collection_info.collections) # Replace datasets with source collections for labelling outputs.
|
||||
|
||||
collection_instances = {}
|
||||
implicit_inputs = self.implicit_inputs
|
||||
|
||||
implicit_collection_jobs = model.ImplicitCollectionJobs()
|
||||
for output_name, output in self.tool.outputs.items():
|
||||
if filter_output(output, self.example_params):
|
||||
continue
|
||||
output_collection_name = self.output_name(trans, history, params, output)
|
||||
effective_structure = self._mapped_output_structure(trans, output)
|
||||
collection_instance = trans.app.dataset_collections_service.precreate_dataset_collection_instance(
|
||||
trans=trans,
|
||||
parent=history,
|
||||
name=output_collection_name,
|
||||
implicit_inputs=implicit_inputs,
|
||||
implicit_output_name=output_name,
|
||||
structure=effective_structure,
|
||||
)
|
||||
collection_instance.implicit_collection_jobs = implicit_collection_jobs
|
||||
collection_instances[output_name] = collection_instance
|
||||
trans.sa_session.add(collection_instance)
|
||||
# Needed to flush the association created just above with
|
||||
# job.add_output_dataset_collection.
|
||||
trans.sa_session.flush()
|
||||
self.implicit_collections = collection_instances
|
||||
|
||||
@property
|
||||
def implicit_collection_jobs(self):
|
||||
# TODO: refactor to track this properly maybe?
|
||||
if self.implicit_collections:
|
||||
return six.next(six.itervalues(self.implicit_collections)).implicit_collection_jobs
|
||||
else:
|
||||
return None
|
||||
|
||||
def finalize_dataset_collections(self, trans):
|
||||
# TODO: this probably needs to be reworked some, we should have the collection methods
|
||||
# return a list of changed objects to add to the session and flush and we should only
|
||||
# be finalizing collections to a depth of self.collection_info.structure. So for instance
|
||||
# if you are mapping a list over a tool that dynamically generates lists - we won't actually
|
||||
# know the structure of the inner list until after its job is complete.
|
||||
if self.failed_jobs > 0:
|
||||
for i, implicit_collection in enumerate(self.implicit_collections.values()):
|
||||
if i == 0:
|
||||
implicit_collection_jobs = implicit_collection.implicit_collection_jobs
|
||||
implicit_collection_jobs.populated_state = "failed"
|
||||
trans.sa_session.add(implicit_collection_jobs)
|
||||
implicit_collection.collection.handle_population_failed("One or more jobs failed during dataset initialization.")
|
||||
trans.sa_session.add(implicit_collection.collection)
|
||||
else:
|
||||
for i, implicit_collection in enumerate(self.implicit_collections.values()):
|
||||
if i == 0:
|
||||
implicit_collection_jobs = implicit_collection.implicit_collection_jobs
|
||||
implicit_collection_jobs.populated_state = "ok"
|
||||
trans.sa_session.add(implicit_collection_jobs)
|
||||
implicit_collection.collection.finalize()
|
||||
trans.sa_session.add(implicit_collection.collection)
|
||||
trans.sa_session.flush()
|
||||
|
||||
@property
|
||||
def implicit_inputs(self):
|
||||
implicit_inputs = list(self.collection_info.collections.items())
|
||||
return implicit_inputs
|
||||
|
||||
def is_implicit_input(self, input_name):
|
||||
return input_name in self.collection_info.collections
|
||||
|
||||
def walk_implicit_collections(self):
|
||||
return self.collection_info.structure.walk_collections(self.implicit_collections)
|
||||
|
||||
def new_execution_slices(self):
|
||||
if self.collection_info is None:
|
||||
for job_index, param_combination in enumerate(self.param_combinations):
|
||||
yield ExecutionSlice(job_index, param_combination)
|
||||
else:
|
||||
for execution_slice in self.new_collection_execution_slices():
|
||||
yield execution_slice
|
||||
|
||||
def record_success(self, execution_slice, job, outputs):
|
||||
# TODO: successful_jobs need to be inserted in the correct place...
|
||||
self.successful_jobs.append(job)
|
||||
self.output_datasets.extend(outputs)
|
||||
for job_output in job.output_dataset_collection_instances:
|
||||
self.output_collections.append((job_output.name, job_output.dataset_collection_instance))
|
||||
if self.implicit_collections:
|
||||
implicit_collection_jobs = None
|
||||
for output_name, collection_instance in self.implicit_collections.items():
|
||||
job.add_output_dataset_collection(output_name, collection_instance)
|
||||
if implicit_collection_jobs is None:
|
||||
implicit_collection_jobs = collection_instance.implicit_collection_jobs
|
||||
|
||||
job_assoc = model.ImplicitCollectionJobsJobAssociation()
|
||||
job_assoc.order_index = execution_slice.job_index
|
||||
job_assoc.implicit_collection_jobs = implicit_collection_jobs
|
||||
job_assoc.job_id = job.id
|
||||
self.trans.sa_session.add(job_assoc)
|
||||
|
||||
|
||||
# Seperate these because workflows need to track their jobs belong to the invocation
|
||||
# in the database immediately and they can be recovered.
|
||||
class ToolExecutionTracker(ExecutionTracker):
|
||||
|
||||
def __init__(self, trans, tool, mapping_params, collection_info):
|
||||
super(ToolExecutionTracker, self).__init__(trans, tool, mapping_params, collection_info)
|
||||
|
||||
# New to track these things for tool output API response in the tool case,
|
||||
# in the workflow case we just write stuff to the database and forget about
|
||||
# it.
|
||||
self.outputs_by_output_name = collections.defaultdict(list)
|
||||
|
||||
def record_success(self, execution_slice, job, outputs):
|
||||
super(ToolExecutionTracker, self).record_success(execution_slice, job, outputs)
|
||||
for output_name, output_dataset in outputs:
|
||||
if ToolOutputCollectionPart.is_named_collection_part_name(output_name):
|
||||
# Skip known collection outputs, these will be covered by
|
||||
@@ -121,101 +384,58 @@ class ToolExecutionTracker(object):
|
||||
self.outputs_by_output_name[output_name].append(output_dataset)
|
||||
for job_output in job.output_dataset_collections:
|
||||
self.outputs_by_output_name[job_output.name].append(job_output.dataset_collection)
|
||||
for job_output in job.output_dataset_collection_instances:
|
||||
self.output_collections.append((job_output.name, job_output.dataset_collection_instance))
|
||||
|
||||
def record_error(self, error):
|
||||
self.failed_jobs += 1
|
||||
message = "There was a failure executing a job for tool [%s] - %s"
|
||||
log.warning(message, self.tool.id, error)
|
||||
self.execution_errors.append(error)
|
||||
def new_collection_execution_slices(self):
|
||||
for job_index, (param_combination, dataset_collection_elements) in enumerate(itertools.izip(self.param_combinations, self.walk_implicit_collections())):
|
||||
for dataset_collection_element in dataset_collection_elements.values():
|
||||
assert dataset_collection_element.element_object is None
|
||||
|
||||
def create_output_collections(self, trans, history, params):
|
||||
# TODO: Move this function - it doesn't belong here but it does need
|
||||
# the information in this class and potential extensions.
|
||||
if self.failed_jobs > 0:
|
||||
return []
|
||||
yield ExecutionSlice(job_index, param_combination, dataset_collection_elements)
|
||||
|
||||
structure = self.collection_info.structure
|
||||
|
||||
# params is just one sample tool param execution with parallelized
|
||||
# collection replaced with a specific dataset. Need to replace this
|
||||
# with the collection and wrap everything up so can evaluate output
|
||||
# label.
|
||||
params.update(self.collection_info.collections) # Replace datasets with source collections for labelling outputs.
|
||||
class WorkflowStepExecutionTracker(ExecutionTracker):
|
||||
|
||||
collection_names = ["collection %d" % c.hid for c in self.collection_info.collections.values()]
|
||||
on_text = on_text_for_names(collection_names)
|
||||
def __init__(self, trans, tool, mapping_params, collection_info, invocation_step, job_callback):
|
||||
super(WorkflowStepExecutionTracker, self).__init__(trans, tool, mapping_params, collection_info)
|
||||
self.invocation_step = invocation_step
|
||||
self.job_callback = job_callback
|
||||
|
||||
collections = {}
|
||||
def record_success(self, execution_slice, job, outputs):
|
||||
super(WorkflowStepExecutionTracker, self).record_success(execution_slice, job, outputs)
|
||||
if self.collection_info:
|
||||
self.invocation_step.implicit_collection_jobs = self.implicit_collection_jobs
|
||||
else:
|
||||
self.invocation_step.job = job
|
||||
self.job_callback(job)
|
||||
|
||||
implicit_inputs = list(self.collection_info.collections.items())
|
||||
for output_name, outputs in self.outputs_by_output_name.items():
|
||||
if not len(structure) == len(outputs):
|
||||
# Output does not have the same structure, if all jobs were
|
||||
# successfully submitted this shouldn't have happened.
|
||||
log.warning("Problem matching up datasets while attempting to create implicit dataset collections")
|
||||
def new_collection_execution_slices(self):
|
||||
for job_index, (param_combination, dataset_collection_elements) in enumerate(itertools.izip(self.param_combinations, self.walk_implicit_collections())):
|
||||
# Two options here - check if the element has been populated or check if the
|
||||
# a WorkflowInvocationStepJobAssociation exists. Not sure which is better but
|
||||
# for now I have the first so lets check.
|
||||
found_result = False
|
||||
for dataset_collection_element in dataset_collection_elements.values():
|
||||
if dataset_collection_element.element_object is not None:
|
||||
found_result = True
|
||||
break
|
||||
if found_result:
|
||||
continue
|
||||
output = self.tool.outputs[output_name]
|
||||
yield ExecutionSlice(job_index, param_combination, dataset_collection_elements)
|
||||
|
||||
element_identifiers = None
|
||||
if hasattr(output, "default_identifier_source"):
|
||||
# Switch the structure for outputs if the output specified a default_identifier_source
|
||||
collection_type_descriptions = trans.app.dataset_collections_service.collection_type_descriptions
|
||||
def ensure_implicit_collections_populated(self, history, params):
|
||||
if not self.collection_info:
|
||||
return
|
||||
|
||||
source_collection = self.collection_info.collections.get(output.default_identifier_source)
|
||||
if source_collection:
|
||||
collection_type_description = collection_type_descriptions.for_collection_type(source_collection.collection.collection_type)
|
||||
_structure = structure.for_dataset_collection(source_collection.collection, collection_type_description=collection_type_description)
|
||||
if structure.can_match(_structure):
|
||||
element_identifiers = _structure.element_identifiers_for_outputs(trans, outputs)
|
||||
|
||||
if not element_identifiers:
|
||||
element_identifiers = structure.element_identifiers_for_outputs(trans, outputs)
|
||||
|
||||
implicit_collection_info = dict(
|
||||
implicit_inputs=implicit_inputs,
|
||||
implicit_output_name=output_name,
|
||||
outputs=outputs
|
||||
)
|
||||
try:
|
||||
output_collection_name = self.tool.tool_action.get_output_name(
|
||||
output,
|
||||
dataset=None,
|
||||
tool=self.tool,
|
||||
on_text=on_text,
|
||||
trans=trans,
|
||||
history=history,
|
||||
params=params,
|
||||
incoming=None,
|
||||
job_params=None,
|
||||
)
|
||||
except Exception:
|
||||
output_collection_name = "%s across %s" % (self.tool.name, on_text)
|
||||
|
||||
child_element_identifiers = element_identifiers["element_identifiers"]
|
||||
collection_type = element_identifiers["collection_type"]
|
||||
collection = trans.app.dataset_collections_service.create(
|
||||
trans=trans,
|
||||
parent=history,
|
||||
name=output_collection_name,
|
||||
element_identifiers=child_element_identifiers,
|
||||
collection_type=collection_type,
|
||||
implicit_collection_info=implicit_collection_info,
|
||||
)
|
||||
for job in self.successful_jobs:
|
||||
# TODO: Think through this, may only want this for output
|
||||
# collections - or we may be already recording data in some
|
||||
# other way.
|
||||
if job not in trans.sa_session:
|
||||
job = trans.sa_session.query(trans.app.model.Job).get(job.id)
|
||||
job.add_output_dataset_collection(output_name, collection)
|
||||
collections[output_name] = collection
|
||||
|
||||
# Needed to flush the association created just above with
|
||||
# job.add_output_dataset_collection.
|
||||
trans.sa_session.flush()
|
||||
self.implicit_collections = collections
|
||||
history = history or self.tool.get_default_history_by_trans(self.trans)
|
||||
if self.invocation_step.is_new:
|
||||
self.precreate_output_collections(history, params)
|
||||
else:
|
||||
collections = {}
|
||||
for output_assoc in self.invocation_step.output_dataset_collections:
|
||||
implicit_collection = output_assoc.dataset_collection
|
||||
assert hasattr(implicit_collection, "history_content_type") # make sure it is an HDCA and not a DC
|
||||
collections[output_assoc.output_name] = output_assoc.dataset_collection
|
||||
self.implicit_collections = collections
|
||||
|
||||
|
||||
__all__ = ('execute', )
|
||||
|
||||
@@ -565,7 +565,7 @@ class UploadDataset(Group):
|
||||
dataset.precreated_name = dataset.name = self.get_composite_dataset_name(context)
|
||||
if dataset.datatype.composite_type == 'auto_primary_file':
|
||||
# replace sniff here with just creating an empty file
|
||||
temp_name, is_multi_byte = sniff.stream_to_file(StringIO(d_type.generate_primary_file(dataset)), prefix='upload_auto_primary_file')
|
||||
temp_name = sniff.stream_to_file(StringIO(d_type.generate_primary_file(dataset)), prefix='upload_auto_primary_file')
|
||||
dataset.primary_file = temp_name
|
||||
dataset.to_posix_lines = True
|
||||
dataset.auto_decompress = True
|
||||
|
||||
@@ -195,7 +195,10 @@ class ToolOutputCollectionStructure(object):
|
||||
if self.structured_like:
|
||||
collection_prototype = inputs[self.structured_like].collection
|
||||
else:
|
||||
collection_prototype = type_registry.prototype(self.collection_type)
|
||||
collection_type = self.collection_type
|
||||
assert collection_type
|
||||
collection_prototype = type_registry.prototype(collection_type)
|
||||
collection_prototype.collection_type = collection_type
|
||||
return collection_prototype
|
||||
|
||||
|
||||
|
||||
@@ -39,39 +39,3 @@ def check_image_type(filename, types):
|
||||
if fmt in types:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_image_ext(file_path):
|
||||
# determine ext
|
||||
fmt = image_type(file_path)
|
||||
if fmt in ['JPG', 'JPEG']:
|
||||
return 'jpg'
|
||||
if fmt == 'PNG':
|
||||
return 'png'
|
||||
if fmt == 'TIFF':
|
||||
return 'tiff'
|
||||
if fmt == 'BMP':
|
||||
return 'bmp'
|
||||
if fmt == 'GIF':
|
||||
return 'gif'
|
||||
if fmt == 'IM':
|
||||
return 'im'
|
||||
if fmt == 'PCD':
|
||||
return 'pcd'
|
||||
if fmt == 'PCX':
|
||||
return 'pcx'
|
||||
if fmt == 'PPM':
|
||||
return 'ppm'
|
||||
if fmt == 'PSD':
|
||||
return 'psd'
|
||||
if fmt == 'XBM':
|
||||
return 'xbm'
|
||||
if fmt == 'XPM':
|
||||
return 'xpm'
|
||||
if fmt == 'RGB':
|
||||
return 'rgb'
|
||||
if fmt == 'PBM':
|
||||
return 'pbm'
|
||||
if fmt == 'PGM':
|
||||
return 'pgm'
|
||||
return None
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
try:
|
||||
import wchartype
|
||||
except ImportError:
|
||||
wchartype = None
|
||||
|
||||
from six import text_type
|
||||
|
||||
|
||||
def is_multi_byte(chars):
|
||||
if wchartype is None:
|
||||
message = "Attempted to use galaxy.util.multi_byte but dependency wchartype is unavailable."
|
||||
raise Exception(message)
|
||||
for char in chars:
|
||||
try:
|
||||
char = text_type(char)
|
||||
except UnicodeDecodeError:
|
||||
# Probably binary
|
||||
return False
|
||||
if (wchartype.is_asian(char) or wchartype.is_full_width(char) or
|
||||
wchartype.is_kanji(char) or wchartype.is_hiragana(char) or
|
||||
wchartype.is_katakana(char) or wchartype.is_half_katakana(char) or
|
||||
wchartype.is_hangul(char) or wchartype.is_full_digit(char) or
|
||||
wchartype.is_full_letter(char)):
|
||||
return True
|
||||
return False
|
||||
@@ -21,6 +21,7 @@ from galaxy.managers.collections_util import (
|
||||
dictify_dataset_collection_instance,
|
||||
get_hda_and_element_identifiers
|
||||
)
|
||||
from galaxy.managers.jobs import fetch_job_states, summarize_jobs_to_dict
|
||||
from galaxy.util.json import safe_dumps
|
||||
from galaxy.util.streamball import StreamBall
|
||||
from galaxy.web import (
|
||||
@@ -126,33 +127,133 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
|
||||
|
||||
return rval
|
||||
|
||||
def __collection_dict(self, trans, dataset_collection_instance, view="collection"):
|
||||
def __collection_dict(self, trans, dataset_collection_instance, **kwds):
|
||||
return dictify_dataset_collection_instance(dataset_collection_instance,
|
||||
security=trans.security, parent=dataset_collection_instance.history, view=view)
|
||||
security=trans.security, parent=dataset_collection_instance.history, **kwds)
|
||||
|
||||
@expose_api_anonymous
|
||||
def show(self, trans, id, history_id, **kwd):
|
||||
"""
|
||||
show( self, trans, id, history_id, **kwd )
|
||||
* GET /api/histories/{history_id}/contents/{id}
|
||||
return detailed information about an HDA within a history
|
||||
* GET /api/histories/{history_id}/contents/{type}/{id}
|
||||
return detailed information about an HDA or HDCA within a history
|
||||
.. note:: Anonymous users are allowed to get their current history contents
|
||||
|
||||
:type id: str
|
||||
:param id: the encoded id of the HDA to return
|
||||
:param id: the encoded id of the HDA or HDCA to return
|
||||
:type type: str
|
||||
:param id: 'dataset' or 'dataset_collection'
|
||||
:type history_id: str
|
||||
:param history_id: encoded id string of the HDA's History
|
||||
:param history_id: encoded id string of the HDA's or HDCA's History
|
||||
:type view: str
|
||||
:param view: if fetching a dataset collection - the view style of
|
||||
the dataset collection to produce.
|
||||
'collection' returns no element information, 'element'
|
||||
returns detailed element information for all datasets,
|
||||
'element-reference' returns a minimal set of information
|
||||
about datasets (for instance id, type, and state but not
|
||||
metadata, peek, info, or name). The default is 'element'.
|
||||
:type fuzzy_count: int
|
||||
:param fuzzy_count: this value can be used to broadly restrict the magnitude
|
||||
of the number of elements returned via the API for large
|
||||
collections. The number of actual elements returned may
|
||||
be "a bit" more than this number or "a lot" less - varying
|
||||
on the depth of nesting, balance of nesting at each level,
|
||||
and size of target collection. The consumer of this API should
|
||||
not expect a stable number or pre-calculable number of
|
||||
elements to be produced given this parameter - the only
|
||||
promise is that this API will not respond with an order
|
||||
of magnitude more elements estimated with this value.
|
||||
The UI uses this parameter to fetch a "balanced" concept of
|
||||
the "start" of large collections at every depth of the
|
||||
collection.
|
||||
|
||||
:rtype: dict
|
||||
:returns: dictionary containing detailed HDA information
|
||||
:returns: dictionary containing detailed HDA or HDCA information
|
||||
"""
|
||||
contents_type = kwd.get('type', 'dataset')
|
||||
contents_type = self.__get_contents_type(trans, kwd)
|
||||
if contents_type == 'dataset':
|
||||
return self.__show_dataset(trans, id, **kwd)
|
||||
elif contents_type == 'dataset_collection':
|
||||
return self.__show_dataset_collection(trans, id, history_id, **kwd)
|
||||
|
||||
@expose_api_anonymous
|
||||
def index_jobs_summary(self, trans, history_id, **kwd):
|
||||
"""
|
||||
* GET /api/histories/{history_id}/jobs_summary
|
||||
return detailed information about an HDA or HDCAs jobs
|
||||
|
||||
Warning: We allow anyone to fetch job state information about any object they
|
||||
can guess an encoded ID for - it isn't considered protected data. This keeps
|
||||
polling IDs as part of state calculation for large histories and collections as
|
||||
efficient as possible.
|
||||
|
||||
:type history_id: str
|
||||
:param history_id: encoded id string of the HDA's or the HDCA's History
|
||||
:type ids: str[]
|
||||
:param ids: the encoded ids of job summary objects to return - if ids
|
||||
is specified types must also be specified and have same length.
|
||||
:type types: str[]
|
||||
:param types: type of object represented by elements in the ids array - either
|
||||
Job or ImplicitCollectionJob.
|
||||
|
||||
:rtype: dict[]
|
||||
:returns: an array of job summary object dictionaries.
|
||||
"""
|
||||
ids = kwd.get("ids", None)
|
||||
types = kwd.get("types", None)
|
||||
if ids is None:
|
||||
assert types is None
|
||||
# TODO: ...
|
||||
pass
|
||||
else:
|
||||
return self.__handle_unknown_contents_type(trans, contents_type)
|
||||
ids = util.listify(ids)
|
||||
types = util.listify(types)
|
||||
return map(lambda s: self.encode_all_ids(trans, s), fetch_job_states(self.app, trans.sa_session, ids, types))
|
||||
|
||||
@expose_api_anonymous
|
||||
def show_jobs_summary(self, trans, id, history_id, **kwd):
|
||||
"""
|
||||
* GET /api/histories/{history_id}/contents/{type}/{id}/jobs_summary
|
||||
return detailed information about an HDA or HDCAs jobs
|
||||
|
||||
Warning: We allow anyone to fetch job state information about any object they
|
||||
can guess an encoded ID for - it isn't considered protected data. This keeps
|
||||
polling IDs as part of state calculation for large histories and collections as
|
||||
efficient as possible.
|
||||
|
||||
:type id: str
|
||||
:param id: the encoded id of the HDA to return
|
||||
:type history_id: str
|
||||
:param history_id: encoded id string of the HDA's or the HDCA's History
|
||||
|
||||
:rtype: dict
|
||||
:returns: dictionary containing jobs summary object
|
||||
"""
|
||||
contents_type = self.__get_contents_type(trans, kwd)
|
||||
# At most one of job or implicit_collection_jobs should be found.
|
||||
job = None
|
||||
implicit_collection_jobs = None
|
||||
if contents_type == 'dataset':
|
||||
hda = self.hda_manager.get_accessible(self.decode_id(id), trans.user)
|
||||
job = hda.creating_job
|
||||
elif contents_type == 'dataset_collection':
|
||||
dataset_collection_instance = self.__get_accessible_collection(trans, id, history_id)
|
||||
job_source_type = dataset_collection_instance.job_source_type
|
||||
if job_source_type == "Job":
|
||||
job = dataset_collection_instance.job
|
||||
elif job_source_type == "ImplicitCollectionJobs":
|
||||
implicit_collection_jobs = dataset_collection_instance.implicit_collection_jobs
|
||||
|
||||
assert job is None or implicit_collection_jobs is None
|
||||
return self.encode_all_ids(trans, summarize_jobs_to_dict(trans.sa_session, job or implicit_collection_jobs))
|
||||
|
||||
def __get_contents_type(self, trans, kwd):
|
||||
contents_type = kwd.get('type', 'dataset')
|
||||
if contents_type not in ['dataset', 'dataset_collection']:
|
||||
self.__handle_unknown_contents_type(trans, contents_type)
|
||||
|
||||
return contents_type
|
||||
|
||||
def __show_dataset(self, trans, id, **kwd):
|
||||
hda = self.hda_manager.get_accessible(self.decode_id(id), trans.user)
|
||||
@@ -162,18 +263,19 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
|
||||
**self._parse_serialization_params(kwd, 'detailed'))
|
||||
|
||||
def __show_dataset_collection(self, trans, id, history_id, **kwd):
|
||||
try:
|
||||
service = trans.app.dataset_collections_service
|
||||
dataset_collection_instance = service.get_dataset_collection_instance(
|
||||
trans=trans,
|
||||
instance_type='history',
|
||||
id=id,
|
||||
)
|
||||
return self.__collection_dict(trans, dataset_collection_instance, view="element")
|
||||
except Exception as e:
|
||||
log.exception("Error in history API at listing dataset collection")
|
||||
trans.response.status = 500
|
||||
return {'error': str(e)}
|
||||
dataset_collection_instance = self.__get_accessible_collection(trans, id, history_id)
|
||||
view = kwd.get("view", "element")
|
||||
fuzzy_count = kwd.get("fuzzy_count", None)
|
||||
if fuzzy_count:
|
||||
fuzzy_count = int(fuzzy_count)
|
||||
return self.__collection_dict(trans, dataset_collection_instance, view=view, fuzzy_count=fuzzy_count)
|
||||
|
||||
def __get_accessible_collection(self, trans, id, history_id):
|
||||
return trans.app.dataset_collections_service.get_dataset_collection_instance(
|
||||
trans=trans,
|
||||
instance_type="history",
|
||||
id=id
|
||||
)
|
||||
|
||||
@expose_api_raw_anonymous
|
||||
def download_dataset_collection(self, trans, id, history_id=None, **kwd):
|
||||
@@ -188,14 +290,8 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
|
||||
:param history_id: encoded id string of the HDCA's History
|
||||
"""
|
||||
try:
|
||||
service = trans.app.dataset_collections_service
|
||||
dataset_collection_instance = service.get_dataset_collection_instance(
|
||||
trans=trans,
|
||||
instance_type='history',
|
||||
id=id,
|
||||
)
|
||||
dataset_collection_instance = self.__get_accessible_collection(trans, id, history_id)
|
||||
return self.__stream_dataset_collection(trans, dataset_collection_instance)
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Error in API while creating dataset collection archive")
|
||||
trans.response.status = 500
|
||||
|
||||
@@ -363,6 +363,15 @@ def populate_api_routes(webapp, app):
|
||||
action='download_dataset_collection',
|
||||
conditions=dict(method=["GET"]))
|
||||
|
||||
webapp.mapper.connect("/api/histories/{history_id}/jobs_summary",
|
||||
action="index_jobs_summary",
|
||||
controller='history_contents',
|
||||
conditions=dict(method=["GET"]))
|
||||
|
||||
webapp.mapper.connect("/api/histories/{history_id}/contents/{type:%s}s/{id}/jobs_summary" % "|".join(valid_history_contents_types),
|
||||
action="show_jobs_summary",
|
||||
controller='history_contents',
|
||||
conditions=dict(method=["GET"]))
|
||||
# ---- visualizations registry ---- generic template renderer
|
||||
# @deprecated: this route should be considered deprecated
|
||||
webapp.add_route('/visualization/show/{visualization_name}', controller='visualization', action='render', visualization_name=None)
|
||||
|
||||
@@ -21,7 +21,7 @@ from galaxy.tools import (
|
||||
DefaultToolState,
|
||||
ToolInputsNotReadyException
|
||||
)
|
||||
from galaxy.tools.execute import execute
|
||||
from galaxy.tools.execute import execute, MappingParameters, PartialJobExecution
|
||||
from galaxy.tools.parameters import (
|
||||
check_param,
|
||||
params_to_incoming,
|
||||
@@ -214,10 +214,15 @@ class WorkflowModule(object):
|
||||
state.decode(runtime_state, Bunch(inputs=self.get_runtime_inputs()), self.trans.app)
|
||||
return state
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
""" Execute the given workflow step in the given workflow invocation.
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
""" Execute the given workflow invocation step.
|
||||
|
||||
Use the supplied workflow progress object to track outputs, find
|
||||
inputs, etc...
|
||||
inputs, etc....
|
||||
|
||||
Return a False if there is additional processing required to
|
||||
on subsequent workflow scheduling runs, None or True means the workflow
|
||||
step executed properly.
|
||||
"""
|
||||
raise TypeError("Abstract method")
|
||||
|
||||
@@ -230,11 +235,19 @@ class WorkflowModule(object):
|
||||
"""
|
||||
raise exceptions.RequestParameterInvalidException("Attempting to perform invocation step action on module that does not support actions.")
|
||||
|
||||
def recover_mapping(self, step, step_invocations, progress):
|
||||
def recover_mapping(self, invocation_step, progress):
|
||||
""" Re-populate progress object with information about connections
|
||||
from previously executed steps recorded via step_invocations.
|
||||
from previously executed steps recorded via invocation_steps.
|
||||
"""
|
||||
raise TypeError("Abstract method")
|
||||
outputs = {}
|
||||
|
||||
for output_dataset_assoc in invocation_step.output_datasets:
|
||||
outputs[output_dataset_assoc.output_name] = output_dataset_assoc.dataset
|
||||
|
||||
for output_dataset_collection_assoc in invocation_step.output_dataset_collections:
|
||||
outputs[output_dataset_collection_assoc.output_name] = output_dataset_collection_assoc.dataset_collection
|
||||
|
||||
progress.set_step_outputs(invocation_step, outputs, already_persisted=True)
|
||||
|
||||
|
||||
class SubWorkflowModule(WorkflowModule):
|
||||
@@ -320,11 +333,12 @@ class SubWorkflowModule(WorkflowModule):
|
||||
def get_content_id(self):
|
||||
return self.trans.security.encode_id(self.subworkflow.id)
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
""" Execute the given workflow step in the given workflow invocation.
|
||||
Use the supplied workflow progress object to track outputs, find
|
||||
inputs, etc...
|
||||
"""
|
||||
step = invocation_step.workflow_step
|
||||
subworkflow_invoker = progress.subworkflow_invoker(trans, step)
|
||||
subworkflow_invoker.invoke()
|
||||
subworkflow = subworkflow_invoker.workflow
|
||||
@@ -334,7 +348,7 @@ class SubWorkflowModule(WorkflowModule):
|
||||
workflow_output_label = workflow_output.label or "%s:%s" % (step.order_index, workflow_output.output_name)
|
||||
replacement = subworkflow_progress.get_replacement_workflow_output(workflow_output)
|
||||
outputs[workflow_output_label] = replacement
|
||||
progress.set_step_outputs(step, outputs)
|
||||
progress.set_step_outputs(invocation_step, outputs)
|
||||
return None
|
||||
|
||||
def get_runtime_state(self):
|
||||
@@ -353,8 +367,10 @@ class InputModule(WorkflowModule):
|
||||
def get_data_inputs(self):
|
||||
return []
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
job, step_outputs = None, dict(output=step.state.inputs['input'])
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
invocation = invocation_step.workflow_invocation
|
||||
step = invocation_step.workflow_step
|
||||
step_outputs = dict(output=step.state.inputs['input'])
|
||||
|
||||
# Web controller may set copy_inputs_to_history, API controller always sets
|
||||
# inputs.
|
||||
@@ -378,11 +394,10 @@ class InputModule(WorkflowModule):
|
||||
content = next(iter(step_outputs.values()))
|
||||
if content:
|
||||
invocation.add_input(content, step.id)
|
||||
progress.set_outputs_for_input(step, step_outputs)
|
||||
return job
|
||||
progress.set_outputs_for_input(invocation_step, step_outputs)
|
||||
|
||||
def recover_mapping(self, step, step_invocations, progress):
|
||||
progress.set_outputs_for_input(step)
|
||||
def recover_mapping(self, invocation_step, progress):
|
||||
progress.set_outputs_for_input(invocation_step)
|
||||
|
||||
|
||||
class InputDataModule(InputModule):
|
||||
@@ -489,10 +504,10 @@ class InputParameterModule(WorkflowModule):
|
||||
def get_data_inputs(self):
|
||||
return []
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
job, step_outputs = None, dict(output=step.state.inputs['input'])
|
||||
progress.set_outputs_for_input(step, step_outputs)
|
||||
return job
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
step = invocation_step.workflow_step
|
||||
step_outputs = dict(output=step.state.inputs['input'])
|
||||
progress.set_outputs_for_input(invocation_step, step_outputs)
|
||||
|
||||
|
||||
class PauseModule(WorkflowModule):
|
||||
@@ -520,18 +535,18 @@ class PauseModule(WorkflowModule):
|
||||
state.inputs = dict()
|
||||
return state
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
step = invocation_step.workflow_step
|
||||
progress.mark_step_outputs_delayed(step, why="executing pause step")
|
||||
return None
|
||||
|
||||
def recover_mapping(self, step, step_invocations, progress):
|
||||
if step_invocations:
|
||||
step_invocation = step_invocations[0]
|
||||
action = step_invocation.action
|
||||
def recover_mapping(self, invocation_step, progress):
|
||||
if invocation_step:
|
||||
step = invocation_step.workflow_step
|
||||
action = invocation_step.action
|
||||
if action:
|
||||
connection = step.input_connections_by_name["input"][0]
|
||||
replacement = progress.replacement_for_connection(connection)
|
||||
progress.set_step_outputs(step, {'output': replacement})
|
||||
progress.set_step_outputs(invocation_step, {'output': replacement})
|
||||
return
|
||||
elif action is False:
|
||||
raise CancelWorkflowEvaluation()
|
||||
@@ -790,7 +805,9 @@ class ToolModule(WorkflowModule):
|
||||
else:
|
||||
raise ToolMissingException("Tool %s missing. Cannot recover runtime state." % self.tool_id)
|
||||
|
||||
def execute(self, trans, progress, invocation, step):
|
||||
def execute(self, trans, progress, invocation_step):
|
||||
invocation = invocation_step.workflow_invocation
|
||||
step = invocation_step.workflow_step
|
||||
tool = trans.app.toolbox.get_tool(step.tool_id, tool_version=step.tool_version)
|
||||
tool_state = step.state
|
||||
# Not strictly needed - but keep Tool state clean by stripping runtime
|
||||
@@ -855,57 +872,53 @@ class ToolModule(WorkflowModule):
|
||||
|
||||
param_combinations.append(execution_state.inputs)
|
||||
|
||||
complete = False
|
||||
try:
|
||||
mapping_params = MappingParameters(tool_state.inputs, param_combinations)
|
||||
max_num_jobs = progress.maximum_jobs_to_schedule_or_none
|
||||
execution_tracker = execute(
|
||||
trans=self.trans,
|
||||
tool=tool,
|
||||
param_combinations=param_combinations,
|
||||
mapping_params=mapping_params,
|
||||
history=invocation.history,
|
||||
collection_info=collection_info,
|
||||
workflow_invocation_uuid=invocation.uuid.hex
|
||||
workflow_invocation_uuid=invocation.uuid.hex,
|
||||
invocation_step=invocation_step,
|
||||
max_num_jobs=max_num_jobs,
|
||||
job_callback=lambda job: self._handle_post_job_actions(step, job, invocation.replacement_dict),
|
||||
)
|
||||
complete = True
|
||||
except PartialJobExecution as pje:
|
||||
execution_tracker = pje.execution_tracker
|
||||
|
||||
except ToolInputsNotReadyException:
|
||||
delayed_why = "tool [%s] inputs are not ready, this special tool requires inputs to be ready" % tool.id
|
||||
raise DelayedWorkflowEvaluation(why=delayed_why)
|
||||
|
||||
progress.record_executed_job_count(len(execution_tracker.successful_jobs))
|
||||
if collection_info:
|
||||
step_outputs = dict(execution_tracker.implicit_collections)
|
||||
else:
|
||||
step_outputs = dict(execution_tracker.output_datasets)
|
||||
step_outputs.update(execution_tracker.output_collections)
|
||||
progress.set_step_outputs(step, step_outputs)
|
||||
jobs = execution_tracker.successful_jobs
|
||||
for job in jobs:
|
||||
self._handle_post_job_actions(step, job, invocation.replacement_dict)
|
||||
progress.set_step_outputs(invocation_step, step_outputs, already_persisted=not invocation_step.is_new)
|
||||
|
||||
if execution_tracker.execution_errors:
|
||||
failed_count = len(execution_tracker.execution_errors)
|
||||
success_count = len(execution_tracker.successful_jobs)
|
||||
all_count = failed_count + success_count
|
||||
message = "Failed to create %d out of %s job(s) for workflow step." % (failed_count, all_count)
|
||||
message = "Failed to create one or more job(s) for workflow step."
|
||||
raise Exception(message)
|
||||
return jobs
|
||||
|
||||
def recover_mapping(self, step, step_invocations, progress):
|
||||
# Grab a job representing this invocation - for normal workflows
|
||||
# there will be just one job but if this step was mapped over there
|
||||
# may be many.
|
||||
job_0 = step_invocations[0].job
|
||||
return complete
|
||||
|
||||
def recover_mapping(self, invocation_step, progress):
|
||||
outputs = {}
|
||||
for job_output in job_0.output_datasets:
|
||||
replacement_name = job_output.name
|
||||
replacement_value = job_output.dataset
|
||||
# If was a mapping step, grab the output mapped collection for
|
||||
# replacement instead.
|
||||
if replacement_value.hidden_beneath_collection_instance:
|
||||
replacement_value = replacement_value.hidden_beneath_collection_instance
|
||||
outputs[replacement_name] = replacement_value
|
||||
for job_output_collection in job_0.output_dataset_collection_instances:
|
||||
replacement_name = job_output_collection.name
|
||||
replacement_value = job_output_collection.dataset_collection_instance
|
||||
outputs[replacement_name] = replacement_value
|
||||
|
||||
progress.set_step_outputs(step, outputs)
|
||||
for output_dataset_assoc in invocation_step.output_datasets:
|
||||
outputs[output_dataset_assoc.output_name] = output_dataset_assoc.dataset
|
||||
|
||||
for output_dataset_collection_assoc in invocation_step.output_dataset_collections:
|
||||
outputs[output_dataset_collection_assoc.output_name] = output_dataset_collection_assoc.dataset_collection
|
||||
|
||||
progress.set_step_outputs(invocation_step, outputs)
|
||||
|
||||
def _find_collections_to_match(self, tool, progress, step):
|
||||
collections_to_match = matching.CollectionsToMatch()
|
||||
|
||||
+85
-31
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from galaxy import model, util
|
||||
from galaxy import model
|
||||
from galaxy.util import ExecutionTimer
|
||||
from galaxy.util.odict import odict
|
||||
from galaxy.workflow import modules
|
||||
@@ -140,12 +140,18 @@ class WorkflowInvoker(object):
|
||||
|
||||
module_injector = modules.WorkflowModuleInjector(trans)
|
||||
if progress is None:
|
||||
progress = WorkflowProgress(self.workflow_invocation, workflow_run_config.inputs, module_injector)
|
||||
progress = WorkflowProgress(
|
||||
self.workflow_invocation,
|
||||
workflow_run_config.inputs,
|
||||
module_injector,
|
||||
jobs_per_scheduling_iteration=getattr(trans.app.config, "maximum_workflow_jobs_per_scheduling_iteration", -1),
|
||||
)
|
||||
self.progress = progress
|
||||
|
||||
def invoke(self):
|
||||
workflow_invocation = self.workflow_invocation
|
||||
maximum_duration = getattr(self.trans.app.config, "maximum_workflow_invocation_duration", -1)
|
||||
config = self.trans.app.config
|
||||
maximum_duration = getattr(config, "maximum_workflow_invocation_duration", -1)
|
||||
if maximum_duration > 0 and workflow_invocation.seconds_since_created > maximum_duration:
|
||||
log.debug("Workflow invocation [%s] exceeded maximum number of seconds allowed for scheduling [%s], failing." % (workflow_invocation.id, maximum_duration))
|
||||
workflow_invocation.state = model.WorkflowInvocation.states.FAILED
|
||||
@@ -162,24 +168,27 @@ class WorkflowInvoker(object):
|
||||
|
||||
remaining_steps = self.progress.remaining_steps()
|
||||
delayed_steps = False
|
||||
for step in remaining_steps:
|
||||
for (step, workflow_invocation_step) in remaining_steps:
|
||||
step_delayed = False
|
||||
step_timer = ExecutionTimer()
|
||||
jobs = None
|
||||
try:
|
||||
self.__check_implicitly_dependent_steps(step)
|
||||
|
||||
# TODO: step may fail to invoke, do something about that.
|
||||
jobs = self._invoke_step(step)
|
||||
for job in (util.listify(jobs) or [None]):
|
||||
# Record invocation
|
||||
if not workflow_invocation_step:
|
||||
workflow_invocation_step = model.WorkflowInvocationStep()
|
||||
workflow_invocation_step.workflow_invocation = workflow_invocation
|
||||
workflow_invocation_step.workflow_step = step
|
||||
# Job may not be generated in this thread if bursting is enabled
|
||||
# https://github.com/galaxyproject/galaxy/issues/2259
|
||||
if job:
|
||||
workflow_invocation_step.job_id = job.id
|
||||
workflow_invocation_step.state = 'new'
|
||||
|
||||
workflow_invocation.steps.append(workflow_invocation_step)
|
||||
|
||||
incomplete_or_none = self._invoke_step(workflow_invocation_step)
|
||||
if incomplete_or_none is False:
|
||||
step_delayed = delayed_steps = True
|
||||
workflow_invocation_step.state = 'ready'
|
||||
self.progress.mark_step_outputs_delayed(step, why="Not all jobs scheduled for state.")
|
||||
else:
|
||||
workflow_invocation_step.state = 'scheduled'
|
||||
except modules.DelayedWorkflowEvaluation as de:
|
||||
step_delayed = delayed_steps = True
|
||||
self.progress.mark_step_outputs_delayed(step, why=de.why)
|
||||
@@ -218,15 +227,19 @@ class WorkflowInvoker(object):
|
||||
self.__check_implicitly_dependent_step(output_id)
|
||||
|
||||
def __check_implicitly_dependent_step(self, output_id):
|
||||
step_invocations = self.workflow_invocation.step_invocations_for_step_id(output_id)
|
||||
step_invocation = self.workflow_invocation.step_invocation_for_step_id(output_id)
|
||||
|
||||
# No steps created yet - have to delay evaluation.
|
||||
if not step_invocations:
|
||||
if not step_invocation:
|
||||
delayed_why = "depends on step [%s] but that step has not been invoked yet" % output_id
|
||||
raise modules.DelayedWorkflowEvaluation(why=delayed_why)
|
||||
|
||||
for step_invocation in step_invocations:
|
||||
job = step_invocation.job
|
||||
if step_invocation.state != 'scheduled':
|
||||
delayed_why = "depends on step [%s] job has not finished scheduling yet" % output_id
|
||||
raise modules.DelayedWorkflowEvaluation(delayed_why)
|
||||
|
||||
for job_assoc in step_invocation.jobs:
|
||||
job = job_assoc.job
|
||||
if job:
|
||||
# At least one job in incomplete.
|
||||
if not job.finished:
|
||||
@@ -241,9 +254,9 @@ class WorkflowInvoker(object):
|
||||
# pause steps.
|
||||
pass
|
||||
|
||||
def _invoke_step(self, step):
|
||||
jobs = step.module.execute(self.trans, self.progress, self.workflow_invocation, step)
|
||||
return jobs
|
||||
def _invoke_step(self, invocation_step):
|
||||
incomplete_or_none = invocation_step.workflow_step.module.execute(self.trans, self.progress, invocation_step)
|
||||
return incomplete_or_none
|
||||
|
||||
|
||||
STEP_OUTPUT_DELAYED = object()
|
||||
@@ -251,16 +264,31 @@ STEP_OUTPUT_DELAYED = object()
|
||||
|
||||
class WorkflowProgress(object):
|
||||
|
||||
def __init__(self, workflow_invocation, inputs_by_step_id, module_injector):
|
||||
def __init__(self, workflow_invocation, inputs_by_step_id, module_injector, jobs_per_scheduling_iteration=-1):
|
||||
self.outputs = odict()
|
||||
self.module_injector = module_injector
|
||||
self.workflow_invocation = workflow_invocation
|
||||
self.inputs_by_step_id = inputs_by_step_id
|
||||
self.jobs_per_scheduling_iteration = jobs_per_scheduling_iteration
|
||||
self.jobs_scheduled_this_iteration = 0
|
||||
|
||||
@property
|
||||
def maximum_jobs_to_schedule_or_none(self):
|
||||
if self.jobs_per_scheduling_iteration > 0:
|
||||
return self.jobs_per_scheduling_iteration - self.jobs_scheduled_this_iteration
|
||||
else:
|
||||
return None
|
||||
|
||||
def record_executed_job_count(self, job_count):
|
||||
self.jobs_scheduled_this_iteration += job_count
|
||||
|
||||
def remaining_steps(self):
|
||||
# Previously computed and persisted step states.
|
||||
step_states = self.workflow_invocation.step_states_by_step_id()
|
||||
steps = self.workflow_invocation.workflow.steps
|
||||
|
||||
# TODO: Wouldn't a generator be much better here so we don't have to reason about
|
||||
# steps we are no where near ready to schedule?
|
||||
remaining_steps = []
|
||||
step_invocations_by_id = self.workflow_invocation.step_invocations_by_step_id()
|
||||
for step in steps:
|
||||
@@ -274,11 +302,11 @@ class WorkflowProgress(object):
|
||||
runtime_state = step_states[step_id].value
|
||||
step.state = step.module.decode_runtime_state(runtime_state)
|
||||
|
||||
invocation_steps = step_invocations_by_id.get(step_id, None)
|
||||
if invocation_steps:
|
||||
self._recover_mapping(step, invocation_steps)
|
||||
invocation_step = step_invocations_by_id.get(step_id, None)
|
||||
if invocation_step and invocation_step.state == 'scheduled':
|
||||
self._recover_mapping(invocation_step)
|
||||
else:
|
||||
remaining_steps.append(step)
|
||||
remaining_steps.append((step, invocation_step))
|
||||
return remaining_steps
|
||||
|
||||
def replacement_for_tool_input(self, step, input, prefixed_name):
|
||||
@@ -340,7 +368,9 @@ class WorkflowProgress(object):
|
||||
output_name = workflow_output.output_name
|
||||
return self.outputs[step.id][output_name]
|
||||
|
||||
def set_outputs_for_input(self, step, outputs=None):
|
||||
def set_outputs_for_input(self, invocation_step, outputs=None):
|
||||
step = invocation_step.workflow_step
|
||||
|
||||
if outputs is None:
|
||||
outputs = {}
|
||||
|
||||
@@ -352,10 +382,34 @@ class WorkflowProgress(object):
|
||||
raise ValueError(message)
|
||||
outputs['output'] = self.inputs_by_step_id[step_id]
|
||||
|
||||
self.set_step_outputs(step, outputs)
|
||||
self.set_step_outputs(invocation_step, outputs)
|
||||
|
||||
def set_step_outputs(self, step, outputs):
|
||||
def set_step_outputs(self, invocation_step, outputs, already_persisted=False):
|
||||
step = invocation_step.workflow_step
|
||||
self.outputs[step.id] = outputs
|
||||
if not already_persisted:
|
||||
for output_name, output_object in outputs.items():
|
||||
if hasattr(output_object, "history_content_type"):
|
||||
invocation_step.add_output(output_name, output_object)
|
||||
else:
|
||||
# This is a problem, this non-data, non-collection output
|
||||
# won't be recovered on a subsequent workflow scheduling
|
||||
# iteration. This seems to have been a pre-existing problem
|
||||
# prior to #4584 though.
|
||||
pass
|
||||
for workflow_output in step.workflow_outputs:
|
||||
output_name = workflow_output.output_name
|
||||
if output_name not in outputs:
|
||||
raise KeyError("Failed to find [%s] in step outputs [%s]" % (output_name, outputs))
|
||||
output = outputs[output_name]
|
||||
self._record_workflow_output(
|
||||
step,
|
||||
workflow_output,
|
||||
output=output,
|
||||
)
|
||||
|
||||
def _record_workflow_output(self, step, workflow_output, output):
|
||||
self.workflow_invocation.add_output(workflow_output, step, output)
|
||||
|
||||
def mark_step_outputs_delayed(self, step, why=None):
|
||||
if why:
|
||||
@@ -414,11 +468,11 @@ class WorkflowProgress(object):
|
||||
self.module_injector,
|
||||
)
|
||||
|
||||
def _recover_mapping(self, step, step_invocations):
|
||||
def _recover_mapping(self, step_invocation):
|
||||
try:
|
||||
step.module.recover_mapping(step, step_invocations, self)
|
||||
step_invocation.workflow_step.module.recover_mapping(step_invocation, self)
|
||||
except modules.DelayedWorkflowEvaluation as de:
|
||||
self.mark_step_outputs_delayed(step, de.why)
|
||||
self.mark_step_outputs_delayed(step_invocation.workflow_step, de.why)
|
||||
|
||||
|
||||
__all__ = ('invoke', 'WorkflowRunConfig')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user