');
return false;
}
// stash the search-text used to generate these results
showingResultsForStudyLookupText = searchText;
- $('#study-lookup-results').html('
');
+ $lookupResults.dropdown('toggle');
};
return false;
}
function resetStudyLookup() {
// Clear/disable tree lookup
- var $treeSelector = $('select[name=tree-lookup]');
+ var context = getPhylesystemLookupContext();
+ // what's the parent element for study+tree lookup UI?
+ var $container = getPhylesystemLookupPanel( context );
+ var $treeSelector = $container.find('select[name=tree-lookup]');
$treeSelector.find('option').remove();
- var $promptOption = $('');
+ var $promptOption = $('');
$treeSelector.append( $promptOption );
- $('select[name=tree-lookup]').val('');
- $('select[name=tree-lookup]').attr('disabled','disabled');
+ $container.find('select[name=tree-lookup]').val('');
+ $container.find('select[name=tree-lookup]').attr('disabled','disabled');
// Toggle the study-lookup widget (vs. indicator)
- $('.study-lookup-passive').hide();
- $('.study-lookup-active').show();
+ $container.find('.study-lookup-passive').hide();
+ $container.find('.study-lookup-active').show();
// N.B. The icon element will shift if its display is set to block
- $('i.study-lookup-active').css('display', 'inline-block');
+ $container.find('i.study-lookup-active').css('display', 'inline-block');
- updateNewCollTreeUI();
+ updateTreeLookupUI();
}
function createNewTreeCollection() {
@@ -2001,7 +2098,7 @@ function bindStudyAndTreeLookups() {
$newTreeStartButton.attr('disabled', null)
.removeClass('btn-info-disabled');
}
-function loadStudyListForLookup() {
+function loadStudyListForLookup( context ) {
///console.warn('STARTING loadStudyListForLookup');
// if list is available, bind UI and return
if (studyListForLookup) {
@@ -2009,10 +2106,17 @@ function loadStudyListForLookup() {
return true;
}
- // disable "Add tree" button until the list is loaded
- var $newTreeStartButton = $('#new-collection-tree-start');
- $newTreeStartButton.attr('disabled', 'disabled')
- .addClass('btn-info-disabled');
+ // find the correct UI components for the current context
+ if (!context) {
+ context = getPhylesystemLookupContext();
+ }
+ // what's the parent element for study+tree lookup UI?
+ var $container = getPhylesystemLookupPanel( context );
+
+ // disable lookup form until the list is loaded
+ var $formInitButtons = $container.parent().find('.form-init');
+ $formInitButtons.attr('disabled', 'disabled')
+ .addClass('btn-info-disabled');
$.ajax({
type: 'POST',
@@ -2020,8 +2124,7 @@ function loadStudyListForLookup() {
url: findAllStudies_url,
data: { verbose: true },
success: function( data, textStatus, jqXHR ) {
- // this should be properly parsed JSON
-
+ // this should be properly parsed JSON;
// report errors or malformed data, if any
if (textStatus !== 'success') {
showErrorMessage('Sorry, there was an error loading the list of studies.');
@@ -2031,10 +2134,11 @@ function loadStudyListForLookup() {
showErrorMessage('Sorry, there is a problem with the study-list data.');
return;
}
-
+ // save global lookup list!
studyListForLookup = data['matched_studies'];
bindStudyAndTreeLookups();
- if (collectionUI === 'FULL_PAGE') {
+ if (context === 'COLLECTION_EDITOR_ADD_TREE' &&
+ collectionUI === 'FULL_PAGE') {
// refresh tree list in collections editor
nudgeTickler('TREES', {modelHasChanged: false});
}
@@ -2881,3 +2985,927 @@ async function confirmHyperlink( link, message ) {
}
}
+// Keep a safe copy of our UI markup, for re-use as a Knockout template (see below)
+var $stashedSynthRunPopup = null;
+var synthRunSpec;
+function showSynthesisRunPopup(options) {
+ /* Prompt for settings and request a new attempt at custom synthesis.
+ * This might initiate a new run, or it might be redirected to an existing
+ * run with identical settings and versions.
+ *
+ * NB: This can be triggered from different contexts:
+ * - from the main synthesis queue
+ * - from a single collection (editor)
+ *
+ * Options can include collection IDs to start the list, if we're doing this
+ * from a particular context like the collection-curation tool.
+ */
+ options = options || {};
+ synthRunSpec = {
+ 'runner': ko.observable({
+ 'login': userLogin,
+ 'displayName': userDisplayName,
+ 'email': ko.observable(userEmail)
+ }),
+ // use any initial values provided above
+ 'description': ko.observable(options.DESCRIPTION || ''),
+ 'rootTaxonID': ko.observable(options.ROOT_TAXON_OTTID || ''),
+ 'rootTaxonName': ko.observable(options.ROOT_TAXON_NAME || ''),
+ 'collections': ko.observableArray(options.COLLECTIONS || [ ])
+ };
+ // any change should prompt QUIET validation
+ synthRunSpec.runner.subscribe(validateSynthRunSpec);
+ synthRunSpec.rootTaxonID.subscribe(validateSynthRunSpec);
+ synthRunSpec.collections.subscribe(validateSynthRunSpec);
+ // TODO: add more subscriptions here?
+
+ // add any missing/empty 'rank' properties
+ ensureSynthRunRanking(synthRunSpec);
+
+ // show a shared popup (from shared page template)
+ var $synthRunPopup = $('#define-synth-run-popup');
+ // stash the pristine markup before binding our UI for the first time
+ if ($stashedSynthRunPopup === null) {
+ $stashedSynthRunPopup = $synthRunPopup.clone();
+ } else {
+ // Replace with pristine markup to avoid weird results when loading a new nameset
+ $synthRunPopup.contents().replaceWith(
+ $stashedSynthRunPopup.clone().contents()
+ );
+ }
+ var popup = $synthRunPopup[0];
+ ko.cleanNode(popup);
+ ko.applyBindings(synthRunSpec, popup);
+ $('#define-synth-run-popup').off('hidden').on('hidden', function () {
+ // clear any proposed spec for next time
+ ko.cleanNode(popup);
+ synthRunSpec = null;
+ });
+
+ // enable taxon search
+ $('input[name=taxon-search]').unbind('keyup change').bind('keyup change', setTaxaSearchFuse );
+ $('select[name=taxon-search-context]').unbind('change').bind('change', searchForMatchingTaxa );
+
+ // don't trigger unrelated form submission when pressing ENTER here
+ $('input[name=taxon-search], select[name=taxon-search-context]')
+ .unbind('keydown')
+ .bind('keydown', function(e) { return e.which !== 13; });
+
+ // enable collection search (in synth-run details popup)
+ console.warn('BINDING COLLECTION SEARCH (SYNTH)');
+ $('input[name=collection-search]').unbind('keyup change')
+ .bind('keyup change', setCollectionSearchFuse )
+ .unbind('keydown') // block errant form submission
+ .bind('keydown', function(e) { return e.which !== 13; });
+ $('#add-collection-search-form').unbind('submit').submit(function() {
+ searchForMatchingCollections( {CONTEXT: 'ADD_COLLECTION_TO_SYNTHESIS_RUN'} );
+ return false;
+ });
+ resetExistingCollectionPrompt( {CONTEXT: 'ADD_COLLECTION_TO_SYNTHESIS_RUN'} );
+
+ $('#define-synth-run-popup').modal('show');
+ // TODO validate initial settings and warn if needed
+ // TODO Upon submission, show response from synth-API server (run started, or redirected, or ???)
+}
+function createSynthesisRunRequest( synthRunInfo ) {
+ /* Actual initiation of synth run, the result of hitting Submit in the
+ * popup above (or any equivalent action). This should bundle up a
+ * submission along with personala identity and credentials, then report
+ * any immediate error or result codes.
+ *
+ * If the submission was accepted, we should clear the synth-queue cache
+ * (if any) and prompt for a fresh listing, then perhaps highlight the
+ * newly-submitted item in the list..?
+ */
+ showInfoMessage("Now I'd submit those detailed settings...");
+}
+
+/* Sensible autocomplete behavior requires the use of timeouts
+ * and sanity checks for unchanged content, etc.
+ */
+clearTimeout(searchTimeoutID); // in case there's a lingering search from last page!
+var searchTimeoutID = null;
+var searchDelay = 1000; // milliseconds
+var hopefulSearchName = null;
+function setTaxaSearchFuse(e) {
+ if (searchTimeoutID) {
+ // kill any pending search, apparently we're still typing
+ clearTimeout(searchTimeoutID);
+ }
+ // reset the timeout for another n milliseconds
+ searchTimeoutID = setTimeout(searchForMatchingTaxa, searchDelay);
+
+ /* If the last key pressed was the ENTER key, stash the current (trimmed)
+ * string and auto-jump if it's a valid taxon name.
+ */
+ if (e.type === 'keyup') {
+ switch (e.which) {
+ case 13:
+ hopefulSearchName = $('input[name=taxon-search]').val().trim();
+ autoApplyExactMatch(); // use existing menu, if found
+ break;
+ case 17:
+ // do nothing (probably a second ENTER key)
+ break;
+ case 39:
+ case 40:
+ // down or right arrows should try to select first result
+ $('#search-results a:eq(0)').focus();
+ break;
+ default:
+ hopefulSearchName = null;
+ }
+ } else {
+ hopefulSearchName = null;
+ }
+}
+
+var showingResultsForSearchText = '';
+var showingResultsForSearchContextName = '';
+function searchForMatchingTaxa() {
+ // clear any pending search timeout and ID
+ clearTimeout(searchTimeoutID);
+ searchTimeoutID = null;
+
+ var $input = $('input[name=taxon-search]');
+ var searchText = $input.val().trimLeft();
+
+ if (searchText.length === 0) {
+ $('#search-results').html('');
+ return false;
+ } else if (searchText.length < 2) {
+ $('#search-results').html('
');
+ $('#search-results').dropdown('toggle');
+ return false;
+ }
+
+ // groom trimmed text based on our search rules
+ var searchContextName = $('select[name=taxon-search-context]').val();
+
+ // is this unchanged from last time? no need to search again..
+ if ((searchText == showingResultsForSearchText) && (searchContextName == showingResultsForSearchContextName)) {
+ ///console.log("Search text and context UNCHANGED!");
+ return false;
+ }
+
+ // stash these to use for later comparison (to avoid redundant searches)
+ var queryText = searchText; // trimmed above
+ var queryContextName = searchContextName;
+ $('#search-results').html('
');
+ $('#search-results').show();
+ $('#search-results').dropdown('toggle');
+
+ $.ajax({
+ global: false, // suppress web2py's aggressive error handling
+ url: doTNRSForAutocomplete_url, // NOTE that actual server-side method name might be quite different!
+ type: 'POST',
+ dataType: 'json',
+ data: JSON.stringify({
+ "name": searchText,
+ "context_name": searchContextName,
+ "include_suppressed": false
+ }), // data (asterisk required for completion suggestions)
+ crossDomain: true,
+ contentType: "application/json; charset=utf-8",
+ success: function(data) { // JSONP callback
+ // stash the search-text used to generate these results
+ showingResultsForSearchText = queryText;
+ showingResultsForSearchContextName = queryContextName;
+
+ $('#search-results').html('');
+ var maxResults = 100;
+ var visibleResults = 0;
+ /*
+ * The returned JSON 'data' is a simple list of objects. Each object is a matching taxon (or name?)
+ * with these properties:
+ * ott_id // taxon ID in OTT taxonomic tree
+ * unique_name // the taxon name, or unique name if it has one
+ * is_higher // points to a genus or higher taxon? T/F
+ */
+ if (data && data.length && data.length > 0) {
+ // sort results to show exact match(es) first, then higher taxa, then others
+ // initial sort on higher taxa (will be overridden by exact matches)
+ // N.B. As of the v3 APIs, an exact match will be returned as the only result.
+ data.sort(function(a,b) {
+ if (a.is_higher === b.is_higher) return 0;
+ if (a.is_higher) return -1;
+ if (b.is_higher) return 1;
+ });
+
+ // show all sorted results, up to our preset maximum
+ var matchingNodeIDs = [ ]; // ignore any duplicate results (point to the same taxon)
+ for (var mpos = 0; mpos < data.length; mpos++) {
+ if (visibleResults >= maxResults) {
+ break;
+ }
+ var match = data[mpos];
+ var matchingName = match.unique_name;
+ var matchingID = match.ott_id;
+ if ($.inArray(matchingID, matchingNodeIDs) === -1) {
+ // we're not showing this yet; add it now
+ $('#search-results').append(
+ '
');
+ $collectionResults.dropdown('toggle');
+ return false;
+ }
+
+ // is this unchanged from last time? no need to search again..
+ if (searchText == showingResultsForCollectionSearchText) {
+ ///console.log("Search text and context UNCHANGED!");
+ return false;
+ }
+
+ // search local viewModel.allCollections for any matches
+ var searchNotAvailable = (!viewModel.allCollections || viewModel.allCollections.length === 0);
+ var statusMsg;
+ if (searchNotAvailable) {
+ // block search (no collection data in the view model)
+ statusMsg = 'Unable to search (no collections found)';
+ } else {
+ // stash our search text to use for later comparison (to avoid redundant searches)
+ showingResultsForCollectionSearchText = searchText; // trimmed above
+ statusMsg = 'Search in progress...';
+ }
+
+ $collectionResults.html('
');
+ $collectionResults.dropdown('toggle');
+ }
+
+ return false;
+}
+
+function validateSynthRunSpec( options ) {
+ // Ignore the specific value, validate the entire spec
+ var verbose = (options && options.VERBOSE) || false;
+ // do some basic sanity checks on the requested synthesis run
+ if (!synthRunSpec) {
+ console.error("EXPECTED to find synthRunSpec in this page!");
+ return false;
+ }
+ // it should have at least one collection
+ if (synthRunSpec.collections().length < 1) {
+ if (verbose) showErrorMessage('Synthesis requires at least one input tree collection');
+ return false;
+ }
+ // it should have a specified taxonomic root (id and name)
+ if ($.trim(synthRunSpec.rootTaxonID()) === '') {
+ if (verbose) showErrorMessage('Synthesis requires a specified root taxon');
+ return false;
+ }
+ // TODO: block submissions by anonymous users?
+ return true;
+}
+function requestNewSynthRun() {
+ /* Try sending this to the server, and respond with a ticket (if successful),
+ * a message if REDIRECTed (due to an prior/identical synth run), or an error message.
+ *
+ * NB - Our payload is constructed using `synthRunSpec`, a persistent, page-level singleton
+ */
+ if (!validateSynthRunSpec({VERBOSE:true})) {
+ return false;
+ }
+ showModalScreen( "Requesting synthesis run...", {SHOW_BUSY_BAR:true});
+ var flattenedCollections = $.map(
+ ko.unwrap(synthRunSpec.collections),
+ function( collectionInfo, i ) {
+ // ignore collection rank and status, keep just the IDs
+ return collectionInfo.id;
+ }
+ );
+ var payload = {
+ 'input_collection': flattenedCollections,
+ 'root_id': ko.unwrap(synthRunSpec.rootTaxonID),
+ // more good stuff we should use in the API
+ 'description': ko.unwrap(synthRunSpec.description),
+ 'root_name': ko.unwrap(synthRunSpec.rootTaxonID),
+ 'runner': ko.unwrap(synthRunSpec.runner)
+ };
+
+ $.ajax({
+ global: false, // suppress web2py's aggressive error handling
+ type: 'POST',
+ dataType: 'json',
+ // crossdomain: true,
+ contentType: "application/json; charset=utf-8",
+ url: requestNewSynthesisRun_url,
+ data: payload,
+ processData: false,
+ complete: function( jqXHR, textStatus ) {
+ // report errors or malformed data, if any
+ if (textStatus !== 'success') {
+ if (jqXHR.status >= 500) {
+ // major server-side error, just show raw response for tech support
+ var errMsg = 'Sorry, there was an error requesting ths run.. Show details
'+ jqXHR.responseText +' [auto-parsed]
';
+ hideModalScreen();
+ showErrorMessage(errMsg);
+ if (typeof(errorCallback) === 'function') errorCallback();
+ return;
+ }
+ // Server blocked the operation due to validation errors(?)
+ var data = $.parseJSON(jqXHR.responseText);
+ // TODO: this should be properly parsed JSON, show it more sensibly
+ // (but for now, repeat the crude feedback used above)
+ var errMsg = 'Sorry, there was an error requesting this run. Show details
'+ jqXHR.responseText +' [parsed in JS]
';
+ hideModalScreen();
+ showErrorMessage(errMsg);
+ if (typeof(errorCallback) === 'function') errorCallback();
+ return;
+ }
+ /* if we're still here, use the success callback provided
+ * NB - We should expect a "run status" object as described here:
+ * https://github.com/OpenTreeOfLife/ws_wrapper/blob/synth-on-demand/synth-on-demand.md#response-1
+ */
+ var responseObj = $.parseJSON(jqXHR.responseText);
+ debugger;
+/*
+ if ($.isArray(responseObj['matched_studies'])) {
+ var matchingStudyIDs = [];
+ $.each(responseObj['matched_studies'], function(i,obj) {
+ matchingStudyIDs.push( obj['ot:studyId'] );
+ });
+ } else {
+ var errMsg = 'Sorry, there was an error checking for duplicate studies. Show details
Missing or malformed "matching_studies" in JSON response:\n\n'+
+ jqXHR.responseText+'
';
+ hideModalScreen();
+ showErrorMessage(errMsg);
+ return;
+ }
+*/
+ }
+ });
+}
+
+/* Adapt tree-ordering features (from tree collection editor) for ordering the
+ * collections in a proposed synth-run
+ */
+function indexOfObservable( array, item ) {
+ // unwrap array items to find the target
+ var foundPosition = -1;
+ $.each(array, function(i, wrappedItem) {
+ if (ko.unwrap(wrappedItem) === item) {
+ foundPosition = i;
+ return false; // skip the rest
+ }
+ });
+ return foundPosition;
+}
+function moveInSynthesisRun( collection, synthRun, newPosition ) {
+ // Move this collection to an explicit position in the list
+ // N.B. We use zero-based counting here!
+ var collectionListObservable = synthRun.collections,
+ collectionList = collectionListObservable();
+ var oldPosition = indexOfObservable( collectionList, collection );
+ if (oldPosition === -1) {
+ // this should *never* happen
+ console.warn('No such collection in this synthesis run!');
+ return false;
+ }
+
+ // Find the new position using simple "stepper" widgets or an
+ // explicit/stated rank.
+ switch(newPosition) {
+ case 'UP':
+ newPosition = Math.max(0, oldPosition - 1);
+ break;
+
+ case 'DOWN':
+ newPosition = Math.min(collectionList.length, oldPosition + 1);
+ break;
+
+ default:
+ // stated rank should be an integer or int-as-string
+ if (isNaN(Number(collection['rank'])) || ($.trim(collection['rank']) == '')) {
+ // don't move if it's not a valid rank!
+ console.log(">> INVALID rank: "+ collection['rank'] +" <"+ typeof(collection['rank']) +">");
+ return false;
+ }
+ var movingRank = Number(collection['rank']);
+ // displace the first collection that has the same or higher stated rank
+ var movingCollection = collection;
+ var sameRankOrHigher = $.grep(collectionList, function(testCollection, i) {
+ testCollection = ko.unwrap(testCollection);
+ if (testCollection === movingCollection) {
+ return false; // skip the moving collection!
+ }
+ // Does its stated 'rank' match its list position? N.B. that we're not
+ // looking for an exact match, just relative value vs. its neighbors
+ // in the collection list.
+ var statedRank = Number(testCollection['rank']);
+ if (isNaN(statedRank) || ($.trim(testCollection['rank']) == '')) {
+ // treat invalid/missing values as zero, I guess
+ statedRank = 0;
+ }
+ if (statedRank >= movingRank) {
+ return true;
+ }
+ return false;
+ });
+ var nextCollection;
+ if (sameRankOrHigher.length === 0) {
+ // looks like we're moving to the end of the list
+ newPosition = collectionList.length - 1;
+ } else {
+ // displace the first matching collection
+ nextCollection = sameRankOrHigher[0];
+ nextCollection = ko.unwrap(nextCollection);
+ newPosition = indexOfObservable( collectionList, nextCollection );
+ }
+ break;
+ }
+
+ // just grab the moving item and move (or append) it
+ var grabbedItem = collectionListObservable.splice( oldPosition, 1 )[0];
+ collectionListObservable.splice(newPosition, 0, grabbedItem);
+
+ resetSynthRunRanking( synthRun );
+}
+
+function showSynthRunMoveUI( collection, itsElement, synthRun ) {
+ // show/add? a simple panel with Move, Move All, and Cancel buttons
+
+ // build the panel if it's not already hidden in the DOM
+ var $synthRunMoveUI = $('#synthrun-move-ui');
+ if ($synthRunMoveUI.length === 0) {
+ $synthRunMoveUI = $(
+ '
'
+ +''
+ +''
+ +''
+ +'
'
+ );
+ }
+
+ // check for integer value, and alert if not valid!
+ if (isNaN(Number(collection['rank'])) || ($.trim(collection['rank']) == '')) {
+ $(itsElement).css('color','#f33');
+ $synthRunMoveUI.hide();
+ return false;
+ } else {
+ $(itsElement).css('color', '');
+ }
+
+ // (re)bind buttons to this collection
+ $synthRunMoveUI.find('button:contains(Move)')
+ .unbind('click').click(function() {
+ var newPosition = (Number(collection.rank) - 1) || 0;
+ moveInSynthesisRun( collection, synthRun, newPosition );
+ resetSynthRunRanking( synthRun );
+ $('#synthrun-move-ui').hide();
+ return false;
+ });
+ $synthRunMoveUI.find('button:contains(Move All)')
+ .unbind('click').click(function() {
+ // sort all collections by rank-as-number, in ascending order
+ var collectionListObservable = synthRun.collections,
+ collectionList = collectionListObservable();
+ collectionListObservable.sort(function(a,b) {
+ // N.B. This works even if there's no such property.
+ a = ko.unwrap(a);
+ b = ko.unwrap(b);
+ var aStatedRank = Number(a['rank']);
+ var bStatedRank = Number(b['rank']);
+ // if either field has an invalid rank value, freeze this pair
+ if (isNaN(aStatedRank) || ($.trim(a['rank']) == '')
+ || isNaN(bStatedRank) || ($.trim(b['rank']) == '')) {
+ return 0;
+ }
+ if (aStatedRank === bStatedRank) {
+ return 0;
+ }
+ // sort these from low to high
+ return (aStatedRank > bStatedRank) ? 1 : -1;
+ });
+ resetSynthRunRanking( synthRun );
+ $('#synthrun-move-ui').hide();
+ return false;
+ });
+ $synthRunMoveUI.find('button:contains(Cancel)')
+ .unbind('click').click(function() {
+ resetSynthRunRanking( synthRun );
+ $('#synthrun-move-ui').hide();
+ return false;
+ });
+
+ // en/disable widgets in the move UI, based on how many pending moves
+ var highestRankSoFar = -1;
+ var collectionsOutOfPlace = $.grep(synthRun.collections(), function(collection, i) {
+ // Does its stated 'rank' match its list position? N.B. that we're not
+ // looking for an exact match, just relative value vs. its neighbors
+ // in the collection list.
+ if (isNaN(Number(collection['rank'])) || ($.trim(collection['rank']) == '')) {
+ // weird values should prompt us to move+refresh
+ return true;
+ }
+ var statedRank = Number(collection['rank']);
+ if (statedRank < highestRankSoFar) {
+ return true;
+ }
+ highestRankSoFar = statedRank;
+ return false;
+ });
+ switch(collectionsOutOfPlace.length) {
+ case 0:
+ // don't show the UI, nothing to move!
+ $('#synthrun-move-ui').hide();
+ return false;
+ case 1:
+ $synthRunMoveUI.find('button:contains(Move)')
+ .attr('disabled', null);
+ $synthRunMoveUI.find('button:contains(Move All)')
+ .attr('disabled', 'disabled');
+ break;
+ default:
+ $synthRunMoveUI.find('button:contains(Move)')
+ .attr('disabled', null);
+ $synthRunMoveUI.find('button:contains(Move All)')
+ .attr('disabled', null);
+ }
+
+ // float this panel alongside the specified collection, IF it's not already there
+ if ($(itsElement).nextAll('#synthrun-move-ui:visible').length === 0) {
+ $synthRunMoveUI.insertAfter(itsElement);
+ }
+ $synthRunMoveUI.css('display','inline-block');
+}
+
+function ensureSynthRunRanking( synthRun ) {
+ // add a 'rank' property to any collection that doesn't have one; if any are
+ // missing, reset ALL values based on their "natural" order in the array
+ var missingRankProperties = false;
+ // check for any missing properties (if so, reset all)
+ $.each(synthRun.collections(), function(i, collection) {
+ collection = ko.unwrap( observable );
+ if (!('rank' in collection)) {
+ collection['rank'] = null;
+ missingRankProperties = true;
+ observable.notifySubscribers();
+ }
+ });
+ if (missingRankProperties) {
+ resetSynthRunRanking( synthRun );
+ }
+}
+function resetSynthRunRanking( synthRun ) {
+ // update existing 'rank' property to each of its collections, using
+ // their "natural" order in the array
+ $.each(synthRun.collections(), function(i, observable) {
+ collection = ko.unwrap( observable );
+ collection.rank = (i+1);
+ observable.notifySubscribers();
+ });
+}
+function stripSynthRunRanking( synthRun ) {
+ // remove explicit 'rank' properties before saving a collection, since the
+ // JSON array already has the current order
+ var collectionList = synthRun.collections();
+ $.each(collectionList, function(i, collection) {
+ collection = ko.unwrap( collection );
+ delete collection['rank'];
+ });
+}
+function stripSynthRunStatusMarkers( synthRun ) {
+ // remove temporary 'status' properties before submitting a synthesis run,
+ // since these are only used to review after updating collections from phylesystem
+ var collectionList = synthRun.collections();
+ $.each(collectionList, function(i, collection) {
+ collection = ko.unwrap( collection );
+ delete collection['status'];
+ });
+}
+
+async function removeCollectionFromSynthRun(collection, synthRun) {
+ if (await asyncConfirm('Are you sure you want to remove this collection from synthesis?')) {
+ var collectionListObservable = synthRun.collections,
+ collectionList = collectionListObservable();
+ var oldPosition = indexOfObservable( collectionList, collection );
+ if (oldPosition === -1) {
+ // this should *never* happen
+ console.warn('No such collection in this synthesis run!');
+ return false;
+ }
+ collectionListObservable.splice(oldPosition, 1);
+ resetSynthRunRanking( synthRun );
+ }
+}
diff --git a/curator/static/js/knockout-3.0.0.MOD.js b/curator/static/js/knockout-3.0.0.MOD.js
new file mode 100644
index 000000000..f1ccdc701
--- /dev/null
+++ b/curator/static/js/knockout-3.0.0.MOD.js
@@ -0,0 +1,4233 @@
+// Knockout JavaScript library v3.0.0
+// (c) Steven Sanderson - http://knockoutjs.com/
+// License: MIT (http://www.opensource.org/licenses/mit-license.php)
+
+(function(){
+var DEBUG=true;
+(function(undefined){
+ // (0, eval)('this') is a robust way of getting a reference to the global object
+ // For details, see http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023
+ var window = this || (0, eval)('this'),
+ document = window['document'],
+ navigator = window['navigator'],
+ jQuery = window["jQuery"],
+ JSON = window["JSON"];
+(function(factory) {
+ // Support three module loading scenarios
+ if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
+ // [1] CommonJS/Node.js
+ var target = module['exports'] || exports; // module.exports is for Node.js
+ factory(target);
+ } else if (typeof define === 'function' && define['amd']) {
+ // [2] AMD anonymous module
+ define(['exports'], factory);
+ } else {
+ // [3] No module loader (plain
+
+
+
+
+
+ {{if 'message' in globals():}}
+
{{=message}}
+ {{pass}}
+
+
+
+
+
+
+
+
+
+
+ The OpenTree projects builds
+ our synthetic tree of life.
+ by merging a list of tree collections over the "backbone" of the
+ latest OpenTree Taxonomy.
+ This page shows a list of all recorded synthesis runs in the
+ current system, the collections and settings used for each run.
+
+
+ Our synthesis tools are now available for you to use!
+ TODO: add lots more detail here.
+
diff --git a/curator/views/layout.html b/curator/views/layout.html
index 9e56e73ea..f26074489 100644
--- a/curator/views/layout.html
+++ b/curator/views/layout.html
@@ -99,6 +99,8 @@
/* N.B. These methods are needed to manage tree collections from any page. */
var findAllTreeCollections_url = "{{=findAllTreeCollections_url}}";
var findAllStudies_url = "{{=findAllStudies_url}}";
+ var findAllSynthesisRuns_url = "{{=findAllSynthesisRuns_url}}";
+ var requestNewSynthesisRun_url = "{{=requestNewSynthesisRun_url}}";
var singlePropertySearchForTrees_url = "{{=singlePropertySearchForTrees_url}}";
var API_create_collection_POST_url = "{{=API_create_collection_POST_url}}";
var API_load_collection_GET_url = "{{=API_load_collection_GET_url}}";
@@ -405,6 +407,7 @@
+ Describe the synthesis you'd like to see, and we'll run it on the
+ first available server. (If someone has already run this synthesis,
+ you will be redirected to the existing data.)
+
+ There are no collections here yet. Use the buttons above to add a collection.
+
+
+
+
+
+
{{ if ('maintenance_info' in locals()) and maintenance_info.get('maintenance_notice', None):
# N.b. Check to make sure it's in the view-dict, or error pages etc. will fail }}
-
+
+
+ This study contains
+ script-managed trees
+ that are too large to store in phylesystem.
+ These cannot be managed in the curation web app, nor can
+ this study's OTUs, but you can edit the study's metadata
+ and see its revision history.
+
+
+
+
+
No matching trees found! Add new trees or clear the
filter above to see trees in this study.
@@ -525,6 +539,7 @@
Trees in this study
+
@@ -1702,11 +1717,11 @@
Import pre-mapped names
-
-
- Analysis results will appear here...
-
+
+ Analysis results will appear here...
+
+
+
@@ -1941,7 +2011,7 @@
- Based on the reference text for this study,
+ Based on the reference text (or current DOI) for this study,
CrossRef.org
returned these matching records (best matches first).