diff --git a/LICENSE.txt b/LICENSE.txt index 7c5650a3f..51e069afd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ -Copyright (c) 2013, Jonathan Rees -Copyright (c) 2013, Mark Holder -Copyright (c) 2013, Jim Allman -Copyright (c) 2013, Stephen Smith +Copyright (c) 2013-2016, Jonathan Rees +Copyright (c) 2013-2016, Mark Holder +Copyright (c) 2013-2016, Jim Allman +Copyright (c) 2013-2016, Stephen Smith All rights reserved. diff --git a/curator/controllers/collection.py b/curator/controllers/collection.py index 5b28f8bfb..f62ccc22e 100644 --- a/curator/controllers/collection.py +++ b/curator/controllers/collection.py @@ -108,6 +108,17 @@ def load(): def store(): return dict(message="collection/store") +def synthesis_dashboard(): + """ + Allow any visitor to view (read-only!) a queue of recent custom-synthesis runs + """ + response.view = 'collection/synthesis_dashboard.html' + view_dict = get_opentree_services_method_urls(request) + view_dict['maintenance_info'] = get_maintenance_info(request) + view_dict['taxonSearchContextNames'] = fetch_current_TNRS_context_names(request) + view_dict['userCanEdit'] = auth.is_logged_in() and True or False + return view_dict + """ TODO: Adapt this for current collection status, based on new APIs """ def _get_latest_synthesis_details_for_collection_id( collection_id ): # Fetch the last SHA for this collection that was used in the latest @@ -137,15 +148,6 @@ def _get_latest_synthesis_details_for_collection_id( collection_id ): # Draft code is based on schema proposed in # https://github.com/OpenTreeOfLife/phylesystem-api/issues/228 - # fetch the full source list, then look for this study and its trees - commit_SHA_in_synthesis = None - # if key (collection ID, e.g. "opentreeoflife/default") matches, read its details - for c_id, collection_details in source_dict.items(): - if c_id == collection_id: - # this is the collection we're interested in! - commit_SHA_in_synthesis = collection_details['git_sha'] - return commit_SHA_in_synthesis # TODO: return more information? - # fetch the full source list, then look for this collection and its SHA # if key (collection ID, e.g. "opentreeoflife/default") matches, read its details for c_id, collection_details in source_dict.items(): @@ -155,4 +157,5 @@ def _get_latest_synthesis_details_for_collection_id( collection_id ): return None except Exception, e: # throw 403 or 500 or just leave it - raise HTTP(500, T('Unable to retrieve latest synthesis details for collection {u}'.format(u=collection))) + ##raise HTTP(500, T('Unable to retrieve latest synthesis details for collection {u}'.format(u=collection_id))) + raise HTTP(500, T('Unable to retrieve latest synthesis details for collection {u}:\n\n{e}'.format(u=collection_id, e=e))) diff --git a/curator/controllers/default.py b/curator/controllers/default.py index 1c8421ae1..6907362b6 100644 --- a/curator/controllers/default.py +++ b/curator/controllers/default.py @@ -470,6 +470,7 @@ def to_nexson(): _LOG = get_logger(request, 'to_nexson') if request.env.request_method == 'OPTIONS': raise HTTP(200, T('Preflight approved!')) + orig_args = {} is_upload = False # several of our NexSON use "uploadid" instead of "uploadId" so we should accept either @@ -622,7 +623,7 @@ def to_nexson(): try: assert(os.path.exists(exe_path)) except: - response.view = 'generic.json'; return {'hb':exe_path} + #response.view = 'generic.json'; return {'hb':exe_path} _LOG.warn("Could not find the 2nexml executable") raise HTTP(501, T("Server is misconfigured for 2nexml conversion")) invoc = [exe_path, '-f{f}'.format(f=inp_format), ] diff --git a/curator/private/config.example b/curator/private/config.example index 617579179..fc58b34d5 100644 --- a/curator/private/config.example +++ b/curator/private/config.example @@ -63,6 +63,9 @@ getContextForNames_url = {taxomachine_domain}/v3/tnrs/infer_context getSynthesisSourceList_url = {CACHED_treemachine_domain}/v3/tree_of_life/about getTaxonomicMRCAForNodes_url = {taxomachine_domain}/v3/taxonomy/mrca getDraftTreeMRCAForNodes_url = {treemachine_domain}/v3/tree_of_life/mrca +findAllSynthesisRuns_url = https://ot38.opentreeoflife.org/v3/tree_of_life/list_custom_built_trees +requestNewSynthesisRun_url = https://ot38.opentreeoflife.org/v3/tree_of_life/build_tree +# TODO: This should start {treemachine_domain} OR {CACHED_treemachine_domain} findAllStudies_url = {CACHED_oti_domain}/v3/studies/find_studies # TODO: Can we use CACHED_oti_domain for this? singlePropertySearchForStudies_url = {oti_domain}/v3/studies/find_studies diff --git a/curator/routes.py b/curator/routes.py new file mode 100644 index 000000000..8bfaeb14a --- /dev/null +++ b/curator/routes.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# adapted from router.example.py + +# NOTE that this requires a parametric router in the web2py root directory. +# Let's keep all the important stuff here, and just copy a minimal router +# (SITE.routes.py) into the site root. + +# NOTE that this (app-specific) routes.py file mainly defines a router by the +# same name. More general settings must be done in the main routes.py alongside +# the web2py/applications/ directory +# root_static (for favicon.ico, robots.txt, etc) +# routes_onerror (defines error pages per app, per error code, or defaults) +# domain (maps domain names and ports to particular app) +# See SITE.routes.py for recommended settings. + +routers = dict( + curator=dict( + # convert dashes (hyphens) in URLs to underscores in web2py controller+action names + map_hyphen=True, + ), +) + +# see router.example.py for (many) more options! diff --git a/curator/static/css/default.css b/curator/static/css/default.css index f182447dd..c975d5871 100644 --- a/curator/static/css/default.css +++ b/curator/static/css/default.css @@ -870,7 +870,13 @@ tr.after-shims th { position: relative; top: -8px; } -.collection-move-panel { + +.form-horizontal.metadata-readonly .control-group { + margin-bottom: 12px; +} + +.collection-move-panel, +.synthrun-move-panel { position: absolute; margin-left: 2px; z-index: 1; @@ -880,7 +886,8 @@ tr.after-shims th { -moz-box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); } -.collection-move-panel button { +.collection-move-panel button, +.synthrun-move-panel button { opacity: 1.0; } @@ -1014,3 +1021,11 @@ form.skip-label * { form.skip-label *.non-fading { opacity: 1.0; } +.loading-message { + display: inline-block; + position: relative; + top: -6px; + left: 8px; + color: #999; + font-style: italic; +} diff --git a/curator/static/js/bootstrap-tagsinput.js b/curator/static/js/bootstrap-tagsinput.js new file mode 100644 index 000000000..4c97aceea --- /dev/null +++ b/curator/static/js/bootstrap-tagsinput.js @@ -0,0 +1,503 @@ +(function ($) { + "use strict"; + + var defaultOptions = { + tagClass: function(item) { + return 'label label-info'; + }, + itemValue: function(item) { + return item ? item.toString() : item; + }, + itemText: function(item) { + return this.itemValue(item); + }, + freeInput: true, + maxTags: undefined, + confirmKeys: [13], + onTagExists: function(item, $tag) { + $tag.hide().fadeIn(); + } + }; + + /** + * Constructor function + */ + function TagsInput(element, options) { + this.itemsArray = []; + + this.$element = $(element); + this.$element.hide(); + + this.isSelect = (element.tagName === 'SELECT'); + this.multiple = (this.isSelect && element.hasAttribute('multiple')); + this.objectItems = options && options.itemValue; + this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; + this.inputSize = Math.max(1, this.placeholderText.length); + + this.$container = $('
'); + this.$input = $('').appendTo(this.$container); + + this.$element.after(this.$container); + + this.build(options); + } + + TagsInput.prototype = { + constructor: TagsInput, + + /** + * Adds the given item as a new tag. Pass true to dontPushVal to prevent + * updating the elements val() + */ + add: function(item, dontPushVal) { + var self = this; + + if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) + return; + + // Ignore falsey values, except false + if (item !== false && !item) + return; + + // Throw an error when trying to add an object while the itemValue option was not set + if (typeof item === "object" && !self.objectItems) + throw("Can't add objects when itemValue option is not set"); + + // Ignore strings only containg whitespace + if (item.toString().match(/^\s*$/)) + return; + + // If SELECT but not multiple, remove current tag + if (self.isSelect && !self.multiple && self.itemsArray.length > 0) + self.remove(self.itemsArray[0]); + + if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { + var items = item.split(','); + if (items.length > 1) { + for (var i = 0; i < items.length; i++) { + this.add(items[i], true); + } + + if (!dontPushVal) + self.pushVal(); + return; + } + } + + var itemValue = self.options.itemValue(item), + itemText = self.options.itemText(item), + tagClass = self.options.tagClass(item); + + // Ignore items allready added + var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; + if (existing) { + // Invoke onTagExists + if (self.options.onTagExists) { + var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); + self.options.onTagExists(item, $existingTag); + } + return; + } + + // register item in internal array and map + self.itemsArray.push(item); + + // add a tag element + var $tag = $('' + htmlEncode(itemText) + ''); + $tag.data('item', item); + self.findInputWrapper().before($tag); + $tag.after(' '); + + // add