From cce1be7eee05461ab584cb4657b8b2669fc4dd96 Mon Sep 17 00:00:00 2001 From: Thomas McGowan Date: Fri, 12 Dec 2014 20:54:33 +0000 Subject: [PATCH 001/473] This pull request is to extend the GET /api/jobs request/response. 1.) If the submitted api key is an admin's key AND the request has an included data_range_min or date_range_max value, the response will include jobs from all galaxy users. 2.) The response for (1) is extended to include a) external_id b) galaxy job id c) user email 3.) All users can submit a date range filter. 4.) Existing calls are unaffected by the change: https://galaxy-ng.msi.umn.edu/api/jobs?key=xxxxxxxxxxxxxxxxxxxxxxx [ { "create_time": "2014-12-01T19:23:37.270768", "exit_code": 0, "id": "cffdbbf7257b6526", "model_class": "Job", "state": "ok", "tool_id": "upload1", "update_time": "2014-12-01T19:24:24.608105" } ] 5.) Extended admin request: https://galaxy-ng.msi.umn.edu/api/jobs?date_range_min=2014-11-01?&key=xxxxxADMIN_KEYxxxxxxxxxx ... { "create_time": "2014-11-11T15:59:19.909902", "exit_code": 0, "external_id": "31325.nokomis0015.msi.umn.edu", "id": "2003774561413b68", "job_id": 3733, "model_class": "Job", "state": "ok", "tool_id": "testtoolshed.g2.bx.psu.edu/repos/tmcgowan/fastqc/fastqc/0.60", "update_time": "2014-11-11T16:00:10.872804", "user_email": "mcgo0092+msistaff@msi.umn.edu" }, { "create_time": "2014-11-10T15:15:31.801047", "exit_code": 0, "external_id": "30299.nokomis0015.msi.umn.edu", "id": "e330bc56cb5095a7", "job_id": 3732, "model_class": "Job", "state": "ok", "tool_id": "testtoolshed.g2.bx.psu.edu/repos/jjohnson/snpeff/snpEff/4.0.1", "update_time": "2014-11-10T15:16:31.996148", "user_email": "jj+support@msi.umn.edu" } ... --- lib/galaxy/webapps/galaxy/api/jobs.py | 45 ++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index bdb5b199eff..3960a4c7a4e 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -24,9 +24,12 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses @expose_api def index( self, trans, **kwd ): """ - index( trans, state=None, tool_id=None, history_id=None ) + index( trans, state=None, tool_id=None, history_id=None, date_range_min=None, date_range_max=None ) * GET /api/jobs: return jobs for current user + + !! if user is admin and date_range_[min|max], then + return jobs for all galaxy users based on filtering - this is an extended service :type state: string or list :param state: limit listing of jobs to those that match one of the included states. If none, all are returned. @@ -36,6 +39,12 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :type tool_id: string or list :param tool_id: limit listing of jobs to those that match one of the included tool_ids. If none, all are returned. + :type date_range_min: string '2014-01-01' + :param date_range_min: limit the listing of jobs to those updated on or after requested date + + :type date_range_max: string '2014-12-31' + :param date_range_max: limit the listing of jobs to those updated on or before requested date + :type history_id: string :param history_id: limit listing of jobs to those that match the history_id. If none, all are returned. @@ -44,9 +53,15 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses """ state = kwd.get( 'state', None ) - query = trans.sa_session.query( trans.app.model.Job ).filter( - trans.app.model.Job.user == trans.user - ) + if trans.user_is_admin() and (kwd.get('date_range_min', None) or kwd.get('date_range_max', None)): + is_extended_service = True + else: + is_extended_service = False + + if is_extended_service: + query = trans.sa_session.query( trans.app.model.Job ) + else: + query = trans.sa_session.query( trans.app.model.Job ).filter(trans.app.model.Job.user == trans.user) def build_and_apply_filters( query, objects, filter_func ): if objects is not None: @@ -63,6 +78,9 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses query = build_and_apply_filters( query, kwd.get( 'tool_id', None ), lambda t: trans.app.model.Job.tool_id == t ) query = build_and_apply_filters( query, kwd.get( 'tool_id_like', None ), lambda t: trans.app.model.Job.tool_id.like(t) ) + + query = build_and_apply_filters( query, kwd.get( 'date_range_min', None ), lambda dmin: trans.app.model.Job.table.c.update_time >= dmin ) + query = build_and_apply_filters( query, kwd.get( 'date_range_max', None ), lambda dmax: trans.app.model.Job.table.c.update_time <= dmax ) history_id = kwd.get( 'history_id', None ) if history_id is not None: @@ -78,7 +96,24 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses else: order_by = trans.app.model.Job.update_time.desc() for job in query.order_by( order_by ).all(): - out.append( self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) ) + j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) + + if is_extended_service: + def get_email_dict(trans): + user_email_dict = {} + user_query = trans.sa_session.query( trans.app.model.User ) + for user in user_query.all(): + u = user.to_dict() + user_email_dict[u['id']] = u['email'] + return user_email_dict + + user_emails = get_email_dict(trans) + j['external_id'] = job.job_runner_external_id + j['job_id'] = job.id + if job.user_id in user_emails: + j['user_email'] = user_emails[job.user_id] + + out.append(j) return out @expose_api From 67aedae007072d41c2e7dc852b1d685bc4c76752 Mon Sep 17 00:00:00 2001 From: Thomas McGowan Date: Mon, 15 Dec 2014 16:21:36 +0000 Subject: [PATCH 002/473] Edits based on reviewer comments. Removed redundant email query, removed job_id from response. Added user_details as param. --- lib/galaxy/webapps/galaxy/api/jobs.py | 28 +++++++++------------------ 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 3960a4c7a4e..5b29af07892 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -24,11 +24,11 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses @expose_api def index( self, trans, **kwd ): """ - index( trans, state=None, tool_id=None, history_id=None, date_range_min=None, date_range_max=None ) + index( trans, state=None, tool_id=None, history_id=None, date_range_min=None, date_range_max=None, user_details=False ) * GET /api/jobs: return jobs for current user - !! if user is admin and date_range_[min|max], then + !! if user is admin and user_details is True, then return jobs for all galaxy users based on filtering - this is an extended service :type state: string or list @@ -39,6 +39,9 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :type tool_id: string or list :param tool_id: limit listing of jobs to those that match one of the included tool_ids. If none, all are returned. + :type user_details: boolean + :param user_details: if true, and requestor is an admin, will return external job id and user email. + :type date_range_min: string '2014-01-01' :param date_range_min: limit the listing of jobs to those updated on or after requested date @@ -51,9 +54,8 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :rtype: list :returns: list of dictionaries containing summary job information """ - state = kwd.get( 'state', None ) - if trans.user_is_admin() and (kwd.get('date_range_min', None) or kwd.get('date_range_max', None)): + if trans.user_is_admin() and kwd.get('user_details', False): is_extended_service = True else: is_extended_service = False @@ -96,24 +98,12 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses else: order_by = trans.app.model.Job.update_time.desc() for job in query.order_by( order_by ).all(): - j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) - + j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) if is_extended_service: - def get_email_dict(trans): - user_email_dict = {} - user_query = trans.sa_session.query( trans.app.model.User ) - for user in user_query.all(): - u = user.to_dict() - user_email_dict[u['id']] = u['email'] - return user_email_dict - - user_emails = get_email_dict(trans) j['external_id'] = job.job_runner_external_id - j['job_id'] = job.id - if job.user_id in user_emails: - j['user_email'] = user_emails[job.user_id] - + j['user_email'] = job.user.email out.append(j) + return out @expose_api From d07cdb2de9d45f69b9e325c86a4933e541e36ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Tue, 16 Dec 2014 11:40:29 +0000 Subject: [PATCH 003/473] Created new branch arff_datatype From 71299a8c75f7c03c037a0a3bb04596630d831b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Tue, 16 Dec 2014 12:08:50 +0000 Subject: [PATCH 004/473] Add Arff datatype class with sniffer and metadata. --- lib/galaxy/datatypes/text.py | 104 +++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/lib/galaxy/datatypes/text.py b/lib/galaxy/datatypes/text.py index 2b1a622f409..c872c042c89 100644 --- a/lib/galaxy/datatypes/text.py +++ b/lib/galaxy/datatypes/text.py @@ -6,6 +6,7 @@ from galaxy.datatypes.data import Text from galaxy.datatypes.data import get_file_peek from galaxy.datatypes.data import nice_size +from galaxy.datatypes.metadata import MetadataElement from galaxy import util import tempfile @@ -155,3 +156,106 @@ class Obo( Text ): return True return False + +class Arff( Text ): + """ + An ARFF (Attribute-Relation File Format) file is an ASCII text file that describes a list of instances sharing a set of attributes. + http://weka.wikispaces.com/ARFF + """ + file_ext = "arff" + + + """Add metadata elements""" + MetadataElement( name="comment_lines", default=0, desc="Number of comment lines", readonly=True, optional=True, no_value=0 ) + MetadataElement( name="columns", default=0, desc="Number of columns", readonly=True, visible=True, no_value=0 ) + + def set_peek( self, dataset, is_multi_byte=False ): + if not dataset.dataset.purged: + dataset.peek = get_file_peek( dataset.file_name, is_multi_byte=is_multi_byte ) + dataset.blurb = "Attribute-Relation File Format (ARFF)" + dataset.blurb += ", %s comments, %s attributes" % ( dataset.metadata.comment_lines, dataset.metadata.columns ) + else: + dataset.peek = 'file does not exist' + dataset.blurb = 'file purged from disc' + + def sniff( self, filename ): + """ + Try to guess the Arff filetype. + It usually starts with a "format-version:" string and has several stanzas which starts with "id:". + """ + with open( filename ) as handle: + relation_found = False + attribute_found = False + prefix = "" + for line_count, line in enumerate( handle ): + if line_count > 1000: + # only investigate the first 1000 lines + return False + line = line.strip() + if not line: + continue + + start_string = line[:20].upper() + if start_string.startswith("@RELATION"): + relation_found = True + elif start_string.startswith("@ATTRIBUTE"): + attribute_found = True + elif start_string.startswith("@DATA"): + # @DATA should be the last data block + if relation_found and attribute_found: + return True + return False + + def set_meta( self, dataset, **kwd ): + """ + Trying to count the comment lines and the number of columns included. + A typical ARFF data block looks like this: + @DATA + 5.1,3.5,1.4,0.2,Iris-setosa + 4.9,3.0,1.4,0.2,Iris-setosa + """ + if dataset.has_data(): + comment_lines = 0 + first_real_line = False + data_block = False + with open( dataset.file_name ) as handle: + for line in handle: + line = line.strip() + if not line: + continue + if line.startswith('%') and not first_real_line: + comment_lines += 1 + else: + first_real_line = True + if data_block: + if line.startswith('{'): + # Sparse representation + """ + @data + 0, X, 0, Y, "class A", {5} + or + @data + {1 X, 3 Y, 4 "class A"}, {5} + """ + token = line.split('}',1) + first_part = token[0] + last_column = first_part.split(',')[-1].strip() + numeric_value = last_column.split()[0] + column_count = int(numeric_value) + if len(token) > 1: + # we have an additional weight + column_count -= 1 + else: + columns = line.strip().split(',') + column_count = len(columns) + if columns[-1].strip().startswith('{'): + # we have an additional weight at the end + column_count -= 1 + + # We have now the column_count and we know the initial comment lines. So we can terminate here. + break + if line[:5].upper() == "@DATA": + data_block = True + dataset.metadata.comment_lines = comment_lines + dataset.metadata.columns = column_count + From cc3ee556ea3128be466ef34d0e02a5b95dbce95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Tue, 16 Dec 2014 12:16:00 +0000 Subject: [PATCH 005/473] Add Arff datatype and sniffer to datatypes_conf.xml. --- config/datatypes_conf.xml.sample | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index fcb9001003c..d092c7d6d0e 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -3,6 +3,7 @@ + @@ -298,6 +299,7 @@ + From c1f421da52ad37cfced2b72f35d4bb35ab4fcc17 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 17 Dec 2014 09:00:15 -0500 Subject: [PATCH 006/473] Update tag latest_2014.10.06 for changeset 5834b1066462 --- .hgtags | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hgtags b/.hgtags index 22ac92bb8d0..9b6296b8107 100644 --- a/.hgtags +++ b/.hgtags @@ -20,4 +20,4 @@ a4113cc1cb5eaa68091c9a73375f00555b66dd11 release_2013.01.13 ca45b78adb4152fc6e7395514d46eba6b7d0b838 release_2014.08.11 548ab24667d6206780237bd807f7d857a484c461 latest_2014.08.11 2092948937ac30ef82f71463a235c66d34987088 release_2014.10.06 -7086b87d83a9092cbece0fec6f3e3ed03266be0c latest_2014.10.06 +5834b1066462dd219f67c1c3cbd77c78e7cf3a6c latest_2014.10.06 From 195eab61fbdfacb9bc1bf1132c83ac7e3d4a5ac8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 17 Dec 2014 10:57:44 -0500 Subject: [PATCH 007/473] Fix a74d320. --- lib/galaxy/tools/test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/galaxy/tools/test.py b/lib/galaxy/tools/test.py index 2c15cacc445..fc87398ffff 100644 --- a/lib/galaxy/tools/test.py +++ b/lib/galaxy/tools/test.py @@ -195,8 +195,7 @@ class ToolTestBuilder( object ): if isinstance( value, basic.DataToolParameter ): if not isinstance(param_value, list): param_value = [ param_value ] - map( lambda v: self.__add_uploaded_dataset( context.for_state(), v, param_extra, value ), -param_value ) + map( lambda v: self.__add_uploaded_dataset( context.for_state(), v, param_extra, value ), param_value ) processed_value = param_value elif isinstance( value, basic.DataCollectionToolParameter ): assert 'collection' in param_extra From 98db1349a403d46602fe93390501f3703288b639 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Wed, 17 Dec 2014 15:26:43 -0500 Subject: [PATCH 008/473] Initial commit for changes to managers: implement base ModelManager, Serializer, and Deserializer; incorporate into hdas and histories managers; implement users, datasets nabagers; re-work histories api to use the managers better; unit tests for managers; search and destroy on UsesHistoryMixin; minor fixes to list-panel --- client/galaxy/scripts/mvc/list/list-panel.js | 8 +- lib/galaxy/managers/base.py | 556 +++++++++++++++++- lib/galaxy/managers/collections.py | 11 +- lib/galaxy/managers/datasets.py | 102 ++++ lib/galaxy/managers/hdas.py | 542 ++++++++++++++--- lib/galaxy/managers/histories.py | 344 ++++++----- lib/galaxy/managers/lddas.py | 2 +- lib/galaxy/managers/sharable.py | 289 +++++++++ lib/galaxy/managers/users.py | 175 ++++++ lib/galaxy/model/__init__.py | 11 + lib/galaxy/model/mapping.py | 7 +- lib/galaxy/web/base/controller.py | 210 +------ lib/galaxy/webapps/galaxy/api/histories.py | 152 ++--- .../webapps/galaxy/api/history_contents.py | 30 +- lib/galaxy/webapps/galaxy/api/provenance.py | 2 +- lib/galaxy/webapps/galaxy/api/tools.py | 5 +- lib/galaxy/webapps/galaxy/api/workflows.py | 2 +- .../webapps/galaxy/controllers/history.py | 51 +- lib/galaxy/webapps/galaxy/controllers/page.py | 8 +- lib/galaxy/webapps/galaxy/controllers/root.py | 2 +- lib/galaxy/workflow/run_request.py | 4 +- static/scripts/mvc/list/list-panel.js | 8 +- static/scripts/packed/mvc/list/list-panel.js | 2 +- test/unit/managers/mock.py | 87 +++ test/unit/managers/test_DatasetManager.py | 114 ++++ test/unit/managers/test_HDAManager.py | 198 +++++++ test/unit/managers/test_HistoryManager.py | 307 ++++++++++ test/unit/managers/test_ModelManager.py | 81 +++ test/unit/managers/test_UserManager.py | 131 +++++ 29 files changed, 2832 insertions(+), 609 deletions(-) create mode 100644 lib/galaxy/managers/datasets.py create mode 100644 lib/galaxy/managers/sharable.py create mode 100644 lib/galaxy/managers/users.py create mode 100644 test/unit/managers/mock.py create mode 100644 test/unit/managers/test_DatasetManager.py create mode 100644 test/unit/managers/test_HDAManager.py create mode 100644 test/unit/managers/test_HistoryManager.py create mode 100644 test/unit/managers/test_ModelManager.py create mode 100644 test/unit/managers/test_UserManager.py diff --git a/client/galaxy/scripts/mvc/list/list-panel.js b/client/galaxy/scripts/mvc/list/list-panel.js index f78ccaaee27..a2e9092fa76 100644 --- a/client/galaxy/scripts/mvc/list/list-panel.js +++ b/client/galaxy/scripts/mvc/list/list-panel.js @@ -872,10 +872,10 @@ var ModelListPanel = ListPanel.extend({ this.log( this + '._setUpModelListeners', this.model ); // bounce model errors up to the panel this.model.on( 'error', function(){ - //TODO: namespace? - //var args = Array.prototype.slice.call( arguments, 0 ); - //args[0] = 'model:' + args[0]; - this.trigger.apply( panel, arguments ); + var args = Array.prototype.slice.call( arguments, 0 ); + //args.unshift( 'model:error' ); + args.unshift( 'error' ); + this.trigger.apply( this, args ); }, this ); return this; }, diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 956bde2f8a0..ff4d97aac1b 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -1,21 +1,20 @@ -from galaxy import exceptions +import re + +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) +import sqlalchemy from galaxy import model +from galaxy import exceptions from galaxy.model import tool_shed_install +from galaxy.util import sanitize_html import logging log = logging.getLogger( __name__ ) -class ModelManager( object ): - pass - - -class ModelSerializer( object ): - pass - - +# ============================================================================= def security_check( trans, item, check_ownership=False, check_accessible=False ): """ Security checks for an item: checks if (a) user owns item or (b) item is accessible to user. This is a generic method for dealing with objects @@ -118,3 +117,542 @@ def get_object( trans, id, class_name, check_ownership=False, check_accessible=F % ( class_name, getattr( item, 'name', id ) ), type="warning" ) return item + +# ============================================================================= +class ModelManager( object ): + model_class = object + default_order_by = None + foreign_key_name = None + + def __init__( self, app ): + assert isinstance( self.model_class, object ), ( + "model class must be set for %s" %( self.__class__.__name__ ) ) + self.model_class_name = self.model_class.__class__.__name__ + assert isinstance( self.default_order_by, tuple ), ( + "default_order_by for %s must be a tuple" %( self.__class__.__name__ ) ) + + self.app = app + + #NOTE: at this layer, all ids are expected to be decoded and in int form + # ------------------------------------------------------------------------- get/read/query + def query( self, trans, eagerloads=True, filters=None, order_by=None, limit=None, offset=None, **kwargs ): + #print 'query:', eagerloads, filters, order_by, limit, offset, kwargs + query = trans.sa_session.query( self.model_class ) + + # joined table loading + if eagerloads is False: + query = query.enable_eagerloads( False ) + +#TODO: if non-orm filters are the only option, here is where they'd go + query = self._apply_filters( query, filters ) + query = self._apply_order_by_limit_offset( query, order_by, limit, offset ) + return query + + def _apply_filters( self, query, filters ): + if filters is None: + return query + + if not isinstance( filters, list ): + filters = [ filters ] + #note: implicit AND + for filter in filters: + query = query.filter( filter ) + return query + + def _munge_filters( self, filtersA, filtersB ): + # ... which sounds like a euphamism for something + final_filters = [ self.model_class.id == id ] + if filtersA is None: + return filtersB + if filtersB is None: + return filtersA + if not isinstance( filtersA, list ): + filtersA = [ filtersA ] + if not isinstance( filtersB, list ): + filtersB = [ filtersB ] + return filtersA + filtersB + + def _apply_order_by_limit_offset( self, query, order_by, limit, offset ): + query = self._apply_order_by( query, order_by ) + return self._apply_limit_offset( query, limit, offset ) + + def _apply_order_by( self, query, order_by ): + if order_by is None: + #TODO: allow non-tuple default_order_by + return query.order_by( *self.default_order_by ) + + if isinstance( order_by, ( list, tuple ) ): + return query.order_by( *order_by ) + return query.order_by( order_by ) + + def _apply_limit_offset( self, query, limit, offset ): + if limit is not None: + query = query.limit( limit ) + if offset is not None: + query = query.offset( offset ) + return query + + # ......................................................................... common queries +#TODO: needed? + def one( self, trans, **kwargs ): + try: + return self.query( trans, **kwargs ).one() + # re-raise as serializable errors + except sqlalchemy.orm.exc.NoResultFound, not_found: + raise exceptions.ObjectNotFound( self.model_class.__name__ + ' not found' ) + except sqlalchemy.orm.exc.MultipleResultsFound, multi_found: + raise exceptions.Conflict( 'found more than one ' + self.model_class.__name__ ) + + def list( self, trans, **kwargs ): + return self.query( trans, **kwargs ).all() + + def by_id( self, trans, id, **kwargs ): + id_filter = self.model_class.id == id + return self.one( trans, filters=id_filter, **kwargs ) + + def _query_by_ids( self, trans, ids, filters=None, **kwargs ): + ids_filter = self.model_class.id.in_( ids ) + return self.query( trans, filters=self._munge_filters( ids_filter, filters ), **kwargs ) + + def by_ids( self, trans, ids, **kwargs ): + found = self._query_by_ids( trans, ids, **kwargs ).all() + #TODO: this does not order by the original 'ids' array + + # ...could use get (supposedly since found are in the session, the db won't be hit twice) + #return map( trans.sa_session.query( self.model_class ).get, ids ) + + # ...could implement own version here - slow? + return self._order_items_by_id( ids, found ) + #return found + + def _order_items_by_id( self, ids, items ): + """ + Given a list of (unique) ids and a list of items having an 'id' attribute, + return items that have the given ids in that order. + + If an id in ids is not found or if an item in items doesn't have a given + id, they will not be in the returned list. + """ + ID_ATTR_NAME = 'id' + #TODO:?? aside from sqlalx.get mentioned above, I haven't seen an in-SQL way + # to make this happen. This may not be the most efficient way either. + #NOTE: that this isn't sorting by id - this is matching the order in items to the order in ids + # move items list into dict by id + item_dict = {} + for item in items: + item_id = getattr( item, ID_ATTR_NAME, None ) + if item_id: + item_dict[ item_id ] = item + # pull from map in order of ids + in_order = [] + for id in ids: + if id in item_dict: + in_order.append( item_dict[ id ] ) + return in_order + + # ------------------------------------------------------------------------- create + def create( self, trans, flush=True, *args, **kwargs ): + item = self.model_class( *args, **kwargs ) + trans.sa_session.add( item ) + if flush: + trans.sa_session.flush() + return item + + def copy( self, trans, item, **kwargs ): + raise exceptions.NotImplemented( 'Abstract method' ) + + # ------------------------------------------------------------------------- update + def update( self, trans, item, new_values, flush=True, **kwargs ): + trans.sa_session.add( item ) + for key, value in new_values.items(): + if hasattr( item, key ): + setattr( item, key, value ) + if flush: + trans.sa_session.flush() + return item + + def associate( self, trans, associate_with, item, foreign_key_name=None ): + foreign_key_name = foreign_key_name or self.foreign_key_name + setattr( associate_with, foreign_key_name, item ) + return item + + def query_associated( self, trans, associated_model_class, item, foreign_key_name=None ): + foreign_key_name = foreign_key_name or self.foreign_key_name + foreign_key = getattr( associated_model_class, foreign_key_name ) + return trans.sa_session.query( associated_model_class ).filter( foreign_key == item ) + + # ------------------------------------------------------------------------- delete + # a rename of sql DELETE to differentiate from the Galaxy notion of mark_as_deleted + #def remove_from_db( self, trans, item, **kwargs ): + # return item + + +# ============================================================================= +class DeletableModelInterface( object ): + + def delete( self, trans, item, flush=True, **kwargs ): + # often only a matter of 'marking as deleted' + trans.sa_session.add( item ) + item.deleted = True + if flush: + trans.sa_session.flush() + return item + + def undelete( self, trans, item, flush=True, **kwargs ): + trans.sa_session.add( item ) + item.deleted = False + if flush: + trans.sa_session.flush() + return item + + +# ============================================================================= +class PurgableModelInterface( DeletableModelInterface ): + + def error_unless_dataset_purge_allowed( self, trans, item, msg=None ): + if not trans.app.config.allow_user_dataset_purge: + msg = msg or 'This instance does not allow user dataset purging' + raise exceptions.ConfigDoesNotAllowException( msg ) + #raise exceptions.ConfigDoesNotAllowException( msg, item=item ) + return item + + def purge( self, trans, item, flush=True, **kwargs ): + trans.sa_session.add( item ) + item.purged = True + if flush: + trans.sa_session.flush() + return item + + +# ============================================================================= +class AccessibleModelInterface( object ): + + # don't want to override by_id since consumers will also want to fetch w/o any security checks + + def accessible_by_id( self, trans, id, user, **kwargs ): + item = ModelManager.by_id( self, trans, id ) + return self.error_unless_accessible( trans, item, user ) + + def error_unless_accessible( self, trans, item, user ): + if self.is_accessible( trans, item, user ): + return item + raise exceptions.ItemAccessibilityException( "%s is not accessible by user" % self.model_class_name ) + + def is_accessible( self, trans, item, user ): + raise exceptions.NotImplemented( "Abstract Interface Method" ) + #OR? return False + + def list_accessible( self, trans, user, **kwargs ): + items = ModelManager.list( self, trans, **kwargs ) + return [ self.error_unless_accessible( trans, item, user ) for item in items ] + + def filter_accessible( self, trans, user, **kwargs ): + items = ModelManager.list( self, trans, **kwargs ) + return filter( lambda item: self.is_accessible( trans, item, user ), items ) + + +# ============================================================================= +class ModelSerializingError( exceptions.ObjectAttributeInvalidException ): + """Thrown when request model values can't be serialized""" + #TODO: better as 500? + pass + + +class ModelDeserializingError( exceptions.ObjectAttributeInvalidException ): + """Thrown when an incoming value isn't usable by the model + (bad type, out of range, etc.) + """ + pass + + +# ----------------------------------------------------------------------------- +class ModelSerializer( object ): + #model_mgr_class = None + + # primarily designed for turning models into/from JSONable dicts + # model --> ModelSerializer --> JSON + # JSON --> ModelSerializer --> model + def __init__( self, app ): + self.app = app + #TODO: + #self.mgr = model_mgr_class( app ) + + # a map of dictionary keys to the functions (often lambdas) that create the values for those keys + self.serializers = {} + # add subclass serializers defined there + self.add_serializers() + # a list of valid serializable keys that can use the default (string) serializer + # this allows us to: 'mention' the key without adding the default serializer + #NOTE: if a key is requested that is in neither serializable_keys or serializers, it is not returned + #TODO: rename to 'keys_using_default_serializer' + #TODO: or (alt) allow key in serializers with None as value (indicating use default_serializer) + self.serializable_keys = [] + + # views are collections of serializable attributes (a named array of keys) + # inspired by model.dict_{view}_visible_keys + self.views = {} + self.default_view = None + + # ......................................................................... serializing + def add_serializers( self ): + # register a map of attribute keys -> functions that will serialize the attribute + # to be overridden in subclasses + pass + + def serialize( self, trans, item, keys ): + # main interface fn for converting a model to a dict + #TODO: try/catch here? + returned = {} + for key in keys: + # check both serializers and serializable keys + if key in self.serializers: + returned[ key ] = self.serializers[ key ]( trans, item, key ) + elif key in self.serializable_keys: + returned[ key ] = self.default_serializer( trans, item, key ) + # ignore bad/unreg keys + return returned + + # ... common deserializers for primitives + def default_serializer( self, trans, item, key ): + return getattr( item, key ) + + def serialize_date( self, trans, item, key ): + date = getattr( item, key ) + return date.isoformat() if date is not None else None + + # .... common serializers for Galaxy + def serialize_id( self, trans, item, key ): + id = getattr( item, key ) + return trans.security.encode_id( id ) if id is not None else None + + # ......................................................................... serializing to a view + # where a view is a predefied list of keys to serialize + def serialize_to_view( self, trans, item, view=None, keys=None ): + keys = keys or [] + all_keys = self.view_to_list( view ) + keys + return self.serialize( trans, item, all_keys ) + + def view_to_list( self, view=None ): + if view is None: + view = self.default_view + if view not in self.views: + raise ModelSerializingError( 'unknown view', view=view, available_views=self.views ) + return self.views[ view ] + + # ......................................................................... aliases + #def to_dict( self, trans, item, view=None, view_keys=None ): + # view_keys = view_keys or self.view_to_list( view ) + # return self.serialize( trans, item, view_keys ) + + +# ----------------------------------------------------------------------------- +class ModelValidator( object ): + + def __init__( self, app, *args, **kwargs ): + self.app = app + + def type( self, key, val, types ): + if not isinstance( val, types ): + msg = 'must be a type: %s' % ( str( types ) ) + raise exceptions.RequestParameterInvalidException( msg, key=key, val=val ) + return val + + # validators for primitives + def basestring( self, key, val ): + return self.type( key, val, basestring ) + + def nullable_basestring( self, key, val ): + return self.type( key, val, ( basestring, type( None ) ) ) + + def bool( self, key, val ): + return self.type( key, val, bool ) + + def int( self, key, val ): + return self.type( key, val, int ) + + def int_range( self, key, val, min=None, max=None ): + val = self.type( key, val, int ) + if val < min: + raise exceptions.RequestParameterInvalidException( "less than minimum", key=key, val=val, min=min ) + if val > max: + raise exceptions.RequestParameterInvalidException( "greater than maximum", key=key, val=val, max=max ) + return val + + def basestring_list( self, key, val ): + val = self.type( key, val, list ) + return [ self.basestring( key, elem ) for elem in val ] + + # validators for Galaxy + def genome_build( self, trans, key, val ): + #TODO: is this correct? + if val is None: + return '?' + # wot...why tuples? + if val not in dict( trans.db_builds ): + raise exceptions.RequestParameterInvalidException( "invalid reference", key=key, val=val ) + return val + + #def slug( self, trans, item, key, val ): + # pass + + +# ----------------------------------------------------------------------------- +class ModelDeserializer( object ): + #model_mgr_class = None + + # primarily designed for creating models from dicts + # Dict --> ModelDeserializer --> model + def __init__( self, app ): + self.app = app + #TODO: + #self.mgr = model_mgr_class( app ) + + self.deserializers = {} + self.add_deserializers() + self.deserializable_keys = [] + self.validate = ModelValidator( self.app ) + + # ......................................................................... deserializing + def add_deserializers( self ): + # register a map of attribute keys -> functions that will deserialize data into the attribute + # to be overridden in subclasses + pass + + def deserialize( self, trans, item, data, flush=True ): + new_dict = {} + for key, val in data.items(): + #print key, val + if key in self.deserializers: + #print '\t deserializer:', self.deserializers[ key ] + new_dict[ key ] = self.deserializers[ key ]( trans, item, key, val ) + #!important: don't error on unreg. keys -- many clients will add weird ass keys onto the model + #TODO:?? here or in manager? + if flush and len( new_dict ): + trans.sa_session.add( item ) + trans.sa_session.flush() + return new_dict + + # ... common deserializers for primitives + def default_deserializer( self, trans, item, key, val ): + #TODO: sets the item attribute to value (this may not work in all instances) + + # only do the following if val == getattr( item, key ) + if hasattr( item, key ) and getattr( item, key ) != val: + setattr( item, key, val ) + return val + + def deserialize_basestring( self, trans, item, key, val ): + val = self.validate.basestring( key, val ) + return self.default_deserializer( trans, item, key, val ) + + def deserialize_bool( self, trans, item, key, val ): + val = self.validate.bool( key, val ) + return self.default_deserializer( trans, item, key, val ) + + def deserialize_int( self, trans, item, key, val, min=None, max=None ): + val = self.validate.int_range( key, val, min, max ) + return self.default_deserializer( trans, item, key, val ) + + # ... common deserializers for Galaxy + def deserialize_genome_build( self, trans, item, key, val ): + val = self.validate.genome_build( trans, key, val ) + return self.default_deserializer( trans, item, key, val ) + + def deserialize_annotation( self, trans, item, key, val ): + val = self.validate.nullable_basestring( key, val ) + #TODO: have to assume trans.user here... + return item.add_item_annotation( trans.sa_session, trans.user, item, val ) + + def deserialize_tags( self, trans, item, key, val ): + new_tags_list = self.validate.basestring_list( key, val ) + #TODO: have to assume trans.user here... + user = trans.user + #TODO: duped from tags manager - de-dupe when moved to taggable mixin + tag_handler = trans.app.tag_handler + tag_handler.delete_item_tags( trans, user, item ) + new_tags_str = ','.join( new_tags_list ) + tag_handler.apply_item_tags( trans, user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) ) + #trans.sa_session.flush() + return item.tags + + #def deserialize_rating( self, trans, item, key, val ): + # val = self.validate.int_range( key, val, 0, 5 ) + # return self.set_rating...( trans, item, val, user=trans.user ) + + # ......................................................................... aliases + #def from_dict( self, trans, item, data ): + # self.deserialize( trans, item, data ) + + +# ----------------------------------------------------------------------------- +#TODO: these are of questionable value if we don't want to enable users to delete/purge via update +class DeletableModelDeserializer( object ): + + def add_deserializers( self ): + self.deserializers[ 'deleted' ] = self.deserialize_deleted + + def deserialize_deleted( self, trans, item, key, val ): + new_deleted = self.validate.bool( key, val ) + if new_deleted == item.deleted: + return item.deleted + #TODO:?? flush=False? + if new_deleted: + self.mgr.delete( trans, item ) + else: + self.mgr.undelete( trans, item ) + return item.deleted + + +# ----------------------------------------------------------------------------- +class PurgableModelDeserializer( DeletableModelDeserializer ): + + def add_deserializers( self ): + self.deserializers[ 'purged' ] = self.deserialize_purged + + def deserialize_purged( self, trans, item, key, val ): + new_purged = self.validate.bool( key, val ) + if new_purged == item.purged: + return item.purged + if new_purged: + self.mgr.purge( trans, item ) + return self.purged + + +# ----------------------------------------------------------------------------- +class TaggableSerializer( object ): + + def add_serializers( self ): + self.serializers[ 'tags' ] = self.serialize_tags + + def serialize_tags( self, trans, item, key ): + if not hasattr( item, 'tags' ): + return None + + tags_str_list = [] + for tag in item.tags: + tag_str = tag.user_tname + if tag.value is not None: + tag_str += ":" + tag.user_value + tags_str_list.append( tag_str ) + print '\t tags_str_list', tags_str_list + return tags_str_list + + +class AnnotatableSerializable( object ): + + def add_serializers( self ): + self.serializers[ 'annotation' ] = lambda t, i, k: unicode( self.get_annotation( t, i ) ) + + +class RatableSerializable( object ): + + def add_serializers( self ): + self.serializers[ 'user_rating' ] = self.serialize_user_rating + self.serializers[ 'community_rating' ] = self.serialize_community_rating + + def serialize_user_rating( self, trans, item, key ): + pass + + def serialize_community_rating( self, trans, item, key ): + pass + diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index f0922d343c2..f474f33d4ea 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -34,10 +34,10 @@ class DatasetCollectionManager( object ): self.collection_type_descriptions = CollectionTypeDescriptionFactory( self.type_registry ) self.model = app.model self.security = app.security - self.hda_manager = hdas.HDAManager() - self.history_manager = histories.HistoryManager() + self.hda_manager = hdas.HDAManager( app ) + self.history_manager = histories.HistoryManager( app ) self.tag_manager = tags.TagsManager( app ) - self.ldda_manager = lddas.LDDAManager( ) + self.ldda_manager = lddas.LDDAManager( app ) def create( self, @@ -278,7 +278,10 @@ class DatasetCollectionManager( object ): def __get_history_collection_instance( self, trans, id, check_ownership=False, check_accessible=True ): instance_id = int( trans.app.security.decode_id( id ) ) collection_instance = trans.sa_session.query( trans.app.model.HistoryDatasetCollectionAssociation ).get( instance_id ) - self.history_manager.secure( trans, collection_instance.history, check_ownership=check_ownership, check_accessible=check_accessible ) + if check_ownership: + self.history_manager.error_unless_owner( trans, collection_instance.history, trans.user ) + if check_accessible: + self.history_manager.error_unless_accessible( trans, collection_instance.history, trans.user ) return collection_instance def __get_library_collection_instance( self, trans, id, check_ownership=False, check_accessible=True ): diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py new file mode 100644 index 00000000000..84805c633b6 --- /dev/null +++ b/lib/galaxy/managers/datasets.py @@ -0,0 +1,102 @@ +""" +Manager and Serializer for Datasets. +""" +from galaxy import model +from galaxy import exceptions + +import base +import users + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): + model_class = model.Dataset + default_order_by = ( model.Dataset.create_time, ) + foreign_key_name = 'dataset' + + def __init__( self, app ): + super( DatasetManager, self ).__init__( app ) + self.user_mgr = users.UserManager( app ) + + # ------------------------------------------------------------------------- CRUD + def create( self, trans, flush=True, **kwargs ): + # default to NEW state on new datasets + kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) ) + dataset = model.Dataset( **kwargs ) + trans.sa_session.add( dataset ) + if flush: + trans.sa_session.flush() + return dataset + + #def to_hda( self, trans, dataset, history, **kwargs ): + # pass + + #def to_ldda( self, trans, dataset, library_folder, **kwargs ): + # pass + + #def copy( self ): + # pass + + def purge( self, trans, dataset, flush=True ): + self.error_unless_dataset_purge_allowed( trans, dataset ) + + # the following also marks dataset as purged and deleted + dataset.full_delete() + trans.sa_session.add( dataset ) + if flush: + trans.sa_session.flush() + return dataset + + # ------------------------------------------------------------------------- accessibility + # datasets can implement the accessible interface, but accessibility is checked in an entirely different way + # than those resources that have a user attribute (histories, pages, etc.) + def is_accessible( self, trans, dataset, user ): + if self.user_mgr.is_admin( trans, user ): + return True + if self.has_access_permission( trans, dataset, user ): + return True + return False + + def has_access_permission( self, trans, dataset, user ): + return trans.app.security_agent.can_access_dataset( user.roles, dataset ) + + #def give_access_permission( self, trans, dataset, user ): + # permission = {} + # #TODO:?? is this correct and reliable? + # user_private_role = self.user_mgr.private_role( trans, user ) + # permission[ trans.app.security_agent.permitted_actions.DATASET_ACCESS ] = [ user_private_role ] + # trans.app.security_agent.set_dataset_permission( dataset, permission ) + + + #def remove_access_permission( self, trans, dataset, user ): + # pass + + + #def has_manage_permission( self, trans, dataset, user ): + # pass + # + #def give_manage_permission( self, trans, dataset, user ): + # pass + # + #def remove_manage_permission( self, trans, dataset, user ): + # pass + + + #TODO: implement above for groups + +#TODO: datatypes? + + +# ============================================================================= +class DatasetAssociationManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): + + def __init__( self, app ): + super( DatasetAssociationManager, self ).__init__( app ) + self.dataset_mgr = DatasetManager( app ) + + def is_accessible( self, trans, dataset_assoc, user ): + # defer to the dataset + return self.dataset_mgr.is_accessible( trans, dataset_assoc.dataset, user ) diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 849af2539fb..9a1db0cbe68 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -4,93 +4,104 @@ Manager and Serializer for HDAs. HistoryDatasetAssociations (HDAs) are datasets contained or created in a history. """ - +from galaxy import model from galaxy import exceptions -from galaxy.managers import base as manager_base -from galaxy.managers import histories as history_manager +import base +import histories +import datasets import galaxy.web import galaxy.datatypes.metadata from galaxy import objectstore -class HDAManager( manager_base.ModelManager ): + +# ============================================================================= +class HDAManager( datasets.DatasetAssociationManager ): """ Interface/service object for interacting with HDAs. """ + model_class = model.HistoryDatasetAssociation + default_order_by = ( model.HistoryDatasetAssociation.create_time, ) - def __init__( self ): + def __init__( self, app ): """ Set up and initialize other managers needed by hdas. """ - self.histories_mgr = history_manager.HistoryManager() + super( HDAManager, self ).__init__( app ) - def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True ): - """ - Get an HDA by its unencoded db id, checking ownership (via its history) - or accessibility (via dataset shares/permissions). - """ - hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( unencoded_id ) - if hda is None: - raise exceptions.ObjectNotFound() - hda = self.secure( trans, hda, check_ownership, check_accessible ) + def create( self, trans, history=None, dataset=None, flush=True, **kwargs ): + if not dataset: + kwargs[ 'create_dataset' ] = True + hda = super( HDAManager, self ).create( trans, flush=flush, + history=history, dataset=dataset, sa_session=trans.sa_session, **kwargs ) + trans.sa_session.add( hda ) + if flush: + trans.sa_session.flush() return hda - def secure( self, trans, hda, check_ownership=True, check_accessible=True ): - """ - check ownership (via its history) or accessibility (via dataset - shares/permissions). - """ - # all items are accessible to an admin - if trans.user and trans.user_is_admin(): - return hda - if check_ownership: - hda = self.check_ownership( trans, hda ) - if check_accessible: - hda = self.check_accessible( trans, hda ) + def copy_hda( self, trans, hda, history=None, **kwargs ): + #TODO:?? not using the following as this fn does not set history and COPIES hid (this doesn't seem correct) + #return hda.copy() + copy = model.HistoryDatasetAssociation( + name = hda.name, + info = hda.info, + blurb = hda.blurb, + peek = hda.peek, + tool_version= hda.tool_version, + extension = hda.extension, + dbkey = hda.dbkey, + dataset = hda.dataset, + visible = hda.visible, + deleted = hda.deleted, + parent_id = kwargs.get( 'parent_id', None ), + ) + # add_dataset will update the hid to the next avail. in history + if history: + history.add_dataset( copy ) + + copy.copied_from_history_dataset_association = hda + copy.set_size() + + # Need to set after flushed, as MetadataFiles require dataset.id + trans.sa_session.add( copy ) + trans.sa_session.flush() + copy.metadata = hda.metadata + + # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + if not hda.datatype.copy_safe_peek: + copy.set_peek() + + trans.sa_session.flush() + return copy + + def copy_ldda( self, trans, history, ldda, **kwargs ): + return ldda.to_history_dataset_association( history, add_to_history=True ) + +# def by_history_id( self, trans, history_id, filters=None, **kwargs ): +# history_id_filter = self.model_class.history_id == history_id +# filters = self._munge_filters( history_id_filter, filters ) +# return self.list( trans, filters=filters, **kwargs ) +# +# #def by_history( self, trans, history, filters=None, **kwargs ): +# # return history.datasets + + def purge( self, trans, hda ): + self.error_unless_dataset_purge_allowed( trans, hda ) + super( HDAManager, self ).purge( trans, hda, flush=True ) + + # signal to stop the creating job? + if hda.creating_job_associations: + job = hda.creating_job_associations[0].job + job.mark_deleted( self.app.config.track_jobs_in_database ) + self.app.job_manager.job_stop_queue.put( job.id ) + + # more importantly, purge dataset as well + if hda.dataset.user_can_purge: + self.dataset_mgr.purge( trans, hda.dataset ) return hda - def can_access_dataset( self, trans, hda ): - """ - Use security agent to see if current user has access to dataset. - """ - current_user_roles = trans.get_current_user_roles() - return trans.app.security_agent.can_access_dataset( current_user_roles, hda.dataset ) - - #TODO: is_owner, is_accessible - - def check_ownership( self, trans, hda ): - """ - Use history to see if current user owns HDA. - """ - if not trans.user: - #if hda.history == trans.history: - # return hda - raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy datasets", type='error' ) - if trans.user_is_admin(): - return hda - # check for ownership of the containing history and accessibility of the underlying dataset - if( self.histories_mgr.is_owner( trans, hda.history ) - and self.can_access_dataset( trans, hda ) ): - return hda - raise exceptions.ItemOwnershipException( - "HistoryDatasetAssociation is not owned by the current user", type='error' ) - - def check_accessible( self, trans, hda ): - """ - Raise error if HDA is not accessible. - """ - if trans.user and trans.user_is_admin(): - return hda - # check for access of the containing history... - self.histories_mgr.check_accessible( trans, hda.history ) - # ...then the underlying dataset - if self.can_access_dataset( trans, hda ): - return hda - raise exceptions.ItemAccessibilityException( - "HistoryDatasetAssociation is not accessible to the current user", type='error' ) - - def err_if_uploading( self, trans, hda ): + def error_if_uploading( self, trans, hda ): """ Raise error if HDA is still uploading. """ @@ -98,6 +109,8 @@ class HDAManager( manager_base.ModelManager ): raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) return hda + # ......................................................................... serialization + #TODO: move into serializer below def get_hda_dict( self, trans, hda ): """ Return full details of this HDA in dictionary form. @@ -232,10 +245,393 @@ class HDAManager( manager_base.ModelManager ): return display_apps + #def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): + # """ + # Get an HDA object by id performing security checks using + # the current transaction. + # """ + # try: + # dataset_id = trans.security.decode_id( dataset_id ) + # except ( AttributeError, TypeError ): + # # DEPRECATION: We still support unencoded ids for backward compatibility + # try: + # dataset_id = int( dataset_id ) + # except ValueError: + # raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) ) + # + # try: + # data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) ) + # except: + # raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) ) + # + # if check_ownership: + # # Verify ownership. + # user = trans.get_user() + # if not user: + # error( "Must be logged in to manage Galaxy items" ) + # if data.history.user != user: + # error( "%s is not owned by current user" % data.__class__.__name__ ) + # + # if check_accessible: + # current_user_roles = trans.get_current_user_roles() + # + # if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ): + # error( "You are not allowed to access this dataset" ) + # + # if check_state and data.state == trans.model.Dataset.states.UPLOAD: + # return trans.show_error_message( "Please wait until this dataset finishes uploading " + # + "before attempting to view it." ) + # return data + # + #def get_history_dataset_association( self, trans, history, dataset_id, + # check_ownership=True, check_accessible=False, check_state=False ): + # """ + # Get a HistoryDatasetAssociation from the database by id, verifying ownership. + # """ + # #TODO: duplicate of above? alias to above (or vis-versa) + # self.security_check( trans, history, check_ownership=check_ownership, check_accessible=check_accessible ) + # hda = self.get_object( trans, dataset_id, 'HistoryDatasetAssociation', + # check_ownership=False, check_accessible=False ) + # + # if check_accessible: + # if( not trans.user_is_admin() + # and not trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) ): + # error( "You are not allowed to access this dataset" ) + # + # if check_state and hda.state == trans.model.Dataset.states.UPLOAD: + # error( "Please wait until this dataset finishes uploading before attempting to view it." ) + # return hda + # + #def get_history_dataset_association_from_ids( self, trans, id, history_id ): + # # Just to echo other TODOs, there seems to be some overlap here, still + # # this block appears multiple places (dataset show, history_contents + # # show, upcoming history job show) so I am consolodating it here. + # # Someone smarter than me should determine if there is some redundancy here. + # + # # for anon users: + # #TODO: check login_required? + # #TODO: this isn't actually most_recently_used (as defined in histories) + # if( ( trans.user == None ) + # and ( history_id == trans.security.encode_id( trans.history.id ) ) ): + # history = trans.history + # #TODO: dataset/hda by id (from history) OR check_ownership for anon user + # hda = self.get_history_dataset_association( trans, history, id, + # check_ownership=False, check_accessible=True ) + # else: + # #TODO: do we really need the history? + # history = self.get_history( trans, history_id, + # check_ownership=False, check_accessible=True, deleted=False ) + # hda = self.get_history_dataset_association( trans, history, id, + # check_ownership=False, check_accessible=True ) + # return hda + # + #def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ): + # """ + # Returns one or more datasets in a list. + # + # If a dataset is not found or is inaccessible to trans.user, + # add None in its place in the list. + # """ + # # precondtion: dataset_ids is a list of encoded id strings + # hdas = [] + # for id in hda_ids: + # hda = None + # try: + # hda = self.get_dataset( trans, id, + # check_ownership=check_ownership, + # check_accessible=check_accessible, + # check_state=check_state ) + # except Exception: + # pass + # hdas.append( hda ) + # return hdas + # + #def get_data( self, dataset, preview=True ): + # """ + # Gets a dataset's data. + # """ + # # Get data from file, truncating if necessary. + # truncated = False + # dataset_data = None + # if os.path.exists( dataset.file_name ): + # if isinstance( dataset.datatype, Text ): + # max_peek_size = 1000000 # 1 MB + # if preview and os.stat( dataset.file_name ).st_size > max_peek_size: + # dataset_data = open( dataset.file_name ).read(max_peek_size) + # truncated = True + # else: + # dataset_data = open( dataset.file_name ).read(max_peek_size) + # truncated = False + # else: + # # For now, cannot get data from non-text datasets. + # dataset_data = None + # return truncated, dataset_data + # + #def check_dataset_state( self, trans, dataset ): + # """ + # Returns a message if dataset is not ready to be used in visualization. + # """ + # if not dataset: + # return dataset.conversion_messages.NO_DATA + # if dataset.state == trans.app.model.Job.states.ERROR: + # return dataset.conversion_messages.ERROR + # if dataset.state != trans.app.model.Job.states.OK: + # return dataset.conversion_messages.PENDING + # return None + # + #def get_hda_dict( self, trans, hda ): + # """Return full details of this HDA in dictionary form. + # """ + # #precondition: the user's access to this hda has already been checked + # #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) + # expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path + # hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) + # hda_dict[ 'api_type' ] = "file" + # + # # Add additional attributes that depend on trans can hence must be added here rather than at the model level. + # can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) + # can_access_hda = ( trans.user_is_admin() or can_access_hda ) + # if not can_access_hda: + # return self.get_inaccessible_hda_dict( trans, hda ) + # hda_dict[ 'accessible' ] = True + # + # #TODO: I'm unclear as to which access pattern is right + # hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) + # #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) + # + # # ---- return here if deleted AND purged OR can't access + # purged = ( hda.purged or hda.dataset.purged ) + # if hda.deleted and purged: + # #TODO: to_dict should really go AFTER this - only summary data + # return trans.security.encode_dict_ids( hda_dict ) + # + # if expose_dataset_path: + # try: + # hda_dict[ 'file_name' ] = hda.file_name + # except objectstore.ObjectNotFound: + # log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) + # + # hda_dict[ 'download_url' ] = url_for( 'history_contents_display', + # history_id = trans.security.encode_id( hda.history.id ), + # history_content_id = trans.security.encode_id( hda.id ) ) + # + # # resubmitted is not a real state + # hda_dict[ 'resubmitted' ] = False + # if hda.state == trans.app.model.Dataset.states.RESUBMITTED: + # hda_dict[ 'state' ] = hda.dataset.state + # hda_dict[ 'resubmitted' ] = True + # + # # indeces, assoc. metadata files, etc. + # meta_files = [] + # for meta_type in hda.metadata.spec.keys(): + # if isinstance( hda.metadata.spec[ meta_type ].param, FileParameter ): + # meta_files.append( dict( file_type=meta_type ) ) + # if meta_files: + # hda_dict[ 'meta_files' ] = meta_files + # + # # currently, the viz reg is optional - handle on/off + # if trans.app.visualizations_registry: + # hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) + # else: + # hda_dict[ 'visualizations' ] = hda.get_visualizations() + # #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations + # # when the visualizations button is clicked (instead of preloading/pre-checking) + # + # return trans.security.encode_dict_ids( hda_dict ) + # + #def get_inaccessible_hda_dict( self, trans, hda ): + # return trans.security.encode_dict_ids({ + # 'id' : hda.id, + # 'history_id': hda.history.id, + # 'hid' : hda.hid, + # 'name' : hda.name, + # 'state' : hda.state, + # 'deleted' : hda.deleted, + # 'visible' : hda.visible, + # 'accessible': False + # }) + # + #def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): + # return trans.security.encode_dict_ids({ + # 'id' : hda.id if hda else id, + # 'history_id': hda.history.id if hda else history_id, + # 'hid' : hda.hid if hda else '(unknown)', + # 'name' : hda.name if hda else '(unknown)', + # 'error' : error_msg, + # 'state' : trans.model.Dataset.states.NEW + # }) + # + #def get_display_apps( self, trans, hda ): + # display_apps = [] + # for display_app in hda.get_display_applications( trans ).itervalues(): + # + # app_links = [] + # for link_app in display_app.links.itervalues(): + # app_links.append({ + # 'target': link_app.url.get( 'target_frame', '_blank' ), + # 'href' : link_app.get_display_url( hda, trans ), + # 'text' : gettext( link_app.name ) + # }) + # if app_links: + # display_apps.append( dict( label=display_app.name, links=app_links ) ) + # + # return display_apps + # + #def get_old_display_applications( self, trans, hda ): + # display_apps = [] + # if not trans.app.config.enable_old_display_applications: + # return display_apps + # + # for display_app in hda.datatype.get_display_types(): + # target_frame, display_links = hda.datatype.get_display_links( hda, + # display_app, trans.app, trans.request.base ) + # + # if len( display_links ) > 0: + # display_label = hda.datatype.get_display_label( display_app ) + # + # app_links = [] + # for display_name, display_link in display_links: + # app_links.append({ + # 'target': target_frame, + # 'href' : display_link, + # 'text' : gettext( display_name ) + # }) + # if app_links: + # display_apps.append( dict( label=display_label, links=app_links ) ) + # + # return display_apps + # + #def set_hda_from_dict( self, trans, hda, new_data ): + # """ + # Changes HDA data using the given dictionary new_data. + # """ + # # precondition: access of the hda has already been checked + # + # # send what we can down into the model + # changed = hda.set_from_dict( new_data ) + # # the rest (often involving the trans) - do here + # if 'annotation' in new_data.keys() and trans.get_user(): + # hda.add_item_annotation( trans.sa_session, trans.get_user(), hda, new_data[ 'annotation' ] ) + # changed[ 'annotation' ] = new_data[ 'annotation' ] + # if 'tags' in new_data.keys() and trans.get_user(): + # self.set_tags_from_list( trans, hda, new_data[ 'tags' ], user=trans.user ) + # # sharing/permissions? + # # purged + # + # if changed.keys(): + # trans.sa_session.flush() + # + # return changed + # + #def get_hda_job( self, hda ): + # # Get dataset's job. + # job = None + # for job_output_assoc in hda.creating_job_associations: + # job = job_output_assoc.job + # break + # return job + # + #def stop_hda_creating_job( self, hda ): + # """ + # Stops an HDA's creating job if all the job's other outputs are deleted. + # """ + # if hda.parent_id is None and len( hda.creating_job_associations ) > 0: + # # Mark associated job for deletion + # job = hda.creating_job_associations[0].job + # if job.state in [ self.app.model.Job.states.QUEUED, self.app.model.Job.states.RUNNING, self.app.model.Job.states.NEW ]: + # # Are *all* of the job's other output datasets deleted? + # if job.check_if_output_datasets_deleted(): + # job.mark_deleted( self.app.config.track_jobs_in_database ) + # self.app.job_manager.job_stop_queue.put( job.id ) + # + # # ============================================================================= -class HistorySerializer( manager_base.ModelSerializer ): - """ - Interface/service object for serializing HDAs into dictionaries. - """ - pass +class HDASerializer( base.ModelSerializer ): + + def __init__( self ): + super( HDASerializer, self ).__init__() + + # most of these views build/add to the previous view + summary_view = [ + 'id', 'history_id', 'hid', 'name', 'dataset_id', + 'state', 'deleted', 'purged', 'visible' + ] + detailed_view = summary_view + [ + 'dbkey', 'info', 'blurb', 'extension', 'create_time', 'update_time', + 'copied_from_history_dataset_association_id', 'copied_from_library_dataset_dataset_association_id', + 'metadata', 'size', + #mixin: annotatable + 'annotation', + #mixin: taggable + 'tags', + ] + extended_view = detailed_view + [ + #NOTE: sending raw 'peek' (not get_display_peek) + 'peek', + 'tool_version', 'parent_id', 'designation', + ] + + self.serializable_keys = extended_view + [ + 'display_apps', + 'display_types', + 'visualizations' + ] + self.views = { + 'summary' : summary_view, + 'detailed' : detailed_view, + 'extended' : extended_view, + } + self.default_view = 'summary' + + def add_serializers( self ): + self.serializers.update({ + 'id' : self.serialize_id, + 'history_id' : self.serialize_id, + 'dataset_id' : self.serialize_id, + + 'create_time' : self.serialize_date, + 'update_time' : self.serialize_date, + 'copied_from_history_dataset_association_id' : self.serialize_id, + 'copied_from_library_dataset_dataset_association_id': self.serialize_id, + 'metadata' : self.serialize_metadata, + 'size' : lambda t, i, k: int( i.get_size() ), + 'nice_size' : lambda t, i, k: i.get_size( nice_size=True ), + 'parent_id' : self.serialize_id, + #TODO: to mixin: annotatable + 'annotation' : lambda t, i, k: i.get_item_annotation_str( t.sa_session, t.user, i ), + #TODO: to mixin: taggable + 'tags' : lambda t, i, k: [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) + for tag in i.tags ], + }) + + def serialize_metadata( self, trans, hda, key ): + not_included = [ 'dbkey' ] + md = {} + for name, spec in hda.metadata.spec.items(): + if name in not_included: + continue + val = hda.metadata.get( name ) + #NOTE: no files + if isinstance( val, model.MetadataFile ): + continue + #TODO:? possibly split this off? + # If no value for metadata, look in datatype for metadata. + elif val is None and hasattr( hda.datatype, name ): + val = getattr( hda.datatype, name ) + md[ name ] = val + + return md + + def add_deserializers( self ): + self.deserializers.update({ + 'name' : self.deserialize_basestring, + 'genome_build' : self.deserialize_genome_build, + 'deleted' : self.deserialize_bool, + 'published' : self.deserialize_bool, + #TODO: to mixin: annotatable + #'annotation' : self.deserialize_annotation, + #TODO: to mixin: taggable + #'tags' : lambda t, i, k, v: , + }) + self.deserializable_keys = self.deserializers.keys() diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index 068a5d2eeb1..ed361ef068f 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -6,9 +6,12 @@ created (or copied) by users over the course of an analysis. """ from galaxy import exceptions +from galaxy import model from galaxy.model import orm -from galaxy.managers import base as manager_base +import base +import sharable +import hdas import galaxy.managers.collections_util import galaxy.web @@ -18,122 +21,76 @@ log = logging.getLogger( __name__ ) # ============================================================================= -class HistoryManager( manager_base.ModelManager ): +class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface ): """ Interface/service object for interacting with HDAs. """ + model_class = model.History + default_order_by = ( model.History.create_time, ) + foreign_key_name = 'history' + user_share_model = model.HistoryUserShareAssociation - #TODO: all the following would be more useful if passed the user instead of defaulting to trans.user - def __init__( self, *args, **kwargs ): - super( HistoryManager, self ).__init__( *args, **kwargs ) + def __init__( self, app, *args, **kwargs ): + super( HistoryManager, self ).__init__( app, *args, **kwargs ) + self.hda_mgr = hdas.HDAManager( app ) - def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True, deleted=None ): - """ - Get a History from the database by id, verifying ownership. - """ - # this is a replacement for UsesHistoryMixin because mixins are a bad soln/structure - history = trans.sa_session.query( trans.app.model.History ).get( unencoded_id ) - if history is None: - raise exceptions.ObjectNotFound() - if deleted is True and not history.deleted: - raise exceptions.ItemDeletionException( 'History "%s" is not deleted' % ( history.name ), type="error" ) - elif deleted is False and history.deleted: - raise exceptions.ItemDeletionException( 'History "%s" is deleted' % ( history.name ), type="error" ) + # ......................................................................... crud + def copy( self, trans, history, user, **kwargs ): + return history.copy( target_user=user, **kwargs ) - history = self.secure( trans, history, check_ownership, check_accessible ) + def get_current( self, trans ): + return trans.get_history() + + def set_current( self, trans, history ): + trans.set_history( history ) return history - def by_user( self, trans, user=None, include_deleted=False, only_deleted=False ): + def set_current_by_id( self, trans, history_id ): + return self.set_current( trans, self.by_id( trans, history_id ) ) + + def most_recent( self, trans, user, **kwargs ): + if not user: + return None if trans.history.deleted else trans.history + desc_update_time = self.model_class.table.c.update_time + return self.by_user( trans, user, order_by=desc_update_time, limit=1, **kwargs ) + + # ......................................................................... sharable + # overriding to handle anonymous users' current histories in both cases + def by_user( self, trans, user, **kwargs ): """ - Get all the histories for a given user (defaulting to `trans.user`) - ordered by update time and filtered on whether they've been deleted. + Get all the histories for a given user (allowing anon users' theirs) + ordered by update time. """ # handle default and/or anonymous user (which still may not have a history yet) - user = user or trans.user - if not user: + if self.user_mgr.is_anonymous( user ): current_history = trans.get_history() return [ current_history ] if current_history else [] - history_model = trans.model.History - query = ( trans.sa_session.query( history_model ) - .filter( history_model.user == user ) - .order_by( orm.desc( history_model.table.c.update_time ) ) ) - if only_deleted: - query = query.filter( history_model.deleted == True ) - elif not include_deleted: - query = query.filter( history_model.deleted == False ) - return query.all() + return super( HistoryManager, self ).by_user( trans, user, **kwargs ) - def secure( self, trans, history, check_ownership=True, check_accessible=True ): - """ - Checks if (a) user owns item or (b) item is accessible to user. - """ - # all items are accessible to an admin - if trans.user and trans.user_is_admin(): - return history - if check_ownership: - history = self.check_ownership( trans, history ) - if check_accessible: - history = self.check_accessible( trans, history ) - return history - - def is_current( self, trans, history ): - """ - True if the given history is the user's current history. - - Returns False if the session has no current history. - """ - if trans.history is None: - return False - return trans.history == history - - def is_owner( self, trans, history ): + def is_owner( self, trans, history, user ): """ True if the current user is the owner of the given history. """ # anon users are only allowed to view their current history - if not trans.user: - return self.is_current( trans, history ) - return trans.user == history.user - - def check_ownership( self, trans, history ): - """ - Raises error if the current user is not the owner of the history. - """ - if trans.user and trans.user_is_admin(): - return history - if not trans.user and not self.is_current( trans, history ): - raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy histories", type='error' ) - if self.is_owner( trans, history ): - return history - raise exceptions.ItemOwnershipException( "History is not owned by the current user", type='error' ) - - def is_accessible( self, trans, history ): - """ - True if the user can access (read) the current history. - """ - # admin always have access - if trans.user and trans.user_is_admin(): + if self.user_mgr.is_anonymous( user ) and history == trans.get_history(): return True - # owner has implicit access - if self.is_owner( trans, history ): - return True - # importable and shared histories are always accessible - if history.importable: - return True - if trans.user in history.users_shared_with_dot_users: - return True - return False + return super( HistoryManager, self ).is_owner( trans, history, user ) - def check_accessible( self, trans, history ): - """ - Raises error if the current user can't access the history. - """ - if self.is_accessible( trans, history ): - return history - raise exceptions.ItemAccessibilityException( "History is not accessible to the current user", type='error' ) + # ......................................................................... purgable + def purge( self, trans, history, flush=True, **kwargs ): + self.error_unless_dataset_purge_allowed( trans, history ) - #TODO: bleh... + # First purge all the datasets + for hda in history.datasets: + if not hda.purged: + self.hda_mgr.purge( trans, hda, flush=True ) + + # Now mark the history as purged + super( HistoryManager, self ).purge( trans, history, flush=flush, **kwargs ) + + # ......................................................................... serialization + #TODO: move to serializer def _get_history_data( self, trans, history ): """ Returns a dictionary containing ``history`` and ``contents``, serialized @@ -191,53 +148,174 @@ class HistoryManager( manager_base.ModelManager ): 'contents' : contents_dictionaries } - def get_history_dict( self, trans, history, contents_dictionaries=None ): + # remove this + def get_state_counts( self, trans, history, exclude_deleted=True, exclude_hidden=False ): """ - Returns history data in the form of a dictionary. """ - #TODO: to serializer - history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id }) - history_dict[ 'user_id' ] = None - if history.user_id: - history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id ) + #TODO: the default flags above may not make a lot of sense (T,T?) + state_counts = {} + for state in trans.app.model.Dataset.states.values(): + state_counts[ state ] = 0 - history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True ) - history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, history.user, history ) - if not history_dict[ 'annotation' ]: - history_dict[ 'annotation' ] = '' + #TODO:?? collections and coll. states? + for hda in history.datasets: + if exclude_deleted and hda.deleted: + continue + if exclude_hidden and not hda.visible: + continue + state_counts[ hda.state ] = state_counts[ hda.state ] + 1 + return state_counts - #TODO: item_slug url - if history_dict[ 'importable' ] and history_dict[ 'slug' ]: - username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] )) - history_dict[ 'username_and_slug' ] = username_and_slug + # remove this + def get_state_ids( self, trans, history ): + """ + """ + state_ids = {} + for state in trans.app.model.Dataset.states.values(): + state_ids[ state ] = [] -#TODO: re-add - #hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history ) - ##TODO remove the following in v2 - #( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries ) - #history_dict[ 'state_details' ] = state_counts - #history_dict[ 'state_ids' ] = state_ids - #history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts ) + #TODO:?? collections and coll. states? + for hda in history.datasets: + #TODO: do not encode ids at this layer + encoded_id = trans.security.encode_id( hda.id ) + state_ids[ hda.state ].append( encoded_id ) + return state_ids - return history_dict + #TODO: remove this (is state used/useful?) + def get_history_state( self, trans, history ): + """Returns the history state based on the states of the HDAs it contains. + """ + states = trans.app.model.Dataset.states - def most_recent( self, trans, user=None, deleted=False ): - user = user or trans.user - if not user: - return None if trans.history.deleted else trans.history + # (default to ERROR) + state = states.ERROR - #TODO: dup with by_user - should return query from there and call first and not all - history_model = trans.model.History - query = ( trans.sa_session.query( history_model ) - .filter( history_model.user == user ) - .order_by( orm.desc( history_model.table.c.update_time ) ) ) - if not deleted: - query = query.filter( history_model.deleted == False ) - return query.first() + #TODO: history_state and state_counts are classically calc'd at the same time + # so this is rel. ineff. - if we keep this... + hda_state_counts = self.get_state_counts( trans, history, exclude_deleted=False ) + num_hdas = sum( hda_state_counts.values() ) + if num_hdas == 0: + state = states.NEW -# ============================================================================= -class HistorySerializer( manager_base.ModelSerializer ): + else: + if( ( hda_state_counts[ states.RUNNING ] > 0 ) + or ( hda_state_counts[ states.SETTING_METADATA ] > 0 ) + or ( hda_state_counts[ states.UPLOAD ] > 0 ) ): + state = states.RUNNING + #TODO: this method may be more useful if we *also* polled the histories jobs here too + + elif hda_state_counts[ states.QUEUED ] > 0: + state = states.QUEUED + + elif( ( hda_state_counts[ states.ERROR ] > 0 ) + or ( hda_state_counts[ states.FAILED_METADATA ] > 0 ) ): + state = states.ERROR + + elif hda_state_counts[ states.OK ] == num_hdas: + state = states.OK + + return state + + +## ============================================================================= +class HistorySerializer( base.ModelSerializer ): """ Interface/service object for serializing histories into dictionaries. """ - pass + + def __init__( self, app ): + super( HistorySerializer, self ).__init__( app ) + self.history_mgr = HistoryManager( app ) + + summary_view = [ + 'id', + 'model_class', + 'name', + 'deleted', 'purged', + 'published', + #'count' + 'annotation', + 'tags', + 'url' + ] + # in the Historys' case, each of these views includes the keys from the previous + detailed_view = summary_view + [ + 'contents_url', + #'hdas', + 'empty', + 'size', 'nice_size', + 'user_id', + 'create_time', 'update_time', + 'importable', 'slug', + 'genome_build', + #username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] )) + #TODO: remove the next three - instead getting the same info from the 'hdas' list + 'state', + 'state_details', + 'state_ids', + ] + extended_view = detailed_view + [ + ] + self.serializable_keys = extended_view + [] + self.views = { + 'summary' : summary_view, + 'detailed' : detailed_view, + 'extended' : extended_view, + } + self.default_view = 'summary' + + #assumes: outgoing to json.dumps and sanitized + def add_serializers( self ): + super( HistorySerializer, self ).add_serializers() + self.serializers.update({ + 'model_class' : lambda *a: 'History', + 'id' : self.serialize_id, + 'count' : lambda trans, item, key: len( item.datasets ), + 'create_time' : self.serialize_date, + 'update_time' : self.serialize_date, + 'user_id' : self.serialize_id, + 'hdas' : lambda t, i, k: [ t.security.encode_id( hda.id ) for hda in i.datasets ], + 'size' : lambda t, i, k: int( i.get_disk_size() ), + 'nice_size' : lambda t, i, k: i.get_disk_size( nice_size=True ), + #TODO: to mixins: annotatable, taggable + 'annotation' : lambda t, i, k: i.get_item_annotation_str( t.sa_session, t.user, i ), + 'tags' : lambda t, i, k: [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) + for tag in i.tags ], + 'url' : lambda t, i, k: galaxy.web.url_for( 'history', id=t.security.encode_id( i.id ) ), + 'contents_url' : lambda t, i, k: + galaxy.web.url_for( 'history_contents', history_id=t.security.encode_id( i.id ) ), + 'empty' : lambda t, i, k: len( i.datasets ) <= 0, + + 'state' : lambda t, i, k: self.history_mgr.get_history_state( t, i ), + 'state_details' : lambda t, i, k: self.history_mgr.get_state_counts( t, i ), + 'state_ids' : lambda t, i, k: self.history_mgr.get_state_ids( t, i ), + }) + +## ============================================================================= +class HistoryDeserializer( base.ModelDeserializer ): + """ + Interface/service object for validating and deserializing dictionaries into histories. + """ + + def __init__( self, app ): + super( HistoryDeserializer, self ).__init__( app ) + self.history_mgr = HistoryManager( app ) + + #assumes: incoming from json.loads and sanitized + def add_deserializers( self ): + super( HistoryDeserializer, self ).add_deserializers() + self.deserializers.update({ + 'name' : self.deserialize_basestring, + 'genome_build' : self.deserialize_genome_build, + # mixin: annotatable + 'annotation' : self.deserialize_annotation, + # mixin: taggable + 'tags' : self.deserialize_tags, + + # mixin: deletable + 'deleted' : self.deserialize_bool, + # sharable + 'published' : self.deserialize_bool, + 'importable' : self.deserialize_bool, + }) + self.deserializable_keys = self.deserializers.keys() diff --git a/lib/galaxy/managers/lddas.py b/lib/galaxy/managers/lddas.py index af1bac40c33..db23dd58da2 100644 --- a/lib/galaxy/managers/lddas.py +++ b/lib/galaxy/managers/lddas.py @@ -5,7 +5,7 @@ class LDDAManager( manager_base.ModelManager ): """ A fairly sparse manager for LDDAs. """ - def __init__( self ): + def __init__( self, app ): """ Set up and initialize other managers needed by lddas. """ diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py new file mode 100644 index 00000000000..532c233738a --- /dev/null +++ b/lib/galaxy/managers/sharable.py @@ -0,0 +1,289 @@ +""" +""" +import re + +from galaxy import exceptions + +import base +import users + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class SharableModelManager( base.ModelManager, base.AccessibleModelInterface ): + # superclass that uses its importable, plublished, or user_share_associations to check accessibility + # histories, pages, stored workflows, visualizations + user_share_model = None + + def __init__( self, app ): + super( SharableModelManager, self ).__init__( app ) + assert hasattr( self.model_class, 'user_id' ), ( + 'No user_id on Sharable model: %s' %( self.model_class_name ) ) + + assert self.user_share_model, ( + 'No UserShareAssociation on Sharable model: %s' %( self.model_class.__name__ ) ) + assert hasattr( self.model_class, 'users_shared_with_dot_users' ), ( + 'No users_shared_with_dot_users on Sharable model: %s' %( self.model_class.__name__ ) ) + +#TODO: libraries do not have importable + assert hasattr( self.model_class, 'importable' ), ( + 'No users_shared_with_dot_users on Sharable model: %s' %( self.model_class.__name__ ) ) + assert hasattr( self.model_class, 'published' ), ( + 'No published on Sharable model: %s' %( self.model_class.__name__ ) ) + + # user manager is needed to check access/ownership/admin + self.user_mgr = users.UserManager( app ) + + # ......................................................................... has a user + def _query_by_user( self, trans, user, filters=None, **kwargs ): + user_filter = self.model_class.user_id == user.id + filters=self._munge_filters( user_filter, filters ) + return self.query( trans, filters=filters, **kwargs ) + + def by_user( self, trans, user, filters=None, **kwargs ): + user_filter = self.model_class.user_id == user.id + filters=self._munge_filters( user_filter, filters ) + #note: no ownership check needed + return self.list( trans, filters=filters, **kwargs ) + + # ......................................................................... owned model interface +# this really is more of a 'check_*editable/write*' - in essence that's what check_ownership boils down to often + def ownership_by_id( self, trans, id, user, **kwargs ): + item = super( SharableModelManager, self ).by_id( trans, id, **kwargs ) + return self.error_unless_owner( trans, item, user ) + + def is_owner( self, trans, item, user ): + if self.user_mgr.is_anonymous( user ): + # note: this needs to be overridden in the case of anon users and session.current_history + return False + if self.user_mgr.is_admin( trans, user ): + return True + return item.user == user + + def error_unless_owner( self, trans, item, user ): + if self.is_owner( trans, item, user ): + return item + raise exceptions.ItemOwnershipException( "%s is not owned by user" % ( self.model_class_name ) ) + + def list_owned( self, trans, user, **kwargs ): + items = base.ModelManager.list( self, trans, **kwargs ) + return [ self.error_unless_owner( trans, item, user ) for item in items ] + + def filter_owned( self, trans, user, **kwargs ): + pass + + # ......................................................................... accessible interface + def is_accessible( self, trans, item, user ): + if item.importable: + return True + # note: owners always have access - checking for accessible implicitly checks for ownership + if self.is_owner( trans, item, user ): + return True + if self.user_mgr.is_anonymous( user ): + return False + if user in item.users_shared_with_dot_users: + return True + return False + + # ......................................................................... importable + def make_importable( self, trans, item, flush=True ): + """ + Makes item accessible--viewable and importable--and sets item's slug. + Does not flush/commit changes, however. Item must have name, user, + importable, and slug attributes. + """ + trans.sa_session.add( item ) + item.importable = True + self.create_unique_slug( trans, item, flush=False ) + if flush: + trans.sa_session.flush() + return item + + def make_non_importable( self, trans, item, flush=True ): + """ + Makes item accessible--viewable and importable--and sets item's slug. + Does not flush/commit changes, however. Item must have name, user, + importable, and slug attributes. + """ + #if item.published: + # if unpublish: + # self.unpublish( trans, item, flush=False ) + # else: + # raise exceptions.BadRequest( 'Item must be non-published to be inaccessible', item=item ) + trans.sa_session.add( item ) + item.importable = False + if flush: + trans.sa_session.flush() + return item + + def list_importable( self, trans, **kwargs ): +#TODO: + pass + + # ......................................................................... published + #def publish( self, trans, item, make_accessible=True, flush=True ): + # if not item.importable: + # if make_accessible: + # self.make_accessible( trans, item, flush=False ) + # else: + # raise exceptions.RequestParameterInvalidException( 'Item must be importable to be published', + # item=str( item ) ) + # a published item is also/already importable(=True) implicitly + def publish( self, trans, item, flush=True ): + if not item.importable: + self.make_importable( trans, item, flush=False ) + trans.sa_session.add( item ) + item.published = True + if flush: + trans.sa_session.flush() + return item + + def unpublish( self, trans, item, flush=True ): + trans.sa_session.add( item ) + item.published = False + if flush: + trans.sa_session.flush() + return item + + def list_published( self, trans, **kwargs ): +#TODO: + pass + + # ......................................................................... user sharing + # sharing is often done via a 3rd table btwn a User and an item -> a UserShareAssociation + def get_share_assocs( self, trans, item, user=None ): + query = self.query_associated( trans, self.user_share_model, item ) + if user is not None: + query = query.filter_by( user=user ) + return query.all() + + def share_with( self, trans, item, user, flush=True ): + # allow user to be a list and call recursivly + if isinstance( user, list ): + return map( lambda user: self.share_with( trans, item, user, flush=False ), user ) + + share = self.user_share_model() + trans.sa_session.add( share ) + self.associate( trans, share, item ) + share.user = user + self.create_unique_slug( trans, item ) + if flush: + trans.sa_session.flush() + return item + + def unshare_with( self, trans, item, user, flush=True ): + if isinstance( user, list ): + return map( lambda user: self.unshare_with( trans, item, user, flush=False ), user ) + # Look for and delete sharing relation for history-user. + for assoc in self.get_share_assocs( trans, item, user=user ): + trans.sa_session.delete( assoc ) + if flush: + trans.sa_session.flush() + return item + + def list_shared_with( self, trans, item, user, **kwargs ): +#TODO: + pass + + # ......................................................................... slugs + # slugs are human readable strings often used to link to sharable resources (replacing ids) + def is_valid_slug( self, slug ): + """ + Returns true if slug is valid. + """ + VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" ) + return VALID_SLUG_RE.match( slug ) + + def set_slug( self, trans, item, new_slug, flush=True ): + if not self.is_valid_slug( new_slug ): + raise exceptions.RequestParameterInvalidException( "Invalid slug", slug=new_slug ) + + # error if slug is already in use + existing_match = ( trans.sa_session.query( self.model_class ) + .filter_by( user=item.user, slug=new_slug, importable=True ) ) + if( existing_match.count() != 0 ): + raise exceptions.BadRequest( "Slug already exists", slug=new_slug ) + + item.slug = new_slug + if flush: + trans.sa_session.flush() + return item + + def get_unique_slug( self, trans, item ): + """ + Returns a slug that is unique among user's importable items + for item's class. + """ + cur_slug = item.slug + + # Setup slug base. + if cur_slug is None or cur_slug == "": + # Item can have either a name or a title. + if hasattr( item, 'name' ): + item_name = item.name + elif hasattr( item, 'title' ): + item_name = item.title + # Replace whitespace with '-' + slug_base = re.sub( "\s+", "-", item_name.lower() ) + # Remove all non-alphanumeric characters. + slug_base = re.sub( "[^a-zA-Z0-9\-]", "", slug_base ) + # Remove trailing '-'. + if slug_base.endswith('-'): + slug_base = slug_base[:-1] + else: + slug_base = cur_slug + + # Using slug base, find a slug that is not taken. If slug is taken, + # add integer to end. + new_slug = slug_base + count = 1 + while ( trans.sa_session.query( item.__class__ ) + .filter_by( user=item.user, slug=new_slug, importable=True ) + .count() != 0 ): + # Slug taken; choose a new slug based on count. This approach can + # handle numerous items with the same name gracefully. + new_slug = '%s-%i' % ( slug_base, count ) + count += 1 + + return new_slug + + def create_unique_slug( self, trans, item, flush=True ): + item.slug = self.get_unique_slug( trans, item ) + trans.sa_session.add( item ) + if flush: + trans.sa_session.flush() + return item + + def by_slug( self, trans, user, **kwargs ): + pass + + # ......................................................................... display + #def display_by_username_and_slug( self, trans, username, slug ): + # """ Display item by username and slug. """ + + #def set_public_username( self, trans, id, username, **kwargs ): + # """ Set user's public username and delegate to sharing() """ + #def sharing( self, trans, id, **kwargs ): + # """ Handle item sharing. """ + #def get_name_and_link_async( self, trans, id=None ): + # """ Returns item's name and link. """ + + +# ============================================================================= +class SharableModelDeserializer( base.ModelDeserializer ): + + def deserialize_published( self, trans, item, val ): + #TODO: call manager.publish/unpublish + pass + + def deserialize_importable( self, trans, item, val ): + #TODO: call manager.make_importable/non_importable + pass + + def deserialize_slug( self, trans, item, val ): + #TODO: call manager.set_slug + pass + + #def deserialize_user_shares(): diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py new file mode 100644 index 00000000000..decbe8dc335 --- /dev/null +++ b/lib/galaxy/managers/users.py @@ -0,0 +1,175 @@ +""" +""" +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +import api_keys + +import base + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class UserManager( base.ModelManager ): + model_class = model.User + default_order_by = ( model.User.create_time, ) + foreign_key_name = 'user' + + def create( self, trans, **kwargs ): + #TODO: deserialize and validate here + email = kwargs[ 'email' ] + username = kwargs[ 'username' ] + password = kwargs[ 'password' ] + self._error_on_duplicate_email( trans, email ) + + user = model.User( email=email, password=password ) + user.username = username + + if trans.app.config.user_activation_on: + user.active = False + else: + # Activation is off, every new user is active by default. + user.active = True + + trans.sa_session.add( user ) + try: + trans.sa_session.flush() + #TODO:?? flush needed for permissions below? If not, make optional + except sqlalchemy.exceptions.IntegrityError, db_err: + raise exceptions.Conflict( db_err.message ) + + # can throw an sqlalx.IntegrityError if username not unique + + trans.app.security_agent.create_private_user_role( user ) + if trans.webapp.name == 'galaxy': + # We set default user permissions, before we log in and set the default history permissions + permissions = trans.app.config.new_user_dataset_access_role_default_private + trans.app.security_agent.user_set_default_permissions( user, default_access_private=permissions ) + return user + + #TODO: incorporate security/validate_user_input.py + + def _error_on_duplicate_email( self, trans, email ): + #TODO: remove this check when unique=True is added to the email column + if self.by_email( trans, email ) is not None: + raise exceptions.Conflict( 'Email must be unique', email=email ) + + # ------------------------------------------------------------------------- filters + def by_email( self, trans, email, filters=None, **kwargs ): + filters = self._munge_filters( self.model_class.email == email, filters ) + try: + return super( UserManager, self ).one( trans, filters=filters, **kwargs ) + except exceptions.ObjectNotFound, not_found: + return None + + def by_email_like( self, trans, email_with_wildcards, filters=None, order_by=None, **kwargs ): + filters = self._munge_filters( self.model_class.email.like( email_with_wildcards ), filters ) + order_by = order_by or ( model.User.email, ) + return super( UserManager, self ).list( trans, filters=filters, order_by=order_by, **kwargs ) + + # ------------------------------------------------------------------------- admin + def is_admin( self, trans, user ): + admin_emails = self._admin_emails( trans ) + return user and admin_emails and user.email in admin_emails + + def _admin_emails( self, trans ): + return [ x.strip() for x in trans.app.config.get( "admin_users", "" ).split( "," ) ] + + def admins( self, trans, filters=None, **kwargs ): + filters = self._munge_filters( self.model_class.email.in_( self._admin_emails( trans ) ), filters ) + return super( UserManager, self ).list( trans, filters=filters, **kwargs ) + + def error_unless_admin( self, trans, user, msg="Administrators only", **kwargs ): + if not self.is_admin( trans, user ): + raise exceptions.AdminRequiredException( msg, **kwargs ) + return user + + # ------------------------------------------------------------------------- anonymous + def is_anonymous( self, user ): + # define here for single point of change + return user is None + + def error_if_anonymous( self, trans, user, msg="Log-in required", **kwargs ): + print 'error_if_anonymous', user + if user is None: + #TODO: code is correct (403), but should be named AuthenticationRequired (401 and 403 are flipped) + raise exceptions.AuthenticationFailed( msg, **kwargs ) + return user + + # ------------------------------------------------------------------------- current + def is_current_user( self, trans, user ): + return user == trans.user + + def is_current_user_anonymous( self, trans ): + return trans.user is None + + def is_current_user_admin( self, trans ): + return self.is_admin( trans, trans.user ) + + # ------------------------------------------------------------------------- ? + #def tags( self, trans, **kwargs ): + # # return all tags from this user + # pass + + #def annotations( self, trans, **kwargs ): + # # return all annotations from this user + # pass + + # ------------------------------------------------------------------------- api keys + def create_api_key( self, trans, user ): + return api_keys.ApiKeyManager( trans.app ).create_api_key( user ) + + # needed? + def api_key( self, trans, user ): + #TODO: catch NoResultFound + return trans.sa_session.query( model.APIKeys ).filter_by( user=user ).one() + + #TODO: there is quite a bit of functionality around the user (authentication, quotas, groups/roles) + # most of which it may be unneccessary to have here + + # ------------------------------------------------------------------------- roles + def private_role( self, trans, user ): + #TODO: not sure we need to go through sec agent... it's just the first role of type private + return trans.app.security_agent.get_private_user_role( user ) + + +# ============================================================================= +class UserSerializer( base.ModelSerializer ): + + def __init__( self ): + super( UserSerializer, self ).__init__() + + summary_view = [ + 'id', 'email', 'username' + ] + # in the Historys' case, each of these views includes the keys from the previous + detailed_view = summary_view + [ + 'update_time', 'create_time', + 'total_disk_usage', 'nice_total_disk_usage', + 'deleted', 'purged', + 'active' + ] + extended_view = detailed_view + [ + #'preferences', + #'tags', + #'annotations' + ] + self.serializable_keys = extended_view + self.views = { + 'summary' : summary_view, + 'detailed' : detailed_view, + 'extended' : extended_view, + } + self.default_view = 'summary' + + def add_serializers( self ): + self.serializers.update({ + 'id' : self.serialize_id, + 'create_time' : self.serialize_date, + 'update_time' : self.serialize_date, + }) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index fddd941df44..f2fffff6d96 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1327,6 +1327,14 @@ class Dataset( object ): FAILED_METADATA = 'failed_metadata', RESUBMITTED = 'resubmitted' ) # failed_metadata and resubmitted are only valid as DatasetInstance states currently + + non_ready_states = ( + states.UPLOAD, + states.QUEUED, + states.RUNNING, + states.SETTING_METADATA + ) + ready_states = tuple( set( states.__dict__.values() ) - set( non_ready_states ) ) conversion_messages = Bunch( PENDING = "pending", NO_DATA = "no data", @@ -1356,6 +1364,9 @@ class Dataset( object ): else: self.uuid = UUID(str(uuid)) + def in_ready_state( self ): + return self.state in self.ready_states + def get_file_name( self ): if not self.external_filename: assert self.id is not None, "ID must be set before filename used (commit the object)" diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 80920fbbd61..fc6d61561c1 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -131,6 +131,7 @@ model.Dataset.table = Table( "dataset", metadata, Column( 'total_size', Numeric( 15, 0 ) ), Column( 'uuid', UUIDType() ) ) +# hda read access permission given by a user to a specific site (gen. for external display applications) model.HistoryDatasetAssociationDisplayAtAuthorization.table = Table( "history_dataset_association_display_at_authorization", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -972,10 +973,10 @@ model.Page.table = Table( "page", metadata, Column( "latest_revision_id", Integer, ForeignKey( "page_revision.id", use_alter=True, name='page_latest_revision_id_fk' ), index=True ), Column( "title", TEXT ), - Column( "slug", TEXT, unique=True, index=True ), - Column( "importable", Boolean, index=True, default=False ), - Column( "published", Boolean, index=True, default=False ), Column( "deleted", Boolean, index=True, default=False ), + Column( "importable", Boolean, index=True, default=False ), + Column( "slug", TEXT, unique=True, index=True ), + Column( "published", Boolean, index=True, default=False ), ) model.PageRevision.table = Table( "page_revision", metadata, diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 578ea61360b..0dccd69f4e4 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -280,6 +280,7 @@ class UsesHistoryMixin( SharableItemSecurityMixin ): query = query.filter( history_model.deleted == False ) return query.all() + #USED IN: history.py, 640 def get_history_datasets( self, trans, history, show_deleted=False, show_hidden=False, show_purged=False ): """ Returns history's datasets. """ @@ -295,215 +296,6 @@ class UsesHistoryMixin( SharableItemSecurityMixin ): query = query.filter( trans.model.Dataset.purged == False ) return query.all() - def get_hda_state_counts( self, trans, history, include_deleted=False, include_hidden=False ): - """ - Returns a dictionary with state counts for history's HDAs. Key is a - dataset state, value is the number of states in that count. - """ - # Build query to get (state, count) pairs. - cols_to_select = [ trans.app.model.Dataset.table.c.state, func.count( '*' ) ] - from_obj = trans.app.model.HistoryDatasetAssociation.table.join( trans.app.model.Dataset.table ) - - conditions = [ trans.app.model.HistoryDatasetAssociation.table.c.history_id == history.id ] - if not include_deleted: - # Only count datasets that have not been deleted. - conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.deleted == False ) - if not include_hidden: - # Only count datasets that are visible. - conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.visible == True ) - - group_by = trans.app.model.Dataset.table.c.state - query = select( columns=cols_to_select, - from_obj=from_obj, - whereclause=and_( *conditions ), - group_by=group_by ) - - # Initialize count dict with all states. - state_count_dict = {} - for state in trans.app.model.Dataset.states.values(): - state_count_dict[ state ] = 0 - - # Process query results, adding to count dict. - for row in trans.sa_session.execute( query ): - state, count = row - state_count_dict[ state ] = count - - return state_count_dict - - def get_hda_summary_dicts( self, trans, history ): - """Returns a list of dictionaries containing summary information - for each HDA in the given history. - """ - hda_model = trans.model.HistoryDatasetAssociation - - # get state, name, etc. - columns = ( hda_model.name, hda_model.hid, hda_model.id, hda_model.deleted, - trans.model.Dataset.state ) - column_keys = [ "name", "hid", "id", "deleted", "state" ] - - query = ( trans.sa_session.query( *columns ) - .enable_eagerloads( False ) - .filter( hda_model.history == history ) - .join( trans.model.Dataset ) - .order_by( hda_model.hid ) ) - - # build dictionaries, adding history id and encoding all ids - hda_dicts = [] - for hda_tuple in query.all(): - hda_dict = dict( zip( column_keys, hda_tuple ) ) - hda_dict[ 'history_id' ] = history.id - trans.security.encode_dict_ids( hda_dict ) - hda_dicts.append( hda_dict ) - return hda_dicts - - def _get_hda_state_summaries( self, trans, hda_dict_list ): - """Returns two dictionaries (in a tuple): state_counts and state_ids. - Each is keyed according to the possible hda states: - _counts contains a sum of the datasets in each state - _ids contains a list of the encoded ids for each hda in that state - - hda_dict_list should be a list of hda data in dictionary form. - """ - #TODO: doc to rst - # init counts, ids for each state - state_counts = {} - state_ids = {} - for state in trans.app.model.Dataset.states.values(): - state_counts[ state ] = 0 - state_ids[ state ] = [] - - for hda_dict in hda_dict_list: - item_state = hda_dict['state'] - if not hda_dict['deleted']: - state_counts[ item_state ] = state_counts[ item_state ] + 1 - # needs to return all ids (no deleted check) - state_ids[ item_state ].append( hda_dict['id'] ) - - return ( state_counts, state_ids ) - - def _get_history_state_from_hdas( self, trans, history, hda_state_counts ): - """Returns the history state based on the states of the HDAs it contains. - """ - states = trans.app.model.Dataset.states - - num_hdas = sum( hda_state_counts.values() ) - # (default to ERROR) - state = states.ERROR - if num_hdas == 0: - state = states.NEW - - else: - if( ( hda_state_counts[ states.RUNNING ] > 0 ) - or ( hda_state_counts[ states.SETTING_METADATA ] > 0 ) - or ( hda_state_counts[ states.UPLOAD ] > 0 ) ): - state = states.RUNNING - - elif hda_state_counts[ states.QUEUED ] > 0: - state = states.QUEUED - - elif( ( hda_state_counts[ states.ERROR ] > 0 ) - or ( hda_state_counts[ states.FAILED_METADATA ] > 0 ) ): - state = states.ERROR - - elif hda_state_counts[ states.OK ] == num_hdas: - state = states.OK - - return state - - def get_history_dict( self, trans, history, hda_dictionaries=None ): - """Returns history data in the form of a dictionary. - """ - history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id }) - history_dict[ 'user_id' ] = None - if history.user_id: - history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id ) - - history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True ) - history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, trans.user, history ) - if not history_dict[ 'annotation' ]: - history_dict[ 'annotation' ] = '' - #TODO: item_slug url - if history_dict[ 'importable' ] and history_dict[ 'slug' ]: - #TODO: this should be in History (or a superclass of) - username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] )) - history_dict[ 'username_and_slug' ] = username_and_slug - - hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history ) - #TODO remove the following in v2 - ( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries ) - history_dict[ 'state_details' ] = state_counts - history_dict[ 'state_ids' ] = state_ids - history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts ) - - return history_dict - - def set_history_from_dict( self, trans, history, new_data ): - """ - Changes history data using the given dictionary new_data. - """ - #precondition: ownership of the history has already been checked - #precondition: user is not None (many of these attributes require a user to set properly) - user = trans.get_user() - - # published histories should always be importable - if 'published' in new_data and new_data[ 'published' ] and not history.importable: - new_data[ 'importable' ] = True - # send what we can down into the model - changed = history.set_from_dict( new_data ) - - # the rest (often involving the trans) - do here - #TODO: the next two could be an aspect/mixin - #TODO: also need a way to check whether they've changed - assume they have for now - if 'annotation' in new_data: - history.add_item_annotation( trans.sa_session, user, history, new_data[ 'annotation' ] ) - changed[ 'annotation' ] = new_data[ 'annotation' ] - - if 'tags' in new_data: - self.set_tags_from_list( trans, history, new_data[ 'tags' ], user=user ) - changed[ 'tags' ] = new_data[ 'tags' ] - - #TODO: sharing with user/permissions? - - if changed.keys(): - trans.sa_session.flush() - - # create a slug if none exists (setting importable to false should not remove the slug) - if 'importable' in changed and changed[ 'importable' ] and not history.slug: - self._create_history_slug( trans, history ) - - return changed - - def _create_history_slug( self, trans, history ): - #TODO: mixins need to die a quick, horrible death - # (this is duplicate from SharableMixin which can't be added to UsesHistory without exposing various urls) - cur_slug = history.slug - - # Setup slug base. - if cur_slug is None or cur_slug == "": - # Item can have either a name or a title. - item_name = history.name - slug_base = util.ready_name_for_url( item_name.lower() ) - else: - slug_base = cur_slug - - # Using slug base, find a slug that is not taken. If slug is taken, - # add integer to end. - new_slug = slug_base - count = 1 - while ( trans.sa_session.query( trans.app.model.History ) - .filter_by( user=history.user, slug=new_slug, importable=True ) - .count() != 0 ): - # Slug taken; choose a new slug based on count. This approach can - # handle numerous items with the same name gracefully. - new_slug = '%s-%i' % ( slug_base, count ) - count += 1 - - # Set slug and return. - trans.sa_session.add( history ) - history.slug = new_slug - trans.sa_session.flush() - return history.slug == cur_slug - class ExportsHistoryMixin: diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 9d2e1b760f2..4db40fab08a 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -13,8 +13,6 @@ from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous from galaxy.web import _future_expose_api_raw as expose_api_raw from galaxy.web.base.controller import BaseAPIController -from galaxy.web.base.controller import UsesHistoryMixin -from galaxy.web.base.controller import UsesTagsMixin from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin @@ -29,15 +27,16 @@ import logging log = logging.getLogger( __name__ ) -class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, - ExportsHistoryMixin, ImportsHistoryMixin ): +class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistoryMixin ): def __init__( self, app ): super( HistoriesController, self ).__init__( app ) self.citations_manager = citations.CitationsManager( app ) self.mgrs = util.bunch.Bunch( - histories=histories.HistoryManager() + histories=histories.HistoryManager( app ) ) + self.history_serializer = histories.HistorySerializer( app ) + self.history_deserializer = histories.HistoryDeserializer( app ) def _decode_id( self, trans, id ): try: @@ -62,15 +61,16 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, :rtype: list :returns: list of dictionaries containing summary history information """ - #TODO: query (by name, date, etc.) rval = [] - deleted = string_as_bool( deleted ) - histories = self.mgrs.histories.by_user( trans, user=trans.user, only_deleted=deleted ) + deleted_filter = ( self.app.model.History.deleted == False ) + if string_as_bool( deleted ): + deleted_filter = ( self.app.model.History.deleted == True ) + + histories = self.mgrs.histories.by_user( trans, user=trans.user, filters=deleted_filter ) for history in histories: - item = history.to_dict( value_mapper={ 'id': trans.security.encode_id } ) - item['url'] = url_for( 'history', id=trans.security.encode_id( history.id ) ) - rval.append( item ) + history_dict = self.history_serializer.serialize_to_view( trans, history, view='summary' ) + rval.append( history_dict ) return rval @@ -91,29 +91,22 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, :param deleted: if True, allow information on a deleted history to be shown. :rtype: dictionary - :returns: detailed history information from - :func:`galaxy.web.base.controller.UsesHistoryMixin.get_history_dict` + :returns: detailed history information """ history_id = id deleted = string_as_bool( deleted ) if history_id == "most_recently_used": - if not trans.user or len( trans.user.galaxy_sessions ) <= 0: - return None - # Most recent active history for user sessions, not deleted - history = trans.user.galaxy_sessions[0].histories[-1].history - + history = self.mgrs.histories.most_recent( trans, trans.user, + filters=( self.app.model.History.deleted == False ) ) else: - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=False, check_accessible=True, deleted=deleted ) + history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) - history_data = self.get_history_dict( trans, history ) - history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) - return history_data + return self.history_serializer.serialize_to_view( trans, history, view=kwd.get( 'view', 'detailed' ) ) @expose_api_anonymous def citations( self, trans, history_id, **kwd ): - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), check_ownership=False, check_accessible=True ) + history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) tool_ids = set([]) for dataset in history.datasets: job = dataset.creating_job @@ -156,8 +149,8 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, new_history = None # if a history id was passed, copy that history if copy_this_history_id: - original_history = self.mgrs.histories.get( trans, self._decode_id( trans, copy_this_history_id ), - check_ownership=False, check_accessible=True ) + decoded_id = self._decode_id( trans, copy_this_history_id ) + original_history = self.mgrs.histories.accessible_by_id( trans, decoded_id, trans.user ) hist_name = hist_name or ( "Copy of '%s'" % original_history.name ) new_history = original_history.copy( name=hist_name, target_user=trans.user ) @@ -168,10 +161,7 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, trans.sa_session.add( new_history ) trans.sa_session.flush() - item = {} - item = self.get_history_dict( trans, new_history ) - item['url'] = url_for( 'history', id=item['id'] ) - return item + return self.history_serializer.serialize_to_view( trans, new_history, view=kwd.get( 'view', 'detailed' ) ) @expose_api def delete( self, trans, id, **kwd ): @@ -201,43 +191,12 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, if kwd.get( 'payload', None ): purge = string_as_bool( kwd['payload'].get( 'purge', False ) ) - rval = { 'id' : history_id } - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=True, check_accessible=False ) - history.deleted = True + history = self.mgrs.histories.ownership_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + self.mgrs.histories.delete( trans, history ) if purge: - if not trans.app.config.allow_user_dataset_purge and not trans.user_is_admin(): - raise exceptions.ConfigDoesNotAllowException( 'This instance does not allow user dataset purging' ) + self.mgrs.histories.purge( trans, history ) - # First purge all the datasets - for hda in history.datasets: - if hda.purged: - continue - if hda.creating_job_associations: - job = hda.creating_job_associations[0].job - job.mark_deleted( self.app.config.track_jobs_in_database ) - self.app.job_manager.job_stop_queue.put( job.id ) - hda.purged = True - trans.sa_session.add( hda ) - trans.sa_session.flush() - - if hda.dataset.user_can_purge: - try: - hda.dataset.full_delete() - trans.sa_session.add( hda.dataset ) - except: - pass - # flush now to preserve deleted state in case of later interruption - trans.sa_session.flush() - - # Now mark the history as purged - history.purged = True - self.sa_session.add( history ) - rval[ 'purged' ] = True - - trans.sa_session.flush() - rval[ 'deleted' ] = True - return rval + return self.history_serializer.serialize_to_view( trans, history, view=kwd.get( 'view', 'detailed' ) ) @expose_api def undelete( self, trans, id, **kwd ): @@ -253,12 +212,10 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, :returns: 'OK' if the history was undeleted """ history_id = id - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=True, check_accessible=False, deleted=True ) - history.deleted = False - trans.sa_session.add( history ) - trans.sa_session.flush() - return 'OK' + history = self.mgrs.histories.ownership_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + self.mgrs.histories.undelete( trans, history ) + + return self.history_serializer.serialize_to_view( trans, history, view=kwd.get( 'view', 'detailed' ) ) @expose_api def update( self, trans, id, payload, **kwd ): @@ -280,16 +237,11 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, any values that were different from the original and, therefore, updated """ #TODO: PUT /api/histories/{encoded_history_id} payload = { rating: rating } (w/ no security checks) - history_id = id + history = self.mgrs.histories.ownership_by_id( trans, self._decode_id( trans, id ), trans.user ) - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=True, check_accessible=True ) - # validation handled here and some parsing, processing, and conversion - payload = self._validate_and_parse_update_payload( payload ) - # additional checks here (security, etc.) - changed = self.set_history_from_dict( trans, history, payload ) - - return changed + #TODO: flushing in deserialize is an iffy pattern... + self.history_deserializer.deserialize( trans, history, payload ) + return self.history_serializer.serialize_to_view( trans, history, view=kwd.get( 'view', 'detailed' ) ) @expose_api def archive_export( self, trans, id, **kwds ): @@ -307,9 +259,7 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, """ # PUT instead of POST because multiple requests should just result # in one object being created. - history_id = id - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=False, check_accessible=True ) + history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) jeha = history.latest_export up_to_date = jeha and jeha.up_to_date if 'force' in kwds: @@ -342,9 +292,7 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, """ # Seems silly to put jeha_id in here, but want GET to be immuatable? # and this is being accomplished this way. - history_id = id - history = self.mgrs.histories.get( trans, trans.security.decode_id( history_id ), - check_ownership=False, check_accessible=True ) + history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) matching_exports = filter( lambda e: trans.security.encode_id( e.id ) == jeha_id, history.exports ) if not matching_exports: raise exceptions.ObjectNotFound() @@ -356,35 +304,3 @@ class HistoriesController( BaseAPIController, UsesHistoryMixin, UsesTagsMixin, raise exceptions.MessageException( "Export not available or not yet ready." ) return self.serve_ready_history_export( trans, jeha ) - - def _validate_and_parse_update_payload( self, payload ): - """ - Validate and parse incomming data payload for a history. - """ - # This layer handles (most of the stricter idiot proofing): - # - unknown/unallowed keys - # - changing data keys from api key to attribute name - # - protection against bad data form/type - # - protection against malicious data content - # all other conversions and processing (such as permissions, etc.) should happen down the line - - # keys listed here don't error when attempting to set, but fail silently - # this allows PUT'ing an entire model back to the server without attribute errors on uneditable attrs - valid_but_uneditable_keys = ( - 'id', 'model_class', 'nice_size', 'contents_url', 'purged', 'tags', - 'state', 'state_details', 'state_ids' - ) - validated_payload = {} - for key, val in payload.items(): - if val is None: - continue - if key in ( 'name', 'genome_build', 'annotation' ): - validated_payload[ key ] = self.validate_and_sanitize_basestring( key, val ) - if key in ( 'deleted', 'published', 'importable' ): - validated_payload[ key ] = self.validate_boolean( key, val ) - elif key == 'tags': - validated_payload[ key ] = self.validate_and_sanitize_basestring_list( key, val ) - elif key not in valid_but_uneditable_keys: - pass - #log.warn( 'unknown key: %s', str( key ) ) - return validated_payload diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 561b353ef51..f9738ed2e63 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -30,8 +30,8 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio def __init__( self, app ): super( HistoryContentsController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=histories.HistoryManager(), - hdas=hdas.HDAManager() + histories=histories.HistoryManager( app ), + hdas=hdas.HDAManager( app ) ) def _decode_id( self, trans, id ): @@ -77,8 +77,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio history = trans.history # otherwise, check permissions for the history first else: - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=False, check_accessible=True ) + history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) # Allow passing in type or types - for continuity rest of methods # take in type - but this one can be passed multiple types and @@ -220,7 +219,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio return { 'error': str( e ) } def __show_dataset( self, trans, id, **kwd ): - hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), check_ownership=False, check_accessible=True ) + hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) #if hda.history.id != self._decode_id( trans, history_id ): # raise exceptions.ObjectNotFound( 'dataset was not found in this history' ) hda_dict = self.get_hda_dict( trans, hda ) @@ -287,8 +286,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio history = trans.history # otherwise, check permissions for the history first else: - history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), - check_ownership=True, check_accessible=True ) + history = self.mgrs.histories.ownership_by_id( trans, self._decode_id( trans, history_id ), trans.user ) type = payload.get('type', 'dataset') if type == 'dataset': @@ -321,7 +319,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio # copy an existing, accessible hda elif source == 'hda': unencoded_hda_id = self._decode_id( trans, content ) - original = self.mgrs.hdas.get( trans, unencoded_hda_id, check_ownership=False, check_accessible=True ) + original = self.mgrs.hdas.accessible_by_id( trans, unencoded_hda_id, trans.user ) data_copy = original.copy( copy_children=True ) hda = history.add_dataset( data_copy ) @@ -403,9 +401,8 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio anon_allowed_payload[ 'visible' ] = payload[ 'visible' ] payload = self._validate_and_parse_update_payload( anon_allowed_payload ) - hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), - check_ownership=False, check_accessible=False ) - hda = self.mgrs.hdas.err_if_uploading( trans, hda ) + hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ) ) + hda = self.mgrs.hdas.error_if_uploading( trans, hda ) if hda.history != trans.history: raise exceptions.AuthenticationRequired( 'You must be logged in to update this dataset' ) @@ -413,10 +410,9 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio payload = self._validate_and_parse_update_payload( payload ) # only check_state if not deleting, otherwise cannot delete uploading files check_state = not payload.get( 'deleted', False ) - hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), - check_ownership=True, check_accessible=True ) + hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) if check_state: - hda = self.mgrs.hdas.err_if_uploading( trans, hda ) + hda = self.mgrs.hdas.error_if_uploading( trans, hda ) #hda = self.get_dataset( trans, id, check_ownership=True, check_accessible=True, check_state=check_state ) if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): @@ -473,9 +469,9 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio # payload takes priority purge = util.string_as_bool( kwd['payload'].get( 'purge', purge ) ) - hda = self.mgrs.hdas.get( trans, self._decode_id( trans, id ), - check_ownership=True, check_accessible=True ) - self.mgrs.hdas.err_if_uploading( trans, hda ) + hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ), trans.user ) + self.mgrs.histories.error_unless_ownership( trans, hda.history, trans.user ) + self.mgrs.hdas.error_if_uploading( trans, hda ) rval = { 'id' : id } hda.deleted = True diff --git a/lib/galaxy/webapps/galaxy/api/provenance.py b/lib/galaxy/webapps/galaxy/api/provenance.py index 0397090eb37..1dcd220531c 100644 --- a/lib/galaxy/webapps/galaxy/api/provenance.py +++ b/lib/galaxy/webapps/galaxy/api/provenance.py @@ -15,7 +15,7 @@ class BaseProvenanceController( BaseAPIController, UsesHistoryMixin ): """ def __init__( self, app ): super( BaseProvenanceController, self ).__init__( app ) - self.hdas = hdas.HDAManager() + self.hdas = hdas.HDAManager( app ) @web.expose_api def index( self, trans, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 07ad81d0580..a73b66d8034 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -33,7 +33,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMix def __init__( self, app ): super( ToolsController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager() + histories=managers.histories.HistoryManager( app ) ) @web.expose_api @@ -144,7 +144,8 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMix # dataset upload. history_id = payload.get("history_id", None) if history_id: - target_history = self.mgrs.histories.get( trans, trans.security.decode_id( history_id ) ) + decoded_id = trans.security.decode_id( history_id ) + target_history = self.mgrs.histories.ownership_by_id( trans, decoded_id, trans.user ) else: target_history = None diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 7dd5afea3c0..05ae638426f 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -26,7 +26,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis def __init__( self, app ): super( BaseAPIController, self ).__init__( app ) - self.history_manager = histories.HistoryManager() + self.history_manager = histories.HistoryManager( app ) self.workflow_manager = workflows.WorkflowsManager( app ) self.workflow_contents_manager = workflows.WorkflowContentsManager() diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index 2414d4e99bb..eae97af6de8 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -1,5 +1,6 @@ import logging from cgi import escape +from traceback import format_exc import urllib import galaxy.util @@ -34,15 +35,24 @@ class HistoryListGrid( grids.Grid ): # Custom column types class DatasetsByStateColumn( grids.GridColumn, UsesHistoryMixin ): def get_value( self, trans, grid, history ): - state_count_dict = self.get_hda_state_counts( trans, history ) + state_counts = { + 'ok' : 0, + 'running' : 0, + 'queued' : 0, + 'error' : 0, + } + for hda in history.datasets: + if hda.visible and not hda.deleted and hda.state in state_counts.keys(): + state_counts[ hda.state ] += 1 rval = '' - for state in ( 'ok', 'running', 'queued', 'error' ): - count = state_count_dict.get( state, 0 ) + for state in state_counts.keys(): + count = state_counts.get( state ) if count: rval += '
%s
' % (state, count) return rval + class HistoryListNameColumn( NameColumn ): def get_link( self, trans, grid, history ): link = None @@ -201,8 +211,10 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI def __init__( self, app ): super( HistoryController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager() + histories=managers.histories.HistoryManager( app ) ) + self.history_serializer = managers.histories.HistorySerializer( self.app ) + @web.expose def index( self, trans ): @@ -626,7 +638,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI history_dictionaries = [] for history in self.get_user_histories( trans, user=trans.user, include_deleted=include_deleted_histories ): - history_dictionary = self.get_history_dict( trans, history ) + history_dictionary = self.history_serializer.serialize_to_view( trans, history, view='detailed' ) history_dictionaries.append( history_dictionary ) return trans.fill_template_mako( "history/view_multiple.mako", @@ -1097,7 +1109,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI trans.app.job_manager.job_stop_queue.put( job.id ) # Regardless of whether it was previously deleted, get the most recent history or create a new one. - most_recent_history = self.mgrs.histories.most_recent( trans, user=trans.user, deleted=False ) + most_recent_history = self.mgrs.histories.most_recent( trans, user=trans.user ) if most_recent_history: trans.set_history( most_recent_history ) return trans.show_ok_message( "History deleted, your most recent history is now active", @@ -1442,7 +1454,6 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI msg = 'Copied and created %d new histories.' % len( histories ) return trans.show_ok_message( msg ) - # ------------------------------------------------------------------------- current history @web.expose @web.require_login( "switch to a history" ) @@ -1459,33 +1470,35 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI def history_data( self, trans, history ): """ """ - #TODO: to manager - history_data = self.get_history_dict( trans, history ) - encoded_history_id = trans.security.encode_id( history.id ) - history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=encoded_history_id ) - return history_data + return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) #TODO: combine these next two - poss. with a redirect flag - @web.require_login( "switch to a history" ) + #@web.require_login( "switch to a history" ) @web.json - def set_as_current( self, trans, id=None ): + def set_as_current( self, trans, id ): """ """ - history = self.get_history( trans, id ) - trans.set_history( history ) - return self.history_data( trans, history ) + try: + history = self.mgrs.histories.ownership_by_id( trans, trans.security.decode_id( id ), trans.user ) + trans.set_history( history ) + return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) + except exceptions.MessageException, msg_exc: + print type( msg_exc ) + trans.response.status = msg_exc.err_code.code + return { 'err_msg': msg_exc.err_msg, 'err_code': msg_exc.err_code.code } @web.json def current_history_json( self, trans ): """ """ history = trans.get_history( create=True ) - return self.history_data( trans, history ) + return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) @web.json def create_new_current( self, trans, name=None ): """ """ - return self.history_data( trans, trans.new_history( name ) ) + new_history = trans.new_history( name ) + return self.history_serializer.serialize_to_view( trans, new_history, view='detailed' ) #TODO: /history/current to do all of the above: if ajax, return json; if post, read id and set to current diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 5681b0e6ef3..aeeeae5cb12 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -290,7 +290,7 @@ class PageController( BaseUIController, SharableMixin, UsesHistoryMixin, def __init__( self, app ): super( PageController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager() + histories=managers.histories.HistoryManager( app ) ) @web.expose @@ -740,12 +740,6 @@ class PageController( BaseUIController, SharableMixin, UsesHistoryMixin, hda_dicts = [] datasets = self.get_history_datasets( trans, history ) - #for hda in datasets: - # hda_dict = self.get_hda_dict( trans, hda ) - # hda_dict[ 'annotation' ] = self.get_item_annotation_str( trans.sa_session, history.user, hda ) - # hda_dicts.append( hda_dict ) - #history_dict = self.get_history_dict( trans, history, hda_dictionaries=hda_dicts ) - #history_dict[ 'annotation' ] = history.annotation # include all datasets: hidden, deleted, and purged #TODO!: doubled query (hda_dictionaries + datasets) diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py index e7b70cc3f72..5f9c5b5f64b 100644 --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -27,7 +27,7 @@ class RootController( BaseUIController, UsesHistoryMixin, UsesHistoryDatasetAsso def __init__( self, app ): super( RootController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager() + histories=managers.histories.HistoryManager( app ) ) @web.expose diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 2c665473b61..07bb5cbe812 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -160,7 +160,7 @@ def _flatten_step_params( param_dict, prefix="" ): def build_workflow_run_config( trans, workflow, payload ): app = trans.app - history_manager = histories.HistoryManager() + history_manager = histories.HistoryManager( app ) # Pull other parameters out of payload. param_map = payload.get( 'parameters', {} ) @@ -196,7 +196,7 @@ def build_workflow_run_config( trans, workflow, payload ): # Passing an existing history to use. encoded_history_id = history_param[ 8: ] history_id = __decode_id( trans, encoded_history_id, model_type="history" ) - history = history_manager.get( trans, history_id, check_ownership=True ) + history = history_manager.ownership_by_id( trans, history_id, trans.user ) else: # Send workflow outputs to new history. history = app.model.History(name=history_param, user=trans.user) diff --git a/static/scripts/mvc/list/list-panel.js b/static/scripts/mvc/list/list-panel.js index f78ccaaee27..a2e9092fa76 100644 --- a/static/scripts/mvc/list/list-panel.js +++ b/static/scripts/mvc/list/list-panel.js @@ -872,10 +872,10 @@ var ModelListPanel = ListPanel.extend({ this.log( this + '._setUpModelListeners', this.model ); // bounce model errors up to the panel this.model.on( 'error', function(){ - //TODO: namespace? - //var args = Array.prototype.slice.call( arguments, 0 ); - //args[0] = 'model:' + args[0]; - this.trigger.apply( panel, arguments ); + var args = Array.prototype.slice.call( arguments, 0 ); + //args.unshift( 'model:error' ); + args.unshift( 'error' ); + this.trigger.apply( this, args ); }, this ); return this; }, diff --git a/static/scripts/packed/mvc/list/list-panel.js b/static/scripts/packed/mvc/list/list-panel.js index 4bbeb2a00b4..36dd7c6b42f 100644 --- a/static/scripts/packed/mvc/list/list-panel.js +++ b/static/scripts/packed/mvc/list/list-panel.js @@ -1 +1 @@ -define(["mvc/list/list-item","ui/loading-indicator","mvc/base-mvc","utils/localization","ui/search-input"],function(d,f,b,c){var e=Backbone.View.extend(b.LoggableMixin).extend({viewClass:d.ListItemView,collectionClass:Backbone.Collection,tagName:"div",className:"list-panel",fxSpeed:"fast",emptyMsg:c("This list is empty"),noneFoundMsg:c("No matching items found"),searchPlaceholder:c("search"),multiselectActions:[],initialize:function(g,h){g=g||{};if(g.logger){this.logger=g.logger}this.log(this+".initialize:",g);this.fxSpeed=_.has(g,"fxSpeed")?(g.fxSpeed):(this.fxSpeed);this.filters=[];this.searchFor=g.searchFor||"";this.indicator=new f(this.$el);this.selecting=(g.selecting!==undefined)?g.selecting:true;this.selected=g.selected||[];this.lastSelected=null;this.dragItems=g.dragItems||false;this.viewClass=g.viewClass||this.viewClass;this.views=[];this.collection=g.collection||(new this.collectionClass([]));this.filters=g.filters||[];this.$scrollContainer=g.$scrollContainer||this.$scrollContainer;this.title=g.title||"";this.subtitle=g.subtitle||"";this.multiselectActions=g.multiselectActions||this.multiselectActions;this.actionsPopup=null;this._setUpListeners()},freeViews:function(){_.each(this.views,function(g){g.off()});this.views=[];return this},_setUpListeners:function(){this.off();this.on("error",function(h,k,g,j,i){console.error(h,k,g,j,i)},this);this.on("loading",function(){this._showLoadingIndicator("loading...",40)},this);this.on("loading-done",function(){this._hideLoadingIndicator(40)},this);this.once("rendered",function(){this.trigger("rendered:initial",this)},this);if(this.logger){this.on("all",function(g){this.log(this+"",arguments)},this)}this._setUpCollectionListeners();this._setUpViewListeners();return this},_setUpCollectionListeners:function(){this.log(this+"._setUpCollectionListeners",this.collection);this.collection.off();this.collection.on("error",function(h,k,g,j,i){this.trigger("error",h,k,g,j,i)},this);this.collection.on("reset",function(){this.renderItems()},this);this.collection.on("add",this.addItemView,this);this.collection.on("remove",this.removeItemView,this);if(this.logger){this.collection.on("all",function(g){this.info(this+"(collection)",arguments)},this)}return this},_setUpViewListeners:function(){this.log(this+"._setUpViewListeners");this.on("view:selected",function(g,h){if(h&&h.shiftKey&&this.lastSelected){var i=this.viewFromModelId(this.lastSelected);if(i){this.selectRange(g,i)}}else{if(h&&h.altKey&&!this.selecting){this.showSelectors()}}this.selected.push(g.model.id);this.lastSelected=g.model.id},this);this.on("view:de-selected",function(g,h){this.selected=_.without(this.selected,g.model.id)},this)},render:function(h){this.log(this+".render",h);var g=this._buildNewRender();this._setUpBehaviors(g);this._queueNewRender(g,h);return this},_buildNewRender:function(){this.debug(this+"(ListPanel)._buildNewRender");var g=$(this.templates.el({},this));this._renderControls(g);this._renderTitle(g);this._renderSubtitle(g);this._renderSearch(g);this.renderItems(g);return g},_renderControls:function(h){this.debug(this+"(ListPanel)._renderControls");var g=$(this.templates.controls({},this));h.find(".controls").replaceWith(g);return g},_renderTitle:function(g){},_renderSubtitle:function(g){},_queueNewRender:function(h,i){i=(i===undefined)?(this.fxSpeed):(i);var g=this;g.log("_queueNewRender:",h,i);$(g).queue("fx",[function(j){this.$el.fadeOut(i,j)},function(j){g._swapNewRender(h);j()},function(j){this.$el.fadeIn(i,j)},function(j){g.trigger("rendered",g);j()}])},_swapNewRender:function(g){this.$el.empty().attr("class",this.className).append(g.children());if(this.selecting){this.showSelectors(0)}return this},_setUpBehaviors:function(g){g=g||this.$el;g.find(".controls [title]").tooltip({placement:"bottom"});return this},$scrollContainer:function(){return this.$el.parent().parent()},$list:function(g){return(g||this.$el).find("> .list-items")},$messages:function(g){return(g||this.$el).find("> .controls .messages")},$emptyMessage:function(g){return(g||this.$el).find("> .empty-message")},renderItems:function(i){i=i||this.$el;var g=this;g.log(this+".renderItems",i);var h=g.$list(i);g.views=g._filterCollection().map(function(j){return g._createItemView(j).render(0)});h.empty();if(g.views.length){g._attachItems(i);g.$emptyMessage(i).hide()}else{g._renderEmptyMessage(i).show()}return g.views},_filterCollection:function(){var g=this;return g.collection.filter(_.bind(g._filterItem,g))},_filterItem:function(h){var g=this;return(_.every(g.filters.map(function(i){return i.call(h)})))&&(!g.searchFor||h.matchesAll(g.searchFor))},_createItemView:function(i){var j=this._getItemViewClass(i),h=_.extend(this._getItemViewOptions(i),{model:i}),g=new j(h);this._setUpItemViewListeners(g);return g},_getItemViewClass:function(g){return this.viewClass},_getItemViewOptions:function(g){return{fxSpeed:this.fxSpeed,expanded:false,selectable:this.selecting,selected:_.contains(this.selected,g.id),draggable:this.dragItems}},_setUpItemViewListeners:function(h){var g=this;h.on("all",function(){var i=Array.prototype.slice.call(arguments,0);i[0]="view:"+i[0];g.trigger.apply(g,i)});h.on("draggable:dragstart",function(l,i){var j={},k=this.getSelectedModels();if(k.length){j=k.toJSON()}else{j=[i.model.toJSON()]}l.dataTransfer.setData("text",JSON.stringify(j))},this);return g},_attachItems:function(g){this.$list(g).append(this.views.map(function(h){return h.$el}));return this},_renderEmptyMessage:function(g){this.debug("_renderEmptyMessage",g,this.searchFor);var h=this.searchFor?this.noneFoundMsg:this.emptyMsg;return this.$emptyMessage(g).text(h)},expandAll:function(){_.each(this.views,function(g){g.expand()})},collapseAll:function(){_.each(this.views,function(g){g.collapse()})},addItemView:function(j,k,i){this.log(this+".addItemView:",j);var h=this;if(!h._filterItem(j)){return undefined}var g=h._createItemView(j);$(g).queue("fx",[function(l){h.$emptyMessage().fadeOut(h.fxSpeed,l)},function(l){h._attachView(g);l()}]);return g},_attachView:function(h){var g=this;g.views.push(h);g.$list().append(h.render(0).$el.hide());h.$el.slideDown(g.fxSpeed)},removeItemView:function(j,k,i){this.log(this+".removeItemView:",j);var h=this,g=h.viewFromModel(j);if(!g){return undefined}h.views=_.without(h.views,g);$({}).queue("fx",[function(l){g.$el.fadeOut(h.fxSpeed,l)},function(l){g.remove();if(!h.views.length){h._renderEmptyMessage().fadeIn(h.fxSpeed,l)}else{l()}}]);return g},viewFromModelId:function(h){for(var g=0;g .controls .search-query").val(g);return this},clearSearch:function(g){this.searchFor="";this.trigger("search:clear",this);this.renderItems();this.$("> .controls .search-query").val("");return this},showSelectors:function(g){g=(g!==undefined)?(g):(this.fxSpeed);this.selecting=true;this.$(".list-actions").slideDown(g);_.each(this.views,function(h){h.showSelector(g)})},hideSelectors:function(g){g=(g!==undefined)?(g):(this.fxSpeed);this.selecting=false;this.$(".list-actions").slideUp(g);_.each(this.views,function(h){h.hideSelector(g)});this.selected=[];this.lastSelected=null},toggleSelectors:function(){if(!this.selecting){this.showSelectors()}else{this.hideSelectors()}},selectAll:function(g){_.each(this.views,function(h){h.select(g)})},deselectAll:function(g){this.lastSelected=null;_.each(this.views,function(h){h.deselect(g)})},selectRange:function(i,h){var g=this.viewRange(i,h);_.each(g,function(j){j.select()});return g},getSelectedViews:function(){return _.filter(this.views,function(g){return g.selected})},getSelectedModels:function(){return new this.collection.constructor(_.map(this.getSelectedViews(),function(g){return g.model}))},_showLoadingIndicator:function(h,g,i){this.debug("_showLoadingIndicator",this.indicator,h,g,i);g=(g!==undefined)?(g):(this.fxSpeed);if(!this.indicator){this.indicator=new f(this.$el,this.$el.parent());this.debug("\t created",this.indicator)}if(!this.$el.is(":visible")){this.indicator.show(0,i)}else{this.$el.fadeOut(g);this.indicator.show(h,g,i)}},_hideLoadingIndicator:function(g,h){this.debug("_hideLoadingIndicator",this.indicator,g,h);g=(g!==undefined)?(g):(this.fxSpeed);if(this.indicator){this.indicator.hide(g,h)}},scrollPosition:function(){return this.$scrollContainer().scrollTop()},scrollTo:function(h,g){g=g||0;this.$scrollContainer().animate({scrollTop:h},g);return this},scrollToTop:function(g){return this.scrollTo(0,g)},scrollToItem:function(g,i){if(!g){return this}var h=g.$el.position().top;return this.scrollTo(h,i)},scrollToId:function(h,g){return this.scrollToItem(this.viewFromModelId(h),g)},events:{"click .select-all":"selectAll","click .deselect-all":"deselectAll"},toString:function(){return"ListPanel("+this.collection+")"}});e.prototype.templates=(function(){var h=b.wrapTemplate(["
",'
','
','
',"
"]);var g=b.wrapTemplate(['
','
','
<%= view.title %>
',"
",'
<%= view.subtitle %>
','
','
','",'
','
','",'","
","
","
"]);return{el:h,controls:g}}());var a=e.extend({modelCollectionKey:"contents",initialize:function(g){e.prototype.initialize.call(this,g);this.selecting=(g.selecting!==undefined)?g.selecting:false;this.setModel(this.model,g)},setModel:function(h,g){g=g||{};this.debug(this+".setModel:",h,g);this.freeModel();this.freeViews();if(h){var i=this.model?this.model.get("id"):null;this.model=h;if(this.logger){this.model.logger=this.logger}this._setUpModelListeners();this.collection.off();this.collection=(this.model[this.modelCollectionKey])?this.model[this.modelCollectionKey]:(g.collection||(new this.collectionClass([])));this._setUpCollectionListeners();if(i&&h.get("id")!==i){this.trigger("new-model",this)}}return this},freeModel:function(){if(this.model){this.stopListening(this.model)}return this},_setUpModelListeners:function(){this.log(this+"._setUpModelListeners",this.model);this.model.on("error",function(){this.trigger.apply(panel,arguments)},this);return this},_renderControls:function(h){this.debug(this+"(ListPanel)._renderControls");var i=this.model?this.model.toJSON():{},g=$(this.templates.controls(i,this));h.find(".controls").replaceWith(g);return g},toString:function(){return"ModelListPanel("+this.model+")"}});a.prototype.templates=(function(){var g=b.wrapTemplate(['
','
','
<%= model.name %>
',"
",'
<%= view.subtitle %>
','
','
','",'
','
','",'","
","
","
"]);return _.extend(_.clone(e.prototype.templates),{controls:g})}());return{ListPanel:e,ModelListPanel:a}}); \ No newline at end of file +define(["mvc/list/list-item","ui/loading-indicator","mvc/base-mvc","utils/localization","ui/search-input"],function(d,f,b,c){var e=Backbone.View.extend(b.LoggableMixin).extend({viewClass:d.ListItemView,collectionClass:Backbone.Collection,tagName:"div",className:"list-panel",fxSpeed:"fast",emptyMsg:c("This list is empty"),noneFoundMsg:c("No matching items found"),searchPlaceholder:c("search"),multiselectActions:[],initialize:function(g,h){g=g||{};if(g.logger){this.logger=g.logger}this.log(this+".initialize:",g);this.fxSpeed=_.has(g,"fxSpeed")?(g.fxSpeed):(this.fxSpeed);this.filters=[];this.searchFor=g.searchFor||"";this.indicator=new f(this.$el);this.selecting=(g.selecting!==undefined)?g.selecting:true;this.selected=g.selected||[];this.lastSelected=null;this.dragItems=g.dragItems||false;this.viewClass=g.viewClass||this.viewClass;this.views=[];this.collection=g.collection||(new this.collectionClass([]));this.filters=g.filters||[];this.$scrollContainer=g.$scrollContainer||this.$scrollContainer;this.title=g.title||"";this.subtitle=g.subtitle||"";this.multiselectActions=g.multiselectActions||this.multiselectActions;this.actionsPopup=null;this._setUpListeners()},freeViews:function(){_.each(this.views,function(g){g.off()});this.views=[];return this},_setUpListeners:function(){this.off();this.on("error",function(h,k,g,j,i){console.error(h,k,g,j,i)},this);this.on("loading",function(){this._showLoadingIndicator("loading...",40)},this);this.on("loading-done",function(){this._hideLoadingIndicator(40)},this);this.once("rendered",function(){this.trigger("rendered:initial",this)},this);if(this.logger){this.on("all",function(g){this.log(this+"",arguments)},this)}this._setUpCollectionListeners();this._setUpViewListeners();return this},_setUpCollectionListeners:function(){this.log(this+"._setUpCollectionListeners",this.collection);this.collection.off();this.collection.on("error",function(h,k,g,j,i){this.trigger("error",h,k,g,j,i)},this);this.collection.on("reset",function(){this.renderItems()},this);this.collection.on("add",this.addItemView,this);this.collection.on("remove",this.removeItemView,this);if(this.logger){this.collection.on("all",function(g){this.info(this+"(collection)",arguments)},this)}return this},_setUpViewListeners:function(){this.log(this+"._setUpViewListeners");this.on("view:selected",function(g,h){if(h&&h.shiftKey&&this.lastSelected){var i=this.viewFromModelId(this.lastSelected);if(i){this.selectRange(g,i)}}else{if(h&&h.altKey&&!this.selecting){this.showSelectors()}}this.selected.push(g.model.id);this.lastSelected=g.model.id},this);this.on("view:de-selected",function(g,h){this.selected=_.without(this.selected,g.model.id)},this)},render:function(h){this.log(this+".render",h);var g=this._buildNewRender();this._setUpBehaviors(g);this._queueNewRender(g,h);return this},_buildNewRender:function(){this.debug(this+"(ListPanel)._buildNewRender");var g=$(this.templates.el({},this));this._renderControls(g);this._renderTitle(g);this._renderSubtitle(g);this._renderSearch(g);this.renderItems(g);return g},_renderControls:function(h){this.debug(this+"(ListPanel)._renderControls");var g=$(this.templates.controls({},this));h.find(".controls").replaceWith(g);return g},_renderTitle:function(g){},_renderSubtitle:function(g){},_queueNewRender:function(h,i){i=(i===undefined)?(this.fxSpeed):(i);var g=this;g.log("_queueNewRender:",h,i);$(g).queue("fx",[function(j){this.$el.fadeOut(i,j)},function(j){g._swapNewRender(h);j()},function(j){this.$el.fadeIn(i,j)},function(j){g.trigger("rendered",g);j()}])},_swapNewRender:function(g){this.$el.empty().attr("class",this.className).append(g.children());if(this.selecting){this.showSelectors(0)}return this},_setUpBehaviors:function(g){g=g||this.$el;g.find(".controls [title]").tooltip({placement:"bottom"});return this},$scrollContainer:function(){return this.$el.parent().parent()},$list:function(g){return(g||this.$el).find("> .list-items")},$messages:function(g){return(g||this.$el).find("> .controls .messages")},$emptyMessage:function(g){return(g||this.$el).find("> .empty-message")},renderItems:function(i){i=i||this.$el;var g=this;g.log(this+".renderItems",i);var h=g.$list(i);g.views=g._filterCollection().map(function(j){return g._createItemView(j).render(0)});h.empty();if(g.views.length){g._attachItems(i);g.$emptyMessage(i).hide()}else{g._renderEmptyMessage(i).show()}return g.views},_filterCollection:function(){var g=this;return g.collection.filter(_.bind(g._filterItem,g))},_filterItem:function(h){var g=this;return(_.every(g.filters.map(function(i){return i.call(h)})))&&(!g.searchFor||h.matchesAll(g.searchFor))},_createItemView:function(i){var j=this._getItemViewClass(i),h=_.extend(this._getItemViewOptions(i),{model:i}),g=new j(h);this._setUpItemViewListeners(g);return g},_getItemViewClass:function(g){return this.viewClass},_getItemViewOptions:function(g){return{fxSpeed:this.fxSpeed,expanded:false,selectable:this.selecting,selected:_.contains(this.selected,g.id),draggable:this.dragItems}},_setUpItemViewListeners:function(h){var g=this;h.on("all",function(){var i=Array.prototype.slice.call(arguments,0);i[0]="view:"+i[0];g.trigger.apply(g,i)});h.on("draggable:dragstart",function(l,i){var j={},k=this.getSelectedModels();if(k.length){j=k.toJSON()}else{j=[i.model.toJSON()]}l.dataTransfer.setData("text",JSON.stringify(j))},this);return g},_attachItems:function(g){this.$list(g).append(this.views.map(function(h){return h.$el}));return this},_renderEmptyMessage:function(g){this.debug("_renderEmptyMessage",g,this.searchFor);var h=this.searchFor?this.noneFoundMsg:this.emptyMsg;return this.$emptyMessage(g).text(h)},expandAll:function(){_.each(this.views,function(g){g.expand()})},collapseAll:function(){_.each(this.views,function(g){g.collapse()})},addItemView:function(j,k,i){this.log(this+".addItemView:",j);var h=this;if(!h._filterItem(j)){return undefined}var g=h._createItemView(j);$(g).queue("fx",[function(l){h.$emptyMessage().fadeOut(h.fxSpeed,l)},function(l){h._attachView(g);l()}]);return g},_attachView:function(h){var g=this;g.views.push(h);g.$list().append(h.render(0).$el.hide());h.$el.slideDown(g.fxSpeed)},removeItemView:function(j,k,i){this.log(this+".removeItemView:",j);var h=this,g=h.viewFromModel(j);if(!g){return undefined}h.views=_.without(h.views,g);$({}).queue("fx",[function(l){g.$el.fadeOut(h.fxSpeed,l)},function(l){g.remove();if(!h.views.length){h._renderEmptyMessage().fadeIn(h.fxSpeed,l)}else{l()}}]);return g},viewFromModelId:function(h){for(var g=0;g .controls .search-query").val(g);return this},clearSearch:function(g){this.searchFor="";this.trigger("search:clear",this);this.renderItems();this.$("> .controls .search-query").val("");return this},showSelectors:function(g){g=(g!==undefined)?(g):(this.fxSpeed);this.selecting=true;this.$(".list-actions").slideDown(g);_.each(this.views,function(h){h.showSelector(g)})},hideSelectors:function(g){g=(g!==undefined)?(g):(this.fxSpeed);this.selecting=false;this.$(".list-actions").slideUp(g);_.each(this.views,function(h){h.hideSelector(g)});this.selected=[];this.lastSelected=null},toggleSelectors:function(){if(!this.selecting){this.showSelectors()}else{this.hideSelectors()}},selectAll:function(g){_.each(this.views,function(h){h.select(g)})},deselectAll:function(g){this.lastSelected=null;_.each(this.views,function(h){h.deselect(g)})},selectRange:function(i,h){var g=this.viewRange(i,h);_.each(g,function(j){j.select()});return g},getSelectedViews:function(){return _.filter(this.views,function(g){return g.selected})},getSelectedModels:function(){return new this.collection.constructor(_.map(this.getSelectedViews(),function(g){return g.model}))},_showLoadingIndicator:function(h,g,i){this.debug("_showLoadingIndicator",this.indicator,h,g,i);g=(g!==undefined)?(g):(this.fxSpeed);if(!this.indicator){this.indicator=new f(this.$el,this.$el.parent());this.debug("\t created",this.indicator)}if(!this.$el.is(":visible")){this.indicator.show(0,i)}else{this.$el.fadeOut(g);this.indicator.show(h,g,i)}},_hideLoadingIndicator:function(g,h){this.debug("_hideLoadingIndicator",this.indicator,g,h);g=(g!==undefined)?(g):(this.fxSpeed);if(this.indicator){this.indicator.hide(g,h)}},scrollPosition:function(){return this.$scrollContainer().scrollTop()},scrollTo:function(h,g){g=g||0;this.$scrollContainer().animate({scrollTop:h},g);return this},scrollToTop:function(g){return this.scrollTo(0,g)},scrollToItem:function(g,i){if(!g){return this}var h=g.$el.position().top;return this.scrollTo(h,i)},scrollToId:function(h,g){return this.scrollToItem(this.viewFromModelId(h),g)},events:{"click .select-all":"selectAll","click .deselect-all":"deselectAll"},toString:function(){return"ListPanel("+this.collection+")"}});e.prototype.templates=(function(){var h=b.wrapTemplate(["
",'
','
','
',"
"]);var g=b.wrapTemplate(['
','
','
<%= view.title %>
',"
",'
<%= view.subtitle %>
','
','
','",'
','
','",'","
","
","
"]);return{el:h,controls:g}}());var a=e.extend({modelCollectionKey:"contents",initialize:function(g){e.prototype.initialize.call(this,g);this.selecting=(g.selecting!==undefined)?g.selecting:false;this.setModel(this.model,g)},setModel:function(h,g){g=g||{};this.debug(this+".setModel:",h,g);this.freeModel();this.freeViews();if(h){var i=this.model?this.model.get("id"):null;this.model=h;if(this.logger){this.model.logger=this.logger}this._setUpModelListeners();this.collection.off();this.collection=(this.model[this.modelCollectionKey])?this.model[this.modelCollectionKey]:(g.collection||(new this.collectionClass([])));this._setUpCollectionListeners();if(i&&h.get("id")!==i){this.trigger("new-model",this)}}return this},freeModel:function(){if(this.model){this.stopListening(this.model)}return this},_setUpModelListeners:function(){this.log(this+"._setUpModelListeners",this.model);this.model.on("error",function(){var g=Array.prototype.slice.call(arguments,0);g.unshift("error");this.trigger.apply(this,g)},this);return this},_renderControls:function(h){this.debug(this+"(ListPanel)._renderControls");var i=this.model?this.model.toJSON():{},g=$(this.templates.controls(i,this));h.find(".controls").replaceWith(g);return g},toString:function(){return"ModelListPanel("+this.model+")"}});a.prototype.templates=(function(){var g=b.wrapTemplate(['
','
','
<%= model.name %>
',"
",'
<%= view.subtitle %>
','
','
','",'
','
','",'","
","
","
"]);return _.extend(_.clone(e.prototype.templates),{controls:g})}());return{ListPanel:e,ModelListPanel:a}}); \ No newline at end of file diff --git a/test/unit/managers/mock.py b/test/unit/managers/mock.py new file mode 100644 index 00000000000..9de61ff236e --- /dev/null +++ b/test/unit/managers/mock.py @@ -0,0 +1,87 @@ +""" +Mock infrastructure for testing ModelManagers. +""" +import sys +import os + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy.web import security +from galaxy import objectstore +from galaxy.model import mapping +from galaxy.util.bunch import Bunch + + +# ============================================================================= +class MockAppConfig( Bunch ): + def __init__( self, **kwargs ): + Bunch.__init__( self, **kwargs ) + self.security = security.SecurityHelper( id_secret='bler' ) + self.file_path = '/tmp' + self.job_working_directory = '/tmp' + self.new_file_path = '/tmp' + + self.object_store_config_file = '' + self.object_store = 'disk' + self.object_store_check_old_style = False + + self.user_activation_on = False + self.new_user_dataset_access_role_default_private = False + + self.allow_user_dataset_purge = True + +class MockApp( object ): + def __init__( self, **kwargs ): + self.config = MockAppConfig( **kwargs ) + self.security = self.config.security + self.object_store = objectstore.build_object_store_from_config( self.config ) + self.model = mapping.init( "/tmp", "sqlite:///:memory:", create_tables=True, object_store=self.object_store ) + self.security_agent = self.model.security_agent + +class MockWebapp( object ): + def __init__( self, **kwargs ): + self.name = kwargs.get( 'name', 'galaxy' ) + +class MockTrans( object ): + def __init__( self, user=None, history=None, **kwargs ): + + self.app = MockApp( **kwargs ) + self.webapp = MockWebapp( **kwargs ) + self.sa_session = self.app.model.session + + self.galaxy_session = None + self.__user = user + self.security = self.app.security + self.history = history + + def get_user( self ): + if self.galaxy_session: + return self.galaxy_session.user + else: + return self.__user + + def set_user( self, user ): + """Set the current user.""" + if self.galaxy_session: + self.galaxy_session.user = user + self.sa_session.add( self.galaxy_session ) + self.sa_session.flush() + self.__user = user + + user = property( get_user, set_user ) + + def get_history( self ): + return self.history + + def set_history( self, history ): + self.history = history + + def fill_template( self, filename, template_lookup=None, **kwargs ): + template = template_lookup.get_template( filename ) + template.output_encoding = 'utf-8' + return template.render( **kwargs ) diff --git a/test/unit/managers/test_DatasetManager.py b/test/unit/managers/test_DatasetManager.py new file mode 100644 index 00000000000..6a65b601e04 --- /dev/null +++ b/test/unit/managers/test_DatasetManager.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +""" +""" +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +from galaxy.util.bunch import Bunch + +import mock +from test_ModelManager import BaseTestCase +from galaxy.managers.datasets import DatasetManager + + +# ============================================================================= +default_password = '123456' +user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) +user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) + + +# ============================================================================= +class DatasetManagerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( DatasetManagerTestCase, self ).set_up_managers() + self.dataset_mgr = DatasetManager( self.app ) + + def test_create( self ): + print "should be able to create a new Dataset" + dataset1 = self.dataset_mgr.create( self.trans ) + self.assertIsInstance( dataset1, model.Dataset ) + self.assertEqual( dataset1, self.trans.sa_session.query( model.Dataset ).get( dataset1.id ) ) + + def test_base( self ): + dataset1 = self.dataset_mgr.create( self.trans ) + dataset2 = self.dataset_mgr.create( self.trans ) + + print "should be able to query" + datasets = self.trans.sa_session.query( model.Dataset ).all() + self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) + self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) + self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) + self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) + + print "should be able to limit and offset" + self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) + self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) + self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) + + self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) + + print "should be able to order" + self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), + [ dataset2, dataset1 ] ) + + def test_delete( self ): + item1 = self.dataset_mgr.create( self.trans ) + + print "should be able to delete and undelete an hda" + self.assertFalse( item1.deleted ) + self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) + self.assertTrue( item1.deleted ) + self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) + self.assertFalse( item1.deleted ) + + def test_purge_allowed( self ): + self.trans.app.config.allow_user_dataset_purge = True + item1 = self.dataset_mgr.create( self.trans ) + + print "should purge an hda if config does allow" + self.assertFalse( item1.purged ) + self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) + self.assertTrue( item1.purged ) + + def test_purge_not_allowed( self ): + self.trans.app.config.allow_user_dataset_purge = False + item1 = self.dataset_mgr.create( self.trans ) + + print "should raise an error when purging an hda if config does not allow" + self.assertFalse( item1.purged ) + self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) + self.assertFalse( item1.purged ) + + def test_accessible( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + dataset = self.dataset_mgr.create( self.trans ) + + print "(by default, dataset permissions are lax) should be accessible to all" + for user in self.user_mgr.list( self.trans ): + self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, user ) ) + +#TODO: this is ... not working... + self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) + self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) + self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() diff --git a/test/unit/managers/test_HDAManager.py b/test/unit/managers/test_HDAManager.py new file mode 100644 index 00000000000..0511391fdd3 --- /dev/null +++ b/test/unit/managers/test_HDAManager.py @@ -0,0 +1,198 @@ +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +from galaxy.util.bunch import Bunch + +import mock +from test_ModelManager import BaseTestCase +from galaxy.managers.histories import HistoryManager +from galaxy.managers.datasets import DatasetManager +from galaxy.managers.hdas import HDAManager + + +# ============================================================================= +default_password = '123456' +user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) +user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) + + +# ============================================================================= +class HDAManagerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( HDAManagerTestCase, self ).set_up_managers() + self.history_mgr = HistoryManager( self.app ) + self.dataset_mgr = DatasetManager( self.app ) + self.hda_mgr = HDAManager( self.app ) + + def test_create( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + + print "should be able to create a new HDA with a specified history and dataset" + hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1, hid=1 ) + self.assertIsInstance( hda1, model.HistoryDatasetAssociation ) + self.assertEqual( hda1, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda1.id ) ) + self.assertEqual( hda1.history, history1 ) + self.assertEqual( hda1.dataset, dataset1 ) + self.assertEqual( hda1.hid, 1 ) + + print "should be able to create a new HDA with only a specified history and no dataset" + hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) + self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) + self.assertIsInstance( hda2.dataset, model.Dataset ) + self.assertEqual( hda2.history, history1 ) + self.assertEqual( hda2.hid, 2 ) + + print "should be able to create a new HDA with no history and no dataset" + hda3 = self.hda_mgr.create( self.trans ) + self.assertIsInstance( hda3, model.HistoryDatasetAssociation ) + self.assertIsInstance( hda3.dataset, model.Dataset, msg="dataset will be auto created" ) + self.assertIsNone( hda3.history, msg="history will be None" ) + self.assertEqual( hda3.hid, None ) + + def test_copy_from_hda( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + + print "should be able to copy an HDA" + hda2 = self.hda_mgr.copy_hda( self.trans, hda1, history=history1 ) + self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) + self.assertEqual( hda2, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda2.id ) ) + self.assertEqual( hda2.name, hda1.name ) + self.assertEqual( hda2.history, hda1.history ) + self.assertEqual( hda2.dataset, hda1.dataset ) + self.assertNotEqual( hda2, hda1 ) + print hda1.hid + print hda2.hid + + #def test_copy_from_ldda( self ): + # owner = self.user_mgr.create( self.trans, **user2_data ) + # history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + # + # print "should be able to copy an HDA" + # hda2 = self.hda_mgr.copy_ldda( self.trans, history1, hda1 ) + + def test_base( self ): + hda_model = model.HistoryDatasetAssociation + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + hda1 = self.hda_mgr.create( self.trans, history=history1, hid=1 ) + hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) + hda3 = self.hda_mgr.create( self.trans, history=history1, hid=3 ) + + print "should be able to query" + hdas = self.trans.sa_session.query( hda_model ).all() + self.assertEqual( self.hda_mgr.list( self.trans ), hdas ) + self.assertEqual( self.hda_mgr.one( self.trans, filters=( hda_model.id == hda1.id ) ), hda1 ) + self.assertEqual( self.hda_mgr.by_id( self.trans, hda1.id ), hda1 ) + self.assertEqual( self.hda_mgr.by_ids( self.trans, [ hda2.id, hda1.id ] ), [ hda2, hda1 ] ) + + print "should be able to limit and offset" + self.assertEqual( self.hda_mgr.list( self.trans, limit=1 ), hdas[0:1] ) + self.assertEqual( self.hda_mgr.list( self.trans, offset=1 ), hdas[1:] ) + self.assertEqual( self.hda_mgr.list( self.trans, limit=1, offset=1 ), hdas[1:2] ) + + self.assertEqual( self.hda_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.hda_mgr.list( self.trans, offset=3 ), [] ) + + print "should be able to order" + self.assertEqual( self.hda_mgr.list( self.trans, order_by=sqlalchemy.desc( hda_model.create_time ) ), + [ hda3, hda2, hda1 ] ) + + def test_delete( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + + print "should be able to delete and undelete an hda" + self.assertFalse( item1.deleted ) + self.assertEqual( self.hda_mgr.delete( self.trans, item1 ), item1 ) + self.assertTrue( item1.deleted ) + self.assertEqual( self.hda_mgr.undelete( self.trans, item1 ), item1 ) + self.assertFalse( item1.deleted ) + + def test_purge_allowed( self ): + self.trans.app.config.allow_user_dataset_purge = True + + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + + print "should purge an hda if config does allow" + self.assertFalse( item1.purged ) + self.assertEqual( self.hda_mgr.purge( self.trans, item1 ), item1 ) + self.assertTrue( item1.purged ) + + def test_purge_not_allowed( self ): + self.trans.app.config.allow_user_dataset_purge = False + self.assertRaises( exceptions.ConfigDoesNotAllowException, + self.hda_mgr.error_unless_dataset_purge_allowed, self.trans, None ) + + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + + print "should raise an error when purging an hda if config does not allow" + self.assertFalse( item1.purged ) + self.assertRaises( exceptions.ConfigDoesNotAllowException, self.hda_mgr.purge, self.trans, item1 ) + self.assertFalse( item1.purged ) + + #def test_ownable( self ): + # owner = self.user_mgr.create( self.trans, **user2_data ) + # non_owner = self.user_mgr.create( self.trans, **user3_data ) + # + # history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + # dataset1 = self.dataset_mgr.create( self.trans ) + # item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + # + # print "should be able to poll whether a given user owns an item" + # self.assertTrue( self.hda_mgr.is_owner( self.trans, item1, owner ) ) + # self.assertFalse( self.hda_mgr.is_owner( self.trans, item1, non_owner ) ) + # + # print "should raise an error when checking ownership with non-owner" + # self.assertRaises( exceptions.ItemOwnershipException, + # self.hda_mgr.check_ownership, self.trans, item1, non_owner ) + # + # print "should not raise an error when checking ownership with owner" + # self.assertEqual( self.hda_mgr.check_ownership( self.trans, item1, owner ), item1 ) + # + # #print "should not raise an error when checking ownership with admin" + # #self.assertEqual( self.hda_mgr.check_ownership( self.trans, item1, self.admin_user ), item1 ) + + def test_accessible( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + + print "(by default, dataset permissions are lax) should be accessible to all" + for user in self.user_mgr.list( self.trans ): + self.assertTrue( self.hda_mgr.is_accessible( self.trans, item1, user ) ) + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() diff --git a/test/unit/managers/test_HistoryManager.py b/test/unit/managers/test_HistoryManager.py new file mode 100644 index 00000000000..9e68890fe84 --- /dev/null +++ b/test/unit/managers/test_HistoryManager.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python +""" +""" +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +from galaxy.util.bunch import Bunch + +import mock +from test_ModelManager import BaseTestCase +from galaxy.managers.histories import HistoryManager + + +# ============================================================================= +default_password = '123456' +user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) +user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) + + +# ============================================================================= +class HistoryManagerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( HistoryManagerTestCase, self ).set_up_managers() + self.history_mgr = HistoryManager( self.app ) + + def test_base( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + user3 = self.user_mgr.create( self.trans, **user3_data ) + + print "should be able to create a new history" + history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + self.assertIsInstance( history1, model.History ) + self.assertEqual( history1, self.trans.sa_session.query( model.History ).get( history1.id ) ) + self.assertEqual( history1, + self.trans.sa_session.query( model.History ).filter( model.History.name == 'history1' ).one() ) + self.assertEqual( history1, + self.trans.sa_session.query( model.History ).filter( model.History.user == user2 ).one() ) + + print "should be able to copy a history" + history2 = self.history_mgr.copy( self.trans, history1, user=user3 ) + self.assertIsInstance( history2, model.History ) + self.assertEqual( history2.user, user3 ) + self.assertEqual( history2, self.trans.sa_session.query( model.History ).get( history2.id ) ) + self.assertEqual( history2.name, history1.name ) + self.assertNotEqual( history2, history1 ) + + print "should be able to query" + histories = self.trans.sa_session.query( model.History ).all() + self.assertEqual( self.history_mgr.one( self.trans, filters=( model.History.id == history1.id ) ), history1 ) + self.assertEqual( self.history_mgr.list( self.trans ), histories ) + self.assertEqual( self.history_mgr.by_id( self.trans, history1.id ), history1 ) + self.assertEqual( self.history_mgr.by_ids( self.trans, [ history2.id, history1.id ] ), [ history2, history1 ] ) + + print "should be able to limit and offset" + self.assertEqual( self.history_mgr.list( self.trans, limit=1 ), histories[0:1] ) + self.assertEqual( self.history_mgr.list( self.trans, offset=1 ), histories[1:] ) + self.assertEqual( self.history_mgr.list( self.trans, limit=1, offset=1 ), histories[1:2] ) + + self.assertEqual( self.history_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.history_mgr.list( self.trans, offset=3 ), [] ) + + print "should be able to order" + history3 = self.history_mgr.create( self.trans, name="history3", user=user2 ) + name_first_then_time = ( model.History.name, sqlalchemy.desc( model.History.create_time ) ) + self.assertEqual( self.history_mgr.list( self.trans, order_by=name_first_then_time ), + [ history2, history1, history3 ] ) + + def test_has_user( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + item1 = self.history_mgr.create( self.trans, user=owner ) + item2 = self.history_mgr.create( self.trans, user=owner ) + item3 = self.history_mgr.create( self.trans, user=non_owner ) + + print "should be able to list items by user" + user_histories = self.history_mgr.by_user( self.trans, owner ) + self.assertEqual( user_histories, [ item1, item2 ] ) + + query = self.history_mgr._query_by_user( self.trans, owner ) + self.assertEqual( query.all(), user_histories ) + + def test_ownable( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + item1 = self.history_mgr.create( self.trans, user=owner ) + + print "should be able to poll whether a given user owns an item" + self.assertTrue( self.history_mgr.is_owner( self.trans, item1, owner ) ) + self.assertFalse( self.history_mgr.is_owner( self.trans, item1, non_owner ) ) + + print "should raise an error when checking ownership with non-owner" + self.assertRaises( exceptions.ItemOwnershipException, + self.history_mgr.error_unless_owner, self.trans, item1, non_owner ) + self.assertRaises( exceptions.ItemOwnershipException, + self.history_mgr.ownership_by_id, self.trans, item1.id, non_owner ) + + print "should not raise an error when checking ownership with owner" + self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, owner ), item1 ) + self.assertEqual( self.history_mgr.ownership_by_id( self.trans, item1.id, owner ), item1 ) + + print "should not raise an error when checking ownership with admin" + self.assertTrue( self.history_mgr.is_owner( self.trans, item1, self.admin_user ) ) + self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, self.admin_user ), item1 ) + self.assertEqual( self.history_mgr.ownership_by_id( self.trans, item1.id, self.admin_user ), item1 ) + + def test_accessible( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + item1 = self.history_mgr.create( self.trans, user=owner ) + + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + print "should be inaccessible by default except to owner" + self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, owner ) ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, self.admin_user ) ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) + + print "should raise an error when checking accessibility with non-owner" + self.assertRaises( exceptions.ItemAccessibilityException, + self.history_mgr.error_unless_accessible, self.trans, item1, non_owner ) + self.assertRaises( exceptions.ItemAccessibilityException, + self.history_mgr.accessible_by_id, self.trans, item1.id, non_owner ) + + print "should not raise an error when checking ownership with owner" + self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, owner ), item1 ) + self.assertEqual( self.history_mgr.accessible_by_id( self.trans, item1.id, owner ), item1 ) + + print "should not raise an error when checking ownership with admin" + self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, self.admin_user ) ) + self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, self.admin_user ), item1 ) + self.assertEqual( self.history_mgr.accessible_by_id( self.trans, item1.id, self.admin_user ), item1 ) + + def test_importable( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + self.trans.set_user( owner ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + item1 = self.history_mgr.create( self.trans, user=owner ) + + print "should not be importable by default" + self.assertFalse( item1.importable ) + self.assertIsNone( item1.slug ) + + print "should be able to make importable (accessible by link) to all users" + accessible = self.history_mgr.make_importable( self.trans, item1 ) + self.assertEqual( accessible, item1 ) + self.assertIsNotNone( accessible.slug ) + self.assertTrue( accessible.importable ) + + for user in self.user_mgr.list( self.trans ): + self.assertTrue( self.history_mgr.is_accessible( self.trans, accessible, user ) ) + + print "should be able to make non-importable/inaccessible again" + inaccessible = self.history_mgr.make_non_importable( self.trans, accessible ) + self.assertEqual( inaccessible, accessible ) + self.assertIsNotNone( inaccessible.slug ) + self.assertFalse( inaccessible.importable ) + + self.assertTrue( self.history_mgr.is_accessible( self.trans, inaccessible, owner ) ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, inaccessible, non_owner ) ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, inaccessible, self.admin_user ) ) + + def test_published( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + self.trans.set_user( owner ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + item1 = self.history_mgr.create( self.trans, user=owner ) + + print "should not be published by default" + self.assertFalse( item1.published ) + self.assertIsNone( item1.slug ) + + print "should be able to publish (listed publicly) to all users" + published = self.history_mgr.publish( self.trans, item1 ) + self.assertEqual( published, item1 ) + self.assertTrue( published.published ) + # note: publishing sets importable to true as well + self.assertTrue( published.importable ) + self.assertIsNotNone( published.slug ) + + for user in self.user_mgr.list( self.trans ): + self.assertTrue( self.history_mgr.is_accessible( self.trans, published, user ) ) + + print "should be able to make non-importable/inaccessible again" + unpublished = self.history_mgr.unpublish( self.trans, published ) + self.assertEqual( unpublished, published ) + self.assertFalse( unpublished.published ) + # note: unpublishing does not make non-importable, you must explicitly do that separately + self.assertTrue( published.importable ) + self.history_mgr.make_non_importable( self.trans, unpublished ) + self.assertFalse( published.importable ) + # note: slug still remains after unpublishing + self.assertIsNotNone( unpublished.slug ) + + self.assertTrue( self.history_mgr.is_accessible( self.trans, unpublished, owner ) ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, unpublished, non_owner ) ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, unpublished, self.admin_user ) ) + + def test_sharable( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + self.trans.set_user( owner ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + item1 = self.history_mgr.create( self.trans, user=owner ) + + print "should be unshared by default" + self.assertEqual( self.history_mgr.get_share_assocs( self.trans, item1 ), [] ) + + print "should be able to share with specific users" + shared_item = self.history_mgr.share_with( self.trans, item1, non_owner ) + self.assertEqual( shared_item, item1 ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, shared_item, non_owner ) ) + self.assertEqual( + len( self.history_mgr.get_share_assocs( self.trans, shared_item ) ), 1 ) + self.assertEqual( + len( self.history_mgr.get_share_assocs( self.trans, shared_item, user=non_owner ) ), 1 ) + + print "should be able to unshare with specific users" + unshared_item = self.history_mgr.unshare_with( self.trans, shared_item, non_owner ) + self.assertEqual( shared_item, unshared_item ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, shared_item, non_owner ) ) + self.assertEqual( self.history_mgr.get_share_assocs( self.trans, unshared_item ), [] ) + self.assertEqual( + self.history_mgr.get_share_assocs( self.trans, unshared_item, user=non_owner ), [] ) + + #TODO: test slug formation + + def test_anon( self ): + anon_user = None + self.trans.set_user( anon_user ) + + print "should not allow access and owner for anon user on a history by another anon user (None)" + anon_history1 = self.history_mgr.create( self.trans, user=None ) + self.assertFalse( self.history_mgr.is_owner( self.trans, anon_history1, anon_user ) ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, anon_history1, anon_user ) ) + + print "should allow access and owner for anon user on a history if it's the session's current history" + anon_history2 = self.history_mgr.create( self.trans, user=anon_user ) + self.trans.set_history( anon_history2 ) + self.assertTrue( self.history_mgr.is_owner( self.trans, anon_history2, anon_user ) ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, anon_history2, anon_user ) ) + + print "should not allow owner or access for anon user on someone elses history" + owner = self.user_mgr.create( self.trans, **user2_data ) + someone_elses = self.history_mgr.create( self.trans, user=owner ) + self.assertFalse( self.history_mgr.is_owner( self.trans, someone_elses, anon_user ) ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) + + print "should allow access for anon user if the history is published or importable" + self.history_mgr.make_importable( self.trans, someone_elses ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) + self.history_mgr.publish( self.trans, someone_elses ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) + + def test_delete_and_purge( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + self.trans.set_user( user2 ) + + history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + self.trans.set_history( history1 ) + + self.assertFalse( history1.deleted ) + + self.history_mgr.delete( self.trans, history1 ) + self.assertTrue( history1.deleted ) + + self.history_mgr.undelete( self.trans, history1 ) + self.assertFalse( history1.deleted ) + + history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) + self.history_mgr.purge( self.trans, history1 ) + self.assertTrue( history1.purged ) + + def test_histories( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + self.trans.set_user( user2 ) + + history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + self.trans.set_history( history1 ) + history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) + + print "should be able to set or get the current history for a user" + self.assertEqual( self.history_mgr.get_current( self.trans ), history1 ) + self.assertEqual( self.history_mgr.set_current( self.trans, history2 ), history2 ) + self.assertEqual( self.history_mgr.get_current( self.trans ), history2 ) + self.assertEqual( self.history_mgr.set_current_by_id( self.trans, history1.id ), history1 ) + self.assertEqual( self.history_mgr.get_current( self.trans ), history1 ) + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() diff --git a/test/unit/managers/test_ModelManager.py b/test/unit/managers/test_ModelManager.py new file mode 100644 index 00000000000..aef85ebe868 --- /dev/null +++ b/test/unit/managers/test_ModelManager.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +""" +""" +from __future__ import print_function + +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +from galaxy.util.bunch import Bunch + +import mock +from galaxy.managers.users import UserManager + + +# ============================================================================= +admin_email = 'admin@admin.admin' +admin_users = admin_email +default_password = '123456' + +#def setUpModule(): +# print '=' * 20, 'begin module' +# +#def tearDownModule(): +# print '=' * 20, 'end module' + + +# ============================================================================= +class BaseTestCase( unittest.TestCase ): + + @classmethod + def setUpClass( cls ): + print( '\n', '-' * 20, 'begin class', cls ) + + @classmethod + def tearDownClass( cls ): + print( '\n', '-' * 20, 'end class', cls ) + + def __init__( self, *args ): + unittest.TestCase.__init__( self, *args ) + + def setUp( self ): + self.log( '.' * 20, 'begin test', self ) + self.set_up_mocks() + self.set_up_managers() + self.set_up_trans() + + def set_up_mocks( self ): + self.trans = mock.MockTrans( admin_users=admin_users ) + self.app = self.trans.app + + def set_up_managers( self ): + self.user_mgr = UserManager( self.app ) + + def set_up_trans( self ): + self.admin_user = self.user_mgr.create( self.trans, + email=admin_email, username='admin', password=default_password ) + self.trans.set_user( self.admin_user ) + self.trans.set_history( None ) + + def tearDown( self ): + self.log( '.' * 20, 'end test', self, '\n' ) + + def log( self, *args, **kwargs ): + print( *args, **kwargs ) + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() diff --git a/test/unit/managers/test_UserManager.py b/test/unit/managers/test_UserManager.py new file mode 100644 index 00000000000..174e811fb67 --- /dev/null +++ b/test/unit/managers/test_UserManager.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +""" +""" +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions +from galaxy.util.bunch import Bunch + +import mock +from test_ModelManager import BaseTestCase + + +# ============================================================================= +admin_email = 'admin@admin.admin' +admin_users = admin_email +default_password = '123456' +user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) +user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) + + +# ============================================================================= +class UserManagerTestCase( BaseTestCase ): + + def test_framework( self ): + self.log( "(for testing) should have admin_user, and admin_user is current" ) + self.assertEqual( self.trans.user, self.admin_user ) + + def test_base( self ): + self.log( "should be able to create a user" ) + user2 = self.user_mgr.create( self.trans, **user2_data ) + self.assertIsInstance( user2, model.User ) + self.assertIsNotNone( user2.id ) + self.assertEqual( user2.email, user2_data[ 'email' ] ) + self.assertEqual( user2.password, default_password ) + + user3 = self.user_mgr.create( self.trans, **user3_data ) + + self.log( "should be able to query" ) + users = self.trans.sa_session.query( model.User ).all() + self.assertEqual( self.user_mgr.list( self.trans ), users ) + + self.assertEqual( self.user_mgr.by_id( self.trans, user2.id ), user2 ) + self.assertEqual( self.user_mgr.by_ids( self.trans, [ user3.id, user2.id ] ), [ user3, user2 ] ) + + self.log( "should be able to limit and offset" ) + self.assertEqual( self.user_mgr.list( self.trans, limit=1 ), users[0:1] ) + self.assertEqual( self.user_mgr.list( self.trans, offset=1 ), users[1:] ) + self.assertEqual( self.user_mgr.list( self.trans, limit=1, offset=1 ), users[1:2] ) + + self.assertEqual( self.user_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.user_mgr.list( self.trans, offset=3 ), [] ) + + self.log( "should be able to order" ) + self.assertEqual( self.user_mgr.list( self.trans, order_by=( sqlalchemy.desc( model.User.create_time ) ) ), + [ user3, user2, self.admin_user ] ) + + def test_invalid_create( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + + self.log( "emails must be unique" ) + self.assertRaises( exceptions.Conflict, self.user_mgr.create, + self.trans, **dict( email='user2@user2.user2', username='user2a', password=default_password ) ) + self.log( "usernames must be unique" ) + self.assertRaises( exceptions.Conflict, self.user_mgr.create, + self.trans, **dict( email='user2a@user2.user2', username='user2', password=default_password ) ) + + def test_email_queries( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + user3 = self.user_mgr.create( self.trans, **user3_data ) + + self.log( "should be able to query by email" ) + self.assertEqual( self.user_mgr.by_email( self.trans, user2_data[ 'email' ] ), user2 ) + + #note: sorted by email alpha + users = self.trans.sa_session.query( model.User ).all() + self.assertEqual( self.user_mgr.by_email_like( self.trans, '%@%' ), [ self.admin_user, user2, user3 ] ) + + def test_admin( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + user3 = self.user_mgr.create( self.trans, **user3_data ) + + self.log( "should be able to test whether admin" ) + self.assertTrue( self.user_mgr.is_admin( self.trans, self.admin_user ) ) + self.assertFalse( self.user_mgr.is_admin( self.trans, user2 ) ) + self.assertEqual( self.user_mgr.admins( self.trans ), [ self.admin_user ] ) + self.assertRaises( exceptions.AdminRequiredException, self.user_mgr.error_unless_admin, self.trans, user2 ) + self.assertEqual( self.user_mgr.error_unless_admin( self.trans, self.admin_user ), self.admin_user ) + + def test_anonymous( self ): + anon = None + user2 = self.user_mgr.create( self.trans, **user2_data ) + + self.log( "should be able to tell if a user is anonymous" ) + self.assertRaises( exceptions.AuthenticationFailed, self.user_mgr.error_if_anonymous, self.trans, anon ) + self.assertEqual( self.user_mgr.error_if_anonymous( self.trans, user2 ), user2 ) + + def test_current( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + user3 = self.user_mgr.create( self.trans, **user3_data ) + + self.log( "should be able to tell if a user is the current (trans) user" ) + self.assertTrue( self.user_mgr.is_current_user( self.trans, self.admin_user ) ) + self.assertFalse( self.user_mgr.is_current_user( self.trans, user2 ) ) + + def test_api_keys( self ): + user2 = self.user_mgr.create( self.trans, **user2_data ) + user3 = self.user_mgr.create( self.trans, **user3_data ) + + self.log( "should be able to generate and retrieve api keys" ) + user2_api_key = self.user_mgr.create_api_key( self.trans, user2 ) + #self.assertIsInstance( user2_api_key, model.APIKeys ) + self.assertIsInstance( user2_api_key, basestring ) + self.assertEqual( self.user_mgr.api_key( self.trans, user2 ).key, user2_api_key ) + + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() From 7b6b044ff32b3b959c2a826108bcdd6273cdf55e Mon Sep 17 00:00:00 2001 From: Eric Rasche Date: Wed, 17 Dec 2014 15:25:07 -0600 Subject: [PATCH 009/473] Allow for shared secret between Galaxy and upstream proxies This prevents an impersonation attack available on Galaxies with command line users and REMOTE_USER authentication. It is not uncommon to run Galaxy on a "lab server" (or similar), where there are command line users in addition to the Galaxy service. With REMOTE_USER authentication, Galaxy blindly (and correctly) trusts the upstream value of REMOTE_USER, enabling such attacks as: curl -H "REMOTE_USER: admin.user@domain.edu" http://localhost:8000/galaxy/ This changest adds a shared secret between Galaxy and the upstream proxy, thereby preventing that attack. Only requests from the upstream proxy with the correct secret key are recognised, everything else is tossed out with a 403 Forbidden. This was implemented as part of the REMOTE_USER stack as the situation only applies if that middleware is in use. --- config/galaxy.ini.sample | 7 ++++ config/tool_shed.ini.sample | 7 ++++ lib/galaxy/config.py | 1 + lib/galaxy/util/__init__.py | 2 ++ .../web/framework/middleware/remoteuser.py | 32 ++++++++++++++++- lib/galaxy/webapps/galaxy/buildapp.py | 4 +-- lib/galaxy/webapps/tool_shed/buildapp.py | 3 +- lib/galaxy/webapps/tool_shed/config.py | 1 + .../framework/middleware/remoteuser.py | 34 ++++++++++++++++++- 9 files changed, 86 insertions(+), 5 deletions(-) diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index b5da4842240..8c7a602203b 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -713,6 +713,13 @@ use_interactive = True # *not* include 'HTTP_' at the beginning of the header name. #remote_user_header = HTTP_REMOTE_USER +# If use_remote_user is enabled, anyone who can log in to the Galaxy host may +# impersonate any other user by simply sending the appropriate header. Thus a +# secret shared between the upstream proxy server, and Galaxy is required. +# If anyone other than the Galaxy user is using the server, then apache/nginx should +# pass a value in the header 'GX_SECRET' that is identical the one below +#remote_user_secret = USING THE DEFAULT IS NOT SECURE! + # If use_remote_user is enabled, you can set this to a URL that will log your # users out. #remote_user_logout_href = None diff --git a/config/tool_shed.ini.sample b/config/tool_shed.ini.sample index 23b15b16e65..e53b82c0e68 100644 --- a/config/tool_shed.ini.sample +++ b/config/tool_shed.ini.sample @@ -67,6 +67,13 @@ id_secret = changethisinproductiontoo # https://wiki.galaxyproject.org/Admin/Config/ApacheProxy #use_remote_user = False +# If use_remote_user is enabled, anyone who can log in to the Galaxy host may +# impersonate any other user by simply sending the appropriate header. Thus a +# secret shared between the upstream proxy server, and Galaxy is required. +# If anyone other than the Galaxy user is using the server, then apache/nginx should +# pass a value in the header 'GX_SECRET' that is identical the one below +#remote_user_secret = changethisinproductiontoo + # Configuration for debugging middleware debug = true use_lint = false diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index bf1cca19dee..85fe1297606 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -132,6 +132,7 @@ class Configuration( object ): self.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None ) self.remote_user_header = kwargs.get( "remote_user_header", 'HTTP_REMOTE_USER' ) self.remote_user_logout_href = kwargs.get( "remote_user_logout_href", None ) + self.remote_user_secret = kwargs.get( "remote_user_secret", None ) self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) ) self.allow_user_deletion = string_as_bool( kwargs.get( "allow_user_deletion", "False" ) ) diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 6e957f2b07a..ed55de106bd 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -1119,6 +1119,8 @@ def move_merge( source, target ): def safe_str_cmp(a, b): + """safely compare two strings in a timing-attack-resistant manner + """ if len(a) != len(b): return False rv = 0 diff --git a/lib/galaxy/web/framework/middleware/remoteuser.py b/lib/galaxy/web/framework/middleware/remoteuser.py index 40271cf4b99..3d1d2adb42a 100644 --- a/lib/galaxy/web/framework/middleware/remoteuser.py +++ b/lib/galaxy/web/framework/middleware/remoteuser.py @@ -3,6 +3,7 @@ Middleware for handling $REMOTE_USER if use_remote_user is enabled. """ import socket +from galaxy.util import safe_str_cmp errorpage = """ @@ -37,12 +38,13 @@ errorpage = """ class RemoteUser( object ): - def __init__( self, app, maildomain=None, display_servers=None, admin_users=None, remote_user_header=None ): + def __init__( self, app, maildomain=None, display_servers=None, admin_users=None, remote_user_header=None, remote_user_secret_header=None ): self.app = app self.maildomain = maildomain self.display_servers = display_servers or [] self.admin_users = admin_users or [] self.remote_user_header = remote_user_header or 'HTTP_REMOTE_USER' + self.config_secret_header = remote_user_secret_header def __call__( self, environ, start_response ): # Allow display servers @@ -59,6 +61,34 @@ class RemoteUser( object ): # Rewrite* method for passing REMOTE_USER and a user is # un-authenticated. Any other possible values need to go here as well. path_info = environ.get('PATH_INFO', '') + + # If the secret header is enabled, we expect upstream to send along some key + # in HTTP_GX_SECRET, so we'll need to compare that here to the correct value + # + # This is not an ideal location for this function. The reason being + # that because this check is done BEFORE the REMOTE_USER check, it is + # possible to attack the GX_SECRET key without having correct + # credentials. However, that's why it's not "ideal", but it is "good + # enough". The only users able to exploit this are ones with access to + # the local system (unless Galaxy is listening on 0.0.0.0....). It + # seems improbable that an attacker with access to the server hosting + # Galaxy would not have acess to Galaxy itself, and be attempting to + # attack the system + if self.config_secret_header is not None: + if not safe_str_cmp(environ.get('HTTP_GX_SECRET'), self.config_secret_header): + title = "Access to Galaxy is denied" + message = """ + Galaxy is configured to authenticate users via an external + method (such as HTTP authentication in Apache), but an + incorrect shared secret key was provided by the + upstream (proxy) server.

+

Please contact your local Galaxy administrator. The + variable remote_user_secret and + GX_SECRET header must be set before you may + access Galaxy. + """ + return self.error( start_response, title, message ) + if not environ.get(self.remote_user_header, '(null)').startswith('(null)'): if not environ[ self.remote_user_header ].count( '@' ): if self.maildomain is not None: diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 7015b23627a..ca31b096ed3 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -503,8 +503,8 @@ def wrap_in_middleware( app, global_conf, **local_conf ): app = RemoteUser( app, maildomain = conf.get( 'remote_user_maildomain', None ), display_servers = util.listify( conf.get( 'display_servers', '' ) ), admin_users = conf.get( 'admin_users', '' ).split( ',' ), - remote_user_header = conf.get( 'remote_user_header', 'HTTP_REMOTE_USER' ) ) - log.debug( "Enabling 'remote user' middleware" ) + remote_user_header = conf.get( 'remote_user_header', 'HTTP_REMOTE_USER' ), + remote_user_secret_header = conf.get('remote_user_secret', None) ) # The recursive middleware allows for including requests in other # requests or forwarding of requests, all on the server side. if asbool(conf.get('use_recursive', True)): diff --git a/lib/galaxy/webapps/tool_shed/buildapp.py b/lib/galaxy/webapps/tool_shed/buildapp.py index a52cbbd87dd..f69ae1e0951 100644 --- a/lib/galaxy/webapps/tool_shed/buildapp.py +++ b/lib/galaxy/webapps/tool_shed/buildapp.py @@ -157,7 +157,8 @@ def wrap_in_middleware( app, global_conf, **local_conf ): from galaxy.webapps.tool_shed.framework.middleware.remoteuser import RemoteUser app = RemoteUser( app, maildomain = conf.get( 'remote_user_maildomain', None ), display_servers = util.listify( conf.get( 'display_servers', '' ) ), - admin_users = conf.get( 'admin_users', '' ).split( ',' ) ) + admin_users = conf.get( 'admin_users', '' ).split( ',' ), + remote_user_secret_header = conf.get('remote_user_secret', None) ) log.debug( "Enabling 'remote user' middleware" ) # The recursive middleware allows for including requests in other # requests or forwarding of requests, all on the server side. diff --git a/lib/galaxy/webapps/tool_shed/config.py b/lib/galaxy/webapps/tool_shed/config.py index 4d097227720..b1b8b433285 100644 --- a/lib/galaxy/webapps/tool_shed/config.py +++ b/lib/galaxy/webapps/tool_shed/config.py @@ -83,6 +83,7 @@ class Configuration( object ): self.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None ) self.remote_user_header = kwargs.get( "remote_user_header", 'HTTP_REMOTE_USER' ) self.remote_user_logout_href = kwargs.get( "remote_user_logout_href", None ) + self.remote_user_secret = kwargs.get( "remote_user_secret", None ) self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) ) self.allow_user_deletion = string_as_bool( kwargs.get( "allow_user_deletion", "False" ) ) diff --git a/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py b/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py index cfca35e853b..8c885da383a 100644 --- a/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py +++ b/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py @@ -3,6 +3,7 @@ Middleware for handling $REMOTE_USER if use_remote_user is enabled. """ import socket +from galaxy.util import safe_str_cmp errorpage = """ @@ -36,11 +37,13 @@ errorpage = """ """ class RemoteUser( object ): - def __init__( self, app, maildomain=None, display_servers=None, admin_users=None ): + def __init__( self, app, maildomain=None, display_servers=None, admin_users=None, remote_user_secret_header=None ): self.app = app self.maildomain = maildomain self.display_servers = display_servers or [] self.admin_users = admin_users or [] + self.config_secret_header = remote_user_secret_header + def __call__( self, environ, start_response ): environ[ 'webapp' ] = 'tool_shed' # Allow display servers @@ -53,6 +56,34 @@ class RemoteUser( object ): if host in self.display_servers: environ[ 'HTTP_REMOTE_USER' ] = 'remote_display_server@%s' % ( self.maildomain or 'example.org' ) return self.app( environ, start_response ) + + # If the secret header is enabled, we expect upstream to send along some key + # in HTTP_GX_SECRET, so we'll need to compare that here to the correct value + # + # This is not an ideal location for this function. The reason being + # that because this check is done BEFORE the REMOTE_USER check, it is + # possible to attack the GX_SECRET key without having correct + # credentials. However, that's why it's not "ideal", but it is "good + # enough". The only users able to exploit this are ones with access to + # the local system (unless Galaxy is listening on 0.0.0.0....). It + # seems improbable that an attacker with access to the server hosting + # Galaxy would not have acess to Galaxy itself, and be attempting to + # attack the system + if self.config_secret_header is not None: + if not safe_str_cmp(environ.get('HTTP_GX_SECRET'), self.config_secret_header): + title = "Access to Galaxy is denied" + message = """ + Galaxy is configured to authenticate users via an external + method (such as HTTP authentication in Apache), but an + incorrect shared secret key was provided by the + upstream (proxy) server.

+

Please contact your local Galaxy administrator. The + variable remote_user_secret and + GX_SECRET header must be set before you may + access Galaxy. + """ + return self.error( start_response, title, message ) + # Apache sets REMOTE_USER to the string '(null)' when using the Rewrite* method for passing REMOTE_USER and a user is # un-authenticated. Any other possible values need to go here as well. path_info = environ.get('PATH_INFO', '') @@ -88,6 +119,7 @@ class RemoteUser( object ):

Contact your local Galaxy tool shed administrator. """ return self.error( start_response, title, message ) + def error( self, start_response, title="Access denied", message="Contact your local Galaxy tool shed administrator." ): start_response( '403 Forbidden', [('Content-type', 'text/html')] ) return [errorpage % (title, message)] From 269e24397db4378ba298892213a016f25e669fe5 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 18 Dec 2014 09:47:08 -0500 Subject: [PATCH 010/473] Don't choke on tool versions switches with significant parameter changes. Just regenerate the tool state from the supplied parameters instead - seems to still perserve parameters on the tool form that are common between the versions because they are coming in throuh kwd. --- .../webapps/galaxy/controllers/tool_runner.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/controllers/tool_runner.py b/lib/galaxy/webapps/galaxy/controllers/tool_runner.py index 22212209e92..f7b028bab2a 100644 --- a/lib/galaxy/webapps/galaxy/controllers/tool_runner.py +++ b/lib/galaxy/webapps/galaxy/controllers/tool_runner.py @@ -80,14 +80,28 @@ class ToolRunner( BaseUIController ): message=message, status=status, redirect=redirect ) ) - params = galaxy.util.Params( kwd, sanitize = False ) #Sanitize parameters when substituting into command line via input wrappers - #do param translation here, used by datasource tools - if tool.input_translator: - tool.input_translator.translate( params ) + + def _validated_params_for( kwd ): + params = galaxy.util.Params( kwd, sanitize=False ) # Sanitize parameters when substituting into command line via input wrappers + #do param translation here, used by datasource tools + if tool.input_translator: + tool.input_translator.translate( params ) + return params + + params = _validated_params_for( kwd ) # We may be visiting Galaxy for the first time ( e.g., sending data from UCSC ), # so make sure to create a new history if we've never had one before. history = tool.get_default_history_by_trans( trans, create=True ) - template, vars = tool.handle_input( trans, params.__dict__ ) + try: + template, vars = tool.handle_input( trans, params.__dict__ ) + except KeyError: + # This error indicates (or at least can indicate) there was a + # problem with the stored tool_state - it is incompatible with + # this variant of the tool - possibly because the tool changed + # or because the tool version changed. + del kwd[ "tool_state" ] + params = _validated_params_for( kwd ) + template, vars = tool.handle_input( trans, params.__dict__ ) if len( params ) > 0: trans.log_event( "Tool params: %s" % ( str( params ) ), tool_id=tool_id ) add_frame = AddFrameData() From 948b8d0375c268d5680a9a77052f18b0990ad2a2 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 18 Dec 2014 10:21:42 -0500 Subject: [PATCH 011/473] Fix bug in 8d5089f2ba8183439a866026f3583c377f8bfa74. --- lib/galaxy/workflow/run_request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 2c665473b61..86d82e341a1 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -56,13 +56,13 @@ def normalize_inputs(steps, inputs, inputs_by): possible_input_keys = [] for inputs_by_el in inputs_by.split("|"): if inputs_by_el == "step_id": - possible_input_keys = [str( step.id )] + possible_input_keys.append(str( step.id )) elif inputs_by_el == "step_index": - possible_input_keys = [str( step.order_index )] + possible_input_keys.append(str( step.order_index )) elif inputs_by_el == "step_uuid": - possible_input_keys = [str( step.uuid )] + possible_input_keys.append(str( step.uuid )) elif inputs_by_el == "name": - possible_input_keys = [step.tool_inputs.get( 'name', None )] + possible_input_keys.append(step.tool_inputs.get( 'name', None )) else: message = "Workflow cannot be run because unexpected inputs_by value specified." raise exceptions.MessageException( message ) From 461638a5707b17236fbf97db430b73b2961ca598 Mon Sep 17 00:00:00 2001 From: Eric Rasche Date: Thu, 18 Dec 2014 13:43:00 -0600 Subject: [PATCH 012/473] Fixed typos in documentation --- lib/galaxy/web/framework/middleware/remoteuser.py | 2 +- lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/web/framework/middleware/remoteuser.py b/lib/galaxy/web/framework/middleware/remoteuser.py index 3d1d2adb42a..d338328718f 100644 --- a/lib/galaxy/web/framework/middleware/remoteuser.py +++ b/lib/galaxy/web/framework/middleware/remoteuser.py @@ -72,7 +72,7 @@ class RemoteUser( object ): # enough". The only users able to exploit this are ones with access to # the local system (unless Galaxy is listening on 0.0.0.0....). It # seems improbable that an attacker with access to the server hosting - # Galaxy would not have acess to Galaxy itself, and be attempting to + # Galaxy would not have access to Galaxy itself, and be attempting to # attack the system if self.config_secret_header is not None: if not safe_str_cmp(environ.get('HTTP_GX_SECRET'), self.config_secret_header): diff --git a/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py b/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py index 8c885da383a..70b98837cd4 100644 --- a/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py +++ b/lib/galaxy/webapps/tool_shed/framework/middleware/remoteuser.py @@ -67,7 +67,7 @@ class RemoteUser( object ): # enough". The only users able to exploit this are ones with access to # the local system (unless Galaxy is listening on 0.0.0.0....). It # seems improbable that an attacker with access to the server hosting - # Galaxy would not have acess to Galaxy itself, and be attempting to + # Galaxy would not have access to Galaxy itself, and be attempting to # attack the system if self.config_secret_header is not None: if not safe_str_cmp(environ.get('HTTP_GX_SECRET'), self.config_secret_header): From 92846d0ae8464f9966913f03c06d6d39488dbb23 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 18 Dec 2014 21:50:15 -0500 Subject: [PATCH 013/473] PEP-8 fix for api/jobs.py. --- lib/galaxy/webapps/galaxy/api/jobs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 5b29af07892..adc933aa619 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -27,7 +27,7 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses index( trans, state=None, tool_id=None, history_id=None, date_range_min=None, date_range_max=None, user_details=False ) * GET /api/jobs: return jobs for current user - + !! if user is admin and user_details is True, then return jobs for all galaxy users based on filtering - this is an extended service @@ -41,10 +41,10 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :type user_details: boolean :param user_details: if true, and requestor is an admin, will return external job id and user email. - + :type date_range_min: string '2014-01-01' - :param date_range_min: limit the listing of jobs to those updated on or after requested date - + :param date_range_min: limit the listing of jobs to those updated on or after requested date + :type date_range_max: string '2014-12-31' :param date_range_max: limit the listing of jobs to those updated on or before requested date @@ -59,7 +59,7 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses is_extended_service = True else: is_extended_service = False - + if is_extended_service: query = trans.sa_session.query( trans.app.model.Job ) else: @@ -80,7 +80,7 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses query = build_and_apply_filters( query, kwd.get( 'tool_id', None ), lambda t: trans.app.model.Job.tool_id == t ) query = build_and_apply_filters( query, kwd.get( 'tool_id_like', None ), lambda t: trans.app.model.Job.tool_id.like(t) ) - + query = build_and_apply_filters( query, kwd.get( 'date_range_min', None ), lambda dmin: trans.app.model.Job.table.c.update_time >= dmin ) query = build_and_apply_filters( query, kwd.get( 'date_range_max', None ), lambda dmax: trans.app.model.Job.table.c.update_time <= dmax ) @@ -98,12 +98,12 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses else: order_by = trans.app.model.Job.update_time.desc() for job in query.order_by( order_by ).all(): - j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) + j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) if is_extended_service: j['external_id'] = job.job_runner_external_id j['user_email'] = job.user.email out.append(j) - + return out @expose_api From 285896dfe18aedeff483c4293b75cd1307382977 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 18 Dec 2014 21:50:15 -0500 Subject: [PATCH 014/473] Increase consistency between job index and show. Add tests for new date range and history filtering as well as to ensure only admins get to see external_id and command_line and that users cannot see each other's jobs. Always allow admins to views all jobs on index (instead of only when user_details is specified) and show. Add user_email and external_id to show (for admins) to bring it inline with index and add command_line to index (for admins) to bring it in line with show. Show still allow more details including job standard error and output as well as job metrics. --- lib/galaxy/model/__init__.py | 8 +++- lib/galaxy/web/security/__init__.py | 2 +- lib/galaxy/webapps/galaxy/api/jobs.py | 35 ++++++++------ test/api/test_jobs.py | 69 ++++++++++++++++++++++++--- test/base/api.py | 4 ++ test/base/api_asserts.py | 5 ++ 6 files changed, 100 insertions(+), 23 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index fddd941df44..fcb1587a3e2 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -520,9 +520,15 @@ class Job( object, HasJobMetrics, Dictifiable ): dataset.blurb = 'deleted' dataset.peek = 'Job deleted' dataset.info = 'Job output deleted by user before job completed' - def to_dict( self, view='collection' ): + + def to_dict( self, view='collection', system_details=False ): rval = super( Job, self ).to_dict( view=view ) rval['tool_id'] = self.tool_id + if system_details: + # System level details that only admins should have. + rval['external_id'] = self.job_runner_external_id + rval['command_line'] = self.command_line + if view == 'element': param_dict = dict( [ ( p.name, p.value ) for p in self.parameters ] ) rval['params'] = param_dict diff --git a/lib/galaxy/web/security/__init__.py b/lib/galaxy/web/security/__init__.py index 95998a7d21a..00a9e370050 100644 --- a/lib/galaxy/web/security/__init__.py +++ b/lib/galaxy/web/security/__init__.py @@ -73,7 +73,7 @@ class SecurityHelper( object ): if not isinstance( rval, dict ): return rval for k, v in rval.items(): - if ( k == 'id' or k.endswith( '_id' ) ) and v is not None and k not in [ 'tool_id' ]: + if ( k == 'id' or k.endswith( '_id' ) ) and v is not None and k not in [ 'tool_id', 'external_id' ]: try: rval[ k ] = self.encode_id( v ) except Exception: diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index adc933aa619..691baa73568 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -55,12 +55,10 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :returns: list of dictionaries containing summary job information """ state = kwd.get( 'state', None ) - if trans.user_is_admin() and kwd.get('user_details', False): - is_extended_service = True - else: - is_extended_service = False + is_admin = trans.user_is_admin() + user_details = kwd.get('user_details', False) - if is_extended_service: + if is_admin: query = trans.sa_session.query( trans.app.model.Job ) else: query = trans.sa_session.query( trans.app.model.Job ).filter(trans.app.model.Job.user == trans.user) @@ -98,9 +96,9 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses else: order_by = trans.app.model.Job.update_time.desc() for job in query.order_by( order_by ).all(): - j = self.encode_all_ids( trans, job.to_dict( 'collection' ), True ) - if is_extended_service: - j['external_id'] = job.job_runner_external_id + job_dict = job.to_dict( 'collection', system_details=is_admin ) + j = self.encode_all_ids( trans, job_dict, True ) + if user_details: j['user_email'] = job.user.email out.append(j) @@ -123,12 +121,13 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses :returns: dictionary containing full description of job data """ job = self.__get_job( trans, id ) - job_dict = self.encode_all_ids( trans, job.to_dict( 'element' ), True ) + is_admin = trans.user_is_admin() + job_dict = self.encode_all_ids( trans, job.to_dict( 'element', system_details=is_admin ), True ) full_output = util.asbool( kwd.get( 'full', 'false' ) ) if full_output: job_dict.update( dict( stderr=job.stderr, stdout=job.stdout ) ) - if trans.user_is_admin(): - job_dict['command_line'] = job.command_line + if is_admin: + job_dict['user_email'] = job.user.email def metric_to_dict(metric): metric_name = metric.metric_name @@ -199,10 +198,16 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses decoded_job_id = trans.security.decode_id( id ) except Exception: raise exceptions.MalformedId() - query = trans.sa_session.query( trans.app.model.Job ).filter( - trans.app.model.Job.user == trans.user, - trans.app.model.Job.id == decoded_job_id - ) + query = trans.sa_session.query( trans.app.model.Job ) + if trans.user_is_admin(): + query = query.filter( + trans.app.model.Job.id == decoded_job_id + ) + else: + query = query.filter( + trans.app.model.Job.user == trans.user, + trans.app.model.Job.id == decoded_job_id + ) job = query.first() if job is None: raise exceptions.ObjectNotFound() diff --git a/test/api/test_jobs.py b/test/api/test_jobs.py index b962f0b914b..d5d6c3d3d51 100644 --- a/test/api/test_jobs.py +++ b/test/api/test_jobs.py @@ -1,4 +1,6 @@ +import datetime import json +import time from operator import itemgetter from base import api @@ -11,14 +13,19 @@ class JobsApiTestCase( api.ApiTestCase, TestsDatasets ): def test_index( self ): # Create HDA to ensure at least one job exists... self.__history_with_new_dataset() - jobs_response = self._get( "jobs" ) - - self._assert_status_code_is( jobs_response, 200 ) - - jobs = jobs_response.json() - assert isinstance( jobs, list ) + jobs = self.__jobs_index() assert "upload1" in map( itemgetter( "tool_id" ), jobs ) + def test_system_details_admin_only( self ): + self.__history_with_new_dataset() + jobs = self.__jobs_index( admin=False ) + job = jobs[0] + self._assert_not_has_keys( job, "command_line", "external_id" ) + + jobs = self.__jobs_index( admin=True ) + job = jobs[0] + self._assert_has_keys( job, "command_line", "external_id" ) + def test_index_state_filter( self ): # Initial number of ok jobs original_count = len( self.__uploads_with_state( "ok" ) ) @@ -31,6 +38,33 @@ class JobsApiTestCase( api.ApiTestCase, TestsDatasets ): new_count = len( self.__uploads_with_state( "ok" ) ) assert original_count < new_count + def test_index_date_filter( self ): + self.__history_with_new_dataset() + two_weeks_ago = (datetime.datetime.utcnow() - datetime.timedelta(7)).isoformat() + last_week = (datetime.datetime.utcnow() - datetime.timedelta(7)).isoformat() + next_week = (datetime.datetime.utcnow() + datetime.timedelta(7)).isoformat() + today = datetime.datetime.utcnow().isoformat() + tomorrow = (datetime.datetime.utcnow() + datetime.timedelta(1)).isoformat() + + jobs = self.__jobs_index( data={"date_range_min": today[0:10], "date_range_max": tomorrow[0:10]} ) + assert len( jobs ) > 0 + today_job_id = jobs[0]["id"] + + jobs = self.__jobs_index( data={"date_range_min": two_weeks_ago, "date_range_max": last_week} ) + assert today_job_id not in map(itemgetter("id"), jobs) + + jobs = self.__jobs_index( data={"date_range_min": last_week, "date_range_max": next_week} ) + assert today_job_id in map(itemgetter("id"), jobs) + + def test_index_history( self ): + history_id, _ = self.__history_with_new_dataset() + jobs = self.__jobs_index( data={"history_id": history_id} ) + assert len( jobs ) > 0 + + history_id = self._new_history() + jobs = self.__jobs_index( data={"history_id": history_id} ) + assert len( jobs ) == 0 + def test_index_multiple_states_filter( self ): # Initial number of ok jobs original_count = len( self.__uploads_with_state( "ok", "new" ) ) @@ -58,6 +92,22 @@ class JobsApiTestCase( api.ApiTestCase, TestsDatasets ): job_details = show_jobs_response.json() self._assert_has_key( job_details, 'id', 'state', 'exit_code', 'update_time', 'create_time' ) + def test_show_security( self ): + history_id, _ = self.__history_with_new_dataset() + jobs_response = self._get( "jobs", data={"history_id": history_id} ) + job = jobs_response.json()[ 0 ] + job_id = job[ "id" ] + + show_jobs_response = self._get( "jobs/%s" % job_id, admin=False ) + self._assert_not_has_keys( show_jobs_response.json(), "command_line", "external_id" ) + + with self._different_user(): + show_jobs_response = self._get( "jobs/%s" % job_id, admin=False ) + self._assert_status_code_is( show_jobs_response, 404 ) + + show_jobs_response = self._get( "jobs/%s" % job_id, admin=True ) + self._assert_has_keys( show_jobs_response.json(), "command_line", "external_id" ) + def test_search( self ): history_id, dataset_id = self.__history_with_ok_dataset() @@ -133,3 +183,10 @@ class JobsApiTestCase( api.ApiTestCase, TestsDatasets ): history_id, dataset_id = self.__history_with_new_dataset() self._wait_for_history( history_id, assert_ok=True ) return history_id, dataset_id + + def __jobs_index( self, **kwds ): + jobs_response = self._get( "jobs", **kwds ) + self._assert_status_code_is( jobs_response, 200 ) + jobs = jobs_response.json() + assert isinstance( jobs, list ) + return jobs diff --git a/test/base/api.py b/test/base/api.py index 0a4d1f4f1ab..7ef7cebca2e 100644 --- a/test/base/api.py +++ b/test/base/api.py @@ -11,6 +11,7 @@ from .api_util import get_user_api_key from .api_asserts import ( assert_status_code_is, assert_has_keys, + assert_not_has_keys, assert_error_code_is, ) @@ -83,6 +84,9 @@ class ApiTestCase( TwillTestCase ): def _assert_has_keys( self, response, *keys ): assert_has_keys( response, *keys ) + def _assert_not_has_keys( self, response, *keys ): + assert_not_has_keys( response, *keys ) + def _assert_error_code_is( self, response, error_code ): assert_error_code_is( response, error_code ) diff --git a/test/base/api_asserts.py b/test/base/api_asserts.py index 129b63fca53..12f7b554eb9 100644 --- a/test/base/api_asserts.py +++ b/test/base/api_asserts.py @@ -20,6 +20,11 @@ def assert_has_keys( response, *keys ): assert key in response, "Response [%s] does not contain key [%s]" % ( response, key ) +def assert_not_has_keys( response, *keys ): + for key in keys: + assert key not in response, "Response [%s] contains invalid key [%s]" % ( response, key ) + + def assert_error_code_is( response, error_code ): if hasattr( response, "json" ): response = response.json() From ec62c5417c28db43755d667159f794f28d4c4694 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 19 Dec 2014 09:15:27 -0500 Subject: [PATCH 015/473] Replace preferred workflow usage terminology in the API (s/usage/invocation) `GET /api/workflows//usage` made sense for read-only operations - but now for instance `POST /api/workflows//usage` is the preferred method for invoking workflows and `PUT /api/workflows//usage//steps/` can be used to interact with steps while the workflow is running. I think the term is a little awkward in that sense and is incongruous with the rest of Galaxy which uses the terminology "invocations". In a backward compatible way this replaces the usage terminology with invocation terminology (and uses the plural in the route which is more consistent with the rest of the API - `POST /api/histories` for instance). --- lib/galaxy/webapps/galaxy/api/workflows.py | 57 ++++++++++---------- lib/galaxy/webapps/galaxy/buildapp.py | 60 +++++++++++++++++++--- 2 files changed, 83 insertions(+), 34 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 7dd5afea3c0..c0f394fcbb2 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -372,9 +372,9 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis return item @expose_api - def workflow_request( self, trans, workflow_id, payload, **kwd ): + def invoke( self, trans, workflow_id, payload, **kwd ): """ - POST /api/workflows/{encoded_workflow_id}/usage + POST /api/workflows/{encoded_workflow_id}/invocations Schedule the workflow specified by `workflow_id` to run. """ @@ -398,10 +398,11 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis return self.encode_all_ids( trans, workflow_invocation.to_dict(), recursive=True ) @expose_api - def workflow_usage(self, trans, workflow_id, **kwd): + def index_invocations(self, trans, workflow_id, **kwd): """ - GET /api/workflows/{workflow_id}/usage - Get the list of the workflow usage + GET /api/workflows/{workflow_id}/invocations + + Get the list of the workflow invocations :param workflow_id: the workflow id (required) :type workflow_id: str @@ -416,53 +417,53 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis return out @expose_api - def workflow_usage_contents(self, trans, workflow_id, usage_id, **kwd): + def show_invocation(self, trans, workflow_id, invocation_id, **kwd): """ - GET /api/workflows/{workflow_id}/usage/{usage_id} - Get detailed description of workflow usage + GET /api/workflows/{workflow_id}/invocation/{invocation_id} + Get detailed description of workflow invocation - :param workflow_id: the workflow id (required) - :type workflow_id: str + :param workflow_id: the workflow id (required) + :type workflow_id: str - :param usage_id: the usage id (required) - :type usage_id: str + :param invocation_id: the invocation id (required) + :type invocation_id: str :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_workflow_invocation_id = self.__decode_id( trans, usage_id ) + decoded_workflow_invocation_id = self.__decode_id( trans, invocation_id ) workflow_invocation = self.workflow_manager.get_invocation( trans, decoded_workflow_invocation_id ) if workflow_invocation: return self.__encode_invocation( trans, workflow_invocation ) return None @expose_api - def cancel_workflow_invocation(self, trans, workflow_id, usage_id, **kwd): + def cancel_invocation(self, trans, workflow_id, invocation_id, **kwd): """ - DELETE /api/workflows/{workflow_id}/usage/{usage_id} + DELETE /api/workflows/{workflow_id}/invocation/{invocation_id} Cancel the specified workflow invocation. :param workflow_id: the workflow id (required) :type workflow_id: str - :param usage_id: the usage id (required) - :type usage_id: str + :param invocation_id: the usage id (required) + :type invocation_id: str :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_workflow_invocation_id = self.__decode_id( trans, usage_id ) + decoded_workflow_invocation_id = self.__decode_id( trans, invocation_id ) workflow_invocation = self.workflow_manager.cancel_invocation( trans, decoded_workflow_invocation_id ) return self.__encode_invocation( trans, workflow_invocation ) @expose_api - def workflow_invocation_step(self, trans, workflow_id, usage_id, step_id, **kwd): + def invocation_step(self, trans, workflow_id, invocation_id, step_id, **kwd): """ - GET /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id} + GET /api/workflows/{workflow_id}/invocation/{invocation_id}/steps/{step_id} - :param workflow_id: the workflow id (required) - :type workflow_id: str + :param workflow_id: the workflow id (required) + :type workflow_id: str - :param usage_id: the usage id (required) - :type usage_id: str + :param invocation_id: the invocation id (required) + :type invocation_id: str :param step_id: encoded id of the WorkflowInvocationStep (required) :type step_id: str @@ -480,9 +481,9 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis return self.__encode_invocation_step( trans, invocation_step ) @expose_api - def workflow_invocation_step_update(self, trans, workflow_id, usage_id, step_id, payload, **kwd): + def update_invocation_step(self, trans, workflow_id, invocation_id, step_id, payload, **kwd): """ - PUT /api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id} + PUT /api/workflows/{workflow_id}/invocation/{invocation_id}/steps/{step_id} Update state of running workflow step invocation - still very nebulous but this would be for stuff like confirming paused steps can proceed etc.... @@ -491,8 +492,8 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis :param workflow_id: the workflow id (required) :type workflow_id: str - :param usage_id: the usage id (required) - :type usage_id: str + :param invocation_id: the usage id (required) + :type invocation_id: str :param step_id: encoded id of the WorkflowInvocationStep (required) :type step_id: str diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 7015b23627a..9864fc78c88 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -227,14 +227,62 @@ def populate_api_routes( webapp, app ): webapp.mapper.connect( 'workflow_dict', '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict( method=['GET'] ) ) # Deprecated in favor of POST /api/workflows with shared_workflow_id in payload. webapp.mapper.connect( 'import_shared_workflow_deprecated', '/api/workflows/import', controller='workflows', action='import_shared_workflow_deprecated', conditions=dict( method=['POST'] ) ) - webapp.mapper.connect( 'workflow_usage', '/api/workflows/{workflow_id}/usage', controller='workflows', action='workflow_usage', conditions=dict(method=['GET'])) - webapp.mapper.connect( 'workflow_usage_contents', '/api/workflows/{workflow_id}/usage/{usage_id}', controller='workflows', action='workflow_usage_contents', conditions=dict(method=['GET'])) - webapp.mapper.connect( 'cancel_workflow_invocation', '/api/workflows/{workflow_id}/usage/{usage_id}', controller='workflows', action='cancel_workflow_invocation', conditions=dict(method=['DELETE'])) - webapp.mapper.connect( 'workflow_invocation_step', '/api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}', controller='workflows', action='workflow_invocation_step', conditions=dict(method=['GET'])) - webapp.mapper.connect( 'workflow_invocation_step_update', '/api/workflows/{workflow_id}/usage/{usage_id}/steps/{step_id}', controller='workflows', action='workflow_invocation_step_update', conditions=dict(method=['PUT'])) + # API refers to usages and invocations - these mean the same thing but the + # usage routes should be considered deprecated. + invoke_names = { + "invocations": "", + "usage": "_deprecated", + } + for noun, suffix in invoke_names.iteritems(): + name = "%s%s" % (noun, suffix) + webapp.mapper.connect( + 'list_workflow_%s' % name, + '/api/workflows/{workflow_id}/%s' % noun, + controller='workflows', + action='index_invocations', + conditions=dict(method=['GET']) + ) - webapp.mapper.connect( 'workflow_request', '/api/workflows/{workflow_id}/usage', controller='workflows', action='workflow_request', conditions=dict( method=['POST'] ) ) + webapp.mapper.connect( + 'workflow_%s_contents' % name, + '/api/workflows/{workflow_id}/%s/{invocation_id}' % noun, + controller='workflows', + action='show_invocation', + conditions=dict(method=['GET']) + ) + + webapp.mapper.connect( + 'cancel_workflow_%s' % name, + '/api/workflows/{workflow_id}/%s/{invocation_id}' % noun, + controller='workflows', + action='cancel_invocation', + conditions=dict(method=['DELETE']) + ) + + webapp.mapper.connect( + 'workflow_%s_step' % name, + '/api/workflows/{workflow_id}/%s/{invocation_id}/steps/{step_id}' % noun, + controller='workflows', + action='invocation_step', + conditions=dict(method=['GET']) + ) + + webapp.mapper.connect( + 'workflow_%s_step_update' % name, + '/api/workflows/{workflow_id}/%s/{invocation_id}/steps/{step_id}' % noun, + controller='workflows', + action='update_invocation_step', + conditions=dict(method=['PUT']) + ) + + webapp.mapper.connect( + 'workflow_%s' % name, + '/api/workflows/{workflow_id}/%s' % noun, + controller='workflows', + action='invoke', + conditions=dict( method=['POST'] ) + ) # ============================ # ===== AUTHENTICATE API ===== # ============================ From 58f677ad4f4473d0549dd417f187cc1f7a3a9a4e Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 19 Dec 2014 16:56:34 -0500 Subject: [PATCH 016/473] Refining the hda manager, serializer, and de-serializer; using same in history contents and datasets api --- lib/galaxy/managers/base.py | 34 +- lib/galaxy/managers/collections.py | 122 +++- lib/galaxy/managers/collections_util.py | 8 +- lib/galaxy/managers/datasets.py | 9 + lib/galaxy/managers/hdas.py | 564 +++++++++++------- lib/galaxy/managers/histories.py | 60 +- lib/galaxy/model/__init__.py | 24 +- lib/galaxy/web/base/controller.py | 93 --- lib/galaxy/webapps/galaxy/api/datasets.py | 32 +- .../webapps/galaxy/api/history_contents.py | 251 ++++---- .../webapps/galaxy/controllers/history.py | 11 +- test/unit/managers/test_DatasetManager.py | 165 ++--- 12 files changed, 786 insertions(+), 587 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index ff4d97aac1b..c1e1d36366b 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -401,7 +401,8 @@ class ModelSerializer( object ): def serialize( self, trans, item, keys ): # main interface fn for converting a model to a dict - #TODO: try/catch here? +#TODO: try/catch here? add option 'fail_fast'/'raise' to allow optionally *catching* the error without raising it +# (and then returning the error message in the serialized data) returned = {} for key in keys: # check both serializers and serializable keys @@ -425,9 +426,25 @@ class ModelSerializer( object ): id = getattr( item, key ) return trans.security.encode_id( id ) if id is not None else None + #TODO: AnnotatableMixin + def serialize_annotation( self, trans, item, key ): +#TODO: which is correct here? + #user = item.user + user = trans.user + return item.get_item_annotation_str( trans.sa_session, user, item ) + + #TODO: TaggableMixin + def serialize_tags( self, trans, item, key ): +#TODO: whose tags are these? + return [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) for tag in item.tags ] + # ......................................................................... serializing to a view # where a view is a predefied list of keys to serialize def serialize_to_view( self, trans, item, view=None, keys=None ): +#TODO: if keys and view - munge view and keys +# : if keys but no view - use keys +# : if no keys but view - use view +# : if no keys and no view - use default view keys = keys or [] all_keys = self.view_to_list( view ) + keys return self.serialize( trans, item, all_keys ) @@ -439,6 +456,20 @@ class ModelSerializer( object ): raise ModelSerializingError( 'unknown view', view=view, available_views=self.views ) return self.views[ view ] + #TODO: this is more util/gen. use + def pluck_from_list( self, l, elems ): + """ + Removes found elems from list l and returns list of found elems if found. + """ + found = [] + for elem in elems: + try: + index = l.index( elem ) + found.append( l.pop( index ) ) + except ValueError, val_err: + pass + return found + # ......................................................................... aliases #def to_dict( self, trans, item, view=None, view_keys=None ): # view_keys = view_keys or self.view_to_list( view ) @@ -493,6 +524,7 @@ class ModelValidator( object ): return val #def slug( self, trans, item, key, val ): + # """validate slug""" # pass diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index f474f33d4ea..428f7652614 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -7,6 +7,7 @@ from galaxy import model from galaxy.exceptions import MessageException from galaxy.exceptions import ItemAccessibilityException from galaxy.exceptions import RequestParameterInvalidException +from galaxy.managers import base from galaxy.managers import hdas # TODO: Refactor all mixin use into managers. from galaxy.managers import histories from galaxy.managers import lddas @@ -244,7 +245,7 @@ class DatasetCollectionManager( object ): if src_type == 'hda': decoded_id = int( trans.app.security.decode_id( encoded_id ) ) - element = self.hda_manager.get( trans, decoded_id, check_ownership=False ) + element = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) elif src_type == 'ldda': element = self.ldda_manager.get( trans, encoded_id ) elif src_type == 'hdca': @@ -293,3 +294,122 @@ class DatasetCollectionManager( object ): if not trans.app.security_agent.can_access_library_item( trans.get_current_user_roles(), collection_instance, trans.user ): raise ItemAccessibilityException( "LibraryDatasetCollectionAssociation is not accessible to the current user", type='error' ) return collection_instance + + +class DatasetCollectionSerializer( base.ModelSerializer ): + + def __init__( self, app ): + super( HDASerializer, self ).__init__( app ) + + # # most of these views build/add to the previous view + # summary_view = [ + # 'id', 'history_id', 'hid', 'name', 'dataset_id', + # 'state', 'deleted', 'purged', 'visible' + # ] + # detailed_view = summary_view + [ + # 'dbkey', 'info', 'blurb', 'extension', 'create_time', 'update_time', + # 'copied_from_history_dataset_association_id', 'copied_from_library_dataset_dataset_association_id', + # 'metadata', 'size', + # #mixin: annotatable + # 'annotation', + # #mixin: taggable + # 'tags', + # ] + # extended_view = detailed_view + [ + # #NOTE: sending raw 'peek' (not get_display_peek) + # 'peek', + # 'tool_version', 'parent_id', 'designation', + # ] + # + # self.serializable_keys = extended_view + [ + # 'display_apps', + # 'display_types', + # 'visualizations' + # ] + # self.views = { + # 'summary' : summary_view, + # 'detailed' : detailed_view, + # 'extended' : extended_view, + # } + # self.default_view = 'summary' + # + #def add_serializers( self ): + # self.serializers.update({ + # 'id' : self.serialize_id, + # 'history_id' : self.serialize_id, + # 'dataset_id' : self.serialize_id, + # + # 'create_time' : self.serialize_date, + # 'update_time' : self.serialize_date, + # 'copied_from_history_dataset_association_id' : self.serialize_id, + # 'copied_from_library_dataset_dataset_association_id': self.serialize_id, + # + # 'metadata' : self.serialize_metadata, + # 'size' : lambda t, i, k: int( i.get_size() ), + # 'nice_size' : lambda t, i, k: i.get_size( nice_size=True ), + # + # 'parent_id' : self.serialize_id, + # #TODO: to mixin: annotatable + # 'annotation' : lambda t, i, k: i.get_item_annotation_str( t.sa_session, t.user, i ), + # #TODO: to mixin: taggable + # 'tags' : lambda t, i, k: [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) + # for tag in i.tags ], + # }) + +#def dictify_dataset_collection_instance( dataset_collection_instance, parent, security, view="element" ): +# dict_value = dataset_collection_instance.to_dict( view=view ) +# encoded_id = security.encode_id( dataset_collection_instance.id ) +# if isinstance( parent, model.History ): +# encoded_history_id = security.encode_id( parent.id ) +# dict_value[ 'url' ] = web.url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset_collection" ) +# elif isinstance( parent, model.LibraryFolder ): +# encoded_library_id = security.encode_id( parent.library.id ) +# 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": +# dict_value[ 'elements' ] = map( dictify_element, dataset_collection_instance.collection.elements ) +# security.encode_all_ids( dict_value, recursive=True ) # TODO: Use Kyle's recusrive formulation of this. +# return dict_value +# +# +#def dictify_element( element ): +# dictified = element.to_dict( view="element" ) +# object_detials = element.element_object.to_dict() +# if element.child_collection: +# # Recursively yield elements for each nested collection... +# object_detials[ "elements" ] = map( dictify_element, element.child_collection.elements ) +# +# dictified[ "object" ] = object_detials +# return dictified +# +# def serialize_metadata( self, trans, hda, key ): +# not_included = [ 'dbkey' ] +# md = {} +# for name, spec in hda.metadata.spec.items(): +# if name in not_included: +# continue +# val = hda.metadata.get( name ) +# #NOTE: no files +# if isinstance( val, model.MetadataFile ): +# continue +# #TODO:? possibly split this off? +# # If no value for metadata, look in datatype for metadata. +# elif val is None and hasattr( hda.datatype, name ): +# val = getattr( hda.datatype, name ) +# md[ name ] = val +# +# return md + + #def add_deserializers( self ): + # self.deserializers.update({ + # 'name' : self.deserialize_basestring, + # 'genome_build' : self.deserialize_genome_build, + # 'deleted' : self.deserialize_bool, + # 'published' : self.deserialize_bool, + # #TODO: to mixin: annotatable + # #'annotation' : self.deserialize_annotation, + # #TODO: to mixin: taggable + # #'tags' : lambda t, i, k, v: , + # }) + # self.deserializable_keys = self.deserializers.keys() diff --git a/lib/galaxy/managers/collections_util.py b/lib/galaxy/managers/collections_util.py index fd401eb028c..e478f7baa42 100644 --- a/lib/galaxy/managers/collections_util.py +++ b/lib/galaxy/managers/collections_util.py @@ -65,9 +65,9 @@ def validate_input_element_identifiers( element_identifiers ): validate_input_element_identifiers( element_identifier[ "element_identifiers" ] ) -def dictify_dataset_collection_instance( dataset_colleciton_instance, parent, security, view="element" ): - dict_value = dataset_colleciton_instance.to_dict( view=view ) - encoded_id = security.encode_id( dataset_colleciton_instance.id ) +def dictify_dataset_collection_instance( dataset_collection_instance, parent, security, view="element" ): + dict_value = dataset_collection_instance.to_dict( view=view ) + encoded_id = security.encode_id( dataset_collection_instance.id ) if isinstance( parent, model.History ): encoded_history_id = security.encode_id( parent.id ) dict_value[ 'url' ] = web.url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset_collection" ) @@ -77,7 +77,7 @@ def dictify_dataset_collection_instance( dataset_colleciton_instance, parent, se # 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": - dict_value[ 'elements' ] = map( dictify_element, dataset_colleciton_instance.collection.elements ) + dict_value[ 'elements' ] = map( dictify_element, dataset_collection_instance.collection.elements ) security.encode_all_ids( dict_value, recursive=True ) # TODO: Use Kyle's recusrive formulation of this. return dict_value diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 84805c633b6..4ac9baf815d 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -92,6 +92,9 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # ============================================================================= class DatasetAssociationManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): + # A dataset association HAS a dataset but contains metadata specific to a library (lda) or user (hda) + model_class = model.DatasetInstance + default_order_by = ( model.Dataset.create_time, ) def __init__( self, app ): super( DatasetAssociationManager, self ).__init__( app ) @@ -100,3 +103,9 @@ class DatasetAssociationManager( base.ModelManager, base.AccessibleModelInterfac def is_accessible( self, trans, dataset_assoc, user ): # defer to the dataset return self.dataset_mgr.is_accessible( trans, dataset_assoc.dataset, user ) + + def metadata( self, trans, dataset_assoc ): + pass + + def is_being_used( self, trans, dataset_assoc ): + pass diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 9a1db0cbe68..07567eea128 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -4,6 +4,8 @@ Manager and Serializer for HDAs. HistoryDatasetAssociations (HDAs) are datasets contained or created in a history. """ +import gettext + from galaxy import model from galaxy import exceptions @@ -111,93 +113,64 @@ class HDAManager( datasets.DatasetAssociationManager ): # ......................................................................... serialization #TODO: move into serializer below - def get_hda_dict( self, trans, hda ): - """ - Return full details of this HDA in dictionary form. - """ - #precondition: the user's access to this hda has already been checked - #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) - expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path - hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) - hda_dict[ 'api_type' ] = "file" - - # Add additional attributes that depend on trans must be added here rather than at the model level. - can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) - can_access_hda = ( trans.user_is_admin() or can_access_hda ) - if not can_access_hda: - return self.get_inaccessible_hda_dict( trans, hda ) - hda_dict[ 'accessible' ] = True - - #TODO: I'm unclear as to which access pattern is right - hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda ) - #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) - - # ---- return here if deleted AND purged OR can't access - purged = ( hda.purged or hda.dataset.purged ) - if ( hda.deleted and purged ): - #TODO: to_dict should really go AFTER this - only summary data - return trans.security.encode_dict_ids( hda_dict ) - - if expose_dataset_path: - try: - hda_dict[ 'file_name' ] = hda.file_name - except objectstore.ObjectNotFound: - log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) - - hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display', - history_id = trans.security.encode_id( hda.history.id ), - history_content_id = trans.security.encode_id( hda.id ) ) - - # indeces, assoc. metadata files, etc. - meta_files = [] - for meta_type in hda.metadata.spec.keys(): - if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): - meta_files.append( dict( file_type=meta_type ) ) - if meta_files: - hda_dict[ 'meta_files' ] = meta_files - - # currently, the viz reg is optional - handle on/off - if trans.app.visualizations_registry: - hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) - else: - hda_dict[ 'visualizations' ] = hda.get_visualizations() - #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations - # when the visualizations button is clicked (instead of preloading/pre-checking) - - # ---- return here if deleted - if hda.deleted and not purged: - return trans.security.encode_dict_ids( hda_dict ) - - return trans.security.encode_dict_ids( hda_dict ) - - def get_inaccessible_hda_dict( self, trans, hda ): - """ - Return truncated serialization of HDA when inaccessible to user. - """ - return trans.security.encode_dict_ids({ - 'id' : hda.id, - 'history_id': hda.history.id, - 'hid' : hda.hid, - 'name' : hda.name, - 'state' : hda.state, - 'deleted' : hda.deleted, - 'visible' : hda.visible, - 'accessible': False - }) - - def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): - """ - Return truncated serialization of HDA when error raised getting - details. - """ - return trans.security.encode_dict_ids({ - 'id' : hda.id if hda else id, - 'history_id': hda.history.id if hda else history_id, - 'hid' : hda.hid if hda else '(unknown)', - 'name' : hda.name if hda else '(unknown)', - 'error' : error_msg, - 'state' : trans.model.Dataset.states.NEW - }) + #def get_hda_dict( self, trans, hda ): + # """ + # Return full details of this HDA in dictionary form. + # """ + # #precondition: the user's access to this hda has already been checked + # #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) + # expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path + # hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) + # hda_dict[ 'api_type' ] = "file" + # + # # Add additional attributes that depend on trans must be added here rather than at the model level. + # can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) + # can_access_hda = ( trans.user_is_admin() or can_access_hda ) + # if not can_access_hda: + # return self.get_inaccessible_hda_dict( trans, hda ) + # hda_dict[ 'accessible' ] = True + # + # #TODO: I'm unclear as to which access pattern is right + # hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda ) + # #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) + # + # # ---- return here if deleted AND purged OR can't access + # purged = ( hda.purged or hda.dataset.purged ) + # if ( hda.deleted and purged ): + # #TODO: to_dict should really go AFTER this - only summary data + # return trans.security.encode_dict_ids( hda_dict ) + # + # if expose_dataset_path: + # try: + # hda_dict[ 'file_name' ] = hda.file_name + # except objectstore.ObjectNotFound: + # log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) + # + # hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display', + # history_id = trans.security.encode_id( hda.history.id ), + # history_content_id = trans.security.encode_id( hda.id ) ) + # + # # indeces, assoc. metadata files, etc. + # meta_files = [] + # for meta_type in hda.metadata.spec.keys(): + # if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): + # meta_files.append( dict( file_type=meta_type ) ) + # if meta_files: + # hda_dict[ 'meta_files' ] = meta_files + # + # # currently, the viz reg is optional - handle on/off + # if trans.app.visualizations_registry: + # hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) + # else: + # hda_dict[ 'visualizations' ] = hda.get_visualizations() + # #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations + # # when the visualizations button is clicked (instead of preloading/pre-checking) + # + # # ---- return here if deleted + # if hda.deleted and not purged: + # return trans.security.encode_dict_ids( hda_dict ) + # + # return trans.security.encode_dict_ids( hda_dict ) def get_display_apps( self, trans, hda ): """ @@ -211,7 +184,7 @@ class HDAManager( datasets.DatasetAssociationManager ): app_links.append({ 'target': link_app.url.get( 'target_frame', '_blank' ), 'href' : link_app.get_display_url( hda, trans ), - 'text' : gettext( link_app.name ) + 'text' : gettext.gettext( link_app.name ) }) if app_links: display_apps.append( dict( label=display_app.name, links=app_links ) ) @@ -238,13 +211,19 @@ class HDAManager( datasets.DatasetAssociationManager ): app_links.append({ 'target': target_frame, 'href' : display_link, - 'text' : gettext( display_name ) + 'text' : gettext.gettext( display_name ) }) if app_links: display_apps.append( dict( label=display_label, links=app_links ) ) return display_apps + def get_visualizations( self, trans, hda ): + # use older system if registry is off in the config + if not trans.app.visualizations_registry: + return hda.get_visualizations() + return trans.app.visualizations_registry.get_visualizations( trans, hda ) + #def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): # """ # Get an HDA object by id performing security checks using @@ -378,67 +357,7 @@ class HDAManager( datasets.DatasetAssociationManager ): # if dataset.state != trans.app.model.Job.states.OK: # return dataset.conversion_messages.PENDING # return None - # - #def get_hda_dict( self, trans, hda ): - # """Return full details of this HDA in dictionary form. - # """ - # #precondition: the user's access to this hda has already been checked - # #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) - # expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path - # hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) - # hda_dict[ 'api_type' ] = "file" - # - # # Add additional attributes that depend on trans can hence must be added here rather than at the model level. - # can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) - # can_access_hda = ( trans.user_is_admin() or can_access_hda ) - # if not can_access_hda: - # return self.get_inaccessible_hda_dict( trans, hda ) - # hda_dict[ 'accessible' ] = True - # - # #TODO: I'm unclear as to which access pattern is right - # hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) - # #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) - # - # # ---- return here if deleted AND purged OR can't access - # purged = ( hda.purged or hda.dataset.purged ) - # if hda.deleted and purged: - # #TODO: to_dict should really go AFTER this - only summary data - # return trans.security.encode_dict_ids( hda_dict ) - # - # if expose_dataset_path: - # try: - # hda_dict[ 'file_name' ] = hda.file_name - # except objectstore.ObjectNotFound: - # log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) - # - # hda_dict[ 'download_url' ] = url_for( 'history_contents_display', - # history_id = trans.security.encode_id( hda.history.id ), - # history_content_id = trans.security.encode_id( hda.id ) ) - # - # # resubmitted is not a real state - # hda_dict[ 'resubmitted' ] = False - # if hda.state == trans.app.model.Dataset.states.RESUBMITTED: - # hda_dict[ 'state' ] = hda.dataset.state - # hda_dict[ 'resubmitted' ] = True - # - # # indeces, assoc. metadata files, etc. - # meta_files = [] - # for meta_type in hda.metadata.spec.keys(): - # if isinstance( hda.metadata.spec[ meta_type ].param, FileParameter ): - # meta_files.append( dict( file_type=meta_type ) ) - # if meta_files: - # hda_dict[ 'meta_files' ] = meta_files - # - # # currently, the viz reg is optional - handle on/off - # if trans.app.visualizations_registry: - # hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) - # else: - # hda_dict[ 'visualizations' ] = hda.get_visualizations() - # #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations - # # when the visualizations button is clicked (instead of preloading/pre-checking) - # - # return trans.security.encode_dict_ids( hda_dict ) - # + #def get_inaccessible_hda_dict( self, trans, hda ): # return trans.security.encode_dict_ids({ # 'id' : hda.id, @@ -450,7 +369,7 @@ class HDAManager( datasets.DatasetAssociationManager ): # 'visible' : hda.visible, # 'accessible': False # }) - # + #def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): # return trans.security.encode_dict_ids({ # 'id' : hda.id if hda else id, @@ -460,47 +379,7 @@ class HDAManager( datasets.DatasetAssociationManager ): # 'error' : error_msg, # 'state' : trans.model.Dataset.states.NEW # }) - # - #def get_display_apps( self, trans, hda ): - # display_apps = [] - # for display_app in hda.get_display_applications( trans ).itervalues(): - # - # app_links = [] - # for link_app in display_app.links.itervalues(): - # app_links.append({ - # 'target': link_app.url.get( 'target_frame', '_blank' ), - # 'href' : link_app.get_display_url( hda, trans ), - # 'text' : gettext( link_app.name ) - # }) - # if app_links: - # display_apps.append( dict( label=display_app.name, links=app_links ) ) - # - # return display_apps - # - #def get_old_display_applications( self, trans, hda ): - # display_apps = [] - # if not trans.app.config.enable_old_display_applications: - # return display_apps - # - # for display_app in hda.datatype.get_display_types(): - # target_frame, display_links = hda.datatype.get_display_links( hda, - # display_app, trans.app, trans.request.base ) - # - # if len( display_links ) > 0: - # display_label = hda.datatype.get_display_label( display_app ) - # - # app_links = [] - # for display_name, display_link in display_links: - # app_links.append({ - # 'target': target_frame, - # 'href' : display_link, - # 'text' : gettext( display_name ) - # }) - # if app_links: - # display_apps.append( dict( label=display_label, links=app_links ) ) - # - # return display_apps - # + #def set_hda_from_dict( self, trans, hda, new_data ): # """ # Changes HDA data using the given dictionary new_data. @@ -522,7 +401,7 @@ class HDAManager( datasets.DatasetAssociationManager ): # trans.sa_session.flush() # # return changed - # + #def get_hda_job( self, hda ): # # Get dataset's job. # job = None @@ -530,7 +409,7 @@ class HDAManager( datasets.DatasetAssociationManager ): # job = job_output_assoc.job # break # return job - # + #def stop_hda_creating_job( self, hda ): # """ # Stops an HDA's creating job if all the job's other outputs are deleted. @@ -543,39 +422,138 @@ class HDAManager( datasets.DatasetAssociationManager ): # if job.check_if_output_datasets_deleted(): # job.mark_deleted( self.app.config.track_jobs_in_database ) # self.app.job_manager.job_stop_queue.put( job.id ) + +# old file + #def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True ): + # """ + # Get an HDA by its unencoded db id, checking ownership (via its history) + # or accessibility (via dataset shares/permissions). + # """ + # hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( unencoded_id ) + # if hda is None: + # raise exceptions.ObjectNotFound() + # hda = self.secure( trans, hda, check_ownership, check_accessible ) + # return hda # + #def secure( self, trans, hda, check_ownership=True, check_accessible=True ): + # """ + # check ownership (via its history) or accessibility (via dataset + # shares/permissions). + # """ + # # all items are accessible to an admin + # if trans.user and trans.user_is_admin(): + # return hda + # if check_ownership: + # hda = self.check_ownership( trans, hda ) + # if check_accessible: + # hda = self.check_accessible( trans, hda ) + # return hda # + #def can_access_dataset( self, trans, hda ): + # """ + # Use security agent to see if current user has access to dataset. + # """ + # current_user_roles = trans.get_current_user_roles() + # return trans.app.security_agent.can_access_dataset( current_user_roles, hda.dataset ) + # + ##TODO: is_owner, is_accessible + # + #def check_ownership( self, trans, hda ): + # """ + # Use history to see if current user owns HDA. + # """ + # if not trans.user: + # #if hda.history == trans.history: + # # return hda + # raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy datasets", type='error' ) + # if trans.user_is_admin(): + # return hda + # # check for ownership of the containing history and accessibility of the underlying dataset + # if( self.histories_mgr.is_owner( trans, hda.history ) + # and self.can_access_dataset( trans, hda ) ): + # return hda + # raise exceptions.ItemOwnershipException( + # "HistoryDatasetAssociation is not owned by the current user", type='error' ) + + #def check_accessible( self, trans, hda ): + # """ + # Raise error if HDA is not accessible. + # """ + # if trans.user and trans.user_is_admin(): + # return hda + # # check for access of the containing history... + # self.histories_mgr.check_accessible( trans, hda.history ) + # # ...then the underlying dataset + # if self.can_access_dataset( trans, hda ): + # return hda + # raise exceptions.ItemAccessibilityException( + # "HistoryDatasetAssociation is not accessible to the current user", type='error' ) + # ============================================================================= class HDASerializer( base.ModelSerializer ): + #TODO: inherit from datasets.DatasetAssociationSerializer - def __init__( self ): - super( HDASerializer, self ).__init__() + def __init__( self, app ): + super( HDASerializer, self ).__init__( app ) + self.hda_mgr = HDAManager( app ) # most of these views build/add to the previous view summary_view = [ - 'id', 'history_id', 'hid', 'name', 'dataset_id', - 'state', 'deleted', 'purged', 'visible' + 'id', 'name', + 'history_id', 'hid', 'history_content_type', + #'dataset_id', + 'state', 'extension', + 'deleted', 'purged', 'visible', 'resubmitted', + 'type', 'url' ] - detailed_view = summary_view + [ - 'dbkey', 'info', 'blurb', 'extension', 'create_time', 'update_time', - 'copied_from_history_dataset_association_id', 'copied_from_library_dataset_dataset_association_id', - 'metadata', 'size', + inaccessible = [ + 'id', 'name', 'history_id', 'hid', 'history_content_type', + 'state', 'deleted', 'visible' + ] + detailed_view = [ + 'id', 'name', 'model_class', + 'history_id', 'hid', + # why include if model_class is there? + 'hda_ldda', 'history_content_type', + 'state', + 'accessible', + 'deleted', 'purged', 'visible', 'resubmitted', + + # remapped + 'genome_build', 'misc_info', 'misc_blurb', + 'file_ext', 'file_size', + + 'create_time', 'update_time', + 'metadata', 'meta_files', 'data_type', + 'peek', + + #TODO: why is this named uuid!? The hda doesn't have a uuid - it's the underlying dataset's uuid! + 'uuid', + # should be: + #'dataset_uuid', + + 'file_path', + 'display_apps', + 'display_types', + 'visualizations', + + #'url', + 'download_url', + #mixin: annotatable 'annotation', #mixin: taggable 'tags', + + 'api_type' ] extended_view = detailed_view + [ #NOTE: sending raw 'peek' (not get_display_peek) - 'peek', 'tool_version', 'parent_id', 'designation', ] self.serializable_keys = extended_view + [ - 'display_apps', - 'display_types', - 'visualizations' ] self.views = { 'summary' : summary_view, @@ -586,52 +564,190 @@ class HDASerializer( base.ModelSerializer ): def add_serializers( self ): self.serializers.update({ + 'model_class' : lambda *a: 'HistoryDatasetAssociation', + 'accessible' : lambda *a: True, + 'id' : self.serialize_id, 'history_id' : self.serialize_id, + 'history_content_type': lambda *a: 'dataset', 'dataset_id' : self.serialize_id, + 'hda_ldda' : lambda *a: 'hda', + + # remapped + 'info' : lambda t, i, k: i.info.strip() if isinstance( i.info, basestring ) else i.info, + 'misc_info' : lambda t, i, k: self.serializers[ 'info' ]( t, i, k ), + 'misc_blurb' : lambda t, i, k: i.blurb, + 'genome_build' : lambda t, i, k: i.dbkey, + 'file_ext' : lambda t, i, k: i.extension, + 'file_size' : lambda t, i, k: self.serializers[ 'size' ]( t, i, k ), 'create_time' : self.serialize_date, 'update_time' : self.serialize_date, 'copied_from_history_dataset_association_id' : self.serialize_id, 'copied_from_library_dataset_dataset_association_id': self.serialize_id, - 'metadata' : self.serialize_metadata, + + 'resubmitted' : lambda t, i, k: i._state == t.app.model.Dataset.states.RESUBMITTED, + + 'meta_files' : self.serialize_meta_files, + 'file_path' : self.serialize_file_path, + 'size' : lambda t, i, k: int( i.get_size() ), 'nice_size' : lambda t, i, k: i.get_size( nice_size=True ), + 'data_type' : lambda t, i, k: i.datatype.__class__.__module__ + '.' + i.datatype.__class__.__name__, + 'peek' : lambda t, i, k: i.display_peek() if i.peek and i.peek != 'no peek' else None, + + # currently we send this sub-obj flattened (see serialize and add_flattened_metadata below) + #'metadata' : self.serialize_metadata, + # make it available here under a different key + 'metadata_dict' : self.serialize_metadata, + 'parent_id' : self.serialize_id, - #TODO: to mixin: annotatable - 'annotation' : lambda t, i, k: i.get_item_annotation_str( t.sa_session, t.user, i ), - #TODO: to mixin: taggable - 'tags' : lambda t, i, k: [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) - for tag in i.tags ], + 'annotation' : self.serialize_annotation, + 'tags' : self.serialize_tags, + + 'display_apps' : lambda t, i, k: self.hda_mgr.get_display_apps( t, i ), + 'display_types' : lambda t, i, k: self.hda_mgr.get_old_display_applications( t, i ), + 'visualizations': lambda t, i, k: self.hda_mgr.get_visualizations( t, i ), + + 'dataset_uuid' : lambda t, i, k: str( i.dataset.uuid ) if i.dataset.uuid else None, + 'uuid' : lambda t, i, k: self.serializers[ 'dataset_uuid' ]( t, i, k ), + + #'url' : url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset" ), + #TODO: this intermittently causes a routes.GenerationException - temp use the legacy route to prevent this + # see also: https://trello.com/c/5d6j4X5y + # see also: https://sentry.galaxyproject.org/galaxy/galaxy-main/group/20769/events/9352883/ + 'url' : lambda t, i, k: galaxy.web.url_for( 'history_content', + history_id=t.security.encode_id( i.history_id ), id=t.security.encode_id( i.id ) ), + + 'urls' : self.serialize_urls, + 'download_url' : lambda t, i, k: galaxy.web.url_for( 'history_contents_display', + history_id=t.security.encode_id( i.history.id ), + history_content_id=t.security.encode_id( i.id ) ), + + 'api_type' : lambda *a: 'file', + 'type' : lambda *a: 'file' }) - def serialize_metadata( self, trans, hda, key ): - not_included = [ 'dbkey' ] - md = {} + def serialize( self, trans, hda, keys ): + # if 'metadata' isn't removed from keys here serialize will retrieve the un-serializable MetadataCollection + #TODO: remove these when metadata is sub-object + KEYS_HANDLED_SEPARATELY = ( 'metadata', ) + left_to_handle = self.pluck_from_list( keys, KEYS_HANDLED_SEPARATELY ) + serialized = super( HDASerializer, self ).serialize( trans, hda, keys ) + + if 'metadata' in left_to_handle: + # we currently add metadata directly to the dict instead of as a sub-object + serialized.update( self.prefixed_metadata( trans, hda ) ) + return serialized + + def prefixed_metadata( self, trans, hda ): + """ + Adds (a prefixed version of) the hda metadata to the dict, prefixing each key + with 'metadata_'. + """ + metadata = self.serialize_metadata( trans, hda, 'metadata' ) + #TODO: this is factored out for removal - metadata should be a sub-object instead: + # i.e. 'metadata' : self.serialize_metadata, + prefixed = {} + for key, val in metadata.items(): + prefixed_key = 'metadata_' + key + prefixed[ prefixed_key ] = val + return prefixed + + def serialize_metadata( self, trans, hda, key, excluded=None ): + # dbkey is a repeat actually (metadata_dbkey == genome_build) + #excluded = [ 'dbkey' ] if excluded is None else excluded + excluded = [] if excluded is None else excluded + + metadata = {} for name, spec in hda.metadata.spec.items(): - if name in not_included: + if name in excluded: continue val = hda.metadata.get( name ) #NOTE: no files if isinstance( val, model.MetadataFile ): - continue + # only when explicitly set: fetching filepaths can be expensive + if not trans.app.config.expose_dataset_path: + continue + val = val.file_name #TODO:? possibly split this off? # If no value for metadata, look in datatype for metadata. elif val is None and hasattr( hda.datatype, name ): val = getattr( hda.datatype, name ) - md[ name ] = val + metadata[ name ] = val - return md + return metadata + # add to serialize_metadata above + def serialize_meta_files( self, trans, hda, key ): + meta_files = [] + for meta_type in hda.metadata.spec.keys(): + if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): + meta_files.append( dict( file_type=meta_type ) ) + return meta_files + + #def file_info #TODO + #TODO: and to dataset instead (passing through object store) + def serialize_file_path( self, trans, hda, key ): +#TODO: allow admin + if trans.app.config.expose_dataset_path: + try: + return hda.file_name + except objectstore.ObjectNotFound: + log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) + return None + + def serialize_urls( self, trans, hda, key ): + url_for = galaxy.web.url_for + encoded_id = trans.security.encode_id( hda.id ) + urls = { + 'purge' : url_for( controller='dataset', action='purge_async', dataset_id=encoded_id ), + 'display' : url_for( controller='dataset', action='display', dataset_id=encoded_id, preview=True ), + 'edit' : url_for( controller='dataset', action='edit', dataset_id=encoded_id ), + 'download' : url_for( controller='dataset', action='display', + dataset_id=encoded_id, to_ext=hda.extension ), + 'report_error' : url_for( controller='dataset', action='errors', id=encoded_id ), + 'rerun' : url_for( controller='tool_runner', action='rerun', id=encoded_id ), + 'show_params' : url_for( controller='dataset', action='show_params', dataset_id=encoded_id ), + 'visualization' : url_for( controller='visualization', action='index', + id=encoded_id, model='HistoryDatasetAssociation' ), + 'meta_download' : url_for( controller='dataset', action='get_metadata_file', + hda_id=encoded_id, metadata_name='' ), + } + return urls + + +# ============================================================================= +class HDADeserializer( base.ModelDeserializer ): + """ + Interface/service object for validating and deserializing dictionaries into histories. + """ + + def __init__( self, app ): + super( HDADeserializer, self ).__init__( app ) + self.hda_mgr = HDAManager( app ) + + #assumes: incoming from json.loads and sanitized def add_deserializers( self ): + super( HDADeserializer, self ).add_deserializers() self.deserializers.update({ 'name' : self.deserialize_basestring, - 'genome_build' : self.deserialize_genome_build, + 'visible' : self.deserialize_bool, + + # remapped + 'genome_build' : lambda t, i, k, v: self.deserialize_genome_build( t, i, 'dbkey', v ), + 'misc_info' : lambda t, i, k, v: self.deserialize_basestring( t, i, 'info', v ), + + # mixin: annotatable + 'annotation' : self.deserialize_annotation, + # mixin: taggable + 'tags' : self.deserialize_tags, + + # mixin: deletable 'deleted' : self.deserialize_bool, + # sharable 'published' : self.deserialize_bool, - #TODO: to mixin: annotatable - #'annotation' : self.deserialize_annotation, - #TODO: to mixin: taggable - #'tags' : lambda t, i, k, v: , + 'importable' : self.deserialize_bool, + }) self.deserializable_keys = self.deserializers.keys() diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index ed361ef068f..883a5fe3741 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -90,31 +90,28 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface super( HistoryManager, self ).purge( trans, history, flush=flush, **kwargs ) # ......................................................................... serialization - #TODO: move to serializer + #TODO: move to serializer (i.e. history with contents attr) def _get_history_data( self, trans, history ): """ Returns a dictionary containing ``history`` and ``contents``, serialized history and an array of serialized history contents respectively. """ - # import here prevents problems related to circular dependecy between histories and hdas managers. - import galaxy.managers.hdas - hda_mgr = galaxy.managers.hdas.HDAManager() + #TODO: instantiate here? really? + history_serializer = HistorySerializer( self.app ) + hda_serializer = hdas.HDASerializer( self.app ) collection_dictifier = galaxy.managers.collections_util.dictify_dataset_collection_instance history_dictionary = {} contents_dictionaries = [] try: + history_dictionary = history_serializer.serialize_to_view( trans, history, view='detailed' ) + #for content in history.contents_iter( **contents_kwds ): for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): - hda_dict = {} + contents_dict = {} if isinstance( content, trans.app.model.HistoryDatasetAssociation ): - try: - hda_dict = hda_mgr.get_hda_dict( trans, content ) - except Exception, exc: - # don't fail entire list if hda err's, record and move on - log.exception( 'Error bootstrapping hda: %s', exc ) - hda_dict = hda_mgr.get_hda_dict_with_error( trans, content, str( exc ) ) + contents_dict = hda_serializer.serialize_to_view( trans, hda, view='detailed' ) elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): try: @@ -124,17 +121,14 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface instance_type='history', id=trans.security.encode_id( content.id ), ) - hda_dict = collection_dictifier( dataset_collection_instance, + contents_dict = collection_dictifier( dataset_collection_instance, security=trans.security, parent=dataset_collection_instance.history, view="element" ) except Exception, exc: log.exception( "Error in history API at listing dataset collection: %s", exc ) #TODO: return some dict with the error - contents_dictionaries.append( hda_dict ) - - # re-use the hdas above to get the history data... - history_dictionary = self.get_history_dict( trans, history, contents_dictionaries=contents_dictionaries ) + contents_dictionaries.append( contents_dict ) except Exception, exc: user_id = str( trans.user.id ) if trans.user else '(anonymous)' @@ -226,6 +220,8 @@ class HistorySerializer( base.ModelSerializer ): def __init__( self, app ): super( HistorySerializer, self ).__init__( app ) self.history_mgr = HistoryManager( app ) + self.hda_mgr = hdas.HDAManager( app ) + self.hda_serializer = hdas.HDASerializer( app ) summary_view = [ 'id', @@ -274,24 +270,44 @@ class HistorySerializer( base.ModelSerializer ): 'create_time' : self.serialize_date, 'update_time' : self.serialize_date, 'user_id' : self.serialize_id, - 'hdas' : lambda t, i, k: [ t.security.encode_id( hda.id ) for hda in i.datasets ], 'size' : lambda t, i, k: int( i.get_disk_size() ), 'nice_size' : lambda t, i, k: i.get_disk_size( nice_size=True ), - #TODO: to mixins: annotatable, taggable - 'annotation' : lambda t, i, k: i.get_item_annotation_str( t.sa_session, t.user, i ), - 'tags' : lambda t, i, k: [ tag.user_tname + ( ':' + tag.user_value if tag.user_value else '' ) - for tag in i.tags ], + + 'annotation' : self.serialize_annotation, + 'tags' : self.serialize_tags, 'url' : lambda t, i, k: galaxy.web.url_for( 'history', id=t.security.encode_id( i.id ) ), 'contents_url' : lambda t, i, k: galaxy.web.url_for( 'history_contents', history_id=t.security.encode_id( i.id ) ), 'empty' : lambda t, i, k: len( i.datasets ) <= 0, + 'hdas' : lambda t, i, k: [ t.security.encode_id( hda.id ) for hda in i.datasets ], 'state' : lambda t, i, k: self.history_mgr.get_history_state( t, i ), 'state_details' : lambda t, i, k: self.history_mgr.get_state_counts( t, i ), 'state_ids' : lambda t, i, k: self.history_mgr.get_state_ids( t, i ), + 'contents' : self.serialize_contents }) -## ============================================================================= + def serialize_contents( self, trans, history, key ): + for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): + contents_dict = {} + if isinstance( content, trans.app.model.HistoryDatasetAssociation ): + contents_dict = self.hda_serializer.serialize_to_view( trans, hda, view='detailed' ) + elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): + contents_dict = self.serialize_collection( trans, collection ) + contents_dictionaries.append( contents_dict ) + + def serialize_collection( self, trans, collection ): + service = trans.app.dataset_collections_service + dataset_collection_instance = service.get_dataset_collection_instance( + trans=trans, + instance_type='history', + id=trans.security.encode_id( collection.id ), + ) + return collection_dictifier( dataset_collection_instance, + security=trans.security, parent=dataset_collection_instance.history, view="element" ) + + +# ============================================================================= class HistoryDeserializer( base.ModelDeserializer ): """ Interface/service object for validating and deserializing dictionaries into histories. diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 451c29aaf23..094ffc9a033 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1111,15 +1111,16 @@ class History( object, Dictifiable, UsesAnnotations, HasName ): def active_datasets_children_and_roles( self ): if not hasattr(self, '_active_datasets_children_and_roles'): db_session = object_session( self ) - query = db_session.query( HistoryDatasetAssociation ).filter( HistoryDatasetAssociation.table.c.history_id == self.id ). \ - filter( not_( HistoryDatasetAssociation.deleted ) ). \ - order_by( HistoryDatasetAssociation.table.c.hid.asc() ). \ - options( - joinedload("children"), - joinedload("dataset"), - joinedload("dataset.actions"), - joinedload("dataset.actions.role"), - ) + query = ( db_session.query( HistoryDatasetAssociation ) + .filter( HistoryDatasetAssociation.table.c.history_id == self.id ) + .filter( not_( HistoryDatasetAssociation.deleted ) ) + .order_by( HistoryDatasetAssociation.table.c.hid.asc() ) + .options( + joinedload("children"), + joinedload("dataset"), + joinedload("dataset.actions"), + joinedload("dataset.actions.role"), + )) self._active_datasets_children_and_roles = query.all() return self._active_datasets_children_and_roles @@ -1551,7 +1552,9 @@ class DatasetInstance( object ): def datatype( self ): return datatypes_registry.get_datatype_by_extension( self.extension ) def get_metadata( self ): - if not hasattr( self, '_metadata_collection' ) or self._metadata_collection.parent != self: #using weakref to store parent (to prevent circ ref), does a Session.clear() cause parent to be invalidated, while still copying over this non-database attribute? + # using weakref to store parent (to prevent circ ref), + # does a Session.clear() cause parent to be invalidated, while still copying over this non-database attribute? + if not hasattr( self, '_metadata_collection' ) or self._metadata_collection.parent != self: self._metadata_collection = MetadataCollection( self ) return self._metadata_collection def set_metadata( self, bunch ): @@ -2078,6 +2081,7 @@ class HistoryDatasetAssociation( DatasetInstance, Dictifiable, UsesAnnotations, rval['extended_metadata'] = hda.extended_metadata.data rval[ 'peek' ] = to_unicode( hda.display_peek() ) + for name, spec in hda.metadata.spec.items(): val = hda.metadata.get( name ) if isinstance( val, MetadataFile ): diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 0dccd69f4e4..dd55877f1bb 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -108,17 +108,6 @@ class BaseController( object ): """ return trans.security.encode_all_ids( rval, recursive=recursive ) - # incoming param validation - # should probably be in sep. serializer class/object _used_ by controller - def validate_and_sanitize_basestring( self, key, val ): - return validation.validate_and_sanitize_basestring( key, val ) - - def validate_and_sanitize_basestring_list( self, key, val ): - return validation.validate_and_sanitize_basestring_list( key, val ) - - def validate_boolean( self, key, val ): - return validation.validate_boolean( key, val ) - Root = BaseController @@ -479,88 +468,6 @@ class UsesHistoryDatasetAssociationMixin: return dataset.conversion_messages.PENDING return None - def get_hda_dict( self, trans, hda ): - """Return full details of this HDA in dictionary form. - """ - #precondition: the user's access to this hda has already been checked - #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) - expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path - hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) - hda_dict[ 'api_type' ] = "file" - - # Add additional attributes that depend on trans can hence must be added here rather than at the model level. - can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) - can_access_hda = ( trans.user_is_admin() or can_access_hda ) - if not can_access_hda: - return self.get_inaccessible_hda_dict( trans, hda ) - hda_dict[ 'accessible' ] = True - - #TODO: I'm unclear as to which access pattern is right - hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) - #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) - - # ---- return here if deleted AND purged OR can't access - purged = ( hda.purged or hda.dataset.purged ) - if hda.deleted and purged: - #TODO: to_dict should really go AFTER this - only summary data - return trans.security.encode_dict_ids( hda_dict ) - - if expose_dataset_path: - try: - hda_dict[ 'file_name' ] = hda.file_name - except objectstore.ObjectNotFound: - log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) - - hda_dict[ 'download_url' ] = url_for( 'history_contents_display', - history_id = trans.security.encode_id( hda.history.id ), - history_content_id = trans.security.encode_id( hda.id ) ) - - # resubmitted is not a real state - hda_dict[ 'resubmitted' ] = False - if hda.state == trans.app.model.Dataset.states.RESUBMITTED: - hda_dict[ 'state' ] = hda.dataset.state - hda_dict[ 'resubmitted' ] = True - - # indeces, assoc. metadata files, etc. - meta_files = [] - for meta_type in hda.metadata.spec.keys(): - if isinstance( hda.metadata.spec[ meta_type ].param, FileParameter ): - meta_files.append( dict( file_type=meta_type ) ) - if meta_files: - hda_dict[ 'meta_files' ] = meta_files - - # currently, the viz reg is optional - handle on/off - if trans.app.visualizations_registry: - hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) - else: - hda_dict[ 'visualizations' ] = hda.get_visualizations() - #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations - # when the visualizations button is clicked (instead of preloading/pre-checking) - - return trans.security.encode_dict_ids( hda_dict ) - - def get_inaccessible_hda_dict( self, trans, hda ): - return trans.security.encode_dict_ids({ - 'id' : hda.id, - 'history_id': hda.history.id, - 'hid' : hda.hid, - 'name' : hda.name, - 'state' : hda.state, - 'deleted' : hda.deleted, - 'visible' : hda.visible, - 'accessible': False - }) - - def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): - return trans.security.encode_dict_ids({ - 'id' : hda.id if hda else id, - 'history_id': hda.history.id if hda else history_id, - 'hid' : hda.hid if hda else '(unknown)', - 'name' : hda.name if hda else '(unknown)', - 'error' : error_msg, - 'state' : trans.model.Dataset.states.NEW - }) - def get_display_apps( self, trans, hda ): display_apps = [] for display_app in hda.get_display_applications( trans ).itervalues(): diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index 91cfff1b9d0..23af3294179 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -2,18 +2,35 @@ API operations on the contents of a history dataset. """ from galaxy import web -from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider, SamDataProvider, BamDataProvider -from galaxy.web.base.controller import BaseAPIController, UsesVisualizationMixin, UsesHistoryDatasetAssociationMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.framework.helpers import is_true +from galaxy import util + +from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider +from galaxy.visualization.data_providers.genome import SamDataProvider +from galaxy.visualization.data_providers.genome import BamDataProvider from galaxy.datatypes import dataproviders -from galaxy.util import string_as_bool_or_none + +from galaxy.web.base.controller import BaseAPIController +from galaxy.web.base.controller import UsesVisualizationMixin +from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import UsesHistoryMixin +from galaxy import managers + import logging log = logging.getLogger( __name__ ) + class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin ): + def __init__( self, app ): + super( DatasetsController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories=managers.histories.HistoryManager( app ), + hdas=managers.hdas.HDAManager( app ) + ) + self.hda_serializer = managers.hdas.HDASerializer( self.app ) + @web.expose_api def index( self, trans, **kwd ): """ @@ -55,10 +72,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory else: # Default: return dataset as dict. if hda_ldda == 'hda': - rval = self.get_hda_dict( trans, dataset ) - # add these to match api/history_contents.py, show - rval[ 'display_types' ] = self.get_old_display_applications( trans, dataset ) - rval[ 'display_apps' ] = self.get_display_apps( trans, dataset ) + return self.hda_serializer.serialize_to_view( trans, hda, view=kwd.get( 'view', 'detailed' ) ) else: rval = dataset.to_dict() @@ -267,7 +281,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory some point in the future without warning. Generally, data should be processed by its datatype prior to display (the defult if raw is unspecified or explicitly false. """ - raw = string_as_bool_or_none( raw ) + raw = util.string_as_bool_or_none( raw ) rval = '' try: hda = self.get_history_dataset_association_from_ids( trans, history_content_id, history_id ) diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index f9738ed2e63..8880c20ff63 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -18,7 +18,10 @@ from galaxy.web.base.controller import UsesTagsMixin from galaxy.managers import histories from galaxy.managers import hdas -from galaxy.managers.collections_util import api_payload_to_create_params, dictify_dataset_collection_instance +from galaxy.managers.collections_util import api_payload_to_create_params +from galaxy.managers.collections_util import dictify_dataset_collection_instance +from galaxy.util import validation + import logging log = logging.getLogger( __name__ ) @@ -33,6 +36,8 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio histories=histories.HistoryManager( app ), hdas=hdas.HDAManager( app ) ) + self.hda_serializer = hdas.HDASerializer( app ) + self.hda_deserializer = hdas.HDADeserializer( app ) def _decode_id( self, trans, id ): try: @@ -65,9 +70,6 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio :rtype: list :returns: dictionaries containing summary or detailed HDA information - .. seealso:: - :func:`_summary_hda_dict` and - :func:`galaxy.web.base.controller.UsesHistoryDatasetAssociationMixin.get_hda_dict` """ rval = [] @@ -110,10 +112,9 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio detailed = details == 'all' or ( encoded_content_id in details ) if isinstance( content, trans.app.model.HistoryDatasetAssociation ): - if detailed: - rval.append( self._detailed_hda_dict( trans, content ) ) - else: - rval.append( self._summary_hda_dict( trans, history_id, content ) ) + view = 'detailed' if detailed else 'summary' + hda_dict = self.hda_serializer.serialize_to_view( trans, content, view=view ) + rval.append( hda_dict ) elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): if detailed: @@ -123,62 +124,10 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio return rval - #TODO: move to model or Mixin - def _summary_hda_dict( self, trans, encoded_history_id, hda ): - """ - Returns a dictionary based on the HDA in summary form:: - { - 'id' : < the encoded dataset id >, - 'name' : < currently only returns 'file' >, - 'type' : < name of the dataset >, - 'url' : < api url to retrieve this datasets full data >, - } - """ - api_type = "file" - encoded_id = trans.security.encode_id( hda.id ) - encoded_dataset_id = trans.security.encode_id( hda.dataset_id ); - - # TODO: handle failed_metadata here as well - return { - 'id' : encoded_id, - 'history_id' : encoded_history_id, - 'dataset_id' : encoded_dataset_id, - 'name' : hda.name, - 'type' : api_type, - 'state' : hda.dataset.state, - 'deleted': hda.deleted, - 'extension': hda.extension, - 'visible': hda.visible, - 'purged': hda.purged, - 'resubmitted': hda._state == trans.app.model.Dataset.states.RESUBMITTED, - 'hid' : hda.hid, - 'history_content_type' : hda.history_content_type, - #'url' : url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset" ), - #TODO: this intermittently causes a routes.GenerationException - temp use the legacy route to prevent this - # see also: https://trello.com/c/5d6j4X5y - # see also: https://sentry.galaxyproject.org/galaxy/galaxy-main/group/20769/events/9352883/ - 'url' : url_for( 'history_content', history_id=encoded_history_id, id=encoded_id ), - } - def __collection_dict( self, trans, dataset_collection_instance, view="collection" ): return dictify_dataset_collection_instance( dataset_collection_instance, security=trans.security, parent=dataset_collection_instance.history, view=view ) - def _detailed_hda_dict( self, trans, hda ): - """ - Detailed dictionary of hda values. - """ - try: - hda_dict = self.get_hda_dict( trans, hda ) - hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda ) - hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda ) - return hda_dict - except Exception, exc: - # catch error here - returning a briefer hda_dict with an error attribute - log.exception( "Error in history API at listing contents with history %s, hda %s: (%s) %s", - hda.history_id, hda.id, type( exc ), str( exc ) ) - return self.get_hda_dict_with_error( trans, hda=hda, error_msg=str( exc ) ) - @expose_api_anonymous def show( self, trans, id, history_id, **kwd ): """ @@ -194,7 +143,6 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio :rtype: dict :returns: dictionary containing detailed HDA information - .. seealso:: :func:`galaxy.web.base.controller.UsesHistoryDatasetAssociationMixin.get_hda_dict` """ contents_type = kwd.get('type', 'dataset') if contents_type == 'dataset': @@ -204,6 +152,12 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio else: return self.__handle_unknown_contents_type( trans, contents_type ) + def __show_dataset( self, trans, id, **kwd ): + hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) + #if hda.history.id != self._decode_id( trans, history_id ): + # raise exceptions.ObjectNotFound( 'dataset was not found in this history' ) + return self.hda_serializer.serialize_to_view( trans, hda, view=kwd.get( 'view', 'detailed' ) ) + def __show_dataset_collection( self, trans, id, history_id, **kwd ): try: service = trans.app.dataset_collections_service @@ -218,15 +172,6 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio trans.response.status = 500 return { 'error': str( e ) } - def __show_dataset( self, trans, id, **kwd ): - hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) - #if hda.history.id != self._decode_id( trans, history_id ): - # raise exceptions.ObjectNotFound( 'dataset was not found in this history' ) - hda_dict = self.get_hda_dict( trans, hda ) - hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda ) - hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda ) - return hda_dict - @expose_api_anonymous def create( self, trans, history_id, payload, **kwd ): """ @@ -326,11 +271,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio trans.sa_session.flush() if not hda: return None - #TODO: duplicate code - use a serializer with a view - hda_dict = self.get_hda_dict( trans, hda ) - hda_dict[ 'display_types' ] = self.get_old_display_applications( trans, hda ) - hda_dict[ 'display_apps' ] = self.get_display_apps( trans, hda ) - return hda_dict + return self.hda_serializer.serialize_to_view( trans, hda, view=kwd.get( 'view', 'detailed' ) ) def __create_dataset_collection( self, trans, history, payload, **kwd ): source = kwd.get("source", "new_collection") @@ -389,38 +330,44 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio return self.__handle_unknown_contents_type( trans, contents_type ) def __update_dataset( self, trans, history_id, id, payload, **kwd ): - changed = {} - # anon user + # anon user: ensure that history ids match up and the history is the current, + # check for uploading, and use only the subset of attribute keys manipulatable by anon users if trans.user == None: - if history_id != trans.security.encode_id( trans.history.id ): - raise exceptions.AuthenticationRequired( 'You must be logged in to update this history' ) + hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ) ) + if hda.history != trans.history: + raise exceptions.AuthenticationRequired( 'You must be logged in to update this dataset' ) + hda = self.mgrs.hdas.error_if_uploading( trans, hda ) + anon_allowed_payload = {} if 'deleted' in payload: anon_allowed_payload[ 'deleted' ] = payload[ 'deleted' ] if 'visible' in payload: anon_allowed_payload[ 'visible' ] = payload[ 'visible' ] - payload = self._validate_and_parse_update_payload( anon_allowed_payload ) - - hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ) ) - hda = self.mgrs.hdas.error_if_uploading( trans, hda ) - if hda.history != trans.history: - raise exceptions.AuthenticationRequired( 'You must be logged in to update this dataset' ) + payload = anon_allowed_payload + # logged in user: use full payload, check state if deleting, and make sure the history is theirs else: - payload = self._validate_and_parse_update_payload( payload ) + hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) + self.mgrs.histories.error_unless_owner( trans, hda.history, trans.user ) + # only check_state if not deleting, otherwise cannot delete uploading files check_state = not payload.get( 'deleted', False ) - hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) if check_state: hda = self.mgrs.hdas.error_if_uploading( trans, hda ) - #hda = self.get_dataset( trans, id, check_ownership=True, check_accessible=True, check_state=check_state ) + # make the actual changes + #TODO: to deserializer + #TODO: is this if still needed? if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): - changed = self.set_hda_from_dict( trans, hda, payload ) + #payload = self._validate_and_parse_update_payload( payload ) + #changed = self.set_hda_from_dict( trans, hda, payload ) + self.hda_deserializer.deserialize( trans, hda, payload ) + #TODO: this should be an effect of deleting the hda if payload.get( 'deleted', False ): self.stop_hda_creating_job( hda ) + return self.hda_serializer.serialize_to_view( trans, hda, view=kwd.get( 'view', 'detailed' ) ) - return changed + return {} def __update_dataset_collection( self, trans, history_id, id, payload, **kwd ): return trans.app.dataset_collections_service.update( trans, "history", id, payload ) @@ -473,68 +420,74 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio self.mgrs.histories.error_unless_ownership( trans, hda.history, trans.user ) self.mgrs.hdas.error_if_uploading( trans, hda ) - rval = { 'id' : id } - hda.deleted = True if purge: - if not trans.app.config.allow_user_dataset_purge: - raise exceptions.ConfigDoesNotAllowException( 'This instance does not allow user dataset purging' ) - hda.purged = True - trans.sa_session.add( hda ) - trans.sa_session.flush() - if hda.dataset.user_can_purge: - try: - hda.dataset.full_delete() - trans.sa_session.add( hda.dataset ) - except: - pass - # flush now to preserve deleted state in case of later interruption - trans.sa_session.flush() - rval[ 'purged' ] = True + self.mgrs.hdas.purge( trans, hda ) + else: + self.mgrs.hdas.delete( trans, hda ) + return self.hda_serializer.serialize_to_view( trans, hda, view=kwd.get( 'view', 'detailed' ) ) - self.stop_hda_creating_job( hda ) - trans.sa_session.flush() + #rval = { 'id' : id } + #hda.deleted = True + #if purge: + # if not trans.app.config.allow_user_dataset_purge: + # raise exceptions.ConfigDoesNotAllowException( 'This instance does not allow user dataset purging' ) + # hda.purged = True + # trans.sa_session.add( hda ) + # trans.sa_session.flush() + # if hda.dataset.user_can_purge: + # try: + # hda.dataset.full_delete() + # trans.sa_session.add( hda.dataset ) + # except: + # pass + # # flush now to preserve deleted state in case of later interruption + # trans.sa_session.flush() + # rval[ 'purged' ] = True + # + #self.stop_hda_creating_job( hda ) + #trans.sa_session.flush() + # + #rval[ 'deleted' ] = True + #return rval - rval[ 'deleted' ] = True - return rval - - def _validate_and_parse_update_payload( self, payload ): - """ - Validate and parse incomming data payload for an HDA. - """ - # This layer handles (most of the stricter idiot proofing): - # - unknown/unallowed keys - # - changing data keys from api key to attribute name - # - protection against bad data form/type - # - protection against malicious data content - # all other conversions and processing (such as permissions, etc.) should happen down the line - - # keys listed here don't error when attempting to set, but fail silently - # this allows PUT'ing an entire model back to the server without attribute errors on uneditable attrs - valid_but_uneditable_keys = ( - 'id', 'name', 'type', 'api_type', 'model_class', 'history_id', 'hid', - 'accessible', 'purged', 'state', 'data_type', 'file_ext', 'file_size', 'misc_blurb', - 'download_url', 'visualizations', 'display_apps', 'display_types', - 'metadata_dbkey', 'metadata_column_names', 'metadata_column_types', 'metadata_columns', - 'metadata_comment_lines', 'metadata_data_lines' - ) - validated_payload = {} - for key, val in payload.items(): - if val is None: - continue - if key in ( 'name', 'genome_build', 'misc_info', 'annotation' ): - val = self.validate_and_sanitize_basestring( key, val ) - #TODO: need better remap system or eliminate the need altogether - key = 'dbkey' if key == 'genome_build' else key - key = 'info' if key == 'misc_info' else key - validated_payload[ key ] = val - if key in ( 'deleted', 'visible' ): - validated_payload[ key ] = self.validate_boolean( key, val ) - elif key == 'tags': - validated_payload[ key ] = self.validate_and_sanitize_basestring_list( key, val ) - elif key not in valid_but_uneditable_keys: - pass - #log.warn( 'unknown key: %s', str( key ) ) - return validated_payload + #def _validate_and_parse_update_payload( self, payload ): + # """ + # Validate and parse incomming data payload for an HDA. + # """ + # # This layer handles (most of the stricter idiot proofing): + # # - unknown/unallowed keys + # # - changing data keys from api key to attribute name + # # - protection against bad data form/type + # # - protection against malicious data content + # # all other conversions and processing (such as permissions, etc.) should happen down the line + # + # # keys listed here don't error when attempting to set, but fail silently + # # this allows PUT'ing an entire model back to the server without attribute errors on uneditable attrs + # valid_but_uneditable_keys = ( + # 'id', 'name', 'type', 'api_type', 'model_class', 'history_id', 'hid', + # 'accessible', 'purged', 'state', 'data_type', 'file_ext', 'file_size', 'misc_blurb', + # 'download_url', 'visualizations', 'display_apps', 'display_types', + # 'metadata_dbkey', 'metadata_column_names', 'metadata_column_types', 'metadata_columns', + # 'metadata_comment_lines', 'metadata_data_lines' + # ) + # validated_payload = {} + # for key, val in payload.items(): + # if val is None: + # continue + # if key in ( 'name', 'genome_build', 'misc_info', 'annotation' ): + # val = validation.validate_and_sanitize_basestring( key, val ) + # #TODO: need better remap system or eliminate the need altogether + # key = 'dbkey' if key == 'genome_build' else key + # key = 'info' if key == 'misc_info' else key + # validated_payload[ key ] = val + # if key in ( 'deleted', 'visible' ): + # validated_payload[ key ] = validation.validate_boolean( key, val ) + # elif key == 'tags': + # validated_payload[ key ] = validation.validate_and_sanitize_basestring_list( key, val ) + # elif key not in valid_but_uneditable_keys: + # pass + # #log.warn( 'unknown key: %s', str( key ) ) + # return validated_payload def __handle_unknown_contents_type( self, trans, contents_type ): raise exceptions.UnknownContentsType('Unknown contents type: %s' % type) diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index eae97af6de8..2f526bb1b7c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -9,17 +9,24 @@ from galaxy import web from galaxy import exceptions from galaxy import managers from galaxy.datatypes.data import nice_size -from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings from galaxy.model.orm import and_, eagerload_all, func from galaxy import util from galaxy.util import Params from galaxy.util.odict import odict from galaxy.util.sanitize_html import sanitize_html from galaxy.web import error, url_for -from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin + +from galaxy.web.base.controller import BaseUIController +from galaxy.web.base.controller import SharableMixin +from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin +from galaxy.model.item_attrs import UsesAnnotations +from galaxy.model.item_attrs import UsesItemRatings + from galaxy.web.base.controller import ERROR, INFO, SUCCESS, WARNING + from galaxy.web.framework.helpers import grids, iff, time_ago, escape log = logging.getLogger( __name__ ) diff --git a/test/unit/managers/test_DatasetManager.py b/test/unit/managers/test_DatasetManager.py index 6a65b601e04..98bef00103e 100644 --- a/test/unit/managers/test_DatasetManager.py +++ b/test/unit/managers/test_DatasetManager.py @@ -20,6 +20,9 @@ from galaxy.util.bunch import Bunch import mock from test_ModelManager import BaseTestCase from galaxy.managers.datasets import DatasetManager +from galaxy.managers.datasets import DatasetAssociationManager +from galaxy.managers.histories import HistoryManager +from galaxy.managers.hdas import HDAManager # ============================================================================= @@ -29,83 +32,101 @@ user3_data = dict( email='user3@user3.user3', username='user3', password=default # ============================================================================= -class DatasetManagerTestCase( BaseTestCase ): +#class DatasetManagerTestCase( BaseTestCase ): +# +# def set_up_managers( self ): +# super( DatasetManagerTestCase, self ).set_up_managers() +# self.dataset_mgr = DatasetManager( self.app ) +# +# def test_create( self ): +# print "should be able to create a new Dataset" +# dataset1 = self.dataset_mgr.create( self.trans ) +# self.assertIsInstance( dataset1, model.Dataset ) +# self.assertEqual( dataset1, self.trans.sa_session.query( model.Dataset ).get( dataset1.id ) ) +# +# def test_base( self ): +# dataset1 = self.dataset_mgr.create( self.trans ) +# dataset2 = self.dataset_mgr.create( self.trans ) +# +# print "should be able to query" +# datasets = self.trans.sa_session.query( model.Dataset ).all() +# self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) +# self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) +# self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) +# self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) +# +# print "should be able to limit and offset" +# self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) +# self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) +# self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) +# +# self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) +# self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) +# +# print "should be able to order" +# self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), +# [ dataset2, dataset1 ] ) +# +# def test_delete( self ): +# item1 = self.dataset_mgr.create( self.trans ) +# +# print "should be able to delete and undelete an hda" +# self.assertFalse( item1.deleted ) +# self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) +# self.assertTrue( item1.deleted ) +# self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) +# self.assertFalse( item1.deleted ) +# +# def test_purge_allowed( self ): +# self.trans.app.config.allow_user_dataset_purge = True +# item1 = self.dataset_mgr.create( self.trans ) +# +# print "should purge an hda if config does allow" +# self.assertFalse( item1.purged ) +# self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) +# self.assertTrue( item1.purged ) +# +# def test_purge_not_allowed( self ): +# self.trans.app.config.allow_user_dataset_purge = False +# item1 = self.dataset_mgr.create( self.trans ) +# +# print "should raise an error when purging an hda if config does not allow" +# self.assertFalse( item1.purged ) +# self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) +# self.assertFalse( item1.purged ) +# +# def test_accessible( self ): +# owner = self.user_mgr.create( self.trans, **user2_data ) +# non_owner = self.user_mgr.create( self.trans, **user3_data ) +# +# dataset = self.dataset_mgr.create( self.trans ) +# +# print "(by default, dataset permissions are lax) should be accessible to all" +# for user in self.user_mgr.list( self.trans ): +# self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, user ) ) +# +# #TODO: this is ... not working... +# self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) +# self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) +# self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) + + +# ============================================================================= +class DatasetAssociationManagerTestCase( BaseTestCase ): def set_up_managers( self ): - super( DatasetManagerTestCase, self ).set_up_managers() - self.dataset_mgr = DatasetManager( self.app ) + super( DatasetAssociationManagerTestCase, self ).set_up_managers() + self.dataset_mgr = DatasetAssociationManager( self.app ) + self.hda_mgr = HDAManager( self.app ) + self.history_mgr = HistoryManager( self.app ) - def test_create( self ): - print "should be able to create a new Dataset" - dataset1 = self.dataset_mgr.create( self.trans ) - self.assertIsInstance( dataset1, model.Dataset ) - self.assertEqual( dataset1, self.trans.sa_session.query( model.Dataset ).get( dataset1.id ) ) - - def test_base( self ): - dataset1 = self.dataset_mgr.create( self.trans ) - dataset2 = self.dataset_mgr.create( self.trans ) - - print "should be able to query" - datasets = self.trans.sa_session.query( model.Dataset ).all() - self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) - self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) - self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) - self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) - - print "should be able to limit and offset" - self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) - self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) - self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) - - self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) - - print "should be able to order" - self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), - [ dataset2, dataset1 ] ) - - def test_delete( self ): - item1 = self.dataset_mgr.create( self.trans ) - - print "should be able to delete and undelete an hda" - self.assertFalse( item1.deleted ) - self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) - self.assertTrue( item1.deleted ) - self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) - self.assertFalse( item1.deleted ) - - def test_purge_allowed( self ): - self.trans.app.config.allow_user_dataset_purge = True - item1 = self.dataset_mgr.create( self.trans ) - - print "should purge an hda if config does allow" - self.assertFalse( item1.purged ) - self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) - self.assertTrue( item1.purged ) - - def test_purge_not_allowed( self ): - self.trans.app.config.allow_user_dataset_purge = False - item1 = self.dataset_mgr.create( self.trans ) - - print "should raise an error when purging an hda if config does not allow" - self.assertFalse( item1.purged ) - self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) - self.assertFalse( item1.purged ) - - def test_accessible( self ): + def test_metadata( self ): owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) - dataset = self.dataset_mgr.create( self.trans ) - - print "(by default, dataset permissions are lax) should be accessible to all" - for user in self.user_mgr.list( self.trans ): - self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, user ) ) - -#TODO: this is ... not working... - self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) - self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) - self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) + print "should be able to create a new HDA with a specified history and dataset" + hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1, hid=1 ) # ============================================================================= From ca370e3cfcd3687b0d6f244f596d141a823488a2 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:25 -0500 Subject: [PATCH 017/473] Remove some unused imports from lib/galaxy/tools/__init__.py. --- lib/galaxy/tools/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index ecfa87e5a45..ed67e1f5658 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -7,20 +7,15 @@ import glob import json import logging import os -import pipes import re import shutil -import sys import string import tarfile import tempfile import threading -import traceback import types import urllib -from math import isinf - from galaxy import eggs eggs.require( "MarkupSafe" ) # MarkupSafe must load before mako eggs.require( "Mako" ) @@ -35,10 +30,8 @@ from paste import httpexceptions from sqlalchemy import and_ from galaxy import jobs, model -from galaxy.jobs.error_level import StdioErrorLevel from galaxy.datatypes.metadata import JobExternalOutputMetadataWrapper from galaxy import exceptions -from galaxy.jobs import ParallelismInfo from galaxy.tools import watcher from galaxy.tools.actions import DefaultToolAction from galaxy.tools.actions.data_source import DataSourceToolAction @@ -58,12 +51,11 @@ from galaxy.tools.filters import FilterFactory from galaxy.tools.test import parse_tests from galaxy.tools.parser import get_tool_source from galaxy.tools.parser.xml import XmlPageSource -from galaxy.util import listify, parse_xml, rst_to_html, string_as_bool, string_to_object, xml_text, xml_to_string +from galaxy.util import listify, parse_xml, rst_to_html, string_as_bool, string_to_object from galaxy.tools.parameters.meta import expand_meta_parameters from galaxy.util.bunch import Bunch from galaxy.util.expressions import ExpressionContext from galaxy.util.hash_util import hmac_new -from galaxy.util.none_like import NoneDataset from galaxy.util.odict import odict from galaxy.util.template import fill_template from galaxy.web import url_for @@ -73,7 +65,7 @@ from galaxy.model.item_attrs import Dictifiable from galaxy.model import Workflow from tool_shed.util import common_util from tool_shed.util import shed_util_common as suc -from .loader import load_tool, template_macro_params, raw_tool_xml_tree, imported_macro_paths +from .loader import template_macro_params, raw_tool_xml_tree, imported_macro_paths from .execute import execute as execute_job from .wrappers import ( ToolParameterValueWrapper, From 7fa267ebcdc601567ea8af7c84ebc5cb42c4f95b Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:25 -0500 Subject: [PATCH 018/473] More PEP-8 style tweaks to lib/galaxy/tools/__init__.py. --- lib/galaxy/tools/__init__.py | 57 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index ed67e1f5658..40213724a46 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -257,8 +257,9 @@ class ToolBox( object, Dictifiable ): return tool_path def __add_tool_to_tool_panel( self, tool, panel_component, section=False ): - # See if a version of this tool is already loaded into the tool panel. The value of panel_component - # will be a ToolSection (if the value of section=True) or self.tool_panel (if section=False). + # See if a version of this tool is already loaded into the tool panel. + # The value of panel_component will be a ToolSection (if the value of + # section=True) or self.tool_panel (if section=False). tool_id = str( tool.id ) tool = self.tools_by_id[ tool_id ] if section: @@ -300,13 +301,17 @@ class ToolBox( object, Dictifiable ): if not inserted: inserted = True if not inserted: - if tool.guid is None or \ - tool.tool_shed is None or \ - tool.repository_name is None or \ - tool.repository_owner is None or \ - tool.installed_changeset_revision is None: - # We have a tool that was not installed from the Tool Shed, but is also not yet defined in - # integrated_tool_panel.xml, so append it to the tool panel. + if ( + tool.guid is None or + tool.tool_shed is None or + tool.repository_name is None or + tool.repository_owner is None or + tool.installed_changeset_revision is None + ): + # We have a tool that was not installed from the Tool + # Shed, but is also not yet defined in + # integrated_tool_panel.xml, so append it to the tool + # panel. panel_dict[ key ] = tool log.debug( "Loaded tool id: %s, version: %s into tool panel.." % ( tool.id, tool.version ) ) else: @@ -364,9 +369,10 @@ class ToolBox( object, Dictifiable ): def load_integrated_tool_panel_keys( self ): """ - Load the integrated tool panel keys, setting values for tools and workflows to None. The values will - be reset when the various tool panel config files are parsed, at which time the tools and workflows are - loaded. + Load the integrated tool panel keys, setting values for tools and + workflows to None. The values will be reset when the various tool + panel config files are parsed, at which time the tools and workflows + are loaded. """ tree = parse_xml( self.integrated_tool_panel_config ) root = tree.getroot() @@ -499,11 +505,11 @@ class ToolBox( object, Dictifiable ): # We store only the port, if one exists, in the database. tool_shed = common_util.remove_protocol_from_tool_shed_url( tool_shed ) return self.app.install_model.context.query( self.app.install_model.ToolShedRepository ) \ - .filter( and_( self.app.install_model.ToolShedRepository.table.c.tool_shed == tool_shed, - self.app.install_model.ToolShedRepository.table.c.name == name, - self.app.install_model.ToolShedRepository.table.c.owner == owner, - self.app.install_model.ToolShedRepository.table.c.installed_changeset_revision == installed_changeset_revision ) ) \ - .first() + .filter( and_( self.app.install_model.ToolShedRepository.table.c.tool_shed == tool_shed, + self.app.install_model.ToolShedRepository.table.c.name == name, + self.app.install_model.ToolShedRepository.table.c.owner == owner, + self.app.install_model.ToolShedRepository.table.c.installed_changeset_revision == installed_changeset_revision ) ) \ + .first() def get_tool_components( self, tool_id, tool_version=None, get_loaded_tools_by_lineage=False, set_selected=False ): """ @@ -627,12 +633,14 @@ class ToolBox( object, Dictifiable ): log.exception( "Error reading tool from path: %s" % path ) def __add_tool( self, tool, load_panel_dict, panel_dict ): - # Allow for the same tool to be loaded into multiple places in the tool panel. We have to handle - # the case where the tool is contained in a repository installed from the tool shed, and the Galaxy - # administrator has retrieved updates to the installed repository. In this case, the tool may have - # been updated, but the version was not changed, so the tool should always be reloaded here. We used - # to only load the tool if it was not found in self.tools_by_id, but performing that check did - # not enable this scenario. + # Allow for the same tool to be loaded into multiple places in the + # tool panel. We have to handle the case where the tool is contained + # in a repository installed from the tool shed, and the Galaxy + # administrator has retrieved updates to the installed repository. In + # this case, the tool may have been updated, but the version was not + # changed, so the tool should always be reloaded here. We used to + # only load the tool if it was not found in self.tools_by_id, but + # performing that check did not enable this scenario. self.tools_by_id[ tool.id ] = tool if load_panel_dict: self.__add_tool_to_tool_panel( tool, panel_dict, section=isinstance( panel_dict, ToolSection ) ) @@ -787,8 +795,6 @@ class ToolBox( object, Dictifiable ): :param tool_id: the tool ID from app.toolbox :returns: tuple of tarball filename, success True/False, message/None """ - message = '' - success = True # Make sure the tool is actually loaded. if tool_id not in self.tools_by_id: return None, False, "No tool with id %s" % escape( tool_id ) @@ -870,7 +876,6 @@ class ToolBox( object, Dictifiable ): if os.path.exists( sample_file ): tarfile_path, tarfile_name = os.path.split( tar_file ) tarfile_path = os.path.join( 'tool-data', tarfile_name ) - sample_name = tarfile_path + '.sample' tarball_files.append( ( sample_file, tarfile_path ) ) data_table_definitions.append( data_table.xml_string ) if len( data_table_definitions ) > 0: From a23846839b0fc16e8ca98bdff60131c9b98111dc Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:26 -0500 Subject: [PATCH 019/473] Small simplifications to lib/galaxy/tools/__init__.py. --- lib/galaxy/tools/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 40213724a46..2491fc149dd 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -289,8 +289,7 @@ class ToolBox( object, Dictifiable ): for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ): if key == integrated_panel_key: panel_dict.insert( index, key, tool ) - if not inserted: - inserted = True + inserted = True if not inserted: # Check the tool's installed versions. for lineage_id in tool.lineage_ids: @@ -298,8 +297,7 @@ class ToolBox( object, Dictifiable ): for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ): if lineage_id_key == integrated_panel_key: panel_dict.insert( index, key, tool ) - if not inserted: - inserted = True + inserted = True if not inserted: if ( tool.guid is None or @@ -322,8 +320,7 @@ class ToolBox( object, Dictifiable ): if lineage_id in self.tools_by_id: loaded_version_key = 'tool_%s' % lineage_id if loaded_version_key in panel_dict: - if not already_loaded: - already_loaded = True + already_loaded = True if not already_loaded: # If the tool is not defined in integrated_tool_panel.xml, append it to the tool panel. panel_dict[ key ] = tool From 9d899a06543d19e03f52a7276a8a7c1f84f7e9b8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:26 -0500 Subject: [PATCH 020/473] Remove broken method fix_integrated_tool_panel_dict. As of 5f6c670 (Jan 10, 2014) this does nothing (``if section_value is None`` followed by ``if isinstance( section_value, Tool ):``) and nobody seemed to notice - so I assume it is unneeded. If still needed - someone needs to fix it :). --- lib/galaxy/tools/__init__.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 2491fc149dd..8720be852ea 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -163,24 +163,8 @@ class ToolBox( object, Dictifiable ): # This will cover cases where the Galaxy administrator manually edited one or more of the tool panel # config files, adding or removing locally developed tools or workflows. The value of integrated_tool_panel # will be False when things like functional tests are the caller. - self.fix_integrated_tool_panel_dict() self.write_integrated_tool_panel_config_file() - def fix_integrated_tool_panel_dict( self ): - # HACK: instead of fixing after the fact, I suggest some combination of: - # 1) adjusting init_tools() and called methods to get this right - # 2) redesigning the code and/or data structure used to read/write integrated_tool_panel.xml - for key, value in self.integrated_tool_panel.iteritems(): - if isinstance( value, ToolSection ): - for section_key, section_value in value.elems.iteritems(): - if section_value is None: - if isinstance( section_value, Tool ): - tool_id = section_key[5:] - value.elems[section_key] = self.tools_by_id.get( tool_id ) - elif isinstance( section_value, Workflow ): - workflow_id = section_key[9:] - value.elems[section_key] = self.workflows_by_id.get( workflow_id ) - def init_tools( self, config_filename ): """ Read the configuration file and load each tool. The following tags are currently supported: @@ -723,7 +707,6 @@ class ToolBox( object, Dictifiable ): # This will cover cases where the Galaxy administrator manually edited one or more of the tool panel # config files, adding or removing locally developed tools or workflows. The value of integrated_tool_panel # will be False when things like functional tests are the caller. - self.fix_integrated_tool_panel_dict() self.write_integrated_tool_panel_config_file() return tool.id except Exception: From 92e6a8e4fffd827f7f46da53a71b647b31f5906d Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:27 -0500 Subject: [PATCH 021/473] Slightly rework lib/galaxy/workflow/modules.py for readability. --- lib/galaxy/workflow/modules.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 7fa12e7e8c8..d86f167aaa9 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -506,16 +506,17 @@ class ToolModule( WorkflowModule ): @classmethod def from_workflow_step( Class, trans, step ): + toolbox = trans.app.toolbox tool_id = step.tool_id - if trans.app.toolbox and tool_id not in trans.app.toolbox.tools_by_id: + if toolbox and tool_id not in toolbox.tools_by_id: # See if we have access to a different version of the tool. # TODO: If workflows are ever enhanced to use tool version # in addition to tool id, enhance the selection process here # to retrieve the correct version of the tool. - tool = trans.app.toolbox.get_tool( tool_id ) + tool = toolbox.get_tool( tool_id ) if tool: tool_id = tool.id - if ( trans.app.toolbox and tool_id in trans.app.toolbox.tools_by_id ): + if ( toolbox and tool_id in toolbox.tools_by_id ): if step.config: # This step has its state saved in the config field due to the # tool being previously unavailable. From 917175d2370e6e55164437a55431821cffc1b6d9 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:27 -0500 Subject: [PATCH 022/473] Rework toolbox 'fix' tool tags when multiple configs are loaded. Looks like the tool tag stuff still assumed there was just one config file - that said toolbox doesn't have a self.sa_session so I think tool tags are broken and have been for some time. --- lib/galaxy/tools/__init__.py | 41 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 8720be852ea..162395793e3 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -143,18 +143,7 @@ class ToolBox( object, Dictifiable ): self.tool_watcher = watcher.get_watcher( self, app.config ) self.filter_factory = FilterFactory( self ) self.init_dependency_manager() - config_filenames = listify( config_filenames ) - for config_filename in config_filenames: - if os.path.isdir( config_filename ): - directory_contents = sorted( os.listdir( config_filename ) ) - directory_config_files = [ config_file for config_file in directory_contents if config_file.endswith( ".xml" ) ] - config_filenames.remove( config_filename ) - config_filenames.extend( directory_config_files ) - for config_filename in config_filenames: - try: - self.init_tools( config_filename ) - except: - log.exception( "Error loading tools defined in config %s", config_filename ) + self.init_tools_from_configs( config_filenames ) if self.app.name == 'galaxy' and self.integrated_tool_panel_config_has_contents: # Load self.tool_panel based on the order in self.integrated_tool_panel. self.load_tool_panel() @@ -165,7 +154,29 @@ class ToolBox( object, Dictifiable ): # will be False when things like functional tests are the caller. self.write_integrated_tool_panel_config_file() - def init_tools( self, config_filename ): + def init_tools_from_configs( self, config_filenames ): + """ Read through all tool config files and initialize tools in each + with init_tools_from_config below. + """ + if self.app.config.get_bool( 'enable_tool_tags', False ): + log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" ) + self.sa_session.query( self.app.model.ToolTagAssociation ).delete() + self.sa_session.flush() + + config_filenames = listify( config_filenames ) + for config_filename in config_filenames: + if os.path.isdir( config_filename ): + directory_contents = sorted( os.listdir( config_filename ) ) + directory_config_files = [ config_file for config_file in directory_contents if config_file.endswith( ".xml" ) ] + config_filenames.remove( config_filename ) + config_filenames.extend( directory_config_files ) + for config_filename in config_filenames: + try: + self.init_tools_from_config( config_filename ) + except: + log.exception( "Error loading tools defined in config %s", config_filename ) + + def init_tools_from_config( self, config_filename ): """ Read the configuration file and load each tool. The following tags are currently supported: @@ -183,10 +194,6 @@ class ToolBox( object, Dictifiable ): """ - if self.app.config.get_bool( 'enable_tool_tags', False ): - log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" ) - self.sa_session.query( self.app.model.ToolTagAssociation ).delete() - self.sa_session.flush() log.info( "Parsing the tool configuration %s" % config_filename ) tree = parse_xml( config_filename ) root = tree.getroot() From 34e7ac89c5a749bb92d4dadce9619ec4cc4ea037 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:28 -0500 Subject: [PATCH 023/473] Refactor tool tag handling out of toolbox. Pretty sure this had been broken (toolbox didn't have a self.sa_session). Anyway - this probably didn't fix that but at least now the big read tool tag loop is more readable (cyclomatic complexity of that method down from 18 to 12). --- lib/galaxy/tools/__init__.py | 30 ++--------- lib/galaxy/tools/toolbox/__init__.py | 7 +++ lib/galaxy/tools/toolbox/tags.py | 76 ++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 26 deletions(-) create mode 100644 lib/galaxy/tools/toolbox/__init__.py create mode 100644 lib/galaxy/tools/toolbox/tags.py diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 162395793e3..c200e062395 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -51,6 +51,7 @@ from galaxy.tools.filters import FilterFactory from galaxy.tools.test import parse_tests from galaxy.tools.parser import get_tool_source from galaxy.tools.parser.xml import XmlPageSource +from galaxy.tools.toolbox import tool_tag_manager from galaxy.util import listify, parse_xml, rst_to_html, string_as_bool, string_to_object from galaxy.tools.parameters.meta import expand_meta_parameters from galaxy.util.bunch import Bunch @@ -143,6 +144,7 @@ class ToolBox( object, Dictifiable ): self.tool_watcher = watcher.get_watcher( self, app.config ) self.filter_factory = FilterFactory( self ) self.init_dependency_manager() + self.tool_tag_manager = tool_tag_manager( app ) self.init_tools_from_configs( config_filenames ) if self.app.name == 'galaxy' and self.integrated_tool_panel_config_has_contents: # Load self.tool_panel based on the order in self.integrated_tool_panel. @@ -158,11 +160,7 @@ class ToolBox( object, Dictifiable ): """ Read through all tool config files and initialize tools in each with init_tools_from_config below. """ - if self.app.config.get_bool( 'enable_tool_tags', False ): - log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" ) - self.sa_session.query( self.app.model.ToolTagAssociation ).delete() - self.sa_session.flush() - + self.tool_tag_manager.reset_tags() config_filenames = listify( config_filenames ) for config_filename in config_filenames: if os.path.isdir( config_filename ): @@ -588,27 +586,7 @@ class ToolBox( object, Dictifiable ): self.app.install_model.context.flush() # Load the tool's lineage ids. tool.lineage_ids = tool.tool_version.get_version_ids( self.app ) - if self.app.config.get_bool( 'enable_tool_tags', False ): - tag_names = elem.get( "tags", "" ).split( "," ) - for tag_name in tag_names: - if tag_name == '': - continue - tag = self.sa_session.query( self.app.model.Tag ).filter_by( name=tag_name ).first() - if not tag: - tag = self.app.model.Tag( name=tag_name ) - self.sa_session.add( tag ) - self.sa_session.flush() - tta = self.app.model.ToolTagAssociation( tool_id=tool.id, tag_id=tag.id ) - self.sa_session.add( tta ) - self.sa_session.flush() - else: - for tagged_tool in tag.tagged_tools: - if tagged_tool.tool_id == tool.id: - break - else: - tta = self.app.model.ToolTagAssociation( tool_id=tool.id, tag_id=tag.id ) - self.sa_session.add( tta ) - self.sa_session.flush() + self.tool_tag_manager.handle_tags( tool.id, elem ) self.__add_tool( tool, load_panel_dict, panel_dict ) # Always load the tool into the integrated_panel_dict, or it will not be included in the integrated_tool_panel.xml file. if key in integrated_panel_dict or index is None: diff --git a/lib/galaxy/tools/toolbox/__init__.py b/lib/galaxy/tools/toolbox/__init__.py new file mode 100644 index 00000000000..29bc07741d3 --- /dev/null +++ b/lib/galaxy/tools/toolbox/__init__.py @@ -0,0 +1,7 @@ +""" API for this module containing functionality related to the tool box. +""" + +from .tags import tool_tag_manager + + +__all__ = ["tool_tag_manager"] diff --git a/lib/galaxy/tools/toolbox/tags.py b/lib/galaxy/tools/toolbox/tags.py new file mode 100644 index 00000000000..a8aa6c53360 --- /dev/null +++ b/lib/galaxy/tools/toolbox/tags.py @@ -0,0 +1,76 @@ +""" Handle details of tool tagging - perhaps a deprecated feature. +""" +from abc import ABCMeta +from abc import abstractmethod + +import logging +log = logging.getLogger( __name__ ) + + +def tool_tag_manager( app ): + """ Build a tool tag manager according to app's configuration + and return it. + """ + if hasattr( app.config, "get_bool" ) and app.config.get_bool( 'enable_tool_tags', False ): + return PersistentToolTagManager( app ) + else: + return NullToolTagManager() + + +class AbstractToolTagManager( object ): + __metaclass__ = ABCMeta + + @abstractmethod + def reset_tags( self ): + """ Starting to load tool panels, reset all tags. + """ + + @abstractmethod + def handle_tags( self, tool_id, tool_definition_source ): + """ Parse out tags and persist them. + """ + + +class NullToolTagManager( AbstractToolTagManager ): + + def reset_tags( self ): + return None + + def handle_tags( self, tool_id, tool_definition_source ): + return None + + +class PersistentToolTagManager( AbstractToolTagManager ): + + def __init__( self, app ): + self.app = app + self.sa_session = app.sa_session + + def reset_tags( self ): + log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" ) + self.sa_session.query( self.app.model.ToolTagAssociation ).delete() + self.sa_session.flush() + + def handle_tags( self, tool_id, tool_definition_source ): + elem = tool_definition_source + if self.app.config.get_bool( 'enable_tool_tags', False ): + tag_names = elem.get( "tags", "" ).split( "," ) + for tag_name in tag_names: + if tag_name == '': + continue + tag = self.sa_session.query( self.app.model.Tag ).filter_by( name=tag_name ).first() + if not tag: + tag = self.app.model.Tag( name=tag_name ) + self.sa_session.add( tag ) + self.sa_session.flush() + tta = self.app.model.ToolTagAssociation( tool_id=tool_id, tag_id=tag.id ) + self.sa_session.add( tta ) + self.sa_session.flush() + else: + for tagged_tool in tag.tagged_tools: + if tagged_tool.tool_id == tool_id: + break + else: + tta = self.app.model.ToolTagAssociation( tool_id=tool_id, tag_id=tag.id ) + self.sa_session.add( tta ) + self.sa_session.flush() From dee0793e5a0d5955cdf7da26d694faed04b818b0 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:28 -0500 Subject: [PATCH 024/473] Tweak completely broken DataTransfer._execute_workflow to respect toolbox abstraction. Least of the brokeness at this point I suppose - but I would like to not expose tools_by_id external to the Toolbox. --- lib/galaxy/jobs/deferred/data_transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/jobs/deferred/data_transfer.py b/lib/galaxy/jobs/deferred/data_transfer.py index f1b4969e707..4589b2f7117 100644 --- a/lib/galaxy/jobs/deferred/data_transfer.py +++ b/lib/galaxy/jobs/deferred/data_transfer.py @@ -356,7 +356,7 @@ class DataTransfer( object ): for i, step in enumerate( workflow.steps ): job = None if step.type == 'tool' or step.type is None: - tool = self.app.toolbox.tools_by_id[ step.tool_id ] + tool = self.app.toolbox.get_tool( step.tool_id ) def callback( input, value, prefixed_name, prefixed_label ): if isinstance( input, DataToolParameter ): if prefixed_name in step.input_connections_by_name: From fb1e9fc5f40065c5ca7398e13d2c60d5e405fc85 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:30 -0500 Subject: [PATCH 025/473] Start decoupling Toolbox elements from XML (sections+labels). Just a small tweak and some tests to verify these things can indeed be created from dictionaries. Also slightly simplifies galaxy.tools. --- lib/galaxy/tools/__init__.py | 18 ++++++++++++------ test/unit/tools/test_tool_panel.py | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 test/unit/tools/test_tool_panel.py diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index c200e062395..81983802dba 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -330,12 +330,13 @@ class ToolBox( object, Dictifiable ): elif isinstance( val, ToolSectionLabel ): self.tool_panel[ key ] = val elif isinstance( val, ToolSection ): - elem = ElementTree.Element( 'section' ) - elem.attrib[ 'id' ] = val.id or '' - elem.attrib[ 'name' ] = val.name or '' - elem.attrib[ 'version' ] = val.version or '' - section = ToolSection( elem ) - log.debug( "Loading section: %s" % elem.get( 'name' ) ) + section_dict = { + 'id': val.id or '', + 'name': val.name or '', + 'version': val.version or '', + } + section = ToolSection( section_dict ) + log.debug( "Loading section: %s" % section_dict.get( 'name' ) ) for section_key, section_val in val.elems.items(): if isinstance( section_val, Tool ): tool_id = section_key.replace( 'tool_', '', 1 ) @@ -1050,6 +1051,8 @@ class ToolSection( object, Dictifiable ): dict_collection_visible_keys = ( 'id', 'name', 'version' ) def __init__( self, elem=None ): + """ Build a ToolSection from an ElementTree element or a dictionary. + """ f = lambda elem, val: elem is not None and elem.get( val ) or '' self.name = f( elem, 'name' ) self.id = f( elem, 'id' ) @@ -1089,6 +1092,9 @@ class ToolSectionLabel( object, Dictifiable ): dict_collection_visible_keys = ( 'id', 'text', 'version' ) def __init__( self, elem ): + """ Build a ToolSectionLabel from an ElementTree element or a + dictionary. + """ self.text = elem.get( "text" ) self.id = elem.get( "id" ) self.version = elem.get( "version" ) or '' diff --git a/test/unit/tools/test_tool_panel.py b/test/unit/tools/test_tool_panel.py new file mode 100644 index 00000000000..33831f9ded7 --- /dev/null +++ b/test/unit/tools/test_tool_panel.py @@ -0,0 +1,26 @@ +from xml.etree import ElementTree as ET +from galaxy.tools import ToolSection + + +def test_tool_section( ): + elem = ET.Element( 'section' ) + elem.attrib[ 'name' ] = "Cool Tools" + elem.attrib[ 'id' ] = "cool1" + + section = ToolSection( elem ) + assert section.id == "cool1" + assert section.name == "Cool Tools" + assert section.version == "" + + section = ToolSection( dict( + id="cool1", + name="Cool Tools" + ) ) + assert section.id == "cool1" + assert section.name == "Cool Tools" + assert section.version == "" + + section = ToolSection() + assert section.id == "" + assert section.name == "" + assert section.version == "" From 8a10d14c3260f7b369acdd7e30c6d81a4bafa6d1 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 22 Dec 2014 13:16:30 -0500 Subject: [PATCH 026/473] Add comment to auto-generated integrated_tool_panel.xml. Someone more familiar with integrated_tool_panel.xml should feel free to make this comment sharper - but something is better nothing I think. --- lib/galaxy/tools/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 81983802dba..8b1a2af38d8 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -96,6 +96,16 @@ JOB_RESOURCE_CONDITIONAL_XML = """ HELP_UNINITIALIZED = threading.Lock() +INTEGRATED_TOOL_PANEL_DESCRIPTION = """ +This is Galaxy's integrated tool panel and probably should not be modified +directly. It will be regenerated each time Galaxy starts up. To modify locally +managed tools (e.g. from tool_conf.xml) modify that file directly and restart +Galaxy. Whenever possible Tool Shed managed tools (e.g. from shed_tool_conf.xml) +should be managed from within the Galaxy interface of via is UI - but if changes +are nessecary (such as to hide a tool or re-assign its section) modify that file +and restart Galaxy. +""" + class ToolNotFoundException( Exception ): pass @@ -396,6 +406,9 @@ class ToolBox( object, Dictifiable ): fd, filename = tempfile.mkstemp() os.write( fd, '\n' ) os.write( fd, '\n' ) + os.write( fd, ' \n') for key, item in self.integrated_tool_panel.items(): if item: if isinstance( item, Tool ): From 981954e1077c82ff459c3748517a5ea1c72b7407 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Mon, 22 Dec 2014 14:35:08 -0500 Subject: [PATCH 027/473] UsesHistoryMixin, get_history: allow anon users to get their current history; update browser tests; remove unused selenium tests --- lib/galaxy/web/base/controller.py | 5 + test/casperjs/anon-history-tests.js | 198 +++--- .../api-anon-history-permission-tests.js | 327 +++++---- test/casperjs/api-anon-history-tests.js | 181 +++-- test/casperjs/api-configuration-tests.js | 91 ++- test/casperjs/api-hda-tests.js | 664 +++++++++--------- test/casperjs/api-history-permission-tests.js | 445 ++++++------ test/casperjs/api-history-tests.js | 2 +- test/casperjs/api-tool-tests.js | 2 +- test/casperjs/api-user-tests.js | 68 +- test/casperjs/api-visualizations-tests.js | 336 ++++----- test/casperjs/api-workflow-tests.js | 440 ++++++------ test/casperjs/hda-state-tests.js | 658 +++++++++-------- test/casperjs/history-options-tests.js | 166 ++--- test/casperjs/history-panel-tests.js | 451 ++++++------ test/casperjs/modules/api.js | 20 +- test/casperjs/modules/historyoptions.js | 53 +- test/casperjs/upload-tests.js | 101 +-- test/selenium/root/Login.html | 52 -- test/selenium/root/Suite.html | 15 - test/selenium/root/UserRegistration.html | 137 ---- test/selenium/visualization/Trackster.html | 62 -- .../workflow/CreateSimpleWorkflow.html | 188 ----- test/selenium/workflow/CreateUser.html | 67 -- ...kflowWithGroupingAndRuntimeParameters.html | 217 ------ ...kflowWithGroupingAndRuntimeParameters.html | 92 --- test/selenium/workflow/Suite.html | 18 - .../workflow/VerifyRunSimpleWorkflow.html | 162 ----- 28 files changed, 2018 insertions(+), 3200 deletions(-) delete mode 100644 test/selenium/root/Login.html delete mode 100644 test/selenium/root/Suite.html delete mode 100644 test/selenium/root/UserRegistration.html delete mode 100644 test/selenium/visualization/Trackster.html delete mode 100644 test/selenium/workflow/CreateSimpleWorkflow.html delete mode 100644 test/selenium/workflow/CreateUser.html delete mode 100644 test/selenium/workflow/CreateWorkflowWithGroupingAndRuntimeParameters.html delete mode 100644 test/selenium/workflow/RunWorkflowWithGroupingAndRuntimeParameters.html delete mode 100644 test/selenium/workflow/Suite.html delete mode 100644 test/selenium/workflow/VerifyRunSimpleWorkflow.html diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 7567daa6d5d..08df7a61031 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -255,6 +255,11 @@ class UsesHistoryMixin( SharableItemSecurityMixin ): """ Get a History from the database by id, verifying ownership. """ + if trans.user is None and trans.history: + if id == trans.security.encode_id( trans.history.id ): + return trans.history + raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' ) + history = self.get_object( trans, id, 'History', check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) history = self.security_check( trans, history, check_ownership, check_accessible ) diff --git a/test/casperjs/anon-history-tests.js b/test/casperjs/anon-history-tests.js index 1b9c1b8b01e..08d9908abc2 100644 --- a/test/casperjs/anon-history-tests.js +++ b/test/casperjs/anon-history-tests.js @@ -7,117 +7,111 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Testing histories for anonymous users', 0, function suite( test ){ spaceghost.start(); -// =================================================================== + // =================================================================== + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + spaceghost.info( 'Will use fixtureData.testUser: ' + email ); + } -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; - spaceghost.info( 'Will use fixtureData.testUser: ' + email ); -} + var editableTextClass = spaceghost.data.selectors.editableText, + editableTextInput = spaceghost.data.selectors.editableTextInput, -var tooltipSelector = spaceghost.data.selectors.tooltipBalloon, - editableTextClass = spaceghost.data.selectors.editableText, - editableTextInput = spaceghost.data.selectors.editableTextInput, + unnamedName = spaceghost.historypanel.data.text.history.newName, + nameSelector = spaceghost.historypanel.data.selectors.history.name, + sizeSelector = spaceghost.historypanel.data.selectors.history.size, + initialSizeStr = spaceghost.historypanel.data.text.history.newSize, + tagIconSelector = spaceghost.historypanel.data.selectors.history.tagIcon, + annoIconSelector = spaceghost.historypanel.data.selectors.history.annoIcon, + emptyMsgSelector = spaceghost.historypanel.data.selectors.history.emptyMsg, + emptyMsgStr = spaceghost.historypanel.data.text.history.emptyMsg; - unnamedName = spaceghost.historypanel.data.text.history.newName, - nameSelector = spaceghost.historypanel.data.selectors.history.name, - sizeSelector = spaceghost.historypanel.data.selectors.history.size, - initialSizeStr = spaceghost.historypanel.data.text.history.newSize, - tagIconSelector = spaceghost.historypanel.data.selectors.history.tagIcon, - annoIconSelector = spaceghost.historypanel.data.selectors.history.annoIcon, - emptyMsgSelector = spaceghost.historypanel.data.selectors.history.emptyMsg, - emptyMsgStr = spaceghost.historypanel.data.text.history.emptyMsg, - anonNameTooltip = spaceghost.historypanel.data.text.anonymous.tooltips.name; - -var historyFrameInfo = {}, - filenameToUpload = '1.txt', - filepathToUpload = '../../test-data/' + filenameToUpload, - testUploadInfo = {}; + var filenameToUpload = '1.txt', + filepathToUpload = '../../test-data/' + filenameToUpload; -// =================================================================== TESTS -// ------------------------------------------------------------------- check the anonymous new, history for form -spaceghost.openHomePage().historypanel.waitForHdas( function testPanelStructure(){ - this.test.comment( 'history panel for anonymous user, new history' ); + // ------------------------------------------------------------------- check the anonymous new, history for form + spaceghost.openHomePage().historypanel.waitForHdas( function testPanelStructure(){ + this.test.comment( 'history panel for anonymous user, new history' ); - this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); - this.test.assertExists( nameSelector, nameSelector + ' exists' ); - this.test.assertVisible( nameSelector, 'History name is visible' ); - this.test.assertSelectorHasText( nameSelector, unnamedName, 'History name is ' + unnamedName ); - - this.test.comment( "history should display size and size should be 0 bytes" ); - this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); - this.test.assertVisible( sizeSelector, 'History size is visible' ); - this.test.assertSelectorHasText( sizeSelector, initialSizeStr, - 'History size has "' + initialSizeStr + '"' ); - - this.test.comment( "NO tags or annotations icons should be available for an anonymous user" ); - this.test.assertDoesntExist( tagIconSelector, 'Tag icon button not found' ); - this.test.assertDoesntExist( annoIconSelector, 'Annotation icon button not found' ); - - this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); - this.test.comment( "A message about the current history being empty should be displayed" ); - this.test.assertVisible( emptyMsgSelector, 'Empty history message is visible' ); - this.test.assertSelectorHasText( emptyMsgSelector, emptyMsgStr, - 'Message contains "' + emptyMsgStr + '"' ); - - this.test.comment( 'name should NOT be editable when clicked by anon-user' ); - this.assertDoesntHaveClass( nameSelector, editableTextClass, "Name field is not classed as editable text" ); - this.click( nameSelector ); - this.test.assertDoesntExist( editableTextInput, "Clicking on name does not create an input" ); -}); - -// ------------------------------------------------------------------- anon user can upload file -spaceghost.then( function testAnonUpload(){ - this.test.comment( 'anon-user should be able to upload files' ); - - spaceghost.tools.uploadFile( filepathToUpload, function uploadCallback( _uploadInfo ){ - this.debug( 'uploaded HDA info: ' + this.jsonStr( this.quickInfo( _uploadInfo.hdaElement ) ) ); - var hasHda = _uploadInfo.hdaElement, - hasClass = _uploadInfo.hdaElement.attributes[ 'class' ], - hasOkClass = _uploadInfo.hdaElement.attributes[ 'class' ].indexOf( 'state-ok' ) !== -1; - this.test.assert( ( hasHda && hasClass && hasOkClass ), "Uploaded file: " + _uploadInfo.hdaElement.text ); - testUploadInfo = _uploadInfo; - }); - -}); -spaceghost.then( function testAnonUpload(){ - this.test.comment( "empty should be NO LONGER be displayed" ); - this.test.assertNotVisible( emptyMsgSelector, 'Empty history message is not visible' ); -}); - -// ------------------------------------------------------------------- anon user can run tool on file - -// ------------------------------------------------------------------- anon user registers/logs in -> same history -spaceghost.user.loginOrRegisterUser( email, password ).openHomePage( function(){ - this.test.comment( 'anon-user should login and be associated with previous history' ); - - var loggedInAs = spaceghost.user.loggedInAs(); - this.test.assert( loggedInAs === email, 'loggedInAs() matches email: "' + loggedInAs + '"' ); - - this.historypanel.waitForHdas( function(){ - var hdaInfo = this.historypanel.hdaElementInfoByTitle( filenameToUpload ); - this.test.assert( hdaInfo !== null, "After logging in - found a matching hda by name and hid" ); - if( hdaInfo ){ - this.test.assert( testUploadInfo.hdaElement.attributes.id === hdaInfo.attributes.id, - "After logging in - found a matching hda by hda view id: " + hdaInfo.attributes.id ); - } - }); -}); - -// ------------------------------------------------------------------- logs out -> new history -spaceghost.user.logout().openHomePage( function(){ - this.test.comment( 'logging out should create a new, anonymous history' ); - - this.historypanel.waitForHdas( function(){ + this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); + this.test.assertExists( nameSelector, nameSelector + ' exists' ); + this.test.assertVisible( nameSelector, 'History name is visible' ); this.test.assertSelectorHasText( nameSelector, unnamedName, 'History name is ' + unnamedName ); + + this.test.comment( "history should display size and size should be 0 bytes" ); + this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); + this.test.assertVisible( sizeSelector, 'History size is visible' ); + this.test.assertSelectorHasText( sizeSelector, initialSizeStr, + 'History size has "' + initialSizeStr + '"' ); + + this.test.comment( "NO tags or annotations icons should be available for an anonymous user" ); + this.test.assertDoesntExist( tagIconSelector, 'Tag icon button not found' ); + this.test.assertDoesntExist( annoIconSelector, 'Annotation icon button not found' ); + + this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); + this.test.comment( "A message about the current history being empty should be displayed" ); + this.test.assertVisible( emptyMsgSelector, 'Empty history message is visible' ); this.test.assertSelectorHasText( emptyMsgSelector, emptyMsgStr, 'Message contains "' + emptyMsgStr + '"' ); - }); -}); -// =================================================================== + this.test.comment( 'name should NOT be editable when clicked by anon-user' ); + this.assertDoesntHaveClass( nameSelector, editableTextClass, "Name field is not classed as editable text" ); + this.click( nameSelector ); + this.test.assertDoesntExist( editableTextInput, "Clicking on name does not create an input" ); + }); + + // ------------------------------------------------------------------- anon user can upload file + var uploadedId = null; + spaceghost.api.tools.thenUploadToCurrent({ filepath: filepathToUpload }, function( id, json ){ + uploadedId = id; + }); + spaceghost.openHomePage().historypanel.waitForHdas( function testAnonUpload(){ + this.test.comment( 'anon-user should be able to upload files' ); + this.test.assertExists( '#dataset-' + uploadedId, "found hda" ); + var hdaElement = this.elementInfoOrNull( '#dataset-' + uploadedId ); + this.debug( 'hdaElement: ' + hdaElement ); + if( hdaElement ){ + this.test.assert( hdaElement.attributes[ 'class' ].indexOf( 'state-ok' ) !== -1, + "Uploaded file: " + hdaElement.text ); + } + + this.test.comment( "empty should be NO LONGER be displayed" ); + this.test.assertNotVisible( emptyMsgSelector, 'Empty history message is not visible' ); + }); + + // ------------------------------------------------------------------- anon user can run tool on file + + // ------------------------------------------------------------------- anon user registers/logs in -> same history + spaceghost.user.loginOrRegisterUser( email, password ).openHomePage( function(){ + this.test.comment( 'anon-user should login and be associated with previous history' ); + + var loggedInAs = spaceghost.user.loggedInAs(); + this.test.assert( loggedInAs === email, 'loggedInAs() matches email: "' + loggedInAs + '"' ); + + this.historypanel.waitForHdas( function(){ + var hdaInfo = this.historypanel.hdaElementInfoByTitle( filenameToUpload ); + this.test.assert( hdaInfo !== null, "After logging in - found a matching hda by name and hid" ); + if( hdaInfo ){ + this.test.assert( 'dataset-' + uploadedId === hdaInfo.attributes.id, + "After logging in - found a matching hda by hda view id: " + hdaInfo.attributes.id ); + } + }); + }); + + // ------------------------------------------------------------------- logs out -> new history + spaceghost.user.logout().openHomePage( function(){ + this.test.comment( 'logging out should create a new, anonymous history' ); + + this.historypanel.waitForHdas( function(){ + this.test.assertSelectorHasText( nameSelector, unnamedName, 'History name is ' + unnamedName ); + this.test.assertSelectorHasText( emptyMsgSelector, emptyMsgStr, + 'Message contains "' + emptyMsgStr + '"' ); + }); + }); + + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-anon-history-permission-tests.js b/test/casperjs/api-anon-history-permission-tests.js index f63cdabb650..dba00b98361 100644 --- a/test/casperjs/api-anon-history-permission-tests.js +++ b/test/casperjs/api-anon-history-permission-tests.js @@ -8,195 +8,194 @@ spaceghost.test.begin( 'Test permissions for accessible, published, and inaccess + 'with anonymous users over the API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -var inaccessibleHistory, accessibleHistory, publishedHistory, - inaccessibleHdas, accessibleHdas, publishedHdas, - accessibleLink; + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } + var inaccessibleHistory, accessibleHistory, publishedHistory, + inaccessibleHdas, accessibleHdas, publishedHdas; -//// ------------------------------------------------------------------------------------------- create 3 histories -spaceghost.user.loginOrRegisterUser( email, password ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - // create three histories: make the 2nd importable (via the API), and the third published + //// ------------------------------------------------------------------------------------------- create 3 histories + spaceghost.user.loginOrRegisterUser( email, password ); + spaceghost.openHomePage().then( function(){ + // create three histories: make the 2nd importable (via the API), and the third published - // make the current the inaccessible one - inaccessibleHistory = this.api.histories.index()[0]; - this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); - inaccessibleHistory = this.api.histories.index()[0]; + // make the current the inaccessible one + inaccessibleHistory = this.api.histories.index()[0]; + this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); + inaccessibleHistory = this.api.histories.index()[0]; + + accessibleHistory = this.api.histories.create({ name: 'accessible' }); + var returned = this.api.histories.update( accessibleHistory.id, { + importable : true + }); + //this.debug( this.jsonStr( returned ) ); + accessibleHistory = this.api.histories.show( accessibleHistory.id ); + + publishedHistory = this.api.histories.create({ name: 'published' }); + returned = this.api.histories.update( publishedHistory.id, { + published : true + }); + //this.debug( this.jsonStr( returned ) ); + publishedHistory = this.api.histories.show( publishedHistory.id ); - accessibleHistory = this.api.histories.create({ name: 'accessible' }); - var returned = this.api.histories.update( accessibleHistory.id, { - importable : true }); - //this.debug( this.jsonStr( returned ) ); - accessibleHistory = this.api.histories.show( accessibleHistory.id ); - publishedHistory = this.api.histories.create({ name: 'published' }); - returned = this.api.histories.update( publishedHistory.id, { - published : true + //// ------------------------------------------------------------------------------------------- upload some files + spaceghost.then( function(){ + this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: '../../test-data/1.bed' }); + this.api.tools.thenUpload( accessibleHistory.id, { filepath: '../../test-data/1.bed' }); + this.api.tools.thenUpload( publishedHistory.id, { filepath: '../../test-data/1.bed' }); }); - //this.debug( this.jsonStr( returned ) ); - publishedHistory = this.api.histories.show( publishedHistory.id ); - -}); - -//// ------------------------------------------------------------------------------------------- upload some files -spaceghost.then( function(){ - this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: '../../test-data/1.bed' }); - this.api.tools.thenUpload( accessibleHistory.id, { filepath: '../../test-data/1.bed' }); - this.api.tools.thenUpload( publishedHistory.id, { filepath: '../../test-data/1.bed' }); -}); -spaceghost.then( function(){ - // check that they're there - inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), - accessibleHdas = this.api.hdas.index( accessibleHistory.id ), - publishedHdas = this.api.hdas.index( publishedHistory.id ); -}); -spaceghost.user.logout(); - -// =================================================================== TESTS -//// ------------------------------------------------------------------------------------------- anon user -function testAnonReadFunctionsOnAccessible( history, hdas ){ - this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); - - // read functions for history - this.test.comment( 'show should work for history: ' + history.name ); - this.test.assert( this.api.histories.show( history.id ).id === history.id, - 'show worked' ); - this.test.comment( 'copying should fail for history (multiple histories not allowed): ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.create({ history_id : history.id }); - }, 403, 'API authentication required for this request', 'update authentication required' ); - - // read functions for history contents - this.test.comment( 'index of history contents should work for history: ' + history.name ); - this.test.assert( this.api.hdas.index( history.id ).length === 1, - 'hda index worked' ); - this.test.comment( 'showing of history contents should work for history: ' + history.name ); - this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, - 'hda show worked' ); - - this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' - + ' should work from accessible history: ' + history.name ); - this.api.hdas.create( this.api.histories.index()[0].id, { - source : 'hda', - content : hdas[0].id + spaceghost.then( function(){ + // check that they're there + inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), + accessibleHdas = this.api.hdas.index( accessibleHistory.id ), + publishedHdas = this.api.hdas.index( publishedHistory.id ); }); -} + spaceghost.user.logout(); -function testAnonReadFunctionsOnInaccessible( history, hdas ){ - this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); + // =================================================================== TESTS + //// ------------------------------------------------------------------------------------------- anon user + function testAnonReadFunctionsOnAccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); - // read functions for history - this.test.comment( 'show should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.show( history.id ); - }, 403, 'History is not accessible to the current user', 'show failed with error' ); - this.test.comment( 'copying should fail for history (implicit multiple histories): ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.create({ history_id : history.id }); - }, 403, 'API authentication required for this request', 'copy failed with error' ); + // read functions for history + this.test.comment( 'show should work for history: ' + history.name ); + this.test.assert( this.api.histories.show( history.id ).id === history.id, + 'show worked' ); + this.test.comment( 'copying should fail for history (multiple histories not allowed): ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'API authentication required for this request', 'update authentication required' ); - // read functions for history contents - this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.index( history.id ); - }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); - this.api.assertRaises( function(){ - this.api.hdas.show( history.id, hdas[0].id ); - }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); + // read functions for history contents + this.test.comment( 'index of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.index( history.id ).length === 1, + 'hda index worked' ); + this.test.comment( 'showing of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, + 'hda show worked' ); - this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' - + ' from an inaccessible history should fail for: ' + history.name ); - this.api.assertRaises( function(){ - var returned = this.api.hdas.create( this.api.histories.index()[0].id, { + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' should work from accessible history: ' + history.name ); + this.api.hdas.create( this.api.histories.index()[0].id, { source : 'hda', content : hdas[0].id }); - this.debug( this.jsonStr( returned ) ); - }, 403, 'History is not accessible to the current user', 'hda copy from failed with error' ); + } -} + function testAnonReadFunctionsOnInaccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); -function testAnonWriteFunctions( history, hdas ){ - this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); + // read functions for history + this.test.comment( 'show should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.show( history.id ); + }, 403, 'History is not accessible to the current user', 'show failed with error' ); + this.test.comment( 'copying should fail for history (implicit multiple histories): ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'API authentication required for this request', 'copy failed with error' ); - this.test.comment( 'update should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.update( history.id, { deleted: true }); - }, 403, 'API authentication required for this request', 'update authentication required' ); - this.test.comment( 'delete should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.delete_( history.id ); - }, 403, 'API authentication required for this request', 'delete authentication required' ); + // read functions for history contents + this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.index( history.id ); + }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.show( history.id, hdas[0].id ); + }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); - this.test.comment( 'hda updating should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); - // anon hda update fails w/ this msg if trying to update non-current history hda - }, 403, 'You must be logged in to update this history', 'hda update failed with error' ); - this.test.comment( 'hda deletion should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( history.id, hdas[0].id ); - }, 403, 'API authentication required for this request', 'hda delete failed with error' ); + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' from an inaccessible history should fail for: ' + history.name ); + this.api.assertRaises( function(){ + var returned = this.api.hdas.create( this.api.histories.index()[0].id, { + source : 'hda', + content : hdas[0].id + }); + this.debug( this.jsonStr( returned ) ); + }, 403, 'History is not accessible to the current user', 'hda copy from failed with error' ); - this.test.comment( 'copying hda into history should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.create( history.id, { - source : 'hda', - // should error before it checks the id - content : 'bler' + } + + function testAnonWriteFunctions( history, hdas ){ + this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); + + this.test.comment( 'update should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.update( history.id, { deleted: true }); + }, 403, 'API authentication required for this request', 'update authentication required' ); + this.test.comment( 'delete should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.delete_( history.id ); + }, 403, 'API authentication required for this request', 'delete authentication required' ); + + this.test.comment( 'hda updating should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); + // anon hda update fails w/ this msg if trying to update non-current history hda + }, 403, 'You must be logged in to update this history', 'hda update failed with error' ); + this.test.comment( 'hda deletion should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( history.id, hdas[0].id ); + }, 403, 'API authentication required for this request', 'hda delete failed with error' ); + + this.test.comment( 'copying hda into history should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.create( history.id, { + source : 'hda', + // should error before it checks the id + content : 'bler' + }); + }, 403, 'Must be logged in to manage Galaxy histories', 'hda copy to failed' ); + } + + function testAnonInaccessible( history, hdas ){ + testAnonReadFunctionsOnInaccessible.call( this, history, hdas ); + testAnonWriteFunctions.call( this, history, hdas ); + } + + function testAnonAccessible( history, hdas ){ + testAnonReadFunctionsOnAccessible.call( this, history, hdas ); + testAnonWriteFunctions.call( this, history, hdas ); + } + + spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + testAnonInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); + testAnonAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAnonAccessible.call( spaceghost, publishedHistory, publishedHdas ); + }); + + + // ------------------------------------------------------------------------------------------- user1 revoke perms + spaceghost.user.loginOrRegisterUser( email, password ); + spaceghost.openHomePage().then( function(){ + this.test.comment( 'revoking perms should prevent access' ); + this.api.histories.update( accessibleHistory.id, { + importable : false }); - }, 403, 'Must be logged in to manage Galaxy histories', 'hda copy to failed' ); -} + var returned = this.api.histories.show( accessibleHistory.id ); -function testAnonInaccessible( history, hdas ){ - testAnonReadFunctionsOnInaccessible.call( this, history, hdas ); - testAnonWriteFunctions.call( this, history, hdas ); -} - -function testAnonAccessible( history, hdas ){ - testAnonReadFunctionsOnAccessible.call( this, history, hdas ); - testAnonWriteFunctions.call( this, history, hdas ); -} - -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - testAnonInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); - testAnonAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); - testAnonAccessible.call( spaceghost, publishedHistory, publishedHdas ); -}); - - -// ------------------------------------------------------------------------------------------- user1 revoke perms -spaceghost.user.loginOrRegisterUser( email, password ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - this.test.comment( 'revoking perms should prevent access' ); - this.api.histories.update( accessibleHistory.id, { - importable : false + this.api.histories.update( publishedHistory.id, { + importable : false, + published : false + }); + returned = this.api.histories.show( publishedHistory.id ); }); - var returned = this.api.histories.show( accessibleHistory.id ); + spaceghost.user.logout(); - this.api.histories.update( publishedHistory.id, { - importable : false, - published : false + + // ------------------------------------------------------------------------------------------- anon retry perms + spaceghost.openHomePage().then( function(){ + testAnonInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAnonInaccessible.call( spaceghost, publishedHistory, publishedHdas ); }); - returned = this.api.histories.show( publishedHistory.id ); -}); -spaceghost.user.logout(); -// ------------------------------------------------------------------------------------------- anon retry perms -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - testAnonInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); - testAnonInaccessible.call( spaceghost, publishedHistory, publishedHdas ); -}); - - -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-anon-history-tests.js b/test/casperjs/api-anon-history-tests.js index edd12453210..33ec5de6752 100644 --- a/test/casperjs/api-anon-history-tests.js +++ b/test/casperjs/api-anon-history-tests.js @@ -7,123 +7,116 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test API functions for histories with an anonymous user', 0, function suite( test ){ spaceghost.start(); -// =================================================================== TESTS -spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( spaceghost.historypanel.data.selectors.history.name ); -spaceghost.then( function(){ + // =================================================================== TESTS + spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( spaceghost.historypanel.data.selectors.history.name ); + spaceghost.then( function(){ - // ------------------------------------------------------------------------------------------- anon allowed - this.test.comment( 'index should get a list of histories' ); - var index = this.api.histories.index(); - this.test.assert( utils.isArray( index ), "index returned an array: length " + index.length ); - this.test.assert( index.length === 1, 'Has at least one history' ); + // ------------------------------------------------------------------------------------------- anon allowed + this.test.comment( 'index should get a list of histories' ); + var index = this.api.histories.index(); + this.test.assert( utils.isArray( index ), "index returned an array: length " + index.length ); + this.test.assert( index.length === 1, 'Has at least one history' ); - this.test.comment( 'show should get a history details object' ); - var historyShow = this.api.histories.show( index[0].id ); - //this.debug( this.jsonStr( historyShow ) ); - this.test.assert( historyShow.id === index[0].id, 'Is the first history' ); - this.test.assert( this.hasKeys( historyShow, [ 'id', 'name', 'user_id' ] ) ); + this.test.comment( 'show should get a history details object' ); + var historyShow = this.api.histories.show( index[0].id ); + //this.debug( this.jsonStr( historyShow ) ); + this.test.assert( historyShow.id === index[0].id, 'Is the first history' ); + this.test.assert( this.hasKeys( historyShow, [ 'id', 'name', 'user_id' ] ) ); - // ------------------------------------------------------------------------------------------- anon forbidden - //TODO: why not return the current history? - this.test.comment( 'calling show with "most_recently_used" should return None for an anon user' ); - var recent = this.api.histories.show( 'most_recently_used' ); - this.test.assert( recent === null, 'most_recently_used returned None' ); + // ------------------------------------------------------------------------------------------- anon forbidden + //TODO: why not return the current history? + this.test.comment( 'calling show with "most_recently_used" should return None for an anon user' ); + var recent = this.api.histories.show( 'most_recently_used' ); + this.test.assert( recent === null, 'most_recently_used returned None' ); - this.test.comment( 'Calling create should fail for an anonymous user' ); - this.api.assertRaises( function(){ - this.api.histories.create({ name: 'new' }); - }, 403, 'API authentication required for this request', 'create failed with error' ); + this.test.comment( 'Calling create should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.create({ name: 'new' }); + }, 403, 'API authentication required for this request', 'create failed with error' ); - this.test.comment( 'Calling delete should fail for an anonymous user' ); - this.api.assertRaises( function(){ - this.api.histories.delete_( historyShow.id ); - }, 403, 'API authentication required for this request', 'create failed with error' ); + this.test.comment( 'Calling delete should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.delete_( historyShow.id ); + }, 403, 'API authentication required for this request', 'create failed with error' ); - this.test.comment( 'Calling update should fail for an anonymous user' ); - this.api.assertRaises( function(){ - this.api.histories.update( historyShow.id, {} ); - }, 403, 'API authentication required for this request', 'update failed with error' ); + this.test.comment( 'Calling update should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.histories.update( historyShow.id, {} ); + }, 403, 'API authentication required for this request', 'update failed with error' ); - //TODO: need these two in api.js - //this.test.comment( 'Calling archive_import should fail for an anonymous user' ); - //this.api.assertRaises( function(){ - // this.api.histories.archive_import( historyShow.id, {} ); - //}, 403, 'API authentication required for this request', 'archive_import failed with error' ); + //TODO: need these two in api.js + //this.test.comment( 'Calling archive_import should fail for an anonymous user' ); + //this.api.assertRaises( function(){ + // this.api.histories.archive_import( historyShow.id, {} ); + //}, 403, 'API authentication required for this request', 'archive_import failed with error' ); - //this.test.comment( 'Calling archive_download should fail for an anonymous user' ); - //this.api.assertRaises( function(){ - // this.api.histories.archive_download( historyShow.id, {} ); - //}, 403, 'API authentication required for this request', 'archive_download failed with error' ); + //this.test.comment( 'Calling archive_download should fail for an anonymous user' ); + //this.api.assertRaises( function(){ + // this.api.histories.archive_download( historyShow.id, {} ); + //}, 403, 'API authentication required for this request', 'archive_download failed with error' ); - // test server bad id protection - spaceghost.test.comment( 'A bad id should throw an error' ); - this.api.assertRaises( function(){ - this.api.histories.show( '1234123412341234' ); - }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); + // test server bad id protection + spaceghost.test.comment( 'A bad id should throw an error' ); + this.api.assertRaises( function(){ + this.api.histories.show( '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); -}); - -// ------------------------------------------------------------------------------------------- hdas -spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( spaceghost.historypanel.data.selectors.history.name ); -spaceghost.then( function(){ - spaceghost.tools.uploadFile( '../../test-data/1.sam', function( uploadInfo ){ - this.test.assert( uploadInfo.hdaElement !== null, "Convenience function produced hda" ); - var state = this.historypanel.getHdaState( '#' + uploadInfo.hdaElement.attributes.id ); - this.test.assert( state === 'ok', "Convenience function produced hda in ok state" ); }); -}); -spaceghost.then( function(){ - var current = this.api.histories.index()[0]; + // ------------------------------------------------------------------------------------------- hdas + spaceghost.thenOpen( spaceghost.baseUrl ).waitForSelector( spaceghost.historypanel.data.selectors.history.name ); + spaceghost.api.tools.thenUploadToCurrent({ filepath: '../../test-data/1.sam' }); + spaceghost.then( function(){ + var current = this.api.histories.index()[0]; - // ------------------------------------------------------------------------------------------- anon allowed - this.test.comment( 'anonymous users can index hdas in their current history' ); - var hdaIndex = this.api.hdas.index( current.id ); - this.test.assert( hdaIndex.length === 1, 'indexed hdas' ); + // ------------------------------------------------------------------------------------------- anon allowed + this.test.comment( 'anonymous users can index hdas in their current history' ); + var hdaIndex = this.api.hdas.index( current.id ); + this.test.assert( hdaIndex.length === 1, 'indexed hdas' ); - this.test.comment( 'anonymous users can show hdas in their current history' ); - var hda = this.api.hdas.show( current.id, hdaIndex[0].id ); - this.test.assert( this.hasKeys( hda, [ 'id', 'name' ] ), 'showed hda: ' + hda.name ); + this.test.comment( 'anonymous users can show hdas in their current history' ); + var hda = this.api.hdas.show( current.id, hdaIndex[0].id ); + this.test.assert( this.hasKeys( hda, [ 'id', 'name' ] ), 'showed hda: ' + hda.name ); - this.test.comment( 'anonymous users can hide hdas in their current history' ); - var changed = this.api.hdas.update( current.id, hda.id, { visible: false }); - hda = this.api.hdas.show( current.id, hda.id ); - this.test.assert( hda.visible === false, 'successfully hidden' ); + this.test.comment( 'anonymous users can hide hdas in their current history' ); + var changed = this.api.hdas.update( current.id, hda.id, { visible: false }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( hda.visible === false, 'successfully hidden' ); - this.test.comment( 'anonymous users can mark their hdas as deleted in their current history' ); - changed = this.api.hdas.update( current.id, hda.id, { deleted: true }); - hda = this.api.hdas.show( current.id, hda.id ); - this.test.assert( hda.deleted, 'successfully deleted' ); + this.test.comment( 'anonymous users can mark their hdas as deleted in their current history' ); + changed = this.api.hdas.update( current.id, hda.id, { deleted: true }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( hda.deleted, 'successfully deleted' ); - // ------------------------------------------------------------------------------------------- anon forbidden - this.test.comment( 'Creating an hda should work for an anonymous user' ); - var returned = this.api.hdas.create( current.id, { source: 'hda', content: hda.id }); - //this.debug( this.jsonStr( returned ) ); - this.test.assert( returned.name === hda.name, 'name matches: ' + returned.name ); - this.test.assert( returned.id !== hda.id, 'new id: ' + returned.id ); + // ------------------------------------------------------------------------------------------- anon forbidden + this.test.comment( 'Creating an hda should work for an anonymous user' ); + var returned = this.api.hdas.create( current.id, { source: 'hda', content: hda.id }); + //this.debug( this.jsonStr( returned ) ); + this.test.assert( returned.name === hda.name, 'name matches: ' + returned.name ); + this.test.assert( returned.id !== hda.id, 'new id: ' + returned.id ); - //TODO: should be allowed - this.test.comment( 'Calling hda delete should fail for an anonymous user' ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( current.id, hda.id ); - }, 403, 'API authentication required for this request', 'delete failed with error' ); + //TODO: should be allowed + this.test.comment( 'Calling hda delete should fail for an anonymous user' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( current.id, hda.id ); + }, 403, 'API authentication required for this request', 'delete failed with error' ); - //TODO: only sharing, tags, annotations should be blocked/prevented - this.test.comment( 'Calling update with keys other than "visible" or "deleted" should fail silently' ); - this.test.comment( 'Calling update on tags should fail silently' ); - changed = this.api.hdas.update( current.id, hda.id, { tags: [ 'one' ] }); - hda = this.api.hdas.show( current.id, hda.id ); - this.test.assert( hda.tags.length === 0, 'tags were not set: ' + this.jsonStr( hda.tags ) ); + //TODO: only sharing, tags, annotations should be blocked/prevented + this.test.comment( 'Calling update with keys other than "visible" or "deleted" should fail silently' ); + this.test.comment( 'Calling update on tags should fail silently' ); + changed = this.api.hdas.update( current.id, hda.id, { tags: [ 'one' ] }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( hda.tags.length === 0, 'tags were not set: ' + this.jsonStr( hda.tags ) ); - this.test.comment( 'Calling update on annotation should fail silently' ); - changed = this.api.hdas.update( current.id, hda.id, { annotation: 'yup yup yup' }); - hda = this.api.hdas.show( current.id, hda.id ); - this.test.assert( !hda.annotation, 'annotation was not set: ' + hda.annotation ); + this.test.comment( 'Calling update on annotation should fail silently' ); + changed = this.api.hdas.update( current.id, hda.id, { annotation: 'yup yup yup' }); + hda = this.api.hdas.show( current.id, hda.id ); + this.test.assert( !hda.annotation, 'annotation was not set: ' + hda.annotation ); -}); + }); -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-configuration-tests.js b/test/casperjs/api-configuration-tests.js index eb039382972..faeb301c92a 100644 --- a/test/casperjs/api-configuration-tests.js +++ b/test/casperjs/api-configuration-tests.js @@ -7,57 +7,55 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test the Galaxy configuration API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -spaceghost.user.loginOrRegisterUser( email, password ); + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } + spaceghost.user.loginOrRegisterUser( email, password ); -// =================================================================== TESTS -var normKeys = [ - 'enable_unique_workflow_defaults', - 'ftp_upload_site', - 'ftp_upload_dir', - 'wiki_url', - 'support_url', - 'logo_url', - 'terms_url', - 'allow_user_dataset_purge' - ], - adminKeys = normKeys.concat([ - 'library_import_dir', - 'user_library_import_dir', - 'allow_library_path_paste', - 'allow_user_creation', - 'allow_user_deletion' - ]); + // =================================================================== TESTS + var normKeys = [ + 'enable_unique_workflow_defaults', + 'ftp_upload_site', + 'ftp_upload_dir', + 'wiki_url', + 'support_url', + 'logo_url', + 'terms_url', + 'allow_user_dataset_purge' + ], + adminKeys = normKeys.concat([ + 'library_import_dir', + 'user_library_import_dir', + 'allow_library_path_paste', + 'allow_user_creation', + 'allow_user_deletion' + ]); + // ------------------------------------------------------------------------------------------- INDEX + spaceghost.openHomePage().then( function(){ + this.test.comment( 'index should get a (shortened) list of configuration settings ' + + 'when requested by a normal user' ); -// ------------------------------------------------------------------------------------------- INDEX -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + var configIndex = this.api.configuration.index(); + this.debug( this.jsonStr( configIndex ) ); + this.test.assert( utils.isObject( configIndex ), "index returned an object" ); + this.test.assert( this.hasKeys( configIndex, normKeys ), 'Has the proper keys' ); - this.test.comment( 'index should get a (shortened) list of configuration settings ' - + 'when requested by a normal user' ); - var configIndex = this.api.configuration.index(); - this.debug( this.jsonStr( configIndex ) ); - this.test.assert( utils.isObject( configIndex ), "index returned an object" ); - this.test.assert( this.hasKeys( configIndex, normKeys ), 'Has the proper keys' ); + }); + spaceghost.user.logout(); -}); -spaceghost.user.logout(); + // ------------------------------------------------------------------------------------------- INDEX (admin) + spaceghost.tryStepsCatch( function tryAdminLogin(){ + spaceghost.user.loginAdmin(); + }, function(){} ); -// ------------------------------------------------------------------------------------------- INDEX (admin) -spaceghost.tryStepsCatch( function tryAdminLogin(){ - spaceghost.user.loginAdmin(); -}, function(){} ); - -//}, function failedLoginRegister(){ -// this.info( 'Admin level configuration API tests not run: no admin account available' ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - spaceghost.waitForMasthead( function() { + //}, function failedLoginRegister(){ + // this.info( 'Admin level configuration API tests not run: no admin account available' ); + spaceghost.openHomePage().waitForMasthead( function(){ if( spaceghost.user.userIsAdmin() ){ this.test.comment( 'index should get a (full) list of configuration settings ' + 'when requested by an admin user' ); @@ -70,9 +68,8 @@ spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ this.info( 'Admin level configuration API tests not run: no admin account available' ); } }); -}); -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-hda-tests.js b/test/casperjs/api-hda-tests.js index dd72dee3a49..155e0cef705 100644 --- a/test/casperjs/api-hda-tests.js +++ b/test/casperjs/api-hda-tests.js @@ -7,363 +7,359 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test the HDA API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -spaceghost.user.loginOrRegisterUser( email, password ); + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } + spaceghost.user.loginOrRegisterUser( email, password ); -spaceghost.thenOpen( spaceghost.baseUrl, function(){ - this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); - this.api.tools.thenUpload( spaceghost.api.histories.index()[0].id, { + spaceghost.openHomePage().api.tools.thenUploadToCurrent({ filepath: '../../test-data/1.sam' }); -}); -// =================================================================== TESTS -var summaryKeys = [ 'id', 'name', 'history_id', 'state', 'deleted', 'purged', 'visible', 'url', 'type' ], - detailKeys = [ - // the following are always present regardless of datatype - 'id', 'name', 'api_type', 'model_class', - 'history_id', 'hid', - 'accessible', 'deleted', 'visible', 'purged', - 'state', 'data_type', 'file_ext', 'file_size', - 'misc_info', 'misc_blurb', - 'download_url', 'visualizations', 'display_apps', 'display_types', - 'genome_build', - // the following are NOT always present DEPENDING ON datatype - 'metadata_dbkey', - 'metadata_column_names', 'metadata_column_types', 'metadata_columns', - 'metadata_comment_lines', 'metadata_data_lines' - ]; + // =================================================================== TESTS + var summaryKeys = [ 'id', 'name', 'history_id', 'state', 'deleted', 'purged', 'visible', 'url', 'type' ], + detailKeys = [ + // the following are always present regardless of datatype + 'id', 'name', 'api_type', 'model_class', + 'history_id', 'hid', + 'accessible', 'deleted', 'visible', 'purged', + 'state', 'data_type', 'file_ext', 'file_size', + 'misc_info', 'misc_blurb', + 'download_url', 'visualizations', 'display_apps', 'display_types', + 'genome_build', + // the following are NOT always present DEPENDING ON datatype + 'metadata_dbkey', + 'metadata_column_names', 'metadata_column_types', 'metadata_columns', + 'metadata_comment_lines', 'metadata_data_lines' + ]; -// ------------------------------------------------------------------------------------------- logged in user -spaceghost.then( function(){ - - // ------------------------------------------------------------------------------------------- INDEX - this.test.comment( 'index should return a list of summary data for each hda' ); - var histories = this.api.histories.index(), - lastHistory = histories[0], - hdaIndex = this.api.hdas.index( lastHistory.id ); - //this.debug( 'hdaIndex:' + this.jsonStr( hdaIndex ) ); + // ------------------------------------------------------------------------------------------- logged in user + spaceghost.then( function(){ + // ------------------------------------------------------------------------------------------- INDEX + this.test.comment( 'index should return a list of summary data for each hda' ); + var histories = this.api.histories.index(), + lastHistory = histories[0], + hdaIndex = this.api.hdas.index( lastHistory.id ); + //this.debug( 'hdaIndex:' + this.jsonStr( hdaIndex ) ); - this.test.assert( utils.isArray( hdaIndex ), "index returned an array: length " + hdaIndex.length ); - this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); + this.test.assert( utils.isArray( hdaIndex ), "index returned an array: length " + hdaIndex.length ); + this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); - var firstHda = hdaIndex[0]; - this.test.assert( this.hasKeys( firstHda, summaryKeys ), 'Has the proper keys' ); + var firstHda = hdaIndex[0]; + this.test.assert( this.hasKeys( firstHda, summaryKeys ), 'Has the proper keys' ); - this.test.assert( this.api.isEncodedId( firstHda.id ), 'Id appears well-formed: ' + firstHda.id ); - this.test.assert( firstHda.name === '1.sam', 'Title matches: ' + firstHda.name ); + this.test.assert( this.api.isEncodedId( firstHda.id ), 'Id appears well-formed: ' + firstHda.id ); + this.test.assert( firstHda.name === '1.sam', 'Title matches: ' + firstHda.name ); - // ------------------------------------------------------------------------------------------- SHOW - this.test.comment( 'show should get an HDA details object' ); - var hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - //this.debug( this.jsonStr( hdaShow ) ); - this.test.assert( this.hasKeys( hdaShow, detailKeys ), 'Has the proper keys' ); + // ------------------------------------------------------------------------------------------- SHOW + this.test.comment( 'show should get an HDA details object' ); + var hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + //this.debug( this.jsonStr( hdaShow ) ); + this.test.assert( this.hasKeys( hdaShow, detailKeys ), 'Has the proper keys' ); - //TODO: validate data in each hdaShow attribute... + //TODO: validate data in each hdaShow attribute... - // ------------------------------------------------------------------------------------------- INDEX (detailed) - this.test.comment( 'index should return a list of detailed data for each hda in "ids" when passed' ); - hdaIndex = this.api.hdas.index( lastHistory.id, [ firstHda.id ] ); - this.debug( 'hdaIndex:' + this.jsonStr( hdaIndex ) ); + // ------------------------------------------------------------------------------------------- INDEX (detailed) + this.test.comment( 'index should return a list of detailed data for each hda in "ids" when passed' ); + hdaIndex = this.api.hdas.index( lastHistory.id, [ firstHda.id ] ); + this.debug( 'hdaIndex:' + this.jsonStr( hdaIndex ) ); - this.test.assert( utils.isArray( hdaIndex ), "index returned an array: length " + hdaIndex.length ); - this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); + this.test.assert( utils.isArray( hdaIndex ), "index returned an array: length " + hdaIndex.length ); + this.test.assert( hdaIndex.length >= 1, 'Has at least one hda' ); - firstHda = hdaIndex[0]; - this.test.assert( this.hasKeys( firstHda, detailKeys ), 'Has the proper keys' ); + firstHda = hdaIndex[0]; + this.test.assert( this.hasKeys( firstHda, detailKeys ), 'Has the proper keys' ); - //TODO??: validate data in firstHda attribute? we ASSUME it's from a common method as show... + //TODO??: validate data in firstHda attribute? we ASSUME it's from a common method as show... - // ------------------------------------------------------------------------------------------- CREATE - //TODO: create from_ld_id - this.test.comment( 'create should allow copying an accessible hda' ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - var returned = this.api.hdas.create( lastHistory.id, { - source : 'hda', - content : hdaShow.id - }); - //this.debug( 'returned:' + this.jsonStr( returned ) ); - this.test.assert( this.hasKeys( returned, detailKeys ), 'Has the proper keys' ); - this.test.assert( typeof returned.id !== 'number' && isNaN( Number( returned.id ) ), - 'id seems to be encoded: ' + returned.id ); - this.test.assert( typeof returned.history_id !== 'number' && isNaN( Number( returned.history_id ) ), - 'history_id seems to be encoded: ' + returned.history_id ); - - - // ------------------------------------------------------------------------------------------- UPDATE - // ........................................................................................... idiot proofing - this.test.comment( 'updating to the current value should return no value (no change)' ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - var returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - name : hdaShow.name - }); - this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); - - this.test.comment( 'updating using a nonsense key should NOT fail with an error' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - konamiCode : 'uuddlrlrba' - }); - this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); - - this.test.comment( 'updating by attempting to change type should cause an error' ); - this.api.assertRaises( function(){ - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - //name : false - deleted : 'sure why not' + // ------------------------------------------------------------------------------------------- CREATE + //TODO: create from_ld_id + this.test.comment( 'create should allow copying an accessible hda' ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + var returned = this.api.hdas.create( lastHistory.id, { + source : 'hda', + content : hdaShow.id }); - }, 400, 'deleted must be a boolean', 'changing deleted type failed' ); + //this.debug( 'returned:' + this.jsonStr( returned ) ); + this.test.assert( this.hasKeys( returned, detailKeys ), 'Has the proper keys' ); + this.test.assert( typeof returned.id !== 'number' && isNaN( Number( returned.id ) ), + 'id seems to be encoded: ' + returned.id ); + this.test.assert( typeof returned.history_id !== 'number' && isNaN( Number( returned.history_id ) ), + 'history_id seems to be encoded: ' + returned.history_id ); - // ........................................................................................... name - this.test.comment( 'update should allow changing the name' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - name : 'New name' + + // ------------------------------------------------------------------------------------------- UPDATE + // ........................................................................................... idiot proofing + this.test.comment( 'updating to the current value should return no value (no change)' ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + var returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + name : hdaShow.name + }); + this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); + + this.test.comment( 'updating using a nonsense key should NOT fail with an error' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + konamiCode : 'uuddlrlrba' + }); + this.test.assert( this.countKeys( returned ) === 0, "No changed returned: " + this.jsonStr( returned ) ); + + this.test.comment( 'updating by attempting to change type should cause an error' ); + this.api.assertRaises( function(){ + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + //name : false + deleted : 'sure why not' + }); + }, 400, 'deleted must be a boolean', 'changing deleted type failed' ); + + // ........................................................................................... name + this.test.comment( 'update should allow changing the name' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + name : 'New name' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.name === 'New name', "Name successfully set via update: " + hdaShow.name ); + + this.test.comment( 'update should sanitize any new name' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + name : 'New name' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.name === 'New name', "Update sanitized name: " + hdaShow.name ); + + this.test.comment( 'update should allow unicode in names' ); + var unicodeName = 'Ржевский сапоги'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + name : unicodeName + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.name === unicodeName, "Update accepted unicode name: " + hdaShow.name ); + + this.test.comment( 'update should allow escaped quotations in names' ); + var quotedName = '"Bler"'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + name : quotedName + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.name === quotedName, + "Update accepted escaped quotations in name: " + hdaShow.name ); + + + // ........................................................................................... deleted + this.test.comment( 'update should allow changing the deleted flag' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + deleted: true + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id, true ); + this.test.assert( hdaShow.deleted === true, "Update set the deleted flag: " + hdaShow.deleted ); + + this.test.comment( 'update should allow changing the deleted flag back' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + deleted: false + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.deleted === false, "Update set the deleted flag: " + hdaShow.deleted ); + + + // ........................................................................................... visible/hidden + this.test.comment( 'update should allow changing the visible flag' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + visible: false + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.visible === false, "Update set the visible flag: " + hdaShow.visible ); + + + // ........................................................................................... genome/dbkey + this.test.comment( 'update should allow changing the genome_build' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + genome_build : 'hg18' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + //this.debug( 'hdaShow:\n' + this.jsonStr( hdaShow ) ); + this.test.assert( hdaShow.genome_build === 'hg18', + "genome_build successfully set via update: " + hdaShow.genome_build ); + this.test.assert( hdaShow.metadata_dbkey === 'hg18', + "metadata_dbkey successfully set via the same update: " + hdaShow.metadata_dbkey ); + this.test.comment( 'update should sanitize any genome_build' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + genome_build : 'hg18' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.genome_build === 'hg18', + "Update sanitized genome_build: " + hdaShow.genome_build ); + this.test.assert( hdaShow.metadata_dbkey === 'hg18', + "metadata_dbkey successfully set via the same update: " + hdaShow.metadata_dbkey ); + + this.test.comment( 'update should allow unicode in genome builds' ); + var unicodeBuild = 'Ржевский18'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + genome_build : unicodeBuild + }); + this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.debug( 'hdaShow:\n' + this.jsonStr( hdaShow ) ); + this.test.assert( hdaShow.genome_build === unicodeBuild, + "Update accepted unicode genome_build: " + hdaShow.genome_build ); + + // ........................................................................................... misc_info/info + this.test.comment( 'update should allow changing the misc_info' ); + var newInfo = 'I\'ve made a huge mistake.'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + misc_info : newInfo + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.misc_info === newInfo, + "misc_info successfully set via update: " + hdaShow.misc_info ); + + this.test.comment( 'update should sanitize any misc_info' ); + var newInfo = 'You\'re going to get hop-ons.'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + misc_info : newInfo + '' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.misc_info === newInfo, + "Update sanitized misc_info: " + hdaShow.misc_info ); + + this.test.comment( 'update should allow unicode in misc_info' ); + var unicodeInfo = '여보!'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + misc_info : unicodeInfo + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.misc_info === unicodeInfo, + "Update accepted unicode misc_info: " + hdaShow.misc_info ); + + // ........................................................................................... annotation + // currently fails because no annotation is returned in details + this.test.comment( 'update should allow changing the annotation' ); + var newAnnotation = 'Found this sample on a movie theatre floor'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + annotation : newAnnotation + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.annotation === newAnnotation, + "Annotation successfully set via update: " + hdaShow.annotation ); + + this.test.comment( 'update should sanitize any new annotation' ); + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + annotation : 'New annotation' + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.annotation === 'New annotation', + "Update sanitized annotation: " + hdaShow.annotation ); + + this.test.comment( 'update should allow unicode in annotations' ); + var unicodeAnnotation = 'お願いは、それが落下させない'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + annotation : unicodeAnnotation + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.annotation === unicodeAnnotation, + "Update accepted unicode annotation: " + hdaShow.annotation ); + + this.test.comment( 'update should allow escaped quotations in annotations' ); + var quotedAnnotation = '"Bler"'; + returned = this.api.hdas.update( lastHistory.id, firstHda.id, { + annotation : quotedAnnotation + }); + //this.debug( 'returned:\n' + this.jsonStr( returned ) ); + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.annotation === quotedAnnotation, + "Update accepted escaped quotations in annotation: " + hdaShow.annotation ); + + // ------------------------------------------------------------------------------------------- ERRORS + this.test.comment( 'create should error with "Please define the source" when the param "from_ld_id" is not used' ); + this.api.assertRaises( function(){ + this.api.hdas.create( lastHistory.id, { bler: 'bler' } ); + }, 400, "must be either 'library' or 'hda'", 'create with no source failed' ); + + this.test.comment( 'updating using a nonsense key should fail silently' ); + returned = this.api.hdas.update( lastHistory.id, hdaShow.id, { + konamiCode : 'uuddlrlrba' + }); + this.test.assert( returned.konamiCode === undefined, 'key was not set: ' + returned.konamiCode ); + + spaceghost.test.comment( 'A bad id should throw an error when using show' ); + this.api.assertRaises( function(){ + this.api.hdas.show( lastHistory.id, '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); + spaceghost.test.comment( 'A bad id should throw an error when using update' ); + this.api.assertRaises( function(){ + this.api.hdas.update( lastHistory.id, '1234123412341234', {} ); + }, 400, 'unable to decode', 'Bad Request with invalid id: update' ); + spaceghost.test.comment( 'A bad id should throw an error when using delete' ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( lastHistory.id, '1234123412341234' ); + }, 400, 'unable to decode', 'Bad Request with invalid id: delete' ); + spaceghost.test.comment( 'A bad id should throw an error when using undelete' ); + + this.test.comment( 'updating by attempting to change type should cause an error' ); + [ 'name', 'annotation', 'genome_build', 'misc_info' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = false; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); + }, 400, key + ' must be a string or unicode', 'type validation error' ); + }); + [ 'deleted', 'visible' ].forEach( function( key ){ + var updatedAttrs = {}; + updatedAttrs[ key ] = 'straaang'; + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); + }, 400, key + ' must be a boolean', 'type validation error' ); + }); + [ 'you\'re it', [ true ] ].forEach( function( badVal ){ + spaceghost.api.assertRaises( function(){ + returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, { tags: badVal }); + }, 400, 'tags must be a list', 'type validation error' ); + }); + + // ------------------------------------------------------------------------------------------- DELETE + this.test.comment( 'calling delete on an hda should mark it as deleted but not change the history size' ); + lastHistory = this.api.histories.show( lastHistory.id ); + var sizeBeforeDelete = lastHistory.nice_size; + + returned = this.api.hdas.delete_( lastHistory.id, firstHda.id ); + //this.debug( this.jsonStr( returned ) ); + + hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); + this.test.assert( hdaShow.deleted, 'hda is marked deleted' ); + lastHistory = this.api.histories.show( lastHistory.id ); + this.test.assert( lastHistory.nice_size === sizeBeforeDelete, 'history size has not changed' ); + + // by default, purging fails bc uni.ini:allow_user_dataset_purge=False + this.api.assertRaises( function(){ + returned = this.api.hdas.delete_( lastHistory.id, firstHda.id, { purge : true }); + }, 403, 'This instance does not allow user dataset purging', 'Purge failed' ); + /* + */ }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.name === 'New name', "Name successfully set via update: " + hdaShow.name ); - - this.test.comment( 'update should sanitize any new name' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - name : 'New name' - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.name === 'New name', "Update sanitized name: " + hdaShow.name ); - - this.test.comment( 'update should allow unicode in names' ); - var unicodeName = 'Ржевский сапоги'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - name : unicodeName - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.name === unicodeName, "Update accepted unicode name: " + hdaShow.name ); - - this.test.comment( 'update should allow escaped quotations in names' ); - var quotedName = '"Bler"'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - name : quotedName - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.name === quotedName, - "Update accepted escaped quotations in name: " + hdaShow.name ); + //spaceghost.user.logout(); - // ........................................................................................... deleted - this.test.comment( 'update should allow changing the deleted flag' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - deleted: true - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id, true ); - this.test.assert( hdaShow.deleted === true, "Update set the deleted flag: " + hdaShow.deleted ); - - this.test.comment( 'update should allow changing the deleted flag back' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - deleted: false - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.deleted === false, "Update set the deleted flag: " + hdaShow.deleted ); - - - // ........................................................................................... visible/hidden - this.test.comment( 'update should allow changing the visible flag' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - visible: false - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.visible === false, "Update set the visible flag: " + hdaShow.visible ); - - - // ........................................................................................... genome_build/dbkey - this.test.comment( 'update should allow changing the genome_build' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - genome_build : 'hg18' - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - //this.debug( 'hdaShow:\n' + this.jsonStr( hdaShow ) ); - this.test.assert( hdaShow.genome_build === 'hg18', - "genome_build successfully set via update: " + hdaShow.genome_build ); - this.test.assert( hdaShow.metadata_dbkey === 'hg18', - "metadata_dbkey successfully set via the same update: " + hdaShow.metadata_dbkey ); - this.test.comment( 'update should sanitize any genome_build' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - genome_build : 'hg18' - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.genome_build === 'hg18', - "Update sanitized genome_build: " + hdaShow.genome_build ); - this.test.assert( hdaShow.metadata_dbkey === 'hg18', - "metadata_dbkey successfully set via the same update: " + hdaShow.metadata_dbkey ); - - this.test.comment( 'update should allow unicode in genome builds' ); - var unicodeBuild = 'Ржевский18'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - genome_build : unicodeBuild - }); - this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.debug( 'hdaShow:\n' + this.jsonStr( hdaShow ) ); - this.test.assert( hdaShow.genome_build === unicodeBuild, - "Update accepted unicode genome_build: " + hdaShow.genome_build ); - - // ........................................................................................... misc_info/info - this.test.comment( 'update should allow changing the misc_info' ); - var newInfo = 'I\'ve made a huge mistake.'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - misc_info : newInfo - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.misc_info === newInfo, - "misc_info successfully set via update: " + hdaShow.misc_info ); - - this.test.comment( 'update should sanitize any misc_info' ); - var newInfo = 'You\'re going to get hop-ons.'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - misc_info : newInfo + '' - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.misc_info === newInfo, - "Update sanitized misc_info: " + hdaShow.misc_info ); - - this.test.comment( 'update should allow unicode in misc_info' ); - var unicodeInfo = '여보!'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - misc_info : unicodeInfo - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.misc_info === unicodeInfo, - "Update accepted unicode misc_info: " + hdaShow.misc_info ); - - // ........................................................................................... annotation - // currently fails because no annotation is returned in details - this.test.comment( 'update should allow changing the annotation' ); - var newAnnotation = 'Found this sample on a movie theatre floor'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - annotation : newAnnotation - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.annotation === newAnnotation, - "Annotation successfully set via update: " + hdaShow.annotation ); - - this.test.comment( 'update should sanitize any new annotation' ); - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - annotation : 'New annotation' - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.annotation === 'New annotation', - "Update sanitized annotation: " + hdaShow.annotation ); - - this.test.comment( 'update should allow unicode in annotations' ); - var unicodeAnnotation = 'お願いは、それが落下させない'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - annotation : unicodeAnnotation - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.annotation === unicodeAnnotation, - "Update accepted unicode annotation: " + hdaShow.annotation ); - - this.test.comment( 'update should allow escaped quotations in annotations' ); - var quotedAnnotation = '"Bler"'; - returned = this.api.hdas.update( lastHistory.id, firstHda.id, { - annotation : quotedAnnotation - }); - //this.debug( 'returned:\n' + this.jsonStr( returned ) ); - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.annotation === quotedAnnotation, - "Update accepted escaped quotations in annotation: " + hdaShow.annotation ); - - // ------------------------------------------------------------------------------------------- ERRORS - this.test.comment( 'create should error with "Please define the source" when the param "from_ld_id" is not used' ); - this.api.assertRaises( function(){ - this.api.hdas.create( lastHistory.id, { bler: 'bler' } ); - }, 400, "must be either 'library' or 'hda'", 'create with no source failed' ); - - this.test.comment( 'updating using a nonsense key should fail silently' ); - returned = this.api.hdas.update( lastHistory.id, hdaShow.id, { - konamiCode : 'uuddlrlrba' - }); - this.test.assert( returned.konamiCode === undefined, 'key was not set: ' + returned.konamiCode ); - - spaceghost.test.comment( 'A bad id should throw an error when using show' ); - this.api.assertRaises( function(){ - this.api.hdas.show( lastHistory.id, '1234123412341234' ); - }, 400, 'unable to decode', 'Bad Request with invalid id: show' ); - spaceghost.test.comment( 'A bad id should throw an error when using update' ); - this.api.assertRaises( function(){ - this.api.hdas.update( lastHistory.id, '1234123412341234', {} ); - }, 400, 'unable to decode', 'Bad Request with invalid id: update' ); - spaceghost.test.comment( 'A bad id should throw an error when using delete' ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( lastHistory.id, '1234123412341234' ); - }, 400, 'unable to decode', 'Bad Request with invalid id: delete' ); - spaceghost.test.comment( 'A bad id should throw an error when using undelete' ); - - this.test.comment( 'updating by attempting to change type should cause an error' ); - [ 'name', 'annotation', 'genome_build', 'misc_info' ].forEach( function( key ){ - var updatedAttrs = {}; - updatedAttrs[ key ] = false; - spaceghost.api.assertRaises( function(){ - returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); - }, 400, key + ' must be a string or unicode', 'type validation error' ); - }); - [ 'deleted', 'visible' ].forEach( function( key ){ - var updatedAttrs = {}; - updatedAttrs[ key ] = 'straaang'; - spaceghost.api.assertRaises( function(){ - returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, updatedAttrs ); - }, 400, key + ' must be a boolean', 'type validation error' ); - }); - [ 'you\'re it', [ true ] ].forEach( function( badVal ){ - spaceghost.api.assertRaises( function(){ - returned = spaceghost.api.hdas.update( hdaShow.history_id, hdaShow.id, { tags: badVal }); - }, 400, 'tags must be a list', 'type validation error' ); - }); - - // ------------------------------------------------------------------------------------------- DELETE - this.test.comment( 'calling delete on an hda should mark it as deleted but not change the history size' ); - lastHistory = this.api.histories.show( lastHistory.id ); - var sizeBeforeDelete = lastHistory.nice_size; - - returned = this.api.hdas.delete_( lastHistory.id, firstHda.id ); - //this.debug( this.jsonStr( returned ) ); - - hdaShow = this.api.hdas.show( lastHistory.id, firstHda.id ); - this.test.assert( hdaShow.deleted, 'hda is marked deleted' ); - lastHistory = this.api.histories.show( lastHistory.id ); - this.test.assert( lastHistory.nice_size === sizeBeforeDelete, 'history size has not changed' ); - - // by default, purging fails bc uni.ini:allow_user_dataset_purge=False - this.api.assertRaises( function(){ - returned = this.api.hdas.delete_( lastHistory.id, firstHda.id, { purge : true }); - }, 403, 'This instance does not allow user dataset purging', 'Purge failed' ); -/* -*/ -}); -//spaceghost.user.logout(); - - -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-history-permission-tests.js b/test/casperjs/api-history-permission-tests.js index c85f934839e..a9eebf07b96 100644 --- a/test/casperjs/api-history-permission-tests.js +++ b/test/casperjs/api-history-permission-tests.js @@ -8,254 +8,255 @@ spaceghost.test.begin( 'Test permissions for accessible, published, and inaccess + 'over the API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -var email2 = spaceghost.user.getRandomEmail(), - password2 = '123456'; -if( spaceghost.fixtureData.testUser2 ){ - email2 = spaceghost.fixtureData.testUser2.email; - password2 = spaceghost.fixtureData.testUser2.password; -} + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } + var email2 = spaceghost.user.getRandomEmail(), + password2 = '123456'; + if( spaceghost.fixtureData.testUser2 ){ + email2 = spaceghost.fixtureData.testUser2.email; + password2 = spaceghost.fixtureData.testUser2.password; + } -var inaccessibleHistory, accessibleHistory, publishedHistory, - inaccessibleHdas, accessibleHdas, publishedHdas, - accessibleLink; + var inaccessibleHistory, accessibleHistory, publishedHistory, + inaccessibleHdas, accessibleHdas, publishedHdas, + accessibleLink; -// =================================================================== TESTS -//// ------------------------------------------------------------------------------------------- create 3 histories -spaceghost.user.loginOrRegisterUser( email, password ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); - // create three histories: make the 2nd importable (via the API), and the third published + // =================================================================== TESTS + //// ------------------------------------------------------------------------------------------- create 3 histories + spaceghost.user.loginOrRegisterUser( email, password ); + spaceghost.openHomePage().then( function(){ + this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); + // create three histories: make the 2nd importable (via the API), and the third published - this.test.comment( 'importable, slug, and published should all be returned by show and initially off' ); - // make the current the inaccessible one - inaccessibleHistory = this.api.histories.show( this.api.histories.index()[0].id ); - this.test.assert( this.hasKeys( inaccessibleHistory, [ 'id', 'name', 'slug', 'importable', 'published' ] ), - 'Has the proper keys' ); - this.test.assert( inaccessibleHistory.slug === null, - 'initial slug is null: ' + inaccessibleHistory.slug ); - this.test.assert( inaccessibleHistory.importable === false, - 'initial importable is false: ' + inaccessibleHistory.importable ); - this.test.assert( inaccessibleHistory.published === false, - 'initial published is false: ' + inaccessibleHistory.published ); - this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); - inaccessibleHistory = this.api.histories.show( inaccessibleHistory.id ); + this.test.comment( 'importable, slug, and published should all be returned by show and initially off' ); + // make the current the inaccessible one + inaccessibleHistory = this.api.histories.show( this.api.histories.index()[0].id ); + this.test.assert( this.hasKeys( inaccessibleHistory, [ 'id', 'name', 'slug', 'importable', 'published' ] ), + 'Has the proper keys' ); + this.test.assert( inaccessibleHistory.slug === null, + 'initial slug is null: ' + inaccessibleHistory.slug ); + this.test.assert( inaccessibleHistory.importable === false, + 'initial importable is false: ' + inaccessibleHistory.importable ); + this.test.assert( inaccessibleHistory.published === false, + 'initial published is false: ' + inaccessibleHistory.published ); + this.api.histories.update( inaccessibleHistory.id, { name: 'inaccessible' }); + inaccessibleHistory = this.api.histories.show( inaccessibleHistory.id ); + + this.test.comment( 'Setting importable to true should create a slug, ' + + 'username_and_slug, and importable === true' ); + accessibleHistory = this.api.histories.create({ name: 'accessible' }); + var returned = this.api.histories.update( accessibleHistory.id, { + importable : true + }); + this.debug( this.jsonStr( returned ) ); + accessibleHistory = this.api.histories.show( accessibleHistory.id ); + this.test.assert( this.hasKeys( accessibleHistory, [ 'username_and_slug' ] ), + 'Has the proper keys' ); + this.test.assert( accessibleHistory.slug === 'accessible', + 'slug is not null: ' + accessibleHistory.slug ); + this.test.assert( accessibleHistory.importable, + 'importable is true: ' + accessibleHistory.importable ); + accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/accessible'; + this.test.assert( accessibleHistory.username_and_slug === accessibleLink, + 'username_and_slug is proper: ' + accessibleHistory.username_and_slug ); + + this.test.comment( 'Setting published to true should make accessible and published === true' ); + publishedHistory = this.api.histories.create({ name: 'published' }); + returned = this.api.histories.update( publishedHistory.id, { + published : true + }); + this.debug( this.jsonStr( returned ) ); + publishedHistory = this.api.histories.show( publishedHistory.id ); + this.test.assert( this.hasKeys( publishedHistory, [ 'username_and_slug' ] ), + 'Has the proper keys' ); + this.test.assert( publishedHistory.published, + 'published is true: ' + publishedHistory.published ); + this.test.assert( publishedHistory.importable, + 'importable is true: ' + publishedHistory.importable ); + this.test.assert( publishedHistory.slug === 'published', + 'slug is not null: ' + publishedHistory.slug ); + accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/published'; + this.test.assert( publishedHistory.username_and_slug === accessibleLink, + 'username_and_slug is proper: ' + publishedHistory.username_and_slug ); - this.test.comment( 'Setting importable to true should create a slug, username_and_slug, and importable === true' ); - accessibleHistory = this.api.histories.create({ name: 'accessible' }); - var returned = this.api.histories.update( accessibleHistory.id, { - importable : true }); - this.debug( this.jsonStr( returned ) ); - accessibleHistory = this.api.histories.show( accessibleHistory.id ); - this.test.assert( this.hasKeys( accessibleHistory, [ 'username_and_slug' ] ), - 'Has the proper keys' ); - this.test.assert( accessibleHistory.slug === 'accessible', - 'slug is not null: ' + accessibleHistory.slug ); - this.test.assert( accessibleHistory.importable, - 'importable is true: ' + accessibleHistory.importable ); - accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/accessible'; - this.test.assert( accessibleHistory.username_and_slug === accessibleLink, - 'username_and_slug is proper: ' + accessibleHistory.username_and_slug ); - this.test.comment( 'Setting published to true should make accessible and published === true' ); - publishedHistory = this.api.histories.create({ name: 'published' }); - returned = this.api.histories.update( publishedHistory.id, { - published : true + //// ------------------------------------------------------------------------------------------- upload some files + spaceghost.then( function(){ + this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: '../../test-data/1.bed' }); + this.api.tools.thenUpload( accessibleHistory.id, { filepath: '../../test-data/1.bed' }); + this.api.tools.thenUpload( publishedHistory.id, { filepath: '../../test-data/1.bed' }); }); - this.debug( this.jsonStr( returned ) ); - publishedHistory = this.api.histories.show( publishedHistory.id ); - this.test.assert( this.hasKeys( publishedHistory, [ 'username_and_slug' ] ), - 'Has the proper keys' ); - this.test.assert( publishedHistory.published, - 'published is true: ' + publishedHistory.published ); - this.test.assert( publishedHistory.importable, - 'importable is true: ' + publishedHistory.importable ); - this.test.assert( publishedHistory.slug === 'published', - 'slug is not null: ' + publishedHistory.slug ); - accessibleLink = 'u/' + email.replace( '@test.test', '' ) + '/h/published'; - this.test.assert( publishedHistory.username_and_slug === accessibleLink, - 'username_and_slug is proper: ' + publishedHistory.username_and_slug ); - -}); - -//// ------------------------------------------------------------------------------------------- upload some files -spaceghost.then( function(){ - this.api.tools.thenUpload( inaccessibleHistory.id, { filepath: '../../test-data/1.bed' }); - this.api.tools.thenUpload( accessibleHistory.id, { filepath: '../../test-data/1.bed' }); - this.api.tools.thenUpload( publishedHistory.id, { filepath: '../../test-data/1.bed' }); -}); -spaceghost.then( function(){ - // check that they're there - inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), - accessibleHdas = this.api.hdas.index( accessibleHistory.id ), - publishedHdas = this.api.hdas.index( publishedHistory.id ); - this.test.comment( '---- adding datasets' ); - this.test.assert( inaccessibleHdas.length === 1, - 'uploaded file to inaccessible: ' + inaccessibleHdas.length ); - this.test.assert( accessibleHdas.length === 1, - 'uploaded file to accessible: ' + accessibleHdas.length ); - this.test.assert( publishedHdas.length === 1, - 'uploaded file to published: ' + publishedHdas.length ); -}); -spaceghost.user.logout(); - -//// ------------------------------------------------------------------------------------------- log in user2 -function testReadFunctionsOnAccessible( history, hdas ){ - this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); - - // read functions for history - this.test.comment( 'show should work for history: ' + history.name ); - this.test.assert( this.api.histories.show( history.id ).id === history.id, - 'show worked' ); - this.test.comment( 'copying should work for history: ' + history.name ); - var returned = this.api.histories.create({ history_id : history.id }); - this.test.assert( returned.name === "Copy of '" + history.name + "'", - 'copied name matches: ' + returned.name ); - - // read functions for history contents - this.test.comment( 'index of history contents should work for history: ' + history.name ); - this.test.assert( this.api.hdas.index( history.id ).length === 1, - 'hda index worked' ); - this.test.comment( 'showing of history contents should work for history: ' + history.name ); - this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, - 'hda show worked' ); - - this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' - + ' should work from accessible history: ' + history.name ); - returned = this.api.hdas.create( this.api.histories.index()[0].id, { - source : 'hda', - content : hdas[0].id + spaceghost.then( function(){ + // check that they're there + inaccessibleHdas = this.api.hdas.index( inaccessibleHistory.id ), + accessibleHdas = this.api.hdas.index( accessibleHistory.id ), + publishedHdas = this.api.hdas.index( publishedHistory.id ); + this.test.comment( '---- adding datasets' ); + this.test.assert( inaccessibleHdas.length === 1, + 'uploaded file to inaccessible: ' + inaccessibleHdas.length ); + this.test.assert( accessibleHdas.length === 1, + 'uploaded file to accessible: ' + accessibleHdas.length ); + this.test.assert( publishedHdas.length === 1, + 'uploaded file to published: ' + publishedHdas.length ); }); - this.test.assert( returned.name === hdas[0].name, 'successful hda copy from: ' + returned.name ); -} + spaceghost.user.logout(); -function testReadFunctionsOnInaccessible( history, hdas ){ - this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); + //// ------------------------------------------------------------------------------------------- log in user2 + function testReadFunctionsOnAccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for ACCESSIBLE history: ' + history.name ); - // read functions for history - this.test.comment( 'show should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.show( history.id ); - }, 403, 'History is not accessible to the current user', 'show failed with error' ); - this.test.comment( 'copying should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.create({ history_id : history.id }); - }, 403, 'History is not accessible to the current user', 'copy failed with error' ); + // read functions for history + this.test.comment( 'show should work for history: ' + history.name ); + this.test.assert( this.api.histories.show( history.id ).id === history.id, + 'show worked' ); + this.test.comment( 'copying should work for history: ' + history.name ); + var returned = this.api.histories.create({ history_id : history.id }); + this.test.assert( returned.name === "Copy of '" + history.name + "'", + 'copied name matches: ' + returned.name ); - // read functions for history contents - this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.index( history.id ); - }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); - this.api.assertRaises( function(){ - this.api.hdas.show( history.id, hdas[0].id ); - }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); + // read functions for history contents + this.test.comment( 'index of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.index( history.id ).length === 1, + 'hda index worked' ); + this.test.comment( 'showing of history contents should work for history: ' + history.name ); + this.test.assert( this.api.hdas.show( history.id, hdas[0].id ).id === hdas[0].id, + 'hda show worked' ); - this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' - + ' from an inaccessible history should fail for: ' + history.name ); - this.api.assertRaises( function(){ - var returned = this.api.hdas.create( this.api.histories.index()[0].id, { + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' should work from accessible history: ' + history.name ); + returned = this.api.hdas.create( this.api.histories.index()[0].id, { source : 'hda', content : hdas[0].id }); - this.debug( this.jsonStr( returned ) ); - }, 403, 'History is not accessible to the current user', 'hda copy from failed with error' ); + this.test.assert( returned.name === hdas[0].name, 'successful hda copy from: ' + returned.name ); + } -} + function testReadFunctionsOnInaccessible( history, hdas ){ + this.test.comment( '---- testing read/accessibility functions for INACCESSIBLE history: ' + history.name ); -function testWriteFunctions( history, hdas ){ - this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); + // read functions for history + this.test.comment( 'show should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.show( history.id ); + }, 403, 'History is not accessible to the current user', 'show failed with error' ); + this.test.comment( 'copying should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.create({ history_id : history.id }); + }, 403, 'History is not accessible to the current user', 'copy failed with error' ); - this.test.comment( 'update should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.update( history.id, { deleted: true }); - }, 403, 'History is not owned by the current user', 'update failed with error' ); - this.test.comment( 'delete should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.histories.delete_( history.id ); - }, 403, 'History is not owned by the current user', 'delete failed with error' ); + // read functions for history contents + this.test.comment( 'index and show of history contents should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.index( history.id ); + }, 403, 'History is not accessible to the current user', 'hda index failed with error' ); + this.api.assertRaises( function(){ + this.api.hdas.show( history.id, hdas[0].id ); + }, 403, 'History is not accessible to the current user', 'hda show failed with error' ); - this.test.comment( 'hda updating should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); - }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda update failed with error' ); - this.test.comment( 'hda deletion should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.delete_( history.id, hdas[0].id ); - }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda delete failed with error' ); + this.test.comment( 'Attempting to copy an accessible hda (default is accessible)' + + ' from an inaccessible history should fail for: ' + history.name ); + this.api.assertRaises( function(){ + var returned = this.api.hdas.create( this.api.histories.index()[0].id, { + source : 'hda', + content : hdas[0].id + }); + this.debug( this.jsonStr( returned ) ); + }, 403, 'History is not accessible to the current user', 'hda copy from failed with error' ); - this.test.comment( 'copying hda into history should fail for history: ' + history.name ); - this.api.assertRaises( function(){ - this.api.hdas.create( history.id, { - source : 'hda', - // should error before it checks the id - content : 'bler' + } + + function testWriteFunctions( history, hdas ){ + this.test.comment( '---- testing write/ownership functions for history: ' + history.name ); + + this.test.comment( 'update should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.update( history.id, { deleted: true }); + }, 403, 'History is not owned by the current user', 'update failed with error' ); + this.test.comment( 'delete should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.histories.delete_( history.id ); + }, 403, 'History is not owned by the current user', 'delete failed with error' ); + + this.test.comment( 'hda updating should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.update( history.id, hdas[0].id, { deleted: true }); + }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda update failed with error' ); + this.test.comment( 'hda deletion should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.delete_( history.id, hdas[0].id ); + }, 403, 'HistoryDatasetAssociation is not owned by the current user', 'hda delete failed with error' ); + + this.test.comment( 'copying hda into history should fail for history: ' + history.name ); + this.api.assertRaises( function(){ + this.api.hdas.create( history.id, { + source : 'hda', + // should error before it checks the id + content : 'bler' + }); + }, 403, 'History is not owned by the current user', 'hda copy to failed' ); + } + + function testInaccessible( history, hdas ){ + testReadFunctionsOnInaccessible.call( this, history, hdas ); + testWriteFunctions.call( this, history, hdas ); + } + + function testAccessible( history, hdas ){ + testReadFunctionsOnAccessible.call( this, history, hdas ); + testWriteFunctions.call( this, history, hdas ); + } + + spaceghost.user.loginOrRegisterUser( email2, password2 ); + spaceghost.openHomePage().then( function(){ + this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); + testInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); + testAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testAccessible.call( spaceghost, publishedHistory, publishedHdas ); + }); + spaceghost.user.logout(); + + + //// ------------------------------------------------------------------------------------------- user1 revoke perms + spaceghost.user.loginOrRegisterUser( email, password ); + spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); + this.test.comment( 'revoking perms should prevent access' ); + this.api.histories.update( accessibleHistory.id, { + importable : false }); - }, 403, 'History is not owned by the current user', 'hda copy to failed' ); -} + var returned = this.api.histories.show( accessibleHistory.id ); + this.test.assert( !returned.importable, 'now not importable' ); + this.test.assert( !returned.published, '(still not published)' ); + this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); -function testInaccessible( history, hdas ){ - testReadFunctionsOnInaccessible.call( this, history, hdas ); - testWriteFunctions.call( this, history, hdas ); -} - -function testAccessible( history, hdas ){ - testReadFunctionsOnAccessible.call( this, history, hdas ); - testWriteFunctions.call( this, history, hdas ); -} - -spaceghost.user.loginOrRegisterUser( email2, password2 ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); - testInaccessible.call( spaceghost, inaccessibleHistory, inaccessibleHdas ); - testAccessible.call( spaceghost, accessibleHistory, accessibleHdas ); - testAccessible.call( spaceghost, publishedHistory, publishedHdas ); -}); -spaceghost.user.logout(); - - -//// ------------------------------------------------------------------------------------------- user1 revoke perms -spaceghost.user.loginOrRegisterUser( email, password ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); - this.test.comment( 'revoking perms should prevent access' ); - this.api.histories.update( accessibleHistory.id, { - importable : false + this.api.histories.update( publishedHistory.id, { + importable : false, + published : false + }); + returned = this.api.histories.show( publishedHistory.id ); + this.test.assert( !returned.importable, 'now not importable' ); + this.test.assert( !returned.published, 'now not published' ); + this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); }); - var returned = this.api.histories.show( accessibleHistory.id ); - this.test.assert( !returned.importable, 'now not importable' ); - this.test.assert( !returned.published, '(still not published)' ); - this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); + spaceghost.user.logout(); - this.api.histories.update( publishedHistory.id, { - importable : false, - published : false + + //// ------------------------------------------------------------------------------------------- user2 retry perms + spaceghost.user.loginOrRegisterUser( email2, password2 ); + spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ + this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); + testInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); + testInaccessible.call( spaceghost, publishedHistory, publishedHdas ); }); - returned = this.api.histories.show( publishedHistory.id ); - this.test.assert( !returned.importable, 'now not importable' ); - this.test.assert( !returned.published, 'now not published' ); - this.test.assert( !!returned.slug, '(slug still set) ' + returned.slug ); -}); -spaceghost.user.logout(); + spaceghost.user.logout(); -//// ------------------------------------------------------------------------------------------- user2 retry perms -spaceghost.user.loginOrRegisterUser( email2, password2 ); -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - this.test.comment( '(logged in as ' + this.user.loggedInAs() + ')' ); - testInaccessible.call( spaceghost, accessibleHistory, accessibleHdas ); - testInaccessible.call( spaceghost, publishedHistory, publishedHdas ); -}); -spaceghost.user.logout(); - - -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-history-tests.js b/test/casperjs/api-history-tests.js index 719232a7dad..cd55e27ab5b 100644 --- a/test/casperjs/api-history-tests.js +++ b/test/casperjs/api-history-tests.js @@ -18,7 +18,7 @@ spaceghost.user.loginOrRegisterUser( email, password ); // =================================================================== TESTS -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ +spaceghost.openHomePage().then( function(){ // ------------------------------------------------------------------------------------------- INDEX this.test.comment( 'index should get a list of histories' ); diff --git a/test/casperjs/api-tool-tests.js b/test/casperjs/api-tool-tests.js index 25a6867c1e8..a0e886b565c 100644 --- a/test/casperjs/api-tool-tests.js +++ b/test/casperjs/api-tool-tests.js @@ -92,7 +92,7 @@ function attemptShowOnAllTools(){ this.debug( this.jsonStr( set ) ); } -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ +spaceghost.openHomePage().then( function(){ // ------------------------------------------------------------------------------------------- INDEX // ........................................................................................... (defaults) diff --git a/test/casperjs/api-user-tests.js b/test/casperjs/api-user-tests.js index b1e9d0fea64..7c66b1ba0f6 100644 --- a/test/casperjs/api-user-tests.js +++ b/test/casperjs/api-user-tests.js @@ -7,46 +7,46 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test the user API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -spaceghost.user.loginOrRegisterUser( email, password ); - -// =================================================================== TESTS -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - - // ------------------------------------------------------------------------------------------- INDEX - this.test.comment( 'index should get a list of users' ); - var userIndex = this.api.users.index(); - this.debug( this.jsonStr( userIndex ) ); - this.test.assert( utils.isArray( userIndex ), "index returned an array: length " + userIndex.length ); - - // need a way to import/create a user here for testing - if( userIndex.length <= 0 ){ - log.warn( 'No users available' ); - return; + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; } - this.test.assert( userIndex.length >= 1, 'Has at least one user' ); + spaceghost.user.loginOrRegisterUser( email, password ); - //TODO: index( deleted ) + // =================================================================== TESTS + spaceghost.openHomePage().then( function(){ - // ------------------------------------------------------------------------------------------- SHOW - this.test.comment( 'show should get detailed data about the user with the given id' ); - var userShow = this.api.users.show( userIndex[0].id ); - this.debug( this.jsonStr( userShow ) ); + // ------------------------------------------------------------------------------------------- INDEX + this.test.comment( 'index should get a list of users' ); + var userIndex = this.api.users.index(); + this.debug( this.jsonStr( userIndex ) ); + this.test.assert( utils.isArray( userIndex ), "index returned an array: length " + userIndex.length ); - //TODO: show( current ) - //TODO: show( deleted ) + // need a way to import/create a user here for testing + if( userIndex.length <= 0 ){ + log.warn( 'No users available' ); + return; + } + this.test.assert( userIndex.length >= 1, 'Has at least one user' ); - // ------------------------------------------------------------------------------------------- CREATE + //TODO: index( deleted ) - // ------------------------------------------------------------------------------------------- MISC -}); + // ------------------------------------------------------------------------------------------- SHOW + this.test.comment( 'show should get detailed data about the user with the given id' ); + var userShow = this.api.users.show( userIndex[0].id ); + this.debug( this.jsonStr( userShow ) ); -// =================================================================== + //TODO: show( current ) + //TODO: show( deleted ) + + // ------------------------------------------------------------------------------------------- CREATE + + // ------------------------------------------------------------------------------------------- MISC + }); + + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-visualizations-tests.js b/test/casperjs/api-visualizations-tests.js index a0099589c97..da966b48e5a 100644 --- a/test/casperjs/api-visualizations-tests.js +++ b/test/casperjs/api-visualizations-tests.js @@ -7,189 +7,189 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test the visualizations API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -spaceghost.user.loginOrRegisterUser( email, password ); + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } + spaceghost.user.loginOrRegisterUser( email, password ); -// =================================================================== TESTS -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - var ALWAYS_CREATE = true, - indexKeys = [ - 'id', 'title', 'type', 'dbkey', 'url' - ], - showKeys = indexKeys.concat([ - 'user_id', 'model_class', 'revisions', 'latest_revision', 'annotation' - ]), - revisionKeys = [ - 'id', 'title', 'visualization_id', 'dbkey', 'model_class', 'config' - ]; + // =================================================================== TESTS + spaceghost.openHomePage().then( function(){ + var ALWAYS_CREATE = true, + indexKeys = [ + 'id', 'title', 'type', 'dbkey', 'url' + ], + showKeys = indexKeys.concat([ + 'user_id', 'model_class', 'revisions', 'latest_revision', 'annotation' + ]), + revisionKeys = [ + 'id', 'title', 'visualization_id', 'dbkey', 'model_class', 'config' + ]; - // ------------------------------------------------------------------------------------------- set up - var visualizationIndex = this.api.visualizations.index(); - if( ALWAYS_CREATE || !visualizationIndex.length ){ - // post a visualization - this.info( 'creating new visualization for tests' ); - var testVisualization = this.api.visualizations.create({ - title : 'Test Visualization', + // ------------------------------------------------------------------------------------------- set up + var visualizationIndex = this.api.visualizations.index(); + if( ALWAYS_CREATE || !visualizationIndex.length ){ + // post a visualization + this.info( 'creating new visualization for tests' ); + var testVisualization = this.api.visualizations.create({ + title : 'Test Visualization', + // needs to be unique + slug : 'test-visualization-' + Date.now(), + type : 'test', + dbkey : 'hg17', + annotation : 'this is a test of the emergency visualization system', + config : { + x : 10, + y : 12 + } + }); + this.debug( this.jsonStr( testVisualization ) ); + } + + // ------------------------------------------------------------------------------------------- INDEX + this.test.comment( 'index should get a list of visualizations' ); + visualizationIndex = this.api.visualizations.index(); + this.debug( this.jsonStr( visualizationIndex ) ); + this.test.assert( utils.isArray( visualizationIndex ), + "index returned an array: length " + visualizationIndex.length ); + this.test.assert( visualizationIndex.length >= 1, 'Has at least one visualization' ); + + var firstVisualization = visualizationIndex[0]; + this.test.assert( this.hasKeys( firstVisualization, indexKeys ), 'Has the proper keys' ); + this.test.assert( this.api.isEncodedId( firstVisualization.id ), 'Id appears well-formed' ); + + //TODO: index searching + //TODO: anon user + //TODO: admin user + + // ------------------------------------------------------------------------------------------- SHOW + this.test.comment( 'show should get a visualization details object' ); + var visualizationShow = this.api.visualizations.show( firstVisualization.id ); + this.debug( this.jsonStr( visualizationShow ) ); + this.test.assert( this.hasKeys( visualizationShow, showKeys ), 'Has the proper keys' ); + this.test.assert( visualizationShow.model_class === 'Visualization', + 'Has the proper model_class: ' + visualizationShow.model_class ); + + this.test.comment( 'a visualization details object should contain an array of revision ids' ); + var revisions = visualizationShow.revisions; + this.test.assert( utils.isArray( revisions ), 'revisions is an array' ); + this.test.assert( revisions.length >= 1, 'revisions has at least one entry' ); + var areIds = true; + revisions.forEach( function( revision ){ + if( !spaceghost.api.isEncodedId( revision ) ){ areIds = false; } + }); + this.test.assert( areIds, 'all revisions are ids' ); + + this.test.comment( 'a visualization details object should contain a subobject of the latest revision' ); + var latestRevision = visualizationShow.latest_revision; + this.test.assert( utils.isObject( latestRevision ), 'latestRevision is an object' ); + this.test.assert( this.hasKeys( latestRevision, revisionKeys ), 'latestRevision has the proper keys' ); + this.test.assert( latestRevision.model_class === 'VisualizationRevision', + 'Has the proper model_class: ' + latestRevision.model_class ); + this.test.assert( latestRevision.visualization_id === visualizationShow.id, + 'revision visualization_id matches containing visualization id: ' + latestRevision.visualization_id ); + this.test.assert( visualizationShow.revisions.indexOf( latestRevision.id ) !== -1, + 'found latest_revision id in revisions' ); + + this.test.comment( 'a visualization revision should contain a subobject for the config' ); + var config = latestRevision.config; + this.test.assert( utils.isObject( config ), 'config is an object:\n' + this.jsonStr( config ) ); + + //TODO: url in visualizationIndex == show url + //TODO: non existing id throws error + //TODO: anon user + //TODO: user1 has no permissions to show user2 + + // ------------------------------------------------------------------------------------------- CREATE + this.test.comment( 'Calling create should create a new visualization and allow setting the name' ); + var visualizationData = { + title : 'Created Visualization', // needs to be unique - slug : 'test-visualization-' + Date.now(), + slug : 'created-visualization-' + Date.now(), type : 'test', dbkey : 'hg17', - annotation : 'this is a test of the emergency visualization system', + annotation : 'invisible visualization', config : { x : 10, y : 12 } - }); - this.debug( this.jsonStr( testVisualization ) ); - } + }; + var created = this.api.visualizations.create( visualizationData ); + this.debug( 'returned from create:\n' + this.jsonStr( created ) ); + this.test.assert( this.api.isEncodedId( created.id ), "create returned an id: " + created.id ); - // ------------------------------------------------------------------------------------------- INDEX - this.test.comment( 'index should get a list of visualizations' ); - visualizationIndex = this.api.visualizations.index(); - this.debug( this.jsonStr( visualizationIndex ) ); - this.test.assert( utils.isArray( visualizationIndex ), - "index returned an array: length " + visualizationIndex.length ); - this.test.assert( visualizationIndex.length >= 1, 'Has at least one visualization' ); + // check v. show + visualizationShow = this.api.visualizations.show( created.id ); + this.debug( 'visualizationShow:\n' + this.jsonStr( visualizationShow ) ); + // config is re-located into a revision and won't be there + //this.test.assert( compareObjs( visualizationShow, visualizationData, [ 'config' ] ), + // "show results seem to match create data" ); - var firstVisualization = visualizationIndex[0]; - this.test.assert( this.hasKeys( firstVisualization, indexKeys ), 'Has the proper keys' ); - this.test.assert( this.api.isEncodedId( firstVisualization.id ), 'Id appears well-formed' ); + // the following errors are produced within base.controller.UsesVisualizationsMixin._create_visualization + this.test.comment( 'Calling create with a non-unique slug will cause an API error' ); + this.api.assertRaises( function(){ + created = this.api.visualizations.create( visualizationData ); + }, 400, 'visualization identifier must be unique' ); - //TODO: index searching - //TODO: anon user - //TODO: admin user + this.test.comment( 'Calling create with no title will cause an API error' ); + visualizationData.title = ''; + this.api.assertRaises( function(){ + created = this.api.visualizations.create( visualizationData ); + }, 400, 'visualization name is required' ); + visualizationData.title = 'Created Visualization'; - // ------------------------------------------------------------------------------------------- SHOW - this.test.comment( 'show should get a visualization details object' ); - var visualizationShow = this.api.visualizations.show( firstVisualization.id ); - this.debug( this.jsonStr( visualizationShow ) ); - this.test.assert( this.hasKeys( visualizationShow, showKeys ), 'Has the proper keys' ); - this.test.assert( visualizationShow.model_class === 'Visualization', - 'Has the proper model_class: ' + visualizationShow.model_class ); + this.test.comment( 'Calling create with improper characters in the slug will cause an API error' ); + var oldSlug = visualizationData.slug; + visualizationData.slug = '123_()'; + this.api.assertRaises( function(){ + created = this.api.visualizations.create( visualizationData ); + }, 400, "visualization identifier must consist of only lowercase letters, numbers, and the '-' character" ); + visualizationData.slug = oldSlug; + + this.test.comment( 'Calling create with an unrecognized key will cause an API error' ); + visualizationData.bler = 'blah'; + this.api.assertRaises( function(){ + created = this.api.visualizations.create( visualizationData ); + }, 400, 'unknown key: bler' ); + delete visualizationData.bler; + + this.test.comment( 'Calling create with an unparsable JSON config will cause an API error' ); + visualizationData.config = '3 = nime'; + this.api.assertRaises( function(){ + created = this.api.visualizations.create( visualizationData ); + }, 400, 'config must be a dictionary (JSON)' ); + + // ------------------------------------------------------------------------------------------ UPDATE + // ........................................................................................... idiot proofing + this.test.comment( 'updating using a nonsense key should fail with an error' ); + this.api.assertRaises( function(){ + returned = this.api.visualizations.update( created.id, { bler : 'blah' }); + }, 400, 'unknown key: bler' ); + + this.test.comment( 'updating by attempting to change type should cause an error' ); + this.api.assertRaises( function(){ + returned = this.api.visualizations.update( created.id, { title : 30 }); + }, 400, 'title must be a string or unicode' ); + //TODO: the other types... + + // ........................................................................................... title + //this.test.comment( 'update should create a new visualization revision' ); + // + //this.test.comment( 'updating with a new title should NOT change the visualization title...' ); + //latestRevision = this.api.visualizations.show( created.id ).latest_revision; + //returned = this.api.visualizations.update( created.id, { + // title : 'New title' + //}); + //visualizationShow = this.api.visualizations.show( created.id ); + //this.debug( this.jsonStr( visualizationShow ) ); + //this.test.assert( visualizationShow.title === visualizationData.title, + // "Title does not set via update: " + visualizationShow.title ); - this.test.comment( 'a visualization details object should contain an array of revision ids' ); - var revisions = visualizationShow.revisions; - this.test.assert( utils.isArray( revisions ), 'revisions is an array' ); - this.test.assert( revisions.length >= 1, 'revisions has at least one entry' ); - var areIds = true; - revisions.forEach( function( revision ){ - if( !spaceghost.api.isEncodedId( revision ) ){ areIds = false; } }); - this.test.assert( areIds, 'all revisions are ids' ); - this.test.comment( 'a visualization details object should contain a subobject of the latest revision' ); - var latestRevision = visualizationShow.latest_revision; - this.test.assert( utils.isObject( latestRevision ), 'latestRevision is an object' ); - this.test.assert( this.hasKeys( latestRevision, revisionKeys ), 'latestRevision has the proper keys' ); - this.test.assert( latestRevision.model_class === 'VisualizationRevision', - 'Has the proper model_class: ' + latestRevision.model_class ); - this.test.assert( latestRevision.visualization_id === visualizationShow.id, - 'revision visualization_id matches containing visualization id: ' + latestRevision.visualization_id ); - this.test.assert( visualizationShow.revisions.indexOf( latestRevision.id ) !== -1, - 'found latest_revision id in revisions' ); - - this.test.comment( 'a visualization revision should contain a subobject for the config' ); - var config = latestRevision.config; - this.test.assert( utils.isObject( config ), 'config is an object:\n' + this.jsonStr( config ) ); - - //TODO: url in visualizationIndex == show url - //TODO: non existing id throws error - //TODO: anon user - //TODO: user1 has no permissions to show user2 - - // ------------------------------------------------------------------------------------------- CREATE - this.test.comment( 'Calling create should create a new visualization and allow setting the name' ); - var visualizationData = { - title : 'Created Visualization', - // needs to be unique - slug : 'created-visualization-' + Date.now(), - type : 'test', - dbkey : 'hg17', - annotation : 'invisible visualization', - config : { - x : 10, - y : 12 - } - }; - var created = this.api.visualizations.create( visualizationData ); - this.debug( 'returned from create:\n' + this.jsonStr( created ) ); - this.test.assert( this.api.isEncodedId( created.id ), "create returned an id: " + created.id ); - - // check v. show - visualizationShow = this.api.visualizations.show( created.id ); - this.debug( 'visualizationShow:\n' + this.jsonStr( visualizationShow ) ); - // config is re-located into a revision and won't be there - //this.test.assert( compareObjs( visualizationShow, visualizationData, [ 'config' ] ), - // "show results seem to match create data" ); - - // the following errors are produced within base.controller.UsesVisualizationsMixin._create_visualization - this.test.comment( 'Calling create with a non-unique slug will cause an API error' ); - this.api.assertRaises( function(){ - created = this.api.visualizations.create( visualizationData ); - }, 400, 'visualization identifier must be unique' ); - - this.test.comment( 'Calling create with no title will cause an API error' ); - visualizationData.title = ''; - this.api.assertRaises( function(){ - created = this.api.visualizations.create( visualizationData ); - }, 400, 'visualization name is required' ); - visualizationData.title = 'Created Visualization'; - - this.test.comment( 'Calling create with improper characters in the slug will cause an API error' ); - var oldSlug = visualizationData.slug; - visualizationData.slug = '123_()'; - this.api.assertRaises( function(){ - created = this.api.visualizations.create( visualizationData ); - }, 400, "visualization identifier must consist of only lowercase letters, numbers, and the '-' character" ); - visualizationData.slug = oldSlug; - - this.test.comment( 'Calling create with an unrecognized key will cause an API error' ); - visualizationData.bler = 'blah'; - this.api.assertRaises( function(){ - created = this.api.visualizations.create( visualizationData ); - }, 400, 'unknown key: bler' ); - delete visualizationData.bler; - - this.test.comment( 'Calling create with an unparsable JSON config will cause an API error' ); - visualizationData.config = '3 = nime'; - this.api.assertRaises( function(){ - created = this.api.visualizations.create( visualizationData ); - }, 400, 'config must be a dictionary (JSON)' ); - - // ------------------------------------------------------------------------------------------ UPDATE - // ........................................................................................... idiot proofing - this.test.comment( 'updating using a nonsense key should fail with an error' ); - this.api.assertRaises( function(){ - returned = this.api.visualizations.update( created.id, { bler : 'blah' }); - }, 400, 'unknown key: bler' ); - - this.test.comment( 'updating by attempting to change type should cause an error' ); - this.api.assertRaises( function(){ - returned = this.api.visualizations.update( created.id, { title : 30 }); - }, 400, 'title must be a string or unicode' ); - //TODO: the other types... - - // ........................................................................................... title - //this.test.comment( 'update should create a new visualization revision' ); - // - //this.test.comment( 'updating with a new title should NOT change the visualization title...' ); - //latestRevision = this.api.visualizations.show( created.id ).latest_revision; - //returned = this.api.visualizations.update( created.id, { - // title : 'New title' - //}); - //visualizationShow = this.api.visualizations.show( created.id ); - //this.debug( this.jsonStr( visualizationShow ) ); - //this.test.assert( visualizationShow.title === visualizationData.title, - // "Title does not set via update: " + visualizationShow.title ); - -}); - -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/api-workflow-tests.js b/test/casperjs/api-workflow-tests.js index 5b2fafb72cc..86e1bf8dd63 100644 --- a/test/casperjs/api-workflow-tests.js +++ b/test/casperjs/api-workflow-tests.js @@ -7,240 +7,238 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test the workflows API', 0, function suite( test ){ spaceghost.start(); -// =================================================================== SET UP -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} -spaceghost.user.loginOrRegisterUser( email, password ); - -// =================================================================== TESTS -var workflowJSONFilepath = 'test-data/simple_test.ga', - workflowModelClass = 'StoredWorkflow', - workflowSummaryKeys = [ - 'id', 'model_class', 'name', 'published', 'tags', 'url' - ], - workflowDetailKeys = workflowSummaryKeys.concat([ - 'inputs', 'steps' - ]), - stepKeys = [ - 'id', 'input_steps', 'tool_id', 'type' - ], - simpleBedFilepath = '../../test-data/2.bed', - uploadedFile = null, - workflowCreateKeys = [ 'history', 'outputs' ]; - - -spaceghost.thenOpen( spaceghost.baseUrl ).then( function(){ - - // ------------------------------------------------------------------------------------------- UPLOAD - // upload first or we have no data to test - this.test.comment( 'upload should allow importing a new workflow given in JSON form' ); - var workflowToUpload = this.loadJSONFile( workflowJSONFilepath ); - //this.debug( this.jsonStr( workflowToUpload ) ); - var returned = this.api.workflows.upload( workflowToUpload ); - - this.test.comment( 'upload should return a summary object of what we uploaded' ); - //this.debug( this.jsonStr( returned ) ); - this.test.assert( utils.isObject( returned ), "upload returned an object" ); - this.test.assert( this.hasKeys( returned, workflowSummaryKeys ), "upload's return has the proper keys" ); - this.test.assert( this.api.isEncodedId( returned.id ), - "id is of the proper form: " + returned.id ); - this.test.assert( returned.model_class === workflowModelClass, - "has the proper model_class: " + returned.model_class ); - this.test.assert( returned.name === workflowToUpload.name + ' ' + '(imported from API)', - "has the proper, modified name: " + returned.name ); - this.test.assert( !returned.published, - "uploaded workflow defaults to un-published: " + returned.published ); - this.test.assert( utils.isArray( returned.tags ) && returned.tags.length === 0, - "upload returned an empty tag array: " + this.jsonStr( returned.tags ) ); - this.test.assert( returned.url === '/' + utils.format( this.api.workflows.urlTpls.show, returned.id ), - "url matches the show url: " + returned.url ); - - - // ------------------------------------------------------------------------------------------- INDEX - this.test.comment( 'index should get a list of workflows' ); - var workflowIndex = this.api.workflows.index(); - this.debug( this.jsonStr( workflowIndex ) ); - this.test.assert( utils.isArray( workflowIndex ), "index returned an array: length " + workflowIndex.length ); - this.test.assert( workflowIndex.length >= 1, "index returned at least one job" ); - - this.test.comment( 'index should have returned an object matching the workflow uploaded' ); - var firstWorkflow = workflowIndex[0]; - this.test.assert( this.hasKeys( firstWorkflow, workflowSummaryKeys ), "index has the proper keys" ); - this.test.assert( this.api.isEncodedId( firstWorkflow.id ), - "id is of the proper form: " + firstWorkflow.id ); - this.test.assert( firstWorkflow.model_class === workflowModelClass, - "has the proper model_class: " + firstWorkflow.model_class ); - this.test.assert( firstWorkflow.name === workflowToUpload.name + ' ' + '(imported from API)', - "has the proper, modified name: " + firstWorkflow.name ); - this.test.assert( !firstWorkflow.published, - "workflow is un-published: " + firstWorkflow.published ); - this.test.assert( utils.isArray( firstWorkflow.tags ) && firstWorkflow.tags.length === 0, - "tag array is empty: " + this.jsonStr( firstWorkflow.tags ) ); - this.test.assert( firstWorkflow.url === '/' + utils.format( this.api.workflows.urlTpls.show, firstWorkflow.id ), - "url matches the show url: " + firstWorkflow.url ); - - - // ------------------------------------------------------------------------------------------- SHOW - this.test.comment( 'show should get detailed data about the workflow with the given id' ); - var workflowShow = this.api.workflows.show( firstWorkflow.id ); - this.debug( this.jsonStr( workflowShow ) ); - this.test.assert( utils.isObject( workflowShow ), "show returned an object" ); - this.test.assert( this.hasKeys( workflowShow, workflowDetailKeys ), "show has the proper keys" ); - this.test.assert( this.api.isEncodedId( workflowShow.id ), - "id is of the proper form: " + workflowShow.id ); - this.test.assert( workflowShow.model_class === workflowModelClass, - "has the proper model_class: " + workflowShow.model_class ); - this.test.assert( workflowShow.name === workflowToUpload.name + ' ' + '(imported from API)', - "has the proper, modified name: " + workflowShow.name ); - this.test.assert( !workflowShow.published, - "workflow is un-published: " + workflowShow.published ); - this.test.assert( utils.isArray( workflowShow.tags ) && workflowShow.tags.length === 0, - "tag array is empty: " + this.jsonStr( workflowShow.tags ) ); - this.test.assert( workflowShow.url === '/' + utils.format( this.api.workflows.urlTpls.show, workflowShow.id ), - "url matches the show url: " + workflowShow.url ); - - this.test.comment( 'inputs from show should be an object (and, in this case, empty)' ); - var inputs = workflowShow.inputs; - this.debug( 'inputs:\n' + this.jsonStr( inputs ) ); - this.test.assert( utils.isObject( workflowShow.inputs ), "inputs is an object" ); - this.test.assert( this.countKeys( workflowShow.inputs ) !== 0, "inputs has keys" ); - - this.test.comment( 'steps from show should be an object containing each tool defined as a step' ); - var steps = workflowShow.steps; - //this.debug( 'steps:\n' + this.jsonStr( steps ) ); - this.test.assert( utils.isObject( workflowShow.steps ), "steps is an object" ); - //! ids for steps (and the keys used) are un-encoded (and in strings) - for( var stepKey in steps ){ - if( steps.hasOwnProperty( stepKey ) ){ - // any way to match this up with the workflowToUpload? - - this.test.assert( utils.isString( stepKey ), "step key is a string: " + stepKey ); - var step = steps[ stepKey ]; - this.debug( 'step:\n' + this.jsonStr( step ) ); - this.test.assert( this.hasKeys( step, stepKeys ), "step has the proper keys" ); - - this.test.assert( utils.isNumber( step.id ), - "step id is a number: " + step.id ); - try { - this.test.assert( parseInt( stepKey, 10 ) === step.id, - "step id matches step key: " + step.id ); - } catch( err ){ - this.test.fail( 'couldnt parse stepKey: ' + stepKey + ',' + err ); - } - - this.test.assert( utils.isObject( step.input_steps ), "input_steps is an object" ); - if( this.countKeys( step.input_steps ) !== 0 ){ - this.test.assert( this.hasKeys( step.input_steps, [ 'input' ] ), "input_steps has the proper keys" ); - } - - if( step.type === 'tool' ){ - // check for tools in this wf with the api - this.test.assert( utils.isString( step.tool_id ), - "step tool_id is a string: " + step.tool_id ); - var tool_used = this.api.tools.show( step.tool_id.replace( / /g, '+' ) ); - this.debug( this.jsonStr( tool_used ) ); - this.test.assert( this.countKeys( tool_used ) !== 0, "found tool in api.tools for: " + step.tool_id ); - } - } + // =================================================================== SET UP + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; } + spaceghost.user.loginOrRegisterUser( email, password ); + + // =================================================================== TESTS + var workflowJSONFilepath = 'test-data/simple_test.ga', + workflowModelClass = 'StoredWorkflow', + workflowSummaryKeys = [ + 'id', 'model_class', 'name', 'published', 'tags', 'url' + ], + workflowDetailKeys = workflowSummaryKeys.concat([ + 'inputs', 'steps' + ]), + stepKeys = [ + 'id', 'input_steps', 'tool_id', 'type' + ], + simpleBedFilepath = '../../test-data/2.bed', + uploadedId = null, + workflowCreateKeys = [ 'history', 'outputs' ]; + spaceghost.openHomePage().then( function(){ - // ------------------------------------------------------------------------------------------- MISC -}); + // ------------------------------------------------------------------------------------------- UPLOAD + // upload first or we have no data to test + this.test.comment( 'upload should allow importing a new workflow given in JSON form' ); + var workflowToUpload = this.loadJSONFile( workflowJSONFilepath ); + //this.debug( this.jsonStr( workflowToUpload ) ); + var returned = this.api.workflows.upload( workflowToUpload ); -// now run the uploaded workflow -spaceghost.tools.uploadFile( simpleBedFilepath, function( uploadInfo ){ - uploadedFile = uploadInfo; -}); -spaceghost.then( function(){ - var currentHistory = this.api.histories.index()[0], - firstWorkflow = this.api.workflows.show( this.api.workflows.index()[0].id ); + this.test.comment( 'upload should return a summary object of what we uploaded' ); + //this.debug( this.jsonStr( returned ) ); + this.test.assert( utils.isObject( returned ), "upload returned an object" ); + this.test.assert( this.hasKeys( returned, workflowSummaryKeys ), "upload's return has the proper keys" ); + this.test.assert( this.api.isEncodedId( returned.id ), + "id is of the proper form: " + returned.id ); + this.test.assert( returned.model_class === workflowModelClass, + "has the proper model_class: " + returned.model_class ); + this.test.assert( returned.name === workflowToUpload.name + ' ' + '(imported from API)', + "has the proper, modified name: " + returned.name ); + this.test.assert( !returned.published, + "uploaded workflow defaults to un-published: " + returned.published ); + this.test.assert( utils.isArray( returned.tags ) && returned.tags.length === 0, + "upload returned an empty tag array: " + this.jsonStr( returned.tags ) ); + this.test.assert( returned.url === '/' + utils.format( this.api.workflows.urlTpls.show, returned.id ), + "url matches the show url: " + returned.url ); - //this.debug( this.jsonStr( uploadedFile ) ); - var uploadedFileId = uploadedFile.hdaElement.attributes.id.split( '-' )[1]; - this.debug( this.jsonStr( uploadedFileId ) ); - this.debug( this.jsonStr( this.api.hdas.show( currentHistory.id, uploadedFileId ) ) ); - //this.debug( this.jsonStr( firstWorkflow ) ); - // find the input step by looking for a step where input_steps == {} - var input_step = null; - for( var stepKey in firstWorkflow.steps ){ - if( firstWorkflow.steps.hasOwnProperty( stepKey ) ){ - var step = firstWorkflow.steps[ stepKey ]; - if( this.countKeys( step.input_steps ) === 0 ){ - input_step = stepKey; - this.debug( 'input step: ' + this.jsonStr( step ) ); - break; - } - } - } + // ------------------------------------------------------------------------------------------- INDEX + this.test.comment( 'index should get a list of workflows' ); + var workflowIndex = this.api.workflows.index(); + this.debug( this.jsonStr( workflowIndex ) ); + this.test.assert( utils.isArray( workflowIndex ), "index returned an array: length " + workflowIndex.length ); + this.test.assert( workflowIndex.length >= 1, "index returned at least one job" ); - // ------------------------------------------------------------------------------------------- CREATE - this.test.comment( 'create should allow running an existing workflow' ); - // needs workflow_id, history, ds_map - var executionData = { - workflow_id : firstWorkflow.id, - history : 'hist_id=' + currentHistory.id, - ds_map : {} - }; - executionData.ds_map[ input_step ] = { - src: 'hda', - id: uploadedFileId - }; - var returned = this.api.workflows.create( executionData ); - this.debug( this.jsonStr( returned ) ); - this.test.assert( utils.isObject( returned ), "create returned an object" ); - this.test.assert( this.hasKeys( returned, workflowCreateKeys ), "create returned the proper keys" ); - this.test.assert( this.api.isEncodedId( returned.history ), - "history id is proper: " + returned.history ); - this.test.assert( utils.isArray( returned.outputs ), - "create.outputs is an array: length " + returned.outputs.length ); - this.test.assert( returned.outputs.length >= 1, "there is at least one output" ); - for( var i=0; i= 1, "there is at least one output" ); + for( var i=0; i' + includeDeletedOptionsLabel + ' shows deleted datasets' ); - var uploadSelector = '#' + testUploadInfo.hdaElement.attributes.id; - - this.historyoptions.includeDeleted( function(){ - this.test.assertExists( uploadSelector, - "Deleted HDA is in the DOM (using history options -> " + includeDeletedOptionsLabel + ")" ); - this.test.assertVisible( uploadSelector, - "Deleted HDA is visible again (using history options -> " + includeDeletedOptionsLabel + ")" ); - }); -}); - -spaceghost.then( function(){ - this.test.comment( 'History options->' + includeDeletedOptionsLabel + ' (again) re-hides deleted datasets' ); - - this.historyoptions.excludeDeleted( function(){ - this.test.assertDoesntExist( '#' + testUploadInfo.hdaElement.attributes.id, - "Deleted HDA is not in the DOM (using history options -> " + includeDeletedOptionsLabel + ")" ); - }); -}); - -spaceghost.then( function(){ - // undelete the uploaded file - this.historyoptions.includeDeleted( function(){ - this.historypanel.undeleteHda( '#' + testUploadInfo.hdaElement.attributes.id ); - spaceghost.debug( 'undeleted' ); + // ------------------------------------------------------------------- set up + // start a new user and upload a file + spaceghost.user.loginOrRegisterUser( email, password ); + spaceghost.api.tools.thenUploadToCurrent({ filepath: filepathToUpload }, function uploadCallback( id, json ){ + uploadId = id; }); -}); + // ------------------------------------------------------------------- history options menu structure + //NOTE: options menu should be functionally tested elsewhere + spaceghost.openHomePage().historypanel.waitForHdas().then( function checkHistoryOptions(){ + this.test.comment( 'History options icon should be in place and menu should have the proper structure' ); -// ------------------------------------------------------------------- hidden hdas aren't shown -// ------------------------------------------------------------------- history options allows showing hidden hdas -// can't test this yet w/o a way to make hdas hidden thru the ui or api + // check the button and icon + this.test.assertExists( this.historyoptions.data.selectors.button, "Found history options button" ); + this.test.assertVisible( this.historyoptions.data.selectors.button, "History options button is visible" ); + this.test.assertVisible( this.historyoptions.data.selectors.buttonIcon, "History options icon is visible" ); + // open the menu + this.click( this.historyoptions.data.selectors.button ); + this.test.assertVisible( this.historyoptions.data.selectors.menu, + "Menu is visible when options button is clicked" ); -// ------------------------------------------------------------------- history options collapses all expanded hdas -spaceghost.then( function(){ - this.historypanel.thenExpandHda( '#' + testUploadInfo.hdaElement.attributes.id ); -}); -spaceghost.then( function(){ - this.test.comment( 'History option collapses all expanded hdas' ); + // check the options + var historyOptions = this.historyoptions.data.labels.options; + for( var optionKey in historyOptions ){ + if( historyOptions.hasOwnProperty( optionKey ) ){ + var optionLabel = historyOptions[ optionKey ]; + this.test.assertVisible( this.historyoptions.data.selectors.optionXpathByLabelFn( optionLabel ), + 'Option label is visible: ' + optionLabel ); + } + } - this.historyoptions.collapseExpanded( function(){ - var uploadedSelector = '#' + testUploadInfo.hdaElement.attributes.id; - this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, - "Body for uploaded file is not visible" ); + // clear the menu + this.click( 'body' ); + this.test.assertNotVisible( this.historyoptions.data.selectors.menu, + "Clicking away from the menu closes it" ); }); -}); -// =================================================================== + // ------------------------------------------------------------------- history options collapses all expanded hdas + spaceghost.then( function(){ + this.historypanel.thenExpandHda( '#dataset-' + uploadId ); + }); + spaceghost.then( function(){ + this.test.comment( 'History option collapses all expanded hdas' ); + + this.historyoptions.collapseExpanded( function(){ + var uploadedSelector = '#dataset-' + uploadId; + this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, + "Body for uploaded file is not visible" ); + }); + }); + + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/history-panel-tests.js b/test/casperjs/history-panel-tests.js index b4089f2b26d..627b2177f7e 100644 --- a/test/casperjs/history-panel-tests.js +++ b/test/casperjs/history-panel-tests.js @@ -7,278 +7,265 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Testing the form of the main/current history panel', 0, function suite( test ){ spaceghost.start(); -// =================================================================== -/* TODO: - possibly break this file up -*/ -// =================================================================== globals and helpers -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; - spaceghost.info( 'Will use fixtureData.testUser: ' + email ); -} + // =================================================================== + /* TODO: + possibly break this file up + */ + // =================================================================== globals and helpers + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + spaceghost.info( 'Will use fixtureData.testUser: ' + email ); + } -// selectors and labels -var tooltipSelector = spaceghost.data.selectors.tooltipBalloon, - editableTextClass = spaceghost.data.selectors.editableText, - editableTextInput = spaceghost.historypanel.data.selectors.history.nameEditableTextInput, + // selectors and labels + var tooltipSelector = spaceghost.data.selectors.tooltipBalloon, + editableTextClass = spaceghost.data.selectors.editableText, + editableTextInput = spaceghost.historypanel.data.selectors.history.nameEditableTextInput, - nameSelector = spaceghost.historypanel.data.selectors.history.name, - subtitleSelector = spaceghost.historypanel.data.selectors.history.subtitle, - sizeSelector = spaceghost.historypanel.data.selectors.history.size, - unnamedName = spaceghost.historypanel.data.text.history.newName, - initialSizeStr = spaceghost.historypanel.data.text.history.newSize, - tagIconSelector = spaceghost.historypanel.data.selectors.history.tagIcon, - annoIconSelector = spaceghost.historypanel.data.selectors.history.annoIcon, - emptyMsgSelector = spaceghost.historypanel.data.selectors.history.emptyMsg, - emptyMsgStr = spaceghost.historypanel.data.text.history.emptyMsg, - wrapperOkClassName = spaceghost.historypanel.data.selectors.hda.wrapper.stateClasses.ok, - tagAreaSelector = spaceghost.historypanel.data.selectors.history.tagArea, - annoAreaSelector = spaceghost.historypanel.data.selectors.history.annoArea, - nameTooltip = spaceghost.historypanel.data.text.history.tooltips.name, + nameSelector = spaceghost.historypanel.data.selectors.history.name, + sizeSelector = spaceghost.historypanel.data.selectors.history.size, + unnamedName = spaceghost.historypanel.data.text.history.newName, + initialSizeStr = spaceghost.historypanel.data.text.history.newSize, + tagIconSelector = spaceghost.historypanel.data.selectors.history.tagIcon, + annoIconSelector = spaceghost.historypanel.data.selectors.history.annoIcon, + emptyMsgSelector = spaceghost.historypanel.data.selectors.history.emptyMsg, + emptyMsgStr = spaceghost.historypanel.data.text.history.emptyMsg, + tagAreaSelector = spaceghost.historypanel.data.selectors.history.tagArea, + annoAreaSelector = spaceghost.historypanel.data.selectors.history.annoArea, + nameTooltip = spaceghost.historypanel.data.text.history.tooltips.name, - refreshButtonSelector = 'a#history-refresh-button', - refreshButtonIconSelector = 'span.fa-refresh', + refreshButtonSelector = 'a#history-refresh-button', + refreshButtonIconSelector = 'span.fa-refresh'; - includeDeletedOptionsLabel = spaceghost.historyoptions.data.labels.options.includeDeleted; - -// local -var newHistoryName = "Test History", - filepathToUpload = '../../test-data/1.txt', - historyFrameInfo = {}, - testUploadInfo = {}; + // local + var newHistoryName = "Test History", + filepathToUpload = '../../test-data/1.txt', + uploadId = null; -// =================================================================== TESTS -// ------------------------------------------------------------------- set up -// start a new user -spaceghost.user.loginOrRegisterUser( email, password ); + // =================================================================== TESTS + // ------------------------------------------------------------------- set up + // start a new user + spaceghost.user.loginOrRegisterUser( email, password ); -// ------------------------------------------------------------------- check structure of empty history -spaceghost.openHomePage().historypanel.waitForHdas( function(){ - this.test.comment( 'history panel with a new, empty history should be well formed' ); + // ------------------------------------------------------------------- check structure of empty history + spaceghost.openHomePage().historypanel.waitForHdas( function(){ + this.test.comment( 'history panel with a new, empty history should be well formed' ); - this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); - this.test.assertExists( nameSelector, nameSelector + ' exists' ); - this.test.assertVisible( nameSelector, 'History name is visible' ); - this.test.assertSelectorHasText( nameSelector, unnamedName, 'History name is ' + unnamedName ); + this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); + this.test.assertExists( nameSelector, nameSelector + ' exists' ); + this.test.assertVisible( nameSelector, 'History name is visible' ); + this.test.assertSelectorHasText( nameSelector, unnamedName, 'History name is ' + unnamedName ); - this.test.comment( "history should display size and size should be: " + initialSizeStr ); - this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); - this.test.assertVisible( sizeSelector, 'History size is visible' ); - this.test.assertSelectorHasText( sizeSelector, initialSizeStr, - 'History size has "' + initialSizeStr + '"' ); + this.test.comment( "history should display size and size should be: " + initialSizeStr ); + this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); + this.test.assertVisible( sizeSelector, 'History size is visible' ); + this.test.assertSelectorHasText( sizeSelector, initialSizeStr, + 'History size has "' + initialSizeStr + '"' ); - this.test.comment( "tags and annotation icons should be available" ); - this.test.assertExists( tagIconSelector, 'Tag icon button found' ); - this.test.assertExists( annoIconSelector, 'Annotation icon button found' ); + this.test.comment( "tags and annotation icons should be available" ); + this.test.assertExists( tagIconSelector, 'Tag icon button found' ); + this.test.assertExists( annoIconSelector, 'Annotation icon button found' ); - this.test.comment( "A message about the current history being empty should be displayed" ); - this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); - this.test.assertVisible( emptyMsgSelector, 'Empty history message is visible' ); - this.test.assertSelectorHasText( emptyMsgSelector, emptyMsgStr, - 'Message contains\n"' + emptyMsgStr + '":\n"' + this.fetchText( emptyMsgSelector ) + '"' ); -}); + this.test.comment( "A message about the current history being empty should be displayed" ); + this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); + this.test.assertVisible( emptyMsgSelector, 'Empty history message is visible' ); + this.test.assertSelectorHasText( emptyMsgSelector, emptyMsgStr, + 'Message contains\n"' + emptyMsgStr + '":\n"' + this.fetchText( emptyMsgSelector ) + '"' ); + }); -// ------------------------------------------------------------------- name editing -spaceghost.then( function(){ - this.test.comment( 'history panel, editing the history name' ); + // ------------------------------------------------------------------- name editing + spaceghost.then( function(){ + this.test.comment( 'history panel, editing the history name' ); - this.test.comment( 'name should have a tooltip with proper info on name editing' ); - this.hoverOver( nameSelector ); - this.test.assertExists( tooltipSelector, "Found tooltip after name hover" ); - this.test.assertSelectorHasText( tooltipSelector, nameTooltip ); - // clear the tooltip - this.page.sendEvent( 'mousemove', -1, -1 ); + this.test.comment( 'name should have a tooltip with proper info on name editing' ); + this.hoverOver( nameSelector ); + this.test.assertExists( tooltipSelector, "Found tooltip after name hover" ); + this.test.assertSelectorHasText( tooltipSelector, nameTooltip ); + // clear the tooltip + this.page.sendEvent( 'mousemove', -1, -1 ); - this.test.comment( 'name should be create an input when clicked' ); - this.assertHasClass( nameSelector, editableTextClass, "Name field classed for editable text" ); - this.click( nameSelector ); - spaceghost.debug( editableTextInput ); - this.test.assertExists( editableTextInput, "Clicking on name creates an input" ); + this.test.comment( 'name should be create an input when clicked' ); + this.assertHasClass( nameSelector, editableTextClass, "Name field classed for editable text" ); + this.click( nameSelector ); + spaceghost.debug( editableTextInput ); + this.test.assertExists( editableTextInput, "Clicking on name creates an input" ); - this.test.comment( 'name should be editable by entering keys and pressing enter' ); - //NOTE: casperjs.sendKeys adds a click before and a selector.blur after sending - won't work here - this.page.sendEvent( 'keypress', newHistoryName ); - this.page.sendEvent( 'keypress', this.page.event.key.Enter ); - // wait for send and re-render name - this.wait( 1000, function(){ + this.test.comment( 'name should be editable by entering keys and pressing enter' ); + //NOTE: casperjs.sendKeys adds a click before and a selector.blur after sending - won't work here + this.page.sendEvent( 'keypress', newHistoryName ); + this.page.sendEvent( 'keypress', this.page.event.key.Enter ); + // wait for send and re-render name + this.wait( 1000, function(){ + this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is ' + newHistoryName ); + this.test.assertDoesntExist( editableTextInput, "Input disappears after pressing enter" ); + }); + }); + + spaceghost.then( function(){ + this.test.comment( 'name should revert if user clicks away while editing' ); + + this.click( nameSelector ); + this.page.sendEvent( 'keypress', "Woodchipper metagenomics, Fargo, ND" ); + + this.page.sendEvent( 'mousedown', -1, -1 ); + this.wait( 1000, function(){ + this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is STILL ' + newHistoryName ); + this.test.assertDoesntExist( editableTextInput, "Input disappears after clicking away" ); + }); + }); + + spaceghost.then( function(){ + this.test.comment( 'name should revert if user hits ESC while editing' ); + + this.click( nameSelector ); + this.page.sendEvent( 'keypress', "Arsenic Bacteria" ); + + this.page.sendEvent( 'keypress', this.page.event.key.Escape ); + this.wait( 1000, function(){ + this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is STILL ' + newHistoryName ); + this.test.assertDoesntExist( editableTextInput, "Input disappears after hitting ESC" ); + }); + }); + + // ------------------------------------------------------------------- check structure of NON empty history + // upload file: 1.txt + spaceghost.api.tools.thenUploadToCurrent({ filepath: filepathToUpload }, function uploadCallback( id, json ){ + uploadId = id; + }); + + spaceghost.openHomePage().then( function checkPanelStructure(){ + this.test.comment( 'checking structure of non-empty panel' ); + + this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); + this.test.assertExists( nameSelector, nameSelector + ' exists' ); + this.test.assertVisible( nameSelector, 'History name is visible' ); this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is ' + newHistoryName ); - this.test.assertDoesntExist( editableTextInput, "Input disappears after pressing enter" ); + + var onetxtFilesize = require( 'fs' ).size( filepathToUpload ), + expectedSize = onetxtFilesize + ' bytes'; + this.test.comment( "history should display size and size should be " + onetxtFilesize + " bytes" ); + this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); + this.test.assertVisible( sizeSelector, 'History size is visible' ); + this.test.assertSelectorHasText( sizeSelector, expectedSize, + 'History size has "' + expectedSize + '": ' + this.fetchText( sizeSelector ).trim() ); + + this.test.comment( "tags and annotation icons should be available" ); + this.test.assertExists( tagIconSelector, 'Tag icon button found' ); + this.test.assertExists( annoIconSelector, 'Annotation icon button found' ); + + this.test.comment( "A message about the current history being empty should NOT be displayed" ); + this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); + this.test.assertNotVisible( emptyMsgSelector, 'Empty history message is NOT visible' ); }); -}); -spaceghost.then( function(){ - this.test.comment( 'name should revert if user clicks away while editing' ); + // ------------------------------------------------------------------- tags + // keeping this light here - better for its own test file + //TODO: check tooltips + spaceghost.then( function openTags(){ + this.test.comment( 'tag area should open when the history panel tag icon is clicked' ); - this.click( nameSelector ); - this.page.sendEvent( 'keypress', "Woodchipper metagenomics, Fargo, ND" ); - - this.page.sendEvent( 'mousedown', -1, -1 ); - this.wait( 1000, function(){ - this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is STILL ' + newHistoryName ); - this.test.assertDoesntExist( editableTextInput, "Input disappears after clicking away" ); + this.click( tagIconSelector ); + this.wait( 1000, function(){ + this.test.assertVisible( tagAreaSelector, 'Tag area is now displayed' ); + }); }); -}); + spaceghost.then( function closeAnnotation(){ + this.test.comment( 'annotation area should close when the history panel tag icon is clicked again' ); -spaceghost.then( function(){ - this.test.comment( 'name should revert if user hits ESC while editing' ); - - this.click( nameSelector ); - this.page.sendEvent( 'keypress', "Arsenic Bacteria" ); - - this.page.sendEvent( 'keypress', this.page.event.key.Escape ); - this.wait( 1000, function(){ - this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is STILL ' + newHistoryName ); - this.test.assertDoesntExist( editableTextInput, "Input disappears after hitting ESC" ); + this.click( tagIconSelector ); + this.wait( 1000, function(){ + this.test.assertNotVisible( tagAreaSelector, 'Tag area is now hidden' ); + }); }); -}); -// ------------------------------------------------------------------- check structure of NON empty history -// upload file: 1.txt -spaceghost.tools.uploadFile( filepathToUpload, function uploadCallback( _uploadInfo ){ - this.test.comment( 'uploaded file should appear in history' ); + // ------------------------------------------------------------------- annotation + // keeping this light here - better for its own test file + //TODO: check tooltips + spaceghost.then( function openAnnotation(){ + this.test.comment( 'annotation area should open when the history panel annotation icon is clicked' ); - this.debug( 'uploaded HDA info: ' + this.jsonStr( _uploadInfo ) ); - var hasHda = _uploadInfo.hdaElement, - hasClass = _uploadInfo.hdaElement.attributes[ 'class' ], - hasOkClass = _uploadInfo.hdaElement.attributes[ 'class' ].indexOf( wrapperOkClassName ) !== -1; - this.test.assert( ( hasHda && hasClass && hasOkClass ), "Uploaded file: " + _uploadInfo.filename ); - testUploadInfo = _uploadInfo; -}); - -spaceghost.then( function checkPanelStructure(){ - this.test.comment( 'checking structure of non-empty panel' ); - - this.test.comment( "history name should exist, be visible, and have text " + unnamedName ); - this.test.assertExists( nameSelector, nameSelector + ' exists' ); - this.test.assertVisible( nameSelector, 'History name is visible' ); - this.test.assertSelectorHasText( nameSelector, newHistoryName, 'History name is ' + newHistoryName ); - - var onetxtFilesize = require( 'fs' ).size( filepathToUpload ), - expectedSize = onetxtFilesize + ' bytes'; - this.test.comment( "history should display size and size should be " + onetxtFilesize + " bytes" ); - this.test.assertExists( sizeSelector, 'Found ' + sizeSelector ); - this.test.assertVisible( sizeSelector, 'History size is visible' ); - this.test.assertSelectorHasText( sizeSelector, expectedSize, - 'History size has "' + expectedSize + '": ' + this.fetchText( sizeSelector ).trim() ); - - this.test.comment( "tags and annotation icons should be available" ); - this.test.assertExists( tagIconSelector, 'Tag icon button found' ); - this.test.assertExists( annoIconSelector, 'Annotation icon button found' ); - - this.test.comment( "A message about the current history being empty should NOT be displayed" ); - this.test.assertExists( emptyMsgSelector, emptyMsgSelector + ' exists' ); - this.test.assertNotVisible( emptyMsgSelector, 'Empty history message is NOT visible' ); -}); - -// ------------------------------------------------------------------- tags -// keeping this light here - better for its own test file -//TODO: check tooltips -spaceghost.then( function openTags(){ - this.test.comment( 'tag area should open when the history panel tag icon is clicked' ); - - this.click( tagIconSelector ); - this.wait( 1000, function(){ - this.test.assertVisible( tagAreaSelector, 'Tag area is now displayed' ); + this.click( annoIconSelector ); + this.wait( 1000, function(){ + this.test.assertVisible( annoAreaSelector, 'Annotation area is now displayed' ); + }); }); -}); -spaceghost.then( function closeAnnotation(){ - this.test.comment( 'annotation area should close when the history panel tag icon is clicked again' ); + spaceghost.then( function closeAnnotation(){ + this.test.comment( 'annotation area should close when the history panel tag icon is clicked again' ); - this.click( tagIconSelector ); - this.wait( 1000, function(){ - this.test.assertNotVisible( tagAreaSelector, 'Tag area is now hidden' ); + this.click( annoIconSelector ); + this.wait( 1000, function(){ + this.test.assertNotVisible( annoAreaSelector, 'Annotation area is now hidden' ); + }); }); -}); -// ------------------------------------------------------------------- annotation -// keeping this light here - better for its own test file -//TODO: check tooltips -spaceghost.then( function openAnnotation(){ - this.test.comment( 'annotation area should open when the history panel annotation icon is clicked' ); + // ------------------------------------------------------------------- refresh button + spaceghost.then( function refreshButton(){ + this.test.comment( 'History panel should refresh when the history refresh icon is clicked' ); - this.click( annoIconSelector ); - this.wait( 1000, function(){ - this.test.assertVisible( annoAreaSelector, 'Annotation area is now displayed' ); + this.test.assertExists( refreshButtonSelector, "Found refresh button" ); + this.test.assertVisible( refreshButtonSelector, "Refresh button is visible" ); + this.test.assertVisible( refreshButtonSelector + ' ' + refreshButtonIconSelector, "Refresh icon is visible" ); + + //this.assertNavigationRequested( refreshButtonHref, "History refreshed when clicking refresh icon", function(){ + // this.click( refreshButtonSelector ); + //}); }); -}); -spaceghost.then( function closeAnnotation(){ - this.test.comment( 'annotation area should close when the history panel tag icon is clicked again' ); - this.click( annoIconSelector ); - this.wait( 1000, function(){ - this.test.assertNotVisible( annoAreaSelector, 'Annotation area is now hidden' ); + // ------------------------------------------------------------------- hdas can be expanded by clicking on the hda + // broken in webkit w/ jq 1.7 + spaceghost.historypanel.waitForHdas( function(){ + this.test.comment( 'HDAs can be expanded by clicking on the name' ); + var uploadedSelector = '#dataset-' + uploadId; + + this.click( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.title ); + this.wait( 1000, function(){ + this.test.assertVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, + "Body for uploaded file is visible" ); + }); }); -}); -// ------------------------------------------------------------------- refresh button -spaceghost.then( function refreshButton(){ - this.test.comment( 'History panel should refresh when the history refresh icon is clicked' ); + // ------------------------------------------------------------------- expanded hdas still expanded after refresh + spaceghost.then( function(){ + this.test.comment( 'Expanded hdas are still expanded after a refresh' ); + var uploadedSelector = '#dataset-' + uploadId; - this.test.assertExists( refreshButtonSelector, "Found refresh button" ); - this.test.assertVisible( refreshButtonSelector, "Refresh button is visible" ); - this.test.assertVisible( refreshButtonSelector + ' ' + refreshButtonIconSelector, "Refresh icon is visible" ); - - //this.assertNavigationRequested( refreshButtonHref, "History refreshed when clicking refresh icon", function(){ - // this.click( refreshButtonSelector ); - //}); -}); - -// ------------------------------------------------------------------- hdas can be expanded by clicking on the hda name -// broken in webkit w/ jq 1.7 -spaceghost.historypanel.waitForHdas( function(){ - this.test.comment( 'HDAs can be expanded by clicking on the name' ); - this.debug( this.jsonStr( testUploadInfo ) ); - var uploadedSelector = '#' + testUploadInfo.hdaElement.attributes.id; - - this.click( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.title ); - this.wait( 1000, function(){ - this.test.assertVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, - "Body for uploaded file is visible" ); + this.click( refreshButtonSelector ); + this.historypanel.waitForHdas( function(){ + this.test.assertVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, + "Body for uploaded file is visible" ); + }); + // this will break: webkit + jq 1.7 }); -}); -// ------------------------------------------------------------------- expanded hdas are still expanded after a refresh -spaceghost.then( function(){ - this.test.comment( 'Expanded hdas are still expanded after a refresh' ); - var uploadedSelector = '#' + testUploadInfo.hdaElement.attributes.id; + // ------------------------------------------------------------------- expanded hdas collapse by clicking name again + spaceghost.then( function(){ + this.test.comment( 'Expanded hdas collapse by clicking name again' ); + var uploadedSelector = '#dataset-' + uploadId; - this.click( refreshButtonSelector ); - this.historypanel.waitForHdas( function(){ - this.test.assertVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, - "Body for uploaded file is visible" ); + this.click( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.title ); + this.wait( 500, function(){ + this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, + "Body for uploaded file is not visible" ); + }); }); - // this will break: webkit + jq 1.7 -}); -// ------------------------------------------------------------------- expanded hdas collapse by clicking name again -spaceghost.then( function(){ - this.test.comment( 'Expanded hdas collapse by clicking name again' ); - var uploadedSelector = '#' + testUploadInfo.hdaElement.attributes.id; + // ------------------------------------------------------------------- collapsed hdas still collapsed after refresh + spaceghost.then( function(){ + this.test.comment( 'collapsed hdas still collapsed after a refresh' ); + var uploadedSelector = '#dataset-' + uploadId; - this.click( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.title ); - this.wait( 500, function(){ - this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, - "Body for uploaded file is not visible" ); + this.click( refreshButtonSelector ); + this.historypanel.waitForHdas( function(){ + this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, + "Body for uploaded file is not visible" ); + }); }); -}); - -// ------------------------------------------------------------------- collapsed hdas still collapsed after a refresh -spaceghost.then( function(){ - this.test.comment( 'collapsed hdas still collapsed after a refresh' ); - var uploadedSelector = '#' + testUploadInfo.hdaElement.attributes.id; - - this.click( refreshButtonSelector ); - this.historypanel.waitForHdas( function(){ - this.test.assertNotVisible( uploadedSelector + ' ' + this.historypanel.data.selectors.hda.body, - "Body for uploaded file is not visible" ); - }); -}); -/* -*/ -// =================================================================== + /* + */ + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/casperjs/modules/api.js b/test/casperjs/modules/api.js index 956d0f777d1..cdc8d683fa4 100644 --- a/test/casperjs/modules/api.js +++ b/test/casperjs/modules/api.js @@ -552,6 +552,7 @@ ToolsAPI.prototype.thenUpload = function thenUpload( historyId, options, then ){ var spaceghost = this.api.spaceghost, uploadedId; + // upload via the api spaceghost.then( function(){ var returned = this.api.tools.upload( historyId, options ); this.debug( 'returned: ' + this.jsonStr( returned ) ); @@ -559,20 +560,19 @@ ToolsAPI.prototype.thenUpload = function thenUpload( historyId, options, then ){ this.debug( 'uploadedId: ' + uploadedId ); }); - spaceghost.debug( '---------------------------------------------------------- timeout: ' + ( options.timeout || spaceghost.api.tools.DEFAULT_UPLOAD_TIMEOUT ) ); - spaceghost.debug( 'timeout: ' + options.timeout ); - spaceghost.debug( 'timeout: ' + spaceghost.api.tools.DEFAULT_UPLOAD_TIMEOUT ); + // wait for upload/queued/running to stop spaceghost.then( function(){ + var hda = null; this.waitFor( function testHdaState(){ - var hda = spaceghost.api.hdas.show( historyId, uploadedId ); + hda = spaceghost.api.hdas.show( historyId, uploadedId ); spaceghost.debug( spaceghost.jsonStr( hda.state ) ); return !( hda.state === 'upload' || hda.state === 'queued' || hda.state === 'running' ); }, function _then(){ spaceghost.info( 'upload finished: ' + uploadedId ); if( then ){ - then.call( spaceghost, uploadedId ); + then.call( spaceghost, uploadedId, hda ); } //var hda = spaceghost.api.hdas.show( historyId, uploadedId ); //spaceghost.debug( spaceghost.jsonStr( hda ) ); @@ -631,6 +631,16 @@ ToolsAPI.prototype.thenUploadMultiple = function thenUploadMultiple( historyId, }; +/** get the current history's id, then upload there */ +ToolsAPI.prototype.thenUploadToCurrent = function thenUploadToCurrent( options, then ){ + var spaceghost = this.api.spaceghost; + return spaceghost.then( function(){ + var currentHistoryId = this.api.histories.index()[0].id; + spaceghost.api.tools.thenUpload( currentHistoryId, options, then ); + }); +}; + + // =================================================================== WORKFLOWS var WorkflowsAPI = function WorkflowsAPI( api ){ this.api = api; diff --git a/test/casperjs/modules/historyoptions.js b/test/casperjs/modules/historyoptions.js index 0e83cb4486e..5f19a257640 100644 --- a/test/casperjs/modules/historyoptions.js +++ b/test/casperjs/modules/historyoptions.js @@ -147,19 +147,6 @@ HistoryOptions.prototype.toggle = function toggle( optionLabel, force ){ //}; -// ------------------------------------------------------------------- check the togglable options -// these are easy, one click options (they don't open a new page) -/** Is 'Include Deleted Datasets' on (accrd. to the menu)? - */ -HistoryOptions.prototype.deletedAreIncluded = function deletedAreIncluded(){ - return this.isOn( this.data.labels.options.includeDeleted ); -}; -/** Is 'Include Deleted Datasets' on (accrd. to the menu)? - */ -HistoryOptions.prototype.hiddenAreIncluded = function hiddenAreIncluded(){ - return this.isOn( this.data.labels.options.includeHidden ); -}; - // ------------------------------------------------------------------- options that control the hpanel /** corresponds to history options menu: 'Collapse Expanded Datasets' */ @@ -170,39 +157,6 @@ HistoryOptions.prototype.collapseExpanded = function collapseExpanded( then ){ }); }; -/** set 'Include Deleted Datasets' to on - * @param {Function} then casper step to run when option is set - */ -HistoryOptions.prototype.includeDeleted = function includeDeleted( then ){ - this.toggle( this.data.labels.options.includeDeleted, true ); - this.spaceghost.historypanel.waitForHdas( then ); -}; - -/** set 'Include Deleted Datasets' to off - * @param {Function} then casper step to run when option is set - */ -HistoryOptions.prototype.excludeDeleted = function excludeDeleted( then ){ - this.toggle( this.data.labels.options.includeDeleted, false ); - this.spaceghost.historypanel.waitForHdas( then ); -}; - -/** set 'Include Hidden Datasets' to on - * @param {Function} then casper step to run when option is set - */ -HistoryOptions.prototype.includeHidden = function includeHidden( then ){ - this.toggle( this.data.labels.options.includeHidden, true ); - this.spaceghost.historypanel.waitForHdas( then ); -}; - -/** set 'Include Hidden Datasets' to off - * @param {Function} then casper step to run when option is set - */ -HistoryOptions.prototype.excludeHidden = function excludeHidden( then ){ - this.toggle( this.data.labels.options.includeHidden, false ); - this.spaceghost.historypanel.waitForHdas( then ); -}; - - // =================================================================== SELECTORS //TODO: data is not a very good name HistoryOptions.prototype.data = { @@ -232,14 +186,13 @@ HistoryOptions.prototype.data = { datasetSecurity : "Dataset Security", resumePausedJobs : "Resume Paused Jobs", collapseExpanded : 'Collapse Expanded Datasets', - includeDeleted : 'Include Deleted Datasets', - includeHidden : 'Include Hidden Datasets', unhideHiddenDatasets : "Unhide Hidden Datasets", - purgeDeletedDatasets : "Purge Deleted Datasets", + deleteHiddenDatasets : "Delete Hidden Datasets", showStructure : "Show Structure", + exportCitations : "Export Citations", exportToFile : "Export to File", deleteHistory : "Delete", - deleteHistoryPermanently : "Delete Permanently", + //deleteHistoryPermanently : "Delete Permanently", //Other Actions importFromFile : "Import from File" } diff --git a/test/casperjs/upload-tests.js b/test/casperjs/upload-tests.js index 8617d524474..a6fb8a54a6c 100644 --- a/test/casperjs/upload-tests.js +++ b/test/casperjs/upload-tests.js @@ -6,87 +6,40 @@ var require = patchRequire( require ), spaceghost.test.begin( 'Test uploading data to a history', 0, function suite( test ){ spaceghost.start(); + // =================================================================== -// =================================================================== -/* TODO: + var email = spaceghost.user.getRandomEmail(), + password = '123456'; + if( spaceghost.fixtureData.testUser ){ + email = spaceghost.fixtureData.testUser.email; + password = spaceghost.fixtureData.testUser.password; + } - find a way to error on bad upload? - general tool execution -*/ -// =================================================================== globals and helpers -var email = spaceghost.user.getRandomEmail(), - password = '123456'; -if( spaceghost.fixtureData.testUser ){ - email = spaceghost.fixtureData.testUser.email; - password = spaceghost.fixtureData.testUser.password; -} - -var testUploadInfo = {}; - -// =================================================================== TESTS -// ------------------------------------------------------------------- start a new user -spaceghost.user.loginOrRegisterUser( email, password ).openHomePage( function(){ - var loggedInAs = spaceghost.user.loggedInAs(); - this.test.assert( loggedInAs === email, 'loggedInAs() matches email: "' + loggedInAs + '"' ); -}); - -// ------------------------------------------------------------------- long form -// upload a file... -spaceghost.then( function(){ - this.test.comment( 'Test uploading a file' ); - - var filename = '1.txt', - filepath = '../../test-data/' + filename; - this.tools._uploadFile( filepath ); - - // when an upload begins successfully main should reload with a infomessagelarge - this.withMainPanel( function mainAfterUpload(){ - var infoInfo = this.elementInfoOrNull( this.data.selectors.messages.infolarge ); - this.test.assert( infoInfo !== null, - "Found infomessagelarge after uploading file" ); - this.test.assert( infoInfo.text.indexOf( this.data.text.upload.success ) !== -1, - "Found upload success message: " + this.data.text.upload.success ); - - testUploadInfo.name = filename; + // ------------------------------------------------------------------- start a new user + spaceghost.user.loginOrRegisterUser( email, password ).openHomePage( function(){ + var loggedInAs = spaceghost.user.loggedInAs(); + this.test.assert( loggedInAs === email, 'loggedInAs() matches email: "' + loggedInAs + '"' ); }); -}); -// ... and move to the history panel and wait for upload to finish -spaceghost.historypanel.waitForHda( '1.txt', - function uploadComplete( hdaElement ){ - this.test.pass( 'Upload completed successfully for: ' + this.jsonStr( hdaElement.attributes.id ) ); - }, - function timeout( hdaElement ){ - this.debug( 'hdaElement:\n' + this.jsonStr( hdaElement ) ); - this.test.fail( 'Test timed out for upload: ' + testUploadInfo.name ); - }, - 30 * 1000 -); - -// ------------------------------------------------------------------- short form -spaceghost.then( function(){ - this.test.comment( 'Test convenience function' ); - - spaceghost.tools.uploadFile( '../../test-data/1.sam', function( uploadInfo ){ - this.test.assert( uploadInfo.hdaElement !== null, "Convenience function produced hda" ); - var state = this.historypanel.getHdaState( '#' + uploadInfo.hdaElement.attributes.id ); - this.test.assert( state === 'ok', "Convenience function produced hda in ok state" ); + spaceghost.api.tools.thenUploadToCurrent({ filepath: '../../test-data/1.sam' }, function( uploadedId, json ){ + var currentHistoryId = this.api.histories.index()[0].id, + contents = this.api.hdas.index( currentHistoryId ); + this.test.assert( contents.length === 1, 'found one hda in history' ); + this.test.assert( contents[0].id === uploadedId, 'id matches' ); + this.test.assert( contents[0].name === '1.sam', 'name matches' ); }); -}); + spaceghost.user.logout(); -// ------------------------------------------------------------------- test conv. fn error -/* -//??: this error's AND waitFor()s THREE times (or more) - something to do with assertStepsRaise + waitFor -spaceghost.then( function(){ - this.test.comment( 'testing convenience function timeout error' ); - this.assertStepsRaise( 'GalaxyError: Upload Error: timeout waiting', function(){ - spaceghost.tools.uploadFile( '../../test-data/1.sam', function( uploadInfo ){ - this.test.fail( "Convenience function did not timeout!" ); - }, 50 ); + // ------------------------------------------------------------------- anon user + spaceghost.openHomePage(); + spaceghost.api.tools.thenUploadToCurrent({ filepath: '../../test-data/1.bed' }, function( uploadedId, json ){ + var currentHistoryId = this.api.histories.index()[0].id, + contents = this.api.hdas.index( currentHistoryId ); + this.test.assert( contents.length === 1, 'found one hda in history' ); + this.test.assert( contents[0].id === uploadedId, 'id matches' ); + this.test.assert( contents[0].name === '1.bed', 'name matches' ); }); -}); -*/ -// =================================================================== + // =================================================================== spaceghost.run( function(){ test.done(); }); }); diff --git a/test/selenium/root/Login.html b/test/selenium/root/Login.html deleted file mode 100644 index 2b84f64ebf1..00000000000 --- a/test/selenium/root/Login.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - -Login - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Login
open/
clicklink=Login
selectFramegalaxy_main
typeemailtest${username}@test.com
typepasswordtesttest
clickAndWait//input[@value='Login']
verifyTextPresentNow logged in as test52396610364@test.com.
- - diff --git a/test/selenium/root/Suite.html b/test/selenium/root/Suite.html deleted file mode 100644 index 6869b4d73ea..00000000000 --- a/test/selenium/root/Suite.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Test Suite - - - - - - -
Test Suite
Register and logout
Login
- - diff --git a/test/selenium/root/UserRegistration.html b/test/selenium/root/UserRegistration.html deleted file mode 100644 index 9769d9046ef..00000000000 --- a/test/selenium/root/UserRegistration.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - -Register and logout - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Register and logout
open/
clicklink=Register
selectFramegalaxy_main
storejavascript{Math.round( Math.random() * 100000000000 )}username
typeemailtest${username}@test.com
typepasswordtest
typeconfirmtest
clickAndWait//input[@value='Create']
typepasswordtestest
verifyTextPresentPlease use a password of at least 6 characters
typeconfirmtest
clickAndWait//input[@value='Create']
verifyTextPresentPasswords do not match
typepasswordtesttest
typeconfirmtesttest
clickAndWait//input[@value='Create']
verifyTextPresentNow logged in as test${username}@test.com
selectFramerelative=up
clicklink=Logout
verifyTextPresentLogged in as test${username}@test.com
selectFramegalaxy_main
verifyTextPresentYou are no longer logged in.
selectFramerelative=up
verifyTextPresentLogin
- - diff --git a/test/selenium/visualization/Trackster.html b/test/selenium/visualization/Trackster.html deleted file mode 100644 index a2cc1806cc6..00000000000 --- a/test/selenium/visualization/Trackster.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - -Trackster - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Trackster
open/visualization/list
clickAndWaitlink=New Track Browser
typenew-titleTest
typenew-dbkeyHuman Mar. 2006 (NCBI36/hg18) (hg18)
click//div[@id='overlay']/table/tbody/tr/td/div/div/div[3]/div[1]/button[2]
verifyTextPresentTest (hg18)
selectchromlabel=chr21
verifyValue//div[@id='center']/div[5]/div[2]/div[2]/form/input[1]0
verifyValue//div[@id='center']/div[5]/div[2]/div[2]/form/input[2]46,944,323
- - diff --git a/test/selenium/workflow/CreateSimpleWorkflow.html b/test/selenium/workflow/CreateSimpleWorkflow.html deleted file mode 100644 index 5f52c55497e..00000000000 --- a/test/selenium/workflow/CreateSimpleWorkflow.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - -Create Simple Workflow - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create Simple Workflow
setSpeed100
open/workflow
selectFramegalaxy_main
clickAndWait//span
typeworkflow_nameTest workflow
clickAndWait//input[@value='Create']
clickwf-0-popup
clickAndWait//div[@id='wf-0-popup-menu']/div[1]
waitForNotVisibleoverlay
click//div[@id='title___workflow__input__']/a/span
clicklink=Input dataset
waitForElementPresent//div[@id='wf-node-step-1']/div[1]
verifyText//div[@id='wf-node-step-1']/div[1]Input Dataset
verifyText//div[@id='wf-node-step-1']/div[2]/div[2]output
click//div[@id='title_group']/a/span
clicklink-join1
waitForElementPresent//div[@id='wf-node-step-2']/div[1]
verifyText//div[@id='wf-node-step-2']/div[1]Join two Queries
verifyText//div[@id='wf-node-step-2']/div[2]/div[3]out_file1
assertElementPresent//div[@id='wf-node-step-1']/div[2]/div[2]/div
assertElementPresent//div[@id='wf-node-step-2']/div[2]/div[1]/div[1]/div
mouseDownAt//div[@id='wf-node-step-1']/div[2]/div[2]/div0,0
mouseMoveAt//div[@id='wf-node-step-2']/div[2]/div[1]/div[1]/div0,0
mouseUpAt//div[@id='wf-node-step-2']/div[2]/div[1]/div[1]/div0,0
clickAt//div[@id='wf-node-step-2']
typefield11
typefield21
selectunmatchedlabel=Yes
clicktool-form-save-button
waitForAttributetool-form-save-button@valueSave
clickclose-button
clickAndWait//div[@id='overlay']/table/tbody/tr/td/div/div/div[3]/div[1]/button[2]
selectFramegalaxy_main
verifyText//td[2]2
- - diff --git a/test/selenium/workflow/CreateUser.html b/test/selenium/workflow/CreateUser.html deleted file mode 100644 index 3beb4e1dabf..00000000000 --- a/test/selenium/workflow/CreateUser.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - -Create User - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create User
setSpeed100
open/
clickAndWaitlink=Register
selectFramegalaxy_main
storejavascript{Math.round( Math.random() * 100000000000 )}username
typeemailtest${username}@test.com
typepasswordtesttest
typeconfirmtesttest
clickAndWait//input[@value='Create']
verifyTextPresentNow logged in as test${username}@test.com
- - diff --git a/test/selenium/workflow/CreateWorkflowWithGroupingAndRuntimeParameters.html b/test/selenium/workflow/CreateWorkflowWithGroupingAndRuntimeParameters.html deleted file mode 100644 index 9650a4cba81..00000000000 --- a/test/selenium/workflow/CreateWorkflowWithGroupingAndRuntimeParameters.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - -Create Workflow With Grouping And Runtime Parameters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create Workflow With Grouping And Runtime Parameters
open/workflow
selectFramegalaxy_main
clickAndWait//span
typeworkflow_nameWorkflow with grouping
clickAndWait//input[@value='Create']
clickwf-0-popup
clickAndWait//div[@id='wf-0-popup-menu']/div[1]
click//div[@id='title___workflow__input__']/a/span
clicklink=Input dataset
waitForElementPresent//div[@id='wf-node-step-1']/div[1]
mouseDownAt//div[@id='wf-node-step-1']/div[1]
mouseMoveAt//div[@id='wf-node-step-1']/div[1]100,100
mouseUpAt//div[@id='wf-node-step-1']/div[1]100,100
click//div[@id='title_bxops']/a/span
clicklink-gops_complement_1
waitForElementPresent//div[@id='wf-node-step-2']/div[1]
mouseDownAt//div[@id='wf-node-step-2']/div[1]
mouseMoveAt//div[@id='wf-node-step-2']/div[1]200,200
mouseUpAt//div[@id='wf-node-step-2']/div[1]200,200
click//div[@id='title_plots']/a/span
clicklink-build_ucsc_custom_track_1
waitForElementPresent//div[@id='wf-node-step-3']/div[1]
mouseDownAt//div[@id='wf-node-step-3']/div[1]
mouseMoveAt//div[@id='wf-node-step-3']/div[1]300,300
mouseUpAt//div[@id='wf-node-step-3']/div[1]300,300
clicktracks_add
waitForElementPresent//div[@name='tracks_0|input']
clickpopup-arrow-for-row-tracks_0|name
click//div[@id='popup-arrow-for-row-tracks_0|name-menu']/div
verifyText//div[@id='row-tracks_0|name']/div[1]/iTo be set at runtime
clicktool-form-save-button
mouseDownAt//div[@id='wf-node-step-1']/div[2]/div[2]/div
mouseMoveAt//div[@id='wf-node-step-2']/div[2]/div[1]/div/div
mouseUpAt//div[@id='wf-node-step-2']/div[2]/div[1]/div/div
mouseDownAt//div[@id='wf-node-step-2']/div[2]/div[3]/div
mouseMoveAt//div[@id='wf-node-step-3']/div[2]/div[1]/div/div
mouseUpAt//div[@id='wf-node-step-3']/div[2]/div[1]/div/div
clicksave-button
waitForNotVisibleoverlay
clickAndWaitclose-button
- - diff --git a/test/selenium/workflow/RunWorkflowWithGroupingAndRuntimeParameters.html b/test/selenium/workflow/RunWorkflowWithGroupingAndRuntimeParameters.html deleted file mode 100644 index fb4b2dd52c8..00000000000 --- a/test/selenium/workflow/RunWorkflowWithGroupingAndRuntimeParameters.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - -Run Workflow with Grouping - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Run Workflow with Grouping
open/workflow
selectFramegalaxy_main
clickwf-0-popup
clickAndWait//div[@id='wf-0-popup-menu']/div[2]
selectFramegalaxy_main
type//form[@id='tool_form']/div[3]/div[2]/div/div[2]/div[3]/div[1]/input[1]Title set at runtime
select//form[@id='tool_form']/div[1]/div[2]/div/div[1]/selectlabel=1: 1.bed
clickAndWaitrun_workflow
verifyTextPresentMust have length no more than 15
type//form[@id='tool_form']/div[3]/div[2]/div/div[2]/div[3]/div[1]/input[1]Runtime
clickAndWaitrun_workflow
selectFramerelative=up
selectFramegalaxy_history
waitForTextPresentexact:85 regions, format: bed, database: ?
waitForTextPresentexact:Info: Generated a custom track containing 1 subtracks.
- - diff --git a/test/selenium/workflow/Suite.html b/test/selenium/workflow/Suite.html deleted file mode 100644 index c111996f61e..00000000000 --- a/test/selenium/workflow/Suite.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Test Suite - - - - - - - - - -
Test Suite
Create User
Create Simple Workflow
Verify / Run Simple Workflow
Create Workflow With Grouping And Runtime Parameters
Run Workflow with Grouping
- - diff --git a/test/selenium/workflow/VerifyRunSimpleWorkflow.html b/test/selenium/workflow/VerifyRunSimpleWorkflow.html deleted file mode 100644 index fb0eb03b484..00000000000 --- a/test/selenium/workflow/VerifyRunSimpleWorkflow.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - -Verify / Run Simple Workflow - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Verify / Run Simple Workflow
setSpeed100
selectFramerelative=up
open/
selectFramegalaxy_tools
click//div[@id='title_getext']/a/span
clickAndWaitlink-upload1
selectFramerelative=up
selectFramegalaxy_main
typefile_data/Users/james/projects/galaxy/code/galaxy-security/test-data/1.bed
click//form[@id='tool_form']/div[8]/input
selectFramerelative=up
selectFramegalaxy_history
waitForTextPresentexact:Info: uploaded bed file
selectFramerelative=up
clickAndWaitlink=Workflow
selectFramegalaxy_main
clickAtwf-0-popup
clickAndWait//div[@id='wf-0-popup-menu']/div[2]
selectFramegalaxy_main
verifyText//form[@id='tool_form']/div[2]/div[2]/div[1]/div[1]Output dataset 'output' from step 1
verifyText//form[@id='tool_form']/div[2]/div[2]/div[2]/div[1]1 (value not yet validated)
verifyText//form[@id='tool_form']/div[2]/div[2]/div[5]/div[1]Yes
verifyText//form[@id='tool_form']/div[2]/div[2]/div[6]/div[1]No
clickAndWaitrun_workflow
selectFramerelative=up
selectFramegalaxy_history
waitForElementPresentcss=div.historyItemWrapper b
verifyTextPresentJoin two Queries on
waitForTextPresent245 regions
- - From 656c04b77a151c0c75d128f125ed7611a78cb370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Tue, 23 Dec 2014 00:27:36 +0000 Subject: [PATCH 028/473] Activate arff datatype by default --- config/datatypes_conf.xml.sample | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index d092c7d6d0e..e247f22e727 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -3,7 +3,7 @@ - + @@ -299,7 +299,7 @@ - + From 99a2c1950fec944b040700ceeb52cce4a118db79 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 029/473] Make tool filters properties optional attributes of config. This way the tool shed doesn't need to pointlessly add them to its config object. --- lib/galaxy/tools/filters/__init__.py | 6 +++--- lib/galaxy/webapps/tool_shed/config.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tools/filters/__init__.py b/lib/galaxy/tools/filters/__init__.py index 53b48810eb8..04d72ff1e38 100644 --- a/lib/galaxy/tools/filters/__init__.py +++ b/lib/galaxy/tools/filters/__init__.py @@ -20,9 +20,9 @@ class FilterFactory( object ): self.default_filters = dict( tool=[ _not_hidden, _handle_requires_login ], section=[], label=[] ) # Add dynamic filters to these default filters. config = toolbox.app.config - self.__init_filters( "tool", config.tool_filters, self.default_filters ) - self.__init_filters( "section", config.tool_section_filters, self.default_filters ) - self.__init_filters( "label", config.tool_label_filters, self.default_filters ) + self.__init_filters( "tool", getattr( config, "tool_filters", "" ), self.default_filters ) + self.__init_filters( "section", getattr( config, "tool_section_filters", "" ), self.default_filters ) + self.__init_filters( "label", getattr( config, "tool_label_filters", "" ), self.default_filters ) def build_filters( self, trans, **kwds ): """ diff --git a/lib/galaxy/webapps/tool_shed/config.py b/lib/galaxy/webapps/tool_shed/config.py index 4d097227720..4acf6a5bc68 100644 --- a/lib/galaxy/webapps/tool_shed/config.py +++ b/lib/galaxy/webapps/tool_shed/config.py @@ -50,9 +50,6 @@ class Configuration( object ): self.test_conf = resolve_path( kwargs.get( "test_conf", "" ), self.root ) self.id_secret = kwargs.get( "id_secret", "USING THE DEFAULT IS NOT SECURE!" ) # Tool stuff - self.tool_filters = listify( kwargs.get( "tool_filters", [] ) ) - self.tool_label_filters = listify( kwargs.get( "tool_label_filters", [] ) ) - self.tool_section_filters = listify( kwargs.get( "tool_section_filters", [] ) ) self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root ) self.tool_secret = kwargs.get( "tool_secret", "" ) self.tool_data_path = resolve_path( kwargs.get( "tool_data_path", "shed-tool-data" ), os.getcwd() ) From cc57715b15bc1ab99a337b9da4dfc6a8951941ba Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 030/473] Make use_tool_dependencies an optional attribute of config. This way the tool shed doesn't need to pointlessly configure this property to create a toolbox. --- lib/galaxy/tools/deps/__init__.py | 2 +- lib/galaxy/webapps/tool_shed/config.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/galaxy/tools/deps/__init__.py b/lib/galaxy/tools/deps/__init__.py index 3f4944b02e1..f72b917ba1b 100644 --- a/lib/galaxy/tools/deps/__init__.py +++ b/lib/galaxy/tools/deps/__init__.py @@ -14,7 +14,7 @@ from galaxy.util import plugin_config def build_dependency_manager( config ): - if config.use_tool_dependencies: + if getattr( config, "use_tool_dependencies", False ): dependency_manager_kwds = { 'default_base_path': config.tool_dependency_dir, 'conf_file': config.dependency_resolvers_config_file, diff --git a/lib/galaxy/webapps/tool_shed/config.py b/lib/galaxy/webapps/tool_shed/config.py index 4acf6a5bc68..78fb2e7e67f 100644 --- a/lib/galaxy/webapps/tool_shed/config.py +++ b/lib/galaxy/webapps/tool_shed/config.py @@ -59,13 +59,6 @@ class Configuration( object ): self.ftp_upload_dir = kwargs.get( 'ftp_upload_dir', None ) # Install and test framework for testing tools contained in repositories. self.num_tool_test_results_saved = kwargs.get( 'num_tool_test_results_saved', 5 ) - # Location for dependencies - if 'tool_dependency_dir' in kwargs: - self.tool_dependency_dir = resolve_path( kwargs.get( "tool_dependency_dir" ), self.root ) - self.use_tool_dependencies = True - else: - self.tool_dependency_dir = None - self.use_tool_dependencies = False self.update_integrated_tool_panel = False # Galaxy flavor Docker Image self.enable_galaxy_flavor_docker_image = string_as_bool( kwargs.get( "enable_galaxy_flavor_docker_image", "False" ) ) From 82533c33e426d04e6fa0072a19da73b578f7c601 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 031/473] Update XML library use during tool parsing to elimiante FutureWarnings. --- lib/galaxy/tools/__init__.py | 10 +++++----- lib/galaxy/tools/parameters/basic.py | 2 +- lib/galaxy/tools/parameters/input_translation.py | 4 ++-- lib/galaxy/tools/parameters/output.py | 2 +- lib/galaxy/tools/parser/xml.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 8b1a2af38d8..8a23a502b03 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -742,13 +742,13 @@ class ToolBox( object, Dictifiable ): root = getattr( tool_source, "root", None ) # TODO: mucking with the XML directly like this is terrible, # modify inputs directly post load if possible. - if root and hasattr( self.app, "job_config" ): # toolshed may not have job_config? - tool_id = root.get( 'id' ) if root else None + if root is not None and hasattr( self.app, "job_config" ): # toolshed may not have job_config? + tool_id = root.get( 'id' ) parameters = self.app.job_config.get_tool_resource_parameters( tool_id ) if parameters: inputs = root.find('inputs') # If tool has not inputs, create some so we can insert conditional - if not inputs: + if inputs is None: inputs = ElementTree.fromstring( "") root.append( inputs ) # Insert a conditional allowing user to specify resource parameters. @@ -1563,7 +1563,7 @@ class Tool( object, Dictifiable ): root = tool_source.root conf_parent_elem = root.find("configfiles") - if conf_parent_elem: + if conf_parent_elem is not None: for conf_elem in conf_parent_elem.findall( "configfile" ): name = conf_elem.get( "name" ) filename = conf_elem.get( "filename", None ) @@ -1697,7 +1697,7 @@ class Tool( object, Dictifiable ): root = tool_source.root citations = [] citations_elem = root.find("citations") - if not citations_elem: + if citations_elem is None: return citations for citation_elem in citations_elem: diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 0abbb936bb6..7773fdd1175 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -49,7 +49,7 @@ class ToolParameter( object, Dictifiable ): self.label = input_source.parse_label() self.help = input_source.parse_help() sanitizer_elem = input_source.parse_sanitizer_elem() - if sanitizer_elem: + if sanitizer_elem is not None: self.sanitizer = ToolParameterSanitizer.from_element( sanitizer_elem ) else: self.sanitizer = None diff --git a/lib/galaxy/tools/parameters/input_translation.py b/lib/galaxy/tools/parameters/input_translation.py index 6c4b9051679..bb2f99dbe73 100644 --- a/lib/galaxy/tools/parameters/input_translation.py +++ b/lib/galaxy/tools/parameters/input_translation.py @@ -59,7 +59,7 @@ class ToolInputTranslator( object ): append_param = None value_trans_elem = req_param.find( 'value_translation' ) - if value_trans_elem: + if value_trans_elem is not None: for value_elem in value_trans_elem.findall( 'value' ): remote_value = value_elem.get( "remote_value" ) galaxy_value = value_elem.get( "galaxy_value" ) @@ -67,7 +67,7 @@ class ToolInputTranslator( object ): value_trans[ remote_value ] = galaxy_value append_param_elem = req_param.find( "append_param" ) - if append_param_elem: + if append_param_elem is not None: separator = append_param_elem.get( 'separator', ',' ) first_separator = append_param_elem.get( 'first_separator', None ) join_str = append_param_elem.get( 'join', '=' ) diff --git a/lib/galaxy/tools/parameters/output.py b/lib/galaxy/tools/parameters/output.py index 0ef856ecb4d..7513014fdb5 100644 --- a/lib/galaxy/tools/parameters/output.py +++ b/lib/galaxy/tools/parameters/output.py @@ -15,7 +15,7 @@ class ToolOutputActionGroup( object ): def __init__( self, parent, config_elem ): self.parent = parent self.actions = [] - if config_elem: + if config_elem is not None: for elem in config_elem: if elem.tag == "conditional": self.actions.append( ToolOutputActionConditional( self, elem ) ) diff --git a/lib/galaxy/tools/parser/xml.py b/lib/galaxy/tools/parser/xml.py index 10cb32b614b..ad4ef04fb27 100644 --- a/lib/galaxy/tools/parser/xml.py +++ b/lib/galaxy/tools/parser/xml.py @@ -604,7 +604,7 @@ class XmlPagesSource(PagesSource): def __init__(self, root): self.input_elem = root.find("inputs") page_sources = [] - if self.input_elem: + if self.input_elem is not None: pages_elem = self.input_elem.findall( "page" ) for page in ( pages_elem or [ self.input_elem ] ): page_sources.append(XmlPageSource(page)) From d2081e1feafee8339aa27fc6c07199e72ed5bc00 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 032/473] Start work on unit tests for galaxy.tools.ToolBox. --- test/unit/tools/test_toolbox.py | 75 +++++++++++++++++++++++++++++++++ test/unit/tools_support.py | 1 + 2 files changed, 76 insertions(+) create mode 100644 test/unit/tools/test_toolbox.py diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py new file mode 100644 index 00000000000..f1f6607d1b7 --- /dev/null +++ b/test/unit/tools/test_toolbox.py @@ -0,0 +1,75 @@ +import os +import unittest + +from galaxy.tools import ToolBox +from galaxy.model.tool_shed_install import mapping +import tools_support + + +class ToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.UsesTools ): + + def test_load_file( self ): + self._init_tool() + self.__add_config( """""" ) + + toolbox = self.toolbox + assert toolbox.get_tool( "test_tool" ) is not None + assert toolbox.get_tool( "not_a_test_tool" ) is None + + def test_load_file_in_section( self ): + self._init_tool() + self.__add_config( """

""" ) + + toolbox = self.toolbox + assert toolbox.get_tool( "test_tool" ) is not None + assert toolbox.get_tool( "not_a_test_tool" ) is None + + def test_writes_integrate_tool_panel( self ): + self._init_tool() + self.__add_config( """""" ) + + itp = os.path.join(self.test_directory, "integrated_tool_panel.xml") + + assert not os.path.exists( itp ) + self.toolbox + assert os.path.exists( itp ) + + def test_groups_tools( self ): + self._init_tool() + self.__add_config( """""" ) + + def setUp( self ): + self.setup_app( mock_model=False ) + itp_config = os.path.join(self.test_directory, "integrated_tool_panel.xml") + self.app.config.integrated_tool_panel_config = itp_config + self.__toolbox = None + self.config_files = [] + + @property + def toolbox( self ): + if self.__toolbox is None: + self.__toolbox = SimplifiedToolBox( self ) + return self.__toolbox + + def __add_config( self, xml, name="tool_conf.xml" ): + path = os.path.join( self.test_directory, "tool_conf.xml" ) + with open( path, "w" ) as f: + f.write( xml ) + self.config_files.append( path ) + + +class SimplifiedToolBox( ToolBox ): + + def __init__( self, test_case ): + app = test_case.app + # Handle app/config stuff needed by toolbox but not by tools. + app.install_model = mapping.init( "sqlite:///:memory:", create_tables=True ) + app.job_config.get_tool_resource_parameters = lambda tool_id: None + app.config.update_integrated_tool_panel = True + config_files = test_case.config_files + tool_root_dir = test_case.test_directory + super( SimplifiedToolBox, self ).__init__( + config_files, + tool_root_dir, + app, + ) diff --git a/test/unit/tools_support.py b/test/unit/tools_support.py index 3ecd246ccbf..b89268a82da 100644 --- a/test/unit/tools_support.py +++ b/test/unit/tools_support.py @@ -131,6 +131,7 @@ class MockApp( object ): self.tool_data_tables = {} self.dataset_collections_service = None self.container_finder = NullContainerFinder() + self.name = "galaxy" class MockContext(object): From 41628b3fafb987b2291b20f38ddd679d87649dd8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 033/473] PEP-8 fixes to tool_shed.galaxy_install.tools.tool_panel_manager. --- lib/tool_shed/galaxy_install/tools/tool_panel_manager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index e966fab1d15..dd68bd37f26 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -1,5 +1,4 @@ import logging -import os import threading import galaxy.tools @@ -355,7 +354,7 @@ class ToolPanelManager( object ): # If tool_panel_section_id is received, the section exists in the tool panel. In this # case, the value of the received tool_panel_section_id must be the id retrieved from a # tool panel config (e.g., tool_conf.xml, which may have getext). If new_tool_panel_section_label - # is received, a new section will be added to the tool panel. + # is received, a new section will be added to the tool panel. if new_tool_panel_section_label: section_id = str( new_tool_panel_section_label.lower().replace( ' ', '_' ) ) tool_panel_section_key, tool_section = \ @@ -396,7 +395,6 @@ class ToolPanelManager( object ): tool_section_dicts = tool_panel_dict[ tool_panel_dict.keys()[ 0 ] ] tool_section_dict = tool_section_dicts[ 0 ] original_section_id = tool_section_dict[ 'id' ] - original_section_name = tool_section_dict[ 'name' ] if original_section_id: tool_panel_section_key, tool_section = \ self.get_or_create_tool_section( toolbox, From 540b6edacd17076fd0764386a74146eeda7d826e Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 034/473] Introduce abstraction for reindexing tool search. Fixes re-indexing after tool shed modifications for 5c44ac1 and reduces duplication. --- lib/galaxy/config.py | 13 ++++++++----- .../galaxy_install/tools/tool_panel_manager.py | 5 ++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index bf1cca19dee..f93c2217419 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -706,10 +706,7 @@ class ConfiguresGalaxyMixin: from galaxy import tools self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) - # Search support for tools - import galaxy.tools.search - index_help = getattr( self.config, "index_tool_help", True ) - self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox, index_help ) + self.reindex_tool_search() from galaxy.tools.deps import containers galaxy_root_dir = os.path.abspath(self.config.root) @@ -720,7 +717,13 @@ class ConfiguresGalaxyMixin: outputs_to_working_directory=self.config.outputs_to_working_directory, container_image_cache_path=self.config.container_image_cache_path, ) - self.container_finder = galaxy.tools.deps.containers.ContainerFinder(app_info) + self.container_finder = containers.ContainerFinder(app_info) + + def reindex_tool_search( self ): + # Call this when tools are added or removed. + import galaxy.tools.search + index_help = getattr( self.config, "index_tool_help", True ) + self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox, index_help ) def _configure_tool_data_tables( self, from_shed_config ): from galaxy.tools.data import ToolDataTableManager diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index dd68bd37f26..5b50cb00560 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -2,7 +2,6 @@ import logging import threading import galaxy.tools -from galaxy.tools.search import ToolBoxSearch from xml.etree import ElementTree as XmlET from tool_shed.util import basic_util @@ -81,7 +80,7 @@ class ToolPanelManager( object ): if self.app.config.update_integrated_tool_panel: # Write the current in-memory version of the integrated_tool_panel.xml file to disk. self.app.toolbox.write_integrated_tool_panel_config_file() - self.app.toolbox_search = ToolBoxSearch( self.app.toolbox ) + self.app.reindex_tool_search() def config_elems_to_xml_file( self, config_elems, config_filename, tool_path ): """ @@ -571,7 +570,7 @@ class ToolPanelManager( object ): # Update the config_elems of the in-memory shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems self.app.toolbox.shed_tool_confs[ index ] = shed_tool_conf_dict - self.app.toolbox_search = ToolBoxSearch( self.app.toolbox ) + self.app.reindex_tool_search() if uninstall and self.app.config.update_integrated_tool_panel: # Write the current in-memory version of the integrated_tool_panel.xml file to disk. self.app.toolbox.write_integrated_tool_panel_config_file() From 1acda2715fdeefdc5608d5e0522e96573cbdf5fb Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 13:52:39 -0500 Subject: [PATCH 035/473] Introduce abstraction for updating ToolBox's shed configs. Hides some details of writing out integrated tool panel, reindexing, etc... from tool shed install code and reduces duplicatation in tool shed's tool_panel_manager. Add unit tests. --- lib/galaxy/tools/__init__.py | 12 +++++ .../tools/tool_panel_manager.py | 12 +---- test/unit/tools/test_toolbox.py | 46 +++++++++++++++++-- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 8a23a502b03..cd6774e0a34 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -244,6 +244,18 @@ class ToolBox( object, Dictifiable ): return shed_config_dict return default + def update_shed_config( self, shed_conf_index, shed_conf, integrated_panel_changes=True ): + """ Update the in-memory descriptions of tools and write out the changes + to integrated tool panel unless we are just deactivating a tool (since + that doesn't affect that file). + """ + app = self.app + self.shed_tool_confs[ shed_conf_index ] = shed_conf + if integrated_panel_changes and app.config.update_integrated_tool_panel: + # Write the current in-memory version of the integrated_tool_panel.xml file to disk. + self.write_integrated_tool_panel_config_file() + app.reindex_tool_search() + def __resolve_tool_path(self, tool_path, config_filename): if not tool_path: # Default to backward compatible config setting. diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 5b50cb00560..584891a34c9 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -76,11 +76,7 @@ class ToolPanelManager( object ): guid=guid ) # Replace the old list of in-memory config_elems with the new list for this shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems - self.app.toolbox.shed_tool_confs[ index ] = shed_tool_conf_dict - if self.app.config.update_integrated_tool_panel: - # Write the current in-memory version of the integrated_tool_panel.xml file to disk. - self.app.toolbox.write_integrated_tool_panel_config_file() - self.app.reindex_tool_search() + self.app.toolbox.update_shed_config( index, shed_tool_conf_dict ) def config_elems_to_xml_file( self, config_elems, config_filename, tool_path ): """ @@ -569,8 +565,4 @@ class ToolPanelManager( object ): config_elems.remove( config_elem ) # Update the config_elems of the in-memory shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems - self.app.toolbox.shed_tool_confs[ index ] = shed_tool_conf_dict - self.app.reindex_tool_search() - if uninstall and self.app.config.update_integrated_tool_panel: - # Write the current in-memory version of the integrated_tool_panel.xml file to disk. - self.app.toolbox.write_integrated_tool_panel_config_file() + self.app.toolbox.update_shed_config( index, shed_tool_conf_dict, integrated_panel_changes=uninstall ) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index f1f6607d1b7..367382eba61 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -28,23 +28,53 @@ class ToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.U self._init_tool() self.__add_config( """""" ) - itp = os.path.join(self.test_directory, "integrated_tool_panel.xml") - - assert not os.path.exists( itp ) + self.assert_integerated_tool_panel(exists=False) self.toolbox - assert os.path.exists( itp ) + self.assert_integerated_tool_panel(exists=True) def test_groups_tools( self ): self._init_tool() self.__add_config( """""" ) + def test_update_shed_conf(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {} ) + assert self.reindexed + self.assert_integerated_tool_panel(exists=True) + + def test_update_shed_conf_deactivate_only(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) + assert self.reindexed + # No changes, should be regenerated + self.assert_integerated_tool_panel(exists=False) + def setUp( self ): + self.reindexed = False self.setup_app( mock_model=False ) + self.app.reindex_tool_search = self.__reindex itp_config = os.path.join(self.test_directory, "integrated_tool_panel.xml") self.app.config.integrated_tool_panel_config = itp_config self.__toolbox = None self.config_files = [] + def __reindex( self ): + self.reindexed = True + + def __remove_itp( self ): + os.remove( os.path) + + @property + def integerated_tool_panel_path( self ): + return os.path.join(self.test_directory, "integrated_tool_panel.xml") + + def assert_integerated_tool_panel( self, exists=True ): + does_exist = os.path.exists( self.integerated_tool_panel_path ) + if exists: + assert does_exist + else: + assert not does_exist + @property def toolbox( self ): if self.__toolbox is None: @@ -57,6 +87,14 @@ class ToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.U f.write( xml ) self.config_files.append( path ) + def __setup_shed_tool_conf( self ): + self.__add_config( """""" ) + + self.toolbox # create toolbox + assert not self.reindexed + + os.remove( self.integerated_tool_panel_path ) + class SimplifiedToolBox( ToolBox ): From b292873de6eb6a8ee3f7277a8b808604ea665c63 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 14:22:08 -0500 Subject: [PATCH 036/473] Move ToolPanelManager operation to create or get section into ToolBox. Now with unit test. Part of broader effort to make ToolBox interface more explicit, tested, and hide implementation details from other parts of code (such as ToolPanelManager). Also reworked the code to now build up and re-parse XML structures - just use a dictionary - thanks to 34b3e1c. --- lib/galaxy/tools/__init__.py | 21 +++ .../tools/tool_panel_manager.py | 19 +-- test/unit/tools/test_toolbox.py | 121 +++++++++--------- 3 files changed, 84 insertions(+), 77 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index cd6774e0a34..e2740af7bcc 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -256,6 +256,27 @@ class ToolBox( object, Dictifiable ): self.write_integrated_tool_panel_config_file() app.reindex_tool_search() + def get_or_create_section( self, section_id, new_label=None ): + tool_panel_section_key = str( section_id ) + if tool_panel_section_key in self.tool_panel: + # Appending a tool to an existing section in toolbox.tool_panel + tool_section = self.tool_panel[ tool_panel_section_key ] + log.debug( "Appending to tool panel section: %s" % str( tool_section.name ) ) + else: + # Appending a new section to toolbox.tool_panel + if new_label is None: + # This might add an ugly section label to the tool panel, but, oh well... + new_label = section_id + section_dict = { + 'name': new_label, + 'id': section_id, + 'version': '', + } + tool_section = ToolSection( section_dict ) + self.tool_panel[ tool_panel_section_key ] = tool_section + log.debug( "Loading new tool panel section: %s" % str( tool_section.name ) ) + return tool_panel_section_key, tool_section + def __resolve_tool_path(self, tool_path, config_filename): if not tool_path: # Default to backward compatible config setting. diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 584891a34c9..a3e436131bb 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -312,24 +312,7 @@ class ToolPanelManager( object ): return tool_section def get_or_create_tool_section( self, toolbox, tool_panel_section_id, new_tool_panel_section_label=None ): - tool_panel_section_key = str( tool_panel_section_id ) - if tool_panel_section_key in toolbox.tool_panel: - # Appending a tool to an existing section in toolbox.tool_panel - tool_section = toolbox.tool_panel[ tool_panel_section_key ] - log.debug( "Appending to tool panel section: %s" % str( tool_section.name ) ) - else: - # Appending a new section to toolbox.tool_panel - if new_tool_panel_section_label is None: - # This might add an ugly section label to the tool panel, but, oh well... - new_tool_panel_section_label = tool_panel_section_id - elem = XmlET.Element( 'section' ) - elem.attrib[ 'name' ] = new_tool_panel_section_label - elem.attrib[ 'id' ] = tool_panel_section_id - elem.attrib[ 'version' ] = '' - tool_section = galaxy.tools.ToolSection( elem ) - toolbox.tool_panel[ tool_panel_section_key ] = tool_section - log.debug( "Loading new tool panel section: %s" % str( tool_section.name ) ) - return tool_panel_section_key, tool_section + return toolbox.get_or_create_section( section_id=tool_panel_section_id, new_label=new_tool_panel_section_label ) def get_shed_tool_conf_dict( self, shed_tool_conf ): """ diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 367382eba61..5581448c156 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -6,63 +6,7 @@ from galaxy.model.tool_shed_install import mapping import tools_support -class ToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.UsesTools ): - - def test_load_file( self ): - self._init_tool() - self.__add_config( """""" ) - - toolbox = self.toolbox - assert toolbox.get_tool( "test_tool" ) is not None - assert toolbox.get_tool( "not_a_test_tool" ) is None - - def test_load_file_in_section( self ): - self._init_tool() - self.__add_config( """
""" ) - - toolbox = self.toolbox - assert toolbox.get_tool( "test_tool" ) is not None - assert toolbox.get_tool( "not_a_test_tool" ) is None - - def test_writes_integrate_tool_panel( self ): - self._init_tool() - self.__add_config( """""" ) - - self.assert_integerated_tool_panel(exists=False) - self.toolbox - self.assert_integerated_tool_panel(exists=True) - - def test_groups_tools( self ): - self._init_tool() - self.__add_config( """""" ) - - def test_update_shed_conf(self): - self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {} ) - assert self.reindexed - self.assert_integerated_tool_panel(exists=True) - - def test_update_shed_conf_deactivate_only(self): - self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) - assert self.reindexed - # No changes, should be regenerated - self.assert_integerated_tool_panel(exists=False) - - def setUp( self ): - self.reindexed = False - self.setup_app( mock_model=False ) - self.app.reindex_tool_search = self.__reindex - itp_config = os.path.join(self.test_directory, "integrated_tool_panel.xml") - self.app.config.integrated_tool_panel_config = itp_config - self.__toolbox = None - self.config_files = [] - - def __reindex( self ): - self.reindexed = True - - def __remove_itp( self ): - os.remove( os.path) +class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.UsesTools ): @property def integerated_tool_panel_path( self ): @@ -81,14 +25,73 @@ class ToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.U self.__toolbox = SimplifiedToolBox( self ) return self.__toolbox - def __add_config( self, xml, name="tool_conf.xml" ): + def setUp( self ): + self.reindexed = False + self.setup_app( mock_model=False ) + self.app.reindex_tool_search = self.__reindex + itp_config = os.path.join(self.test_directory, "integrated_tool_panel.xml") + self.app.config.integrated_tool_panel_config = itp_config + self.__toolbox = None + self.config_files = [] + + def _add_config( self, xml, name="tool_conf.xml" ): path = os.path.join( self.test_directory, "tool_conf.xml" ) with open( path, "w" ) as f: f.write( xml ) self.config_files.append( path ) + def __reindex( self ): + self.reindexed = True + + +class ToolBoxTestCase( BaseToolBoxTestCase ): + + def test_load_file( self ): + self._init_tool() + self._add_config( """""" ) + + toolbox = self.toolbox + assert toolbox.get_tool( "test_tool" ) is not None + assert toolbox.get_tool( "not_a_test_tool" ) is None + + def test_load_file_in_section( self ): + self._init_tool() + self._add_config( """
""" ) + + toolbox = self.toolbox + assert toolbox.get_tool( "test_tool" ) is not None + assert toolbox.get_tool( "not_a_test_tool" ) is None + + def test_writes_integrate_tool_panel( self ): + self._init_tool() + self._add_config( """""" ) + + self.assert_integerated_tool_panel(exists=False) + self.toolbox + self.assert_integerated_tool_panel(exists=True) + + def test_groups_tools( self ): + self._init_tool() + self._add_config( """""" ) + + def test_update_shed_conf(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {} ) + assert self.reindexed + self.assert_integerated_tool_panel(exists=True) + + def test_update_shed_conf_deactivate_only(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) + assert self.reindexed + # No changes, should be regenerated + self.assert_integerated_tool_panel(exists=False) + + def __remove_itp( self ): + os.remove( os.path) + def __setup_shed_tool_conf( self ): - self.__add_config( """""" ) + self._add_config( """""" ) self.toolbox # create toolbox assert not self.reindexed From 00f68131df1b78acd42db90dd85a17ba5468da31 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 21:42:37 -0500 Subject: [PATCH 037/473] More tool panel manager unit tests. (Including adding file missing with previous commit 697b58f0.) --- .../tools/tool_panel_manager.py | 2 + .../test_tool_panel_manager.py | 73 +++++++++++++++++++ test/unit/tools/test_toolbox.py | 8 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 test/unit/tool_shed_unit_tests/test_tool_panel_manager.py diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index a3e436131bb..84cf73b2e24 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -105,6 +105,8 @@ class ToolPanelManager( object ): else: tool_elem = XmlET.Element( 'tool' ) tool_elem.attrib[ 'file' ] = tool_file_path + if not tool.guid: + raise ValueError("tool has no guid") tool_elem.attrib[ 'guid' ] = tool.guid tool_shed_elem = XmlET.SubElement( tool_elem, 'tool_shed' ) tool_shed_elem.text = tool_shed diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py new file mode 100644 index 00000000000..6ce8af5e8b4 --- /dev/null +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -0,0 +1,73 @@ +import os + +from galaxy.util import parse_xml + +from tools.test_toolbox import BaseToolBoxTestCase +from tool_shed.galaxy_install.tools import tool_panel_manager + + +class ToolPanelManagerTestCase( BaseToolBoxTestCase ): + + def test_handle_tool_panel_section( self ): + self._init_tool() + self._add_config( """
""" ) + toolbox = self.toolbox + tpm = tool_panel_manager.ToolPanelManager( self.app ) + # Test fetch existing section by id. + section_id, section = tpm.handle_tool_panel_section( toolbox, tool_panel_section_id="tid" ) + assert section_id == "tid" + assert len( section.elems ) == 1 # tool.xml + assert section.id == "tid" + assert len( toolbox.tool_panel ) == 1 + + section_id, section = tpm.handle_tool_panel_section( toolbox, new_tool_panel_section_label="tid2" ) + assert section_id == "tid2" + assert len( section.elems ) == 0 # new section + assert section.id == "tid2" + assert len( toolbox.tool_panel ) == 2 + + # Test re-fetch new section by same id. + section_id, section = tpm.handle_tool_panel_section( toolbox, new_tool_panel_section_label="tid2" ) + assert section_id == "tid2" + assert len( section.elems ) == 0 # new section + assert section.id == "tid2" + assert len( toolbox.tool_panel ) == 2 + + def test_add_tool_to_panel( self ): + self._init_tool() + self._add_config( """""" % self.test_directory ) + tool_path = os.path.join(self.test_directory, "tool.xml") + self.tool.guid = "123456" + new_tools = [{"guid": "123456", "tool_config": tool_path}] + repository_tools_tups = [ + ( + tool_path, + "123456", + self.tool, + ) + ] + _, section = self.toolbox.get_or_create_section("tid1") + tpm = tool_panel_manager.ToolPanelManager( self.app ) + tool_panel_dict = tpm.generate_tool_panel_dict_for_new_install( + tool_dicts=new_tools, + tool_section=section, + ) + tpm.add_to_tool_panel( + repository_name="test_repo", + repository_clone_url="http://github.com/galaxyproject/example.git", + changeset_revision="0123456789abcde", + repository_tools_tups=repository_tools_tups, + owner="devteam", + shed_tool_conf="tool_conf.xml", + tool_panel_dict=tool_panel_dict, + ) + self._assert_valid_xml( self.integerated_tool_panel_path ) + self._assert_valid_xml( os.path.join( self.test_directory, "tool_conf.xml" ) ) + + def _assert_valid_xml( self, filename ): + try: + parse_xml( filename ) + except Exception: + message_template = "file %s does not contain valid XML, content %s" + message = message_template % ( filename, open( filename, "r" ).read() ) + raise AssertionError( message ) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 5581448c156..18bf24f0912 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -23,6 +23,8 @@ class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_supp def toolbox( self ): if self.__toolbox is None: self.__toolbox = SimplifiedToolBox( self ) + # wire app with this new toolbox + self.app.toolbox = self.__toolbox return self.__toolbox def setUp( self ): @@ -35,11 +37,15 @@ class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_supp self.config_files = [] def _add_config( self, xml, name="tool_conf.xml" ): - path = os.path.join( self.test_directory, "tool_conf.xml" ) + path = self._tool_conf_path( name=name ) with open( path, "w" ) as f: f.write( xml ) self.config_files.append( path ) + def _tool_conf_path( self, name="tool_conf.xml" ): + path = os.path.join( self.test_directory, name ) + return path + def __reindex( self ): self.reindexed = True From 1e800df93f106a3544af9df9b424508ac098d170 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 21:42:37 -0500 Subject: [PATCH 038/473] PEP-8 and style tweaks to tool_panel_manager.py. --- .../tools/tool_panel_manager.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 84cf73b2e24..daca4363f89 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -1,7 +1,6 @@ import logging import threading -import galaxy.tools from xml.etree import ElementTree as XmlET from tool_shed.util import basic_util @@ -67,13 +66,14 @@ class ToolPanelManager( object ): self.app.toolbox.integrated_tool_panel, load_panel_dict=True ) elif config_elem.tag == 'tool': - guid = config_elem.get( 'guid' ) - self.app.toolbox.load_tool_tag_set( config_elem, - self.app.toolbox.tool_panel, - self.app.toolbox.integrated_tool_panel, - tool_path, - load_panel_dict=True, - guid=guid ) + self.app.toolbox.load_tool_tag_set( + config_elem, + self.app.toolbox.tool_panel, + self.app.toolbox.integrated_tool_panel, + tool_path, + load_panel_dict=True, + guid=config_elem.get( 'guid' ) + ) # Replace the old list of in-memory config_elems with the new list for this shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems self.app.toolbox.update_shed_config( index, shed_tool_conf_dict ) @@ -124,8 +124,9 @@ class ToolPanelManager( object ): def generate_tool_panel_dict_for_new_install( self, tool_dicts, tool_section=None ): """ - When installing a repository that contains tools, all tools must currently be defined - within the same tool section in the tool panel or outside of any sections. + When installing a repository that contains tools, all tools must + currently be defined within the same tool section in the tool panel or + outside of any sections. """ tool_panel_dict = {} if tool_section: @@ -168,11 +169,12 @@ class ToolPanelManager( object ): def generate_tool_panel_dict_from_shed_tool_conf_entries( self, repository ): """ - Keep track of the section in the tool panel in which this repository's tools - will be contained by parsing the shed_tool_conf in which the repository's tools - are defined and storing the tool panel definition of each tool in the repository. - This method is called only when the repository is being deactivated or un-installed - and allows for activation or re-installation using the original layout. + Keep track of the section in the tool panel in which this repository's + tools will be contained by parsing the shed_tool_conf in which the + repository's tools are defined and storing the tool panel definition + of each tool in the repository. This method is called only when the + repository is being deactivated or un-installed and allows for + activation or re-installation using the original layout. """ tool_panel_dict = {} shed_tool_conf, tool_path, relative_install_dir = \ @@ -351,8 +353,8 @@ class ToolPanelManager( object ): def handle_tool_panel_selection( self, toolbox, metadata, no_changes_checked, tool_panel_section_id, new_tool_panel_section_label ): """ - Handle the selected tool panel location for loading tools included in tool shed - repositories when installing or reinstalling them. + Handle the selected tool panel location for loading tools included in + tool shed repositories when installing or reinstalling them. """ # Get the location in the tool panel in which each tool was originally loaded. tool_section = None @@ -390,10 +392,11 @@ class ToolPanelManager( object ): def remove_from_shed_tool_config( self, shed_tool_conf_dict, guids_to_remove ): """ - A tool shed repository is being uninstalled so change the shed_tool_conf file. - Parse the config file to generate the entire list of config_elems instead of - using the in-memory list since it will be a subset of the entire list if one - or more repositories have been deactivated. + A tool shed repository is being uninstalled so change the + shed_tool_conf file. Parse the config file to generate the entire list + of config_elems instead of using the in-memory list since it will be a + subset of the entire list if one or more repositories have been + deactivated. """ shed_tool_conf = shed_tool_conf_dict[ 'config_filename' ] tool_path = shed_tool_conf_dict[ 'tool_path' ] @@ -426,8 +429,8 @@ class ToolPanelManager( object ): def remove_from_tool_panel( self, repository, shed_tool_conf, uninstall ): """ - A tool shed repository is being deactivated or uninstalled, so handle tool panel - alterations accordingly. + A tool shed repository is being deactivated or uninstalled, so handle + tool panel alterations accordingly. """ # Determine where the tools are currently defined in the tool panel and store this # information so the tools can be displayed in the same way when the repository is From fd1ab332311b4548363511cf03f4105ab07ed299 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 21:42:37 -0500 Subject: [PATCH 039/473] Unit test for tool panel merging tools by tool shed lineage. --- test/unit/tools/test_toolbox.py | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 18bf24f0912..292f6f0874a 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -2,6 +2,7 @@ import os import unittest from galaxy.tools import ToolBox +from galaxy.model import tool_shed_install from galaxy.model.tool_shed_install import mapping import tools_support @@ -30,12 +31,50 @@ class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_supp def setUp( self ): self.reindexed = False self.setup_app( mock_model=False ) + install_model = mapping.init( "sqlite:///:memory:", create_tables=True ) + self.app.install_model = install_model self.app.reindex_tool_search = self.__reindex itp_config = os.path.join(self.test_directory, "integrated_tool_panel.xml") self.app.config.integrated_tool_panel_config = itp_config self.__toolbox = None self.config_files = [] + def _repo_install( self, changeset ): + repository = tool_shed_install.ToolShedRepository() + repository.tool_shed = "github.com" + repository.owner = "galaxyproject" + repository.name = "example" + repository.changeset_revision = changeset + repository.installed_changeset_revision = changeset + repository.deleted = False + repository.uninstalled = False + self.app.install_model.context.add( repository ) + self.app.install_model.context.flush( ) + return repository + + def _setup_two_versions( self ): + repository1 = self._repo_install( changeset="1" ) + version1 = tool_shed_install.ToolVersion() + version1.tool_id = "github.com/galaxyproect/example/test_tool/0.1" + version1.repository = repository1 + self.app.install_model.context.add( version1 ) + self.app.install_model.context.flush( ) + + repository2 = self._repo_install( changeset="2" ) + version2 = tool_shed_install.ToolVersion() + version2.tool_id = "github.com/galaxyproect/example/test_tool/0.2" + version2.repository = repository2 + + self.app.install_model.context.add( version2 ) + self.app.install_model.context.flush( ) + + version_association = tool_shed_install.ToolVersionAssociation() + version_association.parent_id = version1.id + version_association.tool_id = version2.id + + self.app.install_model.context.add( version_association ) + self.app.install_model.context.flush( ) + def _add_config( self, xml, name="tool_conf.xml" ): path = self._tool_conf_path( name=name ) with open( path, "w" ) as f: @@ -78,7 +117,49 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): def test_groups_tools( self ): self._init_tool() - self._add_config( """""" ) + self._add_config( """ +
+ + github.com + example + galaxyproject + 2 + github.com/galaxyproect/example/test_tool/0.1 + 0.1 + +
+
+ + github.com + example + galaxyproject + 2 + github.com/galaxyproect/example/test_tool/0.2 + 0.2 + +
+
""" % self.test_directory ) + self._setup_two_versions() + + # Assert tool versions of the tool with simple id 'test_tool' + all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) + assert len( all_versions ) == 2 + + # Verify lineage_ids on both tools is correctly ordered. + for version in ["0.1", "0.2"]: + guid = "github.com/galaxyproect/example/test_tool/" + version + lineage_ids = self.toolbox.get_tool( guid ).lineage_ids + assert lineage_ids[ 0 ] == "github.com/galaxyproect/example/test_tool/0.1" + assert lineage_ids[ 1 ] == "github.com/galaxyproect/example/test_tool/0.2" + + # Test tool_version attribute. + assert self.toolbox.get_tool( "test_tool", tool_version="0.1" ).guid == "github.com/galaxyproect/example/test_tool/0.1" + assert self.toolbox.get_tool( "test_tool", tool_version="0.2" ).guid == "github.com/galaxyproect/example/test_tool/0.2" + + # Assert only newer version of the tool loaded into the panel. + section = self.toolbox.tool_panel["tid"] + assert len(section.elems) == 1 + assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.2" def test_update_shed_conf(self): self.__setup_shed_tool_conf() @@ -110,7 +191,6 @@ class SimplifiedToolBox( ToolBox ): def __init__( self, test_case ): app = test_case.app # Handle app/config stuff needed by toolbox but not by tools. - app.install_model = mapping.init( "sqlite:///:memory:", create_tables=True ) app.job_config.get_tool_resource_parameters = lambda tool_id: None app.config.update_integrated_tool_panel = True config_files = test_case.config_files From 0e0b3199e70f36cead7565d44f1716145699e203 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 23 Dec 2014 22:17:55 -0500 Subject: [PATCH 040/473] Add tool lineage test for tools outside of sections. --- test/unit/tools/test_toolbox.py | 90 ++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 292f6f0874a..9e9180594a4 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -1,4 +1,5 @@ import os +import string import unittest from galaxy.tools import ToolBox @@ -7,6 +8,21 @@ from galaxy.model.tool_shed_install import mapping import tools_support +CONFIG_TEST_TOOL_VERSION_TEMPLATE = string.Template( + """ + github.com + example + galaxyproject + ${version} + github.com/galaxyproect/example/test_tool/0.${version} + 0.${version} + + """ +) +CONFIG_TEST_TOOL_VERSION_1 = CONFIG_TEST_TOOL_VERSION_TEMPLATE.safe_substitute( dict( version="1" ) ) +CONFIG_TEST_TOOL_VERSION_2 = CONFIG_TEST_TOOL_VERSION_TEMPLATE.safe_substitute( dict( version="2" ) ) + + class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_support.UsesTools ): @property @@ -115,32 +131,52 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): self.toolbox self.assert_integerated_tool_panel(exists=True) - def test_groups_tools( self ): + def test_groups_tools_in_section( self ): self._init_tool() self._add_config( """
- - github.com - example - galaxyproject - 2 - github.com/galaxyproect/example/test_tool/0.1 - 0.1 - + %s
- - github.com - example - galaxyproject - 2 - github.com/galaxyproect/example/test_tool/0.2 - 0.2 - + %s
-
""" % self.test_directory ) +""" % (self.test_directory, CONFIG_TEST_TOOL_VERSION_1, CONFIG_TEST_TOOL_VERSION_2 ) ) self._setup_two_versions() + self.toolbox + self.__verify_two_test_tools( ) + # Assert only newer version of the tool loaded into the panel. + section = self.toolbox.tool_panel["tid"] + assert len(section.elems) == 1 + assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.2" + + def test_group_tools_out_of_section( self ): + self._init_tool() + self._add_config( """ +%s +%s +""" % (self.test_directory, CONFIG_TEST_TOOL_VERSION_1, CONFIG_TEST_TOOL_VERSION_2 ) ) + + self._setup_two_versions() + self.__verify_two_test_tools( ) + + # Assert tools merged in tool panel. + assert len( self.toolbox.tool_panel ) == 1 + + def test_update_shed_conf(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {} ) + assert self.reindexed + self.assert_integerated_tool_panel(exists=True) + + def test_update_shed_conf_deactivate_only(self): + self.__setup_shed_tool_conf() + self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) + assert self.reindexed + # No changes, should be regenerated + self.assert_integerated_tool_panel(exists=False) + + def __verify_two_test_tools( self ): # Assert tool versions of the tool with simple id 'test_tool' all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) assert len( all_versions ) == 2 @@ -156,24 +192,6 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): assert self.toolbox.get_tool( "test_tool", tool_version="0.1" ).guid == "github.com/galaxyproect/example/test_tool/0.1" assert self.toolbox.get_tool( "test_tool", tool_version="0.2" ).guid == "github.com/galaxyproect/example/test_tool/0.2" - # Assert only newer version of the tool loaded into the panel. - section = self.toolbox.tool_panel["tid"] - assert len(section.elems) == 1 - assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.2" - - def test_update_shed_conf(self): - self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {} ) - assert self.reindexed - self.assert_integerated_tool_panel(exists=True) - - def test_update_shed_conf_deactivate_only(self): - self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) - assert self.reindexed - # No changes, should be regenerated - self.assert_integerated_tool_panel(exists=False) - def __remove_itp( self ): os.remove( os.path) From f315f8ee76902be87eb89779fee7412258131741 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 24 Dec 2014 00:42:54 -0500 Subject: [PATCH 041/473] Breakup big remove method in tool_panel_manager.py to ease testing. Separate out logic for dealing with toolbox from logic for dealing repository metadata in the database. --- lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py | 6 +++--- lib/tool_shed/galaxy_install/tools/tool_panel_manager.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py index 361ecc6ffa0..883a7cd0056 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -249,9 +249,9 @@ class AdminToolshed( AdminGalaxy ): if tool_shed_repository.includes_tools_for_display_in_tool_panel: # Handle tool panel alterations. tpm = tool_panel_manager.ToolPanelManager( trans.app ) - tpm.remove_from_tool_panel( tool_shed_repository, - shed_tool_conf, - uninstall=remove_from_disk_checked ) + tpm.remove_repository_contents( tool_shed_repository, + shed_tool_conf, + uninstall=remove_from_disk_checked ) if tool_shed_repository.includes_data_managers: dmh = data_manager.DataManagerHandler( trans.app ) dmh.remove_from_data_manager( tool_shed_repository ) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index daca4363f89..e9a19a84e71 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -427,7 +427,7 @@ class ToolPanelManager( object ): # Persist the altered in-memory version of the tool config. self.config_elems_to_xml_file( config_elems, shed_tool_conf, tool_path ) - def remove_from_tool_panel( self, repository, shed_tool_conf, uninstall ): + def remove_repository_contents( self, repository, shed_tool_conf, uninstall ): """ A tool shed repository is being deactivated or uninstalled, so handle tool panel alterations accordingly. @@ -442,6 +442,9 @@ class ToolPanelManager( object ): # Create a list of guids for all tools that will be removed from the in-memory tool panel # and config file on disk. guids_to_remove = [ k for k in tool_panel_dict.keys() ] + self.remove_guids( guids_to_remove, shed_tool_conf, uninstall ) + + def remove_guids( self, guids_to_remove, shed_tool_conf, uninstall ): # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: if guid_to_remove in self.app.toolbox.tools_by_id: From fd1c34b4c9fec55286c3d33e2802062ef393de82 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 24 Dec 2014 00:42:54 -0500 Subject: [PATCH 042/473] Unit test coverage for removing stuff from the toolbox. Covers cases for tools inside and outside of sections and both deactivation versus uninstallation. --- .../test_tool_panel_manager.py | 79 ++++++++++++++++++- test/unit/tools/test_toolbox.py | 36 ++++++--- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py index 6ce8af5e8b4..7fe19990881 100644 --- a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -12,7 +12,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self._init_tool() self._add_config( """
""" ) toolbox = self.toolbox - tpm = tool_panel_manager.ToolPanelManager( self.app ) + tpm = self.tpm # Test fetch existing section by id. section_id, section = tpm.handle_tool_panel_section( toolbox, tool_panel_section_id="tid" ) assert section_id == "tid" @@ -47,7 +47,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): ) ] _, section = self.toolbox.get_or_create_section("tid1") - tpm = tool_panel_manager.ToolPanelManager( self.app ) + tpm = self.tpm tool_panel_dict = tpm.generate_tool_panel_dict_for_new_install( tool_dicts=new_tools, tool_section=section, @@ -61,6 +61,77 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): shed_tool_conf="tool_conf.xml", tool_panel_dict=tool_panel_dict, ) + self._verify_tool_confs() + + def test_deactivate_in_section( self ): + self._setup_two_versions_remove_one( section=True, uninstall=False ) + self._verify_version_2_removed_from_panel( ) + # Still in tool conf since not uninstalled only deactivated... + assert "github.com/galaxyproect/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + self._verify_tool_confs() + + self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.1"], uninstall=False ) + + # Now no versions of this tool are returned by toolbox. + all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) + assert not all_versions + + # Check that tool panel has reverted to old value... + section = self.toolbox.tool_panel["tid"] + assert len(section.elems) == 0 + + def test_uninstall_in_section( self ): + self._setup_two_versions_remove_one( section=True, uninstall=True ) + self._verify_version_2_removed_from_panel( ) + # Not in tool conf because it was uninstalled. + assert "github.com/galaxyproect/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + self._verify_tool_confs() + + def test_deactivate_outside_section( self ): + self._setup_two_versions_remove_one( section=False, uninstall=False ) + self._verify_version_2_removed_from_panel( section=False ) + # Still in tool conf since not uninstalled only deactivated... + assert "github.com/galaxyproect/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + self._verify_tool_confs() + + def test_uninstall_outside_section( self ): + self._setup_two_versions_remove_one( section=False, uninstall=True ) + self._verify_version_2_removed_from_panel( section=False ) + # Still in tool conf since not uninstalled only deactivated... + assert "github.com/galaxyproect/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + self._verify_tool_confs() + + def _setup_two_versions_remove_one( self, section, uninstall ): + self._init_tool() + self._setup_two_versions_in_config( section=True ) + self._setup_two_versions() + self.toolbox + self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.2"], uninstall=uninstall ) + + def _verify_version_2_removed_from_panel( self, section=True ): + # Check that test_tool now only has one version... + all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) + assert len( all_versions ) == 1 + + # Check that tool panel has reverted to old value... + if section: + section = self.toolbox.tool_panel["tid"] + assert len(section.elems) == 1 + assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.1" + + assert "github.com/galaxyproect/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems + else: + self.toolbox.tool_panel.values()[0].id == "github.com/galaxyproect/example/test_tool/0.1" + assert "github.com/galaxyproect/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel + + def _remove_guids( self, guids, uninstall, shed_tool_conf="tool_conf.xml" ): + self.tpm.remove_guids( + guids_to_remove=guids, + shed_tool_conf=shed_tool_conf, + uninstall=uninstall, + ) + + def _verify_tool_confs( self ): self._assert_valid_xml( self.integerated_tool_panel_path ) self._assert_valid_xml( os.path.join( self.test_directory, "tool_conf.xml" ) ) @@ -71,3 +142,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): message_template = "file %s does not contain valid XML, content %s" message = message_template % ( filename, open( filename, "r" ).read() ) raise AssertionError( message ) + + @property + def tpm( self ): + return tool_panel_manager.ToolPanelManager( self.app ) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 9e9180594a4..0e5685f0221 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -91,6 +91,27 @@ class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_supp self.app.install_model.context.add( version_association ) self.app.install_model.context.flush( ) + def _setup_two_versions_in_config( self, section=False ): + if section: + template = """ +
+ %s +
+
+ %s +
+
""" + else: + template = """ +
+ %s +
+
+ %s +
+
""" + self._add_config( template % (self.test_directory, CONFIG_TEST_TOOL_VERSION_1, CONFIG_TEST_TOOL_VERSION_2 ) ) + def _add_config( self, xml, name="tool_conf.xml" ): path = self._tool_conf_path( name=name ) with open( path, "w" ) as f: @@ -133,14 +154,7 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): def test_groups_tools_in_section( self ): self._init_tool() - self._add_config( """ -
- %s -
-
- %s -
-
""" % (self.test_directory, CONFIG_TEST_TOOL_VERSION_1, CONFIG_TEST_TOOL_VERSION_2 ) ) + self._setup_two_versions_in_config( section=True ) self._setup_two_versions() self.toolbox self.__verify_two_test_tools( ) @@ -152,11 +166,7 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): def test_group_tools_out_of_section( self ): self._init_tool() - self._add_config( """ -%s -%s -""" % (self.test_directory, CONFIG_TEST_TOOL_VERSION_1, CONFIG_TEST_TOOL_VERSION_2 ) ) - + self._setup_two_versions_in_config( section=False ) self._setup_two_versions() self.__verify_two_test_tools( ) From 44d94bb7cb004eec9e929c92afb4d09a54c3558e Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 12:24:23 -0500 Subject: [PATCH 043/473] Bugfix: Allow passing key to GET /api/datatypes/mapping. All anonymous API endpoints should also allow authenticated access and just ignore the key. --- lib/galaxy/webapps/galaxy/api/datatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/api/datatypes.py b/lib/galaxy/webapps/galaxy/api/datatypes.py index ac48b328dab..7299cfe0bb9 100644 --- a/lib/galaxy/webapps/galaxy/api/datatypes.py +++ b/lib/galaxy/webapps/galaxy/api/datatypes.py @@ -44,7 +44,7 @@ class DatatypesController( BaseAPIController ): return { 'error': str( exception ) } @web.expose_api_anonymous - def mapping( self, trans ): + def mapping( self, trans, **kwd ): ''' GET /api/datatypes/mapping Return a dictionary of class to class mappings. From f68a103ae0c4cde01274218b27477b33762815ef Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 12:24:23 -0500 Subject: [PATCH 044/473] Suite of API tests for exist datatypes endpoint. --- test/api/test_datatypes.py | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/api/test_datatypes.py diff --git a/test/api/test_datatypes.py b/test/api/test_datatypes.py new file mode 100644 index 00000000000..140a6421b3b --- /dev/null +++ b/test/api/test_datatypes.py @@ -0,0 +1,51 @@ +from base import api + + +HIDDEN_DURING_UPLOAD_DATATYPE = "fli" + + +class DatatypesApiTestCase( api.ApiTestCase ): + + def test_index( self ): + datatypes = self._index_datatypes() + for common_type in ["tabular", "fasta"]: + assert common_type in datatypes, "%s not in %s" % (common_type, datatypes) + + def test_index_upload_only( self ): + # fli is not displayed in upload - so only show it if upload_only + # is explicitly false. + datatypes = self._index_datatypes( data={ "upload_only": False } ) + assert HIDDEN_DURING_UPLOAD_DATATYPE in datatypes + + datatypes = self._index_datatypes( data={ "upload_only": True } ) + assert HIDDEN_DURING_UPLOAD_DATATYPE not in datatypes + + datatypes = self._index_datatypes( ) + assert HIDDEN_DURING_UPLOAD_DATATYPE not in datatypes + + def test_full_index( self ): + datatypes = self._index_datatypes( data={ "extension_only": False } ) + for datatype in datatypes: + self._assert_has_keys( datatype, "extension", "description", "description_url" ) + assert datatype["extension"] != HIDDEN_DURING_UPLOAD_DATATYPE + + def test_mapping( self ): + response = self._get( "datatypes/mapping" ) + self._assert_status_code_is( response, 200 ) + mapping_dict = response.json() + self._assert_has_keys( mapping_dict, "ext_to_class_name", "class_to_classes" ) + + def test_sniffers( self ): + response = self._get( "datatypes/sniffers" ) + self._assert_status_code_is( response, 200 ) + sniffer_list = response.json() + owl_index = sniffer_list.index( "galaxy.datatypes.xml:Owl" ) + xml_index = sniffer_list.index( "galaxy.datatypes.xml:GenericXml" ) + assert owl_index < xml_index + + def _index_datatypes( self, data={} ): + response = self._get( "datatypes", data=data ) + self._assert_status_code_is( response, 200 ) + datatypes = response.json() + assert isinstance( datatypes, list ) + return datatypes From a31a031b3b2bb27ba04b37f5a05790c207c7d00b Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 12:24:23 -0500 Subject: [PATCH 045/473] Update datatypes API to newer API decoarator/exception handling. --- lib/galaxy/webapps/galaxy/api/datatypes.py | 36 +++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/datatypes.py b/lib/galaxy/webapps/galaxy/api/datatypes.py index 7299cfe0bb9..47b9b56de65 100644 --- a/lib/galaxy/webapps/galaxy/api/datatypes.py +++ b/lib/galaxy/webapps/galaxy/api/datatypes.py @@ -2,7 +2,8 @@ API operations allowing clients to determine datatype supported by Galaxy. """ -from galaxy import web +from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous +from galaxy import exceptions from galaxy.web.base.controller import BaseAPIController from galaxy.util import asbool from galaxy.datatypes.data import Data @@ -13,7 +14,7 @@ log = logging.getLogger( __name__ ) class DatatypesController( BaseAPIController ): - @web.expose_api_anonymous + @expose_api_anonymous def index( self, trans, **kwd ): """ GET /api/datatypes @@ -38,12 +39,14 @@ class DatatypesController( BaseAPIController ): dictionary[key] = elem.get(key) rval.append(dictionary) return rval - except Exception, exception: + except Exception as exception: log.error( 'could not get datatypes: %s', str( exception ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exception ) } + if not isinstance( exception, exceptions.MessageException ): + raise exceptions.InternalServerError( str( exception ) ) + else: + raise - @web.expose_api_anonymous + @expose_api_anonymous def mapping( self, trans, **kwd ): ''' GET /api/datatypes/mapping @@ -69,12 +72,15 @@ class DatatypesController( BaseAPIController ): visit_bases( types, c ) class_to_classes[ n ] = dict( ( t, True ) for t in types ) return dict( ext_to_class_name=ext_to_class_name, class_to_classes=class_to_classes ) - except Exception, exception: - log.error( 'could not get datatype mapping: %s', str( exception ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exception ) } - @web.expose_api_anonymous + except Exception as exception: + log.error( 'could not get datatype mapping: %s', str( exception ), exc_info=True ) + if not isinstance( exception, exceptions.MessageException ): + raise exceptions.InternalServerError( str( exception ) ) + else: + raise + + @expose_api_anonymous def sniffers( self, trans, **kwd ): ''' GET /api/datatypes/sniffers @@ -87,7 +93,9 @@ class DatatypesController( BaseAPIController ): if datatype is not None: rval.append( datatype ) return rval - except Exception, exception: + except Exception as exception: log.error( 'could not get datatypes: %s', str( exception ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exception ) } + if not isinstance( exception, exceptions.MessageException ): + raise exceptions.InternalServerError( str( exception ) ) + else: + raise From 648067c932794a95016c5708cd37bcdb81dc47fd Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 12:24:23 -0500 Subject: [PATCH 046/473] Reduce repeated nested object access in galaxy.api.datatypes. --- lib/galaxy/webapps/galaxy/api/datatypes.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/datatypes.py b/lib/galaxy/webapps/galaxy/api/datatypes.py index 47b9b56de65..e1846fcc7cd 100644 --- a/lib/galaxy/webapps/galaxy/api/datatypes.py +++ b/lib/galaxy/webapps/galaxy/api/datatypes.py @@ -20,17 +20,18 @@ class DatatypesController( BaseAPIController ): GET /api/datatypes Return an object containing upload datatypes. """ + datatypes_registry = self._datatypes_registry extension_only = asbool( kwd.get( 'extension_only', True ) ) upload_only = asbool( kwd.get( 'upload_only', True ) ) try: if extension_only: if upload_only: - return trans.app.datatypes_registry.upload_file_formats + return datatypes_registry.upload_file_formats else: - return [ ext for ext in trans.app.datatypes_registry.datatypes_by_extension ] + return [ ext for ext in datatypes_registry.datatypes_by_extension ] else: rval = [] - for elem in trans.app.datatypes_registry.datatype_elems: + for elem in datatypes_registry.datatype_elems: if not asbool(elem.get('display_in_upload')) and upload_only: continue keys = ['extension', 'description', 'description_url'] @@ -55,7 +56,7 @@ class DatatypesController( BaseAPIController ): try: ext_to_class_name = dict() classes = [] - for k, v in trans.app.datatypes_registry.datatypes_by_extension.iteritems(): + for k, v in self._datatypes_registry.datatypes_by_extension.iteritems(): c = v.__class__ ext_to_class_name[k] = c.__module__ + "." + c.__name__ classes.append( c ) @@ -88,7 +89,7 @@ class DatatypesController( BaseAPIController ): ''' try: rval = [] - for sniffer_elem in trans.app.datatypes_registry.sniffer_elems: + for sniffer_elem in self._datatypes_registry.sniffer_elems: datatype = sniffer_elem.get( 'type' ) if datatype is not None: rval.append( datatype ) @@ -99,3 +100,7 @@ class DatatypesController( BaseAPIController ): raise exceptions.InternalServerError( str( exception ) ) else: raise + + @property + def _datatypes_registry( self ): + return self.app.datatypes_registry From c70cc95128394538e6b2e259edec70f37705b806 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 12:24:23 -0500 Subject: [PATCH 047/473] Expose datatype converter information via datatype API. With test case. --- lib/galaxy/webapps/galaxy/api/datatypes.py | 13 +++++++++++++ lib/galaxy/webapps/galaxy/buildapp.py | 2 +- test/api/test_datatypes.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/galaxy/api/datatypes.py b/lib/galaxy/webapps/galaxy/api/datatypes.py index e1846fcc7cd..486111a4907 100644 --- a/lib/galaxy/webapps/galaxy/api/datatypes.py +++ b/lib/galaxy/webapps/galaxy/api/datatypes.py @@ -101,6 +101,19 @@ class DatatypesController( BaseAPIController ): else: raise + @expose_api_anonymous + def converters( self, trans, **kwd ): + converters = [] + for (source_type, targets) in self._datatypes_registry.datatype_converters.iteritems(): + for target_type in targets: + converters.append( { + 'source': source_type, + 'target': target_type, + 'tool_id': targets[ target_type ].id, + } ) + + return converters + @property def _datatypes_registry( self ): return self.app.datatypes_registry diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 7015b23627a..c8bae37dd8e 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -198,7 +198,7 @@ def populate_api_routes( webapp, app ): webapp.mapper.resource( 'datatype', 'datatypes', path_prefix='/api', - collection={ 'sniffers': 'GET', 'mapping' : 'GET' }, + collection={ 'sniffers': 'GET', 'mapping' : 'GET', 'converters': 'GET' }, parent_resources=dict( member_name='datatype', collection_name='datatypes' ) ) #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) diff --git a/test/api/test_datatypes.py b/test/api/test_datatypes.py index 140a6421b3b..2f560b69fb4 100644 --- a/test/api/test_datatypes.py +++ b/test/api/test_datatypes.py @@ -43,6 +43,19 @@ class DatatypesApiTestCase( api.ApiTestCase ): xml_index = sniffer_list.index( "galaxy.datatypes.xml:GenericXml" ) assert owl_index < xml_index + def test_converters( self ): + response = self._get( "datatypes/converters" ) + self._assert_status_code_is( response, 200 ) + converters_list = response.json() + found_fasta_to_tabular = False + + for converter in converters_list: + self._assert_has_key( converter, "source", "target", "tool_id" ) + if converter["source"] == "fasta" and converter["target"] == "tabular": + found_fasta_to_tabular = True + + assert found_fasta_to_tabular + def _index_datatypes( self, data={} ): response = self._get( "datatypes", data=data ) self._assert_status_code_is( response, 200 ) From 7d0c6538b38ffa6eb121f981d4bbb227160bfbbb Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 048/473] More ToolPanelManager unit testing. --- .../tool_shed_unit_tests/test_tool_panel_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py index 7fe19990881..04a79eef355 100644 --- a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -101,6 +101,16 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): assert "github.com/galaxyproect/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() self._verify_tool_confs() + self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.1"], uninstall=True ) + + # Now no versions of this tool are returned by toolbox. + all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) + assert not all_versions + + # Check that tool panel has reverted to old value... + section = self.toolbox.tool_panel["tid"] + assert len(section.elems) == 0 + def _setup_two_versions_remove_one( self, section, uninstall ): self._init_tool() self._setup_two_versions_in_config( section=True ) From 8c40b5734e63fa2cbfbcadd95466bb155ea4ab43 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 049/473] Eliminate repeated nested object access in ToolPanelManager.remove_guids. --- .../tools/tool_panel_manager.py | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index e9a19a84e71..51a713fe54d 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -445,10 +445,11 @@ class ToolPanelManager( object ): self.remove_guids( guids_to_remove, shed_tool_conf, uninstall ) def remove_guids( self, guids_to_remove, shed_tool_conf, uninstall ): + toolbox = self.app.toolbox # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: - if guid_to_remove in self.app.toolbox.tools_by_id: - del self.app.toolbox.tools_by_id[ guid_to_remove ] + if guid_to_remove in toolbox.tools_by_id: + del toolbox.tools_by_id[ guid_to_remove ] index, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) if uninstall: # Remove from the shed_tool_conf file on disk. @@ -469,12 +470,12 @@ class ToolPanelManager( object ): # Remove the tool sub-element from the section element. config_elem.remove( tool_elem ) # Remove the tool from the section in the in-memory tool panel. - if section_key in self.app.toolbox.tool_panel: - tool_section = self.app.toolbox.tool_panel[ section_key ] + if section_key in toolbox.tool_panel: + tool_section = toolbox.tool_panel[ section_key ] guid = tool_elem.get( 'guid' ) tool_key = 'tool_%s' % str( guid ) # Get the list of versions of this tool that are currently available in the toolbox. - available_tool_versions = self.app.toolbox.get_loaded_tools_by_lineage( guid ) + available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) if tool_key in tool_section.elems: if available_tool_versions: available_tool_versions.reverse() @@ -505,8 +506,8 @@ class ToolPanelManager( object ): del tool_section.elems[ tool_key ] if uninstall: # Remove the tool from the section in the in-memory integrated tool panel. - if section_key in self.app.toolbox.integrated_tool_panel: - tool_section = self.app.toolbox.integrated_tool_panel[ section_key ] + if section_key in toolbox.integrated_tool_panel: + tool_section = toolbox.integrated_tool_panel[ section_key ] tool_key = 'tool_%s' % str( tool_elem.get( 'guid' ) ) if tool_key in tool_section.elems: del tool_section.elems[ tool_key ] @@ -518,8 +519,8 @@ class ToolPanelManager( object ): if guid in guids_to_remove: tool_key = 'tool_%s' % str( config_elem.get( 'guid' ) ) # Get the list of versions of this tool that are currently available in the toolbox. - available_tool_versions = self.app.toolbox.get_loaded_tools_by_lineage( guid ) - if tool_key in self.app.toolbox.tool_panel: + available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) + if tool_key in toolbox.tool_panel: if available_tool_versions: available_tool_versions.reverse() replacement_tool_key = None @@ -528,32 +529,32 @@ class ToolPanelManager( object ): # the newest loaded version of the tool. for available_tool_version in available_tool_versions: available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() - if available_tool_version.id in self.app.toolbox.tool_panel.keys() or not available_tool_section_id: + if available_tool_version.id in toolbox.tool_panel.keys() or not available_tool_section_id: replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) replacement_tool_version = available_tool_version break if replacement_tool_key and replacement_tool_version: # Get the index of the tool_key in the tool_section. - for tool_panel_index, key in enumerate( self.app.toolbox.tool_panel.keys() ): + for tool_panel_index, key in enumerate( toolbox.tool_panel.keys() ): if key == tool_key: break # Remove the tool from the tool panel. - del self.app.toolbox.tool_panel[ tool_key ] + del toolbox.tool_panel[ tool_key ] # Add the replacement tool at the same location in the tool panel. - self.app.toolbox.tool_panel.insert( tool_panel_index, - replacement_tool_key, - replacement_tool_version ) + toolbox.tool_panel.insert( tool_panel_index, + replacement_tool_key, + replacement_tool_version ) else: - del self.app.toolbox.tool_panel[ tool_key ] + del toolbox.tool_panel[ tool_key ] else: - del self.app.toolbox.tool_panel[ tool_key ] + del toolbox.tool_panel[ tool_key ] if uninstall: - if tool_key in self.app.toolbox.integrated_tool_panel: - del self.app.toolbox.integrated_tool_panel[ tool_key ] + if tool_key in toolbox.integrated_tool_panel: + del toolbox.integrated_tool_panel[ tool_key ] config_elems_to_remove.append( config_elem ) for config_elem in config_elems_to_remove: # Remove the element from the in-memory list of elements. config_elems.remove( config_elem ) # Update the config_elems of the in-memory shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems - self.app.toolbox.update_shed_config( index, shed_tool_conf_dict, integrated_panel_changes=uninstall ) + toolbox.update_shed_config( index, shed_tool_conf_dict, integrated_panel_changes=uninstall ) From 6c385aa25af34222a005b9ecc16e0ba12661bb7e Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 050/473] Reduce duplication/complexity in tool_panel_manager.py. Big conditional in remove_guids switched on sectioned versus un-sectioned with very little difference in what was actually being done. This reduces the cyclomatic complexity of that method from 35 down to 26 (though it is still the most 'complex' method in that file by a wide margin). --- .../tools/tool_panel_manager.py | 118 +++++++----------- 1 file changed, 45 insertions(+), 73 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 51a713fe54d..3daab5fc5f5 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -446,6 +446,48 @@ class ToolPanelManager( object ): def remove_guids( self, guids_to_remove, shed_tool_conf, uninstall ): toolbox = self.app.toolbox + + def remove_from_panel( tool_elem, has_elems, integrated_has_elems, section_key='' ): + # Hide tool from panel and promote next oldest version if + # available. + guid = tool_elem.get( 'guid' ) + tool_key = 'tool_%s' % str( guid ) + available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) + if tool_key in has_elems: + if available_tool_versions: + available_tool_versions.reverse() + replacement_tool_key = None + replacement_tool_version = None + # Since we are going to remove the tool from the section, replace it with + # the newest loaded version of the tool. + for available_tool_version in available_tool_versions: + available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() + # I suspect "available_tool_version.id in has_elems.keys()" doesn't + # belong in the following line or at least I don't understand what + # purpose it might serve. -John + if available_tool_version.id in has_elems.keys() or (available_tool_section_id == section_key): + replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) + replacement_tool_version = available_tool_version + break + if replacement_tool_key and replacement_tool_version: + # Get the index of the tool_key in the tool_section. + for tool_panel_index, key in enumerate( has_elems.keys() ): + if key == tool_key: + break + # Remove the tool from the tool panel. + del has_elems[ tool_key ] + # Add the replacement tool at the same location in the tool panel. + has_elems.insert( tool_panel_index, + replacement_tool_key, + replacement_tool_version ) + else: + del has_elems[ tool_key ] + else: + del has_elems[ tool_key ] + if uninstall: + if tool_key in toolbox.integrated_tool_panel: + del toolbox.integrated_tool_panel[ tool_key ] + # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: if guid_to_remove in toolbox.tools_by_id: @@ -472,85 +514,15 @@ class ToolPanelManager( object ): # Remove the tool from the section in the in-memory tool panel. if section_key in toolbox.tool_panel: tool_section = toolbox.tool_panel[ section_key ] - guid = tool_elem.get( 'guid' ) - tool_key = 'tool_%s' % str( guid ) - # Get the list of versions of this tool that are currently available in the toolbox. - available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) - if tool_key in tool_section.elems: - if available_tool_versions: - available_tool_versions.reverse() - replacement_tool_key = None - replacement_tool_version = None - # Since we are going to remove the tool from the section, replace it with the - # newest loaded version of the tool. - for available_tool_version in available_tool_versions: - available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() - if available_tool_version.id in tool_section.elems.keys() or section_key == available_tool_section_id: - replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) - replacement_tool_version = available_tool_version - break - if replacement_tool_key and replacement_tool_version: - # Get the index of the tool_key in the tool_section. - for tool_section_elems_index, key in enumerate( tool_section.elems.keys() ): - if key == tool_key: - break - # Remove the tool from the tool section. - del tool_section.elems[ tool_key ] - # Add the replacement tool at the same location in the tool section. - tool_section.elems.insert( tool_section_elems_index, - replacement_tool_key, - replacement_tool_version ) - else: - del tool_section.elems[ tool_key ] - else: - del tool_section.elems[ tool_key ] - if uninstall: - # Remove the tool from the section in the in-memory integrated tool panel. - if section_key in toolbox.integrated_tool_panel: - tool_section = toolbox.integrated_tool_panel[ section_key ] - tool_key = 'tool_%s' % str( tool_elem.get( 'guid' ) ) - if tool_key in tool_section.elems: - del tool_section.elems[ tool_key ] + remove_from_panel( tool_elem, tool_section.elems, toolbox.integrated_tool_panel.get( section_key, {} ), section_key=section_key ) if len( config_elem ) < 1: # Keep a list of all empty section elements so they can be removed. config_elems_to_remove.append( config_elem ) elif config_elem.tag == 'tool': guid = config_elem.get( 'guid' ) if guid in guids_to_remove: - tool_key = 'tool_%s' % str( config_elem.get( 'guid' ) ) - # Get the list of versions of this tool that are currently available in the toolbox. - available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) - if tool_key in toolbox.tool_panel: - if available_tool_versions: - available_tool_versions.reverse() - replacement_tool_key = None - replacement_tool_version = None - # Since we are going to remove the tool from the section, replace it with - # the newest loaded version of the tool. - for available_tool_version in available_tool_versions: - available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() - if available_tool_version.id in toolbox.tool_panel.keys() or not available_tool_section_id: - replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) - replacement_tool_version = available_tool_version - break - if replacement_tool_key and replacement_tool_version: - # Get the index of the tool_key in the tool_section. - for tool_panel_index, key in enumerate( toolbox.tool_panel.keys() ): - if key == tool_key: - break - # Remove the tool from the tool panel. - del toolbox.tool_panel[ tool_key ] - # Add the replacement tool at the same location in the tool panel. - toolbox.tool_panel.insert( tool_panel_index, - replacement_tool_key, - replacement_tool_version ) - else: - del toolbox.tool_panel[ tool_key ] - else: - del toolbox.tool_panel[ tool_key ] - if uninstall: - if tool_key in toolbox.integrated_tool_panel: - del toolbox.integrated_tool_panel[ tool_key ] + # get_panel_section return '' for un-sectioned tools. + remove_from_panel( config_elem, toolbox.tool_panel, toolbox.integrated_tool_panel, section_key='' ) config_elems_to_remove.append( config_elem ) for config_elem in config_elems_to_remove: # Remove the element from the in-memory list of elements. From 047bfc0d3f9a96d208952d2afc3876c98baaa3b0 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 051/473] Abstract out ToolBox internals from tool_shed.util.tool_util. Instead of reloading upload actions at this level and exposing tools_by_id - move this logic into the ToolBox in a new method called handle_datatypes_changed(). --- lib/galaxy/tools/__init__.py | 8 ++++++++ lib/tool_shed/util/tool_util.py | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index e2740af7bcc..bd064cd6e76 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -34,6 +34,7 @@ from galaxy.datatypes.metadata import JobExternalOutputMetadataWrapper from galaxy import exceptions from galaxy.tools import watcher from galaxy.tools.actions import DefaultToolAction +from galaxy.tools.actions.upload import UploadToolAction from galaxy.tools.actions.data_source import DataSourceToolAction from galaxy.tools.actions.data_manager import DataManagerToolAction from galaxy.tools.deps import build_dependency_manager @@ -992,6 +993,13 @@ class ToolBox( object, Dictifiable ): def init_dependency_manager( self ): self.dependency_manager = build_dependency_manager( self.app.config ) + def handle_datatypes_changed( self ): + """ Refresh upload tools when new datatypes are added. """ + for tool_id in self.tools_by_id: + tool = self.tools_by_id[ tool_id ] + if isinstance( tool.tool_action, UploadToolAction ): + self.reload_tool_by_id( tool_id ) + @property def sa_session( self ): """ diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index f3bb8636bee..91eff5ed6c1 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -260,7 +260,4 @@ def panel_entry_per_tool( tool_section_dict ): def reload_upload_tools( app ): if hasattr( app, 'toolbox' ): - for tool_id in app.toolbox.tools_by_id: - tool = app.toolbox.tools_by_id[ tool_id ] - if isinstance( tool.tool_action, UploadToolAction ): - app.toolbox.reload_tool_by_id( tool_id ) + app.toolbox.handle_datatypes_changed() From ccf3794173272cf3d9efb3e26885aa63e38ee861 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 052/473] Centralize abstraction for removing tools from toolbox. Shield's toolbox.tools_by_id from lib/tool_shed/galaxy_install/tools/tool_panel_manager.py. --- lib/galaxy/datatypes/registry.py | 3 +- lib/galaxy/tools/__init__.py | 29 +++++++++++-------- .../tools/tool_panel_manager.py | 4 +-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index 83f52a5b916..d24ba870305 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -529,8 +529,7 @@ class Registry( object ): converter.id = tool_dict[ 'guid' ] break if deactivate: - if converter.id in toolbox.tools_by_id: - del toolbox.tools_by_id[ converter.id ] + toolbox.remove_tool_by_id( converter.id, remove_from_panel=False ) if source_datatype in self.datatype_converters: if target_datatype in self.datatype_converters[ source_datatype ]: del self.datatype_converters[ source_datatype ][ target_datatype ] diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index bd064cd6e76..6d729f2de26 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -952,9 +952,13 @@ class ToolBox( object, Dictifiable ): status = 'done' return message, status - def remove_tool_by_id( self, tool_id ): + def remove_tool_by_id( self, tool_id, remove_from_panel=True ): """ - Attempt to remove the tool identified by 'tool_id'. + Attempt to remove the tool identified by 'tool_id'. Ignores + tool lineage - so to remove a tool with potentially multiple + versions send remove_from_panel=False and handle the logic of + promoting the next newest version of the tool into the panel + if needed. """ if tool_id not in self.tools_by_id: message = "No tool with id %s" % escape( tool_id ) @@ -962,17 +966,18 @@ class ToolBox( object, Dictifiable ): else: tool = self.tools_by_id[ tool_id ] del self.tools_by_id[ tool_id ] - tool_key = 'tool_' + tool_id - for key, val in self.tool_panel.items(): - if key == tool_key: - del self.tool_panel[ key ] - break - elif key.startswith( 'section' ): - if tool_key in val.elems: - del self.tool_panel[ key ].elems[ tool_key ] + if remove_from_panel: + tool_key = 'tool_' + tool_id + for key, val in self.tool_panel.items(): + if key == tool_key: + del self.tool_panel[ key ] break - if tool_id in self.data_manager_tools: - del self.data_manager_tools[ tool_id ] + elif key.startswith( 'section' ): + if tool_key in val.elems: + del self.tool_panel[ key ].elems[ tool_key ] + break + if tool_id in self.data_manager_tools: + del self.data_manager_tools[ tool_id ] #TODO: do we need to manually remove from the integrated panel here? message = "Removed the tool:
" message += "name: %s
" % tool.name diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 3daab5fc5f5..d344f99ca0a 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -490,8 +490,8 @@ class ToolPanelManager( object ): # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: - if guid_to_remove in toolbox.tools_by_id: - del toolbox.tools_by_id[ guid_to_remove ] + # remove_from_tool_panel to false, will handling that logic below. + toolbox.remove_tool_by_id( guid_to_remove, remove_from_panel=False ) index, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) if uninstall: # Remove from the shed_tool_conf file on disk. From e455d42b6167dd1afd12d9e5d4e018fd27cb05e6 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 053/473] Eliminate walking over tools_by_id externally from toolbox. --- lib/galaxy/tools/__init__.py | 3 +++ lib/galaxy/tools/search/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 6d729f2de26..18a1aca8ae6 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -535,6 +535,9 @@ class ToolBox( object, Dictifiable ): .filter( self.app.install_model.ToolVersion.table.c.tool_id == tool_id ) \ .first() + def tools( self ): + return self.tools_by_id.iteritems() + def __get_tool_shed_repository( self, tool_shed, name, owner, installed_changeset_revision ): # We store only the port, if one exists, in the database. tool_shed = common_util.remove_protocol_from_tool_shed_url( tool_shed ) diff --git a/lib/galaxy/tools/search/__init__.py b/lib/galaxy/tools/search/__init__.py index 8f11eec64dc..9c94de83b73 100644 --- a/lib/galaxy/tools/search/__init__.py +++ b/lib/galaxy/tools/search/__init__.py @@ -27,7 +27,7 @@ class ToolBoxSearch( object ): self.index = self.storage.create_index( schema ) writer = self.index.writer() ## TODO: would also be nice to search section headers. - for id, tool in self.toolbox.tools_by_id.iteritems(): + for id, tool in self.toolbox.tools(): add_doc_kwds = { "id": id, "title": to_unicode(tool.name), From 74a58ea2c2816d2e8cdc1a607300b07ea89b7317 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 25 Dec 2014 13:35:13 -0500 Subject: [PATCH 054/473] Abstractions for registering tools from outside toolbox. Part of effort to further limit exposing tools_by_id directly outside Toolbox. --- lib/galaxy/datatypes/registry.py | 5 ++--- lib/galaxy/tools/__init__.py | 11 +++++++++++ lib/galaxy/tools/data_manager/manager.py | 10 +++++----- lib/galaxy/tools/imp_exp/__init__.py | 6 ++---- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index d24ba870305..e86f06087f2 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -535,7 +535,7 @@ class Registry( object ): del self.datatype_converters[ source_datatype ][ target_datatype ] self.log.debug( "Deactivated converter: %s", converter.id ) else: - toolbox.tools_by_id[ converter.id ] = converter + toolbox.register_tool( converter ) if source_datatype not in self.datatype_converters: self.datatype_converters[ source_datatype ] = odict() self.datatype_converters[ source_datatype ][ target_datatype ] = converter @@ -644,8 +644,7 @@ class Registry( object ): tmp_name = tempfile.NamedTemporaryFile() tmp_name.write( tool_xml_text ) tmp_name.flush() - set_meta_tool = toolbox.load_tool( tmp_name.name ) - toolbox.tools_by_id[ set_meta_tool.id ] = set_meta_tool + set_meta_tool = toolbox.load_hidden_tool( tmp_name.name ) self.set_external_metadata_tool = set_meta_tool self.log.debug( "Loaded external metadata tool: %s", self.set_external_metadata_tool.id ) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 18a1aca8ae6..561ed96453e 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -804,6 +804,17 @@ class ToolBox( object, Dictifiable ): self.tool_watcher.watch_file( config_file, tool.id ) return tool + def load_hidden_tool( self, config_file, **kwds ): + """ Load a hidden tool (in this context meaning one that does not + appear in the tool panel) and register it in tools_by_id. + """ + tool = self.load_tool( config_file, **kwds ) + self.register_tool( tool ) + return tool + + def register_tool( self, tool ): + self.tools_by_id[ tool.id ] = tool + def package_tool( self, trans, tool_id ): """ Create a tarball with the tool's xml, help images, and test data. diff --git a/lib/galaxy/tools/data_manager/manager.py b/lib/galaxy/tools/data_manager/manager.py index 088e7c9a54f..15b2da0214d 100644 --- a/lib/galaxy/tools/data_manager/manager.py +++ b/lib/galaxy/tools/data_manager/manager.py @@ -244,12 +244,12 @@ class DataManager( object ): return self.guid or self.declared_id #if we have a guid, we will use that as the data_manager id def load_tool( self, tool_filename, guid=None, data_manager_id=None, tool_shed_repository_id=None ): - tool = self.data_managers.app.toolbox.load_tool( tool_filename, - guid=guid, - data_manager_id=data_manager_id, - repository_id=tool_shed_repository_id ) + toolbox = self.data_managers.app.toolbox + tool = toolbox.load_hidden_tool( tool_filename, + guid=guid, + data_manager_id=data_manager_id, + repository_id=tool_shed_repository_id ) self.data_managers.app.toolbox.data_manager_tools[ tool.id ] = tool - self.data_managers.app.toolbox.tools_by_id[ tool.id ] = tool self.tool = tool return tool diff --git a/lib/galaxy/tools/imp_exp/__init__.py b/lib/galaxy/tools/imp_exp/__init__.py index bb3780c5448..211909977c7 100644 --- a/lib/galaxy/tools/imp_exp/__init__.py +++ b/lib/galaxy/tools/imp_exp/__init__.py @@ -40,14 +40,12 @@ def load_history_imp_exp_tools( toolbox ): tmp_name = tempfile.NamedTemporaryFile() tmp_name.write( tool_xml_text ) tmp_name.flush() - history_exp_tool = toolbox.load_tool( tmp_name.name ) - toolbox.tools_by_id[ history_exp_tool.id ] = history_exp_tool + history_exp_tool = toolbox.load_hidden_tool( tmp_name.name ) log.debug( "Loaded history export tool: %s", history_exp_tool.id ) # Load import tool. tool_xml = os.path.join( os.getcwd(), "lib/galaxy/tools/imp_exp/imp_history_from_archive.xml" ) - history_imp_tool = toolbox.load_tool( tool_xml ) - toolbox.tools_by_id[ history_imp_tool.id ] = history_imp_tool + history_imp_tool = toolbox.load_hidden_tool( tool_xml ) log.debug( "Loaded history import tool: %s", history_imp_tool.id ) From b2d6255a12219fe20be1ee4005a104afadf5f83e Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 26 Dec 2014 09:55:27 -0500 Subject: [PATCH 055/473] Remove unused method lib/galaxy/workflow/modules.py. --- lib/galaxy/workflow/modules.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index d86f167aaa9..f710c1c76aa 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -557,13 +557,6 @@ class ToolModule( WorkflowModule ): def normalize_runtime_state( self, runtime_state ): return runtime_state.encode( self.tool, self.trans.app, secure=False ) - @classmethod - def __get_tool_version( cls, trans, tool_id ): - # Return a ToolVersion if one exists for tool_id. - return trans.install_model.context.query( trans.install_model.ToolVersion ) \ - .filter( trans.install_model.ToolVersion.table.c.tool_id == tool_id ) \ - .first() - def save_to_step( self, step ): step.type = self.type step.tool_id = self.tool_id From 8c18def76025cecfbaccc9f64facb002061bf6b3 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 26 Dec 2014 09:55:27 -0500 Subject: [PATCH 056/473] Eliminate direct in/get checks against toolbox.tools_by_id - use get_tool or has_tool. --- lib/galaxy/jobs/__init__.py | 2 +- lib/galaxy/jobs/handler.py | 2 +- lib/galaxy/managers/workflows.py | 2 +- lib/galaxy/tools/__init__.py | 8 +++++++- lib/galaxy/webapps/galaxy/controllers/admin.py | 6 ++++-- test/unit/jobs/test_job_wrapper.py | 8 ++++---- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 19fd17b4131..677f5b69a30 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -711,7 +711,7 @@ class JobWrapper( object ): self.job_id = job.id self.session_id = job.session_id self.user_id = job.user_id - self.tool = queue.app.toolbox.tools_by_id.get( job.tool_id, None ) + self.tool = queue.app.toolbox.get_tool( job.tool_id, exact=True ) self.queue = queue self.app = queue.app self.sa_session = self.app.model.context diff --git a/lib/galaxy/jobs/handler.py b/lib/galaxy/jobs/handler.py index 95e1495e59e..07a859d7f16 100644 --- a/lib/galaxy/jobs/handler.py +++ b/lib/galaxy/jobs/handler.py @@ -119,7 +119,7 @@ class JobHandlerQueue( object ): & ( model.Job.handler == self.app.config.server_name ) ).all() for job in jobs_at_startup: - if job.tool_id not in self.app.toolbox.tools_by_id: + if not self.app.toolbox.has_tool( job.tool_id, exact=True ): log.warning( "(%s) Tool '%s' removed from tool config, unable to recover job" % ( job.id, job.tool_id ) ) self.job_wrapper( job ).fail( 'This tool was disabled before the job completed. Please contact your Galaxy administrator.' ) elif job.job_runner_name is not None and job.job_runner_external_id is None: diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index c50ff7967a1..c2481b38089 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -224,7 +224,7 @@ class WorkflowContentsManager(UsesAnnotations): errors = [] for key, step_dict in data['steps'].iteritems(): is_tool = is_tool_module_type( step_dict[ 'type' ] ) - if is_tool and step_dict['tool_id'] not in trans.app.toolbox.tools_by_id: + if is_tool and not trans.app.toolbox.has_tool( step_dict['tool_id'], exact=True ): errors.append("Step %s requires tool '%s'." % (step_dict['id'], step_dict['tool_id'])) if errors: raise MissingToolsException(workflow, errors) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 561ed96453e..877be289f66 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -478,8 +478,11 @@ class ToolBox( object, Dictifiable ): shutil.move( filename, os.path.abspath( self.integrated_tool_panel_config ) ) os.chmod( self.integrated_tool_panel_config, 0644 ) - def get_tool( self, tool_id, tool_version=None, get_all_versions=False ): + def get_tool( self, tool_id, tool_version=None, get_all_versions=False, exact=False ): """Attempt to locate a tool in the tool box.""" + if get_all_versions and exact: + raise AssertionError("Cannot specify get_tool with both get_all_versions and exact as True") + if tool_id in self.tools_by_id and not get_all_versions: #tool_id exactly matches an available tool by id (which is 'old' tool_id or guid) return self.tools_by_id[ tool_id ] @@ -513,6 +516,9 @@ class ToolBox( object, Dictifiable ): return[ self.tools_by_id[ tool_id ] ] return None + def has_tool( self, tool_id, exact=False ): + return self.get_tool( tool_id, exact=exact ) is not None + def get_loaded_tools_by_lineage( self, tool_id ): """Get all loaded tools associated by lineage to the tool whose id is tool_id.""" tv = self.__get_tool_version( tool_id ) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 15221c3392d..e53f32c6da0 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -486,7 +486,8 @@ class ToolVersionListGrid( grids.Grid ): class ToolIdColumn( grids.TextColumn ): def get_value( self, trans, grid, tool_version ): - if tool_version.tool_id in trans.app.toolbox.tools_by_id: + toolbox = trans.app.toolbox + if toolbox.has_tool( tool_version.tool_id, exact=True ): link = url_for( controller='tool_runner', tool_id=tool_version.tool_id ) link_str = '' % link return '' % ( link_str, tool_version.tool_id ) @@ -497,8 +498,9 @@ class ToolVersionListGrid( grids.Grid ): def get_value( self, trans, grid, tool_version ): tool_ids_str = '' + toolbox = trans.app.toolbox for tool_id in tool_version.get_version_ids( trans.app ): - if tool_id in trans.app.toolbox.tools_by_id: + if toolbox.has_tool( tool_id, exact=True ): link = url_for( controller='tool_runner', tool_id=tool_version.tool_id ) link_str = '' % link tool_ids_str += '
' % ( link_str, tool_id ) diff --git a/test/unit/jobs/test_job_wrapper.py b/test/unit/jobs/test_job_wrapper.py index 5f7139639eb..91890696d83 100644 --- a/test/unit/jobs/test_job_wrapper.py +++ b/test/unit/jobs/test_job_wrapper.py @@ -190,14 +190,14 @@ class MockToolbox(object): def __init__(self, test_tool): self.test_tool = test_tool - @property - def tools_by_id(self): - return self - def get(self, tool_id, default=None): assert tool_id == TEST_TOOL_ID return self.test_tool + def get_tool( self, tool_id, exact=False ): + tool = self.get(tool_id) + return tool + class MockObjectStore(object): From 974a4c124f3496764f30bd1fb5f09a248568f2d3 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 26 Dec 2014 09:55:27 -0500 Subject: [PATCH 057/473] Introduce abstraction shielding toolbox.tools_by_id from galaxy.workflow.modules. That was the last module outside of ToolBox itself that was accessing tools_by_id directly. --- lib/galaxy/tools/__init__.py | 15 +++++++ lib/galaxy/workflow/modules.py | 8 ++-- test/unit/tools/test_toolbox.py | 9 ++++ test/unit/workflows/test_modules.py | 55 ++++++++++++++++++++++++- test/unit/workflows/workflow_support.py | 4 ++ 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 877be289f66..c4b92a8de7a 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -519,6 +519,21 @@ class ToolBox( object, Dictifiable ): def has_tool( self, tool_id, exact=False ): return self.get_tool( tool_id, exact=exact ) is not None + def get_tool_id( self, tool_id ): + """ Take a tool id (potentially from a different Galaxy instance or that + is no longer loaded - and find the closest match to the currently loaded + tools (using get_tool for inexact matches which currently returns the oldest + tool shed installed tool with the same short id). + """ + if tool_id not in self.tools_by_id: + tool = self.get_tool( tool_id ) + if tool: + tool_id = tool.id + else: + tool_id = None + # else exact match - leave unmodified. + return tool_id + def get_loaded_tools_by_lineage( self, tool_id ): """Get all loaded tools associated by lineage to the tool whose id is tool_id.""" tv = self.__get_tool_version( tool_id ) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index f710c1c76aa..253505f7dd9 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -508,15 +508,13 @@ class ToolModule( WorkflowModule ): def from_workflow_step( Class, trans, step ): toolbox = trans.app.toolbox tool_id = step.tool_id - if toolbox and tool_id not in toolbox.tools_by_id: + if toolbox: # See if we have access to a different version of the tool. # TODO: If workflows are ever enhanced to use tool version # in addition to tool id, enhance the selection process here # to retrieve the correct version of the tool. - tool = toolbox.get_tool( tool_id ) - if tool: - tool_id = tool.id - if ( toolbox and tool_id in toolbox.tools_by_id ): + tool_id = toolbox.get_tool_id( tool_id ) + if ( toolbox and tool_id ): if step.config: # This step has its state saved in the config field due to the # tool being previously unavailable. diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 0e5685f0221..238546cf8c5 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -186,6 +186,15 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): # No changes, should be regenerated self.assert_integerated_tool_panel(exists=False) + def test_get_tool_id( self ): + self._init_tool() + self._setup_two_versions_in_config( ) + self._setup_two_versions() + assert self.toolbox.get_tool_id( "test_tool" ) == "github.com/galaxyproect/example/test_tool/0.1" + assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.1" ) == "github.com/galaxyproect/example/test_tool/0.1" + assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.2" ) == "github.com/galaxyproect/example/test_tool/0.2" + assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.3" ) is None + def __verify_two_test_tools( self ): # Assert tool versions of the tool with simple id 'test_tool' all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) diff --git a/test/unit/workflows/test_modules.py b/test/unit/workflows/test_modules.py index 811940d2ab4..3f728d34fb3 100644 --- a/test/unit/workflows/test_modules.py +++ b/test/unit/workflows/test_modules.py @@ -5,6 +5,7 @@ import json import mock from galaxy import model +from galaxy.util import bunch from galaxy.workflow import modules from .workflow_support import MockTrans @@ -201,6 +202,37 @@ def test_cannot_create_tool_modules_for_missing_tools(): assert exception +def test_updated_tool_version(): + trans = MockTrans() + mock_tool = __mock_tool(id="cat1", version="0.9") + trans.app.toolbox.tools[ "cat1" ] = mock_tool + module = __from_step( + trans=trans, + type="tool", + tool_id="cat1", + tool_version="0.7", + config=None, + ) + # Make sure there is a warnin with tool id, old version, + # and new version. + for val in "cat1", "0.7", "0.9": + assert val in module.version_changes[0] + + +def test_tool_version_same(): + trans = MockTrans() + mock_tool = __mock_tool(id="cat1", version="1.0") + trans.app.toolbox.tools[ "cat1" ] = mock_tool + module = __from_step( + trans=trans, + type="tool", + tool_id="cat1", + tool_version="1.0", + config=None, + ) + assert not module.version_changes + + def __assert_has_runtime_input( module, label=None, collection_type=None ): inputs = module.get_runtime_inputs() assert len( inputs ) == 1 @@ -221,7 +253,11 @@ def __from_state( state ): def __from_step( **kwds ): - trans = MockTrans() + if "trans" in kwds: + trans = kwds["trans"] + del kwds["trans"] + else: + trans = MockTrans() step = __step( **kwds ) @@ -238,3 +274,20 @@ def __step( **kwds ): setattr( step, key, value ) return step + + +def __mock_tool( + id="cat1", + version="1.0", +): + # For now ignoring inputs, params_from_strings, and + # check_and_update_param_values since only have unit tests for version + # handling - but need to write tests for all of this longer term. + tool = bunch.Bunch( + id=id, + version=version, + inputs={}, + params_from_strings=mock.Mock(), + check_and_update_param_values=mock.Mock(), + ) + return tool diff --git a/test/unit/workflows/workflow_support.py b/test/unit/workflows/workflow_support.py index 110a39fc912..03ae1c768f8 100644 --- a/test/unit/workflows/workflow_support.py +++ b/test/unit/workflows/workflow_support.py @@ -40,3 +40,7 @@ class TestToolbox( object ): def get_tool( self, tool_id ): # Real tool box returns None of missing tool also return self.tools.get( tool_id, None ) + + def get_tool_id( self, tool_id ): + tool = self.get_tool( tool_id ) + return tool and tool.id From 1f96fa9b33ddd67589913a893f9da8bb700850f9 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 27 Dec 2014 17:30:59 -0500 Subject: [PATCH 058/473] Fixes for over escaping in c2bed0a. Fixes dozens of tool functional tests. --- lib/galaxy/web/base/controllers/admin.py | 2 +- .../webapps/galaxy/controllers/admin.py | 2 +- .../galaxy/controllers/admin_toolshed.py | 2 +- .../webapps/tool_shed/controllers/admin.py | 2 +- .../tool_shed/controllers/repository.py | 2 +- .../controllers/repository_review.py | 2 +- .../webapps/tool_shed/controllers/upload.py | 2 +- lib/tool_shed/util/repository_util.py | 2 +- lib/tool_shed/util/web_util.py | 21 +++++++++++++++++++ 9 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 lib/tool_shed/util/web_util.py diff --git a/lib/galaxy/web/base/controllers/admin.py b/lib/galaxy/web/base/controllers/admin.py index a2a7573c66b..9673da67474 100644 --- a/lib/galaxy/web/base/controllers/admin.py +++ b/lib/galaxy/web/base/controllers/admin.py @@ -7,7 +7,7 @@ from galaxy.util import inflector from galaxy.web.form_builder import CheckboxField from string import punctuation as PUNCTUATION import galaxy.queue_worker -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.util import shed_util_common as suc diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 15221c3392d..459b64717da 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -17,7 +17,7 @@ from galaxy.web.framework.helpers import grids, time_ago from galaxy.web.params import QuotaParamParser from tool_shed.util import common_util from tool_shed.util import encoding_util -from markupsafe import escape +from tool_shed.util.web_util import escape log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py index 361ecc6ffa0..b31177fabd6 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -8,9 +8,9 @@ from galaxy import util from galaxy.web.form_builder import CheckboxField from galaxy.util import json from galaxy.model.orm import or_ -from markupsafe import escape import tool_shed.repository_types.util as rt_util +from tool_shed.util.web_util import escape from tool_shed.util import common_util from tool_shed.util import encoding_util diff --git a/lib/galaxy/webapps/tool_shed/controllers/admin.py b/lib/galaxy/webapps/tool_shed/controllers/admin.py index 73a314788fc..27c6c922337 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/admin.py +++ b/lib/galaxy/webapps/tool_shed/controllers/admin.py @@ -3,7 +3,7 @@ import logging from galaxy import util from galaxy.util import inflector from galaxy import web -from markupsafe import escape +from tool_shed.util.web_util import escape from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controllers.admin import Admin diff --git a/lib/galaxy/webapps/tool_shed/controllers/repository.py b/lib/galaxy/webapps/tool_shed/controllers/repository.py index 9d130f653be..9d8bc5ab660 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/repository.py +++ b/lib/galaxy/webapps/tool_shed/controllers/repository.py @@ -6,7 +6,6 @@ from time import gmtime from time import strftime from datetime import date from datetime import datetime -from markupsafe import escape from galaxy import util from galaxy import web @@ -19,6 +18,7 @@ from galaxy.model.orm import and_ from tool_shed.capsule import capsule_manager from tool_shed.dependencies.repository import relation_builder +from tool_shed.util.web_util import escape from tool_shed.galaxy_install import dependency_display from tool_shed.metadata import repository_metadata_manager from tool_shed.utility_containers import ToolShedUtilityContainerManager diff --git a/lib/galaxy/webapps/tool_shed/controllers/repository_review.py b/lib/galaxy/webapps/tool_shed/controllers/repository_review.py index 69e11819e9c..d6bf5278ad6 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/repository_review.py +++ b/lib/galaxy/webapps/tool_shed/controllers/repository_review.py @@ -2,7 +2,7 @@ import logging import os from sqlalchemy.sql.expression import func -from markupsafe import escape +from tool_shed.util.web_util import escape from galaxy import util from galaxy import web diff --git a/lib/galaxy/webapps/tool_shed/controllers/upload.py b/lib/galaxy/webapps/tool_shed/controllers/upload.py index 5889c2fd9f9..338ef08c98d 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/upload.py +++ b/lib/galaxy/webapps/tool_shed/controllers/upload.py @@ -9,7 +9,7 @@ from galaxy import util from galaxy import web from galaxy.datatypes import checkers from galaxy.web.base.controller import BaseUIController -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.dependencies import attribute_handlers from tool_shed.galaxy_install import dependency_display diff --git a/lib/tool_shed/util/repository_util.py b/lib/tool_shed/util/repository_util.py index 8ee97941840..b54bf0b6ec3 100644 --- a/lib/tool_shed/util/repository_util.py +++ b/lib/tool_shed/util/repository_util.py @@ -7,7 +7,7 @@ from galaxy import util from galaxy import web from galaxy.web.form_builder import build_select_field from galaxy.webapps.tool_shed.model import directory_hash_id -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.dependencies.repository import relation_builder diff --git a/lib/tool_shed/util/web_util.py b/lib/tool_shed/util/web_util.py new file mode 100644 index 00000000000..c75f3747ff3 --- /dev/null +++ b/lib/tool_shed/util/web_util.py @@ -0,0 +1,21 @@ +from markupsafe import escape as raw_escape + +ALLOWED_ELEMENTS = ["", "", "
"] +ALLOWED_MAP = dict(map(lambda x: (x, raw_escape(x)), ALLOWED_ELEMENTS)) + + +def escape( value ): + """ A tool shed variant of markupsafe.escape that allows a select few + HTML elements that are repeatedly used in messages created deep + in the toolshed components. Ideally abstract things would be produced + in these components and messages in the views or client side - this is + what should be worked toward - but for now - we have this hack. + + >>> escape("A repo") + u'A repo' + """ + escaped = str( raw_escape( value ) ) + # Unescape few selected tags. + for key, value in ALLOWED_MAP.iteritems(): + escaped = escaped.replace(value, key) + return escaped From f01ae9f7b3b9d4f4a10b7799ae5ac235f2e1b126 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 27 Dec 2014 17:30:59 -0500 Subject: [PATCH 059/473] Fixes for over escaping in c2bed0a. Fixes dozens of tool functional tests. --- lib/galaxy/web/base/controllers/admin.py | 2 +- .../webapps/galaxy/controllers/admin.py | 2 +- .../galaxy/controllers/admin_toolshed.py | 2 +- .../webapps/tool_shed/controllers/admin.py | 2 +- .../tool_shed/controllers/repository.py | 2 +- .../controllers/repository_review.py | 2 +- .../webapps/tool_shed/controllers/upload.py | 2 +- lib/tool_shed/util/repository_util.py | 2 +- lib/tool_shed/util/web_util.py | 21 +++++++++++++++++++ 9 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 lib/tool_shed/util/web_util.py diff --git a/lib/galaxy/web/base/controllers/admin.py b/lib/galaxy/web/base/controllers/admin.py index e7860f62d60..7e24f19928b 100644 --- a/lib/galaxy/web/base/controllers/admin.py +++ b/lib/galaxy/web/base/controllers/admin.py @@ -7,7 +7,7 @@ from galaxy.util import inflector from galaxy.web.form_builder import CheckboxField from string import punctuation as PUNCTUATION import galaxy.queue_worker -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.util import shed_util_common as suc diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 15221c3392d..459b64717da 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -17,7 +17,7 @@ from galaxy.web.framework.helpers import grids, time_ago from galaxy.web.params import QuotaParamParser from tool_shed.util import common_util from tool_shed.util import encoding_util -from markupsafe import escape +from tool_shed.util.web_util import escape log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py index 361ecc6ffa0..b31177fabd6 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -8,9 +8,9 @@ from galaxy import util from galaxy.web.form_builder import CheckboxField from galaxy.util import json from galaxy.model.orm import or_ -from markupsafe import escape import tool_shed.repository_types.util as rt_util +from tool_shed.util.web_util import escape from tool_shed.util import common_util from tool_shed.util import encoding_util diff --git a/lib/galaxy/webapps/tool_shed/controllers/admin.py b/lib/galaxy/webapps/tool_shed/controllers/admin.py index 73a314788fc..27c6c922337 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/admin.py +++ b/lib/galaxy/webapps/tool_shed/controllers/admin.py @@ -3,7 +3,7 @@ import logging from galaxy import util from galaxy.util import inflector from galaxy import web -from markupsafe import escape +from tool_shed.util.web_util import escape from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controllers.admin import Admin diff --git a/lib/galaxy/webapps/tool_shed/controllers/repository.py b/lib/galaxy/webapps/tool_shed/controllers/repository.py index 9d130f653be..9d8bc5ab660 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/repository.py +++ b/lib/galaxy/webapps/tool_shed/controllers/repository.py @@ -6,7 +6,6 @@ from time import gmtime from time import strftime from datetime import date from datetime import datetime -from markupsafe import escape from galaxy import util from galaxy import web @@ -19,6 +18,7 @@ from galaxy.model.orm import and_ from tool_shed.capsule import capsule_manager from tool_shed.dependencies.repository import relation_builder +from tool_shed.util.web_util import escape from tool_shed.galaxy_install import dependency_display from tool_shed.metadata import repository_metadata_manager from tool_shed.utility_containers import ToolShedUtilityContainerManager diff --git a/lib/galaxy/webapps/tool_shed/controllers/repository_review.py b/lib/galaxy/webapps/tool_shed/controllers/repository_review.py index 69e11819e9c..d6bf5278ad6 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/repository_review.py +++ b/lib/galaxy/webapps/tool_shed/controllers/repository_review.py @@ -2,7 +2,7 @@ import logging import os from sqlalchemy.sql.expression import func -from markupsafe import escape +from tool_shed.util.web_util import escape from galaxy import util from galaxy import web diff --git a/lib/galaxy/webapps/tool_shed/controllers/upload.py b/lib/galaxy/webapps/tool_shed/controllers/upload.py index 17a4f871d1a..73624d5e644 100644 --- a/lib/galaxy/webapps/tool_shed/controllers/upload.py +++ b/lib/galaxy/webapps/tool_shed/controllers/upload.py @@ -9,7 +9,7 @@ from galaxy import util from galaxy import web from galaxy.datatypes import checkers from galaxy.web.base.controller import BaseUIController -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.dependencies import attribute_handlers from tool_shed.galaxy_install import dependency_display diff --git a/lib/tool_shed/util/repository_util.py b/lib/tool_shed/util/repository_util.py index 8ee97941840..b54bf0b6ec3 100644 --- a/lib/tool_shed/util/repository_util.py +++ b/lib/tool_shed/util/repository_util.py @@ -7,7 +7,7 @@ from galaxy import util from galaxy import web from galaxy.web.form_builder import build_select_field from galaxy.webapps.tool_shed.model import directory_hash_id -from markupsafe import escape +from tool_shed.util.web_util import escape from tool_shed.dependencies.repository import relation_builder diff --git a/lib/tool_shed/util/web_util.py b/lib/tool_shed/util/web_util.py new file mode 100644 index 00000000000..c75f3747ff3 --- /dev/null +++ b/lib/tool_shed/util/web_util.py @@ -0,0 +1,21 @@ +from markupsafe import escape as raw_escape + +ALLOWED_ELEMENTS = ["", "", "
"] +ALLOWED_MAP = dict(map(lambda x: (x, raw_escape(x)), ALLOWED_ELEMENTS)) + + +def escape( value ): + """ A tool shed variant of markupsafe.escape that allows a select few + HTML elements that are repeatedly used in messages created deep + in the toolshed components. Ideally abstract things would be produced + in these components and messages in the views or client side - this is + what should be worked toward - but for now - we have this hack. + + >>> escape("A repo") + u'A repo' + """ + escaped = str( raw_escape( value ) ) + # Unescape few selected tags. + for key, value in ALLOWED_MAP.iteritems(): + escaped = escaped.replace(value, key) + return escaped From 37f2504ac48784c8098efc470798242a26a2d8c5 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 27 Dec 2014 17:49:16 -0500 Subject: [PATCH 060/473] Add --no_cleanup option to tests. --- run_tests.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/run_tests.sh b/run_tests.sh index f3592854f43..5f5924736e4 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -21,6 +21,12 @@ cat < Date: Sat, 27 Dec 2014 17:49:16 -0500 Subject: [PATCH 061/473] Skip checking tool migrations for Galaxy in tool shed tests. --- test/tool_shed/functional_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tool_shed/functional_tests.py b/test/tool_shed/functional_tests.py index 099f213362e..8a9070df7f1 100644 --- a/test/tool_shed/functional_tests.py +++ b/test/tool_shed/functional_tests.py @@ -359,6 +359,7 @@ def main(): database_connection = galaxy_database_connection, database_auto_migrate = galaxy_database_auto_migrate, datatype_converters_config_file = "datatype_converters_conf.xml.sample", + check_migrate_tools = False, enable_tool_shed_check = True, file_path = galaxy_file_path, global_conf = galaxy_global_conf, From 6b6c3ac498fb7a25413e13c7b95aaff5a8b8d98f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 27 Dec 2014 23:46:22 -0500 Subject: [PATCH 062/473] Fix remaining tool shed test cases for new escaped values. I think in this case the values on the page are correct now - but the test framework needs to be updated to handle the escaped value. --- test/tool_shed/base/twilltestcase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/tool_shed/base/twilltestcase.py b/test/tool_shed/base/twilltestcase.py index 3d49066031a..9138212692d 100644 --- a/test/tool_shed/base/twilltestcase.py +++ b/test/tool_shed/base/twilltestcase.py @@ -430,6 +430,8 @@ class ShedTwillTestCase( TwillTestCase ): url = '/admin_toolshed/manage_repository?id=%s' % self.security.encode_id( installed_repository.id ) self.visit_galaxy_url( url ) strings_displayed.append( str( installed_repository.installed_changeset_revision ) ) + # Every place Galaxy's XXXX tool appears in attribute - need to quote. + strings_displayed = map( lambda x: x.replace("'", "'"), strings_displayed ) self.check_for_strings( strings_displayed, strings_not_displayed ) def display_installed_workflow_image( self, repository, workflow_name, strings_displayed=[], strings_not_displayed=[] ): From 9c2f32fc0d7f25762c99a638896200967739b257 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 063/473] PEP-8 fixes for a few Tool Shed install components. - lib/tool_shed/galaxy_install/install_manager.py - lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py - lib/tool_shed/galaxy_install/repair_repository_manager.py --- .../galaxy_install/install_manager.py | 86 +++++++++---------- .../repair_repository_manager.py | 6 +- .../repository_dependency_manager.py | 55 ++++++------ 3 files changed, 68 insertions(+), 79 deletions(-) diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index 7bc50a4d3e9..4d2ff603a32 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -42,11 +42,11 @@ log = logging.getLogger( __name__ ) class InstallToolDependencyManager( object ): - + def __init__( self, app ): self.app = app self.install_model = self.app.install_model - self.INSTALL_ACTIONS = [ 'download_binary', 'download_by_url', 'download_file', + self.INSTALL_ACTIONS = [ 'download_binary', 'download_by_url', 'download_file', 'setup_perl_environment', 'setup_python_environment', 'setup_r_environment', 'setup_ruby_environment', 'shell_command' ] @@ -142,9 +142,9 @@ class InstallToolDependencyManager( object ): # Since there was an installation error, update the tool dependency status to Error. The remove_installation_path option must # be left False here. error_message = '%s\n%s' % ( self.format_traceback(), str( e ) ) - tool_dependency = tool_dependency_util.handle_tool_dependency_installation_error( self.app, - tool_dependency, - error_message, + tool_dependency = tool_dependency_util.handle_tool_dependency_installation_error( self.app, + tool_dependency, + error_message, remove_installation_path=False ) tool_dependency = self.mark_tool_dependency_installed( tool_dependency ) return tool_dependency @@ -208,9 +208,9 @@ class InstallToolDependencyManager( object ): tool_dependency_db_records=tool_dependencies ) if ( tool_dependency.type == 'package' and proceed_with_install ): try: - tool_dependency = self.install_package( elem, - tool_shed_repository, - tool_dependencies=tool_dependencies, + tool_dependency = self.install_package( elem, + tool_shed_repository, + tool_dependencies=tool_dependencies, from_tool_migration_manager=from_tool_migration_manager ) except Exception, e: error_message = "Error installing tool dependency %s version %s: %s" % \ @@ -220,9 +220,9 @@ class InstallToolDependencyManager( object ): # Since there was an installation error, update the tool dependency status to Error. The # remove_installation_path option must be left False here. tool_dependency = \ - tool_dependency_util.handle_tool_dependency_installation_error( self.app, - tool_dependency, - error_message, + tool_dependency_util.handle_tool_dependency_installation_error( self.app, + tool_dependency, + error_message, remove_installation_path=False ) if tool_dependency and tool_dependency.status in [ self.install_model.ToolDependency.installation_status.INSTALLED, self.install_model.ToolDependency.installation_status.ERROR ]: @@ -236,7 +236,7 @@ class InstallToolDependencyManager( object ): def install_via_fabric( self, tool_shed_repository, tool_dependency, install_dir, package_name=None, custom_fabfile_path=None, actions_elem=None, action_elem=None, **kwd ): """ - Parse a tool_dependency.xml file's tag set to gather information for installation using + Parse a tool_dependency.xml file's tag set to gather information for installation using self.install_and_build_package(). The use of fabric is being eliminated, so some of these functions may need to be renamed at some point. """ @@ -363,8 +363,8 @@ class InstallToolDependencyManager( object ): else: # Process the next matching tag, or any defined tags that do not # contain platform dependent recipes. - log.debug( 'Error downloading binary for tool dependency %s version %s: %s' % \ - ( str( package_name ), str( package_version ), str( tool_dependency.error_message ) ) ) + log.debug( 'Error downloading binary for tool dependency %s version %s: %s' % + ( str( package_name ), str( package_version ), str( tool_dependency.error_message ) ) ) else: if actions_elem.tag == 'actions': # We've reached an tag that defines the recipe for installing and compiling from @@ -380,7 +380,7 @@ class InstallToolDependencyManager( object ): if removed: can_install_from_source = True else: - log.debug( 'Error removing old files from installation directory %s: %s' % \ + log.debug( 'Error removing old files from installation directory %s: %s' % ( str( installation_directory, str( error_message ) ) ) ) else: can_install_from_source = True @@ -389,7 +389,7 @@ class InstallToolDependencyManager( object ): if can_install_from_source: # We now know that binary installation was not successful, so proceed with the # tag set that defines the recipe to install and compile from source. - log.debug( 'Proceeding with install and compile recipe for tool dependency %s.' % \ + log.debug( 'Proceeding with install and compile recipe for tool dependency %s.' % str( tool_dependency.name ) ) tool_dependency = self.install_via_fabric( tool_shed_repository, tool_dependency, @@ -398,7 +398,7 @@ class InstallToolDependencyManager( object ): actions_elem=actions_elem, action_elem=None ) if actions_elem.tag == 'action' and \ - tool_dependency.status != self.install_model.ToolDependency.installation_status.ERROR: + tool_dependency.status != self.install_model.ToolDependency.installation_status.ERROR: # If the tool dependency is not in an error state, perform any final actions that have been # defined within the actions_group tag set, but outside of an tag, which defines # the recipe for installing and compiling from source. @@ -419,17 +419,17 @@ class InstallToolDependencyManager( object ): actions_elem=actions_elems, action_elem=None ) if tool_dependency.status != self.install_model.ToolDependency.installation_status.ERROR: - log.debug( 'Tool dependency %s version %s has been installed in %s.' % \ - ( str( package_name ), str( package_version ), str( install_dir ) ) ) + log.debug( 'Tool dependency %s version %s has been installed in %s.' % + ( str( package_name ), str( package_version ), str( install_dir ) ) ) return tool_dependency def mark_tool_dependency_installed( self, tool_dependency ): if tool_dependency.status not in [ self.install_model.ToolDependency.installation_status.ERROR, self.install_model.ToolDependency.installation_status.INSTALLED ]: - log.debug( 'Changing status for tool dependency %s from %s to %s.' % \ - ( str( tool_dependency.name ), - str( tool_dependency.status ), - str( self.install_model.ToolDependency.installation_status.INSTALLED ) ) ) + log.debug( 'Changing status for tool dependency %s from %s to %s.' % + ( str( tool_dependency.name ), + str( tool_dependency.status ), + str( self.install_model.ToolDependency.installation_status.INSTALLED ) ) ) status = self.install_model.ToolDependency.installation_status.INSTALLED tool_dependency = tool_dependency_util.set_tool_dependency_attributes( self.app, tool_dependency=tool_dependency, @@ -531,10 +531,10 @@ class InstallRepositoryManager( object ): self.install_model.context.add( tool_shed_repository ) self.install_model.context.flush() if 'tool_dependencies' in irmm_metadata_dict and not reinstalling: - tool_dependencies = tool_dependency_util.create_tool_dependency_objects( self.app, - tool_shed_repository, - relative_install_dir, - set_status=True ) + tool_dependency_util.create_tool_dependency_objects( self.app, + tool_shed_repository, + relative_install_dir, + set_status=True ) if 'sample_files' in irmm_metadata_dict: sample_files = irmm_metadata_dict.get( 'sample_files', [] ) tool_index_sample_files = tdtm.get_tool_index_sample_files( sample_files ) @@ -579,12 +579,12 @@ class InstallRepositoryManager( object ): new_install=True ) if 'data_manager' in irmm_metadata_dict: dmh = data_manager.DataManagerHandler( self.app ) - new_data_managers = dmh.install_data_managers( self.app.config.shed_data_manager_config_file, - irmm_metadata_dict, - shed_config_dict, - relative_install_dir, - tool_shed_repository, - repository_tools_tups ) + dmh.install_data_managers( self.app.config.shed_data_manager_config_file, + irmm_metadata_dict, + shed_config_dict, + relative_install_dir, + tool_shed_repository, + repository_tools_tups ) if 'datatypes' in irmm_metadata_dict: tool_shed_repository.status = self.install_model.ToolShedRepository.installation_status.LOADING_PROPRIETARY_DATATYPES if not tool_shed_repository.includes_datatypes: @@ -656,13 +656,9 @@ class InstallRepositoryManager( object ): tool_shed_url = installation_dict[ 'tool_shed_url' ] # Handle contained tools. if includes_tools_for_display_in_tool_panel and ( new_tool_panel_section_label or tool_panel_section_id ): - tool_panel_section_key, tool_section = \ - self.tpm.handle_tool_panel_section( self.app.toolbox, - tool_panel_section_id=tool_panel_section_id, - new_tool_panel_section_label=new_tool_panel_section_label ) - else: - tool_panel_section_key = None - tool_section = None + self.tpm.handle_tool_panel_section( self.app.toolbox, + tool_panel_section_id=tool_panel_section_id, + new_tool_panel_section_label=new_tool_panel_section_label ) encoded_repository_ids = [ self.app.security.encode_id( tsr.id ) for tsr in created_or_updated_tool_shed_repositories ] new_kwd = dict( includes_tools=includes_tools, includes_tools_for_display_in_tool_panel=includes_tools_for_display_in_tool_panel, @@ -679,7 +675,7 @@ class InstallRepositoryManager( object ): tool_shed_repository_ids=encoded_repository_ids, tool_shed_url=tool_shed_url ) encoded_kwd = encoding_util.tool_shed_encode( new_kwd ) - tsr_ids = [ r.id for r in created_or_updated_tool_shed_repositories ] + tsr_ids = [ r.id for r in created_or_updated_tool_shed_repositories ] tool_shed_repositories = [] for tsr_id in tsr_ids: tsr = self.install_model.context.query( self.install_model.ToolShedRepository ).get( tsr_id ) @@ -895,10 +891,10 @@ class InstallRepositoryManager( object ): # Get the tool_dependencies.xml file from the repository. tool_dependencies_config = hg_util.get_config_from_disk( 'tool_dependencies.xml', install_dir ) itdm = InstallToolDependencyManager( self.app ) - installed_tool_dependencies = itdm.install_specified_tool_dependencies( tool_shed_repository=tool_shed_repository, - tool_dependencies_config=tool_dependencies_config, - tool_dependencies=tool_shed_repository.tool_dependencies, - from_tool_migration_manager=False ) + itdm.install_specified_tool_dependencies( tool_shed_repository=tool_shed_repository, + tool_dependencies_config=tool_dependencies_config, + tool_dependencies=tool_shed_repository.tool_dependencies, + from_tool_migration_manager=False ) basic_util.remove_dir( work_dir ) self.update_tool_shed_repository_status( tool_shed_repository, self.install_model.ToolShedRepository.installation_status.INSTALLED ) diff --git a/lib/tool_shed/galaxy_install/repair_repository_manager.py b/lib/tool_shed/galaxy_install/repair_repository_manager.py index 1e749c0f940..aa5aff6018e 100644 --- a/lib/tool_shed/galaxy_install/repair_repository_manager.py +++ b/lib/tool_shed/galaxy_install/repair_repository_manager.py @@ -65,7 +65,7 @@ class RepairRepositoryManager(): repair_dict = {} irm = install_manager.InstallRepositoryManager( self.app ) # Get a dictionary of all repositories upon which the contents of the current repository_metadata - #record depend. + # record depend. repository_dependencies_dict = rdim.get_repository_dependencies_for_installed_tool_shed_repository( self.app, repository ) if repository_dependencies_dict: @@ -142,14 +142,14 @@ class RepairRepositoryManager(): return repo_info_dict, tool_panel_section_key def repair_tool_shed_repository( self, repository, repo_info_dict ): - + def add_repair_dict_entry( repository_name, error_message ): if repository_name in repair_dict: repair_dict[ repository_name ].append( error_message ) else: repair_dict[ repository_name ] = [ error_message ] return repair_dict - + metadata = repository.metadata repair_dict = {} tpm = tool_panel_manager.ToolPanelManager( self.app ) diff --git a/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py b/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py index b0e4d68c298..e9f0c31af00 100644 --- a/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py +++ b/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py @@ -38,13 +38,13 @@ class RepositoryDependencyInstallManager( object ): for repo_info_dict in repo_info_dicts: for name, repo_info_tuple in repo_info_dict.items(): description, \ - repository_clone_url, \ - changeset_revision, \ - ctx_rev, \ - repository_owner, \ - repository_dependencies, \ - tool_dependencies = \ - suc.get_repo_info_tuple_contents( repo_info_tuple ) + repository_clone_url, \ + changeset_revision, \ + ctx_rev, \ + repository_owner, \ + repository_dependencies, \ + tool_dependencies = \ + suc.get_repo_info_tuple_contents( repo_info_tuple ) if repository_dependencies: for key, val in repository_dependencies.items(): if key in [ 'root_key', 'description' ]: @@ -71,12 +71,12 @@ class RepositoryDependencyInstallManager( object ): for repository_dependency_components_list in val: required_repository = None rd_toolshed, \ - rd_name, \ - rd_owner, \ - rd_changeset_revision, \ - rd_prior_installation_required, \ - rd_only_if_compiling_contained_td = \ - common_util.parse_repository_dependency_tuple( repository_dependency_components_list ) + rd_name, \ + rd_owner, \ + rd_changeset_revision, \ + rd_prior_installation_required, \ + rd_only_if_compiling_contained_td = \ + common_util.parse_repository_dependency_tuple( repository_dependency_components_list ) # Get the the tool_shed_repository defined by rd_name, rd_owner and rd_changeset_revision. This # is the repository that will be required by the current d_repository. # TODO: Check tool_shed_repository.tool_shed as well when repository dependencies across tool sheds is supported. @@ -101,10 +101,10 @@ class RepositoryDependencyInstallManager( object ): repository_dependency = self.get_repository_dependency_by_repository_id( install_model, required_repository.id ) if not repository_dependency: - log.debug( 'Creating new repository_dependency record for installed revision %s of repository: %s owned by %s.' % \ - ( str( required_repository.installed_changeset_revision ), - str( required_repository.name ), - str( required_repository.owner ) ) ) + log.debug( 'Creating new repository_dependency record for installed revision %s of repository: %s owned by %s.' % + ( str( required_repository.installed_changeset_revision ), + str( required_repository.name ), + str( required_repository.owner ) ) ) repository_dependency = install_model.RepositoryDependency( tool_shed_repository_id=required_repository.id ) install_model.context.add( repository_dependency ) install_model.context.flush() @@ -159,13 +159,13 @@ class RepositoryDependencyInstallManager( object ): for name, repo_info_tuple in repo_info_dict.items(): can_update_db_record = False description, \ - repository_clone_url, \ - changeset_revision, \ - ctx_rev, \ - repository_owner, \ - repository_dependencies, \ - tool_dependencies = \ - suc.get_repo_info_tuple_contents( repo_info_tuple ) + repository_clone_url, \ + changeset_revision, \ + ctx_rev, \ + repository_owner, \ + repository_dependencies, \ + tool_dependencies = \ + suc.get_repo_info_tuple_contents( repo_info_tuple ) # See if the repository has an existing record in the database. repository_db_record, installed_changeset_revision = \ suc.repository_was_previously_installed( self.app, tool_shed_url, name, repo_info_tuple, from_tip=False ) @@ -190,8 +190,6 @@ class RepositoryDependencyInstallManager( object ): # record in the database. name = repository_db_record.name installed_changeset_revision = repository_db_record.installed_changeset_revision - metadata_dict = repository_db_record.metadata - dist_to_shed = repository_db_record.dist_to_shed can_update_db_record = True elif repository_db_record.status in [ install_model.ToolShedRepository.installation_status.DEACTIVATED ]: # The current tool shed repository is deactivated, so updating its database record @@ -209,8 +207,6 @@ class RepositoryDependencyInstallManager( object ): else: # No record exists in the database for the repository currently being processed. installed_changeset_revision = changeset_revision - metadata_dict = {} - dist_to_shed = False can_update_db_record = True if can_update_db_record: # The database record for the tool shed repository currently being processed can be updated. @@ -348,13 +344,10 @@ class RepositoryDependencyInstallManager( object ): # a dependency of the dependent repository's contained tool dependency, and only if that # tool dependency requires compilation. # For backward compatibility to the 12/20/12 Galaxy release. - prior_installation_required = 'False' only_if_compiling_contained_td = 'False' if len( components_list ) == 4: - prior_installation_required = 'False' only_if_compiling_contained_td = 'False' elif len( components_list ) == 5: - prior_installation_required = components_list[ 4 ] only_if_compiling_contained_td = 'False' if not asbool( only_if_compiling_contained_td ): if components_list not in required_repository_tups: From 9e3713da906b16828068ae2467026d37577461a8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 064/473] Tweak two ToolShed modules to clarify they don't use ToolSection... ... objects (directly anyway). I would like to refactor all the tool shed components to not deal directly with ToolSection objects but instead just pass around IDs. --- lib/tool_shed/galaxy_install/repair_repository_manager.py | 4 ++-- .../repository_dependencies/repository_dependency_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/tool_shed/galaxy_install/repair_repository_manager.py b/lib/tool_shed/galaxy_install/repair_repository_manager.py index aa5aff6018e..6b105245fbe 100644 --- a/lib/tool_shed/galaxy_install/repair_repository_manager.py +++ b/lib/tool_shed/galaxy_install/repair_repository_manager.py @@ -123,7 +123,7 @@ class RepairRepositoryManager(): tool_panel_section_name = section_dict[ 'name' ] if tool_panel_section_id: tpm = tool_panel_manager.ToolPanelManager( self.app ) - tool_panel_section_key, tool_panel_section = \ + tool_panel_section_key, _ = \ tpm.get_or_create_tool_section( self.app.toolbox, tool_panel_section_id=tool_panel_section_id, new_tool_panel_section_label=tool_panel_section_name ) @@ -165,7 +165,7 @@ class RepairRepositoryManager(): suc.get_tool_panel_config_tool_path_install_dir( self.app, repository ) # Reset the repository attributes to the New state for installation. if metadata: - tool_section, tool_panel_section_key = \ + _, tool_panel_section_key = \ tpm.handle_tool_panel_selection( self.app.toolbox, metadata, no_changes_checked=True, diff --git a/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py b/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py index e9f0c31af00..f23eb8dc405 100644 --- a/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py +++ b/lib/tool_shed/galaxy_install/repository_dependencies/repository_dependency_manager.py @@ -213,7 +213,7 @@ class RepositoryDependencyInstallManager( object ): # Get the repository metadata to see where it was previously located in the tool panel. tpm = tool_panel_manager.ToolPanelManager( self.app ) if repository_db_record and repository_db_record.metadata: - tool_section, tool_panel_section_key = \ + _, tool_panel_section_key = \ tpm.handle_tool_panel_selection( toolbox=self.app.toolbox, metadata=repository_db_record.metadata, no_changes_checked=no_changes_checked, @@ -221,7 +221,7 @@ class RepositoryDependencyInstallManager( object ): new_tool_panel_section_label=new_tool_panel_section_label ) else: # We're installing a new tool shed repository that does not yet have a database record. - tool_panel_section_key, tool_section = \ + tool_panel_section_key, _ = \ tpm.handle_tool_panel_section( self.app.toolbox, tool_panel_section_id=tool_panel_section_id, new_tool_panel_section_label=new_tool_panel_section_label ) From 7af172dbda0638f8f58fbc239d473feae9bd3d22 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 065/473] More centralization of code for registering tools in ToolBox. --- lib/galaxy/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index c4b92a8de7a..a242047cde7 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -679,7 +679,7 @@ class ToolBox( object, Dictifiable ): # changed, so the tool should always be reloaded here. We used to # only load the tool if it was not found in self.tools_by_id, but # performing that check did not enable this scenario. - self.tools_by_id[ tool.id ] = tool + self.register_tool( tool ) if load_panel_dict: self.__add_tool_to_tool_panel( tool, panel_dict, section=isinstance( panel_dict, ToolSection ) ) From 4068aaac92f40676d84ad42b23d412d2cb0b18d5 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 066/473] Eliminate one reference to ToolBox.tool_panel in install_manager.py. --- lib/galaxy/tools/__init__.py | 18 ++++++++++++++++++ .../galaxy_install/install_manager.py | 11 +---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index a242047cde7..a3b4cf02dd0 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1021,6 +1021,24 @@ class ToolBox( object, Dictifiable ): status = 'done' return message, status + def find_section_id( self, tool_panel_section_id ): + """ + Find the section ID referenced by the key or return '' indicating + no such section id. + """ + if not tool_panel_section_id: + tool_panel_section_id = '' + else: + if tool_panel_section_id not in self.tool_panel: + # Hack introduced without comment in a29d54619813d5da992b897557162a360b8d610c- + # not sure why it is needed. + fixed_tool_panel_section_id = 'section_%s' % tool_panel_section_id + if fixed_tool_panel_section_id in self.tool_panel: + tool_panel_section_id = fixed_tool_panel_section_id + else: + tool_panel_section_id = '' + return tool_panel_section_id + def load_workflow( self, workflow_id ): """ Return an instance of 'Workflow' identified by `id`, diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index 4d2ff603a32..8d5b8d21fa3 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -742,16 +742,7 @@ class InstallRepositoryManager( object ): tool_path = shed_config_dict[ 'tool_path' ] if not shed_tool_conf: raise exceptions.RequestParameterMissingException( "Missing required parameter 'shed_tool_conf'." ) - tool_panel_section_id = install_options.get( 'tool_panel_section_id', '' ) - if tool_panel_section_id not in [ None, '' ]: - if tool_panel_section_id not in self.app.toolbox.tool_panel: - fixed_tool_panel_section_id = 'section_%s' % tool_panel_section_id - if fixed_tool_panel_section_id in self.app.toolbox.tool_panel: - tool_panel_section_id = fixed_tool_panel_section_id - else: - tool_panel_section_id = '' - else: - tool_panel_section_id = '' + tool_panel_section_id = self.app.toolbox.find_section_id( install_options.get( 'tool_panel_section_id', '' ) ) # Build the dictionary of information necessary for creating tool_shed_repository database records # for each repository being installed. installation_dict = dict( install_repository_dependencies=install_repository_dependencies, From 36599a16d2fcc93863e0053a150702446ab7b984 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 067/473] Simplify shed-y logic in load_tool_tag_set. Drops cycolomatic complexity from 14 to 10 for that method. --- lib/galaxy/tools/__init__.py | 52 ++++++++++++++++++--------------- test/unit/tools/test_toolbox.py | 21 +++++++++++++ 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index a3b4cf02dd0..343034c8f1b 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -559,6 +559,23 @@ class ToolBox( object, Dictifiable ): def tools( self ): return self.tools_by_id.iteritems() + def __elem_to_tool_shed_repository( self, elem ): + # The tool is contained in an installed tool shed repository, so load + # the tool only if the repository has not been marked deleted. + tool_shed = elem.find( "tool_shed" ).text + repository_name = elem.find( "repository_name" ).text + repository_owner = elem.find( "repository_owner" ).text + installed_changeset_revision_elem = elem.find( "installed_changeset_revision" ) + if installed_changeset_revision_elem is None: + # Backward compatibility issue - the tag used to be named 'changeset_revision'. + installed_changeset_revision_elem = elem.find( "changeset_revision" ) + installed_changeset_revision = installed_changeset_revision_elem.text + tool_shed_repository = self.__get_tool_shed_repository( tool_shed, + repository_name, + repository_owner, + installed_changeset_revision ) + return tool_shed_repository + def __get_tool_shed_repository( self, tool_shed, name, owner, installed_changeset_revision ): # We store only the port, if one exists, in the database. tool_shed = common_util.remove_protocol_from_tool_shed_url( tool_shed ) @@ -613,42 +630,29 @@ class ToolBox( object, Dictifiable ): try: path = elem.get( "file" ) repository_id = None - if guid is None: - tool_shed_repository = None - can_load_into_panel_dict = True - else: + + tool_shed_repository = None + can_load_into_panel_dict = True + if guid is not None: # The tool is contained in an installed tool shed repository, so load # the tool only if the repository has not been marked deleted. - tool_shed = elem.find( "tool_shed" ).text - repository_name = elem.find( "repository_name" ).text - repository_owner = elem.find( "repository_owner" ).text - installed_changeset_revision_elem = elem.find( "installed_changeset_revision" ) - if installed_changeset_revision_elem is None: - # Backward compatibility issue - the tag used to be named 'changeset_revision'. - installed_changeset_revision_elem = elem.find( "changeset_revision" ) - installed_changeset_revision = installed_changeset_revision_elem.text - tool_shed_repository = self.__get_tool_shed_repository( tool_shed, - repository_name, - repository_owner, - installed_changeset_revision ) + tool_shed_repository = self.__elem_to_tool_shed_repository( elem ) if tool_shed_repository: # Only load tools if the repository is not deactivated or uninstalled. can_load_into_panel_dict = not tool_shed_repository.deleted repository_id = self.app.security.encode_id( tool_shed_repository.id ) - else: - # If there is not yet a tool_shed_repository record, we're in the process of installing - # a new repository, so any included tools can be loaded into the tool panel. - can_load_into_panel_dict = True + # Else there is not yet a tool_shed_repository record, we're in the process of installing + # a new repository, so any included tools can be loaded into the tool panel. tool = self.load_tool( os.path.join( tool_path, path ), guid=guid, repository_id=repository_id ) if string_as_bool(elem.get( 'hidden', False )): tool.hidden = True key = 'tool_%s' % str( tool.id ) if can_load_into_panel_dict: if guid is not None: - tool.tool_shed = tool_shed - tool.repository_name = repository_name - tool.repository_owner = repository_owner - tool.installed_changeset_revision = installed_changeset_revision + tool.tool_shed = tool_shed_repository.tool_shed + tool.repository_name = tool_shed_repository.name + tool.repository_owner = tool_shed_repository.owner + tool.installed_changeset_revision = tool_shed_repository.installed_changeset_revision tool.guid = guid tool.version = elem.find( "version" ).text # Make sure the tool has a tool_version. diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 238546cf8c5..67251b8c879 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -136,6 +136,27 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): assert toolbox.get_tool( "test_tool" ) is not None assert toolbox.get_tool( "not_a_test_tool" ) is None + def test_tool_shed_properties( self ): + self._init_tool() + self._setup_two_versions_in_config( section=False ) + self._setup_two_versions() + + test_tool = self.toolbox.get_tool( "test_tool" ) + assert test_tool.tool_shed == "github.com" + assert test_tool.repository_owner == "galaxyproject" + assert test_tool.repository_name == "example" + assert test_tool.installed_changeset_revision == "1" + + def test_tool_shed_properties_only_on_installed_tools( self ): + self._init_tool() + self._add_config( """""" ) + toolbox = self.toolbox + test_tool = toolbox.get_tool( "test_tool" ) + assert test_tool.tool_shed is None + assert test_tool.repository_name is None + assert test_tool.repository_owner is None + assert test_tool.installed_changeset_revision is None + def test_load_file_in_section( self ): self._init_tool() self._add_config( """
""" ) From 280dc1844264df9fc7f38d28d8393c233d305ac5 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 11:23:44 -0500 Subject: [PATCH 068/473] De-duplication for loading elements into the tool panel. Create one public method load_item on ToolBox that should be used externally instead of load_tool_tag_set, load_section_tag_set, etc.... This reduces the duplication within the toolbox and between the toolbox and the Tool Shed's ToolPanelManager. Probably more importantly it also is another step down the road toward hiding implementation details such as toolbox.tool_panel and toolbox.intergrated_tool_panel from the ToolPanelManager. Already had unit test coverage for ToolPanelManager stuff here - but added test coverage for loading labels, workflows, and tool directories. --- lib/galaxy/tools/__init__.py | 58 +++++++++++------ .../tools/tool_panel_manager.py | 22 ++----- test/unit/tools/test_toolbox.py | 62 +++++++++++++++++++ 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 343034c8f1b..018128396e2 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -222,16 +222,14 @@ class ToolBox( object, Dictifiable ): self.index += 1 if parsing_shed_tool_conf: config_elems.append( elem ) - if elem.tag == 'tool': - self.load_tool_tag_set( elem, self.tool_panel, self.integrated_tool_panel, tool_path, load_panel_dict, guid=elem.get( 'guid' ), index=index ) - elif elem.tag == 'workflow': - self.load_workflow_tag_set( elem, self.tool_panel, self.integrated_tool_panel, load_panel_dict, index=index ) - elif elem.tag == 'section': - self.load_section_tag_set( elem, tool_path, load_panel_dict, index=index ) - elif elem.tag == 'label': - self.load_label_tag_set( elem, self.tool_panel, self.integrated_tool_panel, load_panel_dict, index=index ) - elif elem.tag == 'tool_dir': - self.load_tooldir_tag_set( elem, self.tool_panel, self.integrated_tool_panel, tool_path, load_panel_dict ) + self.load_item( + elem, + tool_path=tool_path, + load_panel_dict=load_panel_dict, + guid=elem.get( 'guid' ), + index=index, + internal=True + ) if parsing_shed_tool_conf: shed_tool_conf_dict = dict( config_filename=config_filename, @@ -239,6 +237,28 @@ class ToolBox( object, Dictifiable ): config_elems=config_elems ) self.shed_tool_confs.append( shed_tool_conf_dict ) + def load_item( self, elem, tool_path, panel_dict=None, integrated_panel_dict=None, load_panel_dict=True, guid=None, index=None, internal=False ): + item_type = elem.tag + if item_type not in ['tool', 'section'] and not internal: + # External calls from tool shed code cannot load labels or tool + # directories. + return + + if panel_dict is None: + panel_dict = self.tool_panel + if integrated_panel_dict is None: + integrated_panel_dict = self.integrated_tool_panel + if item_type == 'tool': + self.load_tool_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, tool_path=tool_path, load_panel_dict=load_panel_dict, guid=guid, index=index ) + elif item_type == 'workflow': + self.load_workflow_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) + elif item_type == 'section': + self.load_section_tag_set( elem, tool_path=tool_path, load_panel_dict=load_panel_dict, index=index ) + elif item_type == 'label': + self.load_label_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) + elif item_type == 'tool_dir': + self.load_tooldir_tag_set( elem, panel_dict, tool_path, integrated_panel_dict, load_panel_dict=load_panel_dict ) + def get_shed_config_dict_by_filename( self, filename, default=None ): for shed_config_dict in self.shed_tool_confs: if shed_config_dict[ 'config_filename' ] == filename: @@ -729,14 +749,16 @@ class ToolBox( object, Dictifiable ): integrated_section = ToolSection( elem ) integrated_elems = integrated_section.elems for sub_index, sub_elem in enumerate( elem ): - if sub_elem.tag == 'tool': - self.load_tool_tag_set( sub_elem, elems, integrated_elems, tool_path, load_panel_dict, guid=sub_elem.get( 'guid' ), index=sub_index ) - elif sub_elem.tag == 'workflow': - self.load_workflow_tag_set( sub_elem, elems, integrated_elems, load_panel_dict, index=sub_index ) - elif sub_elem.tag == 'label': - self.load_label_tag_set( sub_elem, elems, integrated_elems, load_panel_dict, index=sub_index ) - elif sub_elem.tag == 'tool_dir': - self.load_tooldir_tag_set( sub_elem, elems, tool_path, integrated_elems, load_panel_dict ) + self.load_item( + sub_elem, + tool_path=tool_path, + panel_dict=elems, + integrated_panel_dict=integrated_elems, + load_panel_dict=load_panel_dict, + guid=sub_elem.get( 'guid' ), + index=sub_index, + internal=True, + ) if load_panel_dict: self.tool_panel[ key ] = section # Always load sections into the integrated_tool_panel. diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index d344f99ca0a..b6778bd4374 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -58,22 +58,12 @@ class ToolPanelManager( object ): # Add the new elements to the in-memory list of config_elems. config_elems.append( config_elem ) # Load the tools into the in-memory tool panel. - if config_elem.tag == 'section': - self.app.toolbox.load_section_tag_set( config_elem, tool_path, load_panel_dict=True ) - elif config_elem.tag == 'workflow': - self.app.toolbox.load_workflow_tag_set( config_elem, - self.app.toolbox.tool_panel, - self.app.toolbox.integrated_tool_panel, - load_panel_dict=True ) - elif config_elem.tag == 'tool': - self.app.toolbox.load_tool_tag_set( - config_elem, - self.app.toolbox.tool_panel, - self.app.toolbox.integrated_tool_panel, - tool_path, - load_panel_dict=True, - guid=config_elem.get( 'guid' ) - ) + self.app.toolbox.load_item( + config_elem, + tool_path=tool_path, + load_panel_dict=True, + guid=config_elem.get( 'guid' ), + ) # Replace the old list of in-memory config_elems with the new list for this shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems self.app.toolbox.update_shed_config( index, shed_tool_conf_dict ) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 67251b8c879..2031aeac07c 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -3,6 +3,7 @@ import string import unittest from galaxy.tools import ToolBox +from galaxy import model from galaxy.model import tool_shed_install from galaxy.model.tool_shed_install import mapping import tools_support @@ -216,6 +217,67 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.2" ) == "github.com/galaxyproect/example/test_tool/0.2" assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.3" ) is None + def test_tool_dir( self ): + self._init_tool() + self._add_config( """""" % self.test_directory ) + + toolbox = self.toolbox + assert toolbox.get_tool( "test_tool" ) is not None + + def test_workflow_in_panel( self ): + stored_workflow = self.__test_workflow() + encoded_id = self.app.security.encode_id( stored_workflow.id ) + self._add_config( """""" % encoded_id ) + assert len( self.toolbox.tool_panel ) == 1 + panel_workflow = self.toolbox.tool_panel.values()[ 0 ] + assert panel_workflow == stored_workflow.latest_workflow + # TODO: test to_dict with workflows + + def test_workflow_in_section( self ): + stored_workflow = self.__test_workflow() + encoded_id = self.app.security.encode_id( stored_workflow.id ) + self._add_config( """
""" % encoded_id ) + assert len( self.toolbox.tool_panel ) == 1 + section = self.toolbox.tool_panel[ 'tid' ] + assert len( section.elems ) == 1 + panel_workflow = section.elems.values()[ 0 ] + assert panel_workflow == stored_workflow.latest_workflow + + def test_label_in_panel( self ): + self._add_config( """""" ) + assert len( self.toolbox.tool_panel ) == 2 + self.__check_test_labels( self.toolbox.tool_panel ) + + def test_label_in_section( self ): + self._add_config( """
""" ) + assert len( self.toolbox.tool_panel ) == 1 + section = self.toolbox.tool_panel[ 'tid' ] + self.__check_test_labels( section.elems ) + + def __check_test_labels( self, panel_dict ): + assert panel_dict.keys() == ["label_lab1", "label_lab2"] + label1 = panel_dict.values()[ 0 ] + assert label1.id == "lab1" + assert label1.text == "Label 1" + + label2 = panel_dict[ "label_lab2" ] + assert label2.id == "lab2" + assert label2.text == "Label 2" + + def __test_workflow( self ): + stored_workflow = model.StoredWorkflow() + workflow = model.Workflow() + workflow.stored_workflow = stored_workflow + stored_workflow.latest_workflow = workflow + user = model.User() + user.email = "test@example.com" + user.password = "passw0rD1" + stored_workflow.user = user + self.app.model.context.add( workflow ) + self.app.model.context.add( stored_workflow ) + self.app.model.context.flush() + return stored_workflow + def __verify_two_test_tools( self ): # Assert tool versions of the tool with simple id 'test_tool' all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) From 7dca5cb1f6792b37fc0c4b4529c984e342ebc9df Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 28 Dec 2014 23:25:56 -0500 Subject: [PATCH 069/473] Bugfix for 44dfa6f and typos in unit tests that caused it to go undetected. --- .../tools/tool_panel_manager.py | 6 ++-- .../test_tool_panel_manager.py | 23 +++++++-------- test/unit/tools/test_toolbox.py | 28 +++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index b6778bd4374..80bd0e10b1c 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -475,8 +475,10 @@ class ToolPanelManager( object ): else: del has_elems[ tool_key ] if uninstall: - if tool_key in toolbox.integrated_tool_panel: - del toolbox.integrated_tool_panel[ tool_key ] + if hasattr( integrated_has_elems, "elems" ): + integrated_has_elems = integrated_has_elems.elems + if tool_key in integrated_has_elems: + del integrated_has_elems[ tool_key ] # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py index 04a79eef355..9b9bd1d2da3 100644 --- a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -67,10 +67,10 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self._setup_two_versions_remove_one( section=True, uninstall=False ) self._verify_version_2_removed_from_panel( ) # Still in tool conf since not uninstalled only deactivated... - assert "github.com/galaxyproect/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + assert "github.com/galaxyproject/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() self._verify_tool_confs() - self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.1"], uninstall=False ) + self._remove_guids( ["github.com/galaxyproject/example/test_tool/0.1"], uninstall=False ) # Now no versions of this tool are returned by toolbox. all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) @@ -84,24 +84,25 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self._setup_two_versions_remove_one( section=True, uninstall=True ) self._verify_version_2_removed_from_panel( ) # Not in tool conf because it was uninstalled. - assert "github.com/galaxyproect/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + assert "github.com/galaxyproject/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + assert "tool_github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems self._verify_tool_confs() def test_deactivate_outside_section( self ): self._setup_two_versions_remove_one( section=False, uninstall=False ) self._verify_version_2_removed_from_panel( section=False ) # Still in tool conf since not uninstalled only deactivated... - assert "github.com/galaxyproect/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + assert "github.com/galaxyproject/example/test_tool/0.2" in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() self._verify_tool_confs() def test_uninstall_outside_section( self ): self._setup_two_versions_remove_one( section=False, uninstall=True ) self._verify_version_2_removed_from_panel( section=False ) # Still in tool conf since not uninstalled only deactivated... - assert "github.com/galaxyproect/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() + assert "github.com/galaxyproject/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() self._verify_tool_confs() - self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.1"], uninstall=True ) + self._remove_guids( ["github.com/galaxyproject/example/test_tool/0.1"], uninstall=True ) # Now no versions of this tool are returned by toolbox. all_versions = self.toolbox.get_tool( "test_tool", get_all_versions=True ) @@ -116,7 +117,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self._setup_two_versions_in_config( section=True ) self._setup_two_versions() self.toolbox - self._remove_guids( ["github.com/galaxyproect/example/test_tool/0.2"], uninstall=uninstall ) + self._remove_guids( ["github.com/galaxyproject/example/test_tool/0.2"], uninstall=uninstall ) def _verify_version_2_removed_from_panel( self, section=True ): # Check that test_tool now only has one version... @@ -127,12 +128,12 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): if section: section = self.toolbox.tool_panel["tid"] assert len(section.elems) == 1 - assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.1" + assert section.elems.values()[0].id == "github.com/galaxyproject/example/test_tool/0.1" - assert "github.com/galaxyproect/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems + assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems else: - self.toolbox.tool_panel.values()[0].id == "github.com/galaxyproect/example/test_tool/0.1" - assert "github.com/galaxyproect/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel + self.toolbox.tool_panel.values()[0].id == "github.com/galaxyproject/example/test_tool/0.1" + assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel def _remove_guids( self, guids, uninstall, shed_tool_conf="tool_conf.xml" ): self.tpm.remove_guids( diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 2031aeac07c..f04d92f292b 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -10,12 +10,12 @@ import tools_support CONFIG_TEST_TOOL_VERSION_TEMPLATE = string.Template( - """ + """ github.com example galaxyproject ${version} - github.com/galaxyproect/example/test_tool/0.${version} + github.com/galaxyproject/example/test_tool/0.${version} 0.${version} """ @@ -72,14 +72,14 @@ class BaseToolBoxTestCase( unittest.TestCase, tools_support.UsesApp, tools_supp def _setup_two_versions( self ): repository1 = self._repo_install( changeset="1" ) version1 = tool_shed_install.ToolVersion() - version1.tool_id = "github.com/galaxyproect/example/test_tool/0.1" + version1.tool_id = "github.com/galaxyproject/example/test_tool/0.1" version1.repository = repository1 self.app.install_model.context.add( version1 ) self.app.install_model.context.flush( ) repository2 = self._repo_install( changeset="2" ) version2 = tool_shed_install.ToolVersion() - version2.tool_id = "github.com/galaxyproect/example/test_tool/0.2" + version2.tool_id = "github.com/galaxyproject/example/test_tool/0.2" version2.repository = repository2 self.app.install_model.context.add( version2 ) @@ -184,7 +184,7 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): # Assert only newer version of the tool loaded into the panel. section = self.toolbox.tool_panel["tid"] assert len(section.elems) == 1 - assert section.elems.values()[0].id == "github.com/galaxyproect/example/test_tool/0.2" + assert section.elems.values()[0].id == "github.com/galaxyproject/example/test_tool/0.2" def test_group_tools_out_of_section( self ): self._init_tool() @@ -212,10 +212,10 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): self._init_tool() self._setup_two_versions_in_config( ) self._setup_two_versions() - assert self.toolbox.get_tool_id( "test_tool" ) == "github.com/galaxyproect/example/test_tool/0.1" - assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.1" ) == "github.com/galaxyproect/example/test_tool/0.1" - assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.2" ) == "github.com/galaxyproect/example/test_tool/0.2" - assert self.toolbox.get_tool_id( "github.com/galaxyproect/example/test_tool/0.3" ) is None + assert self.toolbox.get_tool_id( "test_tool" ) == "github.com/galaxyproject/example/test_tool/0.1" + assert self.toolbox.get_tool_id( "github.com/galaxyproject/example/test_tool/0.1" ) == "github.com/galaxyproject/example/test_tool/0.1" + assert self.toolbox.get_tool_id( "github.com/galaxyproject/example/test_tool/0.2" ) == "github.com/galaxyproject/example/test_tool/0.2" + assert self.toolbox.get_tool_id( "github.com/galaxyproject/example/test_tool/0.3" ) is None def test_tool_dir( self ): self._init_tool() @@ -285,14 +285,14 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): # Verify lineage_ids on both tools is correctly ordered. for version in ["0.1", "0.2"]: - guid = "github.com/galaxyproect/example/test_tool/" + version + guid = "github.com/galaxyproject/example/test_tool/" + version lineage_ids = self.toolbox.get_tool( guid ).lineage_ids - assert lineage_ids[ 0 ] == "github.com/galaxyproect/example/test_tool/0.1" - assert lineage_ids[ 1 ] == "github.com/galaxyproect/example/test_tool/0.2" + assert lineage_ids[ 0 ] == "github.com/galaxyproject/example/test_tool/0.1" + assert lineage_ids[ 1 ] == "github.com/galaxyproject/example/test_tool/0.2" # Test tool_version attribute. - assert self.toolbox.get_tool( "test_tool", tool_version="0.1" ).guid == "github.com/galaxyproect/example/test_tool/0.1" - assert self.toolbox.get_tool( "test_tool", tool_version="0.2" ).guid == "github.com/galaxyproect/example/test_tool/0.2" + assert self.toolbox.get_tool( "test_tool", tool_version="0.1" ).guid == "github.com/galaxyproject/example/test_tool/0.1" + assert self.toolbox.get_tool( "test_tool", tool_version="0.2" ).guid == "github.com/galaxyproject/example/test_tool/0.2" def __remove_itp( self ): os.remove( os.path) From 23e9a53e8adc7792a904f8fe99ccc22e52a1bfa4 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 070/473] Correct sample value for migrated tool config. --- config/galaxy.ini.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index b5da4842240..d25670832b4 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -148,7 +148,7 @@ paste.app_factory = galaxy.web.buildapp:app_factory # Tool config maintained by tool migration scripts. If you use the migration # scripts to install tools that have been migrated to the tool shed upon a new # release, they will be added to this tool config file. -#migrated_tools_config = migrated_tools_conf.xml +#migrated_tools_config = config/migrated_tools_conf.xml # File that contains the XML section and tool tags from all tool panel config # files integrated into a single file that defines the tool panel layout. This From caaebdf9ae65953a824e6d5c57bd645d994891d0 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 071/473] Small simplification to tool_panel_manager.py. --- .../tools/tool_panel_manager.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 80bd0e10b1c..e1cc363198d 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -243,22 +243,13 @@ class ToolPanelManager( object ): tool_file_path, tup_guid, tool = repository_tool_tup if tup_guid == guid: break - if inside_section: - tool_elem = self.generate_tool_elem( tool_shed, - repository_name, - changeset_revision, - owner, - tool_file_path, - tool, - tool_section ) - else: - tool_elem = self.generate_tool_elem( tool_shed, - repository_name, - changeset_revision, - owner, - tool_file_path, - tool, - None ) + tool_elem = self.generate_tool_elem( tool_shed, + repository_name, + changeset_revision, + owner, + tool_file_path, + tool, + tool_section if inside_section else None ) if inside_section: if section_in_elem_list: elem_list[ index ] = tool_section From 8206f83b96af1a4624a750f8cca64c98eafa136c Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 072/473] Reused get_section method to hide ToolBox.tool_panel from install_manager.py. --- lib/galaxy/tools/__init__.py | 6 ++++-- lib/tool_shed/galaxy_install/install_manager.py | 6 ++---- lib/tool_shed/galaxy_install/tools/tool_panel_manager.py | 2 +- test/unit/tool_shed_unit_tests/test_tool_panel_manager.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 018128396e2..eae5c0f8a95 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -277,13 +277,13 @@ class ToolBox( object, Dictifiable ): self.write_integrated_tool_panel_config_file() app.reindex_tool_search() - def get_or_create_section( self, section_id, new_label=None ): + def get_section( self, section_id, new_label=None, create_if_needed=False ): tool_panel_section_key = str( section_id ) if tool_panel_section_key in self.tool_panel: # Appending a tool to an existing section in toolbox.tool_panel tool_section = self.tool_panel[ tool_panel_section_key ] log.debug( "Appending to tool panel section: %s" % str( tool_section.name ) ) - else: + elif create_if_needed: # Appending a new section to toolbox.tool_panel if new_label is None: # This might add an ugly section label to the tool panel, but, oh well... @@ -296,6 +296,8 @@ class ToolBox( object, Dictifiable ): tool_section = ToolSection( section_dict ) self.tool_panel[ tool_panel_section_key ] = tool_section log.debug( "Loading new tool panel section: %s" % str( tool_section.name ) ) + else: + tool_section = None return tool_panel_section_key, tool_section def __resolve_tool_path(self, tool_path, config_filename): diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index 8d5b8d21fa3..eb580729f00 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -809,12 +809,10 @@ class InstallRepositoryManager( object ): def install_tool_shed_repository( self, tool_shed_repository, repo_info_dict, tool_panel_section_key, shed_tool_conf, tool_path, install_tool_dependencies, reinstalling=False ): if tool_panel_section_key: - try: - tool_section = self.app.toolbox.tool_panel[ tool_panel_section_key ] - except KeyError: + _, tool_section = self.app.toolbox.get_section( tool_panel_section_key ) + if tool_section is None: log.debug( 'Invalid tool_panel_section_key "%s" specified. Tools will be loaded outside of sections in the tool panel.', str( tool_panel_section_key ) ) - tool_section = None else: tool_section = None if isinstance( repo_info_dict, basestring ): diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index e1cc363198d..0e5ec00eaaa 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -297,7 +297,7 @@ class ToolPanelManager( object ): return tool_section def get_or_create_tool_section( self, toolbox, tool_panel_section_id, new_tool_panel_section_label=None ): - return toolbox.get_or_create_section( section_id=tool_panel_section_id, new_label=new_tool_panel_section_label ) + return toolbox.get_section( section_id=tool_panel_section_id, new_label=new_tool_panel_section_label, create_if_needed=True ) def get_shed_tool_conf_dict( self, shed_tool_conf ): """ diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py index 9b9bd1d2da3..422f09d59b9 100644 --- a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -46,7 +46,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self.tool, ) ] - _, section = self.toolbox.get_or_create_section("tid1") + _, section = self.toolbox.get_section("tid1", create_if_needed=True) tpm = self.tpm tool_panel_dict = tpm.generate_tool_panel_dict_for_new_install( tool_dicts=new_tools, From f88115ceaa72f1c19817064a55041c1428fa05cb Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 073/473] PEP-8 fixes for various Tool Shed components. - lib/tool_shed/util/tool_util.py - lib/tool_shed/galaxy_install/tool_migration_manager.py. - lib/tool_shed/util/shed_util_common.py. - lib/galaxy/model/tool_shed_install/__init__.py. --- .../model/tool_shed_install/__init__.py | 12 +- .../galaxy_install/tool_migration_manager.py | 23 ++-- lib/tool_shed/util/shed_util_common.py | 123 +++++++++++++----- lib/tool_shed/util/tool_util.py | 19 ++- 4 files changed, 121 insertions(+), 56 deletions(-) diff --git a/lib/galaxy/model/tool_shed_install/__init__.py b/lib/galaxy/model/tool_shed_install/__init__.py index a301e7cebf0..e30008ddc40 100644 --- a/lib/galaxy/model/tool_shed_install/__init__.py +++ b/lib/galaxy/model/tool_shed_install/__init__.py @@ -11,9 +11,9 @@ log = logging.getLogger( __name__ ) class ToolShedRepository( object ): dict_collection_visible_keys = ( 'id', 'tool_shed', 'name', 'owner', 'installed_changeset_revision', 'changeset_revision', 'ctx_rev', 'includes_datatypes', - 'tool_shed_status', 'deleted', 'uninstalled', 'dist_to_shed', 'status', 'error_message' ) + 'tool_shed_status', 'deleted', 'uninstalled', 'dist_to_shed', 'status', 'error_message' ) dict_element_visible_keys = ( 'id', 'tool_shed', 'name', 'owner', 'installed_changeset_revision', 'changeset_revision', 'ctx_rev', 'includes_datatypes', - 'tool_shed_status', 'deleted', 'uninstalled', 'dist_to_shed', 'status', 'error_message' ) + 'tool_shed_status', 'deleted', 'uninstalled', 'dist_to_shed', 'status', 'error_message' ) installation_status = Bunch( NEW='New', CLONING='Cloning', SETTING_TOOL_VERSIONS='Setting tool versions', @@ -356,12 +356,12 @@ class ToolShedRepository( object ): for rd_tup in rd_tups: if len( rd_tup ) == 5: tool_shed, name, owner, changeset_revision, prior_installation_required, only_if_compiling_contained_td = \ - common_util.parse_repository_dependency_tuple( rd_tup, contains_error=False ) + common_util.parse_repository_dependency_tuple( rd_tup, contains_error=False ) if asbool( prior_installation_required ): required_rd_tups_that_must_be_installed.append( ( tool_shed, name, owner, changeset_revision, 'True', 'False' ) ) elif len( rd_tup ) == 6: tool_shed, name, owner, changeset_revision, prior_installation_required, only_if_compiling_contained_td = \ - common_util.parse_repository_dependency_tuple( rd_tup, contains_error=False ) + common_util.parse_repository_dependency_tuple( rd_tup, contains_error=False ) # The repository dependency will only be required to be previously installed if it does not fall into the category of # a repository that must be installed only so that its contained tool dependency can be used for compiling the tool # dependency of the dependent repository. @@ -493,12 +493,14 @@ class ToolShedRepository( object ): class RepositoryRepositoryDependencyAssociation( object ): + def __init__( self, tool_shed_repository_id=None, repository_dependency_id=None ): self.tool_shed_repository_id = tool_shed_repository_id self.repository_dependency_id = repository_dependency_id class RepositoryDependency( object ): + def __init__( self, tool_shed_repository_id=None ): self.tool_shed_repository_id = tool_shed_repository_id @@ -647,6 +649,7 @@ class ToolVersion( object, Dictifiable ): class ToolVersionAssociation( object ): + def __init__( self, id=None, tool_id=None, parent_id=None ): self.id = id self.tool_id = tool_id @@ -654,6 +657,7 @@ class ToolVersionAssociation( object ): class MigrateTools( object ): + def __init__( self, repository_id=None, repository_path=None, version=None ): self.repository_id = repository_id self.repository_path = repository_path diff --git a/lib/tool_shed/galaxy_install/tool_migration_manager.py b/lib/tool_shed/galaxy_install/tool_migration_manager.py index a5269e31a73..1200cac5ea7 100644 --- a/lib/tool_shed/galaxy_install/tool_migration_manager.py +++ b/lib/tool_shed/galaxy_install/tool_migration_manager.py @@ -171,8 +171,6 @@ class ToolMigrationManager( object ): clone_dir = os.path.join( self.tool_path, relative_clone_dir ) if not self.__iscloned( clone_dir ): repository_clone_url = os.path.join( self.tool_shed_url, 'repos', owner, name ) - relative_install_dir = os.path.join( relative_clone_dir, name ) - install_dir = os.path.join( clone_dir, name ) ctx_rev = suc.get_ctx_rev( self.app, self.tool_shed_url, name, owner, changeset_revision ) tool_shed_repository = suc.create_or_update_tool_shed_repository( app=self.app, name=name, @@ -406,7 +404,7 @@ class ToolMigrationManager( object ): tool_configs_to_filter.append( tool_config ) else: print 'The tool "%s" (%s) has not been enabled because it is not defined in a proprietary tool config (%s).' \ - % ( guid, tool_config, ", ".join( self.proprietary_tool_confs or [] ) ) + % ( guid, tool_config, ", ".join( self.proprietary_tool_confs or [] ) ) if tool_configs_to_filter: lock = threading.Lock() lock.acquire( True ) @@ -421,7 +419,7 @@ class ToolMigrationManager( object ): repository=tool_shed_repository, changeset_revision=tool_shed_repository.changeset_revision, repository_clone_url=repository_clone_url, - shed_config_dict = self.shed_config_dict, + shed_config_dict=self.shed_config_dict, relative_install_dir=relative_install_dir, repository_files_dir=None, resetting_all_metadata_on_repository=False, @@ -504,11 +502,11 @@ class ToolMigrationManager( object ): datatypes_config = hg_util.get_config_from_disk( suc.DATATYPES_CONFIG_FILENAME, repo_install_dir ) # Load proprietary data types required by tools. The value of override is not # important here since the Galaxy server will be started after this installation - #completes. + # completes. converter_path, display_path = \ cdl.alter_config_and_load_prorietary_datatypes( datatypes_config, repo_install_dir, - override=False ) + override=False ) if converter_path or display_path: # Create a dictionary of tool shed repository related information. repository_dict = \ @@ -583,23 +581,23 @@ class ToolMigrationManager( object ): tool_version_dicts = json.loads( text ) tvm.handle_tool_versions( tool_version_dicts, tool_shed_repository ) else: - # Set the tool versions since they seem to be missing for this repository in the tool shed. - # CRITICAL NOTE: These default settings may not properly handle all parent/child associations. + # Set the tool versions since they seem to be missing + # for this repository in the tool shed. CRITICAL NOTE: + # These default settings may not properly handle all + # parent/child associations. for tool_dict in metadata_dict[ 'tools' ]: - flush_needed = False tool_id = tool_dict[ 'guid' ] old_tool_id = tool_dict[ 'id' ] - tool_version = tool_dict[ 'version' ] tool_version_using_old_id = tvm.get_tool_version( old_tool_id ) tool_version_using_guid = tvm.get_tool_version( tool_id ) if not tool_version_using_old_id: tool_version_using_old_id = self.app.install_model.ToolVersion( tool_id=old_tool_id, - tool_shed_repository=tool_shed_repository ) + tool_shed_repository=tool_shed_repository ) self.app.install_model.context.add( tool_version_using_old_id ) self.app.install_model.context.flush() if not tool_version_using_guid: tool_version_using_guid = self.app.install_model.ToolVersion( tool_id=tool_id, - tool_shed_repository=tool_shed_repository ) + tool_shed_repository=tool_shed_repository ) self.app.install_model.context.add( tool_version_using_guid ) self.app.install_model.context.flush() # Associate the two versions as parent / child. @@ -640,7 +638,6 @@ class ToolMigrationManager( object ): ordered_tsr_ids = [] processed_tsr_ids = [] prior_install_required_dict = self.get_prior_install_required_dict( tool_shed_repositories, repository_dependencies_dict ) - tsr_ids = [ tool_shed_repository.id for tool_shed_repository in tool_shed_repositories ] while len( processed_tsr_ids ) != len( prior_install_required_dict.keys() ): tsr_id = suc.get_next_prior_import_or_install_required_dict_entry( prior_install_required_dict, processed_tsr_ids ) processed_tsr_ids.append( tsr_id ) diff --git a/lib/tool_shed/util/shed_util_common.py b/lib/tool_shed/util/shed_util_common.py index be8ef343ffc..9afe3340e8d 100644 --- a/lib/tool_shed/util/shed_util_common.py +++ b/lib/tool_shed/util/shed_util_common.py @@ -3,6 +3,7 @@ import logging import os import re import shutil +import socket import string import sqlalchemy.orm.exc @@ -81,6 +82,7 @@ This message was sent from the Galaxy Tool Shed instance hosted on the server '${host}' """ + def create_or_update_tool_shed_repository( app, name, description, installed_changeset_revision, ctx_rev, repository_clone_url, metadata_dict, status, current_changeset_revision=None, owner='', dist_to_shed=False ): """ @@ -118,8 +120,8 @@ def create_or_update_tool_shed_repository( app, name, description, installed_cha owner, installed_changeset_revision ) if tool_shed_repository: - log.debug( "Updating an existing row for repository '%s' in the tool_shed_repository table, status set to '%s'." % \ - ( str( name ), str( status ) ) ) + log.debug( "Updating an existing row for repository '%s' in the tool_shed_repository table, status set to '%s'." % + ( str( name ), str( status ) ) ) tool_shed_repository.description = description tool_shed_repository.changeset_revision = current_changeset_revision tool_shed_repository.ctx_rev = ctx_rev @@ -129,8 +131,8 @@ def create_or_update_tool_shed_repository( app, name, description, installed_cha tool_shed_repository.uninstalled = uninstalled tool_shed_repository.status = status else: - log.debug( "Adding new row for repository '%s' in the tool_shed_repository table, status set to '%s'." % \ - ( str( name ), str( status ) ) ) + log.debug( "Adding new row for repository '%s' in the tool_shed_repository table, status set to '%s'." % + ( str( name ), str( status ) ) ) tool_shed_repository = \ app.install_model.ToolShedRepository( tool_shed=tool_shed, name=name, @@ -149,6 +151,7 @@ def create_or_update_tool_shed_repository( app, name, description, installed_cha context.flush() return tool_shed_repository + def extract_components_from_tuple( repository_components_tuple ): '''Extract the repository components from the provided tuple in a backward-compatible manner.''' toolshed = repository_components_tuple[ 0 ] @@ -164,6 +167,7 @@ def extract_components_from_tuple( repository_components_tuple ): components_list = [ toolshed, name, owner, changeset_revision, prior_installation_required, only_if_compiling_contained_td ] return components_list + def generate_sharable_link_for_repository_in_tool_shed( repository, changeset_revision=None ): """Generate the URL for sharing a repository that is in the tool shed.""" base_url = url_for( '/', qualified=True ).rstrip( '/' ) @@ -173,6 +177,7 @@ def generate_sharable_link_for_repository_in_tool_shed( repository, changeset_re sharable_url += '/%s' % changeset_revision return sharable_url + def generate_tool_guid( repository_clone_url, tool ): """ Generate a guid for the installed tool. It is critical that this guid matches the guid for @@ -182,6 +187,7 @@ def generate_tool_guid( repository_clone_url, tool ): tmp_url = common_util.remove_protocol_and_user_from_clone_url( repository_clone_url ) return '%s/%s/%s' % ( tmp_url, tool.id, tool.version ) + def generate_tool_shed_repository_install_dir( repository_clone_url, changeset_revision ): """ Generate a repository installation directory that guarantees repositories with the same @@ -196,6 +202,7 @@ def generate_tool_shed_repository_install_dir( repository_clone_url, changeset_r tool_shed_url = common_util.remove_port_from_tool_shed_url( tool_shed_url ) return common_util.url_join( tool_shed_url, 'repos', repo_path, changeset_revision ) + def get_absolute_path_to_file_in_repository( repo_files_dir, file_name ): """Return the absolute path to a specified disk file contained in a repository.""" stripped_file_name = basic_util.strip_path( file_name ) @@ -207,19 +214,22 @@ def get_absolute_path_to_file_in_repository( repo_files_dir, file_name ): return os.path.abspath( os.path.join( root, name ) ) return file_path + def get_categories( app ): """Get all categories from the database.""" sa_session = app.model.context.current return sa_session.query( app.model.Category ) \ - .filter( app.model.Category.table.c.deleted==False ) \ + .filter( app.model.Category.table.c.deleted == False ) \ .order_by( app.model.Category.table.c.name ) \ .all() + def get_category( app, id ): """Get a category from the database.""" sa_session = app.model.context.current return sa_session.query( app.model.Category ).get( app.security.decode_id( id ) ) + def get_category_by_name( app, name ): """Get a category from the database via name.""" sa_session = app.model.context.current @@ -228,6 +238,7 @@ def get_category_by_name( app, name ): except sqlalchemy.orm.exc.NoResultFound: return None + def get_ctx_rev( app, tool_shed_url, name, owner, changeset_revision ): """ Send a request to the tool shed to retrieve the ctx_rev for a repository defined by the @@ -240,6 +251,7 @@ def get_ctx_rev( app, tool_shed_url, name, owner, changeset_revision ): ctx_rev = common_util.tool_shed_get( app, tool_shed_url, url ) return ctx_rev + def get_current_repository_metadata_for_changeset_revision( app, repository, changeset_revision ): encoded_repository_id = app.security.encode_id( repository.id ) repository_metadata = get_repository_metadata_by_changeset_revision( app, @@ -261,6 +273,7 @@ def get_current_repository_metadata_for_changeset_revision( app, repository, cha return repository_metadata return None + def get_ids_of_tool_shed_repositories_being_installed( app, as_string=False ): installing_repository_ids = [] new_status = app.install_model.ToolShedRepository.installation_status.NEW @@ -280,6 +293,7 @@ def get_ids_of_tool_shed_repositories_being_installed( app, as_string=False ): return ','.join( installing_repository_ids ) return installing_repository_ids + def get_latest_downloadable_changeset_revision( app, repository, repo ): repository_tip = repository.tip( app ) repository_metadata = get_repository_metadata_by_changeset_revision( app, app.security.encode_id( repository.id ), repository_tip ) @@ -290,6 +304,7 @@ def get_latest_downloadable_changeset_revision( app, repository, repo ): return changeset_revisions[ -1 ] return hg_util.INITIAL_CHANGELOG_HASH + def get_next_downloadable_changeset_revision( repository, repo, after_changeset_revision ): """ Return the installable changeset_revision in the repository changelog after the changeset to which @@ -311,6 +326,7 @@ def get_next_downloadable_changeset_revision( repository, repo, after_changeset_ found_after_changeset_revision = True return None + def get_next_prior_import_or_install_required_dict_entry( prior_required_dict, processed_tsr_ids ): """ This method is used in the Tool Shed when exporting a repository and its dependencies, and in Galaxy @@ -344,6 +360,7 @@ def get_next_prior_import_or_install_required_dict_entry( prior_required_dict, p continue return key + def get_ordered_metadata_changeset_revisions( repository, repo, downloadable=True ): """ Return an ordered list of changeset_revisions that are associated with metadata @@ -366,6 +383,7 @@ def get_ordered_metadata_changeset_revisions( repository, repo, downloadable=Tru sorted_changeset_revisions = [ str( changeset_tup[ 1 ] ) for changeset_tup in sorted_changeset_tups ] return sorted_changeset_revisions + def get_prior_import_or_install_required_dict( app, tsr_ids, repo_info_dicts ): """ This method is used in the Tool Shed when exporting a repository and its dependencies, @@ -390,6 +408,7 @@ def get_prior_import_or_install_required_dict( app, tsr_ids, repo_info_dicts ): prior_import_or_install_required_dict[ encoded_repository_id ] = prior_import_or_install_ids return prior_import_or_install_required_dict + def get_repo_info_tuple_contents( repo_info_tuple ): """Take care in handling the repo_info_tuple as it evolves over time as new tool shed features are introduced.""" if len( repo_info_tuple ) == 6: @@ -399,6 +418,7 @@ def get_repo_info_tuple_contents( repo_info_tuple ): description, repository_clone_url, changeset_revision, ctx_rev, repository_owner, repository_dependencies, tool_dependencies = repo_info_tuple return description, repository_clone_url, changeset_revision, ctx_rev, repository_owner, repository_dependencies, tool_dependencies + def get_repository_and_repository_dependencies_from_repo_info_dict( app, repo_info_dict ): """Return a tool_shed_repository or repository record defined by the information in the received repo_info_dict.""" repository_name = repo_info_dict.keys()[ 0 ] @@ -414,6 +434,7 @@ def get_repository_and_repository_dependencies_from_repo_info_dict( app, repo_in repository = get_repository_by_name_and_owner( app, repository_name, repository_owner ) return repository, repository_dependencies + def get_repository_by_id( app, id ): """Get a repository from the database via id.""" if is_tool_shed_client( app ): @@ -422,28 +443,31 @@ def get_repository_by_id( app, id ): sa_session = app.model.context.current return sa_session.query( app.model.Repository ).get( app.security.decode_id( id ) ) + def get_repository_by_name( app, name ): """Get a repository from the database via name.""" repository_query = get_repository_query( app ) return repository_query.filter_by( name=name ).first() + def get_repository_by_name_and_owner( app, name, owner ): """Get a repository from the database via name and owner""" repository_query = get_repository_query( app ) if is_tool_shed_client( app ): return repository_query \ - .filter( and_( app.install_model.ToolShedRepository.table.c.name == name, - app.install_model.ToolShedRepository.table.c.owner == owner ) ) \ - .first() + .filter( and_( app.install_model.ToolShedRepository.table.c.name == name, + app.install_model.ToolShedRepository.table.c.owner == owner ) ) \ + .first() # We're in the tool shed. user = get_user_by_username( app, owner ) if user: return repository_query \ - .filter( and_( app.model.Repository.table.c.name == name, - app.model.Repository.table.c.user_id == user.id ) ) \ - .first() + .filter( and_( app.model.Repository.table.c.name == name, + app.model.Repository.table.c.user_id == user.id ) ) \ + .first() return None + def get_repository_dependency_types( repository_dependencies ): """ Inspect the received list of repository_dependencies tuples and return boolean values @@ -470,6 +494,7 @@ def get_repository_dependency_types( repository_dependencies ): break return has_repository_dependencies, has_repository_dependencies_only_if_compiling_contained_td + def get_repository_for_dependency_relationship( app, tool_shed, name, owner, changeset_revision ): """ Return an installed tool_shed_repository database record that is defined by either the current changeset @@ -509,6 +534,7 @@ def get_repository_for_dependency_relationship( app, tool_shed, name, owner, cha changeset_revision=text ) return repository + def get_repository_file_contents( file_path ): """Return the display-safe contents of a repository file for display in a browser.""" if checkers.is_gzip( file_path ): @@ -543,6 +569,7 @@ def get_repository_file_contents( file_path ): beginning_on_size_error=True ) return safe_str + def get_repository_files( folder_path ): """Return the file hierarchy of a tool shed repository.""" contents = [] @@ -558,6 +585,7 @@ def get_repository_files( folder_path ): contents.sort() return contents + def get_repository_from_refresh_on_change( app, **kwd ): # The changeset_revision_select_field in several grids performs a refresh_on_change which sends in request parameters like # changeset_revison_1, changeset_revision_2, etc. One of the many select fields on the grid performed the refresh_on_change, @@ -575,6 +603,7 @@ def get_repository_from_refresh_on_change( app, **kwd ): # This should never be reached - raise an exception? return v, None + def get_repository_ids_requiring_prior_import_or_install( app, tsr_ids, repository_dependencies ): """ This method is used in the Tool Shed when exporting a repository and its dependencies, @@ -593,11 +622,11 @@ def get_repository_ids_requiring_prior_import_or_install( app, tsr_ids, reposito continue for rd_tup in rd_tups: tool_shed, \ - name, \ - owner, \ - changeset_revision, \ - prior_installation_required, \ - only_if_compiling_contained_td = \ + name, \ + owner, \ + changeset_revision, \ + prior_installation_required, \ + only_if_compiling_contained_td = \ common_util.parse_repository_dependency_tuple( rd_tup ) # If only_if_compiling_contained_td is False, then the repository dependency # is not required to be installed prior to the dependent repository even if @@ -626,11 +655,13 @@ def get_repository_ids_requiring_prior_import_or_install( app, tsr_ids, reposito prior_tsr_ids.append( encoded_repository_id ) return prior_tsr_ids + def get_repository_in_tool_shed( app, id ): """Get a repository on the tool shed side from the database via id.""" sa_session = app.model.context.current return sa_session.query( app.model.Repository ).get( app.security.decode_id( id ) ) + def get_repository_metadata_by_changeset_revision( app, id, changeset_revision ): """Get metadata for a specified repository change set from the database.""" # Make sure there are no duplicate records, and return the single unique record for the changeset_revision. @@ -652,6 +683,7 @@ def get_repository_metadata_by_changeset_revision( app, id, changeset_revision ) return all_metadata_records[ 0 ] return None + def get_repository_owner( cleaned_repository_url ): """Gvien a "cleaned" repository clone URL, return the owner of the repository.""" items = cleaned_repository_url.split( '/repos/' ) @@ -660,12 +692,13 @@ def get_repository_owner( cleaned_repository_url ): repo_path = repo_path.replace( '/', '', 1 ) return repo_path.lstrip( '/' ).split( '/' )[ 0 ] + def get_repository_owner_from_clone_url( repository_clone_url ): """Given a repository clone URL, return the owner of the repository.""" tmp_url = common_util.remove_protocol_and_user_from_clone_url( repository_clone_url ) - tool_shed = tmp_url.split( '/repos/' )[ 0 ].rstrip( '/' ) return get_repository_owner( tmp_url ) + def get_repository_query( app ): if is_tool_shed_client( app ): query = app.install_model.context.query( app.install_model.ToolShedRepository ) @@ -673,6 +706,7 @@ def get_repository_query( app ): query = app.model.context.query( app.model.Repository ) return query + def get_tool_panel_config_tool_path_install_dir( app, repository ): """ Return shed-related tool panel config, the tool_path configured in it, and the relative path to @@ -690,12 +724,13 @@ def get_tool_panel_config_tool_path_install_dir( app, repository ): # Just pick a semi-random shed config. for shed_config_dict in app.toolbox.shed_tool_confs: if ( repository.dist_to_shed and shed_config_dict[ 'config_filename' ] == app.config.migrated_tools_config ) \ - or ( not repository.dist_to_shed and shed_config_dict[ 'config_filename' ] != app.config.migrated_tools_config ): + or ( not repository.dist_to_shed and shed_config_dict[ 'config_filename' ] != app.config.migrated_tools_config ): break shed_tool_conf = shed_config_dict[ 'config_filename' ] tool_path = shed_config_dict[ 'tool_path' ] return shed_tool_conf, tool_path, relative_install_dir + def get_tool_path_by_shed_tool_conf_filename( app, shed_tool_conf ): """ Return the tool_path config setting for the received shed_tool_conf file by searching the tool box's in-memory list of shed_tool_confs for the @@ -711,10 +746,12 @@ def get_tool_path_by_shed_tool_conf_filename( app, shed_tool_conf ): return shed_tool_conf_dict[ 'tool_path' ] return None + def get_tool_shed_from_clone_url( repository_clone_url ): tmp_url = common_util.remove_protocol_and_user_from_clone_url( repository_clone_url ) return tmp_url.split( '/repos/' )[ 0 ].rstrip( '/' ) + def get_tool_shed_repository_by_id( app, repository_id ): """Return a tool shed repository database record defined by the id.""" # This method is used only in Galaxy, not the tool shed. @@ -722,6 +759,7 @@ def get_tool_shed_repository_by_id( app, repository_id ): .filter( app.install_model.ToolShedRepository.table.c.id == app.security.decode_id( repository_id ) ) \ .first() + def get_tool_shed_repository_by_shed_name_owner_changeset_revision( app, tool_shed, name, owner, changeset_revision ): """ Return a tool shed repository database record defined by the combination of a tool_shed, repository name, @@ -733,11 +771,12 @@ def get_tool_shed_repository_by_shed_name_owner_changeset_revision( app, tool_sh tool_shed = common_util.remove_protocol_from_tool_shed_url( tool_shed ) return repository_query \ .filter( and_( app.install_model.ToolShedRepository.table.c.tool_shed == tool_shed, - app.install_model.ToolShedRepository.table.c.name == name, - app.install_model.ToolShedRepository.table.c.owner == owner, - app.install_model.ToolShedRepository.table.c.changeset_revision == changeset_revision ) ) \ + app.install_model.ToolShedRepository.table.c.name == name, + app.install_model.ToolShedRepository.table.c.owner == owner, + app.install_model.ToolShedRepository.table.c.changeset_revision == changeset_revision ) ) \ .first() + def get_tool_shed_repository_by_shed_name_owner_installed_changeset_revision( app, tool_shed, name, owner, installed_changeset_revision ): """ @@ -750,11 +789,12 @@ def get_tool_shed_repository_by_shed_name_owner_installed_changeset_revision( ap tool_shed = common_util.remove_protocol_from_tool_shed_url( tool_shed ) return repository_query \ .filter( and_( app.install_model.ToolShedRepository.table.c.tool_shed == tool_shed, - app.install_model.ToolShedRepository.table.c.name == name, - app.install_model.ToolShedRepository.table.c.owner == owner, - app.install_model.ToolShedRepository.table.c.installed_changeset_revision == installed_changeset_revision ) ) \ + app.install_model.ToolShedRepository.table.c.name == name, + app.install_model.ToolShedRepository.table.c.owner == owner, + app.install_model.ToolShedRepository.table.c.installed_changeset_revision == installed_changeset_revision ) ) \ .first() + def get_tool_shed_status_for_installed_repository( app, repository ): """ Send a request to the tool shed to retrieve information about newer installable repository revisions, @@ -775,8 +815,8 @@ def get_tool_shed_status_for_installed_repository( app, repository ): except HTTPError, e: # This should handle backward compatility to the Galaxy 12/20/12 release. We used to only handle updates for an installed revision # using a boolean value. - log.debug( "Error attempting to get tool shed status for installed repository %s: %s\nAttempting older 'check_for_updates' method.\n" % \ - ( str( repository.name ), str( e ) ) ) + log.debug( "Error attempting to get tool shed status for installed repository %s: %s\nAttempting older 'check_for_updates' method.\n" % + ( str( repository.name ), str( e ) ) ) params = '?name=%s&owner=%s&changeset_revision=%s&from_update_manager=True' % ( str( repository.name ), str( repository.owner ), str( repository.changeset_revision ) ) @@ -793,6 +833,7 @@ def get_tool_shed_status_for_installed_repository( app, repository ): log.exception( "Error attempting to get tool shed status for installed repository %s: %s" % ( str( repository.name ), str( e ) ) ) return {} + def get_tool_shed_repository_status_label( app, tool_shed_repository=None, name=None, owner=None, changeset_revision=None, repository_clone_url=None ): """Return a color-coded label for the status of the received tool-shed_repository installed into Galaxy.""" if tool_shed_repository is None: @@ -842,6 +883,7 @@ def get_tool_shed_repository_status_label( app, tool_shed_repository=None, name= status_label = 'unknown status' return '
%s
' % ( bgcolor, status_label ) + def get_updated_changeset_revisions( app, name, owner, changeset_revision ): """ Return a string of comma-separated changeset revision hashes for all available updates to the received changeset @@ -863,6 +905,7 @@ def get_updated_changeset_revisions( app, name, owner, changeset_revision ): return changeset_hashes_str return '' + def get_updated_changeset_revisions_from_tool_shed( app, tool_shed_url, name, owner, changeset_revision ): """ Get all appropriate newer changeset revisions for the repository defined by @@ -880,11 +923,13 @@ def get_updated_changeset_revisions_from_tool_shed( app, tool_shed_url, name, ow text = common_util.tool_shed_get( app, tool_shed_url, url ) return text + def get_user( app, id ): """Get a user from the database by id.""" sa_session = app.model.context.current return sa_session.query( app.model.User ).get( app.security.decode_id( id ) ) + def get_user_by_username( app, username ): """Get a user from the database by username.""" sa_session = app.model.context.current @@ -893,9 +938,10 @@ def get_user_by_username( app, username ): .filter( app.model.User.table.c.username == username ) \ .one() return user - except Exception, e: + except Exception: return None + def handle_email_alerts( app, host, repository, content_alert_str='', new_repo_alert=False, admin_only=False ): """ There are 2 complementary features that enable a tool shed user to receive email notification: @@ -947,7 +993,7 @@ def handle_email_alerts( app, host, repository, content_alert_str='', new_repo_a admin_body = string.Template( template ).safe_substitute( host=host, sharable_link=sharable_link, repository_name=repository.name, - revision='%s:%s' %( str( ctx.rev() ), ctx ), + revision='%s:%s' % ( str( ctx.rev() ), ctx ), display_date=display_date, description=ctx.description(), username=username, @@ -955,7 +1001,7 @@ def handle_email_alerts( app, host, repository, content_alert_str='', new_repo_a body = string.Template( template ).safe_substitute( host=host, sharable_link=sharable_link, repository_name=repository.name, - revision='%s:%s' %( str( ctx.rev() ), ctx ), + revision='%s:%s' % ( str( ctx.rev() ), ctx ), display_date=display_date, description=ctx.description(), username=username, @@ -985,9 +1031,10 @@ def handle_email_alerts( app, host, repository, content_alert_str='', new_repo_a util.send_mail( frm, to, subject, admin_body, app.config ) else: util.send_mail( frm, to, subject, body, app.config ) - except Exception, e: + except Exception: log.exception( "An error occurred sending a tool shed repository update alert by email." ) + def have_shed_tool_conf_for_install( app ): if not app.toolbox.shed_tool_confs: return False @@ -999,6 +1046,7 @@ def have_shed_tool_conf_for_install( app ): return True return False + def is_tool_shed_client( app ): """ The tool shed and clients to the tool (i.e. Galaxy) require a lot @@ -1008,6 +1056,7 @@ def is_tool_shed_client( app ): """ return hasattr( app, "install_model" ) + def open_repository_files_folder( folder_path ): """ Return a list of dictionaries, each of which contains information for a file or directory contained @@ -1026,14 +1075,15 @@ def open_repository_files_folder( folder_path ): is_folder = True if filename: full_path = os.path.join( folder_path, filename ) - node = { "title" : filename, - "isFolder" : is_folder, - "isLazy" : is_folder, - "tooltip" : full_path, - "key" : full_path } + node = { "title": filename, + "isFolder": is_folder, + "isLazy": is_folder, + "tooltip": full_path, + "key": full_path } folder_contents.append( node ) return folder_contents + def repository_was_previously_installed( app, tool_shed_url, repository_name, repo_info_tuple, from_tip=False ): """ Find out if a repository is already installed into Galaxy - there are several scenarios where this @@ -1077,6 +1127,7 @@ def repository_was_previously_installed( app, tool_shed_url, repository_name, re return tool_shed_repository, previous_changeset_revision return None, None + def set_image_paths( app, encoded_repository_id, text ): """ Handle tool help image display for tools that are contained in repositories in @@ -1106,6 +1157,7 @@ def set_image_paths( app, encoded_repository_id, text ): text = re.sub( r'\.\. image:: (?!https?://)/?(.+)', r'.. image:: %s/\1' % route_to_images, text ) return text + def set_repository_attributes( app, repository, status, error_message, deleted, uninstalled, remove_from_disk=False ): if remove_from_disk: relative_install_dir = repository.repo_path( app ) @@ -1123,6 +1175,7 @@ def set_repository_attributes( app, repository, status, error_message, deleted, app.install_model.context.add( repository ) app.install_model.context.flush() + def tool_shed_is_this_tool_shed( toolshed_base_url ): """Determine if a tool shed is the current tool shed.""" cleaned_toolshed_base_url = common_util.remove_protocol_from_tool_shed_url( toolshed_base_url ) diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index 91eff5ed6c1..6900bf7ec67 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -5,15 +5,14 @@ import shutil import galaxy.tools from galaxy import util from galaxy.datatypes import checkers -from galaxy.tools import parameters from galaxy.util.expressions import ExpressionContext from galaxy.web.form_builder import SelectField -from galaxy.tools.actions.upload import UploadToolAction from tool_shed.util import basic_util log = logging.getLogger( __name__ ) + def build_shed_tool_conf_select_field( app ): """Build a SelectField whose options are the keys in app.toolbox.shed_tool_confs.""" options = [] @@ -30,6 +29,7 @@ def build_shed_tool_conf_select_field( app ): select_field.add_option( option_tup[ 0 ], option_tup[ 1 ] ) return select_field + def build_tool_panel_section_select_field( app ): """Build a SelectField whose options are the sections of the current in-memory toolbox.""" options = [] @@ -41,6 +41,7 @@ def build_tool_panel_section_select_field( app ): select_field.add_option( option_tup[ 0 ], option_tup[ 1 ] ) return select_field + def copy_sample_file( app, filename, dest_path=None ): """ Copy xxx.sample to dest_path/xxx.sample and dest_path/xxx. The default value for dest_path @@ -61,6 +62,7 @@ def copy_sample_file( app, filename, dest_path=None ): if not os.path.exists( os.path.join( dest_path, copied_file ) ): shutil.copy( full_source_path, os.path.join( dest_path, copied_file ) ) + def copy_sample_files( app, sample_files, tool_path=None, sample_files_copied=None, dest_path=None ): """ Copy all appropriate files to dest_path in the local Galaxy environment that have not @@ -75,11 +77,12 @@ def copy_sample_files( app, sample_files, tool_path=None, sample_files_copied=No filename_sans_path = os.path.split( filename )[ 1 ] if filename_sans_path not in filenames_not_to_copy and filename not in sample_files_copied: if tool_path: - filename=os.path.join( tool_path, filename ) + filename = os.path.join( tool_path, filename ) # Attempt to ensure we're copying an appropriate file. if is_data_index_sample_file( filename ): copy_sample_file( app, filename, dest_path=dest_path ) + def generate_message_for_invalid_tools( app, invalid_file_tups, repository, metadata_dict, as_html=True, displaying_invalid_tool=False ): if as_html: @@ -124,6 +127,7 @@ def generate_message_for_invalid_tools( app, invalid_file_tups, repository, meta message += "%s%s%s - %s%s" % ( bold_start, tool_file, bold_end, correction_msg, new_line ) return message + def get_headers( fname, sep, count=60, is_multi_byte=False ): """Returns a list with the first 'count' lines split by 'sep'.""" headers = [] @@ -137,6 +141,7 @@ def get_headers( fname, sep, count=60, is_multi_byte=False ): break return headers + def get_tool_path_install_dir( partial_install_dir, shed_tool_conf_dict, tool_dict, config_elems ): for elem in config_elems: if elem.tag == 'tool': @@ -153,6 +158,7 @@ def get_tool_path_install_dir( partial_install_dir, shed_tool_conf_dict, tool_di return tool_path, relative_install_dir return None, None + def handle_missing_index_file( app, tool_path, sample_files, repository_tools_tups, sample_files_copied ): """ Inspect each tool to see if it has any input parameters that are dynamically @@ -180,6 +186,7 @@ def handle_missing_index_file( app, tool_path, sample_files, repository_tools_tu repository_tools_tups[ index ] = ( tup_path, guid, repository_tool ) return repository_tools_tups, sample_files_copied + def is_column_based( fname, sep='\t', skip=0, is_multi_byte=False ): """See if the file is column based with respect to a separator.""" headers = get_headers( fname, sep, is_multi_byte=is_multi_byte ) @@ -199,6 +206,7 @@ def is_column_based( fname, sep='\t', skip=0, is_multi_byte=False ): return False return True + def is_data_index_sample_file( file_path ): """ Attempt to determine if a .sample file is appropriate for copying to ~/tool-data when @@ -224,6 +232,7 @@ def is_data_index_sample_file( file_path ): # Default to copying the file if none of the above are true. return True + def new_state( trans, tool, invalid=False ): """Create a new `DefaultToolState` for the received tool. Only inputs on the first page will be initialized.""" state = galaxy.tools.DefaultToolState() @@ -240,9 +249,10 @@ def new_state( trans, tool, invalid=False ): state.inputs[ input.name ] = [] return state + def panel_entry_per_tool( tool_section_dict ): # Return True if tool_section_dict looks like this. - # { : + # { : # [{ tool_config : , # id: , # version : , @@ -258,6 +268,7 @@ def panel_entry_per_tool( tool_section_dict ): return True return False + def reload_upload_tools( app ): if hasattr( app, 'toolbox' ): app.toolbox.handle_datatypes_changed() From 3bcda4fb87cf655fbf9a118af100bc2582ab5075 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 074/473] Method to abstract away ToolBox.shed_tool_confs from tool_util.py. Start a gradual process of referring to these as dynamic tool confs instead of shed tool confs. I would like like to make them more generally Galaxy-managed tool configurations and distinguish them from deployer-managed configurations - rather then having overtly ToolShed-only logic in ToolBox. --- lib/galaxy/tools/__init__.py | 10 ++++++++++ lib/tool_shed/util/tool_util.py | 14 ++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index eae5c0f8a95..dd6edcca20d 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -632,6 +632,16 @@ class ToolBox( object, Dictifiable ): break return tool_version_select_field, tools, tool + def dynamic_conf_filenames( self, include_migrated_tool_conf=False ): + """ Return list of dynamic tool configuration filenames (shed_tools). + These must be used with various dynamic tool configuration update + operations (e.g. with update_shed_config). + """ + for dynamic_tool_conf_dict in self.shed_tool_confs: + dynamic_tool_conf_filename = dynamic_tool_conf_dict[ 'config_filename' ] + if include_migrated_tool_conf or (dynamic_tool_conf_filename != self.app.config.migrated_tools_config): + yield dynamic_tool_conf_filename + def build_tool_version_select_field( self, tools, tool_id, set_selected ): """Build a SelectField whose options are the ids for the received list of tools.""" options = [] diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index 6900bf7ec67..c57bd90ea5a 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -16,14 +16,12 @@ log = logging.getLogger( __name__ ) def build_shed_tool_conf_select_field( app ): """Build a SelectField whose options are the keys in app.toolbox.shed_tool_confs.""" options = [] - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: - shed_tool_conf_filename = shed_tool_conf_dict[ 'config_filename' ] - if shed_tool_conf_filename != app.config.migrated_tools_config: - if shed_tool_conf_filename.startswith( './' ): - option_label = shed_tool_conf_filename.replace( './', '', 1 ) - else: - option_label = shed_tool_conf_filename - options.append( ( option_label, shed_tool_conf_filename ) ) + for dynamic_tool_conf_filename in app.toolbox.dynamic_conf_filenames(): + if dynamic_tool_conf_filename.startswith( './' ): + option_label = dynamic_tool_conf_filename.replace( './', '', 1 ) + else: + option_label = dynamic_tool_conf_filename + options.append( ( option_label, dynamic_tool_conf_filename ) ) select_field = SelectField( name='shed_tool_conf' ) for option_tup in options: select_field.add_option( option_tup[ 0 ], option_tup[ 1 ] ) From 62927d39f633aa2892f38669c18c6cabb2427bfa Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 075/473] Method to abstract away ToolBox.tool_panel from tool_util.py. Now only thing accessing ToolBox.tool_panel outside ToolBox itself is ToolPanelManager. --- lib/galaxy/tools/__init__.py | 5 +++++ lib/tool_shed/util/tool_util.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index dd6edcca20d..22a0db595db 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1059,6 +1059,11 @@ class ToolBox( object, Dictifiable ): status = 'done' return message, status + def get_sections( self ): + for k, v in self.tool_panel.items(): + if isinstance( v, ToolSection ): + yield (v.id, v.name) + def find_section_id( self, tool_panel_section_id ): """ Find the section ID referenced by the key or return '' indicating diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index c57bd90ea5a..2f907cf4014 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -31,9 +31,8 @@ def build_shed_tool_conf_select_field( app ): def build_tool_panel_section_select_field( app ): """Build a SelectField whose options are the sections of the current in-memory toolbox.""" options = [] - for k, v in app.toolbox.tool_panel.items(): - if isinstance( v, galaxy.tools.ToolSection ): - options.append( ( v.name, v.id ) ) + for section_id, section_name in app.toolbox.get_sections(): + options.append( ( section_name, section_id ) ) select_field = SelectField( name='tool_panel_section_id', display='radio' ) for option_tup in options: select_field.add_option( option_tup[ 0 ], option_tup[ 1 ] ) From ae8b88817eb60f9928a9359719dad2842664a88a Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 076/473] Reuse new ToolBox.get_section method in ToolPanelManager. This is a higher-level method and eliminates a couple more places where Tool Shed code is directly acceessing ToolBox.tool_panel. --- lib/tool_shed/galaxy_install/tools/tool_panel_manager.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 0e5ec00eaaa..05670a89ac9 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -325,8 +325,7 @@ class ToolPanelManager( object ): tool_panel_section_id=section_id, new_tool_panel_section_label=new_tool_panel_section_label ) elif tool_panel_section_id: - tool_panel_section_key = str( tool_panel_section_id ) - tool_section = toolbox.tool_panel[ tool_panel_section_key ] + tool_panel_section_key, tool_section = toolbox.get_section( tool_panel_section_id) else: return None, None return tool_panel_section_key, tool_section @@ -495,8 +494,8 @@ class ToolPanelManager( object ): # Remove the tool sub-element from the section element. config_elem.remove( tool_elem ) # Remove the tool from the section in the in-memory tool panel. - if section_key in toolbox.tool_panel: - tool_section = toolbox.tool_panel[ section_key ] + _, tool_section = toolbox.get_section( section_key ) + if tool_section: remove_from_panel( tool_elem, tool_section.elems, toolbox.integrated_tool_panel.get( section_key, {} ), section_key=section_key ) if len( config_elem ) < 1: # Keep a list of all empty section elements so they can be removed. From 056a60de818ed7ed56f4dd3702f61497421cea7d Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 077/473] Refactor logic for removing tools from panels into ToolBox. May seem sort of like Tool Shed specific stuff - but I think it can be reused elsewhere down the road. Regardless of this however as of this commit no external components directly access ToolBox.tool_panel or ToolBox.integrated_tool_panel - which I think is important - these should be internal details of the ToolBox (or newer modules it leverages). --- lib/galaxy/tools/__init__.py | 49 ++++++++++++++++++ .../tools/tool_panel_manager.py | 51 +------------------ 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 22a0db595db..9a14abd1039 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -658,6 +658,55 @@ class ToolBox( object, Dictifiable ): select_field.add_option( 'version %s' % option_tup[ 0 ], option_tup[ 1 ] ) return select_field + def remove_from_panel( self, tool_id, section_key='', remove_from_config=True ): + + def remove_from_dict( has_elems, integrated_has_elems ): + tool_key = 'tool_%s' % str( tool_id ) + available_tool_versions = self.get_loaded_tools_by_lineage( tool_id ) + if tool_key in has_elems: + if available_tool_versions: + available_tool_versions.reverse() + replacement_tool_key = None + replacement_tool_version = None + # Since we are going to remove the tool from the section, replace it with + # the newest loaded version of the tool. + for available_tool_version in available_tool_versions: + available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() + # I suspect "available_tool_version.id in has_elems.keys()" doesn't + # belong in the following line or at least I don't understand what + # purpose it might serve. -John + if available_tool_version.id in has_elems.keys() or (available_tool_section_id == section_key): + replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) + replacement_tool_version = available_tool_version + break + if replacement_tool_key and replacement_tool_version: + # Get the index of the tool_key in the tool_section. + for tool_panel_index, key in enumerate( has_elems.keys() ): + if key == tool_key: + break + # Remove the tool from the tool panel. + del has_elems[ tool_key ] + # Add the replacement tool at the same location in the tool panel. + has_elems.insert( tool_panel_index, + replacement_tool_key, + replacement_tool_version ) + else: + del has_elems[ tool_key ] + else: + del has_elems[ tool_key ] + if remove_from_config: + if hasattr( integrated_has_elems, "elems" ): + integrated_has_elems = integrated_has_elems.elems + if tool_key in integrated_has_elems: + del integrated_has_elems[ tool_key ] + + if section_key: + _, tool_section = self.get_section( section_key ) + if tool_section: + remove_from_dict( tool_section.elems, self.integrated_tool_panel.get( section_key, {} ) ) + else: + remove_from_dict( self.tool_panel, self.integrated_tool_panel ) + def load_tool_tag_set( self, elem, panel_dict, integrated_panel_dict, tool_path, load_panel_dict, guid=None, index=None ): try: path = elem.get( "file" ) diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index 05670a89ac9..f27445d5254 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -426,50 +426,6 @@ class ToolPanelManager( object ): def remove_guids( self, guids_to_remove, shed_tool_conf, uninstall ): toolbox = self.app.toolbox - - def remove_from_panel( tool_elem, has_elems, integrated_has_elems, section_key='' ): - # Hide tool from panel and promote next oldest version if - # available. - guid = tool_elem.get( 'guid' ) - tool_key = 'tool_%s' % str( guid ) - available_tool_versions = toolbox.get_loaded_tools_by_lineage( guid ) - if tool_key in has_elems: - if available_tool_versions: - available_tool_versions.reverse() - replacement_tool_key = None - replacement_tool_version = None - # Since we are going to remove the tool from the section, replace it with - # the newest loaded version of the tool. - for available_tool_version in available_tool_versions: - available_tool_section_id, available_tool_section_name = available_tool_version.get_panel_section() - # I suspect "available_tool_version.id in has_elems.keys()" doesn't - # belong in the following line or at least I don't understand what - # purpose it might serve. -John - if available_tool_version.id in has_elems.keys() or (available_tool_section_id == section_key): - replacement_tool_key = 'tool_%s' % str( available_tool_version.id ) - replacement_tool_version = available_tool_version - break - if replacement_tool_key and replacement_tool_version: - # Get the index of the tool_key in the tool_section. - for tool_panel_index, key in enumerate( has_elems.keys() ): - if key == tool_key: - break - # Remove the tool from the tool panel. - del has_elems[ tool_key ] - # Add the replacement tool at the same location in the tool panel. - has_elems.insert( tool_panel_index, - replacement_tool_key, - replacement_tool_version ) - else: - del has_elems[ tool_key ] - else: - del has_elems[ tool_key ] - if uninstall: - if hasattr( integrated_has_elems, "elems" ): - integrated_has_elems = integrated_has_elems.elems - if tool_key in integrated_has_elems: - del integrated_has_elems[ tool_key ] - # Remove the tools from the toolbox's tools_by_id dictionary. for guid_to_remove in guids_to_remove: # remove_from_tool_panel to false, will handling that logic below. @@ -494,17 +450,14 @@ class ToolPanelManager( object ): # Remove the tool sub-element from the section element. config_elem.remove( tool_elem ) # Remove the tool from the section in the in-memory tool panel. - _, tool_section = toolbox.get_section( section_key ) - if tool_section: - remove_from_panel( tool_elem, tool_section.elems, toolbox.integrated_tool_panel.get( section_key, {} ), section_key=section_key ) + toolbox.remove_from_panel( tool_elem.get( "guid" ), section_key=section_key, remove_from_config=uninstall ) if len( config_elem ) < 1: # Keep a list of all empty section elements so they can be removed. config_elems_to_remove.append( config_elem ) elif config_elem.tag == 'tool': guid = config_elem.get( 'guid' ) if guid in guids_to_remove: - # get_panel_section return '' for un-sectioned tools. - remove_from_panel( config_elem, toolbox.tool_panel, toolbox.integrated_tool_panel, section_key='' ) + toolbox.remove_from_panel( guid, section_key='', remove_from_config=uninstall ) config_elems_to_remove.append( config_elem ) for config_elem in config_elems_to_remove: # Remove the element from the in-memory list of elements. From 6d85189efa0d040203c5f221a601d1a3200cf915 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 078/473] Update calls to ToolPanelManager.get_shed_tool_conf_dict for clarity... ... in other words to note where index in the resulting tuple is not actually used. Doing this to enable hiding this implementation detail (index) from components in subsequent commits. --- lib/tool_shed/galaxy_install/install_manager.py | 2 +- .../metadata/installed_repository_metadata_manager.py | 2 +- lib/tool_shed/galaxy_install/tool_migration_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index eb580729f00..0c0042c8435 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -730,7 +730,7 @@ class InstallRepositoryManager( object ): shed_tool_conf = install_options.get( 'shed_tool_conf', None ) if shed_tool_conf: # Get the tool_path setting. - index, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( shed_tool_conf ) + _, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( shed_tool_conf ) tool_path = shed_conf_dict[ 'tool_path' ] else: # Pick a semi-random shed-related tool panel configuration file and get the tool_path setting. diff --git a/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py b/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py index bf90c9cf854..9e0b4acaa84 100644 --- a/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py +++ b/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py @@ -66,7 +66,7 @@ class InstalledRepositoryMetadataManager( metadata_generator.MetadataGenerator ) in the received tool shed repository metadata. """ repository_tools_tups = [] - index, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( self.metadata_dict.get( 'shed_config_filename' ) ) + _, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( self.metadata_dict.get( 'shed_config_filename' ) ) if 'tools' in self.metadata_dict: for tool_dict in self.metadata_dict[ 'tools' ]: load_relative_path = relative_path = tool_dict.get( 'tool_config', None ) diff --git a/lib/tool_shed/galaxy_install/tool_migration_manager.py b/lib/tool_shed/galaxy_install/tool_migration_manager.py index 1200cac5ea7..3ed523b677d 100644 --- a/lib/tool_shed/galaxy_install/tool_migration_manager.py +++ b/lib/tool_shed/galaxy_install/tool_migration_manager.py @@ -78,7 +78,7 @@ class ToolMigrationManager( object ): self.tool_shed_url = common_util.get_tool_shed_url_from_tool_shed_registry( self.app, defined_tool_shed_url ) self.tool_shed = common_util.remove_protocol_and_port_from_tool_shed_url( self.tool_shed_url ) self.repository_owner = common_util.REPOSITORY_OWNER - index, self.shed_config_dict = self.tpm.get_shed_tool_conf_dict( self.migrated_tools_config ) + _, self.shed_config_dict = self.tpm.get_shed_tool_conf_dict( self.migrated_tools_config ) # Since tool migration scripts can be executed any number of times, we need to # make sure the appropriate tools are defined in tool_conf.xml. If no tools # associated with the migration stage are defined, no repositories will be installed From 5584ece4fa8949a3cfe22cdece9efd8f98d8663e Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 079/473] Update ToolBox.update_shed_config to use config_filename instead of index. Seems a more robust index, simplifies ToolPanelManager, etc.... --- lib/galaxy/tools/__init__.py | 6 ++++-- lib/tool_shed/galaxy_install/tools/tool_panel_manager.py | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 9a14abd1039..69a2ee7ae82 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -265,13 +265,15 @@ class ToolBox( object, Dictifiable ): return shed_config_dict return default - def update_shed_config( self, shed_conf_index, shed_conf, integrated_panel_changes=True ): + def update_shed_config( self, shed_conf, integrated_panel_changes=True ): """ Update the in-memory descriptions of tools and write out the changes to integrated tool panel unless we are just deactivating a tool (since that doesn't affect that file). """ app = self.app - self.shed_tool_confs[ shed_conf_index ] = shed_conf + for index, my_shed_tool_conf in enumerate( self.shed_tool_confs ): + if shed_conf['config_filename'] == my_shed_tool_conf['config_filename']: + self.shed_tool_confs[ index ] = shed_conf if integrated_panel_changes and app.config.update_integrated_tool_panel: # Write the current in-memory version of the integrated_tool_panel.xml file to disk. self.write_integrated_tool_panel_config_file() diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index f27445d5254..f119c9f2f52 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -40,7 +40,7 @@ class ToolPanelManager( object ): shed_tool_conf, tool_panel_dict, new_install=True ): """A tool shed repository is being installed or updated so handle tool panel alterations accordingly.""" # We need to change the in-memory version and the file system version of the shed_tool_conf file. - index, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) + _, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) tool_path = shed_tool_conf_dict[ 'tool_path' ] # Generate the list of ElementTree Element objects for each section or tool. elem_list = self.generate_tool_panel_elem_list( repository_name, @@ -66,7 +66,7 @@ class ToolPanelManager( object ): ) # Replace the old list of in-memory config_elems with the new list for this shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems - self.app.toolbox.update_shed_config( index, shed_tool_conf_dict ) + self.app.toolbox.update_shed_config( shed_tool_conf_dict ) def config_elems_to_xml_file( self, config_elems, config_filename, tool_path ): """ @@ -430,7 +430,7 @@ class ToolPanelManager( object ): for guid_to_remove in guids_to_remove: # remove_from_tool_panel to false, will handling that logic below. toolbox.remove_tool_by_id( guid_to_remove, remove_from_panel=False ) - index, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) + _, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) if uninstall: # Remove from the shed_tool_conf file on disk. self.remove_from_shed_tool_config( shed_tool_conf_dict, guids_to_remove ) @@ -464,4 +464,4 @@ class ToolPanelManager( object ): config_elems.remove( config_elem ) # Update the config_elems of the in-memory shed_tool_conf_dict. shed_tool_conf_dict[ 'config_elems' ] = config_elems - toolbox.update_shed_config( index, shed_tool_conf_dict, integrated_panel_changes=uninstall ) + toolbox.update_shed_config( shed_tool_conf_dict, integrated_panel_changes=uninstall ) From 3b3ef1f85f4a484c5aaa88e153e30c2d20700a4f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 080/473] Simplifications for ToolPanelManager.get_shed_tool_conf_dict. None of the components using this method are using the index any more - so eliminate it. Also introduce a new ToolBox method to hide the details of ToolBox.shed_tool_confs from this method. --- lib/galaxy/tools/__init__.py | 14 ++++++++++---- lib/tool_shed/galaxy_install/install_manager.py | 2 +- .../installed_repository_metadata_manager.py | 2 +- .../galaxy_install/tool_migration_manager.py | 2 +- .../galaxy_install/tools/tool_panel_manager.py | 10 +++++----- test/unit/tools_support.py | 1 + 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 69a2ee7ae82..b37746d3ce5 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -634,15 +634,21 @@ class ToolBox( object, Dictifiable ): break return tool_version_select_field, tools, tool + def dynamic_confs( self, include_migrated_tool_conf=False ): + confs = [] + for dynamic_tool_conf_dict in self.shed_tool_confs: + dynamic_tool_conf_filename = dynamic_tool_conf_dict[ 'config_filename' ] + if include_migrated_tool_conf or (dynamic_tool_conf_filename != self.app.config.migrated_tools_config): + confs.append( dynamic_tool_conf_dict ) + return confs + def dynamic_conf_filenames( self, include_migrated_tool_conf=False ): """ Return list of dynamic tool configuration filenames (shed_tools). These must be used with various dynamic tool configuration update operations (e.g. with update_shed_config). """ - for dynamic_tool_conf_dict in self.shed_tool_confs: - dynamic_tool_conf_filename = dynamic_tool_conf_dict[ 'config_filename' ] - if include_migrated_tool_conf or (dynamic_tool_conf_filename != self.app.config.migrated_tools_config): - yield dynamic_tool_conf_filename + for dynamic_tool_conf_dict in self.dynamic_confs( include_migrated_tool_conf=include_migrated_tool_conf ): + yield dynamic_tool_conf_dict[ 'config_filename' ] def build_tool_version_select_field( self, tools, tool_id, set_selected ): """Build a SelectField whose options are the ids for the received list of tools.""" diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index 0c0042c8435..ae44cf85ec0 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -730,7 +730,7 @@ class InstallRepositoryManager( object ): shed_tool_conf = install_options.get( 'shed_tool_conf', None ) if shed_tool_conf: # Get the tool_path setting. - _, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( shed_tool_conf ) + shed_conf_dict = self.tpm.get_shed_tool_conf_dict( shed_tool_conf ) tool_path = shed_conf_dict[ 'tool_path' ] else: # Pick a semi-random shed-related tool panel configuration file and get the tool_path setting. diff --git a/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py b/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py index 9e0b4acaa84..1600bb78b83 100644 --- a/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py +++ b/lib/tool_shed/galaxy_install/metadata/installed_repository_metadata_manager.py @@ -66,7 +66,7 @@ class InstalledRepositoryMetadataManager( metadata_generator.MetadataGenerator ) in the received tool shed repository metadata. """ repository_tools_tups = [] - _, shed_conf_dict = self.tpm.get_shed_tool_conf_dict( self.metadata_dict.get( 'shed_config_filename' ) ) + shed_conf_dict = self.tpm.get_shed_tool_conf_dict( self.metadata_dict.get( 'shed_config_filename' ) ) if 'tools' in self.metadata_dict: for tool_dict in self.metadata_dict[ 'tools' ]: load_relative_path = relative_path = tool_dict.get( 'tool_config', None ) diff --git a/lib/tool_shed/galaxy_install/tool_migration_manager.py b/lib/tool_shed/galaxy_install/tool_migration_manager.py index 3ed523b677d..cb4e20a497a 100644 --- a/lib/tool_shed/galaxy_install/tool_migration_manager.py +++ b/lib/tool_shed/galaxy_install/tool_migration_manager.py @@ -78,7 +78,7 @@ class ToolMigrationManager( object ): self.tool_shed_url = common_util.get_tool_shed_url_from_tool_shed_registry( self.app, defined_tool_shed_url ) self.tool_shed = common_util.remove_protocol_and_port_from_tool_shed_url( self.tool_shed_url ) self.repository_owner = common_util.REPOSITORY_OWNER - _, self.shed_config_dict = self.tpm.get_shed_tool_conf_dict( self.migrated_tools_config ) + self.shed_config_dict = self.tpm.get_shed_tool_conf_dict( self.migrated_tools_config ) # Since tool migration scripts can be executed any number of times, we need to # make sure the appropriate tools are defined in tool_conf.xml. If no tools # associated with the migration stage are defined, no repositories will be installed diff --git a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py index f119c9f2f52..7ad9616a841 100644 --- a/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -40,7 +40,7 @@ class ToolPanelManager( object ): shed_tool_conf, tool_panel_dict, new_install=True ): """A tool shed repository is being installed or updated so handle tool panel alterations accordingly.""" # We need to change the in-memory version and the file system version of the shed_tool_conf file. - _, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) + shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) tool_path = shed_tool_conf_dict[ 'tool_path' ] # Generate the list of ElementTree Element objects for each section or tool. elem_list = self.generate_tool_panel_elem_list( repository_name, @@ -304,13 +304,13 @@ class ToolPanelManager( object ): Return the in-memory version of the shed_tool_conf file, which is stored in the config_elems entry in the shed_tool_conf_dict associated with the file. """ - for index, shed_tool_conf_dict in enumerate( self.app.toolbox.shed_tool_confs ): + for shed_tool_conf_dict in self.app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): if shed_tool_conf == shed_tool_conf_dict[ 'config_filename' ]: - return index, shed_tool_conf_dict + return shed_tool_conf_dict else: file_name = basic_util.strip_path( shed_tool_conf_dict[ 'config_filename' ] ) if shed_tool_conf == file_name: - return index, shed_tool_conf_dict + return shed_tool_conf_dict def handle_tool_panel_section( self, toolbox, tool_panel_section_id=None, new_tool_panel_section_label=None ): """Return a ToolSection object retrieved from the current in-memory tool_panel.""" @@ -430,7 +430,7 @@ class ToolPanelManager( object ): for guid_to_remove in guids_to_remove: # remove_from_tool_panel to false, will handling that logic below. toolbox.remove_tool_by_id( guid_to_remove, remove_from_panel=False ) - _, shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) + shed_tool_conf_dict = self.get_shed_tool_conf_dict( shed_tool_conf ) if uninstall: # Remove from the shed_tool_conf file on disk. self.remove_from_shed_tool_config( shed_tool_conf_dict, guids_to_remove ) diff --git a/test/unit/tools_support.py b/test/unit/tools_support.py index b89268a82da..6abd4420e78 100644 --- a/test/unit/tools_support.py +++ b/test/unit/tools_support.py @@ -100,6 +100,7 @@ class MockApp( object ): admin_users="mary@example.com", len_file_path=os.path.join( 'tool-data', 'shared', 'ucsc', 'chrom' ), builds_file_path=os.path.join( 'tool-data', 'shared', 'ucsc', 'builds.txt.sample' ), + migrated_tools_config=os.path.join(test_directory, "migrated_tools_conf.xml"), ) # Setup some attributes for downstream extension by specific tests. From 5809140d0e122a6d56b3b9bc75e4a4e6567b3da1 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 09:11:54 -0500 Subject: [PATCH 081/473] Reuse new ToolBox.dynamic_confs method other places... ... to shield ToolBox.shed_tool_confs from other components including Tool Shed install models, shed_util_common.py, and install_manager.py. After this commit - shed_tool_confs is only accessed directly by the ToolBox itself. --- lib/galaxy/model/tool_shed_install/__init__.py | 8 ++++---- lib/tool_shed/galaxy_install/install_manager.py | 12 +++++------- lib/tool_shed/util/shed_util_common.py | 14 +++----------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/galaxy/model/tool_shed_install/__init__.py b/lib/galaxy/model/tool_shed_install/__init__.py index e30008ddc40..19e13f1fb8f 100644 --- a/lib/galaxy/model/tool_shed_install/__init__.py +++ b/lib/galaxy/model/tool_shed_install/__init__.py @@ -99,7 +99,7 @@ class ToolShedRepository( object ): if not self.shed_config_filename: self.guess_shed_config( app, default=default ) if self.shed_config_filename: - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: + for shed_tool_conf_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): if self.shed_config_filename == shed_tool_conf_dict[ 'config_filename' ]: return shed_tool_conf_dict return default @@ -118,7 +118,7 @@ class ToolShedRepository( object ): metadata = self.metadata or {} for tool in metadata.get( 'tools', [] ): tool_ids.append( tool.get( 'guid' ) ) - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: + for shed_tool_conf_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): name = shed_tool_conf_dict[ 'config_filename' ] for elem in shed_tool_conf_dict[ 'config_elems' ]: if elem.tag == 'tool': @@ -137,7 +137,7 @@ class ToolShedRepository( object ): if self.includes_datatypes: # We need to search by file paths here, which is less desirable. tool_shed = common_util.remove_protocol_and_port_from_tool_shed_url( self.tool_shed ) - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: + for shed_tool_conf_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): tool_path = shed_tool_conf_dict[ 'tool_path' ] relative_path = os.path.join( tool_path, tool_shed, 'repos', self.owner, self.name, self.installed_changeset_revision ) if os.path.exists( relative_path ): @@ -284,7 +284,7 @@ class ToolShedRepository( object ): def repo_path( self, app ): tool_shed = common_util.remove_protocol_and_port_from_tool_shed_url( self.tool_shed ) - for index, shed_tool_conf_dict in enumerate( app.toolbox.shed_tool_confs ): + for shed_tool_conf_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): tool_path = shed_tool_conf_dict[ 'tool_path' ] relative_path = os.path.join( tool_path, tool_shed, 'repos', self.owner, self.name, self.installed_changeset_revision ) if os.path.exists( relative_path ): diff --git a/lib/tool_shed/galaxy_install/install_manager.py b/lib/tool_shed/galaxy_install/install_manager.py index ae44cf85ec0..49fa591ad98 100644 --- a/lib/tool_shed/galaxy_install/install_manager.py +++ b/lib/tool_shed/galaxy_install/install_manager.py @@ -733,15 +733,13 @@ class InstallRepositoryManager( object ): shed_conf_dict = self.tpm.get_shed_tool_conf_dict( shed_tool_conf ) tool_path = shed_conf_dict[ 'tool_path' ] else: - # Pick a semi-random shed-related tool panel configuration file and get the tool_path setting. - for shed_config_dict in self.app.toolbox.shed_tool_confs: - # Don't use migrated_tools_conf.xml. - if shed_config_dict[ 'config_filename' ] != self.app.config.migrated_tools_config: - break + # Don't use migrated_tools_conf.xml. + try: + shed_config_dict = self.app.toolbox.dynamic_confs( include_migrated_tool_conf=False )[ 0 ] + except IndexError: + raise exceptions.RequestParameterMissingException( "Missing required parameter 'shed_tool_conf'." ) shed_tool_conf = shed_config_dict[ 'config_filename' ] tool_path = shed_config_dict[ 'tool_path' ] - if not shed_tool_conf: - raise exceptions.RequestParameterMissingException( "Missing required parameter 'shed_tool_conf'." ) tool_panel_section_id = self.app.toolbox.find_section_id( install_options.get( 'tool_panel_section_id', '' ) ) # Build the dictionary of information necessary for creating tool_shed_repository database records # for each repository being installed. diff --git a/lib/tool_shed/util/shed_util_common.py b/lib/tool_shed/util/shed_util_common.py index 9afe3340e8d..bf0477995ed 100644 --- a/lib/tool_shed/util/shed_util_common.py +++ b/lib/tool_shed/util/shed_util_common.py @@ -722,7 +722,7 @@ def get_tool_panel_config_tool_path_install_dir( app, repository ): shed_config_dict = repository.get_shed_config_dict( app ) if not shed_config_dict: # Just pick a semi-random shed config. - for shed_config_dict in app.toolbox.shed_tool_confs: + for shed_config_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): if ( repository.dist_to_shed and shed_config_dict[ 'config_filename' ] == app.config.migrated_tools_config ) \ or ( not repository.dist_to_shed and shed_config_dict[ 'config_filename' ] != app.config.migrated_tools_config ): break @@ -736,7 +736,7 @@ def get_tool_path_by_shed_tool_conf_filename( app, shed_tool_conf ): Return the tool_path config setting for the received shed_tool_conf file by searching the tool box's in-memory list of shed_tool_confs for the dictionary whose config_filename key has a value matching the received shed_tool_conf. """ - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: + for shed_tool_conf_dict in app.toolbox.dynamic_confs( include_migrated_tool_conf=True ): config_filename = shed_tool_conf_dict[ 'config_filename' ] if config_filename == shed_tool_conf: return shed_tool_conf_dict[ 'tool_path' ] @@ -1036,15 +1036,7 @@ def handle_email_alerts( app, host, repository, content_alert_str='', new_repo_a def have_shed_tool_conf_for_install( app ): - if not app.toolbox.shed_tool_confs: - return False - migrated_tools_conf_path, migrated_tools_conf_name = os.path.split( app.config.migrated_tools_config ) - for shed_tool_conf_dict in app.toolbox.shed_tool_confs: - shed_tool_conf = shed_tool_conf_dict[ 'config_filename' ] - shed_tool_conf_path, shed_tool_conf_name = os.path.split( shed_tool_conf ) - if shed_tool_conf_name != migrated_tools_conf_name: - return True - return False + return bool( app.toolbox.dynamic_confs( include_migrated_tool_conf=False ) ) def is_tool_shed_client( app ): From 3c5c2e90c6a740551bf62227832232f192b115a8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 10:28:37 -0500 Subject: [PATCH 082/473] Abstract tool lineage handling out of ToolBox. Start work on infrastructure to support tool lineages not defined within the tool shed install model. --- lib/galaxy/tools/__init__.py | 40 ++++++----------- lib/galaxy/tools/toolbox/lineages/__init__.py | 5 +++ lib/galaxy/tools/toolbox/lineages/factory.py | 28 ++++++++++++ .../tools/toolbox/lineages/interface.py | 14 ++++++ .../tools/toolbox/lineages/tool_shed.py | 43 +++++++++++++++++++ 5 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 lib/galaxy/tools/toolbox/lineages/__init__.py create mode 100644 lib/galaxy/tools/toolbox/lineages/factory.py create mode 100644 lib/galaxy/tools/toolbox/lineages/interface.py create mode 100644 lib/galaxy/tools/toolbox/lineages/tool_shed.py diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index b37746d3ce5..ab6374b85f5 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -53,6 +53,7 @@ from galaxy.tools.test import parse_tests from galaxy.tools.parser import get_tool_source from galaxy.tools.parser.xml import XmlPageSource from galaxy.tools.toolbox import tool_tag_manager +from galaxy.tools.toolbox.lineages import LineageMap from galaxy.util import listify, parse_xml, rst_to_html, string_as_bool, string_to_object from galaxy.tools.parameters.meta import expand_meta_parameters from galaxy.util.bunch import Bunch @@ -139,6 +140,7 @@ class ToolBox( object, Dictifiable ): self.tool_panel = odict() self.index = 0 self.data_manager_tools = odict() + self.lineage_map = LineageMap( app ) # File that contains the XML section and tool tags from all tool panel config files integrated into a # single file that defines the tool panel layout. This file can be changed by the Galaxy administrator # (in a way similar to the single tool_conf.xml file in the past) to alter the layout of the tool panel. @@ -371,8 +373,8 @@ class ToolBox( object, Dictifiable ): log.debug( "Loaded tool id: %s, version: %s into tool panel.." % ( tool.id, tool.version ) ) else: # We are in the process of installing the tool. - tool_version = self.__get_tool_version( tool_id ) - tool_lineage_ids = tool_version.get_version_ids( self.app, reverse=True ) + tool_lineage = self.lineage_map.get( tool_id ) + tool_lineage_ids = tool_lineage.get_version_ids( reverse=True ) for lineage_id in tool_lineage_ids: if lineage_id in self.tools_by_id: loaded_version_key = 'tool_%s' % lineage_id @@ -512,9 +514,9 @@ class ToolBox( object, Dictifiable ): return self.tools_by_id[ tool_id ] #exact tool id match not found, or all versions requested, search for other options, e.g. migrated tools or different versions rval = [] - tv = self.__get_tool_version( tool_id ) - if tv: - tool_version_ids = tv.get_version_ids( self.app ) + tool_lineage = self.lineage_map.get( tool_id ) + if tool_lineage: + tool_version_ids = tool_lineage.get_version_ids( ) for tool_version_id in tool_version_ids: if tool_version_id in self.tools_by_id: rval.append( self.tools_by_id[ tool_version_id ] ) @@ -560,9 +562,9 @@ class ToolBox( object, Dictifiable ): def get_loaded_tools_by_lineage( self, tool_id ): """Get all loaded tools associated by lineage to the tool whose id is tool_id.""" - tv = self.__get_tool_version( tool_id ) - if tv: - tool_version_ids = tv.get_version_ids( self.app ) + tool_lineage = self.lineage_map.get( tool_id ) + if tool_lineage: + tool_version_ids = tool_lineage.get_version_ids( ) available_tool_versions = [] for tool_version_id in tool_version_ids: if tool_version_id in self.tools_by_id: @@ -574,12 +576,6 @@ class ToolBox( object, Dictifiable ): return [ tool ] return [] - def __get_tool_version( self, tool_id ): - """Return a ToolVersion if one exists for the tool_id""" - return self.app.install_model.context.query( self.app.install_model.ToolVersion ) \ - .filter( self.app.install_model.ToolVersion.table.c.tool_id == tool_id ) \ - .first() - def tools( self ): return self.tools_by_id.iteritems() @@ -745,12 +741,10 @@ class ToolBox( object, Dictifiable ): tool.guid = guid tool.version = elem.find( "version" ).text # Make sure the tool has a tool_version. - if not self.__get_tool_version( tool.id ): - tool_version = self.app.install_model.ToolVersion( tool_id=tool.id, tool_shed_repository=tool_shed_repository ) - self.app.install_model.context.add( tool_version ) - self.app.install_model.context.flush() + tool_lineage = self.lineage_map.register( tool, tool_shed_repository=tool_shed_repository ) # Load the tool's lineage ids. - tool.lineage_ids = tool.tool_version.get_version_ids( self.app ) + tool.lineage = tool_lineage + tool.lineage_ids = tool_lineage.get_version_ids( ) self.tool_tag_manager.handle_tags( tool.id, elem ) self.__add_tool( tool, load_panel_dict, panel_dict ) # Always load the tool into the integrated_panel_dict, or it will not be included in the integrated_tool_panel.xml file. @@ -1492,14 +1486,6 @@ class Tool( object, Dictifiable ): return tool_version.get_versions( self.app ) return [] - @property - def tool_version_ids( self ): - # If we have versions, return a list of their tool_ids. - tool_version = self.tool_version - if tool_version: - return tool_version.get_version_ids( self.app ) - return [] - @property def tool_shed_repository( self ): # If this tool is included in an installed tool shed repository, return it. diff --git a/lib/galaxy/tools/toolbox/lineages/__init__.py b/lib/galaxy/tools/toolbox/lineages/__init__.py new file mode 100644 index 00000000000..49925c2a7a6 --- /dev/null +++ b/lib/galaxy/tools/toolbox/lineages/__init__.py @@ -0,0 +1,5 @@ +from .factory import LineageMap +from .interface import ToolLineage + + +__all__ = ["LineageMap", "ToolLineage"] diff --git a/lib/galaxy/tools/toolbox/lineages/factory.py b/lib/galaxy/tools/toolbox/lineages/factory.py new file mode 100644 index 00000000000..77efe944017 --- /dev/null +++ b/lib/galaxy/tools/toolbox/lineages/factory.py @@ -0,0 +1,28 @@ +from .tool_shed import ToolShedLineage + + +class LineageMap(object): + """ Map each unique tool id to a lineage object. + """ + + def __init__(self, app): + self.lineage_map = {} + self.app = app + + def register(self, tool, **kwds): + tool_id = tool.id + if tool_id not in self.lineage_map: + tool_shed_repository = kwds.get("tool_shed_repository", None) + lineage = ToolShedLineage.from_tool(self.app, tool, tool_shed_repository) + self.lineage_map[tool_id] = lineage + return self.lineage_map[tool_id] + + def get(self, tool_id): + if tool_id not in self.lineage_map: + lineage = ToolShedLineage.from_tool_id( self.app, tool_id ) + if lineage: + self.lineage_map[tool_id] = lineage + + return self.lineage_map.get(tool_id, None) + +__all__ = ["LineageMap"] diff --git a/lib/galaxy/tools/toolbox/lineages/interface.py b/lib/galaxy/tools/toolbox/lineages/interface.py new file mode 100644 index 00000000000..1135213b8d1 --- /dev/null +++ b/lib/galaxy/tools/toolbox/lineages/interface.py @@ -0,0 +1,14 @@ +from abc import ABCMeta +from abc import abstractmethod + + +class ToolLineage(object): + """ + """ + __metaclass__ = ABCMeta + + @abstractmethod + def get_version_ids( self, reverse=False ): + """ Return an ordered list of lineages in this chain, from + oldest to newest. + """ diff --git a/lib/galaxy/tools/toolbox/lineages/tool_shed.py b/lib/galaxy/tools/toolbox/lineages/tool_shed.py new file mode 100644 index 00000000000..66b83e05798 --- /dev/null +++ b/lib/galaxy/tools/toolbox/lineages/tool_shed.py @@ -0,0 +1,43 @@ +from .interface import ToolLineage + +from galaxy.model.tool_shed_install import ToolVersion + + +class ToolShedLineage(ToolLineage): + """ Representation of tool lineage derived from tool shed repository + installations. """ + + def __init__(self, app, tool_version): + self.app = app + self.tool_version_id = tool_version.id + + @staticmethod + def from_tool( app, tool, tool_shed_repository ): + # Make sure the tool has a tool_version. + if not get_install_tool_version( app, tool.id ): + tool_version = ToolVersion( tool_id=tool.id, tool_shed_repository=tool_shed_repository ) + app.install_model.context.add( tool_version ) + app.install_model.context.flush() + return ToolShedLineage( app, tool.tool_version ) + + @staticmethod + def from_tool_id( app, tool_id ): + tool_version = get_install_tool_version( app, tool_id ) + if tool_version: + return ToolShedLineage( app, tool_version ) + else: + return None + + def get_version_ids( self, reverse=False ): + tool_version = self.app.install_model.context.query( ToolVersion ).get( self.tool_version_id ) + return tool_version.get_version_ids( self.app, reverse=reverse ) + + +def get_install_tool_version( app, tool_id ): + return app.install_model.context.query( + app.install_model.ToolVersion + ).filter( + app.install_model.ToolVersion.table.c.tool_id == tool_id + ).first() + +__all__ = [ "ToolShedLineage" ] From 65e95109818fd89e2a6be8b5cb16811de5a8033f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 10:28:37 -0500 Subject: [PATCH 083/473] Abstract out higher-level helpers in ToolBox for lineage reasoning. Reduces code duplication slightly and simplifies __add_tool_to_tool_panel (cyclomatic complexity reduced from 20 to 14). --- lib/galaxy/tools/__init__.py | 47 +++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index ab6374b85f5..19e05bf92f9 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -325,20 +325,13 @@ class ToolBox( object, Dictifiable ): panel_dict = panel_component.elems else: panel_dict = panel_component - already_loaded = False - loaded_version_key = None - lineage_id = None - for lineage_id in tool.lineage_ids: - if lineage_id in self.tools_by_id: - loaded_version_key = 'tool_%s' % lineage_id - if loaded_version_key in panel_dict: - already_loaded = True - break - if already_loaded: - if tool.lineage_ids.index( tool_id ) > tool.lineage_ids.index( lineage_id ): + related_tool = self._lineage_in_panel( panel_dict, tool=tool ) + if related_tool: + if self._newer_tool( tool, related_tool ): + lineage_id = "tool_%s" % related_tool.id + index = panel_dict.keys().index( lineage_id ) + del panel_dict[ lineage_id ] key = 'tool_%s' % tool.id - index = panel_dict.keys().index( loaded_version_key ) - del panel_dict[ loaded_version_key ] panel_dict.insert( index, key, tool ) log.debug( "Loaded tool id: %s, version: %s into tool panel." % ( tool.id, tool.version ) ) else: @@ -374,12 +367,7 @@ class ToolBox( object, Dictifiable ): else: # We are in the process of installing the tool. tool_lineage = self.lineage_map.get( tool_id ) - tool_lineage_ids = tool_lineage.get_version_ids( reverse=True ) - for lineage_id in tool_lineage_ids: - if lineage_id in self.tools_by_id: - loaded_version_key = 'tool_%s' % lineage_id - if loaded_version_key in panel_dict: - already_loaded = True + already_loaded = self._lineage_in_panel( panel_dict, tool_lineage=tool_lineage ) is not None if not already_loaded: # If the tool is not defined in integrated_tool_panel.xml, append it to the tool panel. panel_dict[ key ] = tool @@ -1193,6 +1181,27 @@ class ToolBox( object, Dictifiable ): return rval + def _lineage_in_panel( self, panel_dict, tool=None, tool_lineage=None ): + """ If tool with same lineage already in panel (or section) - find + and return it. Otherwise return None. + """ + if tool_lineage is None: + assert tool is not None + tool_lineage = tool.lineage + lineage_ids = tool_lineage.get_version_ids( reverse=True ) + for lineage_id in lineage_ids: + if lineage_id in self.tools_by_id: + loaded_version_key = 'tool_%s' % lineage_id + if loaded_version_key in panel_dict: + return panel_dict[ loaded_version_key ] + return None + + def _newer_tool( self, tool1, tool2 ): + """ Return True if tool1 is considered "newer" given its own lineage + description. + """ + return tool1.lineage_ids.index( tool1.id ) > tool1.lineage_ids.index( tool2.id ) + def _filter_for_panel( item, filters, context ): """ From 0e17abd7ea6e700a86a72018e2b3957627c98cd2 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 11:10:41 -0500 Subject: [PATCH 084/473] Fix unit tests for 7cee92f. --- test/unit/tools/test_toolbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index f04d92f292b..52616b3a7c8 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -197,13 +197,13 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): def test_update_shed_conf(self): self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {} ) + self.toolbox.update_shed_config( { "config_filename": "tool_conf.xml" } ) assert self.reindexed self.assert_integerated_tool_panel(exists=True) def test_update_shed_conf_deactivate_only(self): self.__setup_shed_tool_conf() - self.toolbox.update_shed_config( 0, {}, integrated_panel_changes=False ) + self.toolbox.update_shed_config( { "config_filename": "tool_conf.xml" }, integrated_panel_changes=False ) assert self.reindexed # No changes, should be regenerated self.assert_integerated_tool_panel(exists=False) From 8edced26852bafdd8f1fd8aa3845403357d5cf5f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 11:48:38 -0500 Subject: [PATCH 085/473] Bugfix: Apply tool panel filtering in workflow editor. Bug originally reported by Eric here - http://dev.list.galaxyproject.org/Bug-Toolbox-filters-not-applied-in-workflows-td4662879.html (though this only fixes the superficial bug of them showing up in the workflow editor - and does not yet implement "strong" toolbox filters as requested). This is actually a broader change that centralizes logic for listing out tool panel contents when displaying components to the user and the added filtering comes about from that. It also eliminates the last places Galaxy was accessing toolbox.tool_panel directly (in those mako templates). --- lib/galaxy/tools/__init__.py | 24 +++++++++---------- templates/admin/package_tool.mako | 2 +- templates/admin/reload_tool.mako | 2 +- templates/webapps/galaxy/workflow/editor.mako | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 19e05bf92f9..c026b8fe481 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1147,24 +1147,22 @@ class ToolBox( object, Dictifiable ): """ return self.app.model.context + def tool_panel_contents( self, trans, **kwds ): + """ Filter tool_panel contents for displaying for user. + """ + context = Bunch( toolbox=self, trans=trans ) + filters = self.filter_factory.build_filters( trans ) + for elt in self.tool_panel.itervalues(): + elt = _filter_for_panel( elt, filters, context ) + if elt: + yield elt + def to_dict( self, trans, in_panel=True, **kwds ): """ to_dict toolbox. """ - - context = Bunch( toolbox=self, trans=trans, **kwds ) if in_panel: - panel_elts = [ val for val in self.tool_panel.itervalues() ] - - filters = self.filter_factory.build_filters( trans, **kwds ) - - filtered_panel_elts = [] - for index, elt in enumerate( panel_elts ): - elt = _filter_for_panel( elt, filters, context ) - if elt: - filtered_panel_elts.append( elt ) - panel_elts = filtered_panel_elts - + panel_elts = list( self.tool_panel_contents( trans, **kwds ) ) # Produce panel. rval = [] kwargs = dict( diff --git a/templates/admin/package_tool.mako b/templates/admin/package_tool.mako index fef8ac765d5..1557a5a8273 100644 --- a/templates/admin/package_tool.mako +++ b/templates/admin/package_tool.mako @@ -26,7 +26,7 @@ $().ready(function() { Tool to bundle: - %for key, val in toolbox.tool_panel.items(): + %for val in toolbox.tool_panel_contents( trans ): %if isinstance( val, Tool ): %elif isinstance( val, ToolSection ): diff --git a/templates/webapps/galaxy/workflow/editor.mako b/templates/webapps/galaxy/workflow/editor.mako index d50d4a27b1e..e4de18ee64f 100644 --- a/templates/webapps/galaxy/workflow/editor.mako +++ b/templates/webapps/galaxy/workflow/editor.mako @@ -284,7 +284,7 @@
- %for key, val in app.toolbox.tool_panel.items(): + %for val in app.toolbox.tool_panel_contents( trans ):
%if isinstance( val, Tool ): ${render_tool( val, False )} From 0466cd9e4784a665a9f16c14e824926a87c9b7b8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 12:14:41 -0500 Subject: [PATCH 086/473] Shield ToolBox details from Tool. Better seperation of concerns. --- lib/galaxy/tools/__init__.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index c026b8fe481..491559aa992 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -304,6 +304,23 @@ class ToolBox( object, Dictifiable ): tool_section = None return tool_panel_section_key, tool_section + def get_integrated_section_for_tool( self, tool ): + tool_id = tool.id + for key, item in self.integrated_tool_panel.items(): + if item: + if isinstance( item, Tool ): + if item.id == tool_id: + return '', '' + if isinstance( item, ToolSection ): + section_id = item.id or '' + section_name = item.name or '' + for section_key, section_item in item.elems.items(): + if isinstance( section_item, Tool ): + if section_item: + if section_item.id == tool_id: + return section_id, section_name + return None, None + def __resolve_tool_path(self, tool_path, config_filename): if not tool_path: # Default to backward compatible config setting. @@ -1553,20 +1570,7 @@ class Tool( object, Dictifiable ): return self.app.job_config.get_destination(self.__get_job_tool_configuration(job_params=job_params).destination) def get_panel_section( self ): - for key, item in self.app.toolbox.integrated_tool_panel.items(): - if item: - if isinstance( item, Tool ): - if item.id == self.id: - return '', '' - if isinstance( item, ToolSection ): - section_id = item.id or '' - section_name = item.name or '' - for section_key, section_item in item.elems.items(): - if isinstance( section_item, Tool ): - if section_item: - if section_item.id == self.id: - return section_id, section_name - return None, None + return self.app.toolbox.get_integrated_section_for_tool( self ) def parse( self, tool_source, guid=None ): """ From 2717c85e54a56a6f98010009b09343eaa701e034 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 12:23:55 -0500 Subject: [PATCH 087/473] Update attribute and method names in ToolBox to reflect encapsulation efforts. Using ToolBox._xxx instead of ToolBox.__xxx because realistically ToolBox is still much to large to grok and so I imagine it will need to be broken up even more (base class focused on just the panel details perhaps - or mixins - etc...). --- lib/galaxy/tools/__init__.py | 290 +++++++++--------- .../test_tool_panel_manager.py | 20 +- test/unit/tools/test_toolbox.py | 20 +- 3 files changed, 166 insertions(+), 164 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 491559aa992..16905e18dbd 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -131,49 +131,50 @@ class ToolBox( object, Dictifiable ): Create a toolbox from the config files named by `config_filenames`, using `tool_root_dir` as the base directory for finding individual tool config files. """ - # The shed_tool_confs list contains dictionaries storing information about the tools defined in each - # shed-related shed_tool_conf.xml file. - self.shed_tool_confs = [] - self.tools_by_id = {} - self.workflows_by_id = {} + # The _dynamic_tool_confs list contains dictionaries storing + # information about the tools defined in each shed-related + # shed_tool_conf.xml file. + self._dynamic_tool_confs = [] + self._tools_by_id = {} + self._workflows_by_id = {} # In-memory dictionary that defines the layout of the tool panel. - self.tool_panel = odict() - self.index = 0 + self._tool_panel = odict() + self._index = 0 self.data_manager_tools = odict() - self.lineage_map = LineageMap( app ) + self._lineage_map = LineageMap( app ) # File that contains the XML section and tool tags from all tool panel config files integrated into a # single file that defines the tool panel layout. This file can be changed by the Galaxy administrator # (in a way similar to the single tool_conf.xml file in the past) to alter the layout of the tool panel. - self.integrated_tool_panel_config = app.config.integrated_tool_panel_config + self._integrated_tool_panel_config = app.config.integrated_tool_panel_config # In-memory dictionary that defines the layout of the tool_panel.xml file on disk. - self.integrated_tool_panel = odict() - self.integrated_tool_panel_config_has_contents = os.path.exists( self.integrated_tool_panel_config ) and os.stat( self.integrated_tool_panel_config ).st_size > 0 - if self.integrated_tool_panel_config_has_contents: - self.load_integrated_tool_panel_keys() + self._integrated_tool_panel = odict() + self._integrated_tool_panel_config_has_contents = os.path.exists( self._integrated_tool_panel_config ) and os.stat( self._integrated_tool_panel_config ).st_size > 0 + if self._integrated_tool_panel_config_has_contents: + self._load_integrated_tool_panel_keys() # The following refers to the tool_path config setting for backward compatibility. The shed-related # (e.g., shed_tool_conf.xml) files include the tool_path attribute within the tag. - self.tool_root_dir = tool_root_dir + self._tool_root_dir = tool_root_dir self.app = app - self.tool_watcher = watcher.get_watcher( self, app.config ) - self.filter_factory = FilterFactory( self ) - self.init_dependency_manager() - self.tool_tag_manager = tool_tag_manager( app ) - self.init_tools_from_configs( config_filenames ) - if self.app.name == 'galaxy' and self.integrated_tool_panel_config_has_contents: - # Load self.tool_panel based on the order in self.integrated_tool_panel. - self.load_tool_panel() + self._tool_watcher = watcher.get_watcher( self, app.config ) + self._filter_factory = FilterFactory( self ) + self._init_dependency_manager() + self._tool_tag_manager = tool_tag_manager( app ) + self._init_tools_from_configs( config_filenames ) + if self.app.name == 'galaxy' and self._integrated_tool_panel_config_has_contents: + # Load self._tool_panel based on the order in self._integrated_tool_panel. + self._load_tool_panel() if app.config.update_integrated_tool_panel: # Write the current in-memory integrated_tool_panel to the integrated_tool_panel.xml file. # This will cover cases where the Galaxy administrator manually edited one or more of the tool panel # config files, adding or removing locally developed tools or workflows. The value of integrated_tool_panel # will be False when things like functional tests are the caller. - self.write_integrated_tool_panel_config_file() + self._write_integrated_tool_panel_config_file() - def init_tools_from_configs( self, config_filenames ): + def _init_tools_from_configs( self, config_filenames ): """ Read through all tool config files and initialize tools in each with init_tools_from_config below. """ - self.tool_tag_manager.reset_tags() + self._tool_tag_manager.reset_tags() config_filenames = listify( config_filenames ) for config_filename in config_filenames: if os.path.isdir( config_filename ): @@ -183,11 +184,11 @@ class ToolBox( object, Dictifiable ): config_filenames.extend( directory_config_files ) for config_filename in config_filenames: try: - self.init_tools_from_config( config_filename ) + self._init_tools_from_config( config_filename ) except: log.exception( "Error loading tools defined in config %s", config_filename ) - def init_tools_from_config( self, config_filename ): + def _init_tools_from_config( self, config_filename ): """ Read the configuration file and load each tool. The following tags are currently supported: @@ -218,10 +219,10 @@ class ToolBox( object, Dictifiable ): parsing_shed_tool_conf = False tool_path = self.__resolve_tool_path(tool_path, config_filename) # Only load the panel_dict under certain conditions. - load_panel_dict = not self.integrated_tool_panel_config_has_contents + load_panel_dict = not self._integrated_tool_panel_config_has_contents for _, elem in enumerate( root ): - index = self.index - self.index += 1 + index = self._index + self._index += 1 if parsing_shed_tool_conf: config_elems.append( elem ) self.load_item( @@ -237,7 +238,7 @@ class ToolBox( object, Dictifiable ): shed_tool_conf_dict = dict( config_filename=config_filename, tool_path=tool_path, config_elems=config_elems ) - self.shed_tool_confs.append( shed_tool_conf_dict ) + self._dynamic_tool_confs.append( shed_tool_conf_dict ) def load_item( self, elem, tool_path, panel_dict=None, integrated_panel_dict=None, load_panel_dict=True, guid=None, index=None, internal=False ): item_type = elem.tag @@ -247,22 +248,22 @@ class ToolBox( object, Dictifiable ): return if panel_dict is None: - panel_dict = self.tool_panel + panel_dict = self._tool_panel if integrated_panel_dict is None: - integrated_panel_dict = self.integrated_tool_panel + integrated_panel_dict = self._integrated_tool_panel if item_type == 'tool': - self.load_tool_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, tool_path=tool_path, load_panel_dict=load_panel_dict, guid=guid, index=index ) + self._load_tool_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, tool_path=tool_path, load_panel_dict=load_panel_dict, guid=guid, index=index ) elif item_type == 'workflow': - self.load_workflow_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) + self._load_workflow_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) elif item_type == 'section': - self.load_section_tag_set( elem, tool_path=tool_path, load_panel_dict=load_panel_dict, index=index ) + self._load_section_tag_set( elem, tool_path=tool_path, load_panel_dict=load_panel_dict, index=index ) elif item_type == 'label': - self.load_label_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) + self._load_label_tag_set( elem, panel_dict=panel_dict, integrated_panel_dict=integrated_panel_dict, load_panel_dict=load_panel_dict, index=index ) elif item_type == 'tool_dir': - self.load_tooldir_tag_set( elem, panel_dict, tool_path, integrated_panel_dict, load_panel_dict=load_panel_dict ) + self._load_tooldir_tag_set( elem, panel_dict, tool_path, integrated_panel_dict, load_panel_dict=load_panel_dict ) def get_shed_config_dict_by_filename( self, filename, default=None ): - for shed_config_dict in self.shed_tool_confs: + for shed_config_dict in self._dynamic_tool_confs: if shed_config_dict[ 'config_filename' ] == filename: return shed_config_dict return default @@ -273,22 +274,22 @@ class ToolBox( object, Dictifiable ): that doesn't affect that file). """ app = self.app - for index, my_shed_tool_conf in enumerate( self.shed_tool_confs ): + for index, my_shed_tool_conf in enumerate( self._dynamic_tool_confs ): if shed_conf['config_filename'] == my_shed_tool_conf['config_filename']: - self.shed_tool_confs[ index ] = shed_conf + self._dynamic_tool_confs[ index ] = shed_conf if integrated_panel_changes and app.config.update_integrated_tool_panel: # Write the current in-memory version of the integrated_tool_panel.xml file to disk. - self.write_integrated_tool_panel_config_file() + self._write_integrated_tool_panel_config_file() app.reindex_tool_search() def get_section( self, section_id, new_label=None, create_if_needed=False ): tool_panel_section_key = str( section_id ) - if tool_panel_section_key in self.tool_panel: - # Appending a tool to an existing section in toolbox.tool_panel - tool_section = self.tool_panel[ tool_panel_section_key ] + if tool_panel_section_key in self._tool_panel: + # Appending a tool to an existing section in toolbox._tool_panel + tool_section = self._tool_panel[ tool_panel_section_key ] log.debug( "Appending to tool panel section: %s" % str( tool_section.name ) ) elif create_if_needed: - # Appending a new section to toolbox.tool_panel + # Appending a new section to toolbox._tool_panel if new_label is None: # This might add an ugly section label to the tool panel, but, oh well... new_label = section_id @@ -298,7 +299,7 @@ class ToolBox( object, Dictifiable ): 'version': '', } tool_section = ToolSection( section_dict ) - self.tool_panel[ tool_panel_section_key ] = tool_section + self._tool_panel[ tool_panel_section_key ] = tool_section log.debug( "Loading new tool panel section: %s" % str( tool_section.name ) ) else: tool_section = None @@ -306,7 +307,7 @@ class ToolBox( object, Dictifiable ): def get_integrated_section_for_tool( self, tool ): tool_id = tool.id - for key, item in self.integrated_tool_panel.items(): + for key, item in self._integrated_tool_panel.items(): if item: if isinstance( item, Tool ): if item.id == tool_id: @@ -324,7 +325,7 @@ class ToolBox( object, Dictifiable ): def __resolve_tool_path(self, tool_path, config_filename): if not tool_path: # Default to backward compatible config setting. - tool_path = self.tool_root_dir + tool_path = self._tool_root_dir else: # Allow use of __tool_conf_dir__ in toolbox config files. tool_conf_dir = os.path.dirname(config_filename) @@ -335,9 +336,9 @@ class ToolBox( object, Dictifiable ): def __add_tool_to_tool_panel( self, tool, panel_component, section=False ): # See if a version of this tool is already loaded into the tool panel. # The value of panel_component will be a ToolSection (if the value of - # section=True) or self.tool_panel (if section=False). + # section=True) or self._tool_panel (if section=False). tool_id = str( tool.id ) - tool = self.tools_by_id[ tool_id ] + tool = self._tools_by_id[ tool_id ] if section: panel_dict = panel_component.elems else: @@ -355,7 +356,7 @@ class ToolBox( object, Dictifiable ): inserted = False key = 'tool_%s' % tool.id # The value of panel_component is the in-memory tool panel dictionary. - for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ): + for index, integrated_panel_key in enumerate( self._integrated_tool_panel.keys() ): if key == integrated_panel_key: panel_dict.insert( index, key, tool ) inserted = True @@ -363,7 +364,7 @@ class ToolBox( object, Dictifiable ): # Check the tool's installed versions. for lineage_id in tool.lineage_ids: lineage_id_key = 'tool_%s' % lineage_id - for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ): + for index, integrated_panel_key in enumerate( self._integrated_tool_panel.keys() ): if lineage_id_key == integrated_panel_key: panel_dict.insert( index, key, tool ) inserted = True @@ -383,27 +384,28 @@ class ToolBox( object, Dictifiable ): log.debug( "Loaded tool id: %s, version: %s into tool panel.." % ( tool.id, tool.version ) ) else: # We are in the process of installing the tool. - tool_lineage = self.lineage_map.get( tool_id ) + + tool_lineage = self._lineage_map.get( tool_id ) already_loaded = self._lineage_in_panel( panel_dict, tool_lineage=tool_lineage ) is not None if not already_loaded: # If the tool is not defined in integrated_tool_panel.xml, append it to the tool panel. panel_dict[ key ] = tool log.debug( "Loaded tool id: %s, version: %s into tool panel...." % ( tool.id, tool.version ) ) - def load_tool_panel( self ): - for key, val in self.integrated_tool_panel.items(): + def _load_tool_panel( self ): + for key, val in self._integrated_tool_panel.items(): if isinstance( val, Tool ): tool_id = key.replace( 'tool_', '', 1 ) - if tool_id in self.tools_by_id: - self.__add_tool_to_tool_panel( val, self.tool_panel, section=False ) + if tool_id in self._tools_by_id: + self.__add_tool_to_tool_panel( val, self._tool_panel, section=False ) elif isinstance( val, Workflow ): workflow_id = key.replace( 'workflow_', '', 1 ) - if workflow_id in self.workflows_by_id: - workflow = self.workflows_by_id[ workflow_id ] - self.tool_panel[ key ] = workflow + if workflow_id in self._workflows_by_id: + workflow = self._workflows_by_id[ workflow_id ] + self._tool_panel[ key ] = workflow log.debug( "Loaded workflow: %s %s" % ( workflow_id, workflow.name ) ) elif isinstance( val, ToolSectionLabel ): - self.tool_panel[ key ] = val + self._tool_panel[ key ] = val elif isinstance( val, ToolSection ): section_dict = { 'id': val.id or '', @@ -415,36 +417,36 @@ class ToolBox( object, Dictifiable ): for section_key, section_val in val.elems.items(): if isinstance( section_val, Tool ): tool_id = section_key.replace( 'tool_', '', 1 ) - if tool_id in self.tools_by_id: + if tool_id in self._tools_by_id: self.__add_tool_to_tool_panel( section_val, section, section=True ) elif isinstance( section_val, Workflow ): workflow_id = section_key.replace( 'workflow_', '', 1 ) - if workflow_id in self.workflows_by_id: - workflow = self.workflows_by_id[ workflow_id ] + if workflow_id in self._workflows_by_id: + workflow = self._workflows_by_id[ workflow_id ] section.elems[ section_key ] = workflow log.debug( "Loaded workflow: %s %s" % ( workflow_id, workflow.name ) ) elif isinstance( section_val, ToolSectionLabel ): if section_val: section.elems[ section_key ] = section_val log.debug( "Loaded label: %s" % ( section_val.text ) ) - self.tool_panel[ key ] = section + self._tool_panel[ key ] = section - def load_integrated_tool_panel_keys( self ): + def _load_integrated_tool_panel_keys( self ): """ Load the integrated tool panel keys, setting values for tools and workflows to None. The values will be reset when the various tool panel config files are parsed, at which time the tools and workflows are loaded. """ - tree = parse_xml( self.integrated_tool_panel_config ) + tree = parse_xml( self._integrated_tool_panel_config ) root = tree.getroot() for elem in root: if elem.tag == 'tool': key = 'tool_%s' % elem.get( 'id' ) - self.integrated_tool_panel[ key ] = None + self._integrated_tool_panel[ key ] = None elif elem.tag == 'workflow': key = 'workflow_%s' % elem.get( 'id' ) - self.integrated_tool_panel[ key ] = None + self._integrated_tool_panel[ key ] = None elif elem.tag == 'section': section = ToolSection( elem ) for section_elem in elem: @@ -458,12 +460,12 @@ class ToolBox( object, Dictifiable ): key = 'label_%s' % section_elem.get( 'id' ) section.elems[ key ] = None key = elem.get( 'id' ) - self.integrated_tool_panel[ key ] = section + self._integrated_tool_panel[ key ] = section elif elem.tag == 'label': key = 'label_%s' % elem.get( 'id' ) - self.integrated_tool_panel[ key ] = None + self._integrated_tool_panel[ key ] = None - def write_integrated_tool_panel_config_file( self ): + def _write_integrated_tool_panel_config_file( self ): """ Write the current in-memory version of the integrated_tool_panel.xml file to disk. Since Galaxy administrators use this file to manage the tool panel, we'll not use xml_to_string() since it doesn't write XML quite right. @@ -474,7 +476,7 @@ class ToolBox( object, Dictifiable ): os.write( fd, ' \n') - for key, item in self.integrated_tool_panel.items(): + for key, item in self._integrated_tool_panel.items(): if item: if isinstance( item, Tool ): os.write( fd, ' \n' % item.id ) @@ -506,28 +508,28 @@ class ToolBox( object, Dictifiable ): os.write( fd, ' \n' ) os.write( fd, '\n' ) os.close( fd ) - shutil.move( filename, os.path.abspath( self.integrated_tool_panel_config ) ) - os.chmod( self.integrated_tool_panel_config, 0644 ) + shutil.move( filename, os.path.abspath( self._integrated_tool_panel_config ) ) + os.chmod( self._integrated_tool_panel_config, 0644 ) def get_tool( self, tool_id, tool_version=None, get_all_versions=False, exact=False ): """Attempt to locate a tool in the tool box.""" if get_all_versions and exact: raise AssertionError("Cannot specify get_tool with both get_all_versions and exact as True") - if tool_id in self.tools_by_id and not get_all_versions: + if tool_id in self._tools_by_id and not get_all_versions: #tool_id exactly matches an available tool by id (which is 'old' tool_id or guid) - return self.tools_by_id[ tool_id ] + return self._tools_by_id[ tool_id ] #exact tool id match not found, or all versions requested, search for other options, e.g. migrated tools or different versions rval = [] - tool_lineage = self.lineage_map.get( tool_id ) + tool_lineage = self._lineage_map.get( tool_id ) if tool_lineage: tool_version_ids = tool_lineage.get_version_ids( ) for tool_version_id in tool_version_ids: - if tool_version_id in self.tools_by_id: - rval.append( self.tools_by_id[ tool_version_id ] ) + if tool_version_id in self._tools_by_id: + rval.append( self._tools_by_id[ tool_version_id ] ) if not rval: #still no tool, do a deeper search and try to match by old ids - for tool in self.tools_by_id.itervalues(): + for tool in self._tools_by_id.itervalues(): if tool.old_id == tool_id: rval.append( tool ) if rval: @@ -543,8 +545,8 @@ class ToolBox( object, Dictifiable ): return rval[0] #We now likely have a Toolshed guid passed in, but no supporting database entries #If the tool exists by exact id and is loaded then provide exact match within a list - if tool_id in self.tools_by_id: - return[ self.tools_by_id[ tool_id ] ] + if tool_id in self._tools_by_id: + return[ self._tools_by_id[ tool_id ] ] return None def has_tool( self, tool_id, exact=False ): @@ -556,7 +558,7 @@ class ToolBox( object, Dictifiable ): tools (using get_tool for inexact matches which currently returns the oldest tool shed installed tool with the same short id). """ - if tool_id not in self.tools_by_id: + if tool_id not in self._tools_by_id: tool = self.get_tool( tool_id ) if tool: tool_id = tool.id @@ -567,22 +569,22 @@ class ToolBox( object, Dictifiable ): def get_loaded_tools_by_lineage( self, tool_id ): """Get all loaded tools associated by lineage to the tool whose id is tool_id.""" - tool_lineage = self.lineage_map.get( tool_id ) + tool_lineage = self._lineage_map.get( tool_id ) if tool_lineage: tool_version_ids = tool_lineage.get_version_ids( ) available_tool_versions = [] for tool_version_id in tool_version_ids: - if tool_version_id in self.tools_by_id: - available_tool_versions.append( self.tools_by_id[ tool_version_id ] ) + if tool_version_id in self._tools_by_id: + available_tool_versions.append( self._tools_by_id[ tool_version_id ] ) return available_tool_versions else: - if tool_id in self.tools_by_id: - tool = self.tools_by_id[ tool_id ] + if tool_id in self._tools_by_id: + tool = self._tools_by_id[ tool_id ] return [ tool ] return [] def tools( self ): - return self.tools_by_id.iteritems() + return self._tools_by_id.iteritems() def __elem_to_tool_shed_repository( self, elem ): # The tool is contained in an installed tool shed repository, so load @@ -637,7 +639,7 @@ class ToolBox( object, Dictifiable ): def dynamic_confs( self, include_migrated_tool_conf=False ): confs = [] - for dynamic_tool_conf_dict in self.shed_tool_confs: + for dynamic_tool_conf_dict in self._dynamic_tool_confs: dynamic_tool_conf_filename = dynamic_tool_conf_dict[ 'config_filename' ] if include_migrated_tool_conf or (dynamic_tool_conf_filename != self.app.config.migrated_tools_config): confs.append( dynamic_tool_conf_dict ) @@ -712,11 +714,11 @@ class ToolBox( object, Dictifiable ): if section_key: _, tool_section = self.get_section( section_key ) if tool_section: - remove_from_dict( tool_section.elems, self.integrated_tool_panel.get( section_key, {} ) ) + remove_from_dict( tool_section.elems, self._integrated_tool_panel.get( section_key, {} ) ) else: - remove_from_dict( self.tool_panel, self.integrated_tool_panel ) + remove_from_dict( self._tool_panel, self._integrated_tool_panel ) - def load_tool_tag_set( self, elem, panel_dict, integrated_panel_dict, tool_path, load_panel_dict, guid=None, index=None ): + def _load_tool_tag_set( self, elem, panel_dict, integrated_panel_dict, tool_path, load_panel_dict, guid=None, index=None ): try: path = elem.get( "file" ) repository_id = None @@ -746,11 +748,11 @@ class ToolBox( object, Dictifiable ): tool.guid = guid tool.version = elem.find( "version" ).text # Make sure the tool has a tool_version. - tool_lineage = self.lineage_map.register( tool, tool_shed_repository=tool_shed_repository ) + tool_lineage = self._lineage_map.register( tool, tool_shed_repository=tool_shed_repository ) # Load the tool's lineage ids. tool.lineage = tool_lineage tool.lineage_ids = tool_lineage.get_version_ids( ) - self.tool_tag_manager.handle_tags( tool.id, elem ) + self._tool_tag_manager.handle_tags( tool.id, elem ) self.__add_tool( tool, load_panel_dict, panel_dict ) # Always load the tool into the integrated_panel_dict, or it will not be included in the integrated_tool_panel.xml file. if key in integrated_panel_dict or index is None: @@ -769,18 +771,18 @@ class ToolBox( object, Dictifiable ): # administrator has retrieved updates to the installed repository. In # this case, the tool may have been updated, but the version was not # changed, so the tool should always be reloaded here. We used to - # only load the tool if it was not found in self.tools_by_id, but + # only load the tool if it was not found in self._tools_by_id, but # performing that check did not enable this scenario. self.register_tool( tool ) if load_panel_dict: self.__add_tool_to_tool_panel( tool, panel_dict, section=isinstance( panel_dict, ToolSection ) ) - def load_workflow_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ): + def _load_workflow_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ): try: # TODO: should id be encoded? workflow_id = elem.get( 'id' ) - workflow = self.load_workflow( workflow_id ) - self.workflows_by_id[ workflow_id ] = workflow + workflow = self._load_workflow( workflow_id ) + self._workflows_by_id[ workflow_id ] = workflow key = 'workflow_' + workflow_id if load_panel_dict: panel_dict[ key ] = workflow @@ -792,7 +794,7 @@ class ToolBox( object, Dictifiable ): except: log.exception( "Error loading workflow: %s" % workflow_id ) - def load_label_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ): + def _load_label_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ): label = ToolSectionLabel( elem ) key = 'label_' + label.id if load_panel_dict: @@ -802,16 +804,16 @@ class ToolBox( object, Dictifiable ): else: integrated_panel_dict.insert( index, key, label ) - def load_section_tag_set( self, elem, tool_path, load_panel_dict, index=None ): + def _load_section_tag_set( self, elem, tool_path, load_panel_dict, index=None ): key = elem.get( "id" ) - if key in self.tool_panel: - section = self.tool_panel[ key ] + if key in self._tool_panel: + section = self._tool_panel[ key ] elems = section.elems else: section = ToolSection( elem ) elems = section.elems - if key in self.integrated_tool_panel: - integrated_section = self.integrated_tool_panel[ key ] + if key in self._integrated_tool_panel: + integrated_section = self._integrated_tool_panel[ key ] integrated_elems = integrated_section.elems else: integrated_section = ToolSection( elem ) @@ -828,14 +830,14 @@ class ToolBox( object, Dictifiable ): internal=True, ) if load_panel_dict: - self.tool_panel[ key ] = section + self._tool_panel[ key ] = section # Always load sections into the integrated_tool_panel. - if key in self.integrated_tool_panel or index is None: - self.integrated_tool_panel[ key ] = integrated_section + if key in self._integrated_tool_panel or index is None: + self._integrated_tool_panel[ key ] = integrated_section else: - self.integrated_tool_panel.insert( index, key, integrated_section ) + self._integrated_tool_panel.insert( index, key, integrated_section ) - def load_tooldir_tag_set(self, sub_elem, elems, tool_path, integrated_elems, load_panel_dict): + def _load_tooldir_tag_set(self, sub_elem, elems, tool_path, integrated_elems, load_panel_dict): directory = os.path.join( tool_path, sub_elem.attrib.get("dir") ) recursive = string_as_bool( sub_elem.attrib.get("recursive", True) ) self.__watch_directory( directory, elems, integrated_elems, load_panel_dict, recursive ) @@ -851,14 +853,14 @@ class ToolBox( object, Dictifiable ): integrated_elems[ key ] = tool if async: - self.load_tool_panel() + self._load_tool_panel() if self.app.config.update_integrated_tool_panel: # Write the current in-memory integrated_tool_panel to the integrated_tool_panel.xml file. # This will cover cases where the Galaxy administrator manually edited one or more of the tool panel # config files, adding or removing locally developed tools or workflows. The value of integrated_tool_panel # will be False when things like functional tests are the caller. - self.write_integrated_tool_panel_config_file() + self._write_integrated_tool_panel_config_file() return tool.id except Exception: log.exception("Failed to load potential tool %s." % tool_file) @@ -873,7 +875,7 @@ class ToolBox( object, Dictifiable ): quick_load( child_path, async=False ) tool_loaded = True if tool_loaded: - self.tool_watcher.watch_directory( directory, quick_load ) + self._tool_watcher.watch_directory( directory, quick_load ) def load_tool( self, config_file, guid=None, repository_id=None, **kwds ): """Load a single tool from the file named by `config_file` and return an instance of `Tool`.""" @@ -916,19 +918,19 @@ class ToolBox( object, Dictifiable ): if not tool_id.startswith("__"): # do not monitor special tools written to tmp directory - no reason # to monitor such a large directory. - self.tool_watcher.watch_file( config_file, tool.id ) + self._tool_watcher.watch_file( config_file, tool.id ) return tool def load_hidden_tool( self, config_file, **kwds ): """ Load a hidden tool (in this context meaning one that does not - appear in the tool panel) and register it in tools_by_id. + appear in the tool panel) and register it in _tools_by_id. """ tool = self.load_tool( config_file, **kwds ) self.register_tool( tool ) return tool def register_tool( self, tool ): - self.tools_by_id[ tool.id ] = tool + self._tools_by_id[ tool.id ] = tool def package_tool( self, trans, tool_id ): """ @@ -938,10 +940,10 @@ class ToolBox( object, Dictifiable ): :returns: tuple of tarball filename, success True/False, message/None """ # Make sure the tool is actually loaded. - if tool_id not in self.tools_by_id: + if tool_id not in self._tools_by_id: return None, False, "No tool with id %s" % escape( tool_id ) else: - tool = self.tools_by_id[ tool_id ] + tool = self._tools_by_id[ tool_id ] tarball_files = [] temp_files = [] tool_xml = file( os.path.abspath( tool.config_file ), 'r' ).read() @@ -1048,11 +1050,11 @@ class ToolBox( object, Dictifiable ): Attempt to reload the tool identified by 'tool_id', if successful replace the old tool. """ - if tool_id not in self.tools_by_id: + if tool_id not in self._tools_by_id: message = "No tool with id %s" % escape( tool_id ) status = 'error' else: - old_tool = self.tools_by_id[ tool_id ] + old_tool = self._tools_by_id[ tool_id ] new_tool = self.load_tool( old_tool.config_file ) # The tool may have been installed from a tool shed, so set the tool shed attributes. # Since the tool version may have changed, we don't override it here. @@ -1063,17 +1065,17 @@ class ToolBox( object, Dictifiable ): new_tool.repository_owner = old_tool.repository_owner new_tool.installed_changeset_revision = old_tool.installed_changeset_revision new_tool.old_id = old_tool.old_id - # Replace old_tool with new_tool in self.tool_panel + # Replace old_tool with new_tool in self._tool_panel tool_key = 'tool_' + tool_id - for key, val in self.tool_panel.items(): + for key, val in self._tool_panel.items(): if key == tool_key: - self.tool_panel[ key ] = new_tool + self._tool_panel[ key ] = new_tool break elif key.startswith( 'section' ): if tool_key in val.elems: - self.tool_panel[ key ].elems[ tool_key ] = new_tool + self._tool_panel[ key ].elems[ tool_key ] = new_tool break - self.tools_by_id[ tool_id ] = new_tool + self._tools_by_id[ tool_id ] = new_tool message = "Reloaded the tool:
" message += "name: %s
" % old_tool.name message += "id: %s
" % old_tool.id @@ -1089,21 +1091,21 @@ class ToolBox( object, Dictifiable ): promoting the next newest version of the tool into the panel if needed. """ - if tool_id not in self.tools_by_id: + if tool_id not in self._tools_by_id: message = "No tool with id %s" % escape( tool_id ) status = 'error' else: - tool = self.tools_by_id[ tool_id ] - del self.tools_by_id[ tool_id ] + tool = self._tools_by_id[ tool_id ] + del self._tools_by_id[ tool_id ] if remove_from_panel: tool_key = 'tool_' + tool_id - for key, val in self.tool_panel.items(): + for key, val in self._tool_panel.items(): if key == tool_key: - del self.tool_panel[ key ] + del self._tool_panel[ key ] break elif key.startswith( 'section' ): if tool_key in val.elems: - del self.tool_panel[ key ].elems[ tool_key ] + del self._tool_panel[ key ].elems[ tool_key ] break if tool_id in self.data_manager_tools: del self.data_manager_tools[ tool_id ] @@ -1116,7 +1118,7 @@ class ToolBox( object, Dictifiable ): return message, status def get_sections( self ): - for k, v in self.tool_panel.items(): + for k, v in self._tool_panel.items(): if isinstance( v, ToolSection ): yield (v.id, v.name) @@ -1128,17 +1130,17 @@ class ToolBox( object, Dictifiable ): if not tool_panel_section_id: tool_panel_section_id = '' else: - if tool_panel_section_id not in self.tool_panel: + if tool_panel_section_id not in self._tool_panel: # Hack introduced without comment in a29d54619813d5da992b897557162a360b8d610c- # not sure why it is needed. fixed_tool_panel_section_id = 'section_%s' % tool_panel_section_id - if fixed_tool_panel_section_id in self.tool_panel: + if fixed_tool_panel_section_id in self._tool_panel: tool_panel_section_id = fixed_tool_panel_section_id else: tool_panel_section_id = '' return tool_panel_section_id - def load_workflow( self, workflow_id ): + def _load_workflow( self, workflow_id ): """ Return an instance of 'Workflow' identified by `id`, which is encoded in the tool panel. @@ -1147,13 +1149,13 @@ class ToolBox( object, Dictifiable ): stored = self.app.model.context.query( self.app.model.StoredWorkflow ).get( id ) return stored.latest_workflow - def init_dependency_manager( self ): + def _init_dependency_manager( self ): self.dependency_manager = build_dependency_manager( self.app.config ) def handle_datatypes_changed( self ): """ Refresh upload tools when new datatypes are added. """ - for tool_id in self.tools_by_id: - tool = self.tools_by_id[ tool_id ] + for tool_id in self._tools_by_id: + tool = self._tools_by_id[ tool_id ] if isinstance( tool.tool_action, UploadToolAction ): self.reload_tool_by_id( tool_id ) @@ -1168,8 +1170,8 @@ class ToolBox( object, Dictifiable ): """ Filter tool_panel contents for displaying for user. """ context = Bunch( toolbox=self, trans=trans ) - filters = self.filter_factory.build_filters( trans ) - for elt in self.tool_panel.itervalues(): + filters = self._filter_factory.build_filters( trans ) + for elt in self._tool_panel.itervalues(): elt = _filter_for_panel( elt, filters, context ) if elt: yield elt @@ -1190,7 +1192,7 @@ class ToolBox( object, Dictifiable ): rval.append( to_dict_helper( elt, kwargs ) ) else: tools = [] - for id, tool in self.tools_by_id.items(): + for id, tool in self._tools_by_id.items(): tools.append( tool.to_dict( trans, link_details=True ) ) rval = tools @@ -1205,7 +1207,7 @@ class ToolBox( object, Dictifiable ): tool_lineage = tool.lineage lineage_ids = tool_lineage.get_version_ids( reverse=True ) for lineage_id in lineage_ids: - if lineage_id in self.tools_by_id: + if lineage_id in self._tools_by_id: loaded_version_key = 'tool_%s' % lineage_id if loaded_version_key in panel_dict: return panel_dict[ loaded_version_key ] diff --git a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py index 422f09d59b9..8fce754a843 100644 --- a/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py +++ b/test/unit/tool_shed_unit_tests/test_tool_panel_manager.py @@ -18,20 +18,20 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): assert section_id == "tid" assert len( section.elems ) == 1 # tool.xml assert section.id == "tid" - assert len( toolbox.tool_panel ) == 1 + assert len( toolbox._tool_panel ) == 1 section_id, section = tpm.handle_tool_panel_section( toolbox, new_tool_panel_section_label="tid2" ) assert section_id == "tid2" assert len( section.elems ) == 0 # new section assert section.id == "tid2" - assert len( toolbox.tool_panel ) == 2 + assert len( toolbox._tool_panel ) == 2 # Test re-fetch new section by same id. section_id, section = tpm.handle_tool_panel_section( toolbox, new_tool_panel_section_label="tid2" ) assert section_id == "tid2" assert len( section.elems ) == 0 # new section assert section.id == "tid2" - assert len( toolbox.tool_panel ) == 2 + assert len( toolbox._tool_panel ) == 2 def test_add_tool_to_panel( self ): self._init_tool() @@ -77,7 +77,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): assert not all_versions # Check that tool panel has reverted to old value... - section = self.toolbox.tool_panel["tid"] + section = self.toolbox._tool_panel["tid"] assert len(section.elems) == 0 def test_uninstall_in_section( self ): @@ -85,7 +85,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): self._verify_version_2_removed_from_panel( ) # Not in tool conf because it was uninstalled. assert "github.com/galaxyproject/example/test_tool/0.2" not in open(os.path.join(self.test_directory, "tool_conf.xml"), "r").read() - assert "tool_github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems + assert "tool_github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox._integrated_tool_panel["tid"].elems self._verify_tool_confs() def test_deactivate_outside_section( self ): @@ -109,7 +109,7 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): assert not all_versions # Check that tool panel has reverted to old value... - section = self.toolbox.tool_panel["tid"] + section = self.toolbox._tool_panel["tid"] assert len(section.elems) == 0 def _setup_two_versions_remove_one( self, section, uninstall ): @@ -126,14 +126,14 @@ class ToolPanelManagerTestCase( BaseToolBoxTestCase ): # Check that tool panel has reverted to old value... if section: - section = self.toolbox.tool_panel["tid"] + section = self.toolbox._tool_panel["tid"] assert len(section.elems) == 1 assert section.elems.values()[0].id == "github.com/galaxyproject/example/test_tool/0.1" - assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel["tid"].elems + assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox._integrated_tool_panel["tid"].elems else: - self.toolbox.tool_panel.values()[0].id == "github.com/galaxyproject/example/test_tool/0.1" - assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox.integrated_tool_panel + self.toolbox._tool_panel.values()[0].id == "github.com/galaxyproject/example/test_tool/0.1" + assert "github.com/galaxyproject/example/test_tool/0.2" not in self.toolbox._integrated_tool_panel def _remove_guids( self, guids, uninstall, shed_tool_conf="tool_conf.xml" ): self.tpm.remove_guids( diff --git a/test/unit/tools/test_toolbox.py b/test/unit/tools/test_toolbox.py index 52616b3a7c8..c1e1ae0ce88 100644 --- a/test/unit/tools/test_toolbox.py +++ b/test/unit/tools/test_toolbox.py @@ -182,7 +182,7 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): self.__verify_two_test_tools( ) # Assert only newer version of the tool loaded into the panel. - section = self.toolbox.tool_panel["tid"] + section = self.toolbox._tool_panel["tid"] assert len(section.elems) == 1 assert section.elems.values()[0].id == "github.com/galaxyproject/example/test_tool/0.2" @@ -193,7 +193,7 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): self.__verify_two_test_tools( ) # Assert tools merged in tool panel. - assert len( self.toolbox.tool_panel ) == 1 + assert len( self.toolbox._tool_panel ) == 1 def test_update_shed_conf(self): self.__setup_shed_tool_conf() @@ -228,8 +228,8 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): stored_workflow = self.__test_workflow() encoded_id = self.app.security.encode_id( stored_workflow.id ) self._add_config( """""" % encoded_id ) - assert len( self.toolbox.tool_panel ) == 1 - panel_workflow = self.toolbox.tool_panel.values()[ 0 ] + assert len( self.toolbox._tool_panel ) == 1 + panel_workflow = self.toolbox._tool_panel.values()[ 0 ] assert panel_workflow == stored_workflow.latest_workflow # TODO: test to_dict with workflows @@ -237,21 +237,21 @@ class ToolBoxTestCase( BaseToolBoxTestCase ): stored_workflow = self.__test_workflow() encoded_id = self.app.security.encode_id( stored_workflow.id ) self._add_config( """
""" % encoded_id ) - assert len( self.toolbox.tool_panel ) == 1 - section = self.toolbox.tool_panel[ 'tid' ] + assert len( self.toolbox._tool_panel ) == 1 + section = self.toolbox._tool_panel[ 'tid' ] assert len( section.elems ) == 1 panel_workflow = section.elems.values()[ 0 ] assert panel_workflow == stored_workflow.latest_workflow def test_label_in_panel( self ): self._add_config( """""" ) - assert len( self.toolbox.tool_panel ) == 2 - self.__check_test_labels( self.toolbox.tool_panel ) + assert len( self.toolbox._tool_panel ) == 2 + self.__check_test_labels( self.toolbox._tool_panel ) def test_label_in_section( self ): self._add_config( """
""" ) - assert len( self.toolbox.tool_panel ) == 1 - section = self.toolbox.tool_panel[ 'tid' ] + assert len( self.toolbox._tool_panel ) == 1 + section = self.toolbox._tool_panel[ 'tid' ] self.__check_test_labels( section.elems ) def __check_test_labels( self, panel_dict ): From e202fc1009a1f15216e868e0fdf07db85c1d83ce Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 12:50:27 -0500 Subject: [PATCH 088/473] Hack to fix bug introduced in 806202d. I may be trying to juggle to many competing branches working on same component. --- lib/galaxy/tools/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 16905e18dbd..3ef117aa950 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1204,6 +1204,8 @@ class ToolBox( object, Dictifiable ): """ if tool_lineage is None: assert tool is not None + if not hasattr( tool, "lineage" ): + return None tool_lineage = tool.lineage lineage_ids = tool_lineage.get_version_ids( reverse=True ) for lineage_id in lineage_ids: From 99963f38c1ef3e8924530b36cc2805f38859fdf5 Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Mon, 29 Dec 2014 14:08:35 -0500 Subject: [PATCH 089/473] Add classic hidden_data field to tool form --- client/galaxy/scripts/mvc/tools/tools-section.js | 5 +++++ static/scripts/mvc/tools/tools-section.js | 5 +++++ static/scripts/packed/mvc/tools/tools-section.js | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/galaxy/scripts/mvc/tools/tools-section.js b/client/galaxy/scripts/mvc/tools/tools-section.js index 744526ec666..fab21d2a8f7 100644 --- a/client/galaxy/scripts/mvc/tools/tools-section.js +++ b/client/galaxy/scripts/mvc/tools/tools-section.js @@ -316,6 +316,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep field = this._fieldHidden(input_def); break; + // hidden field + case 'hidden_data': + field = this._fieldHidden(input_def); + break; + // integer field case 'integer': field = this._fieldSlider(input_def); diff --git a/static/scripts/mvc/tools/tools-section.js b/static/scripts/mvc/tools/tools-section.js index 744526ec666..fab21d2a8f7 100644 --- a/static/scripts/mvc/tools/tools-section.js +++ b/static/scripts/mvc/tools/tools-section.js @@ -316,6 +316,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep field = this._fieldHidden(input_def); break; + // hidden field + case 'hidden_data': + field = this._fieldHidden(input_def); + break; + // integer field case 'integer': field = this._fieldSlider(input_def); diff --git a/static/scripts/packed/mvc/tools/tools-section.js b/static/scripts/packed/mvc/tools/tools-section.js index 23a85c25153..e168486d9ff 100644 --- a/static/scripts/packed/mvc/tools/tools-section.js +++ b/static/scripts/packed/mvc/tools/tools-section.js @@ -1 +1 @@ -define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/tools/tools-repeat","mvc/tools/tools-select-content","mvc/tools/tools-input"],function(d,b,g,c,a,e){var f=Backbone.View.extend({initialize:function(i,h){this.app=i;this.inputs=h.inputs;h.cls_tr="section-row";this.table=new b.View(h);this.setElement(this.table.$el);this.render()},render:function(){this.table.delAll();for(var h in this.inputs){this._add(this.inputs[h])}},_add:function(j){var i=this;var h=jQuery.extend(true,{},j);h.id=j.id=d.uuid();this.app.input_list[h.id]=h;var k=h.type;switch(k){case"conditional":this._addConditional(h);break;case"repeat":this._addRepeat(h);break;default:this._addRow(h)}},_addConditional:function(h){var j=this;h.test_param.id=h.id;var m=this._addRow(h.test_param);m.options.onchange=function(t){var p=j.app.tree.matchCase(h,t);for(var r in h.cases){var w=h.cases[r];var u=h.id+"-section-"+r;var o=j.table.get(u);var v=false;for(var q in w.inputs){var s=w.inputs[q].type;if(s&&s!=="hidden"){v=true;break}}if(r==p&&v){o.fadeIn("fast")}else{o.hide()}}j.app.trigger("refresh")};for(var l in h.cases){var k=h.id+"-section-"+l;var n=new f(this.app,{inputs:h.cases[l].inputs,cls:"ui-table-plain"});n.$el.addClass("ui-table-form-section");this.table.add(n.$el);this.table.append(k)}m.trigger("change")},_addRepeat:function(o){var r=this;var p=0;function m(i,t){var s=o.id+"-section-"+(p++);var u=null;if(t){u=function(){k.del(s);k.retitle(o.title);r.app.trigger("refresh")}}var v=new f(r.app,{inputs:i,cls:"ui-table-plain"});k.add({id:s,title:o.title,$el:v.$el,ondel:u});k.retitle(o.title)}var k=new c.View({title_new:o.title,max:o.max,onnew:function(){m(o.inputs,true);r.app.trigger("refresh")}});var h=o.min;var q=_.size(o.cache);for(var l=0;l=h)}var j=new e(this.app,{label:o.title,help:o.help,field:k});j.$el.addClass("ui-table-form-section");this.table.add(j.$el);this.table.append(o.id)},_addRow:function(h){var k=h.id;var i=this._createField(h);this.app.field_list[k]=i;var j=new e(this.app,{label:h.label,optional:h.optional,help:h.help,field:i});this.app.element_list[k]=j;this.table.add(j.$el);this.table.append(k);return i},_createField:function(h){var i=null;switch(h.type){case"text":i=this._fieldText(h);break;case"select":i=this._fieldSelect(h);break;case"data":i=this._fieldData(h);break;case"data_collection":i=this._fieldData(h);break;case"data_column":h.error_text="Missing columns in referenced dataset.";i=this._fieldSelect(h);break;case"hidden":i=this._fieldHidden(h);break;case"integer":i=this._fieldSlider(h);break;case"float":i=this._fieldSlider(h);break;case"boolean":i=this._fieldBoolean(h);break;case"genomebuild":h.searchable=true;i=this._fieldSelect(h);break;case"drill_down":i=this._fieldDrilldown(h);break;case"baseurl":i=this._fieldHidden(h);break;default:this.app.incompatible=true;if(h.options){i=this._fieldSelect(h)}else{i=this._fieldText(h)}console.debug("tools-form::_addRow() : Auto matched field type ("+h.type+").")}if(h.value!==undefined){i.value(h.value)}return i},_fieldData:function(h){var i=this;return new a.View(this.app,{id:"field-"+h.id,extensions:h.extensions,multiple:h.multiple,type:h.type,data:h.options,onchange:function(){i.app.trigger("refresh")}})},_fieldSelect:function(h){var k=[];for(var l in h.options){var m=h.options[l];k.push({label:m[0],value:m[1]})}var n=g.Select;switch(h.display){case"checkboxes":n=g.Checkbox;break;case"radio":n=g.Radio;break}var j=this;return new n.View({id:"field-"+h.id,data:k,error_text:h.error_text||"No options available",multiple:h.multiple,searchable:h.searchable,onchange:function(){j.app.trigger("refresh")}})},_fieldDrilldown:function(h){var i=this;return new g.Drilldown.View({id:"field-"+h.id,data:h.options,display:h.display,onchange:function(){i.app.trigger("refresh")}})},_fieldText:function(h){var i=this;return new g.Input({id:"field-"+h.id,area:h.area,onchange:function(){i.app.trigger("refresh")}})},_fieldSlider:function(h){return new g.Slider.View({id:"field-"+h.id,precise:h.type=="float",min:h.min,max:h.max})},_fieldHidden:function(h){return new g.Hidden({id:"field-"+h.id})},_fieldBoolean:function(h){return new g.RadioButton.View({id:"field-"+h.id,data:[{label:"Yes",value:"true"},{label:"No",value:"false"}]})}});return{View:f}}); \ No newline at end of file +define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/tools/tools-repeat","mvc/tools/tools-select-content","mvc/tools/tools-input"],function(d,b,g,c,a,e){var f=Backbone.View.extend({initialize:function(i,h){this.app=i;this.inputs=h.inputs;h.cls_tr="section-row";this.table=new b.View(h);this.setElement(this.table.$el);this.render()},render:function(){this.table.delAll();for(var h in this.inputs){this._add(this.inputs[h])}},_add:function(j){var i=this;var h=jQuery.extend(true,{},j);h.id=j.id=d.uuid();this.app.input_list[h.id]=h;var k=h.type;switch(k){case"conditional":this._addConditional(h);break;case"repeat":this._addRepeat(h);break;default:this._addRow(h)}},_addConditional:function(h){var j=this;h.test_param.id=h.id;var m=this._addRow(h.test_param);m.options.onchange=function(t){var p=j.app.tree.matchCase(h,t);for(var r in h.cases){var w=h.cases[r];var u=h.id+"-section-"+r;var o=j.table.get(u);var v=false;for(var q in w.inputs){var s=w.inputs[q].type;if(s&&s!=="hidden"){v=true;break}}if(r==p&&v){o.fadeIn("fast")}else{o.hide()}}j.app.trigger("refresh")};for(var l in h.cases){var k=h.id+"-section-"+l;var n=new f(this.app,{inputs:h.cases[l].inputs,cls:"ui-table-plain"});n.$el.addClass("ui-table-form-section");this.table.add(n.$el);this.table.append(k)}m.trigger("change")},_addRepeat:function(o){var r=this;var p=0;function m(i,t){var s=o.id+"-section-"+(p++);var u=null;if(t){u=function(){k.del(s);k.retitle(o.title);r.app.trigger("refresh")}}var v=new f(r.app,{inputs:i,cls:"ui-table-plain"});k.add({id:s,title:o.title,$el:v.$el,ondel:u});k.retitle(o.title)}var k=new c.View({title_new:o.title,max:o.max,onnew:function(){m(o.inputs,true);r.app.trigger("refresh")}});var h=o.min;var q=_.size(o.cache);for(var l=0;l=h)}var j=new e(this.app,{label:o.title,help:o.help,field:k});j.$el.addClass("ui-table-form-section");this.table.add(j.$el);this.table.append(o.id)},_addRow:function(h){var k=h.id;var i=this._createField(h);this.app.field_list[k]=i;var j=new e(this.app,{label:h.label,optional:h.optional,help:h.help,field:i});this.app.element_list[k]=j;this.table.add(j.$el);this.table.append(k);return i},_createField:function(h){var i=null;switch(h.type){case"text":i=this._fieldText(h);break;case"select":i=this._fieldSelect(h);break;case"data":i=this._fieldData(h);break;case"data_collection":i=this._fieldData(h);break;case"data_column":h.error_text="Missing columns in referenced dataset.";i=this._fieldSelect(h);break;case"hidden":i=this._fieldHidden(h);break;case"hidden_data":i=this._fieldHidden(h);break;case"integer":i=this._fieldSlider(h);break;case"float":i=this._fieldSlider(h);break;case"boolean":i=this._fieldBoolean(h);break;case"genomebuild":h.searchable=true;i=this._fieldSelect(h);break;case"drill_down":i=this._fieldDrilldown(h);break;case"baseurl":i=this._fieldHidden(h);break;default:this.app.incompatible=true;if(h.options){i=this._fieldSelect(h)}else{i=this._fieldText(h)}console.debug("tools-form::_addRow() : Auto matched field type ("+h.type+").")}if(h.value!==undefined){i.value(h.value)}return i},_fieldData:function(h){var i=this;return new a.View(this.app,{id:"field-"+h.id,extensions:h.extensions,multiple:h.multiple,type:h.type,data:h.options,onchange:function(){i.app.trigger("refresh")}})},_fieldSelect:function(h){var k=[];for(var l in h.options){var m=h.options[l];k.push({label:m[0],value:m[1]})}var n=g.Select;switch(h.display){case"checkboxes":n=g.Checkbox;break;case"radio":n=g.Radio;break}var j=this;return new n.View({id:"field-"+h.id,data:k,error_text:h.error_text||"No options available",multiple:h.multiple,searchable:h.searchable,onchange:function(){j.app.trigger("refresh")}})},_fieldDrilldown:function(h){var i=this;return new g.Drilldown.View({id:"field-"+h.id,data:h.options,display:h.display,onchange:function(){i.app.trigger("refresh")}})},_fieldText:function(h){var i=this;return new g.Input({id:"field-"+h.id,area:h.area,onchange:function(){i.app.trigger("refresh")}})},_fieldSlider:function(h){return new g.Slider.View({id:"field-"+h.id,precise:h.type=="float",min:h.min,max:h.max})},_fieldHidden:function(h){return new g.Hidden({id:"field-"+h.id})},_fieldBoolean:function(h){return new g.RadioButton.View({id:"field-"+h.id,data:[{label:"Yes",value:"true"},{label:"No",value:"false"}]})}});return{View:f}}); \ No newline at end of file From 12bcd0ec331e916703a1e34f43fabd4e2b2967ca Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 14:25:12 -0500 Subject: [PATCH 090/473] Bugfix for 267fa4b when tool tags are enabled. Thanks to JJ for the bug report - apparently at least one person has tool tags enabled :). --- lib/galaxy/tools/toolbox/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tools/toolbox/tags.py b/lib/galaxy/tools/toolbox/tags.py index a8aa6c53360..3e8c5107e66 100644 --- a/lib/galaxy/tools/toolbox/tags.py +++ b/lib/galaxy/tools/toolbox/tags.py @@ -44,7 +44,7 @@ class PersistentToolTagManager( AbstractToolTagManager ): def __init__( self, app ): self.app = app - self.sa_session = app.sa_session + self.sa_session = app.model.context def reset_tags( self ): log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" ) From 11b75eb938a3a579e9cad407da8faf066921bade Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 23:07:01 -0500 Subject: [PATCH 091/473] Override ToolSectionLabel.to_dict so don't need to_dict_helper. I feel this is cleaner and it makes ToolSection and ToolSectionLabel easier to move into a new module. --- lib/galaxy/tools/__init__.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 3ef117aa950..5d7f69f340d 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -113,16 +113,6 @@ class ToolNotFoundException( Exception ): pass -def to_dict_helper( obj, kwargs ): - """ Helper function that provides the appropriate kwargs to to_dict an object. """ - - # Label.to_dict cannot have kwargs. - if isinstance( obj, ToolSectionLabel ): - kwargs = {} - - return obj.to_dict( **kwargs ) - - class ToolBox( object, Dictifiable ): """Container for a collection of tools""" @@ -1189,7 +1179,7 @@ class ToolBox( object, Dictifiable ): link_details=True ) for elt in panel_elts: - rval.append( to_dict_helper( elt, kwargs ) ) + rval.append( elt.to_dict( **kwargs ) ) else: tools = [] for id, tool in self._tools_by_id.items(): @@ -1311,7 +1301,7 @@ class ToolSection( object, Dictifiable ): link_details=link_details ) for elt in self.elems.values(): - section_elts.append( to_dict_helper( elt, kwargs ) ) + section_elts.append( elt.to_dict( **kwargs ) ) section_dict[ 'elems' ] = section_elts return section_dict @@ -1333,6 +1323,9 @@ class ToolSectionLabel( object, Dictifiable ): self.id = elem.get( "id" ) self.version = elem.get( "version" ) or '' + def to_dict( self, **kwds ): + return super( ToolSectionLabel, self ).to_dict() + class DefaultToolState( object ): """ From aad51263f01eb1872f270edee10b24d14b82bea5 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 23:07:01 -0500 Subject: [PATCH 092/473] Various ToolBox filtering improvements and tests. - Allow overriding the base module location for ToolBox filters (needed for OS packages, etc...). - Allow scanning multiple base modules for filters. - Unit tests for module loading functionality, custom tool, label, and section filters, default hidden and require_login filters. --- lib/galaxy/config.py | 1 + lib/galaxy/tools/filters/__init__.py | 24 +++++-- test/unit/tools/filter_modules/__init__.py | 0 test/unit/tools/filter_modules/filtermod.py | 22 ++++++ test/unit/tools/test_toolbox_filters.py | 79 +++++++++++++++++++++ 5 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 test/unit/tools/filter_modules/__init__.py create mode 100644 test/unit/tools/filter_modules/filtermod.py create mode 100644 test/unit/tools/test_toolbox_filters.py diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index f93c2217419..e9fcbf2c4dd 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -83,6 +83,7 @@ class Configuration( object ): # The value of migrated_tools_config is the file reserved for containing only those tools that have been eliminated from the distribution # and moved to the tool shed. self.integrated_tool_panel_config = resolve_path( kwargs.get( 'integrated_tool_panel_config', 'integrated_tool_panel.xml' ), self.root ) + self.toolbox_filter_base_modules = listify( kwargs.get( "toolbox_filter_base_modules", "galaxy.tools.filters" ) ) self.tool_filters = listify( kwargs.get( "tool_filters", [] ), do_strip=True ) self.tool_label_filters = listify( kwargs.get( "tool_label_filters", [] ), do_strip=True ) self.tool_section_filters = listify( kwargs.get( "tool_section_filters", [] ), do_strip=True ) diff --git a/lib/galaxy/tools/filters/__init__.py b/lib/galaxy/tools/filters/__init__.py index 04d72ff1e38..c7402d176f0 100644 --- a/lib/galaxy/tools/filters/__init__.py +++ b/lib/galaxy/tools/filters/__init__.py @@ -1,6 +1,8 @@ -import logging -from galaxy.util import listify from copy import deepcopy +import sys +import logging + +from galaxy.util import listify log = logging.getLogger( __name__ ) @@ -20,6 +22,7 @@ class FilterFactory( object ): self.default_filters = dict( tool=[ _not_hidden, _handle_requires_login ], section=[], label=[] ) # Add dynamic filters to these default filters. config = toolbox.app.config + self.__base_modules = listify( getattr( config, "toolbox_filter_base_modules", "galaxy.tools.filters" ) ) self.__init_filters( "tool", getattr( config, "tool_filters", "" ), self.default_filters ) self.__init_filters( "section", getattr( config, "tool_section_filters", "" ), self.default_filters ) self.__init_filters( "label", getattr( config, "tool_label_filters", "" ), self.default_filters ) @@ -65,14 +68,27 @@ class FilterFactory( object ): if ":" in filter_name: # Should be a submodule of filters (e.g. examples:restrict_development_tools) (module_name, function_name) = filter_name.rsplit(":", 1) - module = __import__( module_name.strip(), globals() ) - function = getattr( module, function_name.strip() ) + function = self._import_filter( module_name, function_name ) else: # No module found, just load a function from this file or # one that has be explicitly imported. function = getattr( globals(), filter_name.strip() ) return function + def _import_filter( self, module_name, function_name ): + function_name = function_name.strip() + for base_module in self.__base_modules: + full_module_name = "%s.%s" % ( base_module, module_name.strip() ) + try: + __import__( full_module_name ) + except ImportError: + # log.debug("Failed to load module.", exc_info=True) + continue + module = sys.modules[ full_module_name ] + if hasattr( module, function_name ): + return getattr( module, function_name ) + raise Exception("Failed to find filter %s.%s" % (module_name, function_name)) + ## Stock Filter Functions def _not_hidden( context, tool ): diff --git a/test/unit/tools/filter_modules/__init__.py b/test/unit/tools/filter_modules/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/unit/tools/filter_modules/filtermod.py b/test/unit/tools/filter_modules/filtermod.py new file mode 100644 index 00000000000..64adfd451b0 --- /dev/null +++ b/test/unit/tools/filter_modules/filtermod.py @@ -0,0 +1,22 @@ +""" Test filters used by test_toolbox_filters.py. +""" + + +def filter_tool( context, tool ): + """Test Filter Tool""" + return False + + +def filter_section( context, section ): + """Test Filter Section""" + return False + + +def filter_label_1( context, label ): + """Test Filter Label 1""" + return False + + +def filter_label_2( context, label ): + """Test Filter Label 2""" + return False diff --git a/test/unit/tools/test_toolbox_filters.py b/test/unit/tools/test_toolbox_filters.py new file mode 100644 index 00000000000..482b59d91f0 --- /dev/null +++ b/test/unit/tools/test_toolbox_filters.py @@ -0,0 +1,79 @@ +from galaxy.util.bunch import Bunch + +from galaxy.tools.filters import FilterFactory + + +def test_stock_filtering_requires_login_tools( ): + anonymous_user_trans = mock_trans( has_user=False ) + filters = filter_factory( {} ).build_filters( mock_trans() )[ 'tool' ] + assert not is_filtered( filters, anonymous_user_trans, mock_tool( require_login=False ) ) + assert is_filtered( filters, anonymous_user_trans, mock_tool( require_login=True ) ) + + logged_in_trans = mock_trans( has_user=True ) + filters = filter_factory( {} ).build_filters( logged_in_trans )[ 'tool' ] + assert not is_filtered( filters, logged_in_trans, mock_tool( require_login=True ) ) + + +def test_stock_filtering_hidden_tools( ): + filters = filter_factory( {} ).build_filters( mock_trans() )[ 'tool' ] + assert not is_filtered( filters, mock_trans(), mock_tool( hidden=False ) ) + assert is_filtered( filters, mock_trans(), mock_tool( hidden=True ) ) + + +def test_trackster_filtering( ): + filters = filter_factory( {} ).build_filters( mock_trans(), trackster=True )[ 'tool' ] + # Ekkk... is trackster_conf broken? Why is it showing up. + # assert is_filtered( filters, mock_trans(), mock_tool( trackster_conf=False ) ) + assert not is_filtered( filters, mock_trans(), mock_tool( trackster_conf=True ) ) + + +def test_custom_filters(): + filters = filter_factory().build_filters( mock_trans() ) + tool_filters = filters[ "tool" ] + # TODO: the fact that -1 is the custom filter is an implementation + # detail that should not be tested here. + assert tool_filters[ -1 ].__doc__ == "Test Filter Tool" + + section_filters = filters[ "section" ] + assert section_filters[ 0 ].__doc__ == "Test Filter Section" + + label_filters = filters[ "label" ] + assert label_filters[ 0 ].__doc__ == "Test Filter Label 1" + assert label_filters[ 1 ].__doc__ == "Test Filter Label 2" + + +def filter_factory(config_dict=None): + if config_dict is None: + config_dict = dict( + tool_filters=["filtermod:filter_tool"], + tool_section_filters=["filtermod:filter_section"], + tool_label_filters=["filtermod:filter_label_1", "filtermod:filter_label_2"], + ) + config = Bunch( **config_dict ) + config.toolbox_filter_base_modules = "galaxy.tools.filters,tools.filter_modules" + app = Bunch(config=config) + toolbox = Bunch(app=app) + return FilterFactory(toolbox) + + +def is_filtered( filters, trans, tool ): + context = Bunch( trans=trans ) + return not all( map( lambda filter: filter( context, tool ), filters ) ) + + +def mock_tool( require_login=False, hidden=False, trackster_conf=False ): + tool = Bunch( + require_login=require_login, + hidden=hidden, + trackster_conf=trackster_conf, + ) + return tool + + +def mock_trans( has_user=True ): + trans = Bunch( ) + if has_user: + trans.user = Bunch(preferences={}) + else: + trans.user = None + return trans From 66625dfc699b2ad405578b4d9cf62f8d765b0a1c Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 23:07:01 -0500 Subject: [PATCH 093/473] Move galaxy.tools.watcher into galaxy.tools.toolbox.watcher. --- lib/galaxy/tools/__init__.py | 2 +- lib/galaxy/tools/{ => toolbox}/watcher.py | 0 test/unit/tools/test_watcher.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/galaxy/tools/{ => toolbox}/watcher.py (100%) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 5d7f69f340d..747a8beb5c9 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -32,7 +32,7 @@ from sqlalchemy import and_ from galaxy import jobs, model from galaxy.datatypes.metadata import JobExternalOutputMetadataWrapper from galaxy import exceptions -from galaxy.tools import watcher +from galaxy.tools.toolbox import watcher from galaxy.tools.actions import DefaultToolAction from galaxy.tools.actions.upload import UploadToolAction from galaxy.tools.actions.data_source import DataSourceToolAction diff --git a/lib/galaxy/tools/watcher.py b/lib/galaxy/tools/toolbox/watcher.py similarity index 100% rename from lib/galaxy/tools/watcher.py rename to lib/galaxy/tools/toolbox/watcher.py diff --git a/test/unit/tools/test_watcher.py b/test/unit/tools/test_watcher.py index 171c75df253..3a67ec1dfa4 100644 --- a/test/unit/tools/test_watcher.py +++ b/test/unit/tools/test_watcher.py @@ -4,7 +4,7 @@ from shutil import rmtree import tempfile import time -from galaxy.tools import watcher +from galaxy.tools.toolbox import watcher from galaxy.util import bunch From f14368db137de3a206203a4008beae9ef09f3ddf Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 23:07:01 -0500 Subject: [PATCH 094/473] Remove lib/galaxy/tools/filters/examples.py likely incorrectly added in d816d66. --- lib/galaxy/tools/filters/examples.py | 115 --------------------------- 1 file changed, 115 deletions(-) delete mode 100644 lib/galaxy/tools/filters/examples.py diff --git a/lib/galaxy/tools/filters/examples.py b/lib/galaxy/tools/filters/examples.py deleted file mode 100644 index 607f493faac..00000000000 --- a/lib/galaxy/tools/filters/examples.py +++ /dev/null @@ -1,115 +0,0 @@ -import logging -log = logging.getLogger( __name__ ) - - -def restrict_encode( content, tool ): - """ - Disable the random interval ENCODE tool - - This tool filter will disable all the ENCODE tool when enabled. - """ - if tool.id == 'random_intervals1': - return False - return True - - -def restrict_text( content, section ): - """ - Disable Text sections - - This tool filter will disable all Tools groups under a 'Text' section when enabled. - """ - if section.name.find('Text') != -1: - return False - return True - - -def restrict_upload_to_admins( context, tool ): - """ - Disable Upload tool for all non-admin users. - - This tool filter will hide the upload tool from all users except admin - users. - - This can be enabled by renaming this file to examples.py and adding - the following to the ``app:main`` section of ``galaxy.ini``: - - tool_filters = examples:restrict_upload_to_admins - """ - if tool.name == "Upload File": - return context.trans.user_is_admin() - return True - - -def disable_gatk( context, tool ): - """ - This tool filter will disable all gatk tools when enabled. - - This can be enabled by renaming this file to examples.py and adding the following to the - ``app:main`` section of ``galaxy.ini``: - - tool_filters = examples:disable_gatk - """ - return not any( [ requirement.name == "gatk" for requirement in tool.requirements ] ) - - -def explicit_user_mapping( context, section ): - """ - This tool section filter uses an explicit mapping to describe what users can view - which tool sections. Anonymous users will only be able to view the "Get Data" - tool section (with id getext). This can be enabled by renaming this file to - examples.py and adding the following to the ``app:main`` section of - ``galaxy.ini``: - - tool_section_filters = examples:explicit_user_mapping - """ - users_sections = { - None: [ "getext" ], - "bob@example.com": [ "getext", "textutil", "filter" ], - "mary@example.com": [ "getext", "textutil", "filter", "ngs" ], - } - user = context.trans.user - email = user and user.email - valid_sections = users_sections.get( email, [] ) - return section.id in valid_sections - - -DEVELOPERS = [ "mary@example.com" ] - - -def restrict_development_tools( context, tool ): - """ - This tool filter will disable all tools with the string alpha appearing in - the version for all users except those explicitly appearing in the DEVELOPERS list - defined above. This can be enabled by renaming this file to examples.py and - adding the following to the ``app:main`` section of ``galaxy.ini``: - - tool_filters = examples:restrict_development_tools - """ - version = tool.version - user = context.trans.user - email = user and user.email - return "alpha" not in version or email in DEVELOPERS - - -def per_host_tool_sections( context, section ): - """ - This tool section filter results in different sections being display based on - the URL the user is making the request to. This could allow a single Galaxy instance - to seem like several different instances hosting different tools based on the URL used - to access the Galxy. This can be enabled by renaming this file to examples.py and adding - the following to the ``app:main`` section of ``galaxy.ini``: - - tool_section_filters = examples:per_host_tool_sections - """ - host = context.trans.request.host - # Core tools used by all virtual hosts. - valid_sections = [ "getext", "textutil", "filter" ] - if "ngs.galaxy.example.com" in host: - valid_sections += [ "ngs" ] - elif "microarray.galaxy.example.com" in host: - valid_sections += [ "microarray" ] - elif "proteomics.galaxy.example.com" in host: - valid_sections += [ "proteomics" ] - return section.id in valid_sections - From 9b5f374189e2971a840bb3fff58bd58eb884cc55 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 29 Dec 2014 23:07:01 -0500 Subject: [PATCH 095/473] Move galaxy.tools.filters to galaxy.tools.toolbox.filters. Leave empty module in galaxy.tools.filters and modify config to ensure backward compatibility (filters in this old directory will continue to work for now). Update config/galaxy.ini.sample with more discussion of ToolBox filter. --- config/galaxy.ini.sample | 24 ++-- lib/galaxy/config.py | 2 +- lib/galaxy/tools/__init__.py | 2 +- lib/galaxy/tools/filters/__init__.py | 106 +----------------- lib/galaxy/tools/toolbox/filters/__init__.py | 103 +++++++++++++++++ .../{ => toolbox}/filters/examples.py.sample | 0 test/unit/tools/test_toolbox_filters.py | 2 +- 7 files changed, 127 insertions(+), 112 deletions(-) create mode 100644 lib/galaxy/tools/toolbox/filters/__init__.py rename lib/galaxy/tools/{ => toolbox}/filters/examples.py.sample (100%) diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index d25670832b4..be97c7f3715 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -910,17 +910,27 @@ use_interactive = True #cache_user_job_count = False # ToolBox filtering -# Modules from lib/galaxy/tools/filters/ can be specified in the following lines. -# tool_* filters will be applied for all users and can not be changed by them. -# user_tool_* filters will be shown under user preferences and can be toogled -# on and off by runtime -#tool_filters -#tool_label_filters -#tool_section_filters + +# Modules from lib/galaxy/tools/toolbox/filters/ can be specified in +# the following lines. tool_* filters will be applied for all users +# and can not be changed by them. user_tool_* filters will be shown +# under user preferences and can be toogled on and off by +# runtime. Example shown below are not real defaults (no custom +# filters applied by defualt) but can be enabled with by renaming the +# example.py.sample in the filters directory to example.py. + +#tool_filters = +#tool_label_filters = +#tool_section_filters = #user_tool_filters = examples:restrict_upload_to_admins, examples:restrict_encode #user_tool_section_filters = examples:restrict_text #user_tool_label_filters = examples:restrict_upload_to_admins, examples:restrict_encode +# The base modules that are searched for modules as described above +# can be modified and modules external to Galaxy can be searched by +# modifying the following option. +#toolbox_filter_base_modules = galaxy.tools.toolbox.filters,galaxy.tools.filters + # Galaxy Application Internal Message Queue # Galaxy uses AMQP internally TODO more documentation on what for. diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index e9fcbf2c4dd..1d134f194fa 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -83,7 +83,7 @@ class Configuration( object ): # The value of migrated_tools_config is the file reserved for containing only those tools that have been eliminated from the distribution # and moved to the tool shed. self.integrated_tool_panel_config = resolve_path( kwargs.get( 'integrated_tool_panel_config', 'integrated_tool_panel.xml' ), self.root ) - self.toolbox_filter_base_modules = listify( kwargs.get( "toolbox_filter_base_modules", "galaxy.tools.filters" ) ) + self.toolbox_filter_base_modules = listify( kwargs.get( "toolbox_filter_base_modules", "galaxy.tools.filters,galaxy.tools.toolbox.filters" ) ) self.tool_filters = listify( kwargs.get( "tool_filters", [] ), do_strip=True ) self.tool_label_filters = listify( kwargs.get( "tool_label_filters", [] ), do_strip=True ) self.tool_section_filters = listify( kwargs.get( "tool_section_filters", [] ), do_strip=True ) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 747a8beb5c9..45fca94588f 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -48,7 +48,7 @@ from galaxy.tools.parameters.grouping import Conditional, ConditionalWhen, Repea from galaxy.tools.parameters.input_translation import ToolInputTranslator from galaxy.tools.parameters.output import ToolOutputActionGroup from galaxy.tools.parameters.validation import LateValidationError -from galaxy.tools.filters import FilterFactory +from galaxy.tools.toolbox.filters import FilterFactory from galaxy.tools.test import parse_tests from galaxy.tools.parser import get_tool_source from galaxy.tools.parser.xml import XmlPageSource diff --git a/lib/galaxy/tools/filters/__init__.py b/lib/galaxy/tools/filters/__init__.py index c7402d176f0..c984816ff30 100644 --- a/lib/galaxy/tools/filters/__init__.py +++ b/lib/galaxy/tools/filters/__init__.py @@ -1,103 +1,5 @@ -from copy import deepcopy -import sys -import logging +"""Deprecated module for toolbox filters. -from galaxy.util import listify - -log = logging.getLogger( __name__ ) - - -class FilterFactory( object ): - """ - An instance of this class is responsible for filtering the list - of tools presented to a given user in a given context. - """ - - def __init__( self, toolbox ): - self.toolbox = toolbox - - # Prepopulate dict containing filters that are always checked, - # other filters that get checked depending on context (e.g. coming from - # trackster or no user found are added in build filters). - self.default_filters = dict( tool=[ _not_hidden, _handle_requires_login ], section=[], label=[] ) - # Add dynamic filters to these default filters. - config = toolbox.app.config - self.__base_modules = listify( getattr( config, "toolbox_filter_base_modules", "galaxy.tools.filters" ) ) - self.__init_filters( "tool", getattr( config, "tool_filters", "" ), self.default_filters ) - self.__init_filters( "section", getattr( config, "tool_section_filters", "" ), self.default_filters ) - self.__init_filters( "label", getattr( config, "tool_label_filters", "" ), self.default_filters ) - - def build_filters( self, trans, **kwds ): - """ - Build list of filters to check tools against given current context. - """ - filters = deepcopy( self.default_filters ) - if trans.user: - for name, value in trans.user.preferences.items(): - if value.strip(): - user_filters = listify( value, do_strip=True ) - category = '' - if name == 'toolbox_tool_filters': - category = "tool" - elif name == 'toolbox_section_filters': - category = "section" - elif name == 'toolbox_label_filters': - category = "label" - if category: - validate = getattr( trans.app.config, 'user_%s_filters' % category, [] ) - self.__init_filters( category, user_filters, filters, validate=validate ) - else: - if kwds.get( "trackster", False ): - filters[ "tool" ].append( _has_trackster_conf ) - - return filters - - def __init_filters( self, key, filters, toolbox_filters, validate=None ): - for filter in filters: - if validate is None or filter in validate or filter in self.default_filters: - filter_function = self.__build_filter_function( filter ) - toolbox_filters[ key ].append( filter_function ) - else: - log.warning( "Refusing to load %s filter '%s' which is not defined in config", key, filter ) - return toolbox_filters - - def __build_filter_function( self, filter_name ): - """Obtain python function (importing a submodule if needed) - corresponding to filter_name. - """ - if ":" in filter_name: - # Should be a submodule of filters (e.g. examples:restrict_development_tools) - (module_name, function_name) = filter_name.rsplit(":", 1) - function = self._import_filter( module_name, function_name ) - else: - # No module found, just load a function from this file or - # one that has be explicitly imported. - function = getattr( globals(), filter_name.strip() ) - return function - - def _import_filter( self, module_name, function_name ): - function_name = function_name.strip() - for base_module in self.__base_modules: - full_module_name = "%s.%s" % ( base_module, module_name.strip() ) - try: - __import__( full_module_name ) - except ImportError: - # log.debug("Failed to load module.", exc_info=True) - continue - module = sys.modules[ full_module_name ] - if hasattr( module, function_name ): - return getattr( module, function_name ) - raise Exception("Failed to find filter %s.%s" % (module_name, function_name)) - - -## Stock Filter Functions -def _not_hidden( context, tool ): - return not tool.hidden - - -def _handle_requires_login( context, tool ): - return not tool.require_login or context.trans.user - - -def _has_trackster_conf( context, tool ): - return tool.trackster_conf +Filters placed in this module will still work - but filters should be +moved to lib/galaxy/tools/toolbox/filters. +""" diff --git a/lib/galaxy/tools/toolbox/filters/__init__.py b/lib/galaxy/tools/toolbox/filters/__init__.py new file mode 100644 index 00000000000..c7402d176f0 --- /dev/null +++ b/lib/galaxy/tools/toolbox/filters/__init__.py @@ -0,0 +1,103 @@ +from copy import deepcopy +import sys +import logging + +from galaxy.util import listify + +log = logging.getLogger( __name__ ) + + +class FilterFactory( object ): + """ + An instance of this class is responsible for filtering the list + of tools presented to a given user in a given context. + """ + + def __init__( self, toolbox ): + self.toolbox = toolbox + + # Prepopulate dict containing filters that are always checked, + # other filters that get checked depending on context (e.g. coming from + # trackster or no user found are added in build filters). + self.default_filters = dict( tool=[ _not_hidden, _handle_requires_login ], section=[], label=[] ) + # Add dynamic filters to these default filters. + config = toolbox.app.config + self.__base_modules = listify( getattr( config, "toolbox_filter_base_modules", "galaxy.tools.filters" ) ) + self.__init_filters( "tool", getattr( config, "tool_filters", "" ), self.default_filters ) + self.__init_filters( "section", getattr( config, "tool_section_filters", "" ), self.default_filters ) + self.__init_filters( "label", getattr( config, "tool_label_filters", "" ), self.default_filters ) + + def build_filters( self, trans, **kwds ): + """ + Build list of filters to check tools against given current context. + """ + filters = deepcopy( self.default_filters ) + if trans.user: + for name, value in trans.user.preferences.items(): + if value.strip(): + user_filters = listify( value, do_strip=True ) + category = '' + if name == 'toolbox_tool_filters': + category = "tool" + elif name == 'toolbox_section_filters': + category = "section" + elif name == 'toolbox_label_filters': + category = "label" + if category: + validate = getattr( trans.app.config, 'user_%s_filters' % category, [] ) + self.__init_filters( category, user_filters, filters, validate=validate ) + else: + if kwds.get( "trackster", False ): + filters[ "tool" ].append( _has_trackster_conf ) + + return filters + + def __init_filters( self, key, filters, toolbox_filters, validate=None ): + for filter in filters: + if validate is None or filter in validate or filter in self.default_filters: + filter_function = self.__build_filter_function( filter ) + toolbox_filters[ key ].append( filter_function ) + else: + log.warning( "Refusing to load %s filter '%s' which is not defined in config", key, filter ) + return toolbox_filters + + def __build_filter_function( self, filter_name ): + """Obtain python function (importing a submodule if needed) + corresponding to filter_name. + """ + if ":" in filter_name: + # Should be a submodule of filters (e.g. examples:restrict_development_tools) + (module_name, function_name) = filter_name.rsplit(":", 1) + function = self._import_filter( module_name, function_name ) + else: + # No module found, just load a function from this file or + # one that has be explicitly imported. + function = getattr( globals(), filter_name.strip() ) + return function + + def _import_filter( self, module_name, function_name ): + function_name = function_name.strip() + for base_module in self.__base_modules: + full_module_name = "%s.%s" % ( base_module, module_name.strip() ) + try: + __import__( full_module_name ) + except ImportError: + # log.debug("Failed to load module.", exc_info=True) + continue + module = sys.modules[ full_module_name ] + if hasattr( module, function_name ): + return getattr( module, function_name ) + raise Exception("Failed to find filter %s.%s" % (module_name, function_name)) + + +## Stock Filter Functions +def _not_hidden( context, tool ): + return not tool.hidden + + +def _handle_requires_login( context, tool ): + return not tool.require_login or context.trans.user + + +def _has_trackster_conf( context, tool ): + return tool.trackster_conf diff --git a/lib/galaxy/tools/filters/examples.py.sample b/lib/galaxy/tools/toolbox/filters/examples.py.sample similarity index 100% rename from lib/galaxy/tools/filters/examples.py.sample rename to lib/galaxy/tools/toolbox/filters/examples.py.sample diff --git a/test/unit/tools/test_toolbox_filters.py b/test/unit/tools/test_toolbox_filters.py index 482b59d91f0..74fdbeb2757 100644 --- a/test/unit/tools/test_toolbox_filters.py +++ b/test/unit/tools/test_toolbox_filters.py @@ -1,6 +1,6 @@ from galaxy.util.bunch import Bunch -from galaxy.tools.filters import FilterFactory +from galaxy.tools.toolbox.filters import FilterFactory def test_stock_filtering_requires_login_tools( ): From 5113526830736ccd7b579c6cb993f9e5e6453da3 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 30 Dec 2014 09:43:28 -0500 Subject: [PATCH 096/473] Bugfix for functional tests broken with 68daf6d. --- lib/galaxy/tools/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 45fca94588f..964443a726d 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -573,6 +573,11 @@ class ToolBox( object, Dictifiable ): return [ tool ] return [] + @property + def tools_by_id( self ): + # Deprecated method, TODO - eliminate calls to this in test/. + return self._tools_by_id + def tools( self ): return self._tools_by_id.iteritems() From 7e45ca2a72aad8b6c5e94f3fbb9d470663abf15b Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 31 Dec 2014 18:21:10 -0500 Subject: [PATCH 097/473] Allow multiple tools with the same id in ToolBox. How to use: 1.) Place multiple tools with different IDs in your tool conf. 2.) ... ummm ... no step 2 - just use the tools. Implementation: The Tool Shed allows tool lineages by assigning each tool version a GUID and tracking versions in a database. This implementation works by simply allowing the ToolBox to contain multiple tools with the same ID and orders them by the version specified by the tool author. To track enable this a second tool lineage has been introduced that just uses tool versions instead of a database (non-toolshed installed tools are not longer placed into the Tool Shed install database). The ToolBox has been updated to allow multiple versions per tool id (defaulting to the 'latest' version for all operations which do not specify a version). Both jobs and workflow steps would track tool versions but did not use that version when fetching tools from the Toolbox - these components have been updated to try to use the tool version. Unit tests working through most of the ToolBox and tool panel have been added, as well as functional tests exercising the tools API and to ensure workflows now at least attempt to respect tool versions (still kind of silently switches versions in some cases). Manual tests against the new tool form seem to demonstrate the tool switching and tool re-running work with only minor changes to the tools API and the job handler. --- lib/galaxy/jobs/__init__.py | 2 +- lib/galaxy/jobs/handler.py | 2 +- lib/galaxy/tools/__init__.py | 71 +++++++++++++++---- lib/galaxy/tools/toolbox/lineages/factory.py | 6 +- .../tools/toolbox/lineages/interface.py | 34 ++++++++- lib/galaxy/tools/toolbox/lineages/stock.py | 44 ++++++++++++ .../tools/toolbox/lineages/tool_shed.py | 4 ++ lib/galaxy/workflow/modules.py | 20 ++++-- test/api/test_tools.py | 20 +++++- test/api/test_workflows.py | 24 ++++++- .../tools/multiple_versions_v01.xml | 11 +++ .../tools/multiple_versions_v02.xml | 11 +++ test/functional/tools/samples_tool_conf.xml | 3 + test/unit/jobs/test_job_wrapper.py | 2 +- test/unit/tools/test_toolbox.py | 49 ++++++++++++- test/unit/tools_support.py | 23 ++++-- test/unit/workflows/workflow_support.py | 2 +- 17 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 lib/galaxy/tools/toolbox/lineages/stock.py create mode 100644 test/functional/tools/multiple_versions_v01.xml create mode 100644 test/functional/tools/multiple_versions_v02.xml diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 677f5b69a30..6c98ca43bca 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -711,7 +711,7 @@ class JobWrapper( object ): self.job_id = job.id self.session_id = job.session_id self.user_id = job.user_id - self.tool = queue.app.toolbox.get_tool( job.tool_id, exact=True ) + self.tool = queue.app.toolbox.get_tool( job.tool_id, job.tool_version, exact=True ) self.queue = queue self.app = queue.app self.sa_session = self.app.model.context diff --git a/lib/galaxy/jobs/handler.py b/lib/galaxy/jobs/handler.py index 07a859d7f16..9504a26dbc3 100644 --- a/lib/galaxy/jobs/handler.py +++ b/lib/galaxy/jobs/handler.py @@ -119,7 +119,7 @@ class JobHandlerQueue( object ): & ( model.Job.handler == self.app.config.server_name ) ).all() for job in jobs_at_startup: - if not self.app.toolbox.has_tool( job.tool_id, exact=True ): + if not self.app.toolbox.has_tool( job.tool_id, job.tool_version, exact=True ): log.warning( "(%s) Tool '%s' removed from tool config, unable to recover job" % ( job.id, job.tool_id ) ) self.job_wrapper( job ).fail( 'This tool was disabled before the job completed. Please contact your Galaxy administrator.' ) elif job.job_runner_name is not None and job.job_runner_external_id is None: diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 964443a726d..b7fb93cb653 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -126,6 +126,10 @@ class ToolBox( object, Dictifiable ): # shed_tool_conf.xml file. self._dynamic_tool_confs = [] self._tools_by_id = {} + # Tool lineages can contain chains of related tools with different ids + # so each will be present once in the above dictionary. The following + # dictionary can instead hold multiple tools with different versions. + self._tool_versions_by_id = {} self._workflows_by_id = {} # In-memory dictionary that defines the layout of the tool panel. self._tool_panel = odict() @@ -352,7 +356,8 @@ class ToolBox( object, Dictifiable ): inserted = True if not inserted: # Check the tool's installed versions. - for lineage_id in tool.lineage_ids: + for tool_lineage_version in tool.lineage.get_versions(): + lineage_id = tool_lineage_version.id lineage_id_key = 'tool_%s' % lineage_id for index, integrated_panel_key in enumerate( self._integrated_tool_panel.keys() ): if lineage_id_key == integrated_panel_key: @@ -503,20 +508,26 @@ class ToolBox( object, Dictifiable ): def get_tool( self, tool_id, tool_version=None, get_all_versions=False, exact=False ): """Attempt to locate a tool in the tool box.""" + if tool_version: + tool_version = str( tool_version ) + if get_all_versions and exact: raise AssertionError("Cannot specify get_tool with both get_all_versions and exact as True") if tool_id in self._tools_by_id and not get_all_versions: + if tool_version and tool_version in self._tool_versions_by_id[ tool_id ]: + return self._tool_versions_by_id[ tool_id ][ tool_version ] #tool_id exactly matches an available tool by id (which is 'old' tool_id or guid) return self._tools_by_id[ tool_id ] #exact tool id match not found, or all versions requested, search for other options, e.g. migrated tools or different versions rval = [] tool_lineage = self._lineage_map.get( tool_id ) if tool_lineage: - tool_version_ids = tool_lineage.get_version_ids( ) - for tool_version_id in tool_version_ids: - if tool_version_id in self._tools_by_id: - rval.append( self._tools_by_id[ tool_version_id ] ) + lineage_tool_versions = tool_lineage.get_versions( ) + for lineage_tool_version in lineage_tool_versions: + lineage_tool = self._tool_from_lineage_version( lineage_tool_version ) + if lineage_tool: + rval.append( lineage_tool ) if not rval: #still no tool, do a deeper search and try to match by old ids for tool in self._tools_by_id.itervalues(): @@ -561,11 +572,12 @@ class ToolBox( object, Dictifiable ): """Get all loaded tools associated by lineage to the tool whose id is tool_id.""" tool_lineage = self._lineage_map.get( tool_id ) if tool_lineage: - tool_version_ids = tool_lineage.get_version_ids( ) + lineage_tool_versions = tool_lineage.get_versions( ) available_tool_versions = [] - for tool_version_id in tool_version_ids: - if tool_version_id in self._tools_by_id: - available_tool_versions.append( self._tools_by_id[ tool_version_id ] ) + for lineage_tool_version in lineage_tool_versions: + tool = self._tool_from_lineage_version( lineage_tool_version ) + if tool: + available_tool_versions.append( tool ) return available_tool_versions else: if tool_id in self._tools_by_id: @@ -746,7 +758,6 @@ class ToolBox( object, Dictifiable ): tool_lineage = self._lineage_map.register( tool, tool_shed_repository=tool_shed_repository ) # Load the tool's lineage ids. tool.lineage = tool_lineage - tool.lineage_ids = tool_lineage.get_version_ids( ) self._tool_tag_manager.handle_tags( tool.id, elem ) self.__add_tool( tool, load_panel_dict, panel_dict ) # Always load the tool into the integrated_panel_dict, or it will not be included in the integrated_tool_panel.xml file. @@ -925,7 +936,20 @@ class ToolBox( object, Dictifiable ): return tool def register_tool( self, tool ): - self._tools_by_id[ tool.id ] = tool + tool_id = tool.id + version = tool.version or None + if tool_id not in self._tool_versions_by_id: + self._tool_versions_by_id[ tool_id ] = { version: tool } + else: + self._tool_versions_by_id[ tool_id ][ version ] = tool + if tool_id in self._tools_by_id: + related_tool = self._tools_by_id[ tool_id ] + # This one becomes the default un-versioned tool + # if newer. + if self._newer_tool( tool, related_tool ): + self._tools_by_id[ tool_id ] = tool + else: + self._tools_by_id[ tool_id ] = tool def package_tool( self, trans, tool_id ): """ @@ -1202,9 +1226,11 @@ class ToolBox( object, Dictifiable ): if not hasattr( tool, "lineage" ): return None tool_lineage = tool.lineage - lineage_ids = tool_lineage.get_version_ids( reverse=True ) - for lineage_id in lineage_ids: - if lineage_id in self._tools_by_id: + lineage_tool_versions = tool_lineage.get_versions( reverse=True ) + for lineage_tool_version in lineage_tool_versions: + lineage_tool = self._tool_from_lineage_version( lineage_tool_version ) + if lineage_tool: + lineage_id = lineage_tool.id loaded_version_key = 'tool_%s' % lineage_id if loaded_version_key in panel_dict: return panel_dict[ loaded_version_key ] @@ -1214,7 +1240,22 @@ class ToolBox( object, Dictifiable ): """ Return True if tool1 is considered "newer" given its own lineage description. """ - return tool1.lineage_ids.index( tool1.id ) > tool1.lineage_ids.index( tool2.id ) + if not hasattr( tool1, "lineage" ): + return True + lineage_tool_versions = tool1.lineage.get_versions() + for lineage_tool_version in lineage_tool_versions: + lineage_tool = self._tool_from_lineage_version( lineage_tool_version ) + if lineage_tool is tool1: + return False + if lineage_tool is tool2: + return True + return True + + def _tool_from_lineage_version( self, lineage_tool_version ): + if lineage_tool_version.id_based: + return self._tools_by_id.get( lineage_tool_version.id, None ) + else: + return self._tool_versions_by_id.get( lineage_tool_version.id, {} ).get( lineage_tool_version.version, None ) def _filter_for_panel( item, filters, context ): diff --git a/lib/galaxy/tools/toolbox/lineages/factory.py b/lib/galaxy/tools/toolbox/lineages/factory.py index 77efe944017..eb28ec06f11 100644 --- a/lib/galaxy/tools/toolbox/lineages/factory.py +++ b/lib/galaxy/tools/toolbox/lineages/factory.py @@ -1,4 +1,5 @@ from .tool_shed import ToolShedLineage +from .stock import StockLineage class LineageMap(object): @@ -13,7 +14,10 @@ class LineageMap(object): tool_id = tool.id if tool_id not in self.lineage_map: tool_shed_repository = kwds.get("tool_shed_repository", None) - lineage = ToolShedLineage.from_tool(self.app, tool, tool_shed_repository) + if tool_shed_repository: + lineage = ToolShedLineage.from_tool(self.app, tool, tool_shed_repository) + else: + lineage = StockLineage.from_tool( tool ) self.lineage_map[tool_id] = lineage return self.lineage_map[tool_id] diff --git a/lib/galaxy/tools/toolbox/lineages/interface.py b/lib/galaxy/tools/toolbox/lineages/interface.py index 1135213b8d1..46a49ecb87d 100644 --- a/lib/galaxy/tools/toolbox/lineages/interface.py +++ b/lib/galaxy/tools/toolbox/lineages/interface.py @@ -8,7 +8,35 @@ class ToolLineage(object): __metaclass__ = ABCMeta @abstractmethod - def get_version_ids( self, reverse=False ): - """ Return an ordered list of lineages in this chain, from - oldest to newest. + def get_versions( self, reverse=False ): + """ Return an ordered list of lineages (ToolLineageVersion) in this + chain, from oldest to newest. """ + + +class ToolLineageVersion(object): + """ Represents a single tool in a lineage. If lineage is based + around GUIDs that somehow encode the version (either using GUID + or a simple tool id and a version). """ + + def __init__(self, id, version): + self.id = id + self.version = version + + @staticmethod + def from_id_and_verion( id, version ): + assert version is not None + return ToolLineageVersion( id, version ) + + @staticmethod + def from_guid( guid ): + return ToolLineageVersion( guid, None ) + + @property + def id_based( self ): + """ Return True if the lineage is defined by GUIDs (in this + case the indexer of the tools (i.e. the ToolBox) should ignore + the tool_version (because it is encoded in the GUID and managed + externally). + """ + return self.version is None diff --git a/lib/galaxy/tools/toolbox/lineages/stock.py b/lib/galaxy/tools/toolbox/lineages/stock.py new file mode 100644 index 00000000000..c2db57bee7f --- /dev/null +++ b/lib/galaxy/tools/toolbox/lineages/stock.py @@ -0,0 +1,44 @@ +import threading + +from distutils.version import LooseVersion + +from .interface import ToolLineage +from .interface import ToolLineageVersion + + +class StockLineage(ToolLineage): + """ Simple tool's loaded directly from file system with lineage + determined solely by distutil's LooseVersion naming scheme. + """ + lineages_by_id = {} + lock = threading.Lock() + + def __init__(self, tool_id, **kwds): + self.tool_id = tool_id + self.tool_versions = set() + + @staticmethod + def from_tool( tool ): + tool_id = tool.id + lineages_by_id = StockLineage.lineages_by_id + with StockLineage.lock: + if tool_id not in lineages_by_id: + lineages_by_id[ tool_id ] = StockLineage( tool_id ) + lineage = lineages_by_id[ tool_id ] + lineage.register_version( tool.version ) + return lineage + + def register_version( self, tool_version ): + assert tool_version is not None + self.tool_versions.add( tool_version ) + + def get_versions( self, reverse=False ): + versions = [ ToolLineageVersion( self.tool_id, v ) for v in self.tool_versions ] + # Sort using LooseVersion which defines an appropriate __cmp__ + # method for comparing tool versions. + return sorted( versions, key=_to_loose_version ) + + +def _to_loose_version( tool_lineage_version ): + version = str( tool_lineage_version.version ) + return LooseVersion( version ) diff --git a/lib/galaxy/tools/toolbox/lineages/tool_shed.py b/lib/galaxy/tools/toolbox/lineages/tool_shed.py index 66b83e05798..7e261c367fb 100644 --- a/lib/galaxy/tools/toolbox/lineages/tool_shed.py +++ b/lib/galaxy/tools/toolbox/lineages/tool_shed.py @@ -1,4 +1,5 @@ from .interface import ToolLineage +from .interface import ToolLineageVersion from galaxy.model.tool_shed_install import ToolVersion @@ -32,6 +33,9 @@ class ToolShedLineage(ToolLineage): tool_version = self.app.install_model.context.query( ToolVersion ).get( self.tool_version_id ) return tool_version.get_version_ids( self.app, reverse=reverse ) + def get_versions( self, reverse=False ): + return map( ToolLineageVersion.from_guid, self.get_version_ids( reverse=reverse ) ) + def get_install_tool_version( app, tool_id ): return app.install_model.context.query( diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 253505f7dd9..c46536e27b2 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -467,10 +467,10 @@ class ToolModule( WorkflowModule ): type = "tool" - def __init__( self, trans, tool_id ): + def __init__( self, trans, tool_id, tool_version=None ): self.trans = trans self.tool_id = tool_id - self.tool = trans.app.toolbox.get_tool( tool_id ) + self.tool = trans.app.toolbox.get_tool( tool_id, tool_version=tool_version ) self.post_job_actions = {} self.workflow_outputs = [] self.state = None @@ -493,11 +493,14 @@ class ToolModule( WorkflowModule ): @classmethod def from_dict( Class, trans, d, secure=True ): tool_id = d[ 'tool_id' ] - module = Class( trans, tool_id ) + tool_version = str( d.get( 'tool_version', None ) ) + module = Class( trans, tool_id, tool_version=tool_version ) module.state = galaxy.tools.DefaultToolState() if module.tool is not None: if d.get('tool_version', 'Unspecified') != module.get_tool_version(): - module.version_changes.append( "%s: using version '%s' instead of version '%s' indicated in this workflow." % ( tool_id, d.get( 'tool_version', 'Unspecified' ), module.get_tool_version() ) ) + message = "%s: using version '%s' instead of version '%s' indicated in this workflow." % ( tool_id, d.get( 'tool_version', 'Unspecified' ), module.get_tool_version() ) + log.debug(message) + module.version_changes.append(message) module.state.decode( d[ "tool_state" ], module.tool, module.trans.app, secure=secure ) module.errors = d.get( "tool_errors", None ) module.post_job_actions = d.get( "post_job_actions", {} ) @@ -519,9 +522,12 @@ class ToolModule( WorkflowModule ): # This step has its state saved in the config field due to the # tool being previously unavailable. return module_factory.from_dict(trans, loads(step.config), secure=False) - module = Class( trans, tool_id ) + tool_version = step.tool_version + module = Class( trans, tool_id, tool_version=tool_version ) if step.tool_version and (step.tool_version != module.tool.version): - module.version_changes.append("%s: using version '%s' instead of version '%s' indicated in this workflow." % (tool_id, module.tool.version, step.tool_version)) + message = "%s: using version '%s' instead of version '%s' indicated in this workflow." % (tool_id, module.tool.version, step.tool_version) + log.debug(message) + module.version_changes.append(message) module.recover_state( step.tool_inputs ) module.errors = step.tool_errors module.workflow_outputs = step.workflow_outputs @@ -723,7 +729,7 @@ class ToolModule( WorkflowModule ): return state, step_errors def execute( self, trans, progress, invocation, step ): - tool = trans.app.toolbox.get_tool( step.tool_id ) + tool = trans.app.toolbox.get_tool( step.tool_id, tool_version=step.tool_version ) tool_state = step.state collections_to_match = self._find_collections_to_match( tool, progress, step ) diff --git a/test/api/test_tools.py b/test/api/test_tools.py index 6d02738796c..914ff517de4 100644 --- a/test/api/test_tools.py +++ b/test/api/test_tools.py @@ -165,6 +165,18 @@ class ToolsTestCase( api.ApiTestCase ): output1_content = self.dataset_populator.get_history_dataset_content( history_id, dataset=output1 ) self.assertEqual( output1_content.strip(), "Cat1Testlistified" ) + @skip_without_tool( "multiple_versions" ) + def test_run_by_versions( self ): + for version in ["0.1", "0.2"]: + # Run simple non-upload tool with an input data parameter. + history_id = self.dataset_populator.new_history() + inputs = dict() + outputs = self._run_and_get_outputs( tool_id="multiple_versions", history_id=history_id, inputs=inputs, tool_version=version ) + self.assertEquals( len( outputs ), 1 ) + output1 = outputs[ 0 ] + output1_content = self.dataset_populator.get_history_dataset_content( history_id, dataset=output1 ) + self.assertEqual( output1_content.strip(), "Version " + version ) + @skip_without_tool( "cat1" ) def test_run_cat1_single_meta_wrapper( self ): # Wrap input in a no-op meta parameter wrapper like Sam is planning to @@ -650,8 +662,8 @@ class ToolsTestCase( api.ApiTestCase ): def _cat1_outputs( self, history_id, inputs ): return self._run_outputs( self._run_cat1( history_id, inputs ) ) - def _run_and_get_outputs( self, tool_id, history_id, inputs ): - return self._run_outputs( self._run( tool_id, history_id, inputs ) ) + def _run_and_get_outputs( self, tool_id, history_id, inputs, tool_version=None ): + return self._run_outputs( self._run( tool_id, history_id, inputs, tool_version=tool_version ) ) def _run_outputs( self, create_response ): self._assert_status_code_is( create_response, 200 ) @@ -660,12 +672,14 @@ class ToolsTestCase( api.ApiTestCase ): def _run_cat1( self, history_id, inputs, assert_ok=False ): return self._run( 'cat1', history_id, inputs, assert_ok=assert_ok ) - def _run( self, tool_id, history_id, inputs, assert_ok=False ): + def _run( self, tool_id, history_id, inputs, assert_ok=False, tool_version=None ): payload = self.dataset_populator.run_tool_payload( tool_id=tool_id, inputs=inputs, history_id=history_id, ) + if tool_version is not None: + payload[ "tool_version" ] = tool_version create_response = self._post( "tools", data=payload ) if assert_ok: self._assert_status_code_is( create_response, 200 ) diff --git a/test/api/test_workflows.py b/test/api/test_workflows.py index b092a0284f5..696c2d722a7 100644 --- a/test/api/test_workflows.py +++ b/test/api/test_workflows.py @@ -444,6 +444,28 @@ class WorkflowsApiTestCase( BaseWorkflowsApiTestCase ): def test_run_workflow( self ): self.__run_cat_workflow( inputs_by='step_id' ) + @skip_without_tool( "multiple_versions" ) + def test_run_versioned_tools( self ): + history_01_id = self.dataset_populator.new_history() + workflow_version_01 = self._upload_yaml_workflow( """ +- tool_id: multiple_versions + tool_version: "0.1" + state: + inttest: 0 +""" ) + self.__invoke_workflow( history_01_id, workflow_version_01 ) + self.dataset_populator.wait_for_history( history_01_id, assert_ok=True ) + + history_02_id = self.dataset_populator.new_history() + workflow_version_02 = self._upload_yaml_workflow( """ +- tool_id: multiple_versions + tool_version: "0.2" + state: + inttest: 1 +""" ) + self.__invoke_workflow( history_02_id, workflow_version_02 ) + self.dataset_populator.wait_for_history( history_02_id, assert_ok=True ) + def __run_cat_workflow( self, inputs_by ): workflow = self.workflow_populator.load_workflow( name="test_for_run" ) workflow["steps"]["0"]["uuid"] = str(uuid4()) @@ -910,7 +932,7 @@ test_data: self._assert_status_code_is( hda_info_response, 200 ) self.assertEquals( hda_info_response.json()[ "metadata_data_lines" ], lines ) - def __invoke_workflow( self, history_id, workflow_id, inputs, assert_ok=True ): + def __invoke_workflow( self, history_id, workflow_id, inputs={}, assert_ok=True ): workflow_request = dict( history="hist_id=%s" % history_id, ) diff --git a/test/functional/tools/multiple_versions_v01.xml b/test/functional/tools/multiple_versions_v01.xml new file mode 100644 index 00000000000..8b05454529f --- /dev/null +++ b/test/functional/tools/multiple_versions_v01.xml @@ -0,0 +1,11 @@ + + + echo "Version 0.1" > $out_file1 + + + + + + + + diff --git a/test/functional/tools/multiple_versions_v02.xml b/test/functional/tools/multiple_versions_v02.xml new file mode 100644 index 00000000000..7a7a3a14937 --- /dev/null +++ b/test/functional/tools/multiple_versions_v02.xml @@ -0,0 +1,11 @@ + + + echo "Version 0.2" > $out_file1 + + + + + + + + diff --git a/test/functional/tools/samples_tool_conf.xml b/test/functional/tools/samples_tool_conf.xml index 751eb627264..d0a1f8d1974 100644 --- a/test/functional/tools/samples_tool_conf.xml +++ b/test/functional/tools/samples_tool_conf.xml @@ -36,6 +36,9 @@ + + + \n') - for key, item in self._integrated_tool_panel.items(): + for key, item_type, item in self._integrated_tool_panel.panel_items_iter(): if item: - if isinstance( item, Tool ): + if item_type == panel_item_types.TOOL: os.write( fd, ' \n' % item.id ) - elif isinstance( item, Workflow ): + elif item_type == panel_item_types.WORKFLOW: os.write( fd, ' \n' % item.id ) - elif isinstance( item, ToolSectionLabel ): + elif item_type == panel_item_types.LABEL: label_id = item.id or '' label_text = item.text or '' label_version = item.version or '' os.write( fd, '
- +
From 77638ab961bf8eb79a91975530d075c9bf8d5282 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 7 Jan 2015 14:51:24 -0500 Subject: [PATCH 131/473] Strip whitespace. --- templates/webapps/galaxy/dataset/errors.mako | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/webapps/galaxy/dataset/errors.mako b/templates/webapps/galaxy/dataset/errors.mako index 078f113dbec..bd914f135a3 100644 --- a/templates/webapps/galaxy/dataset/errors.mako +++ b/templates/webapps/galaxy/dataset/errors.mako @@ -32,7 +32,7 @@ { form.elements[i].disabled = true; } - + } var hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; @@ -52,7 +52,7 @@

Dataset ${hda.hid}: ${hda.display_name() | h}

<% job = hda.creating_job %> %if job: - + %if job.traceback: The Galaxy framework encountered the following error while attempting to run the tool:
${ util.unicodify( job.traceback ) | h}
@@ -82,8 +82,8 @@ %>

Report this error to the Galaxy Team

- The Galaxy team regularly reviews errors that occur in the application. - However, if you would like to provide additional information (such as + The Galaxy team regularly reviews errors that occur in the application. + However, if you would like to provide additional information (such as what you were trying to do when the error occurred) and a contact e-mail address, we will be better able to investigate your problem and get back to you. From 2806fbe83eda654308be835089f73606ad4611bf Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Wed, 7 Jan 2015 14:55:37 -0500 Subject: [PATCH 132/473] More documentation; minor fixes --- lib/galaxy/managers/base.py | 42 ++++++-- lib/galaxy/managers/datasets.py | 114 +++++++++++++++----- lib/galaxy/managers/hdas.py | 8 +- lib/galaxy/managers/histories.py | 4 +- lib/galaxy/managers/sharable.py | 90 +++++++++++----- lib/galaxy/managers/users.py | 6 +- test/unit/managers/test_DatasetManager.py | 29 ++++- test/unit/managers/test_HDAManager.py | 122 +++++++++++++--------- 8 files changed, 291 insertions(+), 124 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 92cc494a15a..37f64f84a02 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -131,14 +131,15 @@ class ModelManager( object ): foreign_key_name = None def __init__( self, app ): - """ - """ self.app = app #NOTE: at this layer, all ids are expected to be decoded and in int form # ------------------------------------------------------------------------- get/read/query def query( self, trans, eagerloads=True, filters=None, order_by=None, limit=None, offset=None, **kwargs ): """ + Return a basic query from model_class, filters, order_by, and limit and offset. + + Set eagerloads to False to disable them for this query. """ #print 'query:', eagerloads, filters, order_by, limit, offset, kwargs query = trans.sa_session.query( self.model_class ) @@ -154,6 +155,7 @@ class ModelManager( object ): def _apply_filters( self, query, filters ): """ + Add any filters to the given query. """ if filters is None: return query @@ -167,9 +169,11 @@ class ModelManager( object ): def _munge_filters( self, filtersA, filtersB ): """ + Combine two lists into a single list. + + (While allowing them to be None, non-lists, or lists.) """ # ... which sounds like a euphamism for something - final_filters = [ self.model_class.id == id ] if filtersA is None: return filtersB if filtersB is None: @@ -182,12 +186,16 @@ class ModelManager( object ): def _apply_order_by_limit_offset( self, query, order_by, limit, offset ): """ + Return the query after adding the order_by, limit, and offset clauses. """ query = self._apply_order_by( query, order_by ) return self._apply_limit_offset( query, limit, offset ) def _apply_order_by( self, query, order_by ): """ + Return the query after adding the order_by clauses. + + Use the manager's default_order_by if order_by is None. """ if order_by is None: #TODO: allow non-tuple default_order_by @@ -199,6 +207,7 @@ class ModelManager( object ): def _apply_limit_offset( self, query, limit, offset ): """ + Return the query after applying the given limit and offset (if not None). """ if limit is not None: query = query.limit( limit ) @@ -207,37 +216,47 @@ class ModelManager( object ): return query # ......................................................................... common queries -#TODO: needed? + #TODO: needed? def one( self, trans, **kwargs ): """ + Sends kwargs to build the query and returns one and only one model. + + :raises exceptions.ObjectNotFound: if no model is found + :raises exceptions.Conflict: if more than one model is found """ + # overridden to raise serializable errors try: return self.query( trans, **kwargs ).one() - # re-raise as serializable errors except sqlalchemy.orm.exc.NoResultFound, not_found: raise exceptions.ObjectNotFound( self.model_class.__name__ + ' not found' ) +#TODO: exceptions.InconsistentDatabase? except sqlalchemy.orm.exc.MultipleResultsFound, multi_found: raise exceptions.Conflict( 'found more than one ' + self.model_class.__name__ ) - def list( self, trans, **kwargs ): + def list( self, trans, query=None, **kwargs ): """ + Sends kwargs to build the query return all models found. """ - return self.query( trans, **kwargs ).all() + query = query or self.query( trans, **kwargs ) + return query.all() def by_id( self, trans, id, **kwargs ): """ + Gets a model by primary id. """ id_filter = self.model_class.id == id return self.one( trans, filters=id_filter, **kwargs ) def _query_by_ids( self, trans, ids, filters=None, **kwargs ): """ + Builds a query to find a list of models with the given list of `ids`. """ ids_filter = self.model_class.id.in_( ids ) return self.query( trans, filters=self._munge_filters( ids_filter, filters ), **kwargs ) def by_ids( self, trans, ids, **kwargs ): """ + Returns an in-order list of models with the matching ids in `ids`. """ found = self._query_by_ids( trans, ids, **kwargs ).all() #TODO: this does not order by the original 'ids' array @@ -277,7 +296,9 @@ class ModelManager( object ): # ------------------------------------------------------------------------- create def create( self, trans, flush=True, *args, **kwargs ): """ + Generically create a new model. """ + # override in subclasses item = self.model_class( *args, **kwargs ) trans.sa_session.add( item ) if flush: @@ -286,12 +307,16 @@ class ModelManager( object ): def copy( self, trans, item, **kwargs ): """ + Clone or copy an item. """ raise exceptions.NotImplemented( 'Abstract method' ) # ------------------------------------------------------------------------- update def update( self, trans, item, new_values, flush=True, **kwargs ): """ + Given a dictionary of new values, update `item` and return it. + + ..note: NO validation or deserialization occurs here. """ trans.sa_session.add( item ) for key, value in new_values.items(): @@ -301,8 +326,10 @@ class ModelManager( object ): trans.sa_session.flush() return item + #TODO: yagni? def associate( self, trans, associate_with, item, foreign_key_name=None ): """ + Generically associate `item` with `associate_with` based on `foreign_key_name`. """ foreign_key_name = foreign_key_name or self.foreign_key_name setattr( associate_with, foreign_key_name, item ) @@ -310,6 +337,7 @@ class ModelManager( object ): def query_associated( self, trans, associated_model_class, item, foreign_key_name=None ): """ + Generically query other items that have been associated with this `item`. """ foreign_key_name = foreign_key_name or self.foreign_key_name foreign_key = getattr( associated_model_class, foreign_key_name ) diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 9a56c8c31cb..7204189a76a 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -32,6 +32,7 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # default to NEW state on new datasets kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) ) dataset = model.Dataset( **kwargs ) + trans.sa_session.add( dataset ) if flush: trans.sa_session.flush() @@ -54,6 +55,12 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur pass # ......................................................................... delete + def error_unless_dataset_purge_allowed( self, trans, item, msg=None ): + if not trans.app.config.allow_user_dataset_purge: + msg = msg or 'This instance does not allow user dataset purging' + raise exceptions.ConfigDoesNotAllowException( msg ) + return item + def purge( self, trans, dataset, flush=True ): """ """ @@ -78,42 +85,91 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur return True return False + def _access_permission( self, trans, dataset, user=None, role=None ): + """ + """ + security_agent = trans.app.security_agent + access_action = security_agent.permitted_actions.DATASET_ACCESS.action + + # get a list of role ids to check access for (defaulting to all_roles) + user_roles = [ role ] if role else user.all_roles() + user_role_ids = [ role.id for role in user_roles ] + query = ( trans.sa_session.query( model.DatasetPermissions ) + .filter( model.DatasetPermissions.action == access_action ) + .filter( model.DatasetPermissions.dataset == dataset ) + .filter( model.DatasetPermissions.role_id.in_( user_role_ids ) ) ) + perms = query.all() + if len( perms ): + return perms[0] + return None + + def _create_access_permission( self, trans, dataset, role, flush=True ): + """ + """ + access_action = trans.app.security_agent.permitted_actions.DATASET_ACCESS.action + permission = model.DatasetPermissions( access_action, dataset, role ) + trans.sa_session.add( permission ) + if flush: + trans.sa_session.flush() + return permission + def has_access_permission( self, trans, dataset, user ): """ """ - roles = user.roles if user else [] + + roles = user.all_roles() if user else [] return trans.app.security_agent.can_access_dataset( roles, dataset ) - def give_access_permission( self, trans, dataset, user ): - """ - """ - #permission = {} - ##TODO:?? is this correct and reliable? - #user_private_role = self.user_mgr.private_role( trans, user ) - #permission[ trans.app.security_agent.permitted_actions.DATASET_ACCESS ] = [ user_private_role ] - #trans.app.security_agent.set_dataset_permission( dataset, permission ) - pass + #TODO: this needs work + #def give_access_permission( self, trans, dataset, user, flush=True ): + # """ + # """ + # # for now, use the user's private role + # security_agent = trans.app.security_agent + # user_role = security_agent.get_private_user_role( user ) + # + # existing_permission = self._access_permission( trans, dataset, role=user_role ) + # print 'existing access_perm:', existing_permission + # if existing_permission: + # return dataset + # + # permission = self._create_access_permission( trans, dataset, user_role ) + # print 'access_roles:', [ role.name for role in dataset.get_access_roles( trans ) ] + # + # #access_action = security_agent.permitted_actions.DATASET_ACCESS.action + # #access_action = security_agent.get_action( access_action ) + # #permissions = { access_action : [ user_role ] } + # #security_agent.set_dataset_permission( dataset, permissions ) + # + # #trans.sa_session.add( dataset ) + # #if flush: + # # trans.sa_session.flush() + # + # dbl_chk = self._access_permission( trans, dataset, role=user_role ) + # print 'access_perm:', dbl_chk + # + # return dataset - def remove_access_permission( self, trans, dataset, user ): - """ - """ - pass + #def remove_access_permission( self, trans, dataset, user ): + # """ + # """ + # pass # ......................................................................... manage/modify - def has_manage_permission( self, trans, dataset, user ): - """ - """ - pass - - def give_manage_permission( self, trans, dataset, user ): - """ - """ - pass - - def remove_manage_permission( self, trans, dataset, user ): - """ - """ - pass + #def has_manage_permission( self, trans, dataset, user ): + # """ + # """ + # pass + # + #def give_manage_permission( self, trans, dataset, user ): + # """ + # """ + # pass + # + #def remove_manage_permission( self, trans, dataset, user ): + # """ + # """ + # pass #TODO: implement above for groups #TODO: datatypes? @@ -183,9 +239,11 @@ class DatasetAssociationManager( base.ModelManager, base.AccessibleModelInterfac def metadata( self, trans, dataset_assoc ): """ """ + # get metadata pass def is_being_used( self, trans, dataset_assoc ): """ """ + #TODO: check history_associations, library_associations pass diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index c43efe8d9aa..ac9e8eac6ce 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -37,11 +37,11 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface def is_accessible( self, trans, hda, user ): """ """ - if self.is_owned( trans, hda, user ): + if self.is_owner( trans, hda, user ): return True return super( HDAManager, self ).is_accessible( trans, hda, user ) - def is_owned( self, trans, hda, user ): + def is_owner( self, trans, hda, user ): """ Use history to see if current user owns HDA. """ @@ -116,7 +116,9 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface def purge( self, trans, hda ): """ """ - self.error_unless_dataset_purge_allowed( trans, hda ) + # error here if disallowed - before jobs are stopped + #TODO: poss. move to DatasetAssociationManager + self.dataset_mgr.error_unless_dataset_purge_allowed( trans, hda ) super( HDAManager, self ).purge( trans, hda, flush=True ) # signal to stop the creating job? diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index c3060f2a318..cb267485183 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -225,7 +225,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface ## ============================================================================= -class HistorySerializer( base.ModelSerializer ): +class HistorySerializer( sharable.SharableModelSerializer ): """ Interface/service object for serializing histories into dictionaries. """ @@ -321,7 +321,7 @@ class HistorySerializer( base.ModelSerializer ): # ============================================================================= -class HistoryDeserializer( base.ModelDeserializer ): +class HistoryDeserializer( sharable.SharableModelDeserializer ): """ Interface/service object for validating and deserializing dictionaries into histories. """ diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index 8f56e83e2e7..a648459cd99 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -22,12 +22,12 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. can be published effectively making it available to all other Users can be rated """ - # histories, pages, stored workflows, visualizations + # e.g. histories, pages, stored workflows, visualizations + + # the model used for UserShareAssociations with this model user_share_model = None def __init__( self, app ): - """ - """ super( SharableModelManager, self ).__init__( app ) # user manager is needed to check access/ownership/admin self.user_mgr = users.UserManager( app ) @@ -40,22 +40,21 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. filters=self._munge_filters( user_filter, filters ) return self.query( trans, filters=filters, **kwargs ) - def by_user( self, trans, user, filters=None, **kwargs ): + def by_user( self, trans, user, **kwargs ): """ """ - user_filter = self.model_class.user_id == user.id - filters=self._munge_filters( user_filter, filters ) - #note: no ownership check needed - return self.list( trans, filters=filters, **kwargs ) + query = self._query_by_user( trans, user, **kwargs ) + return self.list( trans, query=query, **kwargs ) # ......................................................................... owned model interface -# this really is more of a 'check_*editable/write*' - in essence that's what check_ownership boils down to often def is_owner( self, trans, item, user ): """ + Return true if this sharable belongs to `user` (or `user` is an admin). """ if self.user_mgr.is_anonymous( user ): # note: this needs to be overridden in the case of anon users and session.current_history return False + # ... effectively a good fit to have this here, but not semantically if self.user_mgr.is_admin( trans, user ): return True return item.user == user @@ -63,6 +62,8 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # ......................................................................... accessible interface def is_accessible( self, trans, item, user ): """ + If the item is importable, is owned by `user`, or (the valid) `user` + is in 'users shared with' list for the item: return True. """ if item.importable: return True @@ -76,6 +77,8 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. return False # ......................................................................... importable +#TODO: the relationship between importable and published is ill defined +#??: can a published item be non-importable? def make_importable( self, trans, item, flush=True ): """ Makes item accessible--viewable and importable--and sets item's slug. @@ -95,7 +98,6 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ - #??: can a published item be non-importable? #if item.published: # if unpublish: # self.unpublish( trans, item, flush=False ) @@ -107,13 +109,21 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. trans.sa_session.flush() return item - def list_importable( self, trans, **kwargs ): -#TODO: - """ - """ - pass + #def _query_importable( self, trans, filters=None, **kwargs ): + # """ + # """ + # importable_filter = self.model_class.importable == True + # filters = self._munge_filters( importable_filter, filters ) + # return self.list( trans, filters=filters, **kwargs ) + # + #def list_importable( self, trans, **kwargs ): + # """ + # """ + # query = self._query_importable( trans, user, **kwargs ) + # return self.list( trans, query=query, **kwargs ) # ......................................................................... published +#TODO: the relationship between importable and published is ill defined #def publish( self, trans, item, make_accessible=True, flush=True ): # if not item.importable: # if make_accessible: @@ -124,6 +134,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # a published item is also/already importable(=True) implicitly def publish( self, trans, item, flush=True ): """ + Set both the importable and published flags on `item` to True. """ if not item.importable: self.make_importable( trans, item, flush=False ) @@ -135,6 +146,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. def unpublish( self, trans, item, flush=True ): """ + Set the published flag on `item` to False. """ trans.sa_session.add( item ) item.published = False @@ -142,11 +154,18 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. trans.sa_session.flush() return item - def list_published( self, trans, **kwargs ): -#TODO: - """ - """ - pass + #def _query_published( self, trans, filters=None, **kwargs ): + # """ + # """ + # published_filter = self.model_class.published == True + # filters = self._munge_filters( published_filter, filters ) + # return self.list( trans, filters=filters, **kwargs ) + # + #def list_published( self, trans, **kwargs ): + # """ + # """ + # query = self._query_published( trans, user, **kwargs ) + # return self.list( trans, query=query, **kwargs ) # ......................................................................... user sharing # sharing is often done via a 3rd table btwn a User and an item -> a UserShareAssociation @@ -186,23 +205,31 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. trans.sa_session.flush() return item - def list_shared_with( self, trans, item, user, **kwargs ): -#TODO: - """ - """ - pass + #def _query_shared_with( self, trans, user, filters=None, **kwargs ): + ##TODO: + # """ + # """ + # pass + + #def list_published( self, trans, user, **kwargs ): + # """ + # """ + # query = self._query_shared_with( trans, user, **kwargs ) + # return self.list( trans, query=query, **kwargs ) # ......................................................................... slugs # slugs are human readable strings often used to link to sharable resources (replacing ids) + #TODO: as validator, deserializer, etc. (maybe another object entirely?) def is_valid_slug( self, slug ): """ - Returns true if slug is valid. + Returns true if `slug` is valid. """ VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" ) return VALID_SLUG_RE.match( slug ) def set_slug( self, trans, item, new_slug, flush=True ): """ + Validate and set the new slug for `item`. """ if not self.is_valid_slug( new_slug ): raise exceptions.RequestParameterInvalidException( "Invalid slug", slug=new_slug ) @@ -228,6 +255,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # Setup slug base. if cur_slug is None or cur_slug == "": # Item can have either a name or a title. +#TODO: this depends on the model having name or title if hasattr( item, 'name' ): item_name = item.name elif hasattr( item, 'title' ): @@ -282,6 +310,16 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # """ Returns item's name and link. """ +# ============================================================================= +class SharableModelSerializer( base.ModelSerializer ): + pass + + # the only ones that needs any fns: + # user/user_id + # user_shares? + # username_and_slug? + + # ============================================================================= class SharableModelDeserializer( base.ModelDeserializer ): diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 18f2074bbe3..14dafa3a620 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -161,9 +161,6 @@ class UserManager( base.ModelManager ): #TODO: catch NoResultFound return trans.sa_session.query( model.APIKeys ).filter_by( user=user ).one() - #TODO: there is quite a bit of functionality around the user (authentication, quotas, groups/roles) - # most of which it may be unneccessary to have here - # ------------------------------------------------------------------------- roles def private_role( self, trans, user ): """ @@ -171,6 +168,9 @@ class UserManager( base.ModelManager ): #TODO: not sure we need to go through sec agent... it's just the first role of type private return trans.app.security_agent.get_private_user_role( user ) + #TODO: there is quite a bit of functionality around the user (authentication, quotas, groups/roles) + # most of which it may be unneccessary to have here + # ============================================================================= class UserSerializer( base.ModelSerializer ): diff --git a/test/unit/managers/test_DatasetManager.py b/test/unit/managers/test_DatasetManager.py index ba55b037f87..e8b47188d2f 100644 --- a/test/unit/managers/test_DatasetManager.py +++ b/test/unit/managers/test_DatasetManager.py @@ -95,6 +95,30 @@ class DatasetManagerTestCase( BaseTestCase ): self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) self.assertFalse( item1.purged ) + ##TODO: I'm unclear as to how these work, so I'm kicking this down the road a bit.... + #def test_access_permission( self ): + # owner = self.user_mgr.create( self.trans, **user2_data ) + # dataset = self.dataset_mgr.create( self.trans ) + # # giving one user access permission makes it non-public, removing access for anyone else + # self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) + # + # user3 = self.user_mgr.create( self.trans, **user3_data ) + # user4 = self.user_mgr.create( self.trans, + # email='user4@user4.user4', username='user4', password=default_password ) + # + # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, owner ) ) + # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user3 ) ) + # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user4 ) ) + # + # # should be able to progressively add more roles without removing the previous + # self.dataset_mgr.give_access_permission( self.trans, dataset, user3 ) + # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, user3 ) ) + # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, owner ) ) + # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user4 ) ) + # + # #self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) + # #self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) + def test_accessible( self ): owner = self.user_mgr.create( self.trans, **user2_data ) non_owner = self.user_mgr.create( self.trans, **user3_data ) @@ -105,11 +129,6 @@ class DatasetManagerTestCase( BaseTestCase ): for user in self.user_mgr.list( self.trans ): self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, user ) ) - #TODO: this is ... not working... - #self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) - #self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) - #self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) - # ============================================================================= class DatasetAssociationManagerTestCase( BaseTestCase ): diff --git a/test/unit/managers/test_HDAManager.py b/test/unit/managers/test_HDAManager.py index c46860fd3d3..50e66db968b 100644 --- a/test/unit/managers/test_HDAManager.py +++ b/test/unit/managers/test_HDAManager.py @@ -36,6 +36,33 @@ class HDAManagerTestCase( BaseTestCase ): self.dataset_mgr = DatasetManager( self.app ) self.hda_mgr = HDAManager( self.app ) + def test_base( self ): + hda_model = model.HistoryDatasetAssociation + owner = self.user_mgr.create( self.trans, **user2_data ) + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + hda1 = self.hda_mgr.create( self.trans, history=history1, hid=1 ) + hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) + hda3 = self.hda_mgr.create( self.trans, history=history1, hid=3 ) + + self.log( "should be able to query" ) + hdas = self.trans.sa_session.query( hda_model ).all() + self.assertEqual( self.hda_mgr.list( self.trans ), hdas ) + self.assertEqual( self.hda_mgr.one( self.trans, filters=( hda_model.id == hda1.id ) ), hda1 ) + self.assertEqual( self.hda_mgr.by_id( self.trans, hda1.id ), hda1 ) + self.assertEqual( self.hda_mgr.by_ids( self.trans, [ hda2.id, hda1.id ] ), [ hda2, hda1 ] ) + + self.log( "should be able to limit and offset" ) + self.assertEqual( self.hda_mgr.list( self.trans, limit=1 ), hdas[0:1] ) + self.assertEqual( self.hda_mgr.list( self.trans, offset=1 ), hdas[1:] ) + self.assertEqual( self.hda_mgr.list( self.trans, limit=1, offset=1 ), hdas[1:2] ) + + self.assertEqual( self.hda_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.hda_mgr.list( self.trans, offset=3 ), [] ) + + self.log( "should be able to order" ) + self.assertEqual( self.hda_mgr.list( self.trans, order_by=sqlalchemy.desc( hda_model.create_time ) ), + [ hda3, hda2, hda1 ] ) + def test_create( self ): owner = self.user_mgr.create( self.trans, **user2_data ) non_owner = self.user_mgr.create( self.trans, **user3_data ) @@ -87,33 +114,6 @@ class HDAManagerTestCase( BaseTestCase ): # self.log( "should be able to copy an HDA" ) # hda2 = self.hda_mgr.copy_ldda( self.trans, history1, hda1 ) - def test_base( self ): - hda_model = model.HistoryDatasetAssociation - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - hda1 = self.hda_mgr.create( self.trans, history=history1, hid=1 ) - hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) - hda3 = self.hda_mgr.create( self.trans, history=history1, hid=3 ) - - self.log( "should be able to query" ) - hdas = self.trans.sa_session.query( hda_model ).all() - self.assertEqual( self.hda_mgr.list( self.trans ), hdas ) - self.assertEqual( self.hda_mgr.one( self.trans, filters=( hda_model.id == hda1.id ) ), hda1 ) - self.assertEqual( self.hda_mgr.by_id( self.trans, hda1.id ), hda1 ) - self.assertEqual( self.hda_mgr.by_ids( self.trans, [ hda2.id, hda1.id ] ), [ hda2, hda1 ] ) - - self.log( "should be able to limit and offset" ) - self.assertEqual( self.hda_mgr.list( self.trans, limit=1 ), hdas[0:1] ) - self.assertEqual( self.hda_mgr.list( self.trans, offset=1 ), hdas[1:] ) - self.assertEqual( self.hda_mgr.list( self.trans, limit=1, offset=1 ), hdas[1:2] ) - - self.assertEqual( self.hda_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.hda_mgr.list( self.trans, offset=3 ), [] ) - - self.log( "should be able to order" ) - self.assertEqual( self.hda_mgr.list( self.trans, order_by=sqlalchemy.desc( hda_model.create_time ) ), - [ hda3, hda2, hda1 ] ) - def test_delete( self ): owner = self.user_mgr.create( self.trans, **user2_data ) history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) @@ -142,8 +142,6 @@ class HDAManagerTestCase( BaseTestCase ): def test_purge_not_allowed( self ): self.trans.app.config.allow_user_dataset_purge = False - self.assertRaises( exceptions.ConfigDoesNotAllowException, - self.hda_mgr.error_unless_dataset_purge_allowed, self.trans, None ) owner = self.user_mgr.create( self.trans, **user2_data ) history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) @@ -155,27 +153,31 @@ class HDAManagerTestCase( BaseTestCase ): self.assertRaises( exceptions.ConfigDoesNotAllowException, self.hda_mgr.purge, self.trans, item1 ) self.assertFalse( item1.purged ) - #def test_ownable( self ): - # owner = self.user_mgr.create( self.trans, **user2_data ) - # non_owner = self.user_mgr.create( self.trans, **user3_data ) - # - # history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - # dataset1 = self.dataset_mgr.create( self.trans ) - # item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) - # - # self.log( "should be able to poll whether a given user owns an item" ) - # self.assertTrue( self.hda_mgr.is_owner( self.trans, item1, owner ) ) - # self.assertFalse( self.hda_mgr.is_owner( self.trans, item1, non_owner ) ) - # - # self.log( "should raise an error when checking ownership with non-owner" ) - # self.assertRaises( exceptions.ItemOwnershipException, - # self.hda_mgr.check_ownership, self.trans, item1, non_owner ) - # - # self.log( "should not raise an error when checking ownership with owner" ) - # self.assertEqual( self.hda_mgr.check_ownership( self.trans, item1, owner ), item1 ) - # - # #self.log( "should not raise an error when checking ownership with admin" ) - # #self.assertEqual( self.hda_mgr.check_ownership( self.trans, item1, self.admin_user ), item1 ) + def test_ownable( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + + history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + + self.log( "should be able to poll whether a given user owns an item" ) + self.assertTrue( self.hda_mgr.is_owner( self.trans, item1, owner ) ) + self.assertFalse( self.hda_mgr.is_owner( self.trans, item1, non_owner ) ) + + self.log( "should raise an error when checking ownership with non-owner" ) + self.assertRaises( exceptions.ItemOwnershipException, + self.hda_mgr.error_unless_owner, self.trans, item1, non_owner ) + + self.log( "should raise an error when checking ownership with anonymous" ) + self.assertRaises( exceptions.ItemOwnershipException, + self.hda_mgr.error_unless_owner, self.trans, item1, None ) + + self.log( "should not raise an error when checking ownership with owner" ) + self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, owner ), item1 ) + + self.log( "should not raise an error when checking ownership with admin" ) + self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, self.admin_user ), item1 ) def test_accessible( self ): owner = self.user_mgr.create( self.trans, **user2_data ) @@ -189,6 +191,26 @@ class HDAManagerTestCase( BaseTestCase ): for user in self.user_mgr.list( self.trans ): self.assertTrue( self.hda_mgr.is_accessible( self.trans, item1, user ) ) + #TODO: set perms on underlying dataset and then test accessible + + def test_anon( self ): + anon_user = None + self.trans.set_user( anon_user ) + + history1 = self.history_mgr.create( self.trans, name='anon_history', user=anon_user ) + self.trans.set_history( history1 ) + dataset1 = self.dataset_mgr.create( self.trans ) + item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + + self.log( "should not raise an error when checking ownership/access on anonymous' own dataset" ) + self.assertTrue( self.hda_mgr.is_accessible( self.trans, item1, anon_user ) ) + self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, anon_user ), item1 ) + + self.log( "should raise an error when checking ownership on anonymous' dataset with other user" ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + self.assertRaises( exceptions.ItemOwnershipException, + self.hda_mgr.error_unless_owner, self.trans, item1, non_owner ) + # ============================================================================= if __name__ == '__main__': From 579cef62f1fa2bfde02a3848372f4acd0b4084f0 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 7 Jan 2015 16:33:11 -0500 Subject: [PATCH 133/473] Update to brew_exts to latest platform-brew. --- lib/galaxy/tools/deps/brew_exts.py | 118 +++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 16 deletions(-) diff --git a/lib/galaxy/tools/deps/brew_exts.py b/lib/galaxy/tools/deps/brew_exts.py index fdf543b964a..763bac60697 100755 --- a/lib/galaxy/tools/deps/brew_exts.py +++ b/lib/galaxy/tools/deps/brew_exts.py @@ -28,6 +28,7 @@ import glob import os import re import sys +import string import subprocess WHITESPACE_PATTERN = re.compile("[\s]+") @@ -43,6 +44,7 @@ NO_BREW_ERROR_MESSAGE = "Could not find brew on PATH, please place on path or pa CANNOT_DETERMINE_TAP_ERROR_MESSAGE = "Cannot determine tap of specified recipe - please use fully qualified recipe (e.g. homebrew/science/samtools)." VERBOSE = False RELAXED = False +BREW_ARGS = [] class BrewContext(object): @@ -104,6 +106,7 @@ class RecipeContext(object): def main(): global VERBOSE global RELAXED + global BREW_ARGS parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument("--brew", help="Path to linuxbrew 'brew' executable to target") actions = ["vinstall", "vuninstall", "vdeps", "vinfo", "env"] @@ -114,11 +117,13 @@ def main(): parser.add_argument('version', metavar='version', help="Version for action (e.g. 0.1.19).") parser.add_argument('--relaxed', action='store_true', help="Relaxed processing - for instance allow use of env on non-vinstall-ed recipes.") parser.add_argument('--verbose', action='store_true', help="Verbose output") + parser.add_argument('restargs', nargs=argparse.REMAINDER) args = parser.parse_args() if args.verbose: VERBOSE = True if args.relaxed: RELAXED = True + BREW_ARGS = args.restargs if not action: action = args.action brew_context = BrewContext(args) @@ -159,7 +164,7 @@ class CommandLineException(Exception): return self.message -def versioned_install(recipe_context, package=None, version=None): +def versioned_install(recipe_context, package=None, version=None, installed_deps=[]): if package is None: package = recipe_context.recipe version = recipe_context.version @@ -176,10 +181,15 @@ def versioned_install(recipe_context, package=None, version=None): versioned = version_info[2] if versioned: dep_to_version[dep] = dep_version + if dep in installed_deps: + continue versioned_install(recipe_context, dep, dep_version) + installed_deps.append(dep) else: # Install latest. dep_to_version[dep] = None + if dep in installed_deps: + continue unversioned_install(dep) try: for dep in deps: @@ -198,7 +208,16 @@ def versioned_install(recipe_context, package=None, version=None): } deps_metadata.append(dep_metadata) - brew_execute(["install", package]) + cellar_root = recipe_context.brew_context.homebrew_cellar + cellar_path = recipe_context.cellar_path + env_actions = build_env_actions(deps_metadata, cellar_root, cellar_path, custom_only=True) + env = EnvAction.build_env(env_actions) + args = ["install"] + if VERBOSE: + args.append("--verbose") + args.extend(BREW_ARGS) + args.append(package) + brew_execute(args, env=env) deps = brew_execute(["deps", package]) deps = [d.strip() for d in deps.split("\n") if d] metadata = { @@ -278,10 +297,10 @@ def attempt_unlink(package): pass -def brew_execute(args): +def brew_execute(args, env=None): os.environ["HOMEBREW_NO_EMOJI"] = "1" # simplify brew parsing. cmds = ["brew"] + args - return execute(cmds) + return execute(cmds, env=env) def build_env_statements_from_recipe_context(recipe_context, **kwds): @@ -290,11 +309,20 @@ def build_env_statements_from_recipe_context(recipe_context, **kwds): return env_statements -def build_env_statements(cellar_root, cellar_path, relaxed=None): +def build_env_statements(cellar_root, cellar_path, relaxed=None, custom_only=False): deps = load_versioned_deps(cellar_path, relaxed=relaxed) + actions = build_env_actions(deps, cellar_root, cellar_path, relaxed, custom_only) + env_statements = [] + for action in actions: + env_statements.extend(action.to_statements()) + return "\n".join(env_statements) + + +def build_env_actions(deps, cellar_root, cellar_path, relaxed=None, custom_only=False): path_appends = [] ld_path_appends = [] + actions = [] def handle_keg(cellar_path): bin_path = os.path.join(cellar_path, "bin") @@ -303,6 +331,14 @@ def build_env_statements(cellar_root, cellar_path, relaxed=None): lib_path = os.path.join(cellar_path, "lib") if os.path.isdir(lib_path): ld_path_appends.append(lib_path) + env_path = os.path.join(cellar_path, "platform_environment.json") + if os.path.exists(env_path): + with open(env_path, "r") as f: + env_metadata = json.load(f) + if "actions" in env_metadata: + def to_action(desc): + return EnvAction(cellar_path, desc) + actions.extend(map(to_action, env_metadata["actions"])) for dep in deps: package = dep['name'] @@ -311,14 +347,54 @@ def build_env_statements(cellar_root, cellar_path, relaxed=None): handle_keg( dep_cellar_path ) handle_keg( cellar_path ) - env_statements = [] - if path_appends: - env_statements.append("PATH=" + ":".join(path_appends) + ":$PATH") - env_statements.append("export PATH") - if ld_path_appends: - env_statements.append("LD_LIBRARY_PATH=" + ":".join(ld_path_appends) + ":$LD_LIBRARY_PATH") - env_statements.append("export LD_LIBRARY_PATH") - return "\n".join(env_statements) + if not custom_only: + if path_appends: + actions.append(EnvAction(cellar_path, {"action": "prepend", "variable": "PATH", "value": ":".join(path_appends)})) + if ld_path_appends: + actions.append(EnvAction(cellar_path, {"action": "prepend", "variable": "LD_LIBRARY_PATH", "value": ":".join(path_appends)})) + return actions + + +class EnvAction(object): + + def __init__(self, keg_root, action_description): + self.variable = action_description["variable"] + self.action = action_description["action"] + self.value = string.Template(action_description["value"]).safe_substitute({ + 'KEG_ROOT': keg_root, + }) + + @staticmethod + def build_env(env_actions): + new_env = os.environ.copy() + map(lambda env_action: env_action.modify_environ(new_env), env_actions) + return new_env + + def modify_environ(self, environ): + if self.action == "set" or not environ.get(self.variable, ""): + environ[self.variable] = self.__eval("${value}") + elif self.action == "prepend": + environ[self.variable] = self.__eval("${value}:%s" % environ[self.variable]) + else: + environ[self.variable] = self.__eval("%s:${value}" % environ[self.variable]) + + def __eval(self, template): + return string.Template(template).safe_substitute( + variable=self.variable, + value=self.value, + ) + + def to_statements(self): + if self.action == "set": + template = '''${variable}="${value}"''' + elif self.action == "prepend": + template = '''${variable}="${value}:$$${variable}"''' + else: + template = '''${variable}="$$${variable}:${value}"''' + return [ + self.__eval(template), + "export %s" % self.variable + ] @contextlib.contextmanager @@ -350,8 +426,15 @@ def git_execute(args): return execute(cmds) -def execute(cmds): - p = subprocess.Popen(cmds, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +def execute(cmds, env=None): + subprocess_kwds = dict( + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if env: + subprocess_kwds["env"] = env + p = subprocess.Popen(cmds, **subprocess_kwds) #log = p.stdout.read() global VERBOSE stdout, stderr = p.communicate() @@ -363,7 +446,10 @@ def execute(cmds): def brew_deps(package): - stdout = brew_execute(["deps", package]) + args = ["deps"] + args.extend(BREW_ARGS) + args.append(package) + stdout = brew_execute(args) return [p.strip() for p in stdout.split("\n") if p] From f83ff130f2170aa4a1ec9edff0a420175aa7c154 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 7 Jan 2015 16:33:11 -0500 Subject: [PATCH 134/473] Implement dependency resolver for Homebrew "tapped" tool shed packages (highly beta). Homebrew/Linuxbrew can be configured the same way the more vanilla Homebrew resolver works (added in 6b7782f). When this resolver is active, if the tool has been installed via the Tool Shed and the dependencies also installed it will use the database information to determine the dependent packages - otherwise it will just try to find the tool_dependencies.xml file on disk and recover the dependencies from that. This is for dependencies installed via Platform Brew (https://github.com/jmchilton/platform-brew) and converted from Tool Shed repositories usiing the shed2tap WIP code (e.g. https://github.com/jmchilton/homebrew-toolshed). --- lib/galaxy/tools/__init__.py | 3 +- .../resolvers/brewed_tool_shed_packages.py | 150 ++++++++++++++++++ .../tools/deps/resolvers/galaxy_packages.py | 7 +- lib/galaxy/tools/deps/resolvers/homebrew.py | 50 +----- .../tools/deps/resolvers/resolver_mixins.py | 73 +++++++++ .../deps/resolvers/tool_shed_packages.py | 36 ++--- lib/galaxy/util/plugin_config.py | 2 +- 7 files changed, 249 insertions(+), 72 deletions(-) create mode 100644 lib/galaxy/tools/deps/resolvers/brewed_tool_shed_packages.py create mode 100644 lib/galaxy/tools/deps/resolvers/resolver_mixins.py diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 63409c2191b..2c44b60b5bf 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1843,7 +1843,8 @@ class Tool( object, Dictifiable ): """Return a list of commands to be run to populate the current environment to include this tools requirements.""" return self.app.toolbox.dependency_manager.dependency_shell_commands( self.requirements, - installed_tool_dependencies=self.installed_tool_dependencies + installed_tool_dependencies=self.installed_tool_dependencies, + tool_dir=self.tool_dir, ) @property diff --git a/lib/galaxy/tools/deps/resolvers/brewed_tool_shed_packages.py b/lib/galaxy/tools/deps/resolvers/brewed_tool_shed_packages.py new file mode 100644 index 00000000000..6a4cb989809 --- /dev/null +++ b/lib/galaxy/tools/deps/resolvers/brewed_tool_shed_packages.py @@ -0,0 +1,150 @@ +""" +This dependency resolver resolves tool shed dependencies (those defined +tool_dependencies.xml) installed using Platform Homebrew and converted +via shed2tap (e.g. https://github.com/jmchilton/homebrew-toolshed). +""" +import logging +import os +from xml.etree import ElementTree as ET + +from .resolver_mixins import ( + UsesHomebrewMixin, + UsesToolDependencyDirMixin, + UsesInstalledRepositoriesMixin, +) +from ..resolvers import DependencyResolver, INDETERMINATE_DEPENDENCY + +log = logging.getLogger(__name__) + + +class HomebrewToolShedDependencyResolver( + DependencyResolver, + UsesHomebrewMixin, + UsesToolDependencyDirMixin, + UsesInstalledRepositoriesMixin, +): + resolver_type = "tool_shed_tap" + + def __init__(self, dependency_manager, **kwds): + self._init_homebrew(**kwds) + self._init_base_path(dependency_manager, **kwds) + + def resolve(self, name, version, type, **kwds): + if type != "package": + return INDETERMINATE_DEPENDENCY + + if version is None: + return INDETERMINATE_DEPENDENCY + + return self._find_tool_dependencies(name, version, type, **kwds) + + def _find_tool_dependencies(self, name, version, type, **kwds): + installed_tool_dependency = self._get_installed_dependency(name, type, version=version, **kwds) + if installed_tool_dependency: + return self._resolve_from_installed_tool_dependency(name, version, installed_tool_dependency) + + if "tool_dir" in kwds: + tool_directory = os.path.abspath(kwds["tool_dir"]) + tool_depenedencies_path = os.path.join(tool_directory, "tool_dependencies.xml") + if os.path.exists(tool_depenedencies_path): + return self._resolve_from_tool_dependencies_path(name, version, tool_depenedencies_path) + + return INDETERMINATE_DEPENDENCY + + def _resolve_from_installed_tool_dependency(self, name, version, installed_tool_dependency): + tool_shed_repository = installed_tool_dependency.tool_shed_repository + recipe_name = build_recipe_name( + package_name=name, + package_version=version, + repository_owner=tool_shed_repository.owner, + repository_name=tool_shed_repository.name, + ) + return self._find_dep_default(recipe_name, None) + + def _resolve_from_tool_dependencies_path(self, name, version, tool_dependencies_path): + try: + raw_dependencies = RawDependencies(tool_dependencies_path) + except Exception: + log.debug("Failed to parse dependencies in file %s" % tool_dependencies_path) + return INDETERMINATE_DEPENDENCY + + raw_dependency = raw_dependencies.find(name, version) + if not raw_dependency: + return INDETERMINATE_DEPENDENCY + + recipe_name = build_recipe_name( + package_name=name, + package_version=version, + repository_owner=raw_dependency.repository_owner, + repository_name=raw_dependency.repository_name + ) + dep = self._find_dep_default(recipe_name, None) + return dep + + +class RawDependencies(object): + + def __init__(self, dependencies_file): + self.root = ET.parse(dependencies_file).getroot() + dependencies = [] + package_els = self.root.findall("package") or [] + for package_el in package_els: + repository_el = package_el.find("repository") + if repository_el is None: + continue + dependency = RawDependency(self, package_el, repository_el) + dependencies.append(dependency) + self.dependencies = dependencies + + def find(self, package_name, package_version): + target_dependency = None + + for dependency in self.dependencies: + if dependency.package_name == package_name and dependency.package_version == package_version: + target_dependency = dependency + break + return target_dependency + + +class RawDependency(object): + + def __init__(self, dependencies, package_el, repository_el): + self.dependencies = dependencies + self.package_el = package_el + self.repository_el = repository_el + + def __repr__(self): + temp = "Dependency[package_name=%s,version=%s,dependent_package=%s]" + return temp % ( + self.package_el.attrib["name"], + self.package_el.attrib["version"], + self.repository_el.attrib["name"] + ) + + @property + def repository_owner(self): + return self.repository_el.attrib["owner"] + + @property + def repository_name(self): + return self.repository_el.attrib["name"] + + @property + def package_name(self): + return self.package_el.attrib["name"] + + @property + def package_version(self): + return self.package_el.attrib["version"] + + +def build_recipe_name(package_name, package_version, repository_owner, repository_name): + # TODO: Consider baking package_name and package_version into name? (would be more "correct") + owner = repository_owner.replace("-", "") + name = repository_name + name = name.replace("_", "").replace("-", "") + base = "%s_%s" % (owner, name) + return base + + +__all__ = [HomebrewToolShedDependencyResolver] diff --git a/lib/galaxy/tools/deps/resolvers/galaxy_packages.py b/lib/galaxy/tools/deps/resolvers/galaxy_packages.py index 7f250479315..99eab3a6939 100644 --- a/lib/galaxy/tools/deps/resolvers/galaxy_packages.py +++ b/lib/galaxy/tools/deps/resolvers/galaxy_packages.py @@ -1,12 +1,13 @@ -from os.path import join, islink, realpath, basename, exists, abspath +from os.path import join, islink, realpath, basename, exists from ..resolvers import DependencyResolver, INDETERMINATE_DEPENDENCY, Dependency +from .resolver_mixins import UsesToolDependencyDirMixin import logging log = logging.getLogger( __name__ ) -class GalaxyPackageDependencyResolver(DependencyResolver): +class GalaxyPackageDependencyResolver(DependencyResolver, UsesToolDependencyDirMixin): resolver_type = "galaxy_packages" def __init__(self, dependency_manager, **kwds): @@ -16,7 +17,7 @@ class GalaxyPackageDependencyResolver(DependencyResolver): ## resolver that will just grab 'default' version of exact version ## unavailable. self.versionless = str(kwds.get('versionless', "false")).lower() == "true" - self.base_path = abspath( kwds.get('base_path', dependency_manager.default_base_path) ) + self._init_base_path( dependency_manager, **kwds ) def resolve( self, name, version, type, **kwds ): """ diff --git a/lib/galaxy/tools/deps/resolvers/homebrew.py b/lib/galaxy/tools/deps/resolvers/homebrew.py index 20f180a2928..53885041ebd 100644 --- a/lib/galaxy/tools/deps/resolvers/homebrew.py +++ b/lib/galaxy/tools/deps/resolvers/homebrew.py @@ -12,20 +12,19 @@ This is still an experimental module and there will almost certainly be backward incompatible changes coming. """ -import os -from ..brew_exts import DEFAULT_HOMEBREW_ROOT, recipe_cellar_path, build_env_statements -from ..resolvers import DependencyResolver, INDETERMINATE_DEPENDENCY, Dependency +from .resolver_mixins import UsesHomebrewMixin +from ..resolvers import DependencyResolver, INDETERMINATE_DEPENDENCY # TODO: Implement prefer version linked... PREFER_VERSION_LINKED = 'linked' PREFER_VERSION_LATEST = 'latest' -UNKNOWN_PREFER_VERSION_MESSAGE_TEMPLATE = "HomebrewDependencyResolver prefer_version must be latest %s" +UNKNOWN_PREFER_VERSION_MESSAGE_TEMPLATE = "HomebrewDependencyResolver prefer_version must be %s" UNKNOWN_PREFER_VERSION_MESSAGE = UNKNOWN_PREFER_VERSION_MESSAGE_TEMPLATE % (PREFER_VERSION_LATEST) DEFAULT_PREFER_VERSION = PREFER_VERSION_LATEST -class HomebrewDependencyResolver(DependencyResolver): +class HomebrewDependencyResolver(DependencyResolver, UsesHomebrewMixin): resolver_type = "homebrew" def __init__(self, dependency_manager, **kwds): @@ -38,11 +37,7 @@ class HomebrewDependencyResolver(DependencyResolver): if self.versionless and self.prefer_version not in [PREFER_VERSION_LATEST]: raise Exception(UNKNOWN_PREFER_VERSION_MESSAGE) - cellar_root = kwds.get('cellar', None) - if cellar_root is None: - cellar_root = os.path.join(DEFAULT_HOMEBREW_ROOT, "Cellar") - - self.cellar_root = cellar_root + self._init_homebrew(**kwds) def resolve(self, name, version, type, **kwds): if type != "package": @@ -53,41 +48,6 @@ class HomebrewDependencyResolver(DependencyResolver): else: return self._find_dep_versioned(name, version) - def _find_dep_versioned(self, name, version): - recipe_path = recipe_cellar_path(self.cellar_root, name, version) - if not os.path.exists(recipe_path) or not os.path.isdir(recipe_path): - return INDETERMINATE_DEPENDENCY - - commands = build_env_statements(self.cellar_root, recipe_path, relaxed=True) - return HomebrewDependency(commands) - - def _find_dep_default(self, name, version): - installed_versions = self._installed_versions(name) - if not installed_versions: - return INDETERMINATE_DEPENDENCY - - # Just grab newest installed version - may make sense some day to find - # the linked version instead. - default_version = sorted(installed_versions, reverse=True)[0] - return self._find_dep_versioned(name, default_version) - - def _installed_versions(self, recipe): - recipe_base_path = os.path.join(self.cellar_root, recipe) - if not os.path.exists(recipe_base_path): - return [] - - names = os.listdir(recipe_base_path) - return filter(lambda n: os.path.isdir(os.path.join(recipe_base_path, n)), names) - - -class HomebrewDependency(Dependency): - - def __init__(self, commands): - self.commands = commands - - def shell_commands(self, requirement): - return self.commands.replace("\n", ";") + "\n" - def _string_as_bool( value ): return str( value ).lower() == "true" diff --git a/lib/galaxy/tools/deps/resolvers/resolver_mixins.py b/lib/galaxy/tools/deps/resolvers/resolver_mixins.py new file mode 100644 index 00000000000..661d87ecaee --- /dev/null +++ b/lib/galaxy/tools/deps/resolvers/resolver_mixins.py @@ -0,0 +1,73 @@ +import os +from ..brew_exts import DEFAULT_HOMEBREW_ROOT, recipe_cellar_path, build_env_statements +from ..resolvers import INDETERMINATE_DEPENDENCY, Dependency + + +class UsesHomebrewMixin: + + def _init_homebrew(self, **kwds): + cellar_root = kwds.get('cellar', None) + if cellar_root is None: + cellar_root = os.path.join(DEFAULT_HOMEBREW_ROOT, "Cellar") + + self.cellar_root = cellar_root + + def _find_dep_versioned(self, name, version): + recipe_path = recipe_cellar_path(self.cellar_root, name, version) + if not os.path.exists(recipe_path) or not os.path.isdir(recipe_path): + return INDETERMINATE_DEPENDENCY + + commands = build_env_statements(self.cellar_root, recipe_path, relaxed=True) + return HomebrewDependency(commands) + + def _find_dep_default(self, name, version): + installed_versions = self._installed_versions(name) + if not installed_versions: + return INDETERMINATE_DEPENDENCY + + # Just grab newest installed version - may make sense some day to find + # the linked version instead. + default_version = sorted(installed_versions, reverse=True)[0] + return self._find_dep_versioned(name, default_version) + + def _installed_versions(self, recipe): + recipe_base_path = os.path.join(self.cellar_root, recipe) + if not os.path.exists(recipe_base_path): + return [] + + names = os.listdir(recipe_base_path) + return filter(lambda n: os.path.isdir(os.path.join(recipe_base_path, n)), names) + + +class UsesToolDependencyDirMixin: + + def _init_base_path(self, dependency_manager, **kwds): + self.base_path = os.path.abspath( kwds.get('base_path', dependency_manager.default_base_path) ) + + +class UsesInstalledRepositoriesMixin: + + def _get_installed_dependency( self, name, type, version=None, **kwds ): + installed_tool_dependencies = kwds.get("installed_tool_dependencies", []) + for installed_tool_dependency in (installed_tool_dependencies or []): + name_and_type_equal = installed_tool_dependency.name == name and installed_tool_dependency.type == type + if version: + if name_and_type_equal and installed_tool_dependency.version == version: + return installed_tool_dependency + else: + if name_and_type_equal: + return installed_tool_dependency + return None + + +class HomebrewDependency(Dependency): + + def __init__(self, commands): + self.commands = commands + + def shell_commands(self, requirement): + raw_commands = self.commands.replace("\n", ";") + return raw_commands + + def __repr__(self): + return "PlatformBrewDependency[commands=%s]" % self.commands diff --git a/lib/galaxy/tools/deps/resolvers/tool_shed_packages.py b/lib/galaxy/tools/deps/resolvers/tool_shed_packages.py index d72b4d6a20b..a58703bd6fb 100644 --- a/lib/galaxy/tools/deps/resolvers/tool_shed_packages.py +++ b/lib/galaxy/tools/deps/resolvers/tool_shed_packages.py @@ -1,10 +1,11 @@ from os.path import abspath, join, exists +from .resolver_mixins import UsesInstalledRepositoriesMixin from .galaxy_packages import GalaxyPackageDependencyResolver, GalaxyPackageDependency from ..resolvers import INDETERMINATE_DEPENDENCY -class ToolShedPackageDependencyResolver(GalaxyPackageDependencyResolver): +class ToolShedPackageDependencyResolver(GalaxyPackageDependencyResolver, UsesInstalledRepositoriesMixin): resolver_type = "tool_shed_packages" def __init__(self, dependency_manager, **kwds): @@ -12,9 +13,8 @@ class ToolShedPackageDependencyResolver(GalaxyPackageDependencyResolver): def _find_dep_versioned( self, name, version, type='package', **kwds ): installed_tool_dependency = self._get_installed_dependency( name, type, version=version, **kwds ) - base_path = self.base_path if installed_tool_dependency: - path = self._get_package_installed_dependency_path( installed_tool_dependency, base_path, name, version ) + path = self._get_package_installed_dependency_path( installed_tool_dependency, name, version ) return self._galaxy_package_dep(path, version) else: return INDETERMINATE_DEPENDENCY @@ -29,26 +29,17 @@ class ToolShedPackageDependencyResolver(GalaxyPackageDependencyResolver): return GalaxyPackageDependency(dependency.script, dependency.path, None) return INDETERMINATE_DEPENDENCY - def _get_installed_dependency( self, name, type, version=None, **kwds ): - installed_tool_dependencies = kwds.get("installed_tool_dependencies", []) - for installed_tool_dependency in (installed_tool_dependencies or []): - name_and_type_equal = installed_tool_dependency.name == name and installed_tool_dependency.type == type - if version: - if name_and_type_equal and installed_tool_dependency.version == version: - return installed_tool_dependency - else: - if name_and_type_equal: - return installed_tool_dependency - return None - - def _get_package_installed_dependency_path( self, installed_tool_dependency, base_path, name, version ): + def _get_package_installed_dependency_path( self, installed_tool_dependency, name, version ): tool_shed_repository = installed_tool_dependency.tool_shed_repository - return join( base_path, - name, - version, - tool_shed_repository.owner, - tool_shed_repository.name, - tool_shed_repository.installed_changeset_revision ) + base_path = self.base_path + return join( + base_path, + name, + version, + tool_shed_repository.owner, + tool_shed_repository.name, + tool_shed_repository.installed_changeset_revision + ) def _get_set_environment_installed_dependency_script_path( self, installed_tool_dependency, name ): tool_shed_repository = installed_tool_dependency.tool_shed_repository @@ -64,4 +55,5 @@ class ToolShedPackageDependencyResolver(GalaxyPackageDependencyResolver): return GalaxyPackageDependency(script, path, None) return INDETERMINATE_DEPENDENCY + __all__ = [ToolShedPackageDependencyResolver] diff --git a/lib/galaxy/util/plugin_config.py b/lib/galaxy/util/plugin_config.py index 8cff759d534..a3b6b9fb78b 100644 --- a/lib/galaxy/util/plugin_config.py +++ b/lib/galaxy/util/plugin_config.py @@ -24,7 +24,7 @@ def plugins_dict(module, plugin_type_identifier): for plugin_module in submodules( module ): # FIXME: this is not how one is suppose to use __all__ why did you do # this past John? - for clazz in plugin_module.__all__: + for clazz in getattr( plugin_module, "__all__", [] ): plugin_type = getattr( clazz, plugin_type_identifier, None ) if plugin_type: plugin_dict[ plugin_type ] = clazz From 2d5eb8aed932907cdae463a11e6fb1057545f871 Mon Sep 17 00:00:00 2001 From: Nicola Soranzo Date: Thu, 8 Jan 2015 13:00:50 +0000 Subject: [PATCH 135/473] PEP-8 and style fixes. --- lib/galaxy/webapps/galaxy/api/libraries.py | 39 ++++++++----------- .../webapps/galaxy/api/library_contents.py | 13 +++---- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/libraries.py b/lib/galaxy/webapps/galaxy/api/libraries.py index 97f1a41545c..aaf777707a1 100644 --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -6,10 +6,7 @@ from galaxy import exceptions from galaxy.managers import libraries, folders, roles from galaxy.web import _future_expose_api as expose_api from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous -from galaxy.model.orm import and_, not_, or_ from galaxy.web.base.controller import BaseAPIController -from sqlalchemy.orm.exc import MultipleResultsFound -from sqlalchemy.orm.exc import NoResultFound import logging log = logging.getLogger( __name__ ) @@ -48,7 +45,7 @@ class LibrariesController( BaseAPIController ): def __decode_id( self, trans, encoded_id, object_name=None ): """ - Try to decode the id. + Try to decode the id. :param object_name: Name of the object the id belongs to. (optional) :type object_name: str @@ -149,7 +146,7 @@ class LibrariesController( BaseAPIController ): synopsis = payload.get( 'synopsis', None ) else: raise exceptions.RequestParameterMissingException( "You did not specify any payload." ) - updated_library = self.library_manager.update( trans, library, name, description, synopsis ) + updated_library = self.library_manager.update( trans, library, name, description, synopsis ) library_dict = self.library_manager.get_library_dict( trans, updated_library ) return library_dict @@ -248,20 +245,20 @@ class LibrariesController( BaseAPIController ): *POST /api/libraries/{encoded_library_id}/permissions :param encoded_library_id: the encoded id of the library to set the permissions of - :type encoded_library_id: an encoded id string + :type encoded_library_id: an encoded id string :param action: (required) describes what action should be performed available actions: remove_restrictions, set_permissions - :type action: string + :type action: string :param access_ids[]: list of Role.id defining roles that should have access permission on the library - :type access_ids[]: string or list + :type access_ids[]: string or list :param add_ids[]: list of Role.id defining roles that should have add item permission on the library - :type add_ids[]: string or list + :type add_ids[]: string or list :param manage_ids[]: list of Role.id defining roles that should have manage permission on the library - :type manage_ids[]: string or list + :type manage_ids[]: string or list :param modify_ids[]: list of Role.id defining roles that should have modify permission on the library - :type modify_ids[]: string or list + :type modify_ids[]: string or list :rtype: dictionary :returns: dict of current roles for all available permission types @@ -306,7 +303,7 @@ class LibrariesController( BaseAPIController ): else: invalid_access_roles_names.append( role_id ) if len( invalid_access_roles_names ) > 0: - log.warning( "The following roles could not be added to the library access permission: " + str( invalid_access_roles_names ) ) + log.warning( "The following roles could not be added to the library access permission: " + str( invalid_access_roles_names ) ) # ADD TO LIBRARY ROLES valid_add_roles = [] @@ -319,8 +316,8 @@ class LibrariesController( BaseAPIController ): else: invalid_add_roles_names.append( role_id ) if len( invalid_add_roles_names ) > 0: - log.warning( "The following roles could not be added to the add library item permission: " + str( invalid_add_roles_names ) ) - + log.warning( "The following roles could not be added to the add library item permission: " + str( invalid_add_roles_names ) ) + # MANAGE LIBRARY ROLES valid_manage_roles = [] invalid_manage_roles_names = [] @@ -332,8 +329,8 @@ class LibrariesController( BaseAPIController ): else: invalid_manage_roles_names.append( role_id ) if len( invalid_manage_roles_names ) > 0: - log.warning( "The following roles could not be added to the manage library permission: " + str( invalid_manage_roles_names ) ) - + log.warning( "The following roles could not be added to the manage library permission: " + str( invalid_manage_roles_names ) ) + # MODIFY LIBRARY ROLES valid_modify_roles = [] invalid_modify_roles_names = [] @@ -345,7 +342,7 @@ class LibrariesController( BaseAPIController ): else: invalid_modify_roles_names.append( role_id ) if len( invalid_modify_roles_names ) > 0: - log.warning( "The following roles could not be added to the modify library permission: " + str( invalid_modify_roles_names ) ) + log.warning( "The following roles could not be added to the modify library permission: " + str( invalid_modify_roles_names ) ) permissions = { trans.app.security_agent.permitted_actions.LIBRARY_ACCESS : valid_access_roles } permissions.update( { trans.app.security_agent.permitted_actions.LIBRARY_ADD : valid_add_roles } ) @@ -357,7 +354,7 @@ class LibrariesController( BaseAPIController ): # Copy the permissions to the root folder trans.app.security_agent.copy_library_permissions( trans, library, library.root_folder ) else: - raise exceptions.RequestParameterInvalidException( 'The mandatory parameter "action" has an invalid value.' + raise exceptions.RequestParameterInvalidException( 'The mandatory parameter "action" has an invalid value.' 'Allowed values are: "remove_restrictions", set_permissions"' ) roles = self.library_manager.get_current_roles( trans, library ) return roles @@ -369,18 +366,16 @@ class LibrariesController( BaseAPIController ): POST /api/libraries/{encoded_library_id}/permissions Updates the library permissions. """ - import galaxy.util - params = galaxy.util.Params( payload ) + params = util.Params( payload ) permissions = {} for k, v in trans.app.model.Library.permitted_actions.items(): role_params = params.get( k + '_in', [] ) - in_roles = [ trans.sa_session.query( trans.app.model.Role ).get( trans.security.decode_id( x ) ) for x in galaxy.util.listify( role_params ) ] + in_roles = [ trans.sa_session.query( trans.app.model.Role ).get( trans.security.decode_id( x ) ) for x in util.listify( role_params ) ] permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles trans.app.security_agent.set_all_library_permissions( trans, library, permissions ) trans.sa_session.refresh( library ) # Copy the permissions to the root folder trans.app.security_agent.copy_library_permissions( trans, library, library.root_folder ) - message = "Permissions updated for library '%s'." % library.name item = library.to_dict( view='element', value_mapper={ 'id' : trans.security.encode_id , 'root_folder_id' : trans.security.encode_id } ) return item diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 1c01fbefaa0..6f1d4cf139d 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -119,7 +119,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar :func:`galaxy.model.LibraryDataset.to_dict` and :attr:`galaxy.model.LibraryFolder.dict_element_visible_keys` """ - class_name, content_id = self.__decode_library_content_id( trans, id ) + class_name, content_id = self.__decode_library_content_id( id ) if class_name == 'LibraryFolder': content = self.get_library_folder( trans, content_id, check_ownership=False, check_accessible=True ) rval = content.to_dict( view='element', value_mapper={ 'id': trans.security.encode_id } ) @@ -166,7 +166,6 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar :returns: a dictionary containing the id, name, and 'show' url of the new item """ - create_type = None if 'create_type' not in payload: trans.response.status = 400 return "Missing required 'create_type' parameter." @@ -178,10 +177,10 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar if 'folder_id' not in payload: trans.response.status = 400 - return "Missing requred 'folder_id' parameter." + return "Missing required 'folder_id' parameter." else: folder_id = payload.pop( 'folder_id' ) - class_name, folder_id = self.__decode_library_content_id( trans, folder_id ) + class_name, folder_id = self.__decode_library_content_id( folder_id ) try: # security is checked in the downstream controller parent = self.get_library_folder( trans, folder_id, check_ownership=False, check_accessible=False ) @@ -198,9 +197,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar #check for extended metadata, store it and pop it out of the param #otherwise sanitize_param will have a fit - ex_meta_payload = None - if 'extended_metadata' in payload: - ex_meta_payload = payload.pop('extended_metadata') + ex_meta_payload = payload.pop('extended_metadata', None) # Now create the desired content object, either file or folder. if create_type == 'file': @@ -344,7 +341,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar trans.sa_session.add( assoc ) trans.sa_session.flush() - def __decode_library_content_id( self, trans, content_id ): + def __decode_library_content_id( self, content_id ): if len( content_id ) % 16 == 0: return 'LibraryDataset', content_id elif content_id.startswith( 'F' ): From b40ca1c03cc813c87305e6a9ba7a608d273ebb44 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Thu, 8 Jan 2015 13:23:15 -0500 Subject: [PATCH 136/473] Minor fixes, cruft-removal; Exploring common dataset container interface for Histories, Collections, Libraries --- lib/galaxy/managers/collections.py | 6 +- lib/galaxy/managers/collections_util.py | 1 - lib/galaxy/managers/datasets.py | 3 + lib/galaxy/managers/hdas.py | 10 + lib/galaxy/managers/histories.py | 3 + lib/galaxy/web/base/controller.py | 32 +--- .../webapps/galaxy/api/dataset_collections.py | 3 +- .../webapps/galaxy/controllers/history.py | 6 +- test/unit/managers/test_CollectionManager.py | 178 ++++++++++++++++++ test/unit/managers/test_HDAManager.py | 8 +- 10 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 test/unit/managers/test_CollectionManager.py diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index a7091b98aa7..a326a01b295 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -38,7 +38,7 @@ class DatasetCollectionManager( object ): self.hda_manager = hdas.HDAManager( app ) self.history_manager = histories.HistoryManager( app ) - self.tag_manager = tags.TagsManager( app ) + self.tag_manager = tags.TagManager( app ) self.ldda_manager = lddas.LDDAManager( app ) def create( @@ -65,6 +65,7 @@ class DatasetCollectionManager( object ): element_identifiers=element_identifiers, elements=elements, ) + if isinstance( parent, model.History ): dataset_collection_instance = self.model.HistoryDatasetCollectionAssociation( collection=dataset_collection, @@ -78,15 +79,18 @@ class DatasetCollectionManager( object ): trans.sa_session.add( output_dataset ) dataset_collection_instance.implicit_output_name = implicit_collection_info[ "implicit_output_name" ] + log.debug("Created collection with %d elements" % ( len( dataset_collection_instance.collection.elements ) ) ) # Handle setting hid parent.add_dataset_collection( dataset_collection_instance ) + elif isinstance( parent, model.LibraryFolder ): dataset_collection_instance = self.model.LibraryDatasetCollectionAssociation( collection=dataset_collection, folder=parent, name=name, ) + else: message = "Internal logic error - create called with unknown parent type %s" % type( parent ) log.exception( message ) diff --git a/lib/galaxy/managers/collections_util.py b/lib/galaxy/managers/collections_util.py index e478f7baa42..236bf2a8a46 100644 --- a/lib/galaxy/managers/collections_util.py +++ b/lib/galaxy/managers/collections_util.py @@ -28,7 +28,6 @@ def api_payload_to_create_params( payload ): element_identifiers=payload.get( "element_identifiers" ), name=payload.get( "name", None ), ) - return params diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 7204189a76a..389acfd1b2b 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -14,7 +14,10 @@ log = logging.getLogger( __name__ ) # ============================================================================= class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): """ + Manipulate datasets: the components contained in DatasetAssociations/DatasetInstances/HDAs/LDDAs + """ + model_class = model.Dataset default_order_by = ( model.Dataset.create_time, ) foreign_key_name = 'dataset' diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index ac9e8eac6ce..2377ec6a8ea 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -50,11 +50,21 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface def create( self, trans, history=None, dataset=None, flush=True, **kwargs ): """ + Create a new hda optionally passing in it's history and dataset. + + ..note: to explicitly set hid to `None` you must pass in `hid=None`, otherwise + it will be automatically set. """ if not dataset: kwargs[ 'create_dataset' ] = True hda = super( HDAManager, self ).create( trans, flush=flush, history=history, dataset=dataset, sa_session=trans.sa_session, **kwargs ) + + if history: + set_hid = not ( 'hid' in kwargs ) + history.add_dataset( hda ) + #TODO:?? some internal sanity check here (or maybe in add_dataset) to make sure hids are not duped? + trans.sa_session.add( hda ) if flush: trans.sa_session.flush() diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index cb267485183..cde63b5ad81 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -102,6 +102,9 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface # Now mark the history as purged super( HistoryManager, self ).purge( trans, history, flush=flush, **kwargs ) + # ......................................................................... contents + + # ......................................................................... serialization #TODO: move to serializer (i.e. history with contents attr) def _get_history_data( self, trans, history ): diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index d8e2c548bcc..889d6356c8c 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -252,29 +252,11 @@ class UsesHistoryMixin( SharableItemSecurityMixin ): check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) history = self.security_check( trans, history, check_ownership, check_accessible ) return history - - def get_user_histories( self, trans, user=None, include_deleted=False, only_deleted=False ): - """ - Get all the histories for a given user (defaulting to `trans.user`) - ordered by update time and filtered on whether they've been deleted. - """ - # handle default and/or anonymous user (which still may not have a history yet) - user = user or trans.user - if not user: - current_history = trans.get_history() - return [ current_history ] if current_history else [] - - history_model = trans.model.History - query = ( trans.sa_session.query( history_model ) - .filter( history_model.user == user ) - .order_by( desc( history_model.table.c.update_time ) ) ) - if only_deleted: - query = query.filter( history_model.deleted == True ) - elif not include_deleted: - query = query.filter( history_model.deleted == False ) - - return query.all() - #USED IN: history.py, 640 +#lib/galaxy/webapps/galaxy/api/annotations.py:55: hist = self.get_history( trans, idstr ) +#lib/galaxy/webapps/galaxy/api/dataset_collections.py:44: history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) +#lib/galaxy/webapps/galaxy/api/workflows.py:152: history = self.get_history( trans, from_history_id, check_ownership=False, check_accessible=True ) +#lib/galaxy/webapps/galaxy/controllers/dataset.py:996: history = self.get_history(trans, source_history) +# and alll over history controller def get_history_datasets( self, trans, history, show_deleted=False, show_hidden=False, show_purged=False ): """ Returns history's datasets. """ @@ -289,7 +271,9 @@ class UsesHistoryMixin( SharableItemSecurityMixin ): if not show_purged: query = query.filter( trans.model.Dataset.purged == False ) return query.all() - +#lib/galaxy/tools/imp_exp/__init__.py:417: datasets = self.get_history_datasets( trans, history ) +#lib/galaxy/webapps/galaxy/controllers/history.py:1280: datasets = self.get_history_datasets( trans, history ) +#lib/galaxy/webapps/galaxy/controllers/page.py:742: datasets = self.get_history_datasets( trans, history ) class ExportsHistoryMixin: diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index cc04f5f22bf..7ce5ba7364a 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -52,7 +52,8 @@ class DatasetCollectionsController( trans.status = 501 return dataset_collection_instance = self.__service( trans ).create( trans=trans, **create_params ) - return dictify_dataset_collection_instance( dataset_collection_instance, security=trans.security, parent=create_params[ "parent" ] ) + return dictify_dataset_collection_instance( dataset_collection_instance, + security=trans.security, parent=create_params[ "parent" ] ) @expose_api def show( self, trans, instance_type, id, **kwds ): diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index e835b93f6cd..a5f8d9e5e56 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -640,11 +640,15 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI include_deleted_histories = galaxy.util.string_as_bool( include_deleted_histories ) order = order if order in ( 'update', 'name', 'size' ) else 'update' + deleted_filter = None + if not include_deleted_histories: + deleted_filter = model.History.deleted == False + current_history = trans.get_history() current_history_id = trans.security.encode_id( current_history.id ) if current_history else None history_dictionaries = [] - for history in self.get_user_histories( trans, user=trans.user, include_deleted=include_deleted_histories ): + for history in self.mgrs.histories.by_user( trans, trans.user, filters=deleted_filter ): history_dictionary = self.history_serializer.serialize_to_view( trans, history, view='detailed' ) history_dictionaries.append( history_dictionary ) diff --git a/test/unit/managers/test_CollectionManager.py b/test/unit/managers/test_CollectionManager.py new file mode 100644 index 00000000000..aff0af693dd --- /dev/null +++ b/test/unit/managers/test_CollectionManager.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +""" +""" +import sys +import os +import pprint +import unittest + +__GALAXY_ROOT__ = os.getcwd() + '/../../../' +sys.path.append( __GALAXY_ROOT__ + 'lib' ) + +from galaxy import eggs +eggs.require( 'SQLAlchemy >= 0.4' ) +import sqlalchemy + +from galaxy import model +from galaxy import exceptions + +import mock +from test_ModelManager import BaseTestCase +from galaxy.managers.datasets import DatasetManager +from galaxy.managers.histories import HistoryManager +from galaxy.managers.hdas import HDAManager + +from galaxy.managers.collections import DatasetCollectionManager + + +# ============================================================================= +default_password = '123456' +user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) +user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) + + +# ============================================================================= +class DatasetCollectionManagerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( DatasetCollectionManagerTestCase, self ).set_up_managers() + self.dataset_mgr = DatasetManager( self.app ) + self.hda_mgr = HDAManager( self.app ) + self.history_mgr = HistoryManager( self.app ) + self.collection_mgr = DatasetCollectionManager( self.app ) + + def build_element_identifiers( self, elements ): + identifier_list = [] + for element in elements: + src = 'hda' + #if isinstance( element, model.DatasetCollection ): + # src = 'collection'#? + #elif isinstance( element, model.LibraryDatasetDatasetAssociation ): + # src = 'ldda'#? + encoded_id = self.trans.security.encode_id( element.id ) + identifier_list.append( dict( src=src, name=element.name, id=encoded_id ) ) + return identifier_list + + def test_create_simple_list( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + + history = self.history_mgr.create( self.trans, name='history1', user=owner ) + + hda1 = self.hda_mgr.create( self.trans, name='one', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda2 = self.hda_mgr.create( self.trans, name='two', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda3 = self.hda_mgr.create( self.trans, name='three', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + + self.log( "should be able to create a new Collection via ids" ) + element_identifiers = self.build_element_identifiers( [ hda1, hda2, hda3 ] ) + hdca = self.collection_mgr.create( self.trans, history, 'test collection', 'list', + element_identifiers=element_identifiers ) + self.assertIsInstance( hdca, model.HistoryDatasetCollectionAssociation ) + self.assertEqual( hdca.name, 'test collection' ) + self.assertEqual( hdca.hid, 4 ) + self.assertFalse( hdca.deleted ) + self.assertTrue( hdca.visible ) + + #print 'hdca dir:' + #for k in dir( hdca ): + # print k, getattr( hdca, k, '(?)' ) + + self.log( "should contain an underlying, well-formed DatasetCollection" ) + self.assertIsInstance( hdca.collection, model.DatasetCollection ) + collection = hdca.collection + self.assertEqual( collection.collection_type, 'list' ) + self.assertEqual( collection.state, 'ok' ) + self.assertEqual( len( collection.dataset_instances ), 3 ) + self.assertEqual( len( collection.elements ), 3 ) + + #print 'hdca.collection dir:' + #for k in dir( hdca.collection ): + # print k, getattr( hdca.collection, k, '(?)' ) + + #elements = collection.elements + #print 'hdca.collection element dir:' + #for k in dir( elements[0] ): + # print k, getattr( elements[0], k, '(?)' ) + + self.log( "and that collection should have three well-formed Elements" ) + self.assertIsInstance( collection.elements[0], model.DatasetCollectionElement ) + self.assertEqual( collection.elements[0].element_identifier, 'one' ) + self.assertEqual( collection.elements[0].element_index, 0 ) + self.assertEqual( collection.elements[0].element_type, 'hda' ) + self.assertEqual( collection.elements[0].element_object, hda1 ) + + self.assertIsInstance( collection.elements[1], model.DatasetCollectionElement ) + self.assertEqual( collection.elements[1].element_identifier, 'two' ) + self.assertEqual( collection.elements[1].element_index, 1 ) + self.assertEqual( collection.elements[1].element_type, 'hda' ) + self.assertEqual( collection.elements[1].element_object, hda2 ) + + self.assertIsInstance( collection.elements[2], model.DatasetCollectionElement ) + self.assertEqual( collection.elements[2].element_identifier, 'three' ) + self.assertEqual( collection.elements[2].element_index, 2 ) + self.assertEqual( collection.elements[2].element_type, 'hda' ) + self.assertEqual( collection.elements[2].element_object, hda3 ) + + self.log( "should be able to create a new Collection via objects" ) + elements = dict( one=hda1, two=hda2, three=hda3 ) + hdca2 = self.collection_mgr.create( self.trans, history, 'test collection 2', 'list', elements=elements ) + self.assertIsInstance( hdca2, model.HistoryDatasetCollectionAssociation ) + + #def test_base( self ): + # dataset1 = self.dataset_mgr.create( self.trans ) + # dataset2 = self.dataset_mgr.create( self.trans ) + # + # self.log( "should be able to query" ) + # datasets = self.trans.sa_session.query( model.Dataset ).all() + # self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) + # self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) + # self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) + # self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) + # + # self.log( "should be able to limit and offset" ) + # self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) + # self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) + # self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) + # + # self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) + # self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) + # + # self.log( "should be able to order" ) + # self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), + # [ dataset2, dataset1 ] ) + # + #def test_delete( self ): + # item1 = self.dataset_mgr.create( self.trans ) + # + # self.log( "should be able to delete and undelete an hda" ) + # self.assertFalse( item1.deleted ) + # self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) + # self.assertTrue( item1.deleted ) + # self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) + # self.assertFalse( item1.deleted ) + # + #def test_purge_allowed( self ): + # self.trans.app.config.allow_user_dataset_purge = True + # item1 = self.dataset_mgr.create( self.trans ) + # + # self.log( "should purge an hda if config does allow" ) + # self.assertFalse( item1.purged ) + # self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) + # self.assertTrue( item1.purged ) + # + #def test_purge_not_allowed( self ): + # self.trans.app.config.allow_user_dataset_purge = False + # item1 = self.dataset_mgr.create( self.trans ) + # + # self.log( "should raise an error when purging an hda if config does not allow" ) + # self.assertFalse( item1.purged ) + # self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) + # self.assertFalse( item1.purged ) + + +# ============================================================================= +if __name__ == '__main__': + # or more generally, nosetests test_resourcemanagers.py -s -v + unittest.main() diff --git a/test/unit/managers/test_HDAManager.py b/test/unit/managers/test_HDAManager.py index 50e66db968b..b8391cbe9b6 100644 --- a/test/unit/managers/test_HDAManager.py +++ b/test/unit/managers/test_HDAManager.py @@ -71,7 +71,7 @@ class HDAManagerTestCase( BaseTestCase ): dataset1 = self.dataset_mgr.create( self.trans ) self.log( "should be able to create a new HDA with a specified history and dataset" ) - hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1, hid=1 ) + hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) self.assertIsInstance( hda1, model.HistoryDatasetAssociation ) self.assertEqual( hda1, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda1.id ) ) self.assertEqual( hda1.history, history1 ) @@ -79,18 +79,18 @@ class HDAManagerTestCase( BaseTestCase ): self.assertEqual( hda1.hid, 1 ) self.log( "should be able to create a new HDA with only a specified history and no dataset" ) - hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) + hda2 = self.hda_mgr.create( self.trans, history=history1 ) self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) self.assertIsInstance( hda2.dataset, model.Dataset ) self.assertEqual( hda2.history, history1 ) self.assertEqual( hda2.hid, 2 ) self.log( "should be able to create a new HDA with no history and no dataset" ) - hda3 = self.hda_mgr.create( self.trans ) + hda3 = self.hda_mgr.create( self.trans, hid=None ) self.assertIsInstance( hda3, model.HistoryDatasetAssociation ) self.assertIsInstance( hda3.dataset, model.Dataset, msg="dataset will be auto created" ) self.assertIsNone( hda3.history, msg="history will be None" ) - self.assertEqual( hda3.hid, None ) + self.assertEqual( hda3.hid, None, msg="should allow setting hid to None (or any other value)" ) def test_copy_from_hda( self ): owner = self.user_mgr.create( self.trans, **user2_data ) From 594daffc513dfc9f1fc3a19a8163e24c656216c3 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Thu, 8 Jan 2015 16:23:51 -0500 Subject: [PATCH 137/473] Unit test the collections manager; tweaks to user_shares in sharable --- lib/galaxy/managers/base.py | 22 ++++- lib/galaxy/managers/histories.py | 2 +- lib/galaxy/managers/sharable.py | 50 ++++++++---- test/unit/managers/test_CollectionManager.py | 85 ++++++++------------ test/unit/managers/test_HistoryManager.py | 26 +++--- 5 files changed, 106 insertions(+), 79 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 37f64f84a02..83ee9c92584 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -220,19 +220,37 @@ class ModelManager( object ): def one( self, trans, **kwargs ): """ Sends kwargs to build the query and returns one and only one model. + """ + query = self.query( trans, **kwargs ) + return self._one_with_recast_errors( query ) + + def _one_with_recast_errors( self, query ): + """ + Call sqlalchemy's one and recast errors to serializable errors if any. :raises exceptions.ObjectNotFound: if no model is found :raises exceptions.Conflict: if more than one model is found """ # overridden to raise serializable errors try: - return self.query( trans, **kwargs ).one() + return query.one() except sqlalchemy.orm.exc.NoResultFound, not_found: raise exceptions.ObjectNotFound( self.model_class.__name__ + ' not found' ) #TODO: exceptions.InconsistentDatabase? except sqlalchemy.orm.exc.MultipleResultsFound, multi_found: raise exceptions.Conflict( 'found more than one ' + self.model_class.__name__ ) + def _one_or_none( self, query ): + """ + Return the object if found, None if it's not. + + :raises exceptions.Conflict: if more than one model is found + """ + try: + return self._one_with_recast_errors( query ) + except exceptions.ObjectNotFound, not_found: + return None + def list( self, trans, query=None, **kwargs ): """ Sends kwargs to build the query return all models found. @@ -345,7 +363,7 @@ class ModelManager( object ): # ------------------------------------------------------------------------- delete # a rename of sql DELETE to differentiate from the Galaxy notion of mark_as_deleted - #def remove_from_db( self, trans, item, **kwargs ): + #def destroy( self, trans, item, **kwargs ): # return item diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index cde63b5ad81..769119ac1a3 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -92,7 +92,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface def purge( self, trans, history, flush=True, **kwargs ): """ """ - self.error_unless_dataset_purge_allowed( trans, history ) + self.hda_mgr.dataset_mgr.error_unless_dataset_purge_allowed( trans, history ) # First purge all the datasets for hda in history.datasets: diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index a648459cd99..fa4cedb1fb5 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -35,6 +35,8 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # ......................................................................... has a user def _query_by_user( self, trans, user, filters=None, **kwargs ): """ + Return query for all items (of model_class type) associated with the given + `user`. """ user_filter = self.model_class.user_id == user.id filters=self._munge_filters( user_filter, filters ) @@ -42,6 +44,8 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. def by_user( self, trans, user, **kwargs ): """ + Return list for all items (of model_class type) associated with the given + `user`. """ query = self._query_by_user( trans, user, **kwargs ) return self.list( trans, query=query, **kwargs ) @@ -171,6 +175,9 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # sharing is often done via a 3rd table btwn a User and an item -> a UserShareAssociation def get_share_assocs( self, trans, item, user=None ): """ + Get the UserShareAssociations for the `item`. + + Optionally send in `user` to test for a single match. """ query = self.query_associated( trans, self.user_share_model, item ) if user is not None: @@ -179,31 +186,45 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. def share_with( self, trans, item, user, flush=True ): """ + Get or create a share for the given user (or users if `user` is a list). """ # allow user to be a list and call recursivly if isinstance( user, list ): return map( lambda user: self.share_with( trans, item, user, flush=False ), user ) + # get or create + existing = self.get_share_assocs( trans, item, user=user ) + if existing: + return existing.pop( 0 ) + return self._create_user_share_assoc( trans, item, user, flush=flush ) - share = self.user_share_model() - trans.sa_session.add( share ) - self.associate( trans, share, item ) - share.user = user + def _create_user_share_assoc( self, trans, item, user, flush=True ): + """ + Create a share for the given user. + """ + user_share_assoc = self.user_share_model() + trans.sa_session.add( user_share_assoc ) + self.associate( trans, user_share_assoc, item ) + user_share_assoc.user = user + + # waat? self.create_unique_slug( trans, item ) + if flush: trans.sa_session.flush() - return item + return user_share_assoc def unshare_with( self, trans, item, user, flush=True ): """ + Delete a user share (or list of shares) from the database. """ if isinstance( user, list ): return map( lambda user: self.unshare_with( trans, item, user, flush=False ), user ) - # Look for and delete sharing relation for history-user. - for assoc in self.get_share_assocs( trans, item, user=user ): - trans.sa_session.delete( assoc ) + # Look for and delete sharing relation for user. + user_share_assoc = self.get_share_assocs( trans, item, user=user )[0] + trans.sa_session.delete( user_share_assoc ) if flush: trans.sa_session.flush() - return item + return user_share_assoc #def _query_shared_with( self, trans, user, filters=None, **kwargs ): ##TODO: @@ -211,7 +232,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # """ # pass - #def list_published( self, trans, user, **kwargs ): + #def list_shared_with( self, trans, user, **kwargs ): # """ # """ # query = self._query_shared_with( trans, user, **kwargs ) @@ -286,6 +307,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. def create_unique_slug( self, trans, item, flush=True ): """ + Set a new, unique slug on the item. """ item.slug = self.get_unique_slug( trans, item ) trans.sa_session.add( item ) @@ -293,10 +315,10 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. trans.sa_session.flush() return item - def by_slug( self, trans, user, **kwargs ): - """ - """ - pass + #def by_slug( self, trans, user, **kwargs ): + # """ + # """ + # pass # ......................................................................... display #def display_by_username_and_slug( self, trans, username, slug ): diff --git a/test/unit/managers/test_CollectionManager.py b/test/unit/managers/test_CollectionManager.py index aff0af693dd..aaf918614ad 100644 --- a/test/unit/managers/test_CollectionManager.py +++ b/test/unit/managers/test_CollectionManager.py @@ -120,56 +120,41 @@ class DatasetCollectionManagerTestCase( BaseTestCase ): hdca2 = self.collection_mgr.create( self.trans, history, 'test collection 2', 'list', elements=elements ) self.assertIsInstance( hdca2, model.HistoryDatasetCollectionAssociation ) - #def test_base( self ): - # dataset1 = self.dataset_mgr.create( self.trans ) - # dataset2 = self.dataset_mgr.create( self.trans ) - # - # self.log( "should be able to query" ) - # datasets = self.trans.sa_session.query( model.Dataset ).all() - # self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) - # self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) - # self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) - # self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) - # - # self.log( "should be able to limit and offset" ) - # self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) - # self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) - # self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) - # - # self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) - # self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) - # - # self.log( "should be able to order" ) - # self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), - # [ dataset2, dataset1 ] ) - # - #def test_delete( self ): - # item1 = self.dataset_mgr.create( self.trans ) - # - # self.log( "should be able to delete and undelete an hda" ) - # self.assertFalse( item1.deleted ) - # self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) - # self.assertTrue( item1.deleted ) - # self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) - # self.assertFalse( item1.deleted ) - # - #def test_purge_allowed( self ): - # self.trans.app.config.allow_user_dataset_purge = True - # item1 = self.dataset_mgr.create( self.trans ) - # - # self.log( "should purge an hda if config does allow" ) - # self.assertFalse( item1.purged ) - # self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) - # self.assertTrue( item1.purged ) - # - #def test_purge_not_allowed( self ): - # self.trans.app.config.allow_user_dataset_purge = False - # item1 = self.dataset_mgr.create( self.trans ) - # - # self.log( "should raise an error when purging an hda if config does not allow" ) - # self.assertFalse( item1.purged ) - # self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) - # self.assertFalse( item1.purged ) + def test_update_from_dict( self ): + owner = self.user_mgr.create( self.trans, **user2_data ) + + history = self.history_mgr.create( self.trans, name='history1', user=owner ) + + hda1 = self.hda_mgr.create( self.trans, name='one', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda2 = self.hda_mgr.create( self.trans, name='two', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda3 = self.hda_mgr.create( self.trans, name='three', + history=history, dataset=self.dataset_mgr.create( self.trans ) ) + + elements = dict( one=hda1, two=hda2, three=hda3 ) + hdca = self.collection_mgr.create( self.trans, history, 'test collection', 'list', elements=elements ) + + self.log( "should be set from a dictionary" ) + self.collection_mgr._set_from_dict( self.trans, hdca, { + 'deleted' : True, + 'visible' : False, + 'name' : 'New Name', + #TODO: doesn't work + #'tags' : [ 'one', 'two', 'three' ] + #'annotations' : [?] + }) + self.assertEqual( hdca.name, 'New Name' ) + self.assertTrue( hdca.deleted ) + self.assertFalse( hdca.visible ) + #self.assertEqual( hdca.tags, [ 'one', 'two', 'three' ] ) + #self.assertEqual( hdca.annotations, [ 'one', 'two', 'three' ] ) + + #def test_validation( self ): + # self.log( "should be able to change the name" ) + # self.log( "should be able to set deleted" ) + # self.log( "should be able to set visible" ) + # self.log( "should be able to set tags" ) # ============================================================================= diff --git a/test/unit/managers/test_HistoryManager.py b/test/unit/managers/test_HistoryManager.py index 4147e3d38ac..de40a433e4d 100644 --- a/test/unit/managers/test_HistoryManager.py +++ b/test/unit/managers/test_HistoryManager.py @@ -26,6 +26,7 @@ from galaxy.managers.histories import HistoryManager default_password = '123456' user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) +user4_data = dict( email='user4@user4.user4', username='user4', password=default_password ) # ============================================================================= @@ -213,29 +214,30 @@ class HistoryManagerTestCase( BaseTestCase ): def test_sharable( self ): owner = self.user_mgr.create( self.trans, **user2_data ) self.trans.set_user( owner ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) + non_owner = self.user_mgr.create( self.trans, **user3_data ) + #third_party = self.user_mgr.create( self.trans, **user4_data ) + self.log( "should be unshared by default" ) self.assertEqual( self.history_mgr.get_share_assocs( self.trans, item1 ), [] ) self.log( "should be able to share with specific users" ) - shared_item = self.history_mgr.share_with( self.trans, item1, non_owner ) - self.assertEqual( shared_item, item1 ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, shared_item, non_owner ) ) + share_assoc = self.history_mgr.share_with( self.trans, item1, non_owner ) + self.assertIsInstance( share_assoc, model.HistoryUserShareAssociation ) + self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) self.assertEqual( - len( self.history_mgr.get_share_assocs( self.trans, shared_item ) ), 1 ) + len( self.history_mgr.get_share_assocs( self.trans, item1 ) ), 1 ) self.assertEqual( - len( self.history_mgr.get_share_assocs( self.trans, shared_item, user=non_owner ) ), 1 ) + len( self.history_mgr.get_share_assocs( self.trans, item1, user=non_owner ) ), 1 ) self.log( "should be able to unshare with specific users" ) - unshared_item = self.history_mgr.unshare_with( self.trans, shared_item, non_owner ) - self.assertEqual( shared_item, unshared_item ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, shared_item, non_owner ) ) - self.assertEqual( self.history_mgr.get_share_assocs( self.trans, unshared_item ), [] ) + share_assoc = self.history_mgr.unshare_with( self.trans, item1, non_owner ) + self.assertIsInstance( share_assoc, model.HistoryUserShareAssociation ) + self.assertFalse( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) + self.assertEqual( self.history_mgr.get_share_assocs( self.trans, item1 ), [] ) self.assertEqual( - self.history_mgr.get_share_assocs( self.trans, unshared_item, user=non_owner ), [] ) + self.history_mgr.get_share_assocs( self.trans, item1, user=non_owner ), [] ) #TODO: test slug formation From fac23b2a1ae9d910a85d924065979c7d5c5e17fd Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 9 Jan 2015 13:22:06 -0500 Subject: [PATCH 138/473] History Mgr: eliminate the UsesHistoryMixin; HDA Mgr: move serveral methods into mgr from UsesHDAMixin; User Mgr: improve api generation/checking --- lib/galaxy/managers/base.py | 4 +- lib/galaxy/managers/collections.py | 2 - lib/galaxy/managers/datasets.py | 63 +++-- lib/galaxy/managers/hdas.py | 256 ++++++------------ lib/galaxy/managers/histories.py | 2 +- lib/galaxy/managers/users.py | 63 ++++- lib/galaxy/tools/imp_exp/__init__.py | 23 +- lib/galaxy/web/base/controller.py | 182 +------------ lib/galaxy/webapps/galaxy/api/annotations.py | 15 +- .../webapps/galaxy/api/dataset_collections.py | 10 +- lib/galaxy/webapps/galaxy/api/datasets.py | 19 +- .../webapps/galaxy/api/extended_metadata.py | 4 +- .../webapps/galaxy/api/history_contents.py | 6 +- lib/galaxy/webapps/galaxy/api/provenance.py | 4 +- lib/galaxy/webapps/galaxy/api/tools.py | 3 +- lib/galaxy/webapps/galaxy/api/workflows.py | 9 +- .../webapps/galaxy/controllers/dataset.py | 14 +- .../webapps/galaxy/controllers/history.py | 71 +++-- lib/galaxy/webapps/galaxy/controllers/page.py | 13 +- lib/galaxy/webapps/galaxy/controllers/root.py | 4 +- templates/tagging_common.mako | 2 +- templates/webapps/galaxy/history/embed.mako | 3 +- test/unit/managers/test_UserManager.py | 16 +- 23 files changed, 315 insertions(+), 473 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 83ee9c92584..0280d911bf9 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -803,9 +803,9 @@ class ModelDeserializer( object ): user = trans.user #TODO: duped from tags manager - de-dupe when moved to taggable mixin tag_handler = trans.app.tag_handler - tag_handler.delete_item_tags( trans, user, item ) + tag_handler.delete_item_tags( user, item ) new_tags_str = ','.join( new_tags_list ) - tag_handler.apply_item_tags( trans, user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) ) + tag_handler.apply_item_tags( user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) ) #trans.sa_session.flush() #TODO:!! does the creation of new_tags_list mean there are now more and more unused tag rows in the db? diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index a326a01b295..4cd6265ccf4 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -160,8 +160,6 @@ class DatasetCollectionManager( object ): return source_hdca def _set_from_dict( self, trans, dataset_collection_instance, new_data ): - # Blatantly stolen from UsesHistoryDatasetAssociationMixin.set_hda_from_dict. - # send what we can down into the model changed = dataset_collection_instance.set_from_dict( new_data ) # the rest (often involving the trans) - do here diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 389acfd1b2b..cfce86e96ea 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -15,7 +15,6 @@ log = logging.getLogger( __name__ ) class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): """ Manipulate datasets: the components contained in DatasetAssociations/DatasetInstances/HDAs/LDDAs - """ model_class = model.Dataset @@ -23,14 +22,14 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur foreign_key_name = 'dataset' def __init__( self, app ): - """ - """ super( DatasetManager, self ).__init__( app ) + # need for admin test self.user_mgr = users.UserManager( app ) # ......................................................................... create def create( self, trans, flush=True, **kwargs ): """ + Create and return a new Dataset object. """ # default to NEW state on new datasets kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) ) @@ -41,23 +40,28 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur trans.sa_session.flush() return dataset - def copy( self ): - """ - """ - pass + #def copy( self, dataset, **kwargs ): + # """ + # Clone, update, and return the given dataset. + # """ + # pass # ......................................................................... add to dataset association - def to_hda( self, trans, dataset, history, **kwargs ): - """ - """ - pass + #def to_hda( self, trans, dataset, history, **kwargs ): + # """ + # Create an hda from this dataset. + # """ + # pass - def to_ldda( self, trans, dataset, library_folder, **kwargs ): - """ - """ - pass + #def to_ldda( self, trans, dataset, library_folder, **kwargs ): + # """ + # Create an ldda from this dataset. + # """ + # pass # ......................................................................... delete + #TODO: this may be more conv. somewhere else +#TODO: allow admin bypass def error_unless_dataset_purge_allowed( self, trans, item, msg=None ): if not trans.app.config.allow_user_dataset_purge: msg = msg or 'This instance does not allow user dataset purging' @@ -66,6 +70,10 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur def purge( self, trans, dataset, flush=True ): """ + Remove the object_store/file for this dataset from storage and mark + as purged. + + :raises exceptions.ConfigDoesNotAllowException: if the instance doesn't allow """ self.error_unless_dataset_purge_allowed( trans, dataset ) @@ -81,6 +89,7 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # than those resources that have a user attribute (histories, pages, etc.) def is_accessible( self, trans, dataset, user ): """ + Is this dataset readable/viewable to user? """ if self.user_mgr.is_admin( trans, user ): return True @@ -88,8 +97,17 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur return True return False + def has_access_permission( self, trans, dataset, user ): + """ + Return T/F if the user has role-based access to the dataset. + """ + roles = user.all_roles() if user else [] + return trans.app.security_agent.can_access_dataset( roles, dataset ) + +#TODO: these need work def _access_permission( self, trans, dataset, user=None, role=None ): """ + Return most recent DatasetPermissions for the dataset and user. """ security_agent = trans.app.security_agent access_action = security_agent.permitted_actions.DATASET_ACCESS.action @@ -101,13 +119,12 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur .filter( model.DatasetPermissions.action == access_action ) .filter( model.DatasetPermissions.dataset == dataset ) .filter( model.DatasetPermissions.role_id.in_( user_role_ids ) ) ) - perms = query.all() - if len( perms ): - return perms[0] - return None + #TODO:?? most recent? + return query.first() def _create_access_permission( self, trans, dataset, role, flush=True ): """ + Create a new DatasetPermissions for the given dataset and role. """ access_action = trans.app.security_agent.permitted_actions.DATASET_ACCESS.action permission = model.DatasetPermissions( access_action, dataset, role ) @@ -116,14 +133,6 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur trans.sa_session.flush() return permission - def has_access_permission( self, trans, dataset, user ): - """ - """ - - roles = user.all_roles() if user else [] - return trans.app.security_agent.can_access_dataset( roles, dataset ) - - #TODO: this needs work #def give_access_permission( self, trans, dataset, user, flush=True ): # """ # """ diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 2377ec6a8ea..f5465950b5c 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -70,8 +70,9 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface trans.sa_session.flush() return hda - def copy_hda( self, trans, hda, history=None, **kwargs ): + def copy( self, trans, hda, history=None, **kwargs ): """ + Copy and return the given HDA. """ #TODO:?? not using the following as this fn does not set history and COPIES hid (this doesn't seem correct) #return hda.copy() @@ -95,6 +96,8 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface copy.copied_from_history_dataset_association = hda copy.set_size() + #TODO: update from kwargs? + # Need to set after flushed, as MetadataFiles require dataset.id trans.sa_session.add( copy ) trans.sa_session.flush() @@ -151,66 +154,6 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface return hda # ......................................................................... serialization - #TODO: move into serializer below - #def get_hda_dict( self, trans, hda ): - # """ - # Return full details of this HDA in dictionary form. - # """ - # #precondition: the user's access to this hda has already been checked - # #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) - # expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path - # hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) - # hda_dict[ 'api_type' ] = "file" - # - # # Add additional attributes that depend on trans must be added here rather than at the model level. - # can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) - # can_access_hda = ( trans.user_is_admin() or can_access_hda ) - # if not can_access_hda: - # return self.get_inaccessible_hda_dict( trans, hda ) - # hda_dict[ 'accessible' ] = True - # - # #TODO: I'm unclear as to which access pattern is right - # hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda ) - # #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) - # - # # ---- return here if deleted AND purged OR can't access - # purged = ( hda.purged or hda.dataset.purged ) - # if ( hda.deleted and purged ): - # #TODO: to_dict should really go AFTER this - only summary data - # return trans.security.encode_dict_ids( hda_dict ) - # - # if expose_dataset_path: - # try: - # hda_dict[ 'file_name' ] = hda.file_name - # except objectstore.ObjectNotFound: - # log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) - # - # hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display', - # history_id = trans.security.encode_id( hda.history.id ), - # history_content_id = trans.security.encode_id( hda.id ) ) - # - # # indeces, assoc. metadata files, etc. - # meta_files = [] - # for meta_type in hda.metadata.spec.keys(): - # if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): - # meta_files.append( dict( file_type=meta_type ) ) - # if meta_files: - # hda_dict[ 'meta_files' ] = meta_files - # - # # currently, the viz reg is optional - handle on/off - # if trans.app.visualizations_registry: - # hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) - # else: - # hda_dict[ 'visualizations' ] = hda.get_visualizations() - # #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations - # # when the visualizations button is clicked (instead of preloading/pre-checking) - # - # # ---- return here if deleted - # if hda.deleted and not purged: - # return trans.security.encode_dict_ids( hda_dict ) - # - # return trans.security.encode_dict_ids( hda_dict ) - def get_display_apps( self, trans, hda ): """ Return dictionary containing new-style display app urls. @@ -302,70 +245,7 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface # return trans.show_error_message( "Please wait until this dataset finishes uploading " # + "before attempting to view it." ) # return data - # - #def get_history_dataset_association( self, trans, history, dataset_id, - # check_ownership=True, check_accessible=False, check_state=False ): - # """ - # Get a HistoryDatasetAssociation from the database by id, verifying ownership. - # """ - # #TODO: duplicate of above? alias to above (or vis-versa) - # self.security_check( trans, history, check_ownership=check_ownership, check_accessible=check_accessible ) - # hda = self.get_object( trans, dataset_id, 'HistoryDatasetAssociation', - # check_ownership=False, check_accessible=False ) - # - # if check_accessible: - # if( not trans.user_is_admin() - # and not trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) ): - # error( "You are not allowed to access this dataset" ) - # - # if check_state and hda.state == trans.model.Dataset.states.UPLOAD: - # error( "Please wait until this dataset finishes uploading before attempting to view it." ) - # return hda - # - #def get_history_dataset_association_from_ids( self, trans, id, history_id ): - # # Just to echo other TODOs, there seems to be some overlap here, still - # # this block appears multiple places (dataset show, history_contents - # # show, upcoming history job show) so I am consolodating it here. - # # Someone smarter than me should determine if there is some redundancy here. - # - # # for anon users: - # #TODO: check login_required? - # #TODO: this isn't actually most_recently_used (as defined in histories) - # if( ( trans.user == None ) - # and ( history_id == trans.security.encode_id( trans.history.id ) ) ): - # history = trans.history - # #TODO: dataset/hda by id (from history) OR check_ownership for anon user - # hda = self.get_history_dataset_association( trans, history, id, - # check_ownership=False, check_accessible=True ) - # else: - # #TODO: do we really need the history? - # history = self.get_history( trans, history_id, - # check_ownership=False, check_accessible=True, deleted=False ) - # hda = self.get_history_dataset_association( trans, history, id, - # check_ownership=False, check_accessible=True ) - # return hda - # - #def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ): - # """ - # Returns one or more datasets in a list. - # - # If a dataset is not found or is inaccessible to trans.user, - # add None in its place in the list. - # """ - # # precondtion: dataset_ids is a list of encoded id strings - # hdas = [] - # for id in hda_ids: - # hda = None - # try: - # hda = self.get_dataset( trans, id, - # check_ownership=check_ownership, - # check_accessible=check_accessible, - # check_state=check_state ) - # except Exception: - # pass - # hdas.append( hda ) - # return hdas - # + #def get_data( self, dataset, preview=True ): # """ # Gets a dataset's data. @@ -399,49 +279,6 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface # return dataset.conversion_messages.PENDING # return None - #def get_inaccessible_hda_dict( self, trans, hda ): - # return trans.security.encode_dict_ids({ - # 'id' : hda.id, - # 'history_id': hda.history.id, - # 'hid' : hda.hid, - # 'name' : hda.name, - # 'state' : hda.state, - # 'deleted' : hda.deleted, - # 'visible' : hda.visible, - # 'accessible': False - # }) - - #def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): - # return trans.security.encode_dict_ids({ - # 'id' : hda.id if hda else id, - # 'history_id': hda.history.id if hda else history_id, - # 'hid' : hda.hid if hda else '(unknown)', - # 'name' : hda.name if hda else '(unknown)', - # 'error' : error_msg, - # 'state' : trans.model.Dataset.states.NEW - # }) - - #def set_hda_from_dict( self, trans, hda, new_data ): - # """ - # Changes HDA data using the given dictionary new_data. - # """ - # # precondition: access of the hda has already been checked - # - # # send what we can down into the model - # changed = hda.set_from_dict( new_data ) - # # the rest (often involving the trans) - do here - # if 'annotation' in new_data.keys() and trans.get_user(): - # hda.add_item_annotation( trans.sa_session, trans.get_user(), hda, new_data[ 'annotation' ] ) - # changed[ 'annotation' ] = new_data[ 'annotation' ] - # if 'tags' in new_data.keys() and trans.get_user(): - # self.set_tags_from_list( trans, hda, new_data[ 'tags' ], user=trans.user ) - # # sharing/permissions? - # # purged - # - # if changed.keys(): - # trans.sa_session.flush() - # - # return changed #def get_hda_job( self, hda ): # # Get dataset's job. @@ -718,6 +555,89 @@ class HDASerializer( base.ModelSerializer ): return urls + #TODO: move into serializer below + #def get_hda_dict( self, trans, hda ): + # """ + # Return full details of this HDA in dictionary form. + # """ + # #precondition: the user's access to this hda has already been checked + # #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) + # expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path + # hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) + # hda_dict[ 'api_type' ] = "file" + # + # # Add additional attributes that depend on trans must be added here rather than at the model level. + # can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) + # can_access_hda = ( trans.user_is_admin() or can_access_hda ) + # if not can_access_hda: + # return self.get_inaccessible_hda_dict( trans, hda ) + # hda_dict[ 'accessible' ] = True + # + # #TODO: I'm unclear as to which access pattern is right + # hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda ) + # #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) + # + # # ---- return here if deleted AND purged OR can't access + # purged = ( hda.purged or hda.dataset.purged ) + # if ( hda.deleted and purged ): + # #TODO: to_dict should really go AFTER this - only summary data + # return trans.security.encode_dict_ids( hda_dict ) + # + # if expose_dataset_path: + # try: + # hda_dict[ 'file_name' ] = hda.file_name + # except objectstore.ObjectNotFound: + # log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) + # + # hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display', + # history_id = trans.security.encode_id( hda.history.id ), + # history_content_id = trans.security.encode_id( hda.id ) ) + # + # # indeces, assoc. metadata files, etc. + # meta_files = [] + # for meta_type in hda.metadata.spec.keys(): + # if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): + # meta_files.append( dict( file_type=meta_type ) ) + # if meta_files: + # hda_dict[ 'meta_files' ] = meta_files + # + # # currently, the viz reg is optional - handle on/off + # if trans.app.visualizations_registry: + # hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) + # else: + # hda_dict[ 'visualizations' ] = hda.get_visualizations() + # #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations + # # when the visualizations button is clicked (instead of preloading/pre-checking) + # + # # ---- return here if deleted + # if hda.deleted and not purged: + # return trans.security.encode_dict_ids( hda_dict ) + # + # return trans.security.encode_dict_ids( hda_dict ) + + #def get_inaccessible_hda_dict( self, trans, hda ): + # return trans.security.encode_dict_ids({ + # 'id' : hda.id, + # 'history_id': hda.history.id, + # 'hid' : hda.hid, + # 'name' : hda.name, + # 'state' : hda.state, + # 'deleted' : hda.deleted, + # 'visible' : hda.visible, + # 'accessible': False + # }) + + #def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): + # return trans.security.encode_dict_ids({ + # 'id' : hda.id if hda else id, + # 'history_id': hda.history.id if hda else history_id, + # 'hid' : hda.hid if hda else '(unknown)', + # 'name' : hda.name if hda else '(unknown)', + # 'error' : error_msg, + # 'state' : trans.model.Dataset.states.NEW + # }) + + # ============================================================================= class HDADeserializer( base.ModelDeserializer ): """ diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index 769119ac1a3..cb6f00a8d1b 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -127,7 +127,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface contents_dict = {} if isinstance( content, trans.app.model.HistoryDatasetAssociation ): - contents_dict = hda_serializer.serialize_to_view( trans, hda, view='detailed' ) + contents_dict = hda_serializer.serialize_to_view( trans, content, view='detailed' ) elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): try: diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 14dafa3a620..8045375bd23 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -23,8 +23,17 @@ class UserManager( base.ModelManager ): default_order_by = ( model.User.create_time, ) foreign_key_name = 'user' + #TODO: there is quite a bit of functionality around the user (authentication, permissions, quotas, groups/roles) + # most of which it may be unneccessary to have here + + #TODO: incorp BaseAPIController.validate_in_users_and_groups + #TODO: incorp CreatesUsersMixin + #TODO: incorp CreatesApiKeysMixin + + def create( self, trans, **kwargs ): """ + Create a new user. """ #TODO: deserialize and validate here email = kwargs[ 'email' ] @@ -61,6 +70,9 @@ class UserManager( base.ModelManager ): def _error_on_duplicate_email( self, trans, email ): """ + Check for a duplicate email and raise if found. + + :raises exceptions.Conflict: if any are found """ #TODO: remove this check when unique=True is added to the email column if self.by_email( trans, email ) is not None: @@ -72,6 +84,7 @@ class UserManager( base.ModelManager ): """ filters = self._munge_filters( self.model_class.email == email, filters ) try: +#TODO: use one_or_none return super( UserManager, self ).one( trans, filters=filters, **kwargs ) except exceptions.ObjectNotFound, not_found: return None @@ -86,24 +99,31 @@ class UserManager( base.ModelManager ): # ------------------------------------------------------------------------- admin def is_admin( self, trans, user ): """ + Return True if this user is an admin. """ admin_emails = self._admin_emails( trans ) return user and admin_emails and user.email in admin_emails def _admin_emails( self, trans ): """ + Return a list of admin email addresses from the config file. """ return [ x.strip() for x in trans.app.config.get( "admin_users", "" ).split( "," ) ] def admins( self, trans, filters=None, **kwargs ): """ + Return a list of admin Users. """ filters = self._munge_filters( self.model_class.email.in_( self._admin_emails( trans ) ), filters ) return super( UserManager, self ).list( trans, filters=filters, **kwargs ) def error_unless_admin( self, trans, user, msg="Administrators only", **kwargs ): """ + Raise an error if `user` is not an admin. + + :raises exceptions.AdminRequiredException: if `user` is not an admin. """ + # useful in admin only methods if not self.is_admin( trans, user ): raise exceptions.AdminRequiredException( msg, **kwargs ) return user @@ -111,12 +131,14 @@ class UserManager( base.ModelManager ): # ------------------------------------------------------------------------- anonymous def is_anonymous( self, user ): """ + Return True if `user` is anonymous. """ - # define here for single point of change + # define here for single point of change and make more readable return user is None def error_if_anonymous( self, trans, user, msg="Log-in required", **kwargs ): """ + Raise an error if `user` is anonymous. """ if user is None: #TODO: code is correct (403), but should be named AuthenticationRequired (401 and 403 are flipped) @@ -126,16 +148,20 @@ class UserManager( base.ModelManager ): # ------------------------------------------------------------------------- current def is_current_user( self, trans, user ): """ + Return True if this user is the trans' current user. """ + # define here for single point of change and make more readable return user == trans.user def is_current_user_anonymous( self, trans ): """ + Return True if the current user is anonymous. """ - return trans.user is None + return self.is_anonymous( trans.user ) def is_current_user_admin( self, trans ): """ + Return True if the current user is admin. """ return self.is_admin( trans, trans.user ) @@ -151,32 +177,47 @@ class UserManager( base.ModelManager ): # ------------------------------------------------------------------------- api keys def create_api_key( self, trans, user ): """ + Create and return an API key for `user`. """ + #TODO: seems like this should return the model return api_keys.ApiKeyManager( trans.app ).create_api_key( user ) - # needed? - def api_key( self, trans, user ): + #TODO: possibly move to ApiKeyManager + def valid_api_key( self, trans, user ): """ + Return this most recent APIKey for this user or None if none have been created. """ - #TODO: catch NoResultFound - return trans.sa_session.query( model.APIKeys ).filter_by( user=user ).one() + query = ( trans.sa_session.query( model.APIKeys ) + .filter_by( user=user ) + .order_by( model.APIKeys.create_time ) ) + return query.first() + + #TODO: possibly move to ApiKeyManager + def get_or_create_valid_api_key( self, trans, user ): + """ + Return this most recent APIKey for this user or create one if none have been + created. + """ + existing = self.valid_api_key( trans, user ) + if existing: + return existing + return self.create_api_key( self, trans, user ) # ------------------------------------------------------------------------- roles def private_role( self, trans, user ): """ + Return the security agent's private role for `user`. """ #TODO: not sure we need to go through sec agent... it's just the first role of type private return trans.app.security_agent.get_private_user_role( user ) - #TODO: there is quite a bit of functionality around the user (authentication, quotas, groups/roles) - # most of which it may be unneccessary to have here - # ============================================================================= class UserSerializer( base.ModelSerializer ): def __init__( self ): """ + Convert a User and associated data to a dictionary representation. """ super( UserSerializer, self ).__init__() @@ -192,8 +233,8 @@ class UserSerializer( base.ModelSerializer ): ] extended_view = detailed_view + [ #'preferences', - #'tags', - #'annotations' + #'tags', # all tags + #'annotations' # all annotations ] self.serializable_keys = extended_view self.views = { diff --git a/lib/galaxy/tools/imp_exp/__init__.py b/lib/galaxy/tools/imp_exp/__init__.py index 211909977c7..339729a095b 100644 --- a/lib/galaxy/tools/imp_exp/__init__.py +++ b/lib/galaxy/tools/imp_exp/__init__.py @@ -9,7 +9,6 @@ from galaxy.tools.parameters.basic import UnvalidatedValue from galaxy.web.framework.helpers import to_unicode from galaxy.model.item_attrs import UsesAnnotations from galaxy.util.json import loads, dumps -from galaxy.web.base.controller import UsesHistoryMixin log = logging.getLogger(__name__) @@ -49,7 +48,7 @@ def load_history_imp_exp_tools( toolbox ): log.debug( "Loaded history import tool: %s", history_imp_tool.id ) -class JobImportHistoryArchiveWrapper( object, UsesHistoryMixin, UsesAnnotations ): +class JobImportHistoryArchiveWrapper( object, UsesAnnotations ): """ Class provides support for performing jobs that import a history from an archive. @@ -315,14 +314,28 @@ class JobImportHistoryArchiveWrapper( object, UsesHistoryMixin, UsesAnnotations self.sa_session.flush() -class JobExportHistoryArchiveWrapper( object, UsesHistoryMixin, UsesAnnotations ): +class JobExportHistoryArchiveWrapper( object, UsesAnnotations ): """ - Class provides support for performing jobs that export a history to an - archive. + Class provides support for performing jobs that export a history to an + archive. """ def __init__( self, job_id ): self.job_id = job_id + def get_history_datasets( self, trans, history ): + """ + Returns history's datasets. + """ + query = ( trans.sa_session.query( trans.model.HistoryDatasetAssociation ) + .filter( trans.model.HistoryDatasetAssociation.history == history ) + .options( eagerload( "children" ) ) + .join( "dataset" ) + .options( eagerload_all( "dataset.actions" ) ) + .order_by( trans.model.HistoryDatasetAssociation.hid ) + .filter( trans.model.HistoryDatasetAssociation.deleted == False ) + .filter( trans.model.Dataset.purged == False ) ) + return query.all() + # TODO: should use db_session rather than trans in this method. def setup_job( self, trans, jeha, include_hidden=False, include_deleted=False ): """ Perform setup for job to export a history into an archive. Method generates diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 889d6356c8c..174845f85d8 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -213,7 +213,7 @@ class CreatesUsersMixin: if trans.webapp.name == 'galaxy': # We set default user permissions, before we log in and set the default history permissions trans.app.security_agent.user_set_default_permissions( user, - default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) + default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) return user @@ -236,45 +236,6 @@ class SharableItemSecurityMixin: return managers_base.security_check( trans, item, check_ownership=check_ownership, check_accessible=check_accessible ) -class UsesHistoryMixin( SharableItemSecurityMixin ): - """ Mixin for controllers that use History objects. """ - - def get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ): - """ - Get a History from the database by id, verifying ownership. - """ - if trans.user is None and trans.history: - if id == trans.security.encode_id( trans.history.id ): - return trans.history - raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' ) - - history = self.get_object( trans, id, 'History', - check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) - history = self.security_check( trans, history, check_ownership, check_accessible ) - return history -#lib/galaxy/webapps/galaxy/api/annotations.py:55: hist = self.get_history( trans, idstr ) -#lib/galaxy/webapps/galaxy/api/dataset_collections.py:44: history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) -#lib/galaxy/webapps/galaxy/api/workflows.py:152: history = self.get_history( trans, from_history_id, check_ownership=False, check_accessible=True ) -#lib/galaxy/webapps/galaxy/controllers/dataset.py:996: history = self.get_history(trans, source_history) -# and alll over history controller - - def get_history_datasets( self, trans, history, show_deleted=False, show_hidden=False, show_purged=False ): - """ Returns history's datasets. """ - query = trans.sa_session.query( trans.model.HistoryDatasetAssociation ) \ - .filter( trans.model.HistoryDatasetAssociation.history == history ) \ - .options( eagerload( "children" ) ) \ - .join( "dataset" ) \ - .options( eagerload_all( "dataset.actions" ) ) \ - .order_by( trans.model.HistoryDatasetAssociation.hid ) - if not show_deleted: - query = query.filter( trans.model.HistoryDatasetAssociation.deleted == False ) - if not show_purged: - query = query.filter( trans.model.Dataset.purged == False ) - return query.all() -#lib/galaxy/tools/imp_exp/__init__.py:417: datasets = self.get_history_datasets( trans, history ) -#lib/galaxy/webapps/galaxy/controllers/history.py:1280: datasets = self.get_history_datasets( trans, history ) -#lib/galaxy/webapps/galaxy/controllers/page.py:742: datasets = self.get_history_datasets( trans, history ) - class ExportsHistoryMixin: def serve_ready_history_export( self, trans, jeha ): @@ -289,7 +250,6 @@ class ExportsHistoryMixin: def queue_history_export( self, trans, history, gzip=True, include_hidden=False, include_deleted=False ): # Convert options to booleans. - # if isinstance( gzip, basestring ): gzip = ( gzip in [ 'True', 'true', 'T', 't' ] ) if isinstance( include_hidden, basestring ): @@ -361,69 +321,6 @@ class UsesHistoryDatasetAssociationMixin: + "before attempting to view it." ) return data - def get_history_dataset_association( self, trans, history, dataset_id, - check_ownership=True, check_accessible=False, check_state=False ): - """ - Get a HistoryDatasetAssociation from the database by id, verifying ownership. - """ - #TODO: duplicate of above? alias to above (or vis-versa) - self.security_check( trans, history, check_ownership=check_ownership, check_accessible=check_accessible ) - hda = self.get_object( trans, dataset_id, 'HistoryDatasetAssociation', - check_ownership=False, check_accessible=False ) - - if check_accessible: - if( not trans.user_is_admin() - and not trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) ): - error( "You are not allowed to access this dataset" ) - - if check_state and hda.state == trans.model.Dataset.states.UPLOAD: - error( "Please wait until this dataset finishes uploading before attempting to view it." ) - return hda - - def get_history_dataset_association_from_ids( self, trans, id, history_id ): - # Just to echo other TODOs, there seems to be some overlap here, still - # this block appears multiple places (dataset show, history_contents - # show, upcoming history job show) so I am consolodating it here. - # Someone smarter than me should determine if there is some redundancy here. - - # for anon users: - #TODO: check login_required? - #TODO: this isn't actually most_recently_used (as defined in histories) - if( ( trans.user == None ) - and ( history_id == trans.security.encode_id( trans.history.id ) ) ): - history = trans.history - #TODO: dataset/hda by id (from history) OR check_ownership for anon user - hda = self.get_history_dataset_association( trans, history, id, - check_ownership=False, check_accessible=True ) - else: - #TODO: do we really need the history? - history = self.get_history( trans, history_id, - check_ownership=False, check_accessible=True, deleted=False ) - hda = self.get_history_dataset_association( trans, history, id, - check_ownership=False, check_accessible=True ) - return hda - - def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ): - """ - Returns one or more datasets in a list. - - If a dataset is not found or is inaccessible to trans.user, - add None in its place in the list. - """ - # precondtion: dataset_ids is a list of encoded id strings - hdas = [] - for id in hda_ids: - hda = None - try: - hda = self.get_dataset( trans, id, - check_ownership=check_ownership, - check_accessible=check_accessible, - check_state=check_state ) - except Exception: - pass - hdas.append( hda ) - return hdas - def get_data( self, dataset, preview=True ): """ Gets a dataset's data. @@ -444,6 +341,10 @@ class UsesHistoryDatasetAssociationMixin: # For now, cannot get data from non-text datasets. dataset_data = None return truncated, dataset_data +#lib/galaxy/webapps/galaxy/controllers/dataset.py:566: truncated, dataset_data = self.get_data( dataset, preview ) +#lib/galaxy/webapps/galaxy/controllers/dataset.py:604: truncated, dataset_data = self.get_data( dataset, preview=True ) +#lib/galaxy/webapps/galaxy/controllers/page.py:787: data = self.get_data( dataset ) +#lib/galaxy/webapps/galaxy/controllers/visualization.py:1012: data = pd.get_data( tree_index=tree_index ) def check_dataset_state( self, trans, dataset ): """ @@ -456,68 +357,11 @@ class UsesHistoryDatasetAssociationMixin: if dataset.state != trans.app.model.Job.states.OK: return dataset.conversion_messages.PENDING return None - - def get_display_apps( self, trans, hda ): - display_apps = [] - for display_app in hda.get_display_applications( trans ).itervalues(): - - app_links = [] - for link_app in display_app.links.itervalues(): - app_links.append({ - 'target': link_app.url.get( 'target_frame', '_blank' ), - 'href' : link_app.get_display_url( hda, trans ), - 'text' : gettext( link_app.name ) - }) - if app_links: - display_apps.append( dict( label=display_app.name, links=app_links ) ) - - return display_apps - - def get_old_display_applications( self, trans, hda ): - display_apps = [] - if not trans.app.config.enable_old_display_applications: - return display_apps - - for display_app in hda.datatype.get_display_types(): - target_frame, display_links = hda.datatype.get_display_links( hda, - display_app, trans.app, trans.request.base ) - - if len( display_links ) > 0: - display_label = hda.datatype.get_display_label( display_app ) - - app_links = [] - for display_name, display_link in display_links: - app_links.append({ - 'target': target_frame, - 'href' : display_link, - 'text' : gettext( display_name ) - }) - if app_links: - display_apps.append( dict( label=display_label, links=app_links ) ) - - return display_apps - - def set_hda_from_dict( self, trans, hda, new_data ): - """ - Changes HDA data using the given dictionary new_data. - """ - # precondition: access of the hda has already been checked - - # send what we can down into the model - changed = hda.set_from_dict( new_data ) - # the rest (often involving the trans) - do here - if 'annotation' in new_data.keys() and trans.get_user(): - hda.add_item_annotation( trans.sa_session, trans.get_user(), hda, new_data[ 'annotation' ] ) - changed[ 'annotation' ] = new_data[ 'annotation' ] - if 'tags' in new_data.keys() and trans.get_user(): - self.set_tags_from_list( trans, hda, new_data[ 'tags' ], user=trans.user ) - # sharing/permissions? - # purged - - if changed.keys(): - trans.sa_session.flush() - - return changed +#lib/galaxy/webapps/galaxy/api/datasets.py:89: msg = self.check_dataset_state( trans, dataset ) +#lib/galaxy/webapps/galaxy/api/datasets.py:100: msg = self.check_dataset_state( trans, dataset ) +#lib/galaxy/webapps/galaxy/api/datasets.py:149: msg = self.check_dataset_state( trans, dataset ) +#lib/galaxy/webapps/galaxy/api/datasets.py:242: msg = self.check_dataset_state( trans, dataset ) +#lib/galaxy/webapps/galaxy/api/tools.py:287: msg = self.check_dataset_state( trans, original_dataset ) def get_hda_job( self, hda ): # Get dataset's job. @@ -526,6 +370,9 @@ class UsesHistoryDatasetAssociationMixin: job = job_output_assoc.job break return job +#lib/galaxy/web/base/controller.py:846: job = self.get_hda_job( hda ) +#lib/galaxy/webapps/galaxy/api/tools.py:296: original_job = self.get_hda_job( original_dataset ) +#lib/galaxy/webapps/galaxy/controllers/visualization.py:977: job = self.get_hda_job( dataset ) def stop_hda_creating_job( self, hda ): """ @@ -539,7 +386,8 @@ class UsesHistoryDatasetAssociationMixin: if job.check_if_output_datasets_deleted(): job.mark_deleted( self.app.config.track_jobs_in_database ) self.app.job_manager.job_stop_queue.put( job.id ) - +#lib/galaxy/webapps/galaxy/api/history_contents.py:366: self.stop_hda_creating_job( hda ) +#lib/galaxy/webapps/galaxy/controllers/dataset.py:772: self.stop_hda_creating_job( hda ) class UsesLibraryMixin: diff --git a/lib/galaxy/webapps/galaxy/api/annotations.py b/lib/galaxy/webapps/galaxy/api/annotations.py index 98eb4dead74..b1859e44311 100644 --- a/lib/galaxy/webapps/galaxy/api/annotations.py +++ b/lib/galaxy/webapps/galaxy/api/annotations.py @@ -5,12 +5,14 @@ import logging from galaxy import web from galaxy.model.item_attrs import UsesAnnotations from galaxy.util.sanitize_html import sanitize_html -from galaxy.web.base.controller import BaseAPIController, HTTPNotImplemented, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, UsesStoredWorkflowMixin +from galaxy.web.base.controller import BaseAPIController, HTTPNotImplemented, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin + +from galaxy import managers log = logging.getLogger( __name__ ) -class BaseAnnotationsController( BaseAPIController, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesAnnotations ): +class BaseAnnotationsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesAnnotations ): @web.expose_api def index( self, trans, **kwd ): @@ -51,9 +53,14 @@ class HistoryAnnotationsController(BaseAnnotationsController): controller_name = "history_annotations" tagged_item_id = "history_id" + def __init__( self, app ): + super( HistoryAnnotationsController, self ).__init__( app ) + self.history_manager = managers.histories.HistoryManager( app ) + def _get_item_from_id(self, trans, idstr): - hist = self.get_history( trans, idstr ) - return hist + decoded_idstr = trans.security.decode_id( idstr ) + history = self.history_manager.accessible_by_id( trans, decoded_idstr, trans.uer ) + return history class HistoryContentAnnotationsController(BaseAnnotationsController): diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index 7ce5ba7364a..08193bf4476 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -1,9 +1,9 @@ from galaxy.web import _future_expose_api as expose_api from galaxy.web.base.controller import BaseAPIController -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import UsesLibraryMixinItems +from galaxy import managers from galaxy.managers.collections_util import api_payload_to_create_params, dictify_dataset_collection_instance from logging import getLogger @@ -12,10 +12,13 @@ log = getLogger( __name__ ) class DatasetCollectionsController( BaseAPIController, - UsesHistoryMixin, UsesLibraryMixinItems, ): + def __init__( self, app ): + super( DatasetCollectionsController, self ).__init__( app ) + self.history_manager = managers.histories.HistoryManager( app ) + @expose_api def index( self, trans, **kwd ): trans.response.status = 501 @@ -41,7 +44,8 @@ class DatasetCollectionsController( instance_type = payload.pop( "instance_type", "history" ) if instance_type == "history": history_id = payload.get( 'history_id' ) - history = self.get_history( trans, history_id, check_ownership=True, check_accessible=False ) + history_id = trans.security.decode_id( history_id ) + history = self.history_manager.owned_by_id( trans, history_id, trans.user ) create_params[ "parent" ] = history elif instance_type == "library": folder_id = payload.get( 'folder_id' ) diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index 23af3294179..817645b62af 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -13,15 +13,13 @@ from galaxy.datatypes import dataproviders from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy import managers import logging log = logging.getLogger( __name__ ) -class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMixin, - UsesHistoryDatasetAssociationMixin ): +class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryDatasetAssociationMixin ): def __init__( self, app ): super( DatasetsController, self ).__init__( app ) @@ -281,24 +279,25 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory some point in the future without warning. Generally, data should be processed by its datatype prior to display (the defult if raw is unspecified or explicitly false. """ + decoded_content_id = trans.security.decode_id( history_content_id ) raw = util.string_as_bool_or_none( raw ) + rval = '' try: - hda = self.get_history_dataset_association_from_ids( trans, history_content_id, history_id ) + hda = self.mgrs.hdas.accessible_by_id( trans, decoded_content_id, trans.user ) - display_kwd = kwd.copy() - try: - del display_kwd["key"] - except KeyError: - pass if raw: if filename and filename != 'index': - file_path = trans.app.object_store.get_filename(hda.dataset, extra_dir='dataset_%s_files' % hda.dataset.id, alt_name=filename) + file_path = trans.app.object_store.get_filename( hda.dataset, + extra_dir=( 'dataset_%s_files' % hda.dataset.id ), alt_name=filename) else: file_path = hda.file_name rval = open( file_path ) else: + display_kwd = kwd.copy() + if 'key' in display_kwd: + del display_kwd["key"] rval = hda.datatype.display_data( trans, hda, preview, filename, to_ext, chunk, **display_kwd ) except Exception, exception: diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index c6dbc074658..56885f432f9 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -3,11 +3,11 @@ API operations on annotations. """ import logging from galaxy import web -from galaxy.web.base.controller import BaseAPIController, UsesHistoryMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesExtendedMetadataMixin, HTTPNotImplemented +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesExtendedMetadataMixin, HTTPNotImplemented log = logging.getLogger( __name__ ) -class BaseExtendedMetadataController( BaseAPIController, UsesExtendedMetadataMixin, UsesHistoryMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin ): +class BaseExtendedMetadataController( BaseAPIController, UsesExtendedMetadataMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin ): @web.expose_api def index( self, trans, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 86bd373bbde..98bfe0681f3 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -11,7 +11,6 @@ from galaxy.web import url_for from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import UsesLibraryMixin from galaxy.web.base.controller import UsesLibraryMixinItems from galaxy.web.base.controller import UsesTagsMixin @@ -27,7 +26,7 @@ import logging log = logging.getLogger( __name__ ) -class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, +class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesLibraryMixin, UsesLibraryMixinItems, UsesTagsMixin ): def __init__( self, app ): @@ -358,11 +357,8 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio hda = self.mgrs.hdas.error_if_uploading( trans, hda ) # make the actual changes - #TODO: to deserializer #TODO: is this if still needed? if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): - #payload = self._validate_and_parse_update_payload( payload ) - #changed = self.set_hda_from_dict( trans, hda, payload ) self.hda_deserializer.deserialize( trans, hda, payload ) #TODO: this should be an effect of deleting the hda if payload.get( 'deleted', False ): diff --git a/lib/galaxy/webapps/galaxy/api/provenance.py b/lib/galaxy/webapps/galaxy/api/provenance.py index 1dcd220531c..c42d8e5bdf5 100644 --- a/lib/galaxy/webapps/galaxy/api/provenance.py +++ b/lib/galaxy/webapps/galaxy/api/provenance.py @@ -3,14 +3,14 @@ API operations provenance """ import logging from galaxy import web -from galaxy.web.base.controller import BaseAPIController, UsesHistoryMixin +from galaxy.web.base.controller import BaseAPIController from paste.httpexceptions import HTTPNotImplemented, HTTPBadRequest from galaxy.managers import hdas log = logging.getLogger( __name__ ) -class BaseProvenanceController( BaseAPIController, UsesHistoryMixin ): +class BaseProvenanceController( BaseAPIController ): """ """ def __init__( self, app ): diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 21a7823142e..2c869af0422 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -7,7 +7,6 @@ from galaxy.web import _future_expose_api_anonymous from galaxy.web import _future_expose_api from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.visualization.genomes import GenomeRegion from galaxy.util.json import dumps from galaxy.visualization.data_providers.genome import * @@ -25,7 +24,7 @@ import logging log = logging.getLogger( __name__ ) -class ToolsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMixin ): +class ToolsController( BaseAPIController, UsesVisualizationMixin ): """ RESTful controller for interactions with tools. """ diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 99fd8d30274..0dcebd52ae1 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -13,7 +13,6 @@ from galaxy.managers import histories from galaxy.managers import workflows from galaxy.web import _future_expose_api as expose_api from galaxy.web.base.controller import BaseAPIController, url_for, UsesStoredWorkflowMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import SharableMixin from galaxy.workflow.extract import extract_workflow from galaxy.workflow.run import invoke, queue_invoke @@ -22,10 +21,10 @@ from galaxy.workflow.run_request import build_workflow_run_config log = logging.getLogger(__name__) -class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHistoryMixin, UsesAnnotations, SharableMixin): +class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnnotations, SharableMixin): def __init__( self, app ): - super( BaseAPIController, self ).__init__( app ) + super( WorkflowsAPIController, self ).__init__( app ) self.history_manager = histories.HistoryManager( app ) self.workflow_manager = workflows.WorkflowsManager( app ) self.workflow_contents_manager = workflows.WorkflowContentsManager() @@ -149,7 +148,9 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesHis if 'from_history_id' in payload: from_history_id = payload.get( 'from_history_id' ) - history = self.get_history( trans, from_history_id, check_ownership=False, check_accessible=True ) + from_history_id = trans.security.decode_id( from_history_id ) + history = self.history_manager.accessible_by_id( trans, from_history_id, trans.user ) + job_ids = map( trans.security.decode_id, payload.get( 'job_ids', [] ) ) dataset_ids = payload.get( 'dataset_ids', [] ) dataset_collection_ids = payload.get( 'dataset_collection_ids', [] ) diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index 191a6e4c6e1..5d90a0e7bb2 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -3,12 +3,13 @@ import os import urllib from galaxy import datatypes, eggs, model, util, web +from galaxy import managers from galaxy.datatypes.display_applications.util import decode_dataset_user, encode_dataset_user from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings from galaxy.util import inflector, smart_str from galaxy.util.sanitize_html import sanitize_html from galaxy.util.json import loads -from galaxy.web.base.controller import BaseUIController, ERROR, SUCCESS, url_for, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin, UsesExtendedMetadataMixin +from galaxy.web.base.controller import BaseUIController, ERROR, SUCCESS, url_for, UsesHistoryDatasetAssociationMixin, UsesExtendedMetadataMixin from galaxy.web.framework.helpers import grids, iff, time_ago, to_unicode, escape from galaxy.tools.errors import EmailErrorReporter @@ -85,10 +86,16 @@ class HistoryDatasetAssociationListGrid( grids.Grid ): .filter( model.History.deleted==False ) \ .filter( self.model_class.visible==True ) -class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, UsesItemRatings, UsesExtendedMetadataMixin ): +class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAssociationMixin, UsesItemRatings, UsesExtendedMetadataMixin ): stored_list_grid = HistoryDatasetAssociationListGrid() + def __init__( self, app ): + super( DatasetInterface, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories=managers.histories.HistoryManager( app ) + ) + def _get_job_for_dataset( self, trans, dataset_id ): ''' Return the job for the given dataset. This will throw an error if the @@ -993,7 +1000,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryMixin, Use def copy_datasets( self, trans, source_history=None, source_content_ids="", target_history_id=None, target_history_ids="", new_history_name="", do_copy=False, **kwd ): user = trans.get_user() if source_history is not None: - history = self.get_history(trans, source_history) + decoded_source_history_id = trans.security.decode_id( source_history ) + history = self.mgrs.histories.owned_by_id( trans, decoded_source_history_id, trans.user ) current_history = trans.get_history() else: history = current_history = trans.get_history() diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index a5f8d9e5e56..cc029e9da73 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -19,7 +19,6 @@ from galaxy.web import error, url_for from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controller import SharableMixin from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin -from galaxy.web.base.controller import UsesHistoryMixin from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin from galaxy.model.item_attrs import UsesAnnotations @@ -40,7 +39,7 @@ class NameColumn( grids.TextColumn ): class HistoryListGrid( grids.Grid ): # Custom column types - class DatasetsByStateColumn( grids.GridColumn, UsesHistoryMixin ): + class DatasetsByStateColumn( grids.GridColumn ): def get_value( self, trans, grid, history ): state_counts = { 'ok' : 0, @@ -212,7 +211,7 @@ class HistoryAllPublishedGrid( grids.Grid ): return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesItemRatings, - UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, ExportsHistoryMixin, + UsesHistoryDatasetAssociationMixin, ExportsHistoryMixin, ImportsHistoryMixin ): def __init__( self, app ): @@ -222,6 +221,19 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI ) self.history_serializer = managers.histories.HistorySerializer( self.app ) + def _get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ): + """ + Get a History from the database by id, verifying ownership. + """ + if trans.user is None and trans.history: + if id == trans.security.encode_id( trans.history.id ): + return trans.history + raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' ) + + history = self.get_object( trans, id, 'History', + check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) + history = self.security_check( trans, history, check_ownership, check_accessible ) + return history @web.expose def index( self, trans ): @@ -264,7 +276,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI del kwargs['name'] # Remove ajax name param that rename method uses return self.rename( trans, **kwargs ) if operation == "view": - history = self.get_history( trans, kwargs.get( 'id', None ) ) + history = self._get_history( trans, kwargs.get( 'id', None ) ) if history: return trans.response.send_redirect( url_for( controller='history', action='view', @@ -281,7 +293,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Load the histories and ensure they all belong to the current user histories = [] for history_id in history_ids: - history = self.get_history( trans, history_id ) + history = self._get_history( trans, history_id ) if history: # Ensure history is owned by current user if history.user_id != None and trans.user: @@ -324,7 +336,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI self._make_item_importable( trans.sa_session, history ) elif operation == "disable import via link": if history_ids: - histories = [ self.get_history( trans, history_id ) for history_id in history_ids ] + histories = [ self._get_history( trans, history_id ) for history_id in history_ids ] for history in histories: if history.importable: history.importable = False @@ -441,7 +453,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI operation = kwargs['operation'].lower() if operation == "view": # Display history. - history = self.get_history( trans, ids[0], False) + history = self._get_history( trans, ids[0], False) return self.display_by_username_and_slug( trans, history.user.username, history.slug ) elif operation == "copy": if not ids: @@ -456,7 +468,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return self.shared_list_grid( trans, status='error', message=message, **kwargs ) # No need to check security, association below won't yield a # hit if this user isn't having the history shared with her. - histories = [ self.get_history( trans, history_id, check_ownership=False ) for history_id in ids ] + histories = [ self._get_history( trans, history_id, check_ownership=False ) for history_id in ids ] for history in histories: # Current user is the user with which the histories were shared association = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=trans.user, history=history ).one() @@ -556,7 +568,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if not id: id = trans.security.encode_id( trans.history.id ) - history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + history_to_view = self._get_history( trans, id, check_ownership=False, check_accessible=True ) history_data = self.mgrs.histories._get_history_data( trans, history_to_view ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] @@ -595,7 +607,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI hda_dictionaries = [] user_is_owner = False try: - history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=False ) + history_to_view = self._get_history( trans, id, check_ownership=False, check_accessible=False ) if not history_to_view: return trans.show_error_message( "The specified history does not exist." ) if history_to_view.user == trans.user: @@ -708,7 +720,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if id: ids = galaxy.util.listify( id ) if ids: - histories = [ self.get_history( trans, history_id ) for history_id in ids ] + histories = [ self._get_history( trans, history_id ) for history_id in ids ] elif not histories: histories = [ trans.history ] @@ -777,7 +789,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_err = err_msg histories = [] for history_id in id: - histories.append( self.get_history( trans, history_id ) ) + histories.append( self._get_history( trans, history_id ) ) return trans.fill_template( "/history/share.mako", histories=histories, email=email, @@ -885,7 +897,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_err = "" histories = [] for history_id in id: - histories.append( self.get_history( trans, history_id ) ) + histories.append( self._get_history( trans, history_id ) ) send_to_users = [] for email_address in galaxy.util.listify( email ): email_address = email_address.strip() @@ -1164,7 +1176,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.json def rate_async( self, trans, id, rating ): """ Rate a history asynchronously and return updated community data. """ - history = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + history = self._get_history( trans, id, check_ownership=False, check_accessible=True ) if not history: return trans.show_error_message( "The specified history does not exist." ) # Rate history. @@ -1208,7 +1220,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Get history to export. # if id: - history = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + history = self._get_history( trans, id, check_ownership=False, check_accessible=True ) else: # Use current history. history = trans.history @@ -1246,7 +1258,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "get history name and link" ) def get_name_and_link_async( self, trans, id=None ): """ Returns history's name and link. """ - history = self.get_history( trans, id, False ) + history = self._get_history( trans, id, False ) if self.create_item_slug( trans.sa_session, history ): trans.sa_session.flush() return_dict = { @@ -1260,7 +1272,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "set history's accessible flag" ) def set_accessible_async( self, trans, id=None, accessible=False ): """ Set history's importable attribute and slug. """ - history = self.get_history( trans, id, True ) + history = self._get_history( trans, id, True ) # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. importable = accessible in ['True', 'true', 't', 'T']; if history and history.importable != importable: @@ -1272,23 +1284,6 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return #TODO: used in page/editor.mako - @web.expose - def get_item_content_async( self, trans, id ): - """ Returns item content in HTML format. """ - - history = self.get_history( trans, id, False, True ) - if history is None: - raise web.httpexceptions.HTTPNotFound() - - # Get datasets. - datasets = self.get_history_datasets( trans, history ) - # Get annotations. - history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history ) - for dataset in datasets: - dataset.annotation = self.get_item_annotation_str( trans.sa_session, history.user, dataset ) - return trans.stream_template_mako( "/history/item_content.mako", item = history, item_data = datasets ) - #TODO: used in embed_base.mako - @web.expose def name_autocomplete_data( self, trans, q=None, limit=None, timestamp=None ): """Return autocomplete data for history names""" @@ -1326,7 +1321,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Do import. if not id: return trans.show_error_message( "You must specify a history you want to import.
You can %s." % referer_message, use_panels=True ) - import_history = self.get_history( trans, id, check_ownership=False, check_accessible=False ) + import_history = self._get_history( trans, id, check_ownership=False, check_accessible=False ) if not import_history: return trans.show_error_message( "The specified history does not exist.
You can %s." % referer_message, use_panels=True ) # History is importable if user is admin or it's accessible. TODO: probably want to have app setting to enable admin access to histories. @@ -1396,7 +1391,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI histories = [] cur_names = [] for history_id in id: - history = self.get_history( trans, history_id ) + history = self._get_history( trans, history_id ) if history and history.user_id == user.id: histories.append( history ) cur_names.append( history.get_display_name() ) @@ -1438,7 +1433,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI ids = galaxy.util.listify( id ) histories = [] for history_id in ids: - history = self.get_history( trans, history_id, check_ownership=False ) + history = self._get_history( trans, history_id, check_ownership=False ) histories.append( history ) user = trans.get_user() for history in histories: @@ -1475,7 +1470,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return trans.response.send_redirect( url_for( "/" ) ) def get_item( self, trans, id ): - return self.get_history( trans, id ) + return self._get_history( trans, id ) #TODO: override of base ui controller? def history_data( self, trans, history ): diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index aeeeae5cb12..cf8cc94b88c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -3,7 +3,7 @@ from galaxy import model, web from galaxy import managers from galaxy.web import error, url_for from galaxy.model.item_attrs import UsesItemRatings -from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesHistoryMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin +from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin from galaxy.web.framework.helpers import time_ago, grids from galaxy import util from galaxy.util.sanitize_html import sanitize_html, _BaseHTMLProcessor @@ -276,7 +276,7 @@ class _PageContentProcessor( _BaseHTMLProcessor ): # Default behavior: _BaseHTMLProcessor.unknown_endtag( self, tag ) -class PageController( BaseUIController, SharableMixin, UsesHistoryMixin, +class PageController( BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin, UsesItemRatings ): _page_list = PageListGrid() @@ -729,7 +729,8 @@ class PageController( BaseUIController, SharableMixin, UsesHistoryMixin, Returns html suitable for embedding in another page. """ #TODO: should be moved to history controller and/or called via ajax from the template - history = self.get_history( trans, id, False, True ) + decoded_id = trans.security.decode_id( id ) + history = self.mgrs.histories.accessible_by_id( trans, decoded_id, trans.user ) if not history: return None @@ -738,17 +739,13 @@ class PageController( BaseUIController, SharableMixin, UsesHistoryMixin, user_is_owner = trans.user == history.user history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history ) - hda_dicts = [] - datasets = self.get_history_datasets( trans, history ) - # include all datasets: hidden, deleted, and purged - #TODO!: doubled query (hda_dictionaries + datasets) history_data = self.mgrs.histories._get_history_data( trans, history ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] history_dictionary[ 'annotation' ] = history.annotation - filled = trans.fill_template( "history/embed.mako", item=history, item_data=datasets, + filled = trans.fill_template( "history/embed.mako", item=history, user_is_owner=user_is_owner, history_dict=history_dictionary, hda_dicts=hda_dictionaries ) return filled diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py index 5f9c5b5f64b..172e351b367 100644 --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -7,7 +7,7 @@ import urllib from paste.httpexceptions import HTTPNotFound, HTTPBadGateway -from galaxy.web.base.controller import BaseUIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin +from galaxy.web.base.controller import BaseUIController, UsesHistoryDatasetAssociationMixin from galaxy import managers from galaxy import web @@ -20,7 +20,7 @@ from galaxy.util.json import dumps import logging log = logging.getLogger( __name__ ) -class RootController( BaseUIController, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, UsesAnnotations ): +class RootController( BaseUIController, UsesHistoryDatasetAssociationMixin, UsesAnnotations ): """ Controller class that maps to the url root of Galaxy (i.e. '/'). """ diff --git a/templates/tagging_common.mako b/templates/tagging_common.mako index 35ccfb4550a..77ecf8cd4c1 100644 --- a/templates/tagging_common.mako +++ b/templates/tagging_common.mako @@ -105,7 +105,7 @@ ## Build HTML. <% elt_id = int ( floor ( random()*maxint ) ) - community_tags = trans.app.tag_handler.get_community_tags( trans, item=tagged_item, limit=5 ) + community_tags = trans.app.tag_handler.get_community_tags( item=tagged_item, limit=5 ) %> ${self.render_tagging_element_html(elt_id=elt_id, \ tags=community_tags, \ diff --git a/templates/webapps/galaxy/history/embed.mako b/templates/webapps/galaxy/history/embed.mako index 85f5a2087bd..e11f803a66b 100644 --- a/templates/webapps/galaxy/history/embed.mako +++ b/templates/webapps/galaxy/history/embed.mako @@ -23,7 +23,8 @@ %if hasattr( item, "annotation") and item.annotation: -

${item.annotation | h}
+ <% print type( item.annotation ), item.annotation %> +
${ item.annotation | h }
%endif
diff --git a/test/unit/managers/test_UserManager.py b/test/unit/managers/test_UserManager.py index 174e811fb67..4a375d76acd 100644 --- a/test/unit/managers/test_UserManager.py +++ b/test/unit/managers/test_UserManager.py @@ -117,12 +117,18 @@ class UserManagerTestCase( BaseTestCase ): user2 = self.user_mgr.create( self.trans, **user2_data ) user3 = self.user_mgr.create( self.trans, **user3_data ) - self.log( "should be able to generate and retrieve api keys" ) - user2_api_key = self.user_mgr.create_api_key( self.trans, user2 ) - #self.assertIsInstance( user2_api_key, model.APIKeys ) - self.assertIsInstance( user2_api_key, basestring ) - self.assertEqual( self.user_mgr.api_key( self.trans, user2 ).key, user2_api_key ) + self.log( "should return None if no APIKey has been created" ) + no_api_key_yet = self.user_mgr.api_key( self.trans, user2 ) + self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, None ) + self.log( "should be able to generate and retrieve valid api key" ) + user2_api_key = self.user_mgr.create_api_key( self.trans, user2 ) + self.assertIsInstance( user2_api_key, basestring ) + self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, user2_api_key ) + + self.log( "should return the most recent (i.e. most valid) api key" ) + user2_api_key_2 = self.user_mgr.create_api_key( self.trans, user2 ) + self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, user2_api_key_2 ) # ============================================================================= From 203a574ffda0656f80f8e610b4d2b2a79f3930ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Fri, 9 Jan 2015 18:59:30 +0000 Subject: [PATCH 139/473] Created new branch toolshed_blacklist_additions From 6b7634011e7e48480f9aa3e3f1d48889705f98fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gr=C3=BCning?= Date: Fri, 9 Jan 2015 19:02:42 +0000 Subject: [PATCH 140/473] Add planemo test files to the tool shed upload blacklist. --- lib/tool_shed/util/commit_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tool_shed/util/commit_util.py b/lib/tool_shed/util/commit_util.py index 4b8d9680f68..e6ff14ad1ec 100644 --- a/lib/tool_shed/util/commit_util.py +++ b/lib/tool_shed/util/commit_util.py @@ -18,7 +18,7 @@ import tool_shed.repository_types.util as rt_util log = logging.getLogger( __name__ ) UNDESIRABLE_DIRS = [ '.hg', '.svn', '.git', '.cvs' ] -UNDESIRABLE_FILES = [ '.hg_archival.txt', 'hgrc', '.DS_Store' ] +UNDESIRABLE_FILES = [ '.hg_archival.txt', 'hgrc', '.DS_Store', 'tool_test_output.html', 'tool_test_output.json' ] def check_archive( repository, archive ): for member in archive.getmembers(): From b022e92f5b549e25084855cbde543090a57c17a4 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 9 Jan 2015 14:48:56 -0500 Subject: [PATCH 141/473] Add gemini.sqlite datatype. --- config/datatypes_conf.xml.sample | 4 ++- lib/galaxy/datatypes/binary.py | 60 +++++++++++++++++++++++++++++++- tools/data_source/upload.py | 2 ++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index 4f9aca67bd4..50b55b28cca 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -181,7 +181,8 @@ - + + @@ -271,6 +272,7 @@ --> + diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 8b45b610d53..19669f6dd02 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -20,7 +20,7 @@ eggs.require( "bx-python" ) from bx.seq.twobit import TWOBIT_MAGIC_NUMBER, TWOBIT_MAGIC_NUMBER_SWAP, TWOBIT_MAGIC_SIZE from galaxy.util import sqlite -from galaxy.datatypes.metadata import MetadataElement,ListParameter,DictParameter +from galaxy.datatypes.metadata import MetadataElement, MetadataParameter, ListParameter, DictParameter from galaxy.datatypes import metadata import dataproviders @@ -640,8 +640,66 @@ 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", + readonly=True, visible=True, no_value='0.10.0' ) + file_ext = "gemini.sqlite" + + def set_meta( self, dataset, overwrite = True, **kwd ): + super( GeminiSQLite, self ).set_meta( dataset, overwrite = overwrite, **kwd ) + try: + conn = sqlite.connect( dataset.file_name ) + c = conn.cursor() + tables_query = "SELECT version FROM version" + result = c.execute( tables_query ).fetchall() + for version, in result: + dataset.metadata.gemini_version = version + # TODO: Can/should we detect even more attributes, such as use of PED file, what was input annotation type, etc. + except Exception, e: + log.warn( '%s, set_meta Exception: %s', self, e ) + + def sniff( self, filename ): + if super( GeminiSQLite, self ).sniff( filename ): + gemini_table_names = [ "gene_detailed", "gene_summary", "resources", "sample_genotype_counts", "sample_genotypes", "samples", + "variant_impacts", "variants", "version" ] + try: + conn = sqlite.connect( filename ) + c = conn.cursor() + tables_query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + result = c.execute( tables_query ).fetchall() + result = map( lambda x: x[0], result ) + for table_name in gemini_table_names: + if table_name not in result: + return False + return True + except Exception, e: + log.warn( '%s, sniff Exception: %s', self, e ) + return False + + def set_peek( self, dataset, is_multi_byte=False ): + if not dataset.dataset.purged: + dataset.peek = "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' ) + dataset.blurb = data.nice_size( dataset.get_size() ) + else: + dataset.peek = 'file does not exist' + dataset.blurb = 'file purged from disk' + + def display_peek( self, dataset ): + try: + return dataset.peek + except: + return "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' ) + +Binary.register_sniffable_binary_format( "gemini.sqlite", "gemini.sqlite", GeminiSQLite ) +# FIXME: We need to register gemini.sqlite 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" diff --git a/tools/data_source/upload.py b/tools/data_source/upload.py index 42fd67f5fc1..0c968f1e5ba 100644 --- a/tools/data_source/upload.py +++ b/tools/data_source/upload.py @@ -113,6 +113,8 @@ def add_file( dataset, registry, json_file, output_path ): ext = sniff.guess_ext( dataset.path, is_multi_byte=True ) # Is dataset content supported sniffable binary? else: + # FIXME: This ignores the declared sniff order in datatype_conf.xml + # resulting in improper behavior type_info = Binary.is_sniffable_binary( dataset.path ) if type_info: data_type = type_info[0] From b307fb76fec285da93b55ee7020aa5c98d3fa32e Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 9 Jan 2015 14:48:56 -0500 Subject: [PATCH 142/473] Add gemini.sqlite datatype. --- config/datatypes_conf.xml.sample | 4 ++- lib/galaxy/datatypes/binary.py | 60 +++++++++++++++++++++++++++++++- tools/data_source/upload.py | 2 ++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index fcb9001003c..89be1e7b68b 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -180,7 +180,8 @@ - + + @@ -270,6 +271,7 @@ --> + diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index d17af05551a..2077131558b 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -20,7 +20,7 @@ eggs.require( "bx-python" ) from bx.seq.twobit import TWOBIT_MAGIC_NUMBER, TWOBIT_MAGIC_NUMBER_SWAP, TWOBIT_MAGIC_SIZE from galaxy.util import sqlite -from galaxy.datatypes.metadata import MetadataElement,ListParameter,DictParameter +from galaxy.datatypes.metadata import MetadataElement, MetadataParameter, ListParameter, DictParameter from galaxy.datatypes import metadata import dataproviders @@ -630,8 +630,66 @@ class SQlite ( Binary ): return dataproviders.dataset.SQliteDataProvider( 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", + readonly=True, visible=True, no_value='0.10.0' ) + file_ext = "gemini.sqlite" + + def set_meta( self, dataset, overwrite = True, **kwd ): + super( GeminiSQLite, self ).set_meta( dataset, overwrite = overwrite, **kwd ) + try: + conn = sqlite.connect( dataset.file_name ) + c = conn.cursor() + tables_query = "SELECT version FROM version" + result = c.execute( tables_query ).fetchall() + for version, in result: + dataset.metadata.gemini_version = version + # TODO: Can/should we detect even more attributes, such as use of PED file, what was input annotation type, etc. + except Exception, e: + log.warn( '%s, set_meta Exception: %s', self, e ) + + def sniff( self, filename ): + if super( GeminiSQLite, self ).sniff( filename ): + gemini_table_names = [ "gene_detailed", "gene_summary", "resources", "sample_genotype_counts", "sample_genotypes", "samples", + "variant_impacts", "variants", "version" ] + try: + conn = sqlite.connect( filename ) + c = conn.cursor() + tables_query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + result = c.execute( tables_query ).fetchall() + result = map( lambda x: x[0], result ) + for table_name in gemini_table_names: + if table_name not in result: + return False + return True + except Exception, e: + log.warn( '%s, sniff Exception: %s', self, e ) + return False + + def set_peek( self, dataset, is_multi_byte=False ): + if not dataset.dataset.purged: + dataset.peek = "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' ) + dataset.blurb = data.nice_size( dataset.get_size() ) + else: + dataset.peek = 'file does not exist' + dataset.blurb = 'file purged from disk' + + def display_peek( self, dataset ): + try: + return dataset.peek + except: + return "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' ) + +Binary.register_sniffable_binary_format( "gemini.sqlite", "gemini.sqlite", GeminiSQLite ) +# FIXME: We need to register gemini.sqlite 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" diff --git a/tools/data_source/upload.py b/tools/data_source/upload.py index 42fd67f5fc1..0c968f1e5ba 100644 --- a/tools/data_source/upload.py +++ b/tools/data_source/upload.py @@ -113,6 +113,8 @@ def add_file( dataset, registry, json_file, output_path ): ext = sniff.guess_ext( dataset.path, is_multi_byte=True ) # Is dataset content supported sniffable binary? else: + # FIXME: This ignores the declared sniff order in datatype_conf.xml + # resulting in improper behavior type_info = Binary.is_sniffable_binary( dataset.path ) if type_info: data_type = type_info[0] From 32750f747abad64f3d0f605598e68cd4d139b30b Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 9 Jan 2015 17:39:20 -0500 Subject: [PATCH 143/473] Remove UsesHistoryDatasetAssociationMixin --- lib/galaxy/managers/datasets.py | 3 +- lib/galaxy/managers/hdas.py | 127 ++++++++------ lib/galaxy/managers/histories.py | 35 ++-- lib/galaxy/web/base/controller.py | 161 +++++------------- lib/galaxy/webapps/galaxy/api/annotations.py | 14 +- lib/galaxy/webapps/galaxy/api/datasets.py | 11 +- .../webapps/galaxy/api/extended_metadata.py | 15 +- .../webapps/galaxy/api/folder_contents.py | 10 +- .../webapps/galaxy/api/history_contents.py | 6 +- lib/galaxy/webapps/galaxy/api/jobs.py | 10 +- .../webapps/galaxy/api/library_contents.py | 11 +- lib/galaxy/webapps/galaxy/api/tools.py | 12 +- .../webapps/galaxy/controllers/dataset.py | 48 ++++-- .../webapps/galaxy/controllers/history.py | 4 +- lib/galaxy/webapps/galaxy/controllers/page.py | 10 +- lib/galaxy/webapps/galaxy/controllers/root.py | 4 +- .../galaxy/controllers/visualization.py | 25 ++- 17 files changed, 259 insertions(+), 247 deletions(-) diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index cfce86e96ea..7632c23242a 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -185,7 +185,8 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur #TODO: implement above for groups #TODO: datatypes? - #TODO: data, object_store + + # ......................................................................... data, object_store # ============================================================================= diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index f5465950b5c..60ef428d2cb 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -5,10 +5,12 @@ HistoryDatasetAssociations (HDAs) are datasets contained or created in a history. """ +import os import gettext from galaxy import model from galaxy import exceptions +from galaxy import datatypes import base import histories @@ -28,12 +30,15 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface model_class = model.HistoryDatasetAssociation default_order_by = ( model.HistoryDatasetAssociation.create_time, ) + #TODO: which of these are common with LDDAs and can be pushed down into DatasetAssociationManager? + def __init__( self, app ): """ Set up and initialize other managers needed by hdas. """ super( HDAManager, self ).__init__( app ) + # ......................................................................... security and permissions def is_accessible( self, trans, hda, user ): """ """ @@ -48,6 +53,7 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface #TODO: this may be slow due to HistoryManager instantiation and possible 2nd transaction to get the hda.history return histories.HistoryManager( self.app ).is_owner( trans, hda.history, user ) + # ......................................................................... create and copy def create( self, trans, history=None, dataset=None, flush=True, **kwargs ): """ Create a new hda optionally passing in it's history and dataset. @@ -115,6 +121,13 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface """ return ldda.to_history_dataset_association( history, add_to_history=True ) + def is_a_copy( self, trans, hda ): + pass + + def copied_from( self, trans, hda ): + pass + + # ......................................................................... # def by_history_id( self, trans, history_id, filters=None, **kwargs ): # history_id_filter = self.model_class.history_id == history_id # filters = self._munge_filters( history_id_filter, filters ) @@ -153,6 +166,15 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) return hda + # ......................................................................... associated +#TODO: is this needed? Can't you use the hda.creating_job attribute? When is this None? + def creating_job( self, trans, hda ): + job = None + for job_output_assoc in hda.creating_job_associations: + job = job_output_assoc.job + break + return job + # ......................................................................... serialization def get_display_apps( self, trans, hda ): """ @@ -208,6 +230,27 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface return hda.get_visualizations() return trans.app.visualizations_registry.get_visualizations( trans, hda ) + #TODO: to data provider or Text datatype directly + def text_data( self, dataset, preview=True ): + """ + Get data from text file, truncating if necessary. + """ + truncated = False + dataset_data = None + if os.path.exists( dataset.file_name ): + if isinstance( dataset.datatype, datatypes.data.Text ): + max_peek_size = 1000000 # 1 MB + if preview and os.stat( dataset.file_name ).st_size > max_peek_size: + dataset_data = open( dataset.file_name ).read( max_peek_size ) + truncated = True + else: + dataset_data = open( dataset.file_name ).read( max_peek_size ) + truncated = False + else: + # For now, cannot get data from non-text datasets. + dataset_data = None + return truncated, dataset_data + #def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): # """ # Get an HDA object by id performing security checks using @@ -246,60 +289,38 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface # + "before attempting to view it." ) # return data - #def get_data( self, dataset, preview=True ): - # """ - # Gets a dataset's data. - # """ - # # Get data from file, truncating if necessary. - # truncated = False - # dataset_data = None - # if os.path.exists( dataset.file_name ): - # if isinstance( dataset.datatype, Text ): - # max_peek_size = 1000000 # 1 MB - # if preview and os.stat( dataset.file_name ).st_size > max_peek_size: - # dataset_data = open( dataset.file_name ).read(max_peek_size) - # truncated = True - # else: - # dataset_data = open( dataset.file_name ).read(max_peek_size) - # truncated = False - # else: - # # For now, cannot get data from non-text datasets. - # dataset_data = None - # return truncated, dataset_data - # - #def check_dataset_state( self, trans, dataset ): - # """ - # Returns a message if dataset is not ready to be used in visualization. - # """ - # if not dataset: - # return dataset.conversion_messages.NO_DATA - # if dataset.state == trans.app.model.Job.states.ERROR: - # return dataset.conversion_messages.ERROR - # if dataset.state != trans.app.model.Job.states.OK: - # return dataset.conversion_messages.PENDING - # return None + # this is a weird syntax and return val + def data_conversion_status( self, trans, hda ): + """ + Returns a message if dataset is not ready to be used in visualization. + """ + if not hda: + return hda.conversion_messages.NO_DATA + if hda.state == trans.app.model.Job.states.ERROR: + return hda.conversion_messages.ERROR + if hda.state != trans.app.model.Job.states.OK: + return hda.conversion_messages.PENDING + return None - - #def get_hda_job( self, hda ): - # # Get dataset's job. - # job = None - # for job_output_assoc in hda.creating_job_associations: - # job = job_output_assoc.job - # break - # return job - - #def stop_hda_creating_job( self, hda ): - # """ - # Stops an HDA's creating job if all the job's other outputs are deleted. - # """ - # if hda.parent_id is None and len( hda.creating_job_associations ) > 0: - # # Mark associated job for deletion - # job = hda.creating_job_associations[0].job - # if job.state in [ self.app.model.Job.states.QUEUED, self.app.model.Job.states.RUNNING, self.app.model.Job.states.NEW ]: - # # Are *all* of the job's other output datasets deleted? - # if job.check_if_output_datasets_deleted(): - # job.mark_deleted( self.app.config.track_jobs_in_database ) - # self.app.job_manager.job_stop_queue.put( job.id ) + def stop_creating_job( self, hda ): + """ + Stops an HDA's creating job if all the job's other outputs are deleted. + """ + RUNNING_STATES = ( + self.app.model.Job.states.QUEUED, + self.app.model.Job.states.RUNNING, + self.app.model.Job.states.NEW + ) + if hda.parent_id is None and len( hda.creating_job_associations ) > 0: + # Mark associated job for deletion + job = hda.creating_job_associations[0].job + if job.state in RUNNING_STATES: + # Are *all* of the job's other output datasets deleted? + if job.check_if_output_datasets_deleted(): + job.mark_deleted( self.app.config.track_jobs_in_database ) + self.app.job_manager.job_stop_queue.put( job.id ) + return True + return False # ============================================================================= class HDASerializer( base.ModelSerializer ): diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index cb6f00a8d1b..e7a1f3fe4ab 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -24,6 +24,7 @@ log = logging.getLogger( __name__ ) class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface ): """ """ + model_class = model.History default_order_by = ( model.History.create_time, ) foreign_key_name = 'history' @@ -35,28 +36,11 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface super( HistoryManager, self ).__init__( app, *args, **kwargs ) self.hda_mgr = hdas.HDAManager( app ) - # ......................................................................... crud def copy( self, trans, history, user, **kwargs ): """ """ return history.copy( target_user=user, **kwargs ) - def get_current( self, trans ): - """ - """ - return trans.get_history() - - def set_current( self, trans, history ): - """ - """ - trans.set_history( history ) - return history - - def set_current_by_id( self, trans, history_id ): - """ - """ - return self.set_current( trans, self.by_id( trans, history_id ) ) - def most_recent( self, trans, user, **kwargs ): """ """ @@ -105,6 +89,23 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface # ......................................................................... contents + # ......................................................................... current + def get_current( self, trans ): + """ + """ + return trans.get_history() + + def set_current( self, trans, history ): + """ + """ + trans.set_history( history ) + return history + + def set_current_by_id( self, trans, history_id ): + """ + """ + return self.set_current( trans, self.by_id( trans, history_id ) ) + # ......................................................................... serialization #TODO: move to serializer (i.e. history with contents attr) def _get_history_data( self, trans, history ): diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 174845f85d8..b988674e416 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -32,7 +32,6 @@ from galaxy.util.sanitize_html import sanitize_html from galaxy.model.item_attrs import Dictifiable, UsesAnnotations from galaxy.datatypes.interval import ChromatinInteractions -from galaxy.datatypes.data import Text from galaxy.model import ExtendedMetadata, ExtendedMetadataIndex, LibraryDatasetDatasetAssociation, HistoryDatasetAssociation @@ -278,117 +277,6 @@ class ImportsHistoryMixin: history_imp_tool.execute( trans, incoming=incoming ) -class UsesHistoryDatasetAssociationMixin: - """ - Mixin for controllers that use HistoryDatasetAssociation objects. - """ - - def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): - """ - Get an HDA object by id performing security checks using - the current transaction. - """ - try: - dataset_id = trans.security.decode_id( dataset_id ) - except ( AttributeError, TypeError ): - # DEPRECATION: We still support unencoded ids for backward compatibility - try: - dataset_id = int( dataset_id ) - except ValueError: - raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) ) - - try: - data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) ) - except: - raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) ) - - if check_ownership: - # Verify ownership. - user = trans.get_user() - if not user: - error( "Must be logged in to manage Galaxy items" ) - if data.history.user != user: - error( "%s is not owned by current user" % data.__class__.__name__ ) - - if check_accessible: - current_user_roles = trans.get_current_user_roles() - - if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ): - error( "You are not allowed to access this dataset" ) - - if check_state and data.state == trans.model.Dataset.states.UPLOAD: - return trans.show_error_message( "Please wait until this dataset finishes uploading " - + "before attempting to view it." ) - return data - - def get_data( self, dataset, preview=True ): - """ - Gets a dataset's data. - """ - # Get data from file, truncating if necessary. - truncated = False - dataset_data = None - if os.path.exists( dataset.file_name ): - if isinstance( dataset.datatype, Text ): - max_peek_size = 1000000 # 1 MB - if preview and os.stat( dataset.file_name ).st_size > max_peek_size: - dataset_data = open( dataset.file_name ).read(max_peek_size) - truncated = True - else: - dataset_data = open( dataset.file_name ).read(max_peek_size) - truncated = False - else: - # For now, cannot get data from non-text datasets. - dataset_data = None - return truncated, dataset_data -#lib/galaxy/webapps/galaxy/controllers/dataset.py:566: truncated, dataset_data = self.get_data( dataset, preview ) -#lib/galaxy/webapps/galaxy/controllers/dataset.py:604: truncated, dataset_data = self.get_data( dataset, preview=True ) -#lib/galaxy/webapps/galaxy/controllers/page.py:787: data = self.get_data( dataset ) -#lib/galaxy/webapps/galaxy/controllers/visualization.py:1012: data = pd.get_data( tree_index=tree_index ) - - def check_dataset_state( self, trans, dataset ): - """ - Returns a message if dataset is not ready to be used in visualization. - """ - if not dataset: - return dataset.conversion_messages.NO_DATA - if dataset.state == trans.app.model.Job.states.ERROR: - return dataset.conversion_messages.ERROR - if dataset.state != trans.app.model.Job.states.OK: - return dataset.conversion_messages.PENDING - return None -#lib/galaxy/webapps/galaxy/api/datasets.py:89: msg = self.check_dataset_state( trans, dataset ) -#lib/galaxy/webapps/galaxy/api/datasets.py:100: msg = self.check_dataset_state( trans, dataset ) -#lib/galaxy/webapps/galaxy/api/datasets.py:149: msg = self.check_dataset_state( trans, dataset ) -#lib/galaxy/webapps/galaxy/api/datasets.py:242: msg = self.check_dataset_state( trans, dataset ) -#lib/galaxy/webapps/galaxy/api/tools.py:287: msg = self.check_dataset_state( trans, original_dataset ) - - def get_hda_job( self, hda ): - # Get dataset's job. - job = None - for job_output_assoc in hda.creating_job_associations: - job = job_output_assoc.job - break - return job -#lib/galaxy/web/base/controller.py:846: job = self.get_hda_job( hda ) -#lib/galaxy/webapps/galaxy/api/tools.py:296: original_job = self.get_hda_job( original_dataset ) -#lib/galaxy/webapps/galaxy/controllers/visualization.py:977: job = self.get_hda_job( dataset ) - - def stop_hda_creating_job( self, hda ): - """ - Stops an HDA's creating job if all the job's other outputs are deleted. - """ - if hda.parent_id is None and len( hda.creating_job_associations ) > 0: - # Mark associated job for deletion - job = hda.creating_job_associations[0].job - if job.state in [ self.app.model.Job.states.QUEUED, self.app.model.Job.states.RUNNING, self.app.model.Job.states.NEW ]: - # Are *all* of the job's other output datasets deleted? - if job.check_if_output_datasets_deleted(): - job.mark_deleted( self.app.config.track_jobs_in_database ) - self.app.job_manager.job_stop_queue.put( job.id ) -#lib/galaxy/webapps/galaxy/api/history_contents.py:366: self.stop_hda_creating_job( hda ) -#lib/galaxy/webapps/galaxy/controllers/dataset.py:772: self.stop_hda_creating_job( hda ) - class UsesLibraryMixin: def get_library( self, trans, id, check_ownership=False, check_accessible=True ): @@ -527,7 +415,7 @@ class UsesLibraryMixinItems( SharableItemSecurityMixin ): return security_agent.get_permissions( ldda ) -class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ): +class UsesVisualizationMixin( UsesLibraryMixinItems ): """ Mixin for controllers that use Visualization objects. """ @@ -816,9 +704,14 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMix def get_tool_def( self, trans, hda ): """ Returns definition of an interactive tool for an HDA. """ - job = self.get_hda_job( hda ) + # Get dataset's job. + job = None + for job_output_assoc in hda.creating_job_associations: + job = job_output_assoc.job + break if not job: return None + tool = trans.app.toolbox.get_tool( job.tool_id ) if not tool: return None @@ -956,10 +849,48 @@ class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMix def get_hda_or_ldda( self, trans, hda_ldda, dataset_id ): """ Returns either HDA or LDDA for hda/ldda and id combination. """ if hda_ldda == "hda": - return self.get_dataset( trans, dataset_id, check_ownership=False, check_accessible=True ) + return self.get_hda( trans, dataset_id, check_ownership=False, check_accessible=True ) else: return self.get_library_dataset_dataset_association( trans, dataset_id ) + def get_hda( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): + """ + Get an HDA object by id performing security checks using + the current transaction. + """ + try: + dataset_id = trans.security.decode_id( dataset_id ) + except ( AttributeError, TypeError ): + # DEPRECATION: We still support unencoded ids for backward compatibility + try: + dataset_id = int( dataset_id ) + except ValueError: + raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) ) + + try: + data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) ) + except: + raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) ) + + if check_ownership: + # Verify ownership. + user = trans.get_user() + if not user: + error( "Must be logged in to manage Galaxy items" ) + if data.history.user != user: + error( "%s is not owned by current user" % data.__class__.__name__ ) + + if check_accessible: + current_user_roles = trans.get_current_user_roles() + + if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ): + error( "You are not allowed to access this dataset" ) + + if check_state and data.state == trans.model.Dataset.states.UPLOAD: + return trans.show_error_message( "Please wait until this dataset finishes uploading " + + "before attempting to view it." ) + return data + # -- Helper functions -- def _create_visualization( self, trans, title, type, dbkey=None, slug=None, annotation=None, save=True ): diff --git a/lib/galaxy/webapps/galaxy/api/annotations.py b/lib/galaxy/webapps/galaxy/api/annotations.py index b1859e44311..a7fcf5f84af 100644 --- a/lib/galaxy/webapps/galaxy/api/annotations.py +++ b/lib/galaxy/webapps/galaxy/api/annotations.py @@ -5,14 +5,14 @@ import logging from galaxy import web from galaxy.model.item_attrs import UsesAnnotations from galaxy.util.sanitize_html import sanitize_html -from galaxy.web.base.controller import BaseAPIController, HTTPNotImplemented, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin +from galaxy.web.base.controller import BaseAPIController, HTTPNotImplemented, UsesStoredWorkflowMixin from galaxy import managers log = logging.getLogger( __name__ ) -class BaseAnnotationsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesAnnotations ): +class BaseAnnotationsController( BaseAPIController, UsesStoredWorkflowMixin, UsesAnnotations ): @web.expose_api def index( self, trans, **kwd ): @@ -59,7 +59,7 @@ class HistoryAnnotationsController(BaseAnnotationsController): def _get_item_from_id(self, trans, idstr): decoded_idstr = trans.security.decode_id( idstr ) - history = self.history_manager.accessible_by_id( trans, decoded_idstr, trans.uer ) + history = self.history_manager.accessible_by_id( trans, decoded_idstr, trans.user ) return history @@ -67,8 +67,14 @@ class HistoryContentAnnotationsController(BaseAnnotationsController): controller_name = "history_content_annotations" tagged_item_id = "history_content_id" + def __init__( self, app ): + super( HistoryContentAnnotationsController, self ).__init__( app ) + self.hda_manager = managers.hdas.HDAManager( app ) + def _get_item_from_id(self, trans, idstr): - hda = self.get_dataset(trans, idstr) + decoded_idstr = trans.security.decode_id( idstr ) + hda = self.hda_manager.accessible_by_id( trans, decoded_idstr, trans.user ) + hda = self.hda_manager.error_if_uploading( trans, hda ) return hda diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index 817645b62af..f2eaf1aaba3 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -12,14 +12,13 @@ from galaxy.datatypes import dataproviders from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin -from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin from galaxy import managers import logging log = logging.getLogger( __name__ ) -class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryDatasetAssociationMixin ): +class DatasetsController( BaseAPIController, UsesVisualizationMixin ): def __init__( self, app ): super( DatasetsController, self ).__init__( app ) @@ -84,7 +83,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory """ Returns state of dataset. """ - msg = self.check_dataset_state( trans, dataset ) + msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) if not msg: msg = dataset.conversion_messages.DATA @@ -95,7 +94,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory Init-like method that returns state of dataset's converted datasets. Returns valid chroms for that dataset as well. """ - msg = self.check_dataset_state( trans, dataset ) + msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) if msg: return msg @@ -144,7 +143,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory return dataset.conversion_messages.NO_DATA # Dataset check. - msg = self.check_dataset_state( trans, dataset ) + msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) if msg: return msg @@ -237,7 +236,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin, UsesHistory be slow because indexes need to be created. """ # Dataset check. - msg = self.check_dataset_state( trans, dataset ) + msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) if msg: return msg diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index 56885f432f9..bbabf2d0675 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -3,11 +3,11 @@ API operations on annotations. """ import logging from galaxy import web -from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin, UsesExtendedMetadataMixin, HTTPNotImplemented +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems, UsesStoredWorkflowMixin, UsesExtendedMetadataMixin, HTTPNotImplemented log = logging.getLogger( __name__ ) -class BaseExtendedMetadataController( BaseAPIController, UsesExtendedMetadataMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin, UsesStoredWorkflowMixin ): +class BaseExtendedMetadataController( BaseAPIController, UsesExtendedMetadataMixin, UsesLibraryMixinItems, UsesStoredWorkflowMixin ): @web.expose_api def index( self, trans, **kwd ): @@ -61,8 +61,15 @@ class LibraryDatasetExtendMetadataController(BaseExtendedMetadataController): class HistoryDatasetExtendMetadataController(BaseExtendedMetadataController): controller_name = "history_dataset_extended_metadata" exmeta_item_id = "history_content_id" + + def __init__( self, app ): + super( HistoryDatasetExtendMetadataController, self ).__init__( app ) + self.hda_manager = managers.hdas.HDAManager( app ) + def _get_item_from_id(self, trans, idstr, check_writable=True): + decoded_idstr = trans.security.decode_id( idstr ) if check_writable: - return self.get_dataset( trans, idstr, check_ownership=True, check_accessible=True, check_state=True ) + return self.hda_manager.owned_by_id( trans, decoded_idstr, trans.user ) else: - return self.get_dataset( trans, idstr, check_ownership=False, check_accessible=True, check_state=True ) + hda = self.hda_manager.accessible_by_id( trans, decoded_idstr, trans.user ) + return self.hda_manager.error_if_uploading( trans, hda ) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index 8a4b37a5ec0..2051840ecbf 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -4,18 +4,19 @@ API operations on the contents of a library folder. from galaxy import web from galaxy import util from galaxy import exceptions +from galaxy import managers from galaxy.managers import folders from galaxy.web import _future_expose_api as expose_api from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous from sqlalchemy.orm.exc import MultipleResultsFound from sqlalchemy.orm.exc import NoResultFound -from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems import logging log = logging.getLogger( __name__ ) -class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin ): +class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): """ Class controls retrieval, creation and updating of folder contents. """ @@ -23,6 +24,7 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary def __init__( self, app ): super( FolderContentsController, self ).__init__( app ) self.folder_manager = folders.FolderManager() + self.hda_manager = managers.hdas.HDAManager() @expose_api_anonymous def index( self, trans, folder_id, **kwd ): @@ -265,7 +267,9 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary ldda_message = util.sanitize_html.sanitize_html( ldda_message, 'utf-8' ) rval = {} try: - hda = self.get_dataset( trans, from_hda_id, check_ownership=True, check_accessible=True, check_state=True ) + decoded_hda_id = trans.security.decode_id( from_hda_id ) + hda = self.hda_manager.owned_by_id( trans, decoded_hda_id, trans.user ) + hda = self.hda_manager.error_if_uploading( trans, hda ) folder = self.get_library_folder( trans, encoded_folder_id_16, check_accessible=True ) library = folder.parent_library diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 98bfe0681f3..24031600792 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -10,7 +10,6 @@ from galaxy.web import _future_expose_api_anonymous as expose_api_anonymous from galaxy.web import url_for from galaxy.web.base.controller import BaseAPIController -from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin from galaxy.web.base.controller import UsesLibraryMixin from galaxy.web.base.controller import UsesLibraryMixinItems from galaxy.web.base.controller import UsesTagsMixin @@ -26,8 +25,7 @@ import logging log = logging.getLogger( __name__ ) -class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociationMixin, - UsesLibraryMixin, UsesLibraryMixinItems, UsesTagsMixin ): +class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems, UsesTagsMixin ): def __init__( self, app ): super( HistoryContentsController, self ).__init__( app ) @@ -362,7 +360,7 @@ class HistoryContentsController( BaseAPIController, UsesHistoryDatasetAssociatio self.hda_deserializer.deserialize( trans, hda, payload ) #TODO: this should be an effect of deleting the hda if payload.get( 'deleted', False ): - self.stop_hda_creating_job( hda ) + self.mgrs.hdas.stop_creating_job( hda ) return self.hda_serializer.serialize_to_view( trans, hda, **self._parse_serialization_params( kwd, 'detailed' ) ) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 691baa73568..3fffb5a4201 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -9,7 +9,6 @@ from sqlalchemy.orm import aliased import json from galaxy.web import _future_expose_api as expose_api from galaxy.web.base.controller import BaseAPIController -from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin from galaxy.web.base.controller import UsesLibraryMixinItems from galaxy import exceptions from galaxy import util @@ -19,7 +18,11 @@ import logging log = logging.getLogger( __name__ ) -class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ): +class JobController( BaseAPIController, UsesLibraryMixinItems ): + + def __init__( self, app ): + super( JobController, self ).__init__( app ) + self.hda_manager = managers.hdas.HDAManager() @expose_api def index( self, trans, **kwd ): @@ -256,7 +259,8 @@ class JobController( BaseAPIController, UsesHistoryDatasetAssociationMixin, Uses if isinstance( v, dict ): if 'id' in v: if 'src' not in v or v[ 'src' ] == 'hda': - dataset = self.get_dataset( trans, v['id'], check_ownership=False, check_accessible=True ) + hda_id = trans.security.decode_id( v['id'] ) + dataset = self.hda_manager.accessible_by_id( trans, hda_id, trans.user ) else: dataset = self.get_library_dataset_dataset_association( trans, v['id'] ) if dataset is None: diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 1c01fbefaa0..31fedfaefa8 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -6,7 +6,6 @@ from galaxy import web from galaxy import exceptions from galaxy.web import _future_expose_api as expose_api from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems -from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin from galaxy.web.base.controller import HTTPBadRequest, url_for from galaxy.managers.collections_util import api_payload_to_create_params, dictify_dataset_collection_instance from galaxy.model import ExtendedMetadata, ExtendedMetadataIndex @@ -17,7 +16,11 @@ import logging log = logging.getLogger( __name__ ) -class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems, UsesHistoryDatasetAssociationMixin ): +class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + + def __init__( self, app ): + super( JobController, self ).__init__( app ) + self.hda_manager = managers.hdas.HDAManager() @expose_api def index( self, trans, library_id, **kwd ): @@ -288,7 +291,9 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar try: # check permissions on (all three?) resources: hda, library, folder #TODO: do we really need the library?? - hda = self.get_dataset( trans, from_hda_id, check_ownership=True, check_accessible=True, check_state=True ) + from_hda_id = trans.security.decode_id( from_hda_id ) + hda = self.hda_manager.owned_by_id( trans, from_hda_id, trans.user ) + hda = self.hda_manager.error_if_uploading( trans, hda ) # library = self.get_library( trans, library_id, check_accessible=True ) folder = self.get_library_folder( trans, folder_id, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 2c869af0422..52bf2633ecb 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -32,7 +32,8 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): def __init__( self, app ): super( ToolsController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ) + histories=managers.histories.HistoryManager( app ), + hdas=managers.hdas.HDAManager( app ) ) @web.expose_api @@ -282,17 +283,18 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): run_on_regions = True # Dataset check. - original_dataset = self.get_dataset( trans, payload[ 'target_dataset_id' ], check_ownership=False, check_accessible=True ) - msg = self.check_dataset_state( trans, original_dataset ) + decoded_dataset_id = trans.security.decode_id( payload.get( 'target_dataset_id' ) ) + original_dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_dataset_id, user=trans.user ) + original_dataset = self.hda_manager.error_if_uploading( trans, original_dataset ) + msg = self.mgrs.hdas.data_conversion_status( trans, original_dataset ) if msg: return msg - # # Set tool parameters--except non-hidden dataset parameters--using combination of # job's previous parameters and incoming parameters. Incoming parameters # have priority. # - original_job = self.get_hda_job( original_dataset ) + original_job = self.mgrs.hdas.creating_job( original_dataset ) tool = trans.app.toolbox.get_tool( original_job.tool_id ) if not tool: return trans.app.model.Dataset.conversion_messages.NO_TOOL diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index 5d90a0e7bb2..225ae28d8fe 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -9,7 +9,7 @@ from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings from galaxy.util import inflector, smart_str from galaxy.util.sanitize_html import sanitize_html from galaxy.util.json import loads -from galaxy.web.base.controller import BaseUIController, ERROR, SUCCESS, url_for, UsesHistoryDatasetAssociationMixin, UsesExtendedMetadataMixin +from galaxy.web.base.controller import BaseUIController, ERROR, SUCCESS, url_for, UsesExtendedMetadataMixin from galaxy.web.framework.helpers import grids, iff, time_ago, to_unicode, escape from galaxy.tools.errors import EmailErrorReporter @@ -86,14 +86,15 @@ class HistoryDatasetAssociationListGrid( grids.Grid ): .filter( model.History.deleted==False ) \ .filter( self.model_class.visible==True ) -class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAssociationMixin, UsesItemRatings, UsesExtendedMetadataMixin ): +class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, UsesExtendedMetadataMixin ): stored_list_grid = HistoryDatasetAssociationListGrid() def __init__( self, app ): super( DatasetInterface, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ) + histories=managers.histories.HistoryManager( app ), + hdas=managers.hdas.HDAManager( app ), ) def _get_job_for_dataset( self, trans, dataset_id ): @@ -529,14 +530,18 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss @web.require_login( "use Galaxy datasets" ) def get_name_and_link_async( self, trans, id=None ): """ Returns dataset's name and link. """ - dataset = self.get_dataset( trans, id, False, True ) + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) return_dict = { "name" : dataset.name, "link" : url_for( controller='dataset', action="display_by_username_and_slug", username=dataset.history.user.username, slug=trans.security.encode_id( dataset.id ) ) } return return_dict @web.expose def get_embed_html_async( self, trans, id ): """ Returns HTML for embedding a dataset in a page. """ - dataset = self.get_dataset( trans, id, False, True ) + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if dataset: return "Embedded Dataset '%s'" % dataset.name @@ -552,7 +557,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss def rate_async( self, trans, id, rating ): """ Rate a dataset asynchronously and return updated community data. """ - dataset = self.get_dataset( trans, id, check_ownership=False, check_accessible=True ) + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if not dataset: return trans.show_error_message( "The specified dataset does not exist." ) @@ -564,13 +571,16 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss @web.expose def display_by_username_and_slug( self, trans, username, slug, filename=None, preview=True ): """ Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id. """ - dataset = self.get_dataset( trans, slug, False, True ) + id = slug + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if dataset: # Filename used for composite types. if filename: return self.display( trans, dataset_id=slug, filename=filename) - truncated, dataset_data = self.get_data( dataset, preview ) + truncated, dataset_data = self.mgrs.hdas.text_data( dataset, preview ) dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) # If dataset is chunkable, get first chunk. @@ -605,17 +615,22 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss def get_item_content_async( self, trans, id ): """ Returns item content in HTML format. """ - dataset = self.get_dataset( trans, id, False, True ) + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if dataset is None: raise web.httpexceptions.HTTPNotFound() - truncated, dataset_data = self.get_data( dataset, preview=True ) + truncated, dataset_data = self.mgrs.hdas.text_data( dataset, preview=True ) # Get annotation. dataset.annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) return trans.stream_template_mako( "/dataset/item_content.mako", item=dataset, item_data=dataset_data, truncated=truncated ) @web.expose def annotate_async( self, trans, id, new_annotation=None, **kwargs ): - dataset = self.get_dataset( trans, id, False, True ) + #TODO:?? why is this an access check only? + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() if dataset and new_annotation: @@ -627,7 +642,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss @web.expose def get_annotation_async( self, trans, id ): - dataset = self.get_dataset( trans, id, False, True ) + decoded_id = trans.security.decode_id( id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) @@ -776,7 +793,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss hda.mark_deleted() hda.clear_associated_files() trans.log_event( "Dataset id %s marked as deleted" % str(id) ) - self.stop_hda_creating_job( hda ) + self.mgrs.hdas.stop_creating_job( hda ) trans.sa_session.flush() except Exception, e: msg = 'HDA deletion failed (encoded: %s, decoded: %s)' % ( dataset_id, id ) @@ -1102,7 +1119,10 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesHistoryDatasetAss if user != history.user: error_msg = error_msg + "You do not have permission to add datasets to %i requested histories. " % ( len( target_histories ) ) for dataset_id in dataset_ids: - data = self.get_dataset( trans, dataset_id, False, True ) + decoded_id = trans.security.decode_id( dataset_id ) + data = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + data = self.mgrs.hdas.error_if_uploading( data ) + if data is None: error_msg = error_msg + "You tried to copy a dataset that does not exist or that you do not have access to. " invalid_datasets += 1 diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index cc029e9da73..ba9c4757d8c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -18,7 +18,6 @@ from galaxy.web import error, url_for from galaxy.web.base.controller import BaseUIController from galaxy.web.base.controller import SharableMixin -from galaxy.web.base.controller import UsesHistoryDatasetAssociationMixin from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin from galaxy.model.item_attrs import UsesAnnotations @@ -211,8 +210,7 @@ class HistoryAllPublishedGrid( grids.Grid ): return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesItemRatings, - UsesHistoryDatasetAssociationMixin, ExportsHistoryMixin, - ImportsHistoryMixin ): + ExportsHistoryMixin, ImportsHistoryMixin ): def __init__( self, app ): super( HistoryController, self ).__init__( app ) diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index cf8cc94b88c..a92dae5de09 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -290,7 +290,8 @@ class PageController( BaseUIController, SharableMixin, def __init__( self, app ): super( PageController, self ).__init__( app ) self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ) + histories=managers.histories.HistoryManager( app ), + hdas=managers.hdas.HDAManager( app ), ) @web.expose @@ -778,10 +779,13 @@ class PageController( BaseUIController, SharableMixin, return self._get_embedded_history_html( trans, item_id ) elif item_class == model.HistoryDatasetAssociation: - dataset = self.get_dataset( trans, item_id, False, True ) + decoded_id = trans.security.decode_id( item_id ) + dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.mgrs.hdas.error_if_uploading( dataset ) + dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) if dataset: - data = self.get_data( dataset ) + data = self.mgrs.hdas.text_data( dataset ) return trans.fill_template( "dataset/embed.mako", item=dataset, item_data=data ) elif item_class == model.StoredWorkflow: diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py index 172e351b367..f7c275c5ca1 100644 --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -7,7 +7,7 @@ import urllib from paste.httpexceptions import HTTPNotFound, HTTPBadGateway -from galaxy.web.base.controller import BaseUIController, UsesHistoryDatasetAssociationMixin +from galaxy.web.base.controller import BaseUIController from galaxy import managers from galaxy import web @@ -20,7 +20,7 @@ from galaxy.util.json import dumps import logging log = logging.getLogger( __name__ ) -class RootController( BaseUIController, UsesHistoryDatasetAssociationMixin, UsesAnnotations ): +class RootController( BaseUIController, UsesAnnotations ): """ Controller class that maps to the url root of Galaxy (i.e. '/'). """ diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index d3ec6f74c49..bdf72e6c0b2 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -7,6 +7,7 @@ from sqlalchemy import desc, or_, and_ from paste.httpexceptions import HTTPNotFound, HTTPBadRequest from galaxy import model, web +from galaxy import managers from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings from galaxy.web.base.controller import BaseUIController, SharableMixin, UsesVisualizationMixin from galaxy.web.framework.helpers import time_ago, grids, escape @@ -271,9 +272,8 @@ class VisualizationAllPublishedGrid( grids.Grid ): return query.filter( self.model_class.deleted==False ).filter( self.model_class.published==True ) -class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, - UsesVisualizationMixin, - UsesItemRatings ): +class VisualizationController( BaseUIController, SharableMixin, UsesVisualizationMixin, + UsesAnnotations, UsesItemRatings ): _user_list_grid = VisualizationListGrid() _published_list_grid = VisualizationAllPublishedGrid() _libraries_grid = LibrarySelectionGrid() @@ -281,6 +281,12 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, _history_datasets_grid = HistoryDatasetsSelectionGrid() _tracks_grid = TracksterSelectionGrid() + def __init__( self, app ): + super( VisualizationController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + hdas=managers.hdas.HDAManager( app ), + ) + # # -- Functions for listing visualizations. -- # @@ -862,7 +868,9 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, # use dbkey from dataset to be added or from incoming parameter dbkey = None if new_dataset_id: - dbkey = self.get_dataset( trans, new_dataset_id ).dbkey + decoded_id = trans.security.decode_id( new_dataset_id ) + hda = self.mgrs.hdas.owned_by_id( trans, decoded_id, trans.user ) + dbkey = hda.dbkey if dbkey == '?': dbkey = kwargs.get( "dbkey", None ) @@ -970,11 +978,12 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, # Loading a shared visualization. viz = self.get_visualization( trans, id ) viz_config = self.get_visualization_config( trans, viz ) - dataset = self.get_dataset( trans, viz_config[ 'dataset_id' ] ) + decoded_id = trans.security.decode_id( viz_config[ 'dataset_id' ] ) + dataset = self.mgrs.hdas.owned_by_id( trans, decoded_id, trans.user ) else: # Loading new visualization. dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) - job = self.get_hda_job( dataset ) + job = self.mgrs.hdas.creating_job( dataset ) viz_config = { 'dataset_id': dataset_id, 'tool_id': job.tool_id, @@ -1004,7 +1013,9 @@ class VisualizationController( BaseUIController, SharableMixin, UsesAnnotations, # get the hda if we can, then its data using the phyloviz parsers if dataset_id: - hda = self.get_dataset( trans, dataset_id, check_ownership=False, check_accessible=True ) + decoded_id = trans.security.decode_id( dataset_id ) + hda = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) + hda = self.mgrs.hdas.error_if_uploading( hda ) else: return trans.show_message( "Phyloviz couldn't find a dataset_id" ) From 260e1a9096d4f9847fb8711ac776fceb42d287e1 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 9 Jan 2015 17:40:01 -0500 Subject: [PATCH 144/473] Remove UsesHistoryDatasetAssociationMixin --- lib/galaxy/webapps/galaxy/api/extended_metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index bbabf2d0675..63b78cae1ac 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -3,6 +3,8 @@ API operations on annotations. """ import logging from galaxy import web +from galaxy import managers + from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems, UsesStoredWorkflowMixin, UsesExtendedMetadataMixin, HTTPNotImplemented log = logging.getLogger( __name__ ) From 0762bc81f11fd0876bbbcc2ed679110c4df9d3b2 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Fri, 9 Jan 2015 17:43:36 -0500 Subject: [PATCH 145/473] See ...this is what I get trying to commit on a Friday Evening... --- lib/galaxy/webapps/galaxy/api/folder_contents.py | 2 +- lib/galaxy/webapps/galaxy/api/jobs.py | 3 ++- lib/galaxy/webapps/galaxy/api/library_contents.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index 2051840ecbf..ff2815c226c 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -24,7 +24,7 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary def __init__( self, app ): super( FolderContentsController, self ).__init__( app ) self.folder_manager = folders.FolderManager() - self.hda_manager = managers.hdas.HDAManager() + self.hda_manager = managers.hdas.HDAManager( app ) @expose_api_anonymous def index( self, trans, folder_id, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 3fffb5a4201..6517be6846a 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -13,6 +13,7 @@ from galaxy.web.base.controller import UsesLibraryMixinItems from galaxy import exceptions from galaxy import util from galaxy import model +from galaxy import managers import logging log = logging.getLogger( __name__ ) @@ -22,7 +23,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): def __init__( self, app ): super( JobController, self ).__init__( app ) - self.hda_manager = managers.hdas.HDAManager() + self.hda_manager = managers.hdas.HDAManager( app ) @expose_api def index( self, trans, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 31fedfaefa8..304af18d276 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -4,6 +4,7 @@ API operations on the contents of a data library. from galaxy import util from galaxy import web from galaxy import exceptions +from galaxy import managers from galaxy.web import _future_expose_api as expose_api from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems from galaxy.web.base.controller import HTTPBadRequest, url_for @@ -19,8 +20,8 @@ log = logging.getLogger( __name__ ) class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): def __init__( self, app ): - super( JobController, self ).__init__( app ) - self.hda_manager = managers.hdas.HDAManager() + super( LibraryContentsController, self ).__init__( app ) + self.hda_manager = managers.hdas.HDAManager( app ) @expose_api def index( self, trans, library_id, **kwd ): From a513165c04521c48e39ff6195ca9f302e6c4fe8c Mon Sep 17 00:00:00 2001 From: Nicola Soranzo Date: Fri, 9 Jan 2015 22:57:23 +0000 Subject: [PATCH 146/473] API doc enhancements. --- .../webapps/galaxy/api/library_contents.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 6f1d4cf139d..33b8f32201a 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -29,12 +29,12 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar .. note:: May be slow! Returns all content traversing recursively through all folders. .. seealso:: :class:`galaxy.webapps.galaxy.api.FolderContentsController.index` for a non-recursive solution - :param library_id: encoded id string of the library - :type library_id: string + :param library_id: the encoded id of the library + :type library_id: str :returns: list of dictionaries of the form: * id: the encoded id of the library item - * name: the 'libary path' + * name: the 'library path' or relationship of the library item to the root * type: 'file' or 'folder' * url: the url to get detailed information on the library item @@ -109,7 +109,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar :param id: the encoded id of the library item to return :type id: str - :param library_id: encoded id string of the library that contains this item + :param library_id: the encoded id of the library that contains this item :type library_id: str :returns: detailed library item information @@ -146,21 +146,34 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar the HDA's encoded id in ``from_hda_id`` (and optionally ``ldda_message``). :type library_id: str - :param library_id: encoded id string of the library where to create the new item + :param library_id: the encoded id of the library where to create the new item :type payload: dict :param payload: dictionary structure containing: - * folder_id: the parent folder of the new item + * folder_id: the encoded id of the parent folder of the new item * create_type: the type of item to create ('file', 'folder' or 'collection') - * from_hda_id: (optional) the id of an accessible HDA to copy into the - library + * from_hda_id: (optional, only if create_type is 'file') the + encoded id of an accessible HDA to copy into the library * ldda_message: (optional) the new message attribute of the LDDA created * extended_metadata: (optional) dub-dictionary containing any extended metadata to associate with the item - * link_data_only: (optional) either 'copy_files' (default) or 'link_to_files' * upload_option: (optional) one of 'upload_file' (default), 'upload_directory' or 'upload_paths' - * server_dir: (optional) only if upload_option is 'upload_directory' - * filesystem_paths: (optional) only if upload_option is 'upload_paths' and the user is an admin + * server_dir: (optional, only if upload_option is + 'upload_directory') relative path of the subdirectory of Galaxy + ``library_import_dir`` to upload. All and only the files (i.e. + no subdirectories) contained in the specified directory will be + uploaded. + * filesystem_paths: (optional, only if upload_option is + 'upload_paths' and the user is an admin) file paths on the + Galaxy server to upload to the library, one file per line + * link_data_only: (optional, only when upload_option is + 'upload_directory' or 'upload_paths') either 'copy_files' + (default) or 'link_to_files'. Setting to 'link_to_files' + symlinks instead of copying the files + * name: (optional, only if create_type is 'folder') name of the + folder to create + * description: (optional, only if create_type is 'folder') + description of the folder to create :rtype: dict :returns: a dictionary containing the id, name, @@ -269,7 +282,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar def _copy_hda_to_library_folder( self, trans, from_hda_id, library_id, folder_id, ldda_message='' ): """ - Copies hda ``from_hda_id`` to library folder ``library_folder_id`` optionally + Copies hda ``from_hda_id`` to library folder ``folder_id``, optionally adding ``ldda_message`` to the new ldda's ``message``. ``library_contents.create`` will branch to this if called with 'from_hda_id' @@ -322,7 +335,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar :type id: str :param id: the encoded id of the library item to return :type library_id: str - :param library_id: encoded id string of the library that contains this item + :param library_id: the encoded id of the library that contains this item :type payload: dict :param payload: dictionary structure containing:: 'converted_dataset_id': From ebfc74011db01d4c65cb921d4b7fcf12851cad4d Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Sun, 11 Jan 2015 12:02:11 -0500 Subject: [PATCH 147/473] ToolForm/Workflow: Generalize/revise tool model building process, insert form into workflow editor --- .../scripts/galaxy.workflow_editor.canvas.js | 12 +- client/galaxy/scripts/mvc/tools/tools-form.js | 335 +----------------- client/galaxy/scripts/mvc/tools/tools-jobs.js | 2 +- .../galaxy/scripts/mvc/tools/tools-section.js | 16 +- .../scripts/mvc/tools/tools-select-content.js | 9 +- client/galaxy/scripts/mvc/tools/tools-tree.js | 4 +- client/galaxy/scripts/mvc/ui/ui-misc.js | 14 +- client/galaxy/scripts/mvc/ui/ui-options.js | 11 +- .../scripts/mvc/ui/ui-select-default.js | 11 +- client/galaxy/scripts/utils/utils.js | 38 +- lib/galaxy/tools/__init__.py | 332 ++++++++++++++++- lib/galaxy/webapps/galaxy/api/tools.py | 309 +--------------- .../webapps/galaxy/controllers/workflow.py | 16 +- lib/galaxy/workflow/modules.py | 7 +- .../scripts/galaxy.workflow_editor.canvas.js | 12 +- static/scripts/mvc/tools/tools-form.js | 335 +----------------- static/scripts/mvc/tools/tools-jobs.js | 2 +- static/scripts/mvc/tools/tools-section.js | 16 +- .../scripts/mvc/tools/tools-select-content.js | 9 +- static/scripts/mvc/tools/tools-tree.js | 4 +- static/scripts/mvc/ui/ui-misc.js | 14 +- static/scripts/mvc/ui/ui-options.js | 11 +- static/scripts/mvc/ui/ui-select-default.js | 11 +- .../packed/galaxy.workflow_editor.canvas.js | 2 +- static/scripts/packed/mvc/tools/tools-form.js | 2 +- static/scripts/packed/mvc/tools/tools-jobs.js | 2 +- .../scripts/packed/mvc/tools/tools-section.js | 2 +- .../packed/mvc/tools/tools-select-content.js | 2 +- static/scripts/packed/mvc/tools/tools-tree.js | 2 +- static/scripts/packed/mvc/ui/ui-misc.js | 2 +- static/scripts/packed/mvc/ui/ui-options.js | 2 +- .../packed/mvc/ui/ui-select-default.js | 2 +- static/scripts/packed/utils/utils.js | 2 +- static/scripts/utils/utils.js | 38 +- templates/webapps/galaxy/tool_form.mako | 4 +- .../galaxy/workflow/editor_tool_form.mako | 264 ++++++++------ 36 files changed, 699 insertions(+), 1157 deletions(-) diff --git a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js index b22dc903c69..918ecf0d124 100644 --- a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js +++ b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js @@ -805,7 +805,7 @@ var Node = Backbone.Model.extend({ nodeView.addDataOutput( output ); } ); nodeView.render(); - workflow.node_changed( this ); + workflow.node_changed( this, true); }, update_field_data : function( data ) { var node = this; @@ -1094,9 +1094,9 @@ $.extend( Workflow.prototype, { this.active_node = node; } }, - node_changed : function ( node ) { + node_changed : function ( node, force ) { this.has_changes = true; - if ( this.active_node == node ) { + if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) { // Reactive with new form_html this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped) parent.show_form_for_tool( node.form_html + node.tooltip, node ); @@ -1305,6 +1305,12 @@ function add_node( type, title_text, tool_id ) { return node; } +function update_node( data ) { + var node = workflow.active_node; + if (node) { + node.update_field_data(data, true); + } +} var ext_to_type = null; var type_to_type = null; diff --git a/client/galaxy/scripts/mvc/tools/tools-form.js b/client/galaxy/scripts/mvc/tools/tools-form.js index a92abe0ad34..94a0af0a7f4 100644 --- a/client/galaxy/scripts/mvc/tools/tools-form.js +++ b/client/galaxy/scripts/mvc/tools/tools-form.js @@ -1,79 +1,28 @@ /** This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. */ -define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', - 'mvc/citation/citation-model', 'mvc/citation/citation-view', - 'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree', 'mvc/tools/tools-jobs'], - function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView, - Tools, ToolTemplate, ToolContent, ToolSection, ToolTree, ToolJobs) { +define(['utils/utils', 'mvc/ui/ui-misc', 'mvc/tools/tools-form-base', 'mvc/tools/tools-jobs'], + function(Utils, Ui, ToolFormBase, ToolJobs) { // create form view - var View = Backbone.View.extend({ - // base element - container: 'body', - + var View = ToolFormBase.extend({ // initialize initialize: function(options) { - // log options - console.debug(options); - - // link galaxy modal or create one - var galaxy = parent.Galaxy; - if (galaxy && galaxy.modal) { - this.modal = galaxy.modal; - } else { - this.modal = new Ui.Modal.View(); - } - - // check if the user is an admin - if (galaxy && galaxy.currUser) { - this.is_admin = galaxy.currUser.get('is_admin') - } else { - this.is_admin = false; - } - - // link options - this.options = options; - - // create deferred processing queue handler - // this handler reduces the number of requests to the api by filtering redundant requests - this.deferred = new Deferred(); - - // set element - this.setElement('
'); - - // add to main element - $(this.container).append(this.$el); - - // build this form - this._buildForm(); - }, - - /** Shows the final message (usually upon successful job submission) - */ - reciept: function($el) { - $(this.container).empty(); - $(this.container).append($el); - }, - - /** Highlight and scroll to input element (currently only used for error notifications) - */ - highlight: function (input_id, message, silent) { - // get input field - var input_element = this.element_list[input_id]; - - // check input element - if (input_element) { - // mark error - input_element.error(message || 'Please verify this parameter.'); - - // scroll to first input element - if (!silent) { - $(this.container).animate({ - scrollTop: input_element.$el.offset().top - 20 - }, 500); - } + var self = this; + this.job_handler = new ToolJobs(this); + this.buttons = { + execute : new Ui.Button({ + icon : 'fa-check', + tooltip : 'Execute: ' + options.name, + title : 'Execute', + cls : 'btn btn-primary', + floating : 'clear', + onclick : function() { + self.job_handler.submit(); + } + }) } + ToolFormBase.prototype.initialize.call(this, options); }, /** Builds a new model through api call and recreates the entire form @@ -250,256 +199,6 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', console.debug(response); } }); - }, - - /** Main tool form build function. This function is called once a new model is available. - */ - _buildForm: function() { - // link this - var self = this; - - // reset events - this.off('refresh'); - this.off('reset'); - - // reset field list, which contains the input field elements - this.field_list = {}; - - // reset sequential input definition list, which contains the input definitions as provided from the api - this.input_list = {}; - - // reset input element list, which contains the dom elements of each input element (includes also the input field) - this.element_list = {}; - - // creates a tree/json data structure from the input form - this.tree = new ToolTree(this); - - // creates the job handler - this.job_handler = new ToolJobs(this); - - // request history content and build form - this.content = new ToolContent(this); - - // link model options - var options = this.options; - - // create ui elements - this._renderForm(options); - - // rebuild the underlying data structure - this.tree.finalize(); - - // show errors - if (options.errors) { - var error_messages = this.tree.matchResponse(options.errors); - for (var input_id in error_messages) { - this.highlight(input_id, error_messages[input_id], true); - } - } - - // add refresh listener - this.on('refresh', function() { - // by using/reseting the deferred ajax queue the number of redundant calls is reduced - self.deferred.reset(); - self.deferred.execute(function(){self._updateModel()}); - }); - - // add reset listener - this.on('reset', function() { - for (var i in this.element_list) { - this.element_list[i].reset(); - } - }); - }, - - /** Renders the UI elements required for the form - */ - _renderForm: function(options) { - // link this - var self = this; - - // create message view - this.message = new Ui.Message(); - - // button for version selection - var requirements_button = new Ui.ButtonIcon({ - icon : 'fa-info-circle', - title : 'Requirements', - tooltip : 'Display tool requirements', - onclick : function() { - if (!this.visible) { - this.visible = true; - self.message.update({ - persistent : true, - message : ToolTemplate.requirements(options), - status : 'info' - }); - } else { - this.visible = false; - self.message.update({ - message : '' - }); - } - } - }); - if (!options.requirements || options.requirements.length == 0) { - requirements_button.$el.hide(); - } - - // button for version selection - var versions_button = new Ui.ButtonMenu({ - icon : 'fa-cubes', - title : 'Versions', - tooltip : 'Select another tool version' - }); - if (options.versions && options.versions.length > 1) { - for (var i in options.versions) { - var version = options.versions[i]; - if (version != options.version) { - versions_button.addMenu({ - title : 'Switch to ' + version, - version : version, - icon : 'fa-cube', - onclick : function() { - // here we update the tool version (some tools encode the version also in the id) - options.id = options.id.replace(options.version, this.version); - options.version = this.version; - - // rebuild the model and form - self.deferred.reset(); - self.deferred.execute(function(){self._buildModel()}); - } - }); - } - } - } else { - versions_button.$el.hide(); - } - - // button menu - var menu_button = new Ui.ButtonMenu({ - icon : 'fa-caret-down', - title : 'Options', - tooltip : 'View available options' - }); - - // configure button selection - if(options.biostar_url) { - // add question option - menu_button.addMenu({ - icon : 'fa-question-circle', - title : 'Question?', - tooltip : 'Ask a question about this tool (Biostar)', - onclick : function() { - window.open(options.biostar_url + '/p/new/post/'); - } - }); - - // create search button - menu_button.addMenu({ - icon : 'fa-search', - title : 'Search', - tooltip : 'Search help for this tool (Biostar)', - onclick : function() { - window.open(options.biostar_url + '/t/' + options.id + '/'); - } - }); - }; - - // create share button - menu_button.addMenu({ - icon : 'fa-share', - title : 'Share', - tooltip : 'Share this tool', - onclick : function() { - prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id); - } - }); - - // add admin operations - if (this.is_admin) { - // create download button - menu_button.addMenu({ - icon : 'fa-download', - title : 'Download', - tooltip : 'Download this tool', - onclick : function() { - window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download'; - } - }); - } - - // create tool form section - this.section = new ToolSection.View(self, { - inputs : options.inputs, - cls : 'ui-table-plain' - }); - - // switch to classic tool form mako if the form definition is incompatible - if (this.incompatible) { - this.$el.hide(); - $('#tool-form-classic').show(); - return; - } - - // create portlet - this.portlet = new Portlet.View({ - icon : 'fa-wrench', - title : '' + options.name + ' ' + options.description + ' (Galaxy Tool Version ' + options.version + ')', - cls : 'ui-portlet-slim', - operations: { - requirements : requirements_button, - menu : menu_button, - versions : versions_button - }, - buttons: { - execute : new Ui.Button({ - icon : 'fa-check', - tooltip : 'Execute: ' + options.name, - title : 'Execute', - cls : 'btn btn-primary', - floating : 'clear', - onclick : function() { - self.job_handler.submit(); - } - }) - } - }); - - // append message - this.portlet.append(this.message.$el, true); - - // append tool section - this.portlet.append(this.section.$el); - - // start form - this.$el.empty(); - this.$el.append(this.portlet.$el); - - // append help - if (options.help != '') { - this.$el.append(ToolTemplate.help(options.help)); - } - - // append citations - if (options.citations) { - var $citations = $('
'); - var citations = new CitationModel.ToolCitationCollection(); - citations.tool_id = options.id; - var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } ); - citation_list_view.render(); - citations.fetch(); - this.$el.append($citations); - } - - // show message if available in model - if (options.message) { - this.message.update({ - persistent : true, - status : 'warning', - message : options.message - }); - } } }); diff --git a/client/galaxy/scripts/mvc/tools/tools-jobs.js b/client/galaxy/scripts/mvc/tools/tools-jobs.js index 469d9b67daf..790b44dfa0c 100644 --- a/client/galaxy/scripts/mvc/tools/tools-jobs.js +++ b/client/galaxy/scripts/mvc/tools/tools-jobs.js @@ -98,7 +98,7 @@ return Backbone.Model.extend({ } // validate non-optional fields - if (!input_def.optional && input_field.validate && !input_field.validate()) { + if (!input_def.optional && input_value == null) { this.app.highlight(input_id); return false; } diff --git a/client/galaxy/scripts/mvc/tools/tools-section.js b/client/galaxy/scripts/mvc/tools/tools-section.js index fab21d2a8f7..8038a403079 100644 --- a/client/galaxy/scripts/mvc/tools/tools-section.js +++ b/client/galaxy/scripts/mvc/tools/tools-section.js @@ -382,6 +382,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Data input field */ _fieldData : function(input_def) { + if (this.app.workflow) { + var extensions = Utils.textify(input_def.extensions.toString()); + input_def.info = 'Data input \'' + input_def.name + '\' (' + extensions + ')'; + return this._fieldHidden(input_def); + } var self = this; return new SelectContent.View(this.app, { id : 'field-' + input_def.id, @@ -398,6 +403,14 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Select/Checkbox/Radio options field */ _fieldSelect : function (input_def) { + // show text field in workflow + if (this.app.workflow && input_def.is_dynamic) { + if (!Utils.validate(input_def.value)) { + input_def.value = ''; + } + return this._fieldText(input_def); + } + // configure options fields var options = []; for (var i in input_def.options) { @@ -475,7 +488,8 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep */ _fieldHidden : function(input_def) { return new Ui.Hidden({ - id : 'field-' + input_def.id + id : 'field-' + input_def.id, + info : input_def.info }); }, diff --git a/client/galaxy/scripts/mvc/tools/tools-select-content.js b/client/galaxy/scripts/mvc/tools/tools-select-content.js index 7453963d69d..fa7e64d922c 100644 --- a/client/galaxy/scripts/mvc/tools/tools-select-content.js +++ b/client/galaxy/scripts/mvc/tools/tools-select-content.js @@ -36,14 +36,7 @@ var View = Backbone.View.extend({ this.list = {}; // error messages - var extensions = options.extensions.toString(); - if (extensions) { - extensions = extensions.replace(/,/g, ', '); - var pos = extensions.lastIndexOf(', '); - if (pos != -1) { - extensions = extensions.substr(0, pos) + ' or ' + extensions.substr(pos+1); - } - } + var extensions = Utils.textify(options.extensions); var hda_error = 'No dataset available.'; if (extensions) { hda_error = 'No ' + extensions + ' dataset available.'; diff --git a/client/galaxy/scripts/mvc/tools/tools-tree.js b/client/galaxy/scripts/mvc/tools/tools-tree.js index af08497a719..3a8cde64e78 100644 --- a/client/galaxy/scripts/mvc/tools/tools-tree.js +++ b/client/galaxy/scripts/mvc/tools/tools-tree.js @@ -2,7 +2,7 @@ This class maps the tool form dom to an api compatible javascript dictionary. */ // dependencies -define([], function() { +define(['utils/utils'], function(Utils) { // tool form tree return Backbone.Model.extend({ @@ -120,7 +120,7 @@ return Backbone.Model.extend({ // handle default value if (!field.skip) { - if (input.optional && field.validate && !field.validate()) { + if (field.validate && !field.validate(value)) { value = null; } add (job_input_id, input.id, value); diff --git a/client/galaxy/scripts/mvc/ui/ui-misc.js b/client/galaxy/scripts/mvc/ui/ui-misc.js index 666fd78882c..efedef2c0c3 100644 --- a/client/galaxy/scripts/mvc/ui/ui-misc.js +++ b/client/galaxy/scripts/mvc/ui/ui-misc.js @@ -412,7 +412,7 @@ var Hidden = Backbone.View.extend({ initialize : function(options) { // configure options this.options = options; - + // create new element this.setElement(this._template(this.options)); @@ -425,14 +425,20 @@ var Hidden = Backbone.View.extend({ // value value : function (new_val) { if (new_val !== undefined) { - this.$el.val(new_val); + this.$('hidden').val(new_val); } - return this.$el.val(); + return this.$('hidden').val(); }, // element _template: function(options) { - return ''; + var tmpl = '
'; + if (options.info) { + tmpl += ''; + } + tmpl += '' + + '
'; + return tmpl; } }); diff --git a/client/galaxy/scripts/mvc/ui/ui-options.js b/client/galaxy/scripts/mvc/ui/ui-options.js index 8d47dfe0330..21e0911747f 100644 --- a/client/galaxy/scripts/mvc/ui/ui-options.js +++ b/client/galaxy/scripts/mvc/ui/ui-options.js @@ -137,16 +137,7 @@ var Base = Backbone.View.extend({ /** Validate the selected option/options */ validate: function() { - var current = this.value(); - if (!(current instanceof Array)) { - current = [current]; - } - for (var i in current) { - if ([null, 'null', undefined].indexOf(current[i]) > -1) { - return false; - } - } - return true; + return Utils.validate(this.value()); }, /** Wait message during request processing diff --git a/client/galaxy/scripts/mvc/ui/ui-select-default.js b/client/galaxy/scripts/mvc/ui/ui-select-default.js index 6171ca3043d..99b63065e92 100644 --- a/client/galaxy/scripts/mvc/ui/ui-select-default.js +++ b/client/galaxy/scripts/mvc/ui/ui-select-default.js @@ -96,16 +96,7 @@ var View = Backbone.View.extend({ /** Validate the current selection */ validate: function() { - var current = this.value(); - if (!(current instanceof Array)) { - current = [current]; - } - for (var i in current) { - if ([null, 'null', undefined].indexOf(current[i]) > -1) { - return false; - } - } - return true; + return Utils.validate(this.value()); }, /** Return the label/text of the current selection diff --git a/client/galaxy/scripts/utils/utils.js b/client/galaxy/scripts/utils/utils.js index 86432291d25..be978959a7d 100644 --- a/client/galaxy/scripts/utils/utils.js +++ b/client/galaxy/scripts/utils/utils.js @@ -14,6 +14,40 @@ function sanitize(content) { return $('
').text(content).html(); }; +/** + * Validate atomic values or list of values + * usually used for selectable options + * @param{String} value - Value or list to be validated + */ +function validate (value) { + if (!(value instanceof Array)) { + value = [value]; + } + for (var i in value) { + if (['None', null, 'null', undefined, 'undefined'].indexOf(value[i]) > -1) { + return false; + } + } + return true; +}; + +/** + * Convert list to pretty string + * @param{String} lst - List of strings to be converted in human readable list sentence + */ +function textify(lst) { + var lst = lst.toString(); + if (lst) { + lst = lst.replace(/,/g, ', '); + var pos = lst.lastIndexOf(', '); + if (pos != -1) { + lst = lst.substr(0, pos) + ' or ' + lst.substr(pos+1); + } + return lst; + } + return ''; +}; + /** * Request handler for GET * @param{String} url - Url request is made to @@ -212,7 +246,9 @@ return { time: time, wrap: wrap, request: request, - sanitize: sanitize + sanitize: sanitize, + textify: textify, + validate: validate }; }); diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 2c44b60b5bf..9d0231ddc1c 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -33,7 +33,7 @@ from galaxy.tools.actions.upload import UploadToolAction from galaxy.tools.actions.data_source import DataSourceToolAction from galaxy.tools.actions.data_manager import DataManagerToolAction from galaxy.tools.deps import build_dependency_manager -from galaxy.tools.parameters import check_param, params_from_strings, params_to_strings +from galaxy.tools.parameters import params_to_incoming, check_param, params_from_strings, params_to_strings from galaxy.tools.parameters import output_collect from galaxy.tools.parameters.basic import (BaseURLToolParameter, DataToolParameter, HiddenToolParameter, LibraryDatasetToolParameter, @@ -2073,6 +2073,336 @@ class Tool( object, Dictifiable ): return tool_dict + def to_json (self, trans, **kwd): + """ + Recursively creates a tool dictionary containing repeats, dynamic options and updated states. + """ + job_id = kwd.get('job_id', None) + dataset_id = kwd.get('dataset_id', None) + + # load job details if provided + job = None + if job_id: + try: + job_id = trans.security.decode_id( job_id ) + job = trans.sa_session.query( trans.app.model.Job ).get( job_id ) + except Exception, exception: + trans.response.status = 500 + log.error('Failed to retrieve job.') + return { 'error': 'Failed to retrieve job.' } + elif dataset_id: + try: + dataset_id = trans.security.decode_id( dataset_id ) + data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( dataset_id ) + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), data.dataset ) ): + trans.response.status = 500 + log.error('User has no access to dataset.') + return { 'error': 'User has no access to dataset.' } + job = data.creating_job + if not job: + trans.response.status = 500 + log.error('Creating job not found.') + return { 'error': 'Creating job not found.' } + except Exception, exception: + trans.response.status = 500 + log.error('Failed to get job information.') + return { 'error': 'Failed to get job information.' } + + # load job parameters into incoming + tool_message = '' + if job: + try: + job_params = job.get_param_values( trans.app, ignore_errors = True ) + job_messages = self.check_and_update_param_values( job_params, trans, update_values=False ) + tool_message = self._compare_tool_version(trans, job) + params_to_incoming( kwd, self.inputs, job_params, trans.app ) + except Exception, exception: + trans.response.status = 500 + return { 'error': str( exception ) } + + # create parameter object + params = galaxy.util.Params( kwd, sanitize = False ) + + # convert value to jsonifiable value + def jsonify(v): + # check if value is numeric + isnumber = False + try: + float(v) + isnumber = True + except Exception: + pass + + # fix hda parsing + if isinstance(v, trans.app.model.HistoryDatasetAssociation): + return { + 'id' : trans.security.encode_id(v.id), + 'src' : 'hda' + } + elif isinstance(v, bool): + if v is True: + return 'true' + else: + return 'false' + elif isinstance(v, basestring) or isnumber: + return v + else: + return None + + # ensures that input dictionary is jsonifiable + def sanitize(dict): + # get current value + value = dict['value'] if 'value' in dict else None + + # jsonify by type + if dict['type'] in ['data']: + if isinstance(value, list): + value = [ jsonify(v) for v in value ] + else: + value = [ jsonify(value) ] + value = { 'values': value } + elif isinstance(value, list): + value = [ jsonify(v) for v in value ] + else: + value = jsonify(value) + + # update and return + dict['value'] = value + return dict + + # initialize state using default parameters + def initialize_state(trans, inputs, state, context=None): + context = ExpressionContext(state, context) + for input in inputs.itervalues(): + state[input.name] = input.get_initial_value(trans, context) + + # check the current state of a value and update it if necessary + def check_state(trans, input, default_value, context): + value = default_value + error = 'State validation failed.' + try: + # resolves the inconsistent definition of boolean parameters (see base.py) without modifying shared code + if input.type == 'boolean' and isinstance(default_value, basestring): + value, error = [util.string_as_bool(default_value), None] + else: + value, error = check_param(trans, input, default_value, context) + except Exception, err: + log.error('Checking parameter failed. %s', str(err)) + pass + return [value, error] + + # populates state with incoming url parameters + def populate_state(trans, inputs, state, errors, incoming, prefix="", context=None ): + context = ExpressionContext(state, context) + for input in inputs.itervalues(): + key = prefix + input.name + if input.type == 'repeat': + group_state = state[input.name] + rep_index = 0 + del group_state[:] + while True: + rep_name = "%s_%d" % (key, rep_index) + if not any([incoming_key.startswith(rep_name) for incoming_key in incoming.keys()]): + break + if rep_index < input.max: + new_state = {} + new_state['__index__'] = rep_index + initialize_state(trans, input.inputs, new_state, context) + group_state.append(new_state) + populate_state(trans, input.inputs, new_state, errors, incoming, prefix=rep_name + "|", context=context) + rep_index += 1 + elif input.type == 'conditional': + group_state = state[input.name] + group_prefix = "%s|" % ( key ) + test_param_key = group_prefix + input.test_param.name + default_value = incoming.get(test_param_key, group_state.get(input.test_param.name, None)) + value, error = check_state(trans, input.test_param, default_value, context) + if error: + errors[test_param_key] = error + else: + try: + current_case = input.get_current_case(value, trans) + group_state = state[input.name] = {} + initialize_state(trans, input.cases[current_case].inputs, group_state, context) + populate_state( trans, input.cases[current_case].inputs, group_state, errors, incoming, prefix=group_prefix, context=context) + group_state['__current_case__'] = current_case + except Exception, e: + errors[test_param_key] = 'The selected case is unavailable/invalid.' + pass + group_state[input.test_param.name] = value + else: + default_value = incoming.get(key, state.get(input.name, None)) + value, error = check_state(trans, input, default_value, context) + if error: + errors[key] = error + state[input.name] = value + + # builds tool model including all attributes + def iterate(group_inputs, inputs, state_inputs, other_values=None): + other_values = ExpressionContext( state_inputs, other_values ) + for input_index, input in enumerate( inputs.itervalues() ): + # create model dictionary + tool_dict = input.to_dict(trans) + if tool_dict is None: + continue + + # state for subsection/group + group_state = state_inputs[input.name] + + # iterate and update values + if input.type == 'repeat': + group_cache = tool_dict['cache'] = {} + for i in range( len( group_state ) ): + group_cache[i] = {} + iterate( group_cache[i], input.inputs, group_state[i], other_values ) + elif input.type == 'conditional': + if 'test_param' in tool_dict: + test_param = tool_dict['test_param'] + test_param['value'] = jsonify(group_state[test_param['name']]) + if '__current_case__' in group_state: + i = group_state['__current_case__'] + iterate(tool_dict['cases'][i]['inputs'], input.cases[i].inputs, group_state, other_values) + else: + # create input dictionary, try to pass other_values if to_dict function supports it e.g. dynamic options + try: + tool_dict = input.to_dict(trans, other_values=other_values) + except Exception: + pass + + # identify name + input_name = tool_dict.get('name') + if input_name: + # update input value from tool state + if input_name in state_inputs: + tool_dict['value'] = state_inputs[input_name] + + # sanitize if value exists + tool_dict = sanitize(tool_dict) + + # backup final input dictionary + group_inputs[input_index] = tool_dict + + # sanatization for the final tool state + def sanitize_state(state): + keys = None + if isinstance(state, dict): + keys = state + elif isinstance(state, list): + keys = range( len(state) ) + if keys: + for k in keys: + if isinstance(state[k], dict) or isinstance(state[k], list): + sanitize_state(state[k]) + else: + state[k] = jsonify(state[k]) + + # do param translation here, used by datasource tools + if self.input_translator: + self.input_translator.translate( params ) + + # initialize and populate tool state + state_inputs = {} + state_errors = {} + initialize_state(trans, self.inputs, state_inputs) + populate_state(trans, self.inputs, state_inputs, state_errors, params.__dict__) + + # create basic tool model + tool_model = self.to_dict(trans) + tool_model['inputs'] = {} + + # build tool model and tool state + iterate(tool_model['inputs'], self.inputs, state_inputs, '') + + # sanitize tool state + sanitize_state(state_inputs) + + # load tool help + tool_help = '' + if self.help: + tool_help = self.help + tool_help = tool_help.render( static_path=url_for( '/static' ), host_url=url_for('/', qualified=True) ) + if type( tool_help ) is not unicode: + tool_help = unicode( tool_help, 'utf-8') + + # check if citations exist + tool_citations = False + if self.citations: + tool_citations = True + + # get tool versions + tool_versions = [] + tools = self.app.toolbox.get_loaded_tools_by_lineage(self.id) + for t in tools: + tool_versions.append(t.version) + + ## add information with underlying requirements and their versions + tool_requirements = [] + if self.requirements: + for requirement in self.requirements: + tool_requirements.append({ + 'name' : requirement.name, + 'version' : requirement.version + }) + + # add additional properties + tool_model.update({ + 'help' : tool_help, + 'citations' : tool_citations, + 'biostar_url' : trans.app.config.biostar_url, + 'message' : tool_message, + 'versions' : tool_versions, + 'requirements' : tool_requirements, + 'errors' : state_errors, + 'state_inputs' : state_inputs + }) + + # check for errors + if 'error' in tool_message: + return tool_message + + # return enriched tool model + return tool_model + + def _compare_tool_version( self, trans, job ): + """ + Compares a tool version with the tool version from a job (from ToolRunner). + """ + tool_id = job.tool_id + tool_version = job.tool_version + message = '' + try: + select_field, tools, tool = self.app.toolbox.get_tool_components( tool_id, tool_version=tool_version, get_loaded_tools_by_lineage=False, set_selected=True ) + if tool is None: + trans.response.status = 500 + return { 'error': 'This dataset was created by an obsolete tool (%s). Can\'t re-run.' % tool_id } + if ( self.id != tool_id and self.old_id != tool_id ) or self.version != tool_version: + if self.id == tool_id: + if tool_version == None: + # for some reason jobs don't always keep track of the tool version. + message = '' + else: + message = 'This job was initially run with tool version "%s", which is currently not available. ' % tool_version + if len( tools ) > 1: + message += 'You can re-run the job with the selected tool or choose another derivation of the tool.' + else: + message += 'You can re-run the job with this tool version, which is a derivation of the original tool.' + else: + if len( tools ) > 1: + message = 'This job was initially run with tool version "%s", which is currently not available. ' % tool_version + message += 'You can re-run the job with the selected tool or choose another derivation of the tool.' + else: + message = 'This job was initially run with tool id "%s", version "%s", which is ' % ( tool_id, tool_version ) + message += 'currently not available. You can re-run the job with this tool, which is a derivation of the original tool.' + except Exception, error: + trans.response.status = 500 + return { 'error': str (error) } + + # can't rerun upload, external data sources, et cetera. workflow compatible will proxy this for now + #if not self.is_workflow_compatible: + # trans.response.status = 500 + # return { 'error': 'The \'%s\' tool does currently not support re-running.' % self.name } + return message + def get_default_history_by_trans( self, trans, create=False ): return trans.get_history( create=create ) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 07ad81d0580..2de5e84932c 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -11,7 +11,7 @@ from galaxy.web.base.controller import UsesHistoryMixin from galaxy.visualization.genomes import GenomeRegion from galaxy.util.json import dumps from galaxy.visualization.data_providers.genome import * -from galaxy.tools.parameters import params_to_incoming, check_param +from galaxy.tools.parameters import check_param from galaxy.tools.parameters import visit_input_values from galaxy.tools.parameters.meta import expand_meta_parameters from galaxy.managers.collections_util import dictify_dataset_collection_instance @@ -85,7 +85,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMix if not tool: trans.response.status = 500 return { 'error': 'Could not find tool with id \'%s\'' % tool_id } - return self._build_dict(trans, tool, kwd) + return tool.to_json(trans, **kwd) @_future_expose_api @web.require_admin @@ -491,308 +491,3 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin, UsesHistoryMix dataset_dict[ 'id' ] = trans.security.encode_id( dataset_dict[ 'id' ] ) dataset_dict[ 'track_config' ] = self.get_new_track_config( trans, output_dataset ) return dataset_dict - - def _build_dict(self, trans, tool, kwd={}): - """ - Recursively creates a tool dictionary containing repeats, dynamic options and updated states. - """ - job_id = kwd.get('job_id', None) - dataset_id = kwd.get('dataset_id', None) - - # load job details if provided - job = None - if job_id: - try: - job_id = trans.security.decode_id( job_id ) - job = trans.sa_session.query( trans.app.model.Job ).get( job_id ) - except Exception, exception: - trans.response.status = 500 - return { 'error': 'Failed to retrieve job.' } - elif dataset_id: - try: - dataset_id = trans.security.decode_id( dataset_id ) - data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( dataset_id ) - if not ( trans.user_is_admin() or trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), data.dataset ) ): - trans.response.status = 500 - return { 'error': 'User has no access to dataset.' } - job = data.creating_job - if not job: - trans.response.status = 500 - return { 'error': 'Creating job not found.' } - except Exception, exception: - trans.response.status = 500 - return { 'error': 'Failed to get job information.' } - - # load job parameters into incoming - tool_message = '' - if job: - try: - job_params = job.get_param_values( trans.app, ignore_errors = True ) - job_messages = tool.check_and_update_param_values( job_params, trans, update_values=False ) - tool_message = self._compare_tool_version(trans, tool, job) - params_to_incoming( kwd, tool.inputs, job_params, trans.app ) - except Exception, exception: - trans.response.status = 500 - return { 'error': str( exception ) } - - # create parameter object - params = galaxy.util.Params( kwd, sanitize = False ) - - # convert value to jsonifiable value - def convert(v): - # check if value is numeric - isnumber = False - try: - float(v) - isnumber = True - except Exception: - pass - - # fix hda parsing - if isinstance(v, HistoryDatasetAssociation): - return { - 'id' : trans.security.encode_id(v.id), - 'src' : 'hda' - } - elif isinstance(v, bool): - if v is True: - return 'true' - else: - return 'false' - elif isinstance(v, basestring) or isnumber: - return v - else: - return None - - # ensures that input dictionary is jsonifiable - def sanitize(dict): - # get current value - value = dict['value'] if 'value' in dict else None - - # identify lists - if dict['type'] in ['data']: - if isinstance(value, list): - value = [ convert(v) for v in value ] - else: - value = [ convert(value) ] - value = { - 'batch' : dict['multiple'], - 'values' : value - } - elif isinstance(value, list): - value = [ convert(v) for v in value ] - else: - value = convert(value) - - # update and return - dict['value'] = value - return dict - - # initialize state using default parameters - def initialize_state(trans, inputs, state, context=None): - context = ExpressionContext(state, context) - for input in inputs.itervalues(): - state[input.name] = input.get_initial_value(trans, context) - - # check the current state of a value and update it if necessary - def check_state(trans, input, default_value, context): - value = default_value - error = 'State validation failed.' - try: - # resolves the inconsistent definition of boolean parameters (see base.py) without modifying shared code - if input.type == 'boolean' and isinstance(default_value, basestring): - value, error = [util.string_as_bool(default_value), None] - else: - value, error = check_param(trans, input, default_value, context) - except Exception: - log.error('Checking parameter %s failed.', input.name) - pass - return [value, error] - - # populates state with incoming url parameters - def populate_state(trans, inputs, state, errors, incoming, prefix="", context=None ): - context = ExpressionContext(state, context) - for input in inputs.itervalues(): - key = prefix + input.name - if input.type == 'repeat': - group_state = state[input.name] - rep_index = 0 - del group_state[:] - while True: - rep_name = "%s_%d" % (key, rep_index) - if not any([incoming_key.startswith(rep_name) for incoming_key in incoming.keys()]): - break - if rep_index < input.max: - new_state = {} - new_state['__index__'] = rep_index - initialize_state(trans, input.inputs, new_state, context) - group_state.append(new_state) - populate_state(trans, input.inputs, new_state, errors, incoming, prefix=rep_name + "|", context=context) - rep_index += 1 - elif input.type == 'conditional': - group_state = state[input.name] - group_prefix = "%s|" % ( key ) - test_param_key = group_prefix + input.test_param.name - default_value = incoming.get(test_param_key, group_state.get(input.test_param.name, None)) - value, error = check_state(trans, input.test_param, default_value, context) - if error: - errors[test_param_key] = error - else: - current_case = input.get_current_case(value, trans) - group_state = state[input.name] = {} - initialize_state(trans, input.cases[current_case].inputs, group_state, context) - populate_state( trans, input.cases[current_case].inputs, group_state, errors, incoming, prefix=group_prefix, context=context) - group_state['__current_case__'] = current_case - group_state[input.test_param.name] = value - else: - default_value = incoming.get(key, state.get(input.name, None)) - value, error = check_state(trans, input, default_value, context) - if error: - errors[key] = error - state[input.name] = value - - # builds tool model including all attributes - def iterate(group_inputs, inputs, tool_state, other_values=None): - other_values = ExpressionContext( tool_state, other_values ) - for input_index, input in enumerate( inputs.itervalues() ): - # create model dictionary - group_inputs[input_index] = input.to_dict(trans) - if group_inputs[input_index] is None: - continue - - # identify stat for subsection/group - group_state = tool_state[input.name] - - # iterate and update values - if input.type == 'repeat': - group_cache = group_inputs[input_index]['cache'] = {} - for i in range( len( group_state ) ): - group_cache[i] = {} - iterate( group_cache[i], input.inputs, group_state[i], other_values ) - elif input.type == 'conditional': - try: - test_param = group_inputs[input_index]['test_param'] - test_param['value'] = convert(group_state[test_param['name']]) - except Exception: - pass - i = group_state['__current_case__'] - iterate(group_inputs[input_index]['cases'][i]['inputs'], input.cases[i].inputs, group_state, other_values) - else: - # create input dictionary, try to pass other_values if to_dict function supports it e.g. dynamic options - try: - group_inputs[input_index] = input.to_dict(trans, other_values=other_values) - except Exception: - pass - - # update input value from tool state - try: - group_inputs[input_index]['value'] = tool_state[group_inputs[input_index]['name']] - except Exception: - pass - - # sanitize if value exists - if group_inputs[input_index]['value']: - group_inputs[input_index] = sanitize(group_inputs[input_index]) - - # do param translation here, used by datasource tools - if tool.input_translator: - tool.input_translator.translate( params ) - - # create tool state - state_inputs = {} - state_errors = {} - initialize_state(trans, tool.inputs, state_inputs) - populate_state(trans, tool.inputs, state_inputs, state_errors, params.__dict__) - - # create basic tool model - tool_model = tool.to_dict(trans) - tool_model['inputs'] = {} - - # build tool model - iterate(tool_model['inputs'], tool.inputs, state_inputs, '') - - # load tool help - tool_help = '' - if tool.help: - tool_help = tool.help - tool_help = tool_help.render( static_path=web.url_for( '/static' ), host_url=web.url_for('/', qualified=True) ) - if type( tool_help ) is not unicode: - tool_help = unicode( tool_help, 'utf-8') - - # check if citations exist - tool_citations = False - if tool.citations: - tool_citations = True - - # get tool versions - tool_versions = [] - tools = self.app.toolbox.get_loaded_tools_by_lineage(tool.id) - for t in tools: - tool_versions.append(t.version) - - ## add information with underlying requirements and their versions - tool_requirements = [] - if tool.requirements: - for requirement in tool.requirements: - tool_requirements.append({ - 'name' : requirement.name, - 'version' : requirement.version - }) - - # add additional properties - tool_model.update({ - 'help' : tool_help, - 'citations' : tool_citations, - 'biostar_url' : trans.app.config.biostar_url, - 'message' : tool_message, - 'versions' : tool_versions, - 'requirements' : tool_requirements, - 'errors' : state_errors - }) - - # check for errors - if 'error' in tool_message: - return tool_message - - # return enriched tool model - return tool_model - - def _compare_tool_version( self, trans, tool, job ): - """ - Compares a tool version with the tool version from a job (from ToolRunner). - """ - tool_id = job.tool_id - tool_version = job.tool_version - message = '' - try: - select_field, tools, tool = self.app.toolbox.get_tool_components( tool_id, tool_version=tool_version, get_loaded_tools_by_lineage=False, set_selected=True ) - if tool is None: - trans.response.status = 500 - return { 'error': 'This dataset was created by an obsolete tool (%s). Can\'t re-run.' % tool_id } - if ( tool.id != tool_id and tool.old_id != tool_id ) or tool.version != tool_version: - if tool.id == tool_id: - if tool_version == None: - # for some reason jobs don't always keep track of the tool version. - message = '' - else: - message = 'This job was initially run with tool version "%s", which is currently not available. ' % tool_version - if len( tools ) > 1: - message += 'You can re-run the job with the selected tool or choose another derivation of the tool.' - else: - message += 'You can re-run the job with this tool version, which is a derivation of the original tool.' - else: - if len( tools ) > 1: - message = 'This job was initially run with tool version "%s", which is currently not available. ' % tool_version - message += 'You can re-run the job with the selected tool or choose another derivation of the tool.' - else: - message = 'This job was initially run with tool id "%s", version "%s", which is ' % ( tool_id, tool_version ) - message += 'currently not available. You can re-run the job with this tool, which is a derivation of the original tool.' - except Exception, error: - trans.response.status = 500 - return { 'error': str (error) } - - # can't rerun upload, external data sources, et cetera. workflow compatible will proxy this for now - if not tool.is_workflow_compatible: - trans.response.status = 500 - return { 'error': 'The \'%s\' tool does currently not support re-running.' % tool.name } - return message diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 11baa0ef945..5bbda0cff46 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -4,6 +4,7 @@ import json import os import sgmllib import urllib2 +import copy from sqlalchemy import and_ from tool_shed.util import common_util @@ -609,16 +610,26 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix is selected. """ + tool_state = incoming.pop('tool_state', None) trans.workflow_building_mode = True module = module_factory.from_dict( trans, { 'type': type, 'tool_id': tool_id, - 'tool_state': incoming.pop("tool_state") + 'tool_state': tool_state } ) + + # create tool model and default tool state (if missing) + tool_model = None + if not tool_state: + tool_model = module.tool.to_json(trans, **incoming) + module.state.inputs = copy.deepcopy(tool_model['state_inputs']) + + # update module state module.update_state( incoming ) - + if type == 'tool': return { + 'tool_model': tool_model, 'tool_state': module.get_state(), 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), @@ -653,6 +664,7 @@ class WorkflowController( BaseUIController, SharableMixin, UsesStoredWorkflowMix 'name': module.get_name(), 'tool_id': module.get_tool_id(), 'tool_state': module.get_state(), + 'tool_model': module.tool.to_json(trans), 'tooltip': module.get_tooltip( static_path=url_for( '/static' ) ), 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index adf8db4f6ac..13644176ebd 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -18,7 +18,7 @@ from galaxy.dataset_collections import matching from galaxy.web.framework import formbuilder from galaxy.jobs.actions.post import ActionBox from galaxy.model import PostJobAction -from galaxy.tools.parameters import check_param, DataToolParameter, DummyDataset, RuntimeValue, visit_input_values +from galaxy.tools.parameters import params_to_incoming, check_param, DataToolParameter, DummyDataset, RuntimeValue, visit_input_values from galaxy.tools.parameters import DataCollectionToolParameter from galaxy.tools.parameters.wrapped import make_dict_copy from galaxy.tools.execute import execute @@ -509,7 +509,8 @@ class ToolModule( WorkflowModule ): message = "%s: using version '%s' instead of version '%s' indicated in this workflow." % ( tool_id, d.get( 'tool_version', 'Unspecified' ), module.get_tool_version() ) log.debug(message) module.version_changes.append(message) - module.state.decode( d[ "tool_state" ], module.tool, module.trans.app, secure=secure ) + if d[ "tool_state" ]: + module.state.decode( d[ "tool_state" ], module.tool, module.trans.app, secure=secure ) module.errors = d.get( "tool_errors", None ) module.post_job_actions = d.get( "post_job_actions", {} ) module.workflow_outputs = d.get( "workflow_outputs", [] ) @@ -687,7 +688,7 @@ class ToolModule( WorkflowModule ): def get_config_form( self ): self.add_dummy_datasets() - return self.trans.fill_template( "workflow/editor_tool_form.mako", + return self.trans.fill_template( "workflow/editor_tool_form.mako", module=self, tool=self.tool, values=self.state.inputs, errors=( self.errors or {} ) ) def encode_runtime_state( self, trans, state ): diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index b22dc903c69..918ecf0d124 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -805,7 +805,7 @@ var Node = Backbone.Model.extend({ nodeView.addDataOutput( output ); } ); nodeView.render(); - workflow.node_changed( this ); + workflow.node_changed( this, true); }, update_field_data : function( data ) { var node = this; @@ -1094,9 +1094,9 @@ $.extend( Workflow.prototype, { this.active_node = node; } }, - node_changed : function ( node ) { + node_changed : function ( node, force ) { this.has_changes = true; - if ( this.active_node == node ) { + if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) { // Reactive with new form_html this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped) parent.show_form_for_tool( node.form_html + node.tooltip, node ); @@ -1305,6 +1305,12 @@ function add_node( type, title_text, tool_id ) { return node; } +function update_node( data ) { + var node = workflow.active_node; + if (node) { + node.update_field_data(data, true); + } +} var ext_to_type = null; var type_to_type = null; diff --git a/static/scripts/mvc/tools/tools-form.js b/static/scripts/mvc/tools/tools-form.js index a92abe0ad34..94a0af0a7f4 100644 --- a/static/scripts/mvc/tools/tools-form.js +++ b/static/scripts/mvc/tools/tools-form.js @@ -1,79 +1,28 @@ /** This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. */ -define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', - 'mvc/citation/citation-model', 'mvc/citation/citation-view', - 'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree', 'mvc/tools/tools-jobs'], - function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView, - Tools, ToolTemplate, ToolContent, ToolSection, ToolTree, ToolJobs) { +define(['utils/utils', 'mvc/ui/ui-misc', 'mvc/tools/tools-form-base', 'mvc/tools/tools-jobs'], + function(Utils, Ui, ToolFormBase, ToolJobs) { // create form view - var View = Backbone.View.extend({ - // base element - container: 'body', - + var View = ToolFormBase.extend({ // initialize initialize: function(options) { - // log options - console.debug(options); - - // link galaxy modal or create one - var galaxy = parent.Galaxy; - if (galaxy && galaxy.modal) { - this.modal = galaxy.modal; - } else { - this.modal = new Ui.Modal.View(); - } - - // check if the user is an admin - if (galaxy && galaxy.currUser) { - this.is_admin = galaxy.currUser.get('is_admin') - } else { - this.is_admin = false; - } - - // link options - this.options = options; - - // create deferred processing queue handler - // this handler reduces the number of requests to the api by filtering redundant requests - this.deferred = new Deferred(); - - // set element - this.setElement('
'); - - // add to main element - $(this.container).append(this.$el); - - // build this form - this._buildForm(); - }, - - /** Shows the final message (usually upon successful job submission) - */ - reciept: function($el) { - $(this.container).empty(); - $(this.container).append($el); - }, - - /** Highlight and scroll to input element (currently only used for error notifications) - */ - highlight: function (input_id, message, silent) { - // get input field - var input_element = this.element_list[input_id]; - - // check input element - if (input_element) { - // mark error - input_element.error(message || 'Please verify this parameter.'); - - // scroll to first input element - if (!silent) { - $(this.container).animate({ - scrollTop: input_element.$el.offset().top - 20 - }, 500); - } + var self = this; + this.job_handler = new ToolJobs(this); + this.buttons = { + execute : new Ui.Button({ + icon : 'fa-check', + tooltip : 'Execute: ' + options.name, + title : 'Execute', + cls : 'btn btn-primary', + floating : 'clear', + onclick : function() { + self.job_handler.submit(); + } + }) } + ToolFormBase.prototype.initialize.call(this, options); }, /** Builds a new model through api call and recreates the entire form @@ -250,256 +199,6 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', console.debug(response); } }); - }, - - /** Main tool form build function. This function is called once a new model is available. - */ - _buildForm: function() { - // link this - var self = this; - - // reset events - this.off('refresh'); - this.off('reset'); - - // reset field list, which contains the input field elements - this.field_list = {}; - - // reset sequential input definition list, which contains the input definitions as provided from the api - this.input_list = {}; - - // reset input element list, which contains the dom elements of each input element (includes also the input field) - this.element_list = {}; - - // creates a tree/json data structure from the input form - this.tree = new ToolTree(this); - - // creates the job handler - this.job_handler = new ToolJobs(this); - - // request history content and build form - this.content = new ToolContent(this); - - // link model options - var options = this.options; - - // create ui elements - this._renderForm(options); - - // rebuild the underlying data structure - this.tree.finalize(); - - // show errors - if (options.errors) { - var error_messages = this.tree.matchResponse(options.errors); - for (var input_id in error_messages) { - this.highlight(input_id, error_messages[input_id], true); - } - } - - // add refresh listener - this.on('refresh', function() { - // by using/reseting the deferred ajax queue the number of redundant calls is reduced - self.deferred.reset(); - self.deferred.execute(function(){self._updateModel()}); - }); - - // add reset listener - this.on('reset', function() { - for (var i in this.element_list) { - this.element_list[i].reset(); - } - }); - }, - - /** Renders the UI elements required for the form - */ - _renderForm: function(options) { - // link this - var self = this; - - // create message view - this.message = new Ui.Message(); - - // button for version selection - var requirements_button = new Ui.ButtonIcon({ - icon : 'fa-info-circle', - title : 'Requirements', - tooltip : 'Display tool requirements', - onclick : function() { - if (!this.visible) { - this.visible = true; - self.message.update({ - persistent : true, - message : ToolTemplate.requirements(options), - status : 'info' - }); - } else { - this.visible = false; - self.message.update({ - message : '' - }); - } - } - }); - if (!options.requirements || options.requirements.length == 0) { - requirements_button.$el.hide(); - } - - // button for version selection - var versions_button = new Ui.ButtonMenu({ - icon : 'fa-cubes', - title : 'Versions', - tooltip : 'Select another tool version' - }); - if (options.versions && options.versions.length > 1) { - for (var i in options.versions) { - var version = options.versions[i]; - if (version != options.version) { - versions_button.addMenu({ - title : 'Switch to ' + version, - version : version, - icon : 'fa-cube', - onclick : function() { - // here we update the tool version (some tools encode the version also in the id) - options.id = options.id.replace(options.version, this.version); - options.version = this.version; - - // rebuild the model and form - self.deferred.reset(); - self.deferred.execute(function(){self._buildModel()}); - } - }); - } - } - } else { - versions_button.$el.hide(); - } - - // button menu - var menu_button = new Ui.ButtonMenu({ - icon : 'fa-caret-down', - title : 'Options', - tooltip : 'View available options' - }); - - // configure button selection - if(options.biostar_url) { - // add question option - menu_button.addMenu({ - icon : 'fa-question-circle', - title : 'Question?', - tooltip : 'Ask a question about this tool (Biostar)', - onclick : function() { - window.open(options.biostar_url + '/p/new/post/'); - } - }); - - // create search button - menu_button.addMenu({ - icon : 'fa-search', - title : 'Search', - tooltip : 'Search help for this tool (Biostar)', - onclick : function() { - window.open(options.biostar_url + '/t/' + options.id + '/'); - } - }); - }; - - // create share button - menu_button.addMenu({ - icon : 'fa-share', - title : 'Share', - tooltip : 'Share this tool', - onclick : function() { - prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id); - } - }); - - // add admin operations - if (this.is_admin) { - // create download button - menu_button.addMenu({ - icon : 'fa-download', - title : 'Download', - tooltip : 'Download this tool', - onclick : function() { - window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download'; - } - }); - } - - // create tool form section - this.section = new ToolSection.View(self, { - inputs : options.inputs, - cls : 'ui-table-plain' - }); - - // switch to classic tool form mako if the form definition is incompatible - if (this.incompatible) { - this.$el.hide(); - $('#tool-form-classic').show(); - return; - } - - // create portlet - this.portlet = new Portlet.View({ - icon : 'fa-wrench', - title : '' + options.name + ' ' + options.description + ' (Galaxy Tool Version ' + options.version + ')', - cls : 'ui-portlet-slim', - operations: { - requirements : requirements_button, - menu : menu_button, - versions : versions_button - }, - buttons: { - execute : new Ui.Button({ - icon : 'fa-check', - tooltip : 'Execute: ' + options.name, - title : 'Execute', - cls : 'btn btn-primary', - floating : 'clear', - onclick : function() { - self.job_handler.submit(); - } - }) - } - }); - - // append message - this.portlet.append(this.message.$el, true); - - // append tool section - this.portlet.append(this.section.$el); - - // start form - this.$el.empty(); - this.$el.append(this.portlet.$el); - - // append help - if (options.help != '') { - this.$el.append(ToolTemplate.help(options.help)); - } - - // append citations - if (options.citations) { - var $citations = $('
'); - var citations = new CitationModel.ToolCitationCollection(); - citations.tool_id = options.id; - var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } ); - citation_list_view.render(); - citations.fetch(); - this.$el.append($citations); - } - - // show message if available in model - if (options.message) { - this.message.update({ - persistent : true, - status : 'warning', - message : options.message - }); - } } }); diff --git a/static/scripts/mvc/tools/tools-jobs.js b/static/scripts/mvc/tools/tools-jobs.js index 469d9b67daf..790b44dfa0c 100644 --- a/static/scripts/mvc/tools/tools-jobs.js +++ b/static/scripts/mvc/tools/tools-jobs.js @@ -98,7 +98,7 @@ return Backbone.Model.extend({ } // validate non-optional fields - if (!input_def.optional && input_field.validate && !input_field.validate()) { + if (!input_def.optional && input_value == null) { this.app.highlight(input_id); return false; } diff --git a/static/scripts/mvc/tools/tools-section.js b/static/scripts/mvc/tools/tools-section.js index fab21d2a8f7..8038a403079 100644 --- a/static/scripts/mvc/tools/tools-section.js +++ b/static/scripts/mvc/tools/tools-section.js @@ -382,6 +382,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Data input field */ _fieldData : function(input_def) { + if (this.app.workflow) { + var extensions = Utils.textify(input_def.extensions.toString()); + input_def.info = 'Data input \'' + input_def.name + '\' (' + extensions + ')'; + return this._fieldHidden(input_def); + } var self = this; return new SelectContent.View(this.app, { id : 'field-' + input_def.id, @@ -398,6 +403,14 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Select/Checkbox/Radio options field */ _fieldSelect : function (input_def) { + // show text field in workflow + if (this.app.workflow && input_def.is_dynamic) { + if (!Utils.validate(input_def.value)) { + input_def.value = ''; + } + return this._fieldText(input_def); + } + // configure options fields var options = []; for (var i in input_def.options) { @@ -475,7 +488,8 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep */ _fieldHidden : function(input_def) { return new Ui.Hidden({ - id : 'field-' + input_def.id + id : 'field-' + input_def.id, + info : input_def.info }); }, diff --git a/static/scripts/mvc/tools/tools-select-content.js b/static/scripts/mvc/tools/tools-select-content.js index 7453963d69d..fa7e64d922c 100644 --- a/static/scripts/mvc/tools/tools-select-content.js +++ b/static/scripts/mvc/tools/tools-select-content.js @@ -36,14 +36,7 @@ var View = Backbone.View.extend({ this.list = {}; // error messages - var extensions = options.extensions.toString(); - if (extensions) { - extensions = extensions.replace(/,/g, ', '); - var pos = extensions.lastIndexOf(', '); - if (pos != -1) { - extensions = extensions.substr(0, pos) + ' or ' + extensions.substr(pos+1); - } - } + var extensions = Utils.textify(options.extensions); var hda_error = 'No dataset available.'; if (extensions) { hda_error = 'No ' + extensions + ' dataset available.'; diff --git a/static/scripts/mvc/tools/tools-tree.js b/static/scripts/mvc/tools/tools-tree.js index af08497a719..3a8cde64e78 100644 --- a/static/scripts/mvc/tools/tools-tree.js +++ b/static/scripts/mvc/tools/tools-tree.js @@ -2,7 +2,7 @@ This class maps the tool form dom to an api compatible javascript dictionary. */ // dependencies -define([], function() { +define(['utils/utils'], function(Utils) { // tool form tree return Backbone.Model.extend({ @@ -120,7 +120,7 @@ return Backbone.Model.extend({ // handle default value if (!field.skip) { - if (input.optional && field.validate && !field.validate()) { + if (field.validate && !field.validate(value)) { value = null; } add (job_input_id, input.id, value); diff --git a/static/scripts/mvc/ui/ui-misc.js b/static/scripts/mvc/ui/ui-misc.js index 666fd78882c..efedef2c0c3 100644 --- a/static/scripts/mvc/ui/ui-misc.js +++ b/static/scripts/mvc/ui/ui-misc.js @@ -412,7 +412,7 @@ var Hidden = Backbone.View.extend({ initialize : function(options) { // configure options this.options = options; - + // create new element this.setElement(this._template(this.options)); @@ -425,14 +425,20 @@ var Hidden = Backbone.View.extend({ // value value : function (new_val) { if (new_val !== undefined) { - this.$el.val(new_val); + this.$('hidden').val(new_val); } - return this.$el.val(); + return this.$('hidden').val(); }, // element _template: function(options) { - return ''; + var tmpl = '
'; + if (options.info) { + tmpl += ''; + } + tmpl += '' + + '
'; + return tmpl; } }); diff --git a/static/scripts/mvc/ui/ui-options.js b/static/scripts/mvc/ui/ui-options.js index 8d47dfe0330..21e0911747f 100644 --- a/static/scripts/mvc/ui/ui-options.js +++ b/static/scripts/mvc/ui/ui-options.js @@ -137,16 +137,7 @@ var Base = Backbone.View.extend({ /** Validate the selected option/options */ validate: function() { - var current = this.value(); - if (!(current instanceof Array)) { - current = [current]; - } - for (var i in current) { - if ([null, 'null', undefined].indexOf(current[i]) > -1) { - return false; - } - } - return true; + return Utils.validate(this.value()); }, /** Wait message during request processing diff --git a/static/scripts/mvc/ui/ui-select-default.js b/static/scripts/mvc/ui/ui-select-default.js index 6171ca3043d..99b63065e92 100644 --- a/static/scripts/mvc/ui/ui-select-default.js +++ b/static/scripts/mvc/ui/ui-select-default.js @@ -96,16 +96,7 @@ var View = Backbone.View.extend({ /** Validate the current selection */ validate: function() { - var current = this.value(); - if (!(current instanceof Array)) { - current = [current]; - } - for (var i in current) { - if ([null, 'null', undefined].indexOf(current[i]) > -1) { - return false; - } - } - return true; + return Utils.validate(this.value()); }, /** Return the label/text of the current selection diff --git a/static/scripts/packed/galaxy.workflow_editor.canvas.js b/static/scripts/packed/galaxy.workflow_editor.canvas.js index 9f7fff67a09..f3de71974e1 100644 --- a/static/scripts/packed/galaxy.workflow_editor.canvas.js +++ b/static/scripts/packed/galaxy.workflow_editor.canvas.js @@ -1 +1 @@ -function CollectionTypeDescription(a){this.collectionType=a;this.isCollection=true;this.rank=a.split(":").length}$.extend(CollectionTypeDescription.prototype,{append:function(a){if(a===NULL_COLLECTION_TYPE_DESCRIPTION){return this}if(a===ANY_COLLECTION_TYPE_DESCRIPTION){return otherCollectionType}return new CollectionTypeDescription(this.collectionType+":"+a.collectionType)},canMatch:function(a){if(a===NULL_COLLECTION_TYPE_DESCRIPTION){return false}if(a===ANY_COLLECTION_TYPE_DESCRIPTION){return true}return a.collectionType==this.collectionType},canMapOver:function(b){if(b===NULL_COLLECTION_TYPE_DESCRIPTION){return false}if(b===ANY_COLLECTION_TYPE_DESCRIPTION){return false}if(this.rank<=b.rank){return false}var a=b.collectionType;return this._endsWith(this.collectionType,a)},effectiveMapOver:function(a){var c=a.collectionType;var b=this.collectionType.substring(0,this.collectionType.length-c.length-1);return new CollectionTypeDescription(b)},equal:function(a){return a.collectionType==this.collectionType},toString:function(){return"CollectionType["+this.collectionType+"]"},_endsWith:function(b,a){return b.indexOf(a,b.length-a.length)!==-1}});NULL_COLLECTION_TYPE_DESCRIPTION={isCollection:false,canMatch:function(a){return false},canMapOver:function(a){return false},toString:function(){return"NullCollectionType[]"},append:function(a){return a},equal:function(a){return a===this}};ANY_COLLECTION_TYPE_DESCRIPTION={isCollection:true,canMatch:function(a){return NULL_COLLECTION_TYPE_DESCRIPTION!==a},canMapOver:function(a){return false},toString:function(){return"AnyCollectionType[]"},append:function(a){throw"Cannot append to ANY_COLLECTION_TYPE_DESCRIPTION"},equal:function(a){return a===this}};var TerminalMapping=Backbone.Model.extend({initialize:function(a){this.mapOver=a.mapOver||NULL_COLLECTION_TYPE_DESCRIPTION;this.terminal=a.terminal;this.terminal.terminalMapping=this},disableMapOver:function(){this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION)},setMapOver:function(a){this.mapOver=a;this.trigger("change")}});var TerminalMappingView=Backbone.View.extend({tagName:"div",className:"fa-icon-button fa fa-folder-o",initialize:function(b){var a="Run tool in parallel over collection";this.$el.tooltip({delay:500,title:a});this.model.bind("change",_.bind(this.render,this))},render:function(){if(this.model.mapOver.isCollection){this.$el.show()}else{this.$el.hide()}},});var InputTerminalMappingView=TerminalMappingView.extend({events:{click:"onClick",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",},onMouseEnter:function(b){var a=this.model;if(!a.terminal.connected()&&a.mapOver.isCollection){this.$el.css("color","red")}},onMouseLeave:function(a){this.$el.css("color","black")},onClick:function(b){var a=this.model;if(!a.terminal.connected()&&a.mapOver.isCollection){a.terminal.resetMapping()}},});var InputTerminalMapping=TerminalMapping;var InputCollectionTerminalMapping=TerminalMapping;var OutputTerminalMapping=TerminalMapping;var OutputTerminalMappingView=TerminalMappingView;var InputCollectionTerminalMappingView=InputTerminalMappingView;var OutputCollectionTerminalMapping=TerminalMapping;var OutputCollectionTerminalMappingView=TerminalMappingView;var Terminal=Backbone.Model.extend({initialize:function(a){this.element=a.element;this.connectors=[]},connect:function(a){this.connectors.push(a);if(this.node){this.node.markChanged()}},disconnect:function(a){this.connectors.splice($.inArray(a,this.connectors),1);if(this.node){this.node.markChanged();this.resetMappingIfNeeded()}},redraw:function(){$.each(this.connectors,function(a,b){b.redraw()})},destroy:function(){$.each(this.connectors.slice(),function(a,b){b.destroy()})},destroyInvalidConnections:function(){_.each(this.connectors,function(a){a.destroyIfInvalid()})},setMapOver:function(a){if(this.multiple){return}if(!this.mapOver().equal(a)){this.terminalMapping.setMapOver(a);_.each(this.node.output_terminals,function(b){b.setMapOver(a)})}},mapOver:function(){if(!this.terminalMapping){return NULL_COLLECTION_TYPE_DESCRIPTION}else{return this.terminalMapping.mapOver}},isMappedOver:function(){return this.terminalMapping&&this.terminalMapping.mapOver.isCollection},resetMapping:function(){this.terminalMapping.disableMapOver()},resetMappingIfNeeded:function(){},});var OutputTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.datatypes=a.datatypes},resetMappingIfNeeded:function(){if(!this.node.hasConnectedOutputTerminals()&&!this.node.hasConnectedMappedInputTerminals()){_.each(this.node.mappedInputTerminals(),function(b){b.resetMappingIfNeeded()})}var a=!this.node.hasMappedOverInputTerminals();if(a){this.resetMapping()}},resetMapping:function(){this.terminalMapping.disableMapOver();_.each(this.connectors,function(a){var b=a.handle2;if(b){b.resetMappingIfNeeded();a.destroyIfInvalid()}})}});var BaseInputTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.update(a.input)},canAccept:function(a){if(this._inputFilled()){return false}else{return this.attachable(a)}},resetMappingIfNeeded:function(){var b=this.mapOver();if(!b.isCollection){return}var a=this.node.hasConnectedMappedInputTerminals()||(!this.node.hasConnectedOutputTerminals());if(a){this.resetMapping()}},resetMapping:function(){this.terminalMapping.disableMapOver();if(!this.node.hasMappedOverInputTerminals()){_.each(this.node.output_terminals,function(a){a.resetMapping()})}},connected:function(){return this.connectors.length!==0},_inputFilled:function(){var a;if(!this.connected()){a=false}else{if(this.multiple){if(this._collectionAttached()){inputsFilled=true}else{a=false}}else{a=true}}return a},_collectionAttached:function(){if(!this.connected()){return false}else{var a=this.connectors[0].handle1;if(!a){return false}else{if(a.isDataCollectionInput||a.isMappedOver()||a.datatypes.indexOf("input_collection")>0){return true}else{return false}}}},_mappingConstraints:function(){if(!this.node){return[]}var b=this.mapOver();if(b.isCollection){return[b]}var a=[];if(!this.node.hasConnectedOutputTerminals()){_.each(this.node.connectedMappedInputTerminals(),function(c){a.push(c.mapOver())})}else{a.push(_.first(_.values(this.node.output_terminals)).mapOver())}return a},_producesAcceptableDatatype:function(a){for(var c in this.datatypes){var f=new Array();f=f.concat(a.datatypes);if(a.node.post_job_actions){for(var d in a.node.post_job_actions){var g=a.node.post_job_actions[d];if(g.action_type=="ChangeDatatypeAction"&&(g.output_name==""||g.output_name==a.name)&&g.action_arguments){f.push(g.action_arguments.newtype)}}}for(var b in f){var h=f[b];if(h=="input"||h=="input_collection"||issubtype(f[b],this.datatypes[c])){return true}}}return false},_otherCollectionType:function(a){var c=NULL_COLLECTION_TYPE_DESCRIPTION;if(a.isDataCollectionInput){c=a.collectionType}else{var b=a.mapOver();if(b.isCollection){c=b}}return c},});var InputTerminal=BaseInputTerminal.extend({update:function(a){this.datatypes=a.extensions;this.multiple=a.multiple;this.collection=false},connect:function(a){BaseInputTerminal.prototype.connect.call(this,a);var b=a.handle1;if(!b){return}var c=this._otherCollectionType(b);if(c.isCollection){this.setMapOver(c)}},attachable:function(b){var d=this._otherCollectionType(b);var a=this.mapOver();if(d.isCollection){if(this.multiple){if(this.connected()&&!this._collectionAttached()){return false}if(d.rank==1){return this._producesAcceptableDatatype(b)}else{return false}}if(a.isCollection&&a.canMatch(d)){return this._producesAcceptableDatatype(b)}else{var c=this._mappingConstraints();if(c.every(_.bind(d.canMatch,d))){return this._producesAcceptableDatatype(b)}else{return false}}}else{if(a.isCollection){return false}}return this._producesAcceptableDatatype(b)}});var InputCollectionTerminal=BaseInputTerminal.extend({update:function(a){this.multiple=false;this.collection=true;this.datatypes=a.extensions;if(a.collection_type){this.collectionType=new CollectionTypeDescription(a.collection_type)}else{this.collectionType=ANY_COLLECTION_TYPE_DESCRIPTION}},connect:function(b){BaseInputTerminal.prototype.connect.call(this,b);var a=b.handle1;if(!a){return}var c=this._effectiveMapOver(a);this.setMapOver(c)},_effectiveMapOver:function(a){var b=this.collectionType;var c=this._otherCollectionType(a);if(!b.canMatch(c)){return c.effectiveMapOver(b)}else{return NULL_COLLECTION_TYPE_DESCRIPTION}},_effectiveCollectionType:function(){var b=this.collectionType;var a=this.mapOver();return a.append(b)},attachable:function(b){var g=this._otherCollectionType(b);if(g.isCollection){var f=this._effectiveCollectionType();var a=this.mapOver();if(f.canMatch(g)){return this._producesAcceptableDatatype(b)}else{if(a.isCollection){return false}else{if(g.canMapOver(this.collectionType)){var d=this._effectiveMapOver(b);if(!d.isCollection){return false}var c=this._mappingConstraints();if(c.every(d.canMatch)){return this._producesAcceptableDatatype(b)}}}}}return false}});var OutputCollectionTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.datatypes=a.datatypes;this.collectionType=new CollectionTypeDescription(a.collection_type);this.isDataCollectionInput=true},update:function(a){var b=new CollectionTypeDescription(a.collection_type);if(b.collectionType!=this.collectionType.collectionType){_.each(this.connectors,function(c){c.destroy()})}this.collectionType=b}});function Connector(b,a){this.canvas=null;this.dragging=false;this.inner_color="#FFFFFF";this.outer_color="#D8B365";if(b&&a){this.connect(b,a)}}$.extend(Connector.prototype,{connect:function(b,a){this.handle1=b;if(this.handle1){this.handle1.connect(this)}this.handle2=a;if(this.handle2){this.handle2.connect(this)}},destroy:function(){if(this.handle1){this.handle1.disconnect(this)}if(this.handle2){this.handle2.disconnect(this)}$(this.canvas).remove()},destroyIfInvalid:function(){if(this.handle1&&this.handle2&&!this.handle2.attachable(this.handle1)){this.destroy()}},redraw:function(){var f=$("#canvas-container");if(!this.canvas){this.canvas=document.createElement("canvas");if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(this.canvas)}f.append($(this.canvas));if(this.dragging){this.canvas.style.zIndex="300"}}var v=function(c){return $(c).offset().left-f.offset().left};var p=function(c){return $(c).offset().top-f.offset().top};if(!this.handle1||!this.handle2){return}var o=v(this.handle1.element)+5;var n=p(this.handle1.element)+5;var x=v(this.handle2.element)+5;var u=p(this.handle2.element)+5;var k=100;var r=Math.min(o,x);var a=Math.max(o,x);var q=Math.min(n,u);var B=Math.max(n,u);var d=Math.min(Math.max(Math.abs(B-q)/2,100),300);var w=r-k;var A=q-k;var y=a-r+2*k;var s=B-q+2*k;this.canvas.style.left=w+"px";this.canvas.style.top=A+"px";this.canvas.setAttribute("width",y);this.canvas.setAttribute("height",s);o-=w;n-=A;x-=w;u-=A;var z=this.canvas.getContext("2d"),h=null,l=null;var g=1;if(this.handle1&&this.handle1.isMappedOver()){var h=[-6,-3,0,3,6];g=5}else{var h=[0]}if(this.handle2&&this.handle2.isMappedOver()){var l=[-6,-3,0,3,6];g=5}else{var l=[0]}var b=this;for(var t=0;t1||l.length>1){m=1;j=3}b.draw_outlined_curve(o,n,x,u,d,m,j,h[t%h.length],l[t%l.length])}},draw_outlined_curve:function(j,i,l,k,a,b,f,g,d){var g=g||0;var d=d||0;var h=this.canvas.getContext("2d");h.lineCap="round";h.strokeStyle=this.outer_color;h.lineWidth=f;h.beginPath();h.moveTo(j,i+g);h.bezierCurveTo(j+a,i+g,l-a,k+d,l,k+d);h.stroke();h.strokeStyle=this.inner_color;h.lineWidth=b;h.beginPath();h.moveTo(j,i+g);h.bezierCurveTo(j+a,i+g,l-a,k+d,l,k+d);h.stroke()}});var Node=Backbone.Model.extend({initialize:function(a){this.element=a.element;this.input_terminals={};this.output_terminals={};this.tool_errors={}},connectedOutputTerminals:function(){return this._connectedTerminals(this.output_terminals)},_connectedTerminals:function(b){var a=[];$.each(b,function(c,d){if(d.connectors.length>0){a.push(d)}});return a},hasConnectedOutputTerminals:function(){var a=this.output_terminals;for(var b in a){if(a[b].connectors.length>0){return true}}return false},connectedMappedInputTerminals:function(){return this._connectedMappedTerminals(this.input_terminals)},hasConnectedMappedInputTerminals:function(){var c=this.input_terminals;for(var b in c){var a=c[b];if(a.connectors.length>0&&a.isMappedOver()){return true}}return false},_connectedMappedTerminals:function(b){var a=[];$.each(b,function(c,d){var f=d.mapOver();if(f.isCollection){if(d.connectors.length>0){a.push(d)}}});return a},mappedInputTerminals:function(){return this._mappedTerminals(this.input_terminals)},_mappedTerminals:function(b){var a=[];$.each(b,function(c,d){var f=d.mapOver();if(f.isCollection){a.push(d)}});return a},hasMappedOverInputTerminals:function(){var a=false;_.each(this.input_terminals,function(b){var c=b.mapOver();if(c.isCollection){a=true}});return a},redraw:function(){$.each(this.input_terminals,function(a,b){b.redraw()});$.each(this.output_terminals,function(a,b){b.redraw()})},destroy:function(){$.each(this.input_terminals,function(a,b){b.destroy()});$.each(this.output_terminals,function(a,b){b.destroy()});workflow.remove_node(this);$(this.element).remove()},make_active:function(){$(this.element).addClass("toolForm-active")},make_inactive:function(){var a=this.element.get(0);(function(b){b.removeChild(a);b.appendChild(a)})(a.parentNode);$(a).removeClass("toolForm-active")},init_field_data:function(b){if(b.type){this.type=b.type}this.name=b.name;this.form_html=b.form_html;this.tool_state=b.tool_state;this.tool_errors=b.tool_errors;this.tooltip=b.tooltip?b.tooltip:"";this.annotation=b.annotation;this.post_job_actions=b.post_job_actions?b.post_job_actions:{};this.label=b.label;this.uuid=b.uuid;this.workflow_outputs=b.workflow_outputs?b.workflow_outputs:[];var a=this;var c=new NodeView({el:this.element[0],node:a,});a.nodeView=c;$.each(b.data_inputs,function(f,d){c.addDataInput(d)});if((b.data_inputs.length>0)&&(b.data_outputs.length>0)){c.addRule()}$.each(b.data_outputs,function(f,d){c.addDataOutput(d)});c.render();workflow.node_changed(this)},update_field_data:function(d){var c=this;nodeView=c.nodeView;this.tool_state=d.tool_state;this.form_html=d.form_html;this.tool_errors=d.tool_errors;this.annotation=d.annotation;if("post_job_actions" in d){var f=$.parseJSON(d.post_job_actions);this.post_job_actions=f?f:{}}c.nodeView.renderToolErrors();var g=nodeView.$("div.inputs");var a=nodeView.newInputsDiv();var b={};_.each(d.data_inputs,function(h){var i=c.nodeView.addDataInput(h,a);b[h.name]=i});_.each(_.difference(_.values(nodeView.terminalViews),_.values(b)),function(h){h.el.terminal.destroy()});nodeView.terminalViews=b;if(d.data_outputs.length==1&&"collection_type" in d.data_outputs[0]){nodeView.updateDataOutput(d.data_outputs[0])}g.replaceWith(a);this.markChanged();this.redraw()},error:function(d){var a=$(this.element).find(".toolFormBody");a.find("div").remove();var c="
"+d+"
";this.form_html=c;a.html(c);workflow.node_changed(this)},markChanged:function(){workflow.node_changed(this)}});function Workflow(a){this.canvas_container=a;this.id_counter=0;this.nodes={};this.name=null;this.has_changes=false;this.active_form_has_changes=false}$.extend(Workflow.prototype,{add_node:function(a){a.id=this.id_counter;a.element.attr("id","wf-node-step-"+a.id);this.id_counter++;this.nodes[a.id]=a;this.has_changes=true;a.workflow=this},remove_node:function(a){if(this.active_node==a){this.clear_active_node()}delete this.nodes[a.id];this.has_changes=true},remove_all:function(){wf=this;$.each(this.nodes,function(b,a){a.destroy();wf.remove_node(a)})},rectify_workflow_outputs:function(){var b=false;var a=false;$.each(this.nodes,function(c,d){if(d.workflow_outputs&&d.workflow_outputs.length>0){b=true}$.each(d.post_job_actions,function(g,f){if(f.action_type==="HideDatasetAction"){a=true}})});if(b!==false||a!==false){$.each(this.nodes,function(c,g){if(g.type==="tool"){var f=false;if(g.post_job_actions==null){g.post_job_actions={};f=true}var d=[];$.each(g.post_job_actions,function(i,h){if(h.action_type=="HideDatasetAction"){d.push(i)}});if(d.length>0){$.each(d,function(h,j){f=true;delete g.post_job_actions[j]})}if(b){$.each(g.output_terminals,function(i,j){var h=true;$.each(g.workflow_outputs,function(l,m){if(j.name===m){h=false}});if(h===true){f=true;var k={action_type:"HideDatasetAction",output_name:j.name,action_arguments:{}};g.post_job_actions["HideDatasetAction"+j.name]=null;g.post_job_actions["HideDatasetAction"+j.name]=k}})}if(workflow.active_node==g&&f===true){workflow.reload_active_node()}}})}},to_simple:function(){var a={};$.each(this.nodes,function(c,f){var g={};$.each(f.input_terminals,function(i,j){g[j.name]=null;var h=[];$.each(j.connectors,function(k,l){h[k]={id:l.handle1.node.id,output_name:l.handle1.name};g[j.name]=h})});var b={};if(f.post_job_actions){$.each(f.post_job_actions,function(j,h){var k={action_type:h.action_type,output_name:h.output_name,action_arguments:h.action_arguments};b[h.action_type+h.output_name]=null;b[h.action_type+h.output_name]=k})}if(!f.workflow_outputs){f.workflow_outputs=[]}var d={id:f.id,type:f.type,tool_id:f.tool_id,tool_state:f.tool_state,tool_errors:f.tool_errors,input_connections:g,position:$(f.element).position(),annotation:f.annotation,post_job_actions:f.post_job_actions,uuid:f.uuid,label:f.label,workflow_outputs:f.workflow_outputs};a[f.id]=d});return{steps:a}},from_simple:function(b){wf=this;var c=0;wf.name=b.name;var a=false;$.each(b.steps,function(g,f){var d=prebuild_node(f.type,f.name,f.tool_id);d.init_field_data(f);if(f.position){d.element.css({top:f.position.top,left:f.position.left})}d.id=f.id;wf.nodes[d.id]=d;c=Math.max(c,parseInt(g));if(!a&&d.type==="tool"){if(d.workflow_outputs.length>0){a=true}else{$.each(d.post_job_actions,function(i,h){if(h.action_type==="HideDatasetAction"){a=true}})}}});wf.id_counter=c+1;$.each(b.steps,function(g,f){var d=wf.nodes[g];$.each(f.input_connections,function(i,h){if(h){if(!$.isArray(h)){h=[h]}$.each(h,function(k,j){var m=wf.nodes[j.id];var n=new Connector();n.connect(m.output_terminals[j.output_name],d.input_terminals[i]);n.redraw()})}});if(a&&d.type==="tool"){$.each(d.output_terminals,function(h,i){if(d.post_job_actions["HideDatasetAction"+i.name]===undefined){d.workflow_outputs.push(i.name);callout=$(d.element).find(".callout."+i.name);callout.find("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png");workflow.has_changes=true}})}})},check_changes_in_active_form:function(){if(this.active_form_has_changes){this.has_changes=true;$("#right-content").find("form").submit();this.active_form_has_changes=false}},reload_active_node:function(){if(this.active_node){var a=this.active_node;this.clear_active_node();this.activate_node(a)}},clear_active_node:function(){if(this.active_node){this.active_node.make_inactive();this.active_node=null}parent.show_form_for_tool("
No node selected
")},activate_node:function(a){if(this.active_node!=a){this.check_changes_in_active_form();this.clear_active_node();parent.show_form_for_tool(a.form_html+a.tooltip,a);a.make_active();this.active_node=a}},node_changed:function(a){this.has_changes=true;if(this.active_node==a){this.check_changes_in_active_form();parent.show_form_for_tool(a.form_html+a.tooltip,a)}},layout:function(){this.check_changes_in_active_form();this.has_changes=true;var i={};var b={};$.each(this.nodes,function(l,k){if(i[l]===undefined){i[l]=0}if(b[l]===undefined){b[l]=[]}});$.each(this.nodes,function(l,k){$.each(k.input_terminals,function(m,n){$.each(n.connectors,function(p,q){var o=q.handle1.node;i[k.id]+=1;b[o.id].push(k.id)})})});node_ids_by_level=[];while(true){level_parents=[];for(var a in i){if(i[a]==0){level_parents.push(a)}}if(level_parents.length==0){break}node_ids_by_level.push(level_parents);for(var f in level_parents){var j=level_parents[f];delete i[j];for(var g in b[j]){i[b[j][g]]-=1}}}if(i.length){return}var d=this.nodes;var h=80;v_pad=30;var c=h;$.each(node_ids_by_level,function(k,l){l.sort(function(p,o){return $(d[p].element).position().top-$(d[o].element).position().top});var m=0;var n=v_pad;$.each(l,function(o,r){var q=d[r];var p=$(q.element);$(p).css({top:n,left:c});m=Math.max(m,$(p).width());n+=$(p).height()+v_pad});c+=m+h});$.each(d,function(k,l){l.redraw()})},bounds_for_all_nodes:function(){var d=Infinity,b=-Infinity,c=Infinity,a=-Infinity,f;$.each(this.nodes,function(h,g){e=$(g.element);f=e.position();d=Math.min(d,f.left);b=Math.max(b,f.left+e.width());c=Math.min(c,f.top);a=Math.max(a,f.top+e.width())});return{xmin:d,xmax:b,ymin:c,ymax:a}},fit_canvas_to_nodes:function(){var a=this.bounds_for_all_nodes();var f=this.canvas_container.position();var i=this.canvas_container.parent();var d=fix_delta(a.xmin,100);var h=fix_delta(a.ymin,100);d=Math.max(d,f.left);h=Math.max(h,f.top);var c=f.left-d;var g=f.top-h;var b=round_up(a.xmax+100,100)+d;var j=round_up(a.ymax+100,100)+h;b=Math.max(b,-c+i.width());j=Math.max(j,-g+i.height());this.canvas_container.css({left:c,top:g,width:b,height:j});this.canvas_container.children().each(function(){var k=$(this).position();$(this).css("left",k.left+d);$(this).css("top",k.top+h)})}});function fix_delta(a,b){if(a3*b){new_pos=(Math.ceil(((a%b))/b)+1)*b;return(-(a-new_pos))}return 0}function round_up(a,b){return Math.ceil(a/b)*b}function prebuild_node(l,j,r){var i=$("
");var g=new Node({element:i});g.type=l;if(l=="tool"){g.tool_id=r}var n=$("
"+j+"
");i.append(n);i.css("left",$(window).scrollLeft()+20);i.css("top",$(window).scrollTop()+20);var m=$("
");var h="
loading tool info...
";m.append(h);g.form_html=h;i.append(m);var k=$("
");k.append($("
").addClass("fa-icon-button fa fa-times").click(function(b){g.destroy()}));i.appendTo("#canvas-container");var d=$("#canvas-container").position();var c=$("#canvas-container").parent();var a=i.width();var q=i.height();i.css({left:(-d.left)+(c.width()/2)-(a/2),top:(-d.top)+(c.height()/2)-(q/2)});k.prependTo(n);a+=(k.width()+10);i.css("width",a);$(i).bind("dragstart",function(){workflow.activate_node(g)}).bind("dragend",function(){workflow.node_changed(this);workflow.fit_canvas_to_nodes();canvas_manager.draw_overview()}).bind("dragclickonly",function(){workflow.activate_node(g)}).bind("drag",function(o,p){var f=$(this).offsetParent().offset(),b=p.offsetX-f.left,s=p.offsetY-f.top;$(this).css({left:b,top:s});$(this).find(".terminal").each(function(){this.terminal.redraw()})});return g}function add_node(b,d,a){var c=prebuild_node(b,d,a);workflow.add_node(c);workflow.fit_canvas_to_nodes();canvas_manager.draw_overview();workflow.activate_node(c);return c}var ext_to_type=null;var type_to_type=null;function issubtype(b,a){b=ext_to_type[b];a=ext_to_type[a];return(type_to_type[b])&&(a in type_to_type[b])}function populate_datatype_info(a){ext_to_type=a.ext_to_class_name;type_to_type=a.class_to_classes}var NodeView=Backbone.View.extend({initialize:function(a){this.node=a.node;this.output_width=Math.max(150,this.$el.width());this.tool_body=this.$el.find(".toolFormBody");this.tool_body.find("div").remove();this.newInputsDiv().appendTo(this.tool_body);this.terminalViews={};this.outputTerminlViews={}},render:function(){this.renderToolErrors();this.$el.css("width",Math.min(250,Math.max(this.$el.width(),this.output_width)))},renderToolErrors:function(){if(this.node.tool_errors){this.$el.addClass("tool-node-error")}else{this.$el.removeClass("tool-node-error")}},newInputsDiv:function(){return $("
")},updateMaxWidth:function(a){this.output_width=Math.max(this.output_width,a)},addRule:function(){this.tool_body.append($("
"))},addDataInput:function(i,d){var j=true;if(!d){d=this.$(".inputs");j=false}var f=this.terminalViews[i.name];var h=(i.input_type=="dataset_collection")?InputCollectionTerminalView:InputTerminalView;if(f&&!(f instanceof h)){f.el.terminal.destroy();f=null}if(!f){f=new h({node:this.node,input:i})}else{var g=f.el.terminal;g.update(i);g.destroyInvalidConnections()}this.terminalViews[i.name]=f;var c=f.el;var b=new DataInputView({terminalElement:c,input:i,nodeView:this,skipResize:j});var a=b.$el;d.append(a.prepend(f.terminalElements()));return f},addDataOutput:function(a){var d=(a.collection_type)?OutputCollectionTerminalView:OutputTerminalView;var c=new d({node:this.node,output:a});var b=new DataOutputView({output:a,terminalElement:c.el,nodeView:this,});this.tool_body.append(b.$el.append(c.terminalElements()))},updateDataOutput:function(b){var a=this.node.output_terminals[b.name];a.update(b)}});var DataInputView=Backbone.View.extend({className:"form-row dataRow input-data-row",initialize:function(a){this.input=a.input;this.nodeView=a.nodeView;this.terminalElement=a.terminalElement;this.$el.attr("name",this.input.name).html(this.input.label);if(!a.skipResize){this.$el.css({position:"absolute",left:-1000,top:-1000,display:"none"});$("body").append(this.el);this.nodeView.updateMaxWidth(this.$el.outerWidth());this.$el.css({position:"",left:"",top:"",display:""});this.$el.remove()}},});var OutputCalloutView=Backbone.View.extend({tagName:"div",initialize:function(b){this.label=b.label;this.node=b.node;this.output=b.output;var a=this;this.$el.attr("class","callout "+this.label).css({display:"none"}).append($("
").append($("").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png").click(function(){if($.inArray(a.output.name,a.node.workflow_outputs)!=-1){a.node.workflow_outputs.splice($.inArray(a.output.name,a.node.workflow_outputs),1);a.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png")}else{a.node.workflow_outputs.push(a.output.name);a.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png")}workflow.has_changes=true;canvas_manager.draw_overview()}))).tooltip({delay:500,title:"Mark dataset as a workflow output. All unmarked datasets will be hidden."});this.$el.css({top:"50%",margin:"-8px 0px 0px 0px",right:8});this.$el.show();this.resetImage()},resetImage:function(){if($.inArray(this.output.name,this.node.workflow_outputs)===-1){this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png")}else{this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png")}},hoverImage:function(){this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-yellow.png")}});var DataOutputView=Backbone.View.extend({className:"form-row dataRow",initialize:function(c){this.output=c.output;this.terminalElement=c.terminalElement;this.nodeView=c.nodeView;var a=this.output;var b=a.name;var f=this.nodeView.node;var d=a.extensions.indexOf("input")>=0||a.extensions.indexOf("input_collection")>=0;if(!d){b=b+" ("+a.extensions.join(", ")+")"}this.$el.html(b);if(f.type=="tool"){var g=new OutputCalloutView({label:b,output:a,node:f,});this.$el.append(g.el);this.$el.hover(function(){g.hoverImage()},function(){g.resetImage()})}this.$el.css({position:"absolute",left:-1000,top:-1000,display:"none"});$("body").append(this.el);this.nodeView.updateMaxWidth(this.$el.outerWidth()+17);this.$el.css({position:"",left:"",top:"",display:""}).detach()}});var TerminalView=Backbone.View.extend({setupMappingView:function(b){var c=new this.terminalMappingClass({terminal:b});var a=new this.terminalMappingViewClass({model:c});a.render();b.terminalMappingView=a;this.terminalMappingView=a},terminalElements:function(){if(this.terminalMappingView){return[this.terminalMappingView.el,this.el]}else{return[this.el]}}});var BaseInputTerminalView=TerminalView.extend({className:"terminal input-terminal",initialize:function(c){var f=c.node;var a=c.input;var b=a.name;var d=this.terminalForInput(a);if(!d.multiple){this.setupMappingView(d)}this.el.terminal=d;d.node=f;d.name=b;f.input_terminals[b]=d},events:{dropinit:"onDropInit",dropstart:"onDropStart",dropend:"onDropEnd",drop:"onDrop",hover:"onHover",},onDropInit:function(b,c){var a=this.el.terminal;return $(c.drag).hasClass("output-terminal")&&a.canAccept(c.drag.terminal)},onDropStart:function(a,b){if(b.proxy.terminal){b.proxy.terminal.connectors[0].inner_color="#BBFFBB"}},onDropEnd:function(a,b){if(b.proxy.terminal){b.proxy.terminal.connectors[0].inner_color="#FFFFFF"}},onDrop:function(b,c){var a=this.el.terminal;new Connector(c.drag.terminal,a).redraw()},onHover:function(){var c=this.el;var b=c.terminal;if(b.connectors.length>0){var a=$("
").css({display:"none"}).appendTo("body").append($("
").append($("
").addClass("fa-icon-button fa fa-times").click(function(){$.each(b.connectors,function(f,d){if(d){d.destroy()}});a.remove()}))).bind("mouseleave",function(){$(this).remove()});a.css({top:$(c).offset().top-2,left:$(c).offset().left-a.width(),"padding-right":$(c).width()}).show()}},});var InputTerminalView=BaseInputTerminalView.extend({terminalMappingClass:InputTerminalMapping,terminalMappingViewClass:InputTerminalMappingView,terminalForInput:function(a){return new InputTerminal({element:this.el,input:a})},});var InputCollectionTerminalView=BaseInputTerminalView.extend({terminalMappingClass:InputCollectionTerminalMapping,terminalMappingViewClass:InputCollectionTerminalMappingView,terminalForInput:function(a){return new InputCollectionTerminal({element:this.el,input:a})},});var BaseOutputTerminalView=TerminalView.extend({className:"terminal output-terminal",initialize:function(c){var f=c.node;var a=c.output;var b=a.name;var d=this.terminalForOutput(a);this.setupMappingView(d);this.el.terminal=d;d.node=f;d.name=b;f.output_terminals[b]=d},events:{drag:"onDrag",dragstart:"onDragStart",dragend:"onDragEnd",},onDrag:function(b,c){var a=function(){var f=$(c.proxy).offsetParent().offset(),d=c.offsetX-f.left,g=c.offsetY-f.top;$(c.proxy).css({left:d,top:g});c.proxy.terminal.redraw();canvas_manager.update_viewport_overlay()};a();$("#canvas-container").get(0).scroll_panel.test(b,a)},onDragStart:function(b,f){$(f.available).addClass("input-terminal-active");workflow.check_changes_in_active_form();var a=$('
').appendTo("#canvas-container").get(0);a.terminal=new OutputTerminal({element:a});var g=new Connector();g.dragging=true;g.connect(this.el.terminal,a.terminal);return a},onDragEnd:function(b,c){var a=c.proxy.terminal.connectors[0];if(a){a.destroy()}$(c.proxy).remove();$(c.available).removeClass("input-terminal-active");$("#canvas-container").get(0).scroll_panel.stop()}});var OutputTerminalView=BaseOutputTerminalView.extend({terminalMappingClass:OutputTerminalMapping,terminalMappingViewClass:OutputTerminalMappingView,terminalForOutput:function(a){var c=a.extensions;var b=new OutputTerminal({element:this.el,datatypes:c});return b},});var OutputCollectionTerminalView=BaseOutputTerminalView.extend({terminalMappingClass:OutputCollectionTerminalMapping,terminalMappingViewClass:OutputCollectionTerminalMappingView,terminalForOutput:function(a){var c=a.collection_type;var b=new OutputCollectionTerminal({element:this.el,collection_type:c,datatypes:a.extensions});return b},});function ScrollPanel(a){this.panel=a}$.extend(ScrollPanel.prototype,{test:function(v,d){clearTimeout(this.timeout);var k=v.pageX,j=v.pageY,l=$(this.panel),c=l.position(),b=l.width(),i=l.height(),w=l.parent(),s=w.width(),a=w.height(),r=w.offset(),p=r.left,m=r.top,A=p+w.width(),u=m+w.height(),B=-(b-(s/2)),z=-(i-(a/2)),g=(s/2),f=(a/2),h=false,q=5,o=23;if(k-qA){if(c.left>B){var n=Math.min(o,c.left-B);l.css("left",c.left-n);h=true}}else{if(j-qu){if(c.top>z){var n=Math.min(o,c.top-B);l.css("top",(c.top-n)+"px");h=true}}}}}if(h){d();var l=this;this.timeout=setTimeout(function(){l.test(v,d)},50)}},stop:function(b,a){clearTimeout(this.timeout)}});function CanvasManager(b,a){this.cv=b;this.cc=this.cv.find("#canvas-container");this.oc=a.find("#overview-canvas");this.ov=a.find("#overview-viewport");this.init_drag()}$.extend(CanvasManager.prototype,{init_drag:function(){var b=this;var a=function(f,g){f=Math.min(f,b.cv.width()/2);f=Math.max(f,-b.cc.width()+b.cv.width()/2);g=Math.min(g,b.cv.height()/2);g=Math.max(g,-b.cc.height()+b.cv.height()/2);b.cc.css({left:f,top:g});b.update_viewport_overlay()};this.cc.each(function(){this.scroll_panel=new ScrollPanel(this)});var d,c;this.cv.bind("dragstart",function(){var g=$(this).offset();var f=b.cc.position();c=f.top-g.top;d=f.left-g.left}).bind("drag",function(f,g){a(g.offsetX+d,g.offsetY+c)}).bind("dragend",function(){workflow.fit_canvas_to_nodes();b.draw_overview()});this.ov.bind("drag",function(k,l){var h=b.cc.width(),n=b.cc.height(),m=b.oc.width(),j=b.oc.height(),f=$(this).offsetParent().offset(),i=l.offsetX-f.left,g=l.offsetY-f.top;a(-(i/m*h),-(g/j*n))}).bind("dragend",function(){workflow.fit_canvas_to_nodes();b.draw_overview()});$("#overview-border").bind("drag",function(g,i){var j=$(this).offsetParent();var h=j.offset();var f=Math.max(j.width()-(i.offsetX-h.left),j.height()-(i.offsetY-h.top));$(this).css({width:f,height:f});b.draw_overview()});$("#overview-border div").bind("drag",function(){})},update_viewport_overlay:function(){var b=this.cc,f=this.cv,a=this.oc,c=this.ov,d=b.width(),j=b.height(),i=a.width(),g=a.height(),h=b.position();c.css({left:-(h.left/d*i),top:-(h.top/j*g),width:(f.width()/d*i)-2,height:(f.height()/j*g)-2})},draw_overview:function(){var j=$("#overview-canvas"),m=j.parent().parent().width(),i=j.get(0).getContext("2d"),d=$("#canvas-container").width(),l=$("#canvas-container").height();var g,a,k,f;var h=this.cv.width();var b=this.cv.height();if(d0){i.fillStyle="#E8A92D";i.strokeStyle="#E8A92D"}}i.fillRect(c,r,o,p);i.strokeRect(c,r,o,p)});this.update_viewport_overlay()}}); \ No newline at end of file +function CollectionTypeDescription(a){this.collectionType=a;this.isCollection=true;this.rank=a.split(":").length}$.extend(CollectionTypeDescription.prototype,{append:function(a){if(a===NULL_COLLECTION_TYPE_DESCRIPTION){return this}if(a===ANY_COLLECTION_TYPE_DESCRIPTION){return otherCollectionType}return new CollectionTypeDescription(this.collectionType+":"+a.collectionType)},canMatch:function(a){if(a===NULL_COLLECTION_TYPE_DESCRIPTION){return false}if(a===ANY_COLLECTION_TYPE_DESCRIPTION){return true}return a.collectionType==this.collectionType},canMapOver:function(b){if(b===NULL_COLLECTION_TYPE_DESCRIPTION){return false}if(b===ANY_COLLECTION_TYPE_DESCRIPTION){return false}if(this.rank<=b.rank){return false}var a=b.collectionType;return this._endsWith(this.collectionType,a)},effectiveMapOver:function(a){var c=a.collectionType;var b=this.collectionType.substring(0,this.collectionType.length-c.length-1);return new CollectionTypeDescription(b)},equal:function(a){return a.collectionType==this.collectionType},toString:function(){return"CollectionType["+this.collectionType+"]"},_endsWith:function(b,a){return b.indexOf(a,b.length-a.length)!==-1}});NULL_COLLECTION_TYPE_DESCRIPTION={isCollection:false,canMatch:function(a){return false},canMapOver:function(a){return false},toString:function(){return"NullCollectionType[]"},append:function(a){return a},equal:function(a){return a===this}};ANY_COLLECTION_TYPE_DESCRIPTION={isCollection:true,canMatch:function(a){return NULL_COLLECTION_TYPE_DESCRIPTION!==a},canMapOver:function(a){return false},toString:function(){return"AnyCollectionType[]"},append:function(a){throw"Cannot append to ANY_COLLECTION_TYPE_DESCRIPTION"},equal:function(a){return a===this}};var TerminalMapping=Backbone.Model.extend({initialize:function(a){this.mapOver=a.mapOver||NULL_COLLECTION_TYPE_DESCRIPTION;this.terminal=a.terminal;this.terminal.terminalMapping=this},disableMapOver:function(){this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION)},setMapOver:function(a){this.mapOver=a;this.trigger("change")}});var TerminalMappingView=Backbone.View.extend({tagName:"div",className:"fa-icon-button fa fa-folder-o",initialize:function(b){var a="Run tool in parallel over collection";this.$el.tooltip({delay:500,title:a});this.model.bind("change",_.bind(this.render,this))},render:function(){if(this.model.mapOver.isCollection){this.$el.show()}else{this.$el.hide()}},});var InputTerminalMappingView=TerminalMappingView.extend({events:{click:"onClick",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",},onMouseEnter:function(b){var a=this.model;if(!a.terminal.connected()&&a.mapOver.isCollection){this.$el.css("color","red")}},onMouseLeave:function(a){this.$el.css("color","black")},onClick:function(b){var a=this.model;if(!a.terminal.connected()&&a.mapOver.isCollection){a.terminal.resetMapping()}},});var InputTerminalMapping=TerminalMapping;var InputCollectionTerminalMapping=TerminalMapping;var OutputTerminalMapping=TerminalMapping;var OutputTerminalMappingView=TerminalMappingView;var InputCollectionTerminalMappingView=InputTerminalMappingView;var OutputCollectionTerminalMapping=TerminalMapping;var OutputCollectionTerminalMappingView=TerminalMappingView;var Terminal=Backbone.Model.extend({initialize:function(a){this.element=a.element;this.connectors=[]},connect:function(a){this.connectors.push(a);if(this.node){this.node.markChanged()}},disconnect:function(a){this.connectors.splice($.inArray(a,this.connectors),1);if(this.node){this.node.markChanged();this.resetMappingIfNeeded()}},redraw:function(){$.each(this.connectors,function(a,b){b.redraw()})},destroy:function(){$.each(this.connectors.slice(),function(a,b){b.destroy()})},destroyInvalidConnections:function(){_.each(this.connectors,function(a){a.destroyIfInvalid()})},setMapOver:function(a){if(this.multiple){return}if(!this.mapOver().equal(a)){this.terminalMapping.setMapOver(a);_.each(this.node.output_terminals,function(b){b.setMapOver(a)})}},mapOver:function(){if(!this.terminalMapping){return NULL_COLLECTION_TYPE_DESCRIPTION}else{return this.terminalMapping.mapOver}},isMappedOver:function(){return this.terminalMapping&&this.terminalMapping.mapOver.isCollection},resetMapping:function(){this.terminalMapping.disableMapOver()},resetMappingIfNeeded:function(){},});var OutputTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.datatypes=a.datatypes},resetMappingIfNeeded:function(){if(!this.node.hasConnectedOutputTerminals()&&!this.node.hasConnectedMappedInputTerminals()){_.each(this.node.mappedInputTerminals(),function(b){b.resetMappingIfNeeded()})}var a=!this.node.hasMappedOverInputTerminals();if(a){this.resetMapping()}},resetMapping:function(){this.terminalMapping.disableMapOver();_.each(this.connectors,function(a){var b=a.handle2;if(b){b.resetMappingIfNeeded();a.destroyIfInvalid()}})}});var BaseInputTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.update(a.input)},canAccept:function(a){if(this._inputFilled()){return false}else{return this.attachable(a)}},resetMappingIfNeeded:function(){var b=this.mapOver();if(!b.isCollection){return}var a=this.node.hasConnectedMappedInputTerminals()||(!this.node.hasConnectedOutputTerminals());if(a){this.resetMapping()}},resetMapping:function(){this.terminalMapping.disableMapOver();if(!this.node.hasMappedOverInputTerminals()){_.each(this.node.output_terminals,function(a){a.resetMapping()})}},connected:function(){return this.connectors.length!==0},_inputFilled:function(){var a;if(!this.connected()){a=false}else{if(this.multiple){if(this._collectionAttached()){inputsFilled=true}else{a=false}}else{a=true}}return a},_collectionAttached:function(){if(!this.connected()){return false}else{var a=this.connectors[0].handle1;if(!a){return false}else{if(a.isDataCollectionInput||a.isMappedOver()||a.datatypes.indexOf("input_collection")>0){return true}else{return false}}}},_mappingConstraints:function(){if(!this.node){return[]}var b=this.mapOver();if(b.isCollection){return[b]}var a=[];if(!this.node.hasConnectedOutputTerminals()){_.each(this.node.connectedMappedInputTerminals(),function(c){a.push(c.mapOver())})}else{a.push(_.first(_.values(this.node.output_terminals)).mapOver())}return a},_producesAcceptableDatatype:function(a){for(var c in this.datatypes){var f=new Array();f=f.concat(a.datatypes);if(a.node.post_job_actions){for(var d in a.node.post_job_actions){var g=a.node.post_job_actions[d];if(g.action_type=="ChangeDatatypeAction"&&(g.output_name==""||g.output_name==a.name)&&g.action_arguments){f.push(g.action_arguments.newtype)}}}for(var b in f){var h=f[b];if(h=="input"||h=="input_collection"||issubtype(f[b],this.datatypes[c])){return true}}}return false},_otherCollectionType:function(a){var c=NULL_COLLECTION_TYPE_DESCRIPTION;if(a.isDataCollectionInput){c=a.collectionType}else{var b=a.mapOver();if(b.isCollection){c=b}}return c},});var InputTerminal=BaseInputTerminal.extend({update:function(a){this.datatypes=a.extensions;this.multiple=a.multiple;this.collection=false},connect:function(a){BaseInputTerminal.prototype.connect.call(this,a);var b=a.handle1;if(!b){return}var c=this._otherCollectionType(b);if(c.isCollection){this.setMapOver(c)}},attachable:function(b){var d=this._otherCollectionType(b);var a=this.mapOver();if(d.isCollection){if(this.multiple){if(this.connected()&&!this._collectionAttached()){return false}if(d.rank==1){return this._producesAcceptableDatatype(b)}else{return false}}if(a.isCollection&&a.canMatch(d)){return this._producesAcceptableDatatype(b)}else{var c=this._mappingConstraints();if(c.every(_.bind(d.canMatch,d))){return this._producesAcceptableDatatype(b)}else{return false}}}else{if(a.isCollection){return false}}return this._producesAcceptableDatatype(b)}});var InputCollectionTerminal=BaseInputTerminal.extend({update:function(a){this.multiple=false;this.collection=true;this.datatypes=a.extensions;if(a.collection_type){this.collectionType=new CollectionTypeDescription(a.collection_type)}else{this.collectionType=ANY_COLLECTION_TYPE_DESCRIPTION}},connect:function(b){BaseInputTerminal.prototype.connect.call(this,b);var a=b.handle1;if(!a){return}var c=this._effectiveMapOver(a);this.setMapOver(c)},_effectiveMapOver:function(a){var b=this.collectionType;var c=this._otherCollectionType(a);if(!b.canMatch(c)){return c.effectiveMapOver(b)}else{return NULL_COLLECTION_TYPE_DESCRIPTION}},_effectiveCollectionType:function(){var b=this.collectionType;var a=this.mapOver();return a.append(b)},attachable:function(b){var g=this._otherCollectionType(b);if(g.isCollection){var f=this._effectiveCollectionType();var a=this.mapOver();if(f.canMatch(g)){return this._producesAcceptableDatatype(b)}else{if(a.isCollection){return false}else{if(g.canMapOver(this.collectionType)){var d=this._effectiveMapOver(b);if(!d.isCollection){return false}var c=this._mappingConstraints();if(c.every(d.canMatch)){return this._producesAcceptableDatatype(b)}}}}}return false}});var OutputCollectionTerminal=Terminal.extend({initialize:function(a){Terminal.prototype.initialize.call(this,a);this.datatypes=a.datatypes;this.collectionType=new CollectionTypeDescription(a.collection_type);this.isDataCollectionInput=true},update:function(a){var b=new CollectionTypeDescription(a.collection_type);if(b.collectionType!=this.collectionType.collectionType){_.each(this.connectors,function(c){c.destroy()})}this.collectionType=b}});function Connector(b,a){this.canvas=null;this.dragging=false;this.inner_color="#FFFFFF";this.outer_color="#D8B365";if(b&&a){this.connect(b,a)}}$.extend(Connector.prototype,{connect:function(b,a){this.handle1=b;if(this.handle1){this.handle1.connect(this)}this.handle2=a;if(this.handle2){this.handle2.connect(this)}},destroy:function(){if(this.handle1){this.handle1.disconnect(this)}if(this.handle2){this.handle2.disconnect(this)}$(this.canvas).remove()},destroyIfInvalid:function(){if(this.handle1&&this.handle2&&!this.handle2.attachable(this.handle1)){this.destroy()}},redraw:function(){var f=$("#canvas-container");if(!this.canvas){this.canvas=document.createElement("canvas");if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(this.canvas)}f.append($(this.canvas));if(this.dragging){this.canvas.style.zIndex="300"}}var v=function(c){return $(c).offset().left-f.offset().left};var p=function(c){return $(c).offset().top-f.offset().top};if(!this.handle1||!this.handle2){return}var o=v(this.handle1.element)+5;var n=p(this.handle1.element)+5;var x=v(this.handle2.element)+5;var u=p(this.handle2.element)+5;var k=100;var r=Math.min(o,x);var a=Math.max(o,x);var q=Math.min(n,u);var B=Math.max(n,u);var d=Math.min(Math.max(Math.abs(B-q)/2,100),300);var w=r-k;var A=q-k;var y=a-r+2*k;var s=B-q+2*k;this.canvas.style.left=w+"px";this.canvas.style.top=A+"px";this.canvas.setAttribute("width",y);this.canvas.setAttribute("height",s);o-=w;n-=A;x-=w;u-=A;var z=this.canvas.getContext("2d"),h=null,l=null;var g=1;if(this.handle1&&this.handle1.isMappedOver()){var h=[-6,-3,0,3,6];g=5}else{var h=[0]}if(this.handle2&&this.handle2.isMappedOver()){var l=[-6,-3,0,3,6];g=5}else{var l=[0]}var b=this;for(var t=0;t1||l.length>1){m=1;j=3}b.draw_outlined_curve(o,n,x,u,d,m,j,h[t%h.length],l[t%l.length])}},draw_outlined_curve:function(j,i,l,k,a,b,f,g,d){var g=g||0;var d=d||0;var h=this.canvas.getContext("2d");h.lineCap="round";h.strokeStyle=this.outer_color;h.lineWidth=f;h.beginPath();h.moveTo(j,i+g);h.bezierCurveTo(j+a,i+g,l-a,k+d,l,k+d);h.stroke();h.strokeStyle=this.inner_color;h.lineWidth=b;h.beginPath();h.moveTo(j,i+g);h.bezierCurveTo(j+a,i+g,l-a,k+d,l,k+d);h.stroke()}});var Node=Backbone.Model.extend({initialize:function(a){this.element=a.element;this.input_terminals={};this.output_terminals={};this.tool_errors={}},connectedOutputTerminals:function(){return this._connectedTerminals(this.output_terminals)},_connectedTerminals:function(b){var a=[];$.each(b,function(c,d){if(d.connectors.length>0){a.push(d)}});return a},hasConnectedOutputTerminals:function(){var a=this.output_terminals;for(var b in a){if(a[b].connectors.length>0){return true}}return false},connectedMappedInputTerminals:function(){return this._connectedMappedTerminals(this.input_terminals)},hasConnectedMappedInputTerminals:function(){var c=this.input_terminals;for(var b in c){var a=c[b];if(a.connectors.length>0&&a.isMappedOver()){return true}}return false},_connectedMappedTerminals:function(b){var a=[];$.each(b,function(c,d){var f=d.mapOver();if(f.isCollection){if(d.connectors.length>0){a.push(d)}}});return a},mappedInputTerminals:function(){return this._mappedTerminals(this.input_terminals)},_mappedTerminals:function(b){var a=[];$.each(b,function(c,d){var f=d.mapOver();if(f.isCollection){a.push(d)}});return a},hasMappedOverInputTerminals:function(){var a=false;_.each(this.input_terminals,function(b){var c=b.mapOver();if(c.isCollection){a=true}});return a},redraw:function(){$.each(this.input_terminals,function(a,b){b.redraw()});$.each(this.output_terminals,function(a,b){b.redraw()})},destroy:function(){$.each(this.input_terminals,function(a,b){b.destroy()});$.each(this.output_terminals,function(a,b){b.destroy()});workflow.remove_node(this);$(this.element).remove()},make_active:function(){$(this.element).addClass("toolForm-active")},make_inactive:function(){var a=this.element.get(0);(function(b){b.removeChild(a);b.appendChild(a)})(a.parentNode);$(a).removeClass("toolForm-active")},init_field_data:function(b){if(b.type){this.type=b.type}this.name=b.name;this.form_html=b.form_html;this.tool_state=b.tool_state;this.tool_errors=b.tool_errors;this.tooltip=b.tooltip?b.tooltip:"";this.annotation=b.annotation;this.post_job_actions=b.post_job_actions?b.post_job_actions:{};this.label=b.label;this.uuid=b.uuid;this.workflow_outputs=b.workflow_outputs?b.workflow_outputs:[];var a=this;var c=new NodeView({el:this.element[0],node:a,});a.nodeView=c;$.each(b.data_inputs,function(f,d){c.addDataInput(d)});if((b.data_inputs.length>0)&&(b.data_outputs.length>0)){c.addRule()}$.each(b.data_outputs,function(f,d){c.addDataOutput(d)});c.render();workflow.node_changed(this,true)},update_field_data:function(d){var c=this;nodeView=c.nodeView;this.tool_state=d.tool_state;this.form_html=d.form_html;this.tool_errors=d.tool_errors;this.annotation=d.annotation;if("post_job_actions" in d){var f=$.parseJSON(d.post_job_actions);this.post_job_actions=f?f:{}}c.nodeView.renderToolErrors();var g=nodeView.$("div.inputs");var a=nodeView.newInputsDiv();var b={};_.each(d.data_inputs,function(h){var i=c.nodeView.addDataInput(h,a);b[h.name]=i});_.each(_.difference(_.values(nodeView.terminalViews),_.values(b)),function(h){h.el.terminal.destroy()});nodeView.terminalViews=b;if(d.data_outputs.length==1&&"collection_type" in d.data_outputs[0]){nodeView.updateDataOutput(d.data_outputs[0])}g.replaceWith(a);this.markChanged();this.redraw()},error:function(d){var a=$(this.element).find(".toolFormBody");a.find("div").remove();var c="
"+d+"
";this.form_html=c;a.html(c);workflow.node_changed(this)},markChanged:function(){workflow.node_changed(this)}});function Workflow(a){this.canvas_container=a;this.id_counter=0;this.nodes={};this.name=null;this.has_changes=false;this.active_form_has_changes=false}$.extend(Workflow.prototype,{add_node:function(a){a.id=this.id_counter;a.element.attr("id","wf-node-step-"+a.id);this.id_counter++;this.nodes[a.id]=a;this.has_changes=true;a.workflow=this},remove_node:function(a){if(this.active_node==a){this.clear_active_node()}delete this.nodes[a.id];this.has_changes=true},remove_all:function(){wf=this;$.each(this.nodes,function(b,a){a.destroy();wf.remove_node(a)})},rectify_workflow_outputs:function(){var b=false;var a=false;$.each(this.nodes,function(c,d){if(d.workflow_outputs&&d.workflow_outputs.length>0){b=true}$.each(d.post_job_actions,function(g,f){if(f.action_type==="HideDatasetAction"){a=true}})});if(b!==false||a!==false){$.each(this.nodes,function(c,g){if(g.type==="tool"){var f=false;if(g.post_job_actions==null){g.post_job_actions={};f=true}var d=[];$.each(g.post_job_actions,function(i,h){if(h.action_type=="HideDatasetAction"){d.push(i)}});if(d.length>0){$.each(d,function(h,j){f=true;delete g.post_job_actions[j]})}if(b){$.each(g.output_terminals,function(i,j){var h=true;$.each(g.workflow_outputs,function(l,m){if(j.name===m){h=false}});if(h===true){f=true;var k={action_type:"HideDatasetAction",output_name:j.name,action_arguments:{}};g.post_job_actions["HideDatasetAction"+j.name]=null;g.post_job_actions["HideDatasetAction"+j.name]=k}})}if(workflow.active_node==g&&f===true){workflow.reload_active_node()}}})}},to_simple:function(){var a={};$.each(this.nodes,function(c,f){var g={};$.each(f.input_terminals,function(i,j){g[j.name]=null;var h=[];$.each(j.connectors,function(k,l){h[k]={id:l.handle1.node.id,output_name:l.handle1.name};g[j.name]=h})});var b={};if(f.post_job_actions){$.each(f.post_job_actions,function(j,h){var k={action_type:h.action_type,output_name:h.output_name,action_arguments:h.action_arguments};b[h.action_type+h.output_name]=null;b[h.action_type+h.output_name]=k})}if(!f.workflow_outputs){f.workflow_outputs=[]}var d={id:f.id,type:f.type,tool_id:f.tool_id,tool_state:f.tool_state,tool_errors:f.tool_errors,input_connections:g,position:$(f.element).position(),annotation:f.annotation,post_job_actions:f.post_job_actions,uuid:f.uuid,label:f.label,workflow_outputs:f.workflow_outputs};a[f.id]=d});return{steps:a}},from_simple:function(b){wf=this;var c=0;wf.name=b.name;var a=false;$.each(b.steps,function(g,f){var d=prebuild_node(f.type,f.name,f.tool_id);d.init_field_data(f);if(f.position){d.element.css({top:f.position.top,left:f.position.left})}d.id=f.id;wf.nodes[d.id]=d;c=Math.max(c,parseInt(g));if(!a&&d.type==="tool"){if(d.workflow_outputs.length>0){a=true}else{$.each(d.post_job_actions,function(i,h){if(h.action_type==="HideDatasetAction"){a=true}})}}});wf.id_counter=c+1;$.each(b.steps,function(g,f){var d=wf.nodes[g];$.each(f.input_connections,function(i,h){if(h){if(!$.isArray(h)){h=[h]}$.each(h,function(k,j){var m=wf.nodes[j.id];var n=new Connector();n.connect(m.output_terminals[j.output_name],d.input_terminals[i]);n.redraw()})}});if(a&&d.type==="tool"){$.each(d.output_terminals,function(h,i){if(d.post_job_actions["HideDatasetAction"+i.name]===undefined){d.workflow_outputs.push(i.name);callout=$(d.element).find(".callout."+i.name);callout.find("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png");workflow.has_changes=true}})}})},check_changes_in_active_form:function(){if(this.active_form_has_changes){this.has_changes=true;$("#right-content").find("form").submit();this.active_form_has_changes=false}},reload_active_node:function(){if(this.active_node){var a=this.active_node;this.clear_active_node();this.activate_node(a)}},clear_active_node:function(){if(this.active_node){this.active_node.make_inactive();this.active_node=null}parent.show_form_for_tool("
No node selected
")},activate_node:function(a){if(this.active_node!=a){this.check_changes_in_active_form();this.clear_active_node();parent.show_form_for_tool(a.form_html+a.tooltip,a);a.make_active();this.active_node=a}},node_changed:function(a,b){this.has_changes=true;if(this.active_node==a&&(!parent.__NEWTOOLFORM__||b)){this.check_changes_in_active_form();parent.show_form_for_tool(a.form_html+a.tooltip,a)}},layout:function(){this.check_changes_in_active_form();this.has_changes=true;var i={};var b={};$.each(this.nodes,function(l,k){if(i[l]===undefined){i[l]=0}if(b[l]===undefined){b[l]=[]}});$.each(this.nodes,function(l,k){$.each(k.input_terminals,function(m,n){$.each(n.connectors,function(p,q){var o=q.handle1.node;i[k.id]+=1;b[o.id].push(k.id)})})});node_ids_by_level=[];while(true){level_parents=[];for(var a in i){if(i[a]==0){level_parents.push(a)}}if(level_parents.length==0){break}node_ids_by_level.push(level_parents);for(var f in level_parents){var j=level_parents[f];delete i[j];for(var g in b[j]){i[b[j][g]]-=1}}}if(i.length){return}var d=this.nodes;var h=80;v_pad=30;var c=h;$.each(node_ids_by_level,function(k,l){l.sort(function(p,o){return $(d[p].element).position().top-$(d[o].element).position().top});var m=0;var n=v_pad;$.each(l,function(o,r){var q=d[r];var p=$(q.element);$(p).css({top:n,left:c});m=Math.max(m,$(p).width());n+=$(p).height()+v_pad});c+=m+h});$.each(d,function(k,l){l.redraw()})},bounds_for_all_nodes:function(){var d=Infinity,b=-Infinity,c=Infinity,a=-Infinity,f;$.each(this.nodes,function(h,g){e=$(g.element);f=e.position();d=Math.min(d,f.left);b=Math.max(b,f.left+e.width());c=Math.min(c,f.top);a=Math.max(a,f.top+e.width())});return{xmin:d,xmax:b,ymin:c,ymax:a}},fit_canvas_to_nodes:function(){var a=this.bounds_for_all_nodes();var f=this.canvas_container.position();var i=this.canvas_container.parent();var d=fix_delta(a.xmin,100);var h=fix_delta(a.ymin,100);d=Math.max(d,f.left);h=Math.max(h,f.top);var c=f.left-d;var g=f.top-h;var b=round_up(a.xmax+100,100)+d;var j=round_up(a.ymax+100,100)+h;b=Math.max(b,-c+i.width());j=Math.max(j,-g+i.height());this.canvas_container.css({left:c,top:g,width:b,height:j});this.canvas_container.children().each(function(){var k=$(this).position();$(this).css("left",k.left+d);$(this).css("top",k.top+h)})}});function fix_delta(a,b){if(a3*b){new_pos=(Math.ceil(((a%b))/b)+1)*b;return(-(a-new_pos))}return 0}function round_up(a,b){return Math.ceil(a/b)*b}function prebuild_node(l,j,r){var i=$("
");var g=new Node({element:i});g.type=l;if(l=="tool"){g.tool_id=r}var n=$("
"+j+"
");i.append(n);i.css("left",$(window).scrollLeft()+20);i.css("top",$(window).scrollTop()+20);var m=$("
");var h="
loading tool info...
";m.append(h);g.form_html=h;i.append(m);var k=$("
");k.append($("
").addClass("fa-icon-button fa fa-times").click(function(b){g.destroy()}));i.appendTo("#canvas-container");var d=$("#canvas-container").position();var c=$("#canvas-container").parent();var a=i.width();var q=i.height();i.css({left:(-d.left)+(c.width()/2)-(a/2),top:(-d.top)+(c.height()/2)-(q/2)});k.prependTo(n);a+=(k.width()+10);i.css("width",a);$(i).bind("dragstart",function(){workflow.activate_node(g)}).bind("dragend",function(){workflow.node_changed(this);workflow.fit_canvas_to_nodes();canvas_manager.draw_overview()}).bind("dragclickonly",function(){workflow.activate_node(g)}).bind("drag",function(o,p){var f=$(this).offsetParent().offset(),b=p.offsetX-f.left,s=p.offsetY-f.top;$(this).css({left:b,top:s});$(this).find(".terminal").each(function(){this.terminal.redraw()})});return g}function add_node(b,d,a){var c=prebuild_node(b,d,a);workflow.add_node(c);workflow.fit_canvas_to_nodes();canvas_manager.draw_overview();workflow.activate_node(c);return c}function update_node(b){var a=workflow.active_node;if(a){a.update_field_data(b,true)}}var ext_to_type=null;var type_to_type=null;function issubtype(b,a){b=ext_to_type[b];a=ext_to_type[a];return(type_to_type[b])&&(a in type_to_type[b])}function populate_datatype_info(a){ext_to_type=a.ext_to_class_name;type_to_type=a.class_to_classes}var NodeView=Backbone.View.extend({initialize:function(a){this.node=a.node;this.output_width=Math.max(150,this.$el.width());this.tool_body=this.$el.find(".toolFormBody");this.tool_body.find("div").remove();this.newInputsDiv().appendTo(this.tool_body);this.terminalViews={};this.outputTerminlViews={}},render:function(){this.renderToolErrors();this.$el.css("width",Math.min(250,Math.max(this.$el.width(),this.output_width)))},renderToolErrors:function(){if(this.node.tool_errors){this.$el.addClass("tool-node-error")}else{this.$el.removeClass("tool-node-error")}},newInputsDiv:function(){return $("
")},updateMaxWidth:function(a){this.output_width=Math.max(this.output_width,a)},addRule:function(){this.tool_body.append($("
"))},addDataInput:function(i,d){var j=true;if(!d){d=this.$(".inputs");j=false}var f=this.terminalViews[i.name];var h=(i.input_type=="dataset_collection")?InputCollectionTerminalView:InputTerminalView;if(f&&!(f instanceof h)){f.el.terminal.destroy();f=null}if(!f){f=new h({node:this.node,input:i})}else{var g=f.el.terminal;g.update(i);g.destroyInvalidConnections()}this.terminalViews[i.name]=f;var c=f.el;var b=new DataInputView({terminalElement:c,input:i,nodeView:this,skipResize:j});var a=b.$el;d.append(a.prepend(f.terminalElements()));return f},addDataOutput:function(a){var d=(a.collection_type)?OutputCollectionTerminalView:OutputTerminalView;var c=new d({node:this.node,output:a});var b=new DataOutputView({output:a,terminalElement:c.el,nodeView:this,});this.tool_body.append(b.$el.append(c.terminalElements()))},updateDataOutput:function(b){var a=this.node.output_terminals[b.name];a.update(b)}});var DataInputView=Backbone.View.extend({className:"form-row dataRow input-data-row",initialize:function(a){this.input=a.input;this.nodeView=a.nodeView;this.terminalElement=a.terminalElement;this.$el.attr("name",this.input.name).html(this.input.label);if(!a.skipResize){this.$el.css({position:"absolute",left:-1000,top:-1000,display:"none"});$("body").append(this.el);this.nodeView.updateMaxWidth(this.$el.outerWidth());this.$el.css({position:"",left:"",top:"",display:""});this.$el.remove()}},});var OutputCalloutView=Backbone.View.extend({tagName:"div",initialize:function(b){this.label=b.label;this.node=b.node;this.output=b.output;var a=this;this.$el.attr("class","callout "+this.label).css({display:"none"}).append($("
").append($("").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png").click(function(){if($.inArray(a.output.name,a.node.workflow_outputs)!=-1){a.node.workflow_outputs.splice($.inArray(a.output.name,a.node.workflow_outputs),1);a.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png")}else{a.node.workflow_outputs.push(a.output.name);a.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png")}workflow.has_changes=true;canvas_manager.draw_overview()}))).tooltip({delay:500,title:"Mark dataset as a workflow output. All unmarked datasets will be hidden."});this.$el.css({top:"50%",margin:"-8px 0px 0px 0px",right:8});this.$el.show();this.resetImage()},resetImage:function(){if($.inArray(this.output.name,this.node.workflow_outputs)===-1){this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-outline.png")}else{this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small.png")}},hoverImage:function(){this.$("img").attr("src",galaxy_config.root+"static/images/fugue/asterisk-small-yellow.png")}});var DataOutputView=Backbone.View.extend({className:"form-row dataRow",initialize:function(c){this.output=c.output;this.terminalElement=c.terminalElement;this.nodeView=c.nodeView;var a=this.output;var b=a.name;var f=this.nodeView.node;var d=a.extensions.indexOf("input")>=0||a.extensions.indexOf("input_collection")>=0;if(!d){b=b+" ("+a.extensions.join(", ")+")"}this.$el.html(b);if(f.type=="tool"){var g=new OutputCalloutView({label:b,output:a,node:f,});this.$el.append(g.el);this.$el.hover(function(){g.hoverImage()},function(){g.resetImage()})}this.$el.css({position:"absolute",left:-1000,top:-1000,display:"none"});$("body").append(this.el);this.nodeView.updateMaxWidth(this.$el.outerWidth()+17);this.$el.css({position:"",left:"",top:"",display:""}).detach()}});var TerminalView=Backbone.View.extend({setupMappingView:function(b){var c=new this.terminalMappingClass({terminal:b});var a=new this.terminalMappingViewClass({model:c});a.render();b.terminalMappingView=a;this.terminalMappingView=a},terminalElements:function(){if(this.terminalMappingView){return[this.terminalMappingView.el,this.el]}else{return[this.el]}}});var BaseInputTerminalView=TerminalView.extend({className:"terminal input-terminal",initialize:function(c){var f=c.node;var a=c.input;var b=a.name;var d=this.terminalForInput(a);if(!d.multiple){this.setupMappingView(d)}this.el.terminal=d;d.node=f;d.name=b;f.input_terminals[b]=d},events:{dropinit:"onDropInit",dropstart:"onDropStart",dropend:"onDropEnd",drop:"onDrop",hover:"onHover",},onDropInit:function(b,c){var a=this.el.terminal;return $(c.drag).hasClass("output-terminal")&&a.canAccept(c.drag.terminal)},onDropStart:function(a,b){if(b.proxy.terminal){b.proxy.terminal.connectors[0].inner_color="#BBFFBB"}},onDropEnd:function(a,b){if(b.proxy.terminal){b.proxy.terminal.connectors[0].inner_color="#FFFFFF"}},onDrop:function(b,c){var a=this.el.terminal;new Connector(c.drag.terminal,a).redraw()},onHover:function(){var c=this.el;var b=c.terminal;if(b.connectors.length>0){var a=$("
").css({display:"none"}).appendTo("body").append($("
").append($("
").addClass("fa-icon-button fa fa-times").click(function(){$.each(b.connectors,function(f,d){if(d){d.destroy()}});a.remove()}))).bind("mouseleave",function(){$(this).remove()});a.css({top:$(c).offset().top-2,left:$(c).offset().left-a.width(),"padding-right":$(c).width()}).show()}},});var InputTerminalView=BaseInputTerminalView.extend({terminalMappingClass:InputTerminalMapping,terminalMappingViewClass:InputTerminalMappingView,terminalForInput:function(a){return new InputTerminal({element:this.el,input:a})},});var InputCollectionTerminalView=BaseInputTerminalView.extend({terminalMappingClass:InputCollectionTerminalMapping,terminalMappingViewClass:InputCollectionTerminalMappingView,terminalForInput:function(a){return new InputCollectionTerminal({element:this.el,input:a})},});var BaseOutputTerminalView=TerminalView.extend({className:"terminal output-terminal",initialize:function(c){var f=c.node;var a=c.output;var b=a.name;var d=this.terminalForOutput(a);this.setupMappingView(d);this.el.terminal=d;d.node=f;d.name=b;f.output_terminals[b]=d},events:{drag:"onDrag",dragstart:"onDragStart",dragend:"onDragEnd",},onDrag:function(b,c){var a=function(){var f=$(c.proxy).offsetParent().offset(),d=c.offsetX-f.left,g=c.offsetY-f.top;$(c.proxy).css({left:d,top:g});c.proxy.terminal.redraw();canvas_manager.update_viewport_overlay()};a();$("#canvas-container").get(0).scroll_panel.test(b,a)},onDragStart:function(b,f){$(f.available).addClass("input-terminal-active");workflow.check_changes_in_active_form();var a=$('
').appendTo("#canvas-container").get(0);a.terminal=new OutputTerminal({element:a});var g=new Connector();g.dragging=true;g.connect(this.el.terminal,a.terminal);return a},onDragEnd:function(b,c){var a=c.proxy.terminal.connectors[0];if(a){a.destroy()}$(c.proxy).remove();$(c.available).removeClass("input-terminal-active");$("#canvas-container").get(0).scroll_panel.stop()}});var OutputTerminalView=BaseOutputTerminalView.extend({terminalMappingClass:OutputTerminalMapping,terminalMappingViewClass:OutputTerminalMappingView,terminalForOutput:function(a){var c=a.extensions;var b=new OutputTerminal({element:this.el,datatypes:c});return b},});var OutputCollectionTerminalView=BaseOutputTerminalView.extend({terminalMappingClass:OutputCollectionTerminalMapping,terminalMappingViewClass:OutputCollectionTerminalMappingView,terminalForOutput:function(a){var c=a.collection_type;var b=new OutputCollectionTerminal({element:this.el,collection_type:c,datatypes:a.extensions});return b},});function ScrollPanel(a){this.panel=a}$.extend(ScrollPanel.prototype,{test:function(v,d){clearTimeout(this.timeout);var k=v.pageX,j=v.pageY,l=$(this.panel),c=l.position(),b=l.width(),i=l.height(),w=l.parent(),s=w.width(),a=w.height(),r=w.offset(),p=r.left,m=r.top,A=p+w.width(),u=m+w.height(),B=-(b-(s/2)),z=-(i-(a/2)),g=(s/2),f=(a/2),h=false,q=5,o=23;if(k-qA){if(c.left>B){var n=Math.min(o,c.left-B);l.css("left",c.left-n);h=true}}else{if(j-qu){if(c.top>z){var n=Math.min(o,c.top-B);l.css("top",(c.top-n)+"px");h=true}}}}}if(h){d();var l=this;this.timeout=setTimeout(function(){l.test(v,d)},50)}},stop:function(b,a){clearTimeout(this.timeout)}});function CanvasManager(b,a){this.cv=b;this.cc=this.cv.find("#canvas-container");this.oc=a.find("#overview-canvas");this.ov=a.find("#overview-viewport");this.init_drag()}$.extend(CanvasManager.prototype,{init_drag:function(){var b=this;var a=function(f,g){f=Math.min(f,b.cv.width()/2);f=Math.max(f,-b.cc.width()+b.cv.width()/2);g=Math.min(g,b.cv.height()/2);g=Math.max(g,-b.cc.height()+b.cv.height()/2);b.cc.css({left:f,top:g});b.update_viewport_overlay()};this.cc.each(function(){this.scroll_panel=new ScrollPanel(this)});var d,c;this.cv.bind("dragstart",function(){var g=$(this).offset();var f=b.cc.position();c=f.top-g.top;d=f.left-g.left}).bind("drag",function(f,g){a(g.offsetX+d,g.offsetY+c)}).bind("dragend",function(){workflow.fit_canvas_to_nodes();b.draw_overview()});this.ov.bind("drag",function(k,l){var h=b.cc.width(),n=b.cc.height(),m=b.oc.width(),j=b.oc.height(),f=$(this).offsetParent().offset(),i=l.offsetX-f.left,g=l.offsetY-f.top;a(-(i/m*h),-(g/j*n))}).bind("dragend",function(){workflow.fit_canvas_to_nodes();b.draw_overview()});$("#overview-border").bind("drag",function(g,i){var j=$(this).offsetParent();var h=j.offset();var f=Math.max(j.width()-(i.offsetX-h.left),j.height()-(i.offsetY-h.top));$(this).css({width:f,height:f});b.draw_overview()});$("#overview-border div").bind("drag",function(){})},update_viewport_overlay:function(){var b=this.cc,f=this.cv,a=this.oc,c=this.ov,d=b.width(),j=b.height(),i=a.width(),g=a.height(),h=b.position();c.css({left:-(h.left/d*i),top:-(h.top/j*g),width:(f.width()/d*i)-2,height:(f.height()/j*g)-2})},draw_overview:function(){var j=$("#overview-canvas"),m=j.parent().parent().width(),i=j.get(0).getContext("2d"),d=$("#canvas-container").width(),l=$("#canvas-container").height();var g,a,k,f;var h=this.cv.width();var b=this.cv.height();if(d0){i.fillStyle="#E8A92D";i.strokeStyle="#E8A92D"}}i.fillRect(c,r,o,p);i.strokeRect(c,r,o,p)});this.update_viewport_overlay()}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-form.js b/static/scripts/packed/mvc/tools/tools-form.js index 6e8b6b289bf..e66145336be 100644 --- a/static/scripts/packed/mvc/tools/tools-form.js +++ b/static/scripts/packed/mvc/tools/tools-form.js @@ -1 +1 @@ -define(["utils/utils","utils/deferred","mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/citation/citation-model","mvc/citation/citation-view","mvc/tools","mvc/tools/tools-template","mvc/tools/tools-content","mvc/tools/tools-section","mvc/tools/tools-tree","mvc/tools/tools-jobs"],function(i,j,h,m,k,a,e,d,f,l,c,g){var b=Backbone.View.extend({container:"body",initialize:function(n){console.debug(n);var o=parent.Galaxy;if(o&&o.modal){this.modal=o.modal}else{this.modal=new m.Modal.View()}if(o&&o.currUser){this.is_admin=o.currUser.get("is_admin")}else{this.is_admin=false}this.options=n;this.deferred=new j();this.setElement("
");$(this.container).append(this.$el);this._buildForm()},reciept:function(n){$(this.container).empty();$(this.container).append(n)},highlight:function(o,p,n){var q=this.element_list[o];if(q){q.error(p||"Please verify this parameter.");if(!n){$(this.container).animate({scrollTop:q.$el.offset().top-20},500)}}},_buildModel:function(){var n=this;var o=galaxy_config.root+"api/tools/"+this.options.id+"/build?";if(this.options.job_id){o+="job_id="+this.options.job_id}else{if(this.options.dataset_id){o+="dataset_id="+this.options.dataset_id}else{o+="tool_version="+this.options.version+"&";var q=top.location.href;var r=q.indexOf("?");if(q.indexOf("tool_id=")!=-1&&r!==-1){o+=q.slice(r+1)}}}var p=this.deferred.register();i.request({type:"GET",url:o,success:function(s){n.options=s;n._buildForm();n.message.update({status:"success",message:"Now you are using '"+n.options.name+"' version "+n.options.version+".",persistent:false});n.deferred.done(p);console.debug("tools-form::initialize() - Initial tool model ready.");console.debug(s)},error:function(s){n.deferred.done(p);console.debug("tools-form::initialize() - Initial tool model request failed.");console.debug(s);var t=s.error||"Uncaught error.";n.modal.show({title:"Tool cannot be executed",body:t,buttons:{Close:function(){n.modal.hide()}}})}})},_updateModel:function(){var n=this;var o=this.tree.finalize({data:function(s){if(s.values.length>0&&s.values[0]&&s.values[0].src==="hda"){return n.content.get({id:s.values[0].id,src:"hda"}).id_uncoded}return null}});console.debug("tools-form::_refreshForm() - Refreshing states.");console.debug(o);function r(v){for(var t in n.input_list){var u=n.field_list[t];var s=n.input_list[t];if(s.is_dynamic&&u.wait&&u.unwait){if(v){u.wait()}else{u.unwait()}}}}r(true);var q=this.deferred.register();var p=galaxy_config.root+"api/tools/"+this.options.id+"/build?tool_version="+this.options.version;i.request({type:"GET",url:p,data:o,success:function(s){n.tree.matchModel(s,function(u,y){var t=n.input_list[u];if(t&&t.options){if(!_.isEqual(t.options,y.options)){t.options=y.options;var z=n.field_list[u];if(z.update){var x=[];if((["data","data_collection","drill_down"]).indexOf(t.type)!=-1){x=t.options}else{for(var w in y.options){var v=y.options[w];if(v.length>2){x.push({label:v[0],value:v[1]})}}}z.update(x);z.trigger("change");console.debug("Updating options for "+u)}}}});r(false);n.deferred.done(q);console.debug("tools-form::_refreshForm() - States refreshed.");console.debug(s)},error:function(s){n.deferred.done(q);console.debug("tools-form::_refreshForm() - Refresh request failed.");console.debug(s)}})},_buildForm:function(){var n=this;this.off("refresh");this.off("reset");this.field_list={};this.input_list={};this.element_list={};this.tree=new c(this);this.job_handler=new g(this);this.content=new f(this);var p=this.options;this._renderForm(p);this.tree.finalize();if(p.errors){var q=this.tree.matchResponse(p.errors);for(var o in q){this.highlight(o,q[o],true)}}this.on("refresh",function(){n.deferred.reset();n.deferred.execute(function(){n._updateModel()})});this.on("reset",function(){for(var r in this.element_list){this.element_list[r].reset()}})},_renderForm:function(w){var v=this;this.message=new m.Message();var s=new m.ButtonIcon({icon:"fa-info-circle",title:"Requirements",tooltip:"Display tool requirements",onclick:function(){if(!this.visible){this.visible=true;v.message.update({persistent:true,message:d.requirements(w),status:"info"})}else{this.visible=false;v.message.update({message:""})}}});if(!w.requirements||w.requirements.length==0){s.$el.hide()}var o=new m.ButtonMenu({icon:"fa-cubes",title:"Versions",tooltip:"Select another tool version"});if(w.versions&&w.versions.length>1){for(var q in w.versions){var t=w.versions[q];if(t!=w.version){o.addMenu({title:"Switch to "+t,version:t,icon:"fa-cube",onclick:function(){w.id=w.id.replace(w.version,this.version);w.version=this.version;v.deferred.reset();v.deferred.execute(function(){v._buildModel()})}})}}}else{o.$el.hide()}var r=new m.ButtonMenu({icon:"fa-caret-down",title:"Options",tooltip:"View available options"});if(w.biostar_url){r.addMenu({icon:"fa-question-circle",title:"Question?",tooltip:"Ask a question about this tool (Biostar)",onclick:function(){window.open(w.biostar_url+"/p/new/post/")}});r.addMenu({icon:"fa-search",title:"Search",tooltip:"Search help for this tool (Biostar)",onclick:function(){window.open(w.biostar_url+"/t/"+w.id+"/")}})}r.addMenu({icon:"fa-share",title:"Share",tooltip:"Share this tool",onclick:function(){prompt("Copy to clipboard: Ctrl+C, Enter",window.location.origin+galaxy_config.root+"root?tool_id="+w.id)}});if(this.is_admin){r.addMenu({icon:"fa-download",title:"Download",tooltip:"Download this tool",onclick:function(){window.location.href=galaxy_config.root+"api/tools/"+w.id+"/download"}})}this.section=new l.View(v,{inputs:w.inputs,cls:"ui-table-plain"});if(this.incompatible){this.$el.hide();$("#tool-form-classic").show();return}this.portlet=new h.View({icon:"fa-wrench",title:""+w.name+" "+w.description+" (Galaxy Tool Version "+w.version+")",cls:"ui-portlet-slim",operations:{requirements:s,menu:r,versions:o},buttons:{execute:new m.Button({icon:"fa-check",tooltip:"Execute: "+w.name,title:"Execute",cls:"btn btn-primary",floating:"clear",onclick:function(){v.job_handler.submit()}})}});this.portlet.append(this.message.$el,true);this.portlet.append(this.section.$el);this.$el.empty();this.$el.append(this.portlet.$el);if(w.help!=""){this.$el.append(d.help(w.help))}if(w.citations){var u=$("
");var n=new k.ToolCitationCollection();n.tool_id=w.id;var p=new a.CitationListView({el:u,collection:n});p.render();n.fetch();this.$el.append(u)}if(w.message){this.message.update({persistent:true,status:"warning",message:w.message})}}});return{View:b}}); \ No newline at end of file +define(["utils/utils","mvc/ui/ui-misc","mvc/tools/tools-form-base","mvc/tools/tools-jobs"],function(c,e,b,a){var d=b.extend({initialize:function(g){var f=this;this.job_handler=new a(this);this.buttons={execute:new e.Button({icon:"fa-check",tooltip:"Execute: "+g.name,title:"Execute",cls:"btn btn-primary",floating:"clear",onclick:function(){f.job_handler.submit()}})};b.prototype.initialize.call(this,g)},_buildModel:function(){var f=this;var g=galaxy_config.root+"api/tools/"+this.options.id+"/build?";if(this.options.job_id){g+="job_id="+this.options.job_id}else{if(this.options.dataset_id){g+="dataset_id="+this.options.dataset_id}else{g+="tool_version="+this.options.version+"&";var i=top.location.href;var j=i.indexOf("?");if(i.indexOf("tool_id=")!=-1&&j!==-1){g+=i.slice(j+1)}}}var h=this.deferred.register();c.request({type:"GET",url:g,success:function(k){f.options=k;f._buildForm();f.message.update({status:"success",message:"Now you are using '"+f.options.name+"' version "+f.options.version+".",persistent:false});f.deferred.done(h);console.debug("tools-form::initialize() - Initial tool model ready.");console.debug(k)},error:function(k){f.deferred.done(h);console.debug("tools-form::initialize() - Initial tool model request failed.");console.debug(k);var l=k.error||"Uncaught error.";f.modal.show({title:"Tool cannot be executed",body:l,buttons:{Close:function(){f.modal.hide()}}})}})},_updateModel:function(){var f=this;var g=this.tree.finalize({data:function(k){if(k.values.length>0&&k.values[0]&&k.values[0].src==="hda"){return f.content.get({id:k.values[0].id,src:"hda"}).id_uncoded}return null}});console.debug("tools-form::_refreshForm() - Refreshing states.");console.debug(g);function j(n){for(var l in f.input_list){var m=f.field_list[l];var k=f.input_list[l];if(k.is_dynamic&&m.wait&&m.unwait){if(n){m.wait()}else{m.unwait()}}}}j(true);var i=this.deferred.register();var h=galaxy_config.root+"api/tools/"+this.options.id+"/build?tool_version="+this.options.version;c.request({type:"GET",url:h,data:g,success:function(k){f.tree.matchModel(k,function(m,q){var l=f.input_list[m];if(l&&l.options){if(!_.isEqual(l.options,q.options)){l.options=q.options;var r=f.field_list[m];if(r.update){var p=[];if((["data","data_collection","drill_down"]).indexOf(l.type)!=-1){p=l.options}else{for(var o in q.options){var n=q.options[o];if(n.length>2){p.push({label:n[0],value:n[1]})}}}r.update(p);r.trigger("change");console.debug("Updating options for "+m)}}}});j(false);f.deferred.done(i);console.debug("tools-form::_refreshForm() - States refreshed.");console.debug(k)},error:function(k){f.deferred.done(i);console.debug("tools-form::_refreshForm() - Refresh request failed.");console.debug(k)}})}});return{View:d}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-jobs.js b/static/scripts/packed/mvc/tools/tools-jobs.js index 514aee410f5..62ee1607d73 100644 --- a/static/scripts/packed/mvc/tools/tools-jobs.js +++ b/static/scripts/packed/mvc/tools/tools-jobs.js @@ -1 +1 @@ -define(["utils/utils","mvc/tools/tools-template"],function(b,a){return Backbone.Model.extend({initialize:function(c){this.app=c},submit:function(){var c=this;var d={tool_id:this.app.options.id,tool_version:this.app.options.version,inputs:this.app.tree.finalize()};this.app.trigger("reset");if(!this._validation(d)){console.debug("tools-jobs::submit - Submission canceled. Validation failed.");return}console.debug(d);this.app.modal.show({title:"Please wait...",body:"progress",closing_events:true,buttons:{Close:function(){c.app.modal.hide()}}});b.request({type:"POST",url:galaxy_config.root+"api/tools",data:d,success:function(e){c.app.modal.hide();c.app.reciept(a.success(e));c._refreshHdas()},error:function(e,g){c.app.modal.hide();if(e&&e.message&&e.message.data){var h=c.app.tree.matchResponse(e.message.data);for(var f in h){c.app.highlight(f,h[f]);break}}else{console.debug(e);c.app.modal.show({title:"Job submission failed",body:a.error(d),buttons:{Close:function(){c.app.modal.hide()}}})}}})},_validation:function(h){var d=h.inputs;var m=-1;var i=null;for(var k in d){var f=d[k];var l=this.app.tree.match(k);var e=this.app.field_list[l];var j=this.app.input_list[l];if(!l||!j||!e){console.debug("tools-jobs::_validation - Retrieving input objects failed.");continue}if(!j.optional&&e.validate&&!e.validate()){this.app.highlight(l);return false}if(f&&f.batch){var g=f.values.length;var c=null;if(g>0){c=f.values[0]&&f.values[0].src}if(c){if(i===null){i=c}else{if(i!==c){this.app.highlight(l,"Please select either dataset or dataset list fields for all batch mode fields.");return false}}}if(m===-1){m=g}else{if(m!==g){this.app.highlight(l,"Please make sure that you select the same number of inputs for all batch mode fields. This field contains "+g+" selection(s) while a previous field contains "+m+".");return false}}}}return true},_refreshHdas:function(){if(parent.Galaxy&&parent.Galaxy.currHistoryPanel){parent.Galaxy.currHistoryPanel.refreshContents()}}})}); \ No newline at end of file +define(["utils/utils","mvc/tools/tools-template"],function(b,a){return Backbone.Model.extend({initialize:function(c){this.app=c},submit:function(){var c=this;var d={tool_id:this.app.options.id,tool_version:this.app.options.version,inputs:this.app.tree.finalize()};this.app.trigger("reset");if(!this._validation(d)){console.debug("tools-jobs::submit - Submission canceled. Validation failed.");return}console.debug(d);this.app.modal.show({title:"Please wait...",body:"progress",closing_events:true,buttons:{Close:function(){c.app.modal.hide()}}});b.request({type:"POST",url:galaxy_config.root+"api/tools",data:d,success:function(e){c.app.modal.hide();c.app.reciept(a.success(e));c._refreshHdas()},error:function(e,g){c.app.modal.hide();if(e&&e.message&&e.message.data){var h=c.app.tree.matchResponse(e.message.data);for(var f in h){c.app.highlight(f,h[f]);break}}else{console.debug(e);c.app.modal.show({title:"Job submission failed",body:a.error(d),buttons:{Close:function(){c.app.modal.hide()}}})}}})},_validation:function(h){var d=h.inputs;var m=-1;var i=null;for(var k in d){var f=d[k];var l=this.app.tree.match(k);var e=this.app.field_list[l];var j=this.app.input_list[l];if(!l||!j||!e){console.debug("tools-jobs::_validation - Retrieving input objects failed.");continue}if(!j.optional&&f==null){this.app.highlight(l);return false}if(f&&f.batch){var g=f.values.length;var c=null;if(g>0){c=f.values[0]&&f.values[0].src}if(c){if(i===null){i=c}else{if(i!==c){this.app.highlight(l,"Please select either dataset or dataset list fields for all batch mode fields.");return false}}}if(m===-1){m=g}else{if(m!==g){this.app.highlight(l,"Please make sure that you select the same number of inputs for all batch mode fields. This field contains "+g+" selection(s) while a previous field contains "+m+".");return false}}}}return true},_refreshHdas:function(){if(parent.Galaxy&&parent.Galaxy.currHistoryPanel){parent.Galaxy.currHistoryPanel.refreshContents()}}})}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-section.js b/static/scripts/packed/mvc/tools/tools-section.js index e168486d9ff..a9ead10010d 100644 --- a/static/scripts/packed/mvc/tools/tools-section.js +++ b/static/scripts/packed/mvc/tools/tools-section.js @@ -1 +1 @@ -define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/tools/tools-repeat","mvc/tools/tools-select-content","mvc/tools/tools-input"],function(d,b,g,c,a,e){var f=Backbone.View.extend({initialize:function(i,h){this.app=i;this.inputs=h.inputs;h.cls_tr="section-row";this.table=new b.View(h);this.setElement(this.table.$el);this.render()},render:function(){this.table.delAll();for(var h in this.inputs){this._add(this.inputs[h])}},_add:function(j){var i=this;var h=jQuery.extend(true,{},j);h.id=j.id=d.uuid();this.app.input_list[h.id]=h;var k=h.type;switch(k){case"conditional":this._addConditional(h);break;case"repeat":this._addRepeat(h);break;default:this._addRow(h)}},_addConditional:function(h){var j=this;h.test_param.id=h.id;var m=this._addRow(h.test_param);m.options.onchange=function(t){var p=j.app.tree.matchCase(h,t);for(var r in h.cases){var w=h.cases[r];var u=h.id+"-section-"+r;var o=j.table.get(u);var v=false;for(var q in w.inputs){var s=w.inputs[q].type;if(s&&s!=="hidden"){v=true;break}}if(r==p&&v){o.fadeIn("fast")}else{o.hide()}}j.app.trigger("refresh")};for(var l in h.cases){var k=h.id+"-section-"+l;var n=new f(this.app,{inputs:h.cases[l].inputs,cls:"ui-table-plain"});n.$el.addClass("ui-table-form-section");this.table.add(n.$el);this.table.append(k)}m.trigger("change")},_addRepeat:function(o){var r=this;var p=0;function m(i,t){var s=o.id+"-section-"+(p++);var u=null;if(t){u=function(){k.del(s);k.retitle(o.title);r.app.trigger("refresh")}}var v=new f(r.app,{inputs:i,cls:"ui-table-plain"});k.add({id:s,title:o.title,$el:v.$el,ondel:u});k.retitle(o.title)}var k=new c.View({title_new:o.title,max:o.max,onnew:function(){m(o.inputs,true);r.app.trigger("refresh")}});var h=o.min;var q=_.size(o.cache);for(var l=0;l=h)}var j=new e(this.app,{label:o.title,help:o.help,field:k});j.$el.addClass("ui-table-form-section");this.table.add(j.$el);this.table.append(o.id)},_addRow:function(h){var k=h.id;var i=this._createField(h);this.app.field_list[k]=i;var j=new e(this.app,{label:h.label,optional:h.optional,help:h.help,field:i});this.app.element_list[k]=j;this.table.add(j.$el);this.table.append(k);return i},_createField:function(h){var i=null;switch(h.type){case"text":i=this._fieldText(h);break;case"select":i=this._fieldSelect(h);break;case"data":i=this._fieldData(h);break;case"data_collection":i=this._fieldData(h);break;case"data_column":h.error_text="Missing columns in referenced dataset.";i=this._fieldSelect(h);break;case"hidden":i=this._fieldHidden(h);break;case"hidden_data":i=this._fieldHidden(h);break;case"integer":i=this._fieldSlider(h);break;case"float":i=this._fieldSlider(h);break;case"boolean":i=this._fieldBoolean(h);break;case"genomebuild":h.searchable=true;i=this._fieldSelect(h);break;case"drill_down":i=this._fieldDrilldown(h);break;case"baseurl":i=this._fieldHidden(h);break;default:this.app.incompatible=true;if(h.options){i=this._fieldSelect(h)}else{i=this._fieldText(h)}console.debug("tools-form::_addRow() : Auto matched field type ("+h.type+").")}if(h.value!==undefined){i.value(h.value)}return i},_fieldData:function(h){var i=this;return new a.View(this.app,{id:"field-"+h.id,extensions:h.extensions,multiple:h.multiple,type:h.type,data:h.options,onchange:function(){i.app.trigger("refresh")}})},_fieldSelect:function(h){var k=[];for(var l in h.options){var m=h.options[l];k.push({label:m[0],value:m[1]})}var n=g.Select;switch(h.display){case"checkboxes":n=g.Checkbox;break;case"radio":n=g.Radio;break}var j=this;return new n.View({id:"field-"+h.id,data:k,error_text:h.error_text||"No options available",multiple:h.multiple,searchable:h.searchable,onchange:function(){j.app.trigger("refresh")}})},_fieldDrilldown:function(h){var i=this;return new g.Drilldown.View({id:"field-"+h.id,data:h.options,display:h.display,onchange:function(){i.app.trigger("refresh")}})},_fieldText:function(h){var i=this;return new g.Input({id:"field-"+h.id,area:h.area,onchange:function(){i.app.trigger("refresh")}})},_fieldSlider:function(h){return new g.Slider.View({id:"field-"+h.id,precise:h.type=="float",min:h.min,max:h.max})},_fieldHidden:function(h){return new g.Hidden({id:"field-"+h.id})},_fieldBoolean:function(h){return new g.RadioButton.View({id:"field-"+h.id,data:[{label:"Yes",value:"true"},{label:"No",value:"false"}]})}});return{View:f}}); \ No newline at end of file +define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/tools/tools-repeat","mvc/tools/tools-select-content","mvc/tools/tools-input"],function(d,b,g,c,a,e){var f=Backbone.View.extend({initialize:function(i,h){this.app=i;this.inputs=h.inputs;h.cls_tr="section-row";this.table=new b.View(h);this.setElement(this.table.$el);this.render()},render:function(){this.table.delAll();for(var h in this.inputs){this._add(this.inputs[h])}},_add:function(j){var i=this;var h=jQuery.extend(true,{},j);h.id=j.id=d.uuid();this.app.input_list[h.id]=h;var k=h.type;switch(k){case"conditional":this._addConditional(h);break;case"repeat":this._addRepeat(h);break;default:this._addRow(h)}},_addConditional:function(h){var j=this;h.test_param.id=h.id;var m=this._addRow(h.test_param);m.options.onchange=function(t){var p=j.app.tree.matchCase(h,t);for(var r in h.cases){var w=h.cases[r];var u=h.id+"-section-"+r;var o=j.table.get(u);var v=false;for(var q in w.inputs){var s=w.inputs[q].type;if(s&&s!=="hidden"){v=true;break}}if(r==p&&v){o.fadeIn("fast")}else{o.hide()}}j.app.trigger("refresh")};for(var l in h.cases){var k=h.id+"-section-"+l;var n=new f(this.app,{inputs:h.cases[l].inputs,cls:"ui-table-plain"});n.$el.addClass("ui-table-form-section");this.table.add(n.$el);this.table.append(k)}m.trigger("change")},_addRepeat:function(o){var r=this;var p=0;function m(i,t){var s=o.id+"-section-"+(p++);var u=null;if(t){u=function(){k.del(s);k.retitle(o.title);r.app.trigger("refresh")}}var v=new f(r.app,{inputs:i,cls:"ui-table-plain"});k.add({id:s,title:o.title,$el:v.$el,ondel:u});k.retitle(o.title)}var k=new c.View({title_new:o.title,max:o.max,onnew:function(){m(o.inputs,true);r.app.trigger("refresh")}});var h=o.min;var q=_.size(o.cache);for(var l=0;l=h)}var j=new e(this.app,{label:o.title,help:o.help,field:k});j.$el.addClass("ui-table-form-section");this.table.add(j.$el);this.table.append(o.id)},_addRow:function(h){var k=h.id;var i=this._createField(h);this.app.field_list[k]=i;var j=new e(this.app,{label:h.label,optional:h.optional,help:h.help,field:i});this.app.element_list[k]=j;this.table.add(j.$el);this.table.append(k);return i},_createField:function(h){var i=null;switch(h.type){case"text":i=this._fieldText(h);break;case"select":i=this._fieldSelect(h);break;case"data":i=this._fieldData(h);break;case"data_collection":i=this._fieldData(h);break;case"data_column":h.error_text="Missing columns in referenced dataset.";i=this._fieldSelect(h);break;case"hidden":i=this._fieldHidden(h);break;case"hidden_data":i=this._fieldHidden(h);break;case"integer":i=this._fieldSlider(h);break;case"float":i=this._fieldSlider(h);break;case"boolean":i=this._fieldBoolean(h);break;case"genomebuild":h.searchable=true;i=this._fieldSelect(h);break;case"drill_down":i=this._fieldDrilldown(h);break;case"baseurl":i=this._fieldHidden(h);break;default:this.app.incompatible=true;if(h.options){i=this._fieldSelect(h)}else{i=this._fieldText(h)}console.debug("tools-form::_addRow() : Auto matched field type ("+h.type+").")}if(h.value!==undefined){i.value(h.value)}return i},_fieldData:function(h){if(this.app.workflow){var j=d.textify(h.extensions.toString());h.info="Data input '"+h.name+"' ("+j+")";return this._fieldHidden(h)}var i=this;return new a.View(this.app,{id:"field-"+h.id,extensions:h.extensions,multiple:h.multiple,type:h.type,data:h.options,onchange:function(){i.app.trigger("refresh")}})},_fieldSelect:function(h){if(this.app.workflow&&h.is_dynamic){if(!d.validate(h.value)){h.value=""}return this._fieldText(h)}var k=[];for(var l in h.options){var m=h.options[l];k.push({label:m[0],value:m[1]})}var n=g.Select;switch(h.display){case"checkboxes":n=g.Checkbox;break;case"radio":n=g.Radio;break}var j=this;return new n.View({id:"field-"+h.id,data:k,error_text:h.error_text||"No options available",multiple:h.multiple,searchable:h.searchable,onchange:function(){j.app.trigger("refresh")}})},_fieldDrilldown:function(h){var i=this;return new g.Drilldown.View({id:"field-"+h.id,data:h.options,display:h.display,onchange:function(){i.app.trigger("refresh")}})},_fieldText:function(h){var i=this;return new g.Input({id:"field-"+h.id,area:h.area,onchange:function(){i.app.trigger("refresh")}})},_fieldSlider:function(h){return new g.Slider.View({id:"field-"+h.id,precise:h.type=="float",min:h.min,max:h.max})},_fieldHidden:function(h){return new g.Hidden({id:"field-"+h.id,info:h.info})},_fieldBoolean:function(h){return new g.RadioButton.View({id:"field-"+h.id,data:[{label:"Yes",value:"true"},{label:"No",value:"false"}]})}});return{View:f}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-select-content.js b/static/scripts/packed/mvc/tools/tools-select-content.js index dbe99f86fa9..37a7a79faea 100644 --- a/static/scripts/packed/mvc/tools/tools-select-content.js +++ b/static/scripts/packed/mvc/tools/tools-select-content.js @@ -1 +1 @@ -define(["utils/utils","mvc/ui/ui-misc","mvc/ui/ui-tabs","mvc/tools/tools-template"],function(c,e,b,a){var d=Backbone.View.extend({initialize:function(f,o){this.app=f;this.options=o;var n=this;this.setElement("
");this.list={};var m=[];if(o.type=="data_collection"){this.mode="collection"}else{if(o.multiple){this.mode="multiple"}else{this.mode="single"}}this.current=this.mode;this.list={};var j=o.extensions.toString();if(j){j=j.replace(/,/g,", ");var k=j.lastIndexOf(", ");if(k!=-1){j=j.substr(0,k)+" or "+j.substr(k+1)}}var h="No dataset available.";if(j){h="No "+j+" dataset available."}var l="No dataset list available.";if(j){l="No "+j+" dataset collection available."}if(this.mode=="single"){m.push({icon:"fa-file-o",label:"Single dataset",value:"single"});this.select_single=new e.Select.View({error_text:h,onchange:function(){n.trigger("change")}});this.list.single={field:this.select_single,type:"hda"}}if(this.mode=="single"||this.mode=="multiple"){m.push({icon:"fa-files-o",label:"Multiple datasets",value:"multiple"});this.select_multiple=new e.Select.View({multiple:true,error_text:h,onchange:function(){n.trigger("change")}});this.list.multiple={field:this.select_multiple,type:"hda"}}if(this.mode=="single"||this.mode=="collection"){m.push({icon:"fa-folder-o",label:"Dataset collection",value:"collection"});this.select_collection=new e.Select.View({error_text:l,onchange:function(){n.trigger("change")}});this.list.collection={field:this.select_collection,type:"hdca"}}this.button_type=new e.RadioButton.View({value:this.current,data:m,onchange:function(i){n.current=i;n.refresh();n.trigger("change")}});this.$batch=$(a.batchMode());if(_.size(this.list)>1){this.$el.append(c.wrap(this.button_type.$el))}for(var g in this.list){this.$el.append(this.list[g].field.$el)}this.$el.append(this.$batch);this.update(o.data);if(this.options.value!==undefined){this.value(this.options.value)}this.refresh();this.on("change",function(){if(o.onchange){o.onchange(n.value())}})},wait:function(){for(var f in this.list){this.list[f].field.wait()}},unwait:function(){for(var f in this.list){this.list[f].field.unwait()}},update:function(g){var l=[];for(var j in g.hda){var k=g.hda[j];l.push({label:k.hid+": "+k.name,value:k.id})}var f=[];for(var j in g.hdca){var h=g.hdca[j];f.push({label:h.hid+": "+h.name,value:h.id})}this.select_single&&this.select_single.update(l);this.select_multiple&&this.select_multiple.update(l);this.select_collection&&this.select_collection.update(f);this.app.content.add(g)},value:function(m){if(m&&m.values){try{var l=[];for(var h in m.values){l.push(m.values[h].id)}if(m&&m.values.length>0&&m.values[0].src=="hcda"){this.current="collection";this.select_collection.value(l[0])}else{if(this.mode=="multiple"){this.current="multiple";this.select_multiple.value(l)}else{this.current="single";this.select_single.value(l[0])}}this.refresh();var g=this._select();if(!g.validate()){g.value(g.first())}}catch(k){console.debug("tools-select-content::value() - Skipped.")}}var j=this._select().value();if(!(j instanceof Array)){j=[j]}var f={batch:this.mode=="single"&&this.current!="single",values:[]};for(var h in j){f.values.push({id:j[h],src:this.list[this.current].type})}return f},validate:function(){return this._select().validate()},refresh:function(){for(var g in this.list){var f=this.list[g].field.$el;if(this.current==g){f.show()}else{f.hide()}}if(this.mode=="single"&&this.current!="single"){this.$batch.show()}else{this.$batch.hide()}},_select:function(){return this.list[this.current].field}});return{View:d}}); \ No newline at end of file +define(["utils/utils","mvc/ui/ui-misc","mvc/ui/ui-tabs","mvc/tools/tools-template"],function(c,e,b,a){var d=Backbone.View.extend({initialize:function(l,g){this.app=l;this.options=g;var f=this;this.setElement("
");this.list={};var j=[];if(g.type=="data_collection"){this.mode="collection"}else{if(g.multiple){this.mode="multiple"}else{this.mode="single"}}this.current=this.mode;this.list={};var k=c.textify(g.extensions);var n="No dataset available.";if(k){n="No "+k+" dataset available."}var m="No dataset list available.";if(k){m="No "+k+" dataset collection available."}if(this.mode=="single"){j.push({icon:"fa-file-o",label:"Single dataset",value:"single"});this.select_single=new e.Select.View({error_text:n,onchange:function(){f.trigger("change")}});this.list.single={field:this.select_single,type:"hda"}}if(this.mode=="single"||this.mode=="multiple"){j.push({icon:"fa-files-o",label:"Multiple datasets",value:"multiple"});this.select_multiple=new e.Select.View({multiple:true,error_text:n,onchange:function(){f.trigger("change")}});this.list.multiple={field:this.select_multiple,type:"hda"}}if(this.mode=="single"||this.mode=="collection"){j.push({icon:"fa-folder-o",label:"Dataset collection",value:"collection"});this.select_collection=new e.Select.View({error_text:m,onchange:function(){f.trigger("change")}});this.list.collection={field:this.select_collection,type:"hdca"}}this.button_type=new e.RadioButton.View({value:this.current,data:j,onchange:function(i){f.current=i;f.refresh();f.trigger("change")}});this.$batch=$(a.batchMode());if(_.size(this.list)>1){this.$el.append(c.wrap(this.button_type.$el))}for(var h in this.list){this.$el.append(this.list[h].field.$el)}this.$el.append(this.$batch);this.update(g.data);if(this.options.value!==undefined){this.value(this.options.value)}this.refresh();this.on("change",function(){if(g.onchange){g.onchange(f.value())}})},wait:function(){for(var f in this.list){this.list[f].field.wait()}},unwait:function(){for(var f in this.list){this.list[f].field.unwait()}},update:function(g){var l=[];for(var j in g.hda){var k=g.hda[j];l.push({label:k.hid+": "+k.name,value:k.id})}var f=[];for(var j in g.hdca){var h=g.hdca[j];f.push({label:h.hid+": "+h.name,value:h.id})}this.select_single&&this.select_single.update(l);this.select_multiple&&this.select_multiple.update(l);this.select_collection&&this.select_collection.update(f);this.app.content.add(g)},value:function(m){if(m&&m.values){try{var l=[];for(var h in m.values){l.push(m.values[h].id)}if(m&&m.values.length>0&&m.values[0].src=="hcda"){this.current="collection";this.select_collection.value(l[0])}else{if(this.mode=="multiple"){this.current="multiple";this.select_multiple.value(l)}else{this.current="single";this.select_single.value(l[0])}}this.refresh();var g=this._select();if(!g.validate()){g.value(g.first())}}catch(k){console.debug("tools-select-content::value() - Skipped.")}}var j=this._select().value();if(!(j instanceof Array)){j=[j]}var f={batch:this.mode=="single"&&this.current!="single",values:[]};for(var h in j){f.values.push({id:j[h],src:this.list[this.current].type})}return f},validate:function(){return this._select().validate()},refresh:function(){for(var g in this.list){var f=this.list[g].field.$el;if(this.current==g){f.show()}else{f.hide()}}if(this.mode=="single"&&this.current!="single"){this.$batch.show()}else{this.$batch.hide()}},_select:function(){return this.list[this.current].field}});return{View:d}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-tree.js b/static/scripts/packed/mvc/tools/tools-tree.js index ba3f5091ac1..299d40365de 100644 --- a/static/scripts/packed/mvc/tools/tools-tree.js +++ b/static/scripts/packed/mvc/tools/tools-tree.js @@ -1 +1 @@ -define([],function(){return Backbone.Model.extend({initialize:function(a){this.app=a},finalize:function(f){var a=this;this.map_dict={};if(!this.app.section){return{}}f=f||{};var e={};var d={};this._iterate(this.app.section.$el,d);function c(i,h,g){e[i]=g;a.map_dict[i]=h}function b(n,q){for(var l in q){var j=q[l];if(j.input){var s=j.input;var m=n;if(n!=""){m+="|"}m+=s.name;switch(s.type){case"repeat":var h="section-";var v=[];var p=null;for(var u in j){var o=u.indexOf(h);if(o!=-1){o+=h.length;v.push(parseInt(u.substr(o)));if(!p){p=u.substr(0,o)}}}v.sort(function(w,i){return w-i});var l=0;for(var k in v){b(m+"_"+l++,j[p+v[k]])}break;case"conditional":var t=a.app.field_list[s.id].value();if(f[s.test_param.type]){t=f[s.test_param.type](t)}c(m+"|"+s.test_param.name,s.id,t);var g=a.matchCase(s,t);if(g!=-1){b(m,q[s.id+"-section-"+g])}break;default:var r=a.app.field_list[s.id];var t=r.value();if(f[s.type]){t=f[s.type](t)}if(!r.skip){if(s.optional&&r.validate&&!r.validate()){t=null}c(m,s.id,t)}}}}}b("",d);return e},match:function(a){return this.map_dict&&this.map_dict[a]},matchCase:function(a,c){if(a.test_param.type=="boolean"){if(c=="true"){c=a.test_param.truevalue||"true"}else{c=a.test_param.falsevalue||"false"}}for(var b in a.cases){if(a.cases[b].value==c){return b}}return -1},matchModel:function(c,e){var a={};var b=this;function d(f,o){for(var l in o){var h=o[l];var m=h.name;if(f!=""){m=f+"|"+m}switch(h.type){case"repeat":for(var k in h.cache){d(m+"_"+k,h.cache[k])}break;case"conditional":var p=h.test_param&&h.test_param.value;var g=b.matchCase(h,p);if(g!=-1){d(m,h.cases[g].inputs)}break;default:var n=b.map_dict[m];if(n){e(n,h)}}}}d("",c.inputs);return a},matchResponse:function(c){var a={};var b=this;function d(k,h){if(typeof h==="string"){var f=b.map_dict[k];if(f){a[f]=h}}else{for(var g in h){var e=g;if(k!==""){var j="|";if(h instanceof Array){j="_"}e=k+j+e}d(e,h[g])}}}d("",c);return a},_iterate:function(c,d){var a=this;var b=$(c).children();b.each(function(){var g=this;var f=$(g).attr("id");if($(g).hasClass("section-row")){d[f]={};var e=a.app.input_list[f];if(e){d[f]={input:e}}a._iterate(g,d[f])}else{a._iterate(g,d)}})}})}); \ No newline at end of file +define(["utils/utils"],function(a){return Backbone.Model.extend({initialize:function(b){this.app=b},finalize:function(g){var b=this;this.map_dict={};if(!this.app.section){return{}}g=g||{};var f={};var e={};this._iterate(this.app.section.$el,e);function d(j,i,h){f[j]=h;b.map_dict[j]=i}function c(o,r){for(var m in r){var k=r[m];if(k.input){var t=k.input;var n=o;if(o!=""){n+="|"}n+=t.name;switch(t.type){case"repeat":var j="section-";var w=[];var q=null;for(var v in k){var p=v.indexOf(j);if(p!=-1){p+=j.length;w.push(parseInt(v.substr(p)));if(!q){q=v.substr(0,p)}}}w.sort(function(x,i){return x-i});var m=0;for(var l in w){c(n+"_"+m++,k[q+w[l]])}break;case"conditional":var u=b.app.field_list[t.id].value();if(g[t.test_param.type]){u=g[t.test_param.type](u)}d(n+"|"+t.test_param.name,t.id,u);var h=b.matchCase(t,u);if(h!=-1){c(n,r[t.id+"-section-"+h])}break;default:var s=b.app.field_list[t.id];var u=s.value();if(g[t.type]){u=g[t.type](u)}if(!s.skip){if(s.validate&&!s.validate(u)){u=null}d(n,t.id,u)}}}}}c("",e);return f},match:function(b){return this.map_dict&&this.map_dict[b]},matchCase:function(b,d){if(b.test_param.type=="boolean"){if(d=="true"){d=b.test_param.truevalue||"true"}else{d=b.test_param.falsevalue||"false"}}for(var c in b.cases){if(b.cases[c].value==d){return c}}return -1},matchModel:function(d,f){var b={};var c=this;function e(g,p){for(var m in p){var k=p[m];var n=k.name;if(g!=""){n=g+"|"+n}switch(k.type){case"repeat":for(var l in k.cache){e(n+"_"+l,k.cache[l])}break;case"conditional":var q=k.test_param&&k.test_param.value;var h=c.matchCase(k,q);if(h!=-1){e(n,k.cases[h].inputs)}break;default:var o=c.map_dict[n];if(o){f(o,k)}}}}e("",d.inputs);return b},matchResponse:function(d){var b={};var c=this;function e(l,j){if(typeof j==="string"){var g=c.map_dict[l];if(g){b[g]=j}}else{for(var h in j){var f=h;if(l!==""){var k="|";if(j instanceof Array){k="_"}f=l+k+f}e(f,j[h])}}}e("",d);return b},_iterate:function(d,e){var b=this;var c=$(d).children();c.each(function(){var h=this;var g=$(h).attr("id");if($(h).hasClass("section-row")){e[g]={};var f=b.app.input_list[g];if(f){e[g]={input:f}}b._iterate(h,e[g])}else{b._iterate(h,e)}})}})}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/ui/ui-misc.js b/static/scripts/packed/mvc/ui/ui-misc.js index e8022656191..c0ad97f0a5e 100644 --- a/static/scripts/packed/mvc/ui/ui-misc.js +++ b/static/scripts/packed/mvc/ui/ui-misc.js @@ -1 +1 @@ -define(["utils/utils","mvc/ui/ui-select-default","mvc/ui/ui-slider","mvc/ui/ui-options","mvc/ui/ui-drilldown","mvc/ui/ui-button-menu","mvc/ui/ui-modal"],function(k,b,e,o,l,q,m){var d=Backbone.View.extend({optionsDefault:{url:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options))},_template:function(r){return''}});var j=Backbone.View.extend({optionsDefault:{title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options))},title:function(r){this.$el.html(r)},_template:function(r){return'"},value:function(){return options.title}});var c=Backbone.View.extend({optionsDefault:{floating:"right",icon:"",tooltip:"",placement:"bottom",title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).tooltip({title:r.tooltip,placement:"bottom"})},_template:function(r){return'
 '+r.title+"
"}});var g=Backbone.View.extend({optionsDefault:{id:null,title:"",floating:"right",cls:"ui-button btn btn-default",icon:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).on("click",function(){$(".tooltip").hide();if(r.onclick){r.onclick()}});$(this.el).tooltip({title:r.tooltip,placement:"bottom"})},_template:function(r){var s='";return s}});var h=Backbone.View.extend({optionsDefault:{id:null,title:"",floating:"right",cls:"ui-button-icon",icon:"",tooltip:"",onclick:null},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));this.$button=this.$el.find(".button");var r=this;$(this.el).on("click",function(){$(".tooltip").hide();if(s.onclick&&!r.disabled){s.onclick()}});$(this.el).tooltip({title:s.tooltip,placement:"bottom"})},disable:function(){this.$button.addClass("disabled");this.disabled=true},enable:function(){this.$button.removeClass("disabled");this.disabled=false},_template:function(r){var s="";if(r.title){s="width: auto;"}var t='
';if(r.title){t+='
 '+r.title+"
"}else{t+=''}t+="
";return t}});var f=Backbone.View.extend({optionsDefault:{title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).on("click",r.onclick)},_template:function(r){return'"}});var n=Backbone.View.extend({optionsDefault:{message:null,status:"info",persistent:false},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement("
");if(this.options.message){this.update(this.options)}},update:function(s){this.options=k.merge(s,this.optionsDefault);if(s.message!=""){this.$el.html(this._template(this.options));this.$el.find(".alert").append(s.message);this.$el.fadeIn();if(this.timeout){window.clearTimeout(this.timeout)}if(!s.persistent){var r=this;this.timeout=window.setTimeout(function(){if(r.$el.is(":visible")){r.$el.fadeOut()}else{r.$el.hide()}},3000)}}else{this.$el.fadeOut()}},_template:function(r){return'
'}});var a=Backbone.View.extend({optionsDefault:{onclick:null,searchword:""},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));var r=this;if(this.options.onclick){this.$el.on("submit",function(u){var t=r.$el.find("#search");r.options.onclick(t.val())})}},_template:function(r){return''}});var i=Backbone.View.extend({optionsDefault:{type:"text",placeholder:"",disabled:false,visible:true,cls:"",area:false},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));if(this.options.value!==undefined){this.value(this.options.value)}if(this.options.disabled){this.$el.prop("disabled",true)}if(!this.options.visible){this.$el.hide()}var r=this;this.$el.on("input",function(){if(r.options.onchange){r.options.onchange(r.$el.val())}})},value:function(r){if(r!==undefined){this.$el.val(r)}return this.$el.val()},_template:function(r){if(r.area){return''}else{return''}}});var p=Backbone.View.extend({initialize:function(r){this.options=r;this.setElement(this._template(this.options));if(this.options.value!==undefined){this.value(this.options.value)}},value:function(r){if(r!==undefined){this.$el.val(r)}return this.$el.val()},_template:function(r){return''}});return{Anchor:f,Button:g,ButtonIcon:h,ButtonMenu:q,Icon:c,Image:d,Input:i,Label:j,Message:n,Modal:m,RadioButton:o.RadioButton,Checkbox:o.Checkbox,Radio:o.Radio,Searchbox:a,Select:b,Hidden:p,Slider:e,Drilldown:l}}); \ No newline at end of file +define(["utils/utils","mvc/ui/ui-select-default","mvc/ui/ui-slider","mvc/ui/ui-options","mvc/ui/ui-drilldown","mvc/ui/ui-button-menu","mvc/ui/ui-modal"],function(k,b,e,o,l,q,m){var d=Backbone.View.extend({optionsDefault:{url:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options))},_template:function(r){return''}});var j=Backbone.View.extend({optionsDefault:{title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options))},title:function(r){this.$el.html(r)},_template:function(r){return'"},value:function(){return options.title}});var c=Backbone.View.extend({optionsDefault:{floating:"right",icon:"",tooltip:"",placement:"bottom",title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).tooltip({title:r.tooltip,placement:"bottom"})},_template:function(r){return'
 '+r.title+"
"}});var g=Backbone.View.extend({optionsDefault:{id:null,title:"",floating:"right",cls:"ui-button btn btn-default",icon:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).on("click",function(){$(".tooltip").hide();if(r.onclick){r.onclick()}});$(this.el).tooltip({title:r.tooltip,placement:"bottom"})},_template:function(r){var s='";return s}});var h=Backbone.View.extend({optionsDefault:{id:null,title:"",floating:"right",cls:"ui-button-icon",icon:"",tooltip:"",onclick:null},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));this.$button=this.$el.find(".button");var r=this;$(this.el).on("click",function(){$(".tooltip").hide();if(s.onclick&&!r.disabled){s.onclick()}});$(this.el).tooltip({title:s.tooltip,placement:"bottom"})},disable:function(){this.$button.addClass("disabled");this.disabled=true},enable:function(){this.$button.removeClass("disabled");this.disabled=false},_template:function(r){var s="";if(r.title){s="width: auto;"}var t='
';if(r.title){t+='
 '+r.title+"
"}else{t+=''}t+="
";return t}});var f=Backbone.View.extend({optionsDefault:{title:"",cls:""},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement(this._template(this.options));$(this.el).on("click",r.onclick)},_template:function(r){return'"}});var n=Backbone.View.extend({optionsDefault:{message:null,status:"info",persistent:false},initialize:function(r){this.options=k.merge(r,this.optionsDefault);this.setElement("
");if(this.options.message){this.update(this.options)}},update:function(s){this.options=k.merge(s,this.optionsDefault);if(s.message!=""){this.$el.html(this._template(this.options));this.$el.find(".alert").append(s.message);this.$el.fadeIn();if(this.timeout){window.clearTimeout(this.timeout)}if(!s.persistent){var r=this;this.timeout=window.setTimeout(function(){if(r.$el.is(":visible")){r.$el.fadeOut()}else{r.$el.hide()}},3000)}}else{this.$el.fadeOut()}},_template:function(r){return'
'}});var a=Backbone.View.extend({optionsDefault:{onclick:null,searchword:""},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));var r=this;if(this.options.onclick){this.$el.on("submit",function(u){var t=r.$el.find("#search");r.options.onclick(t.val())})}},_template:function(r){return''}});var i=Backbone.View.extend({optionsDefault:{type:"text",placeholder:"",disabled:false,visible:true,cls:"",area:false},initialize:function(s){this.options=k.merge(s,this.optionsDefault);this.setElement(this._template(this.options));if(this.options.value!==undefined){this.value(this.options.value)}if(this.options.disabled){this.$el.prop("disabled",true)}if(!this.options.visible){this.$el.hide()}var r=this;this.$el.on("input",function(){if(r.options.onchange){r.options.onchange(r.$el.val())}})},value:function(r){if(r!==undefined){this.$el.val(r)}return this.$el.val()},_template:function(r){if(r.area){return''}else{return''}}});var p=Backbone.View.extend({initialize:function(r){this.options=r;this.setElement(this._template(this.options));if(this.options.value!==undefined){this.value(this.options.value)}},value:function(r){if(r!==undefined){this.$("hidden").val(r)}return this.$("hidden").val()},_template:function(s){var r='
';if(s.info){r+=""}r+='
';return r}});return{Anchor:f,Button:g,ButtonIcon:h,ButtonMenu:q,Icon:c,Image:d,Input:i,Label:j,Message:n,Modal:m,RadioButton:o.RadioButton,Checkbox:o.Checkbox,Radio:o.Radio,Searchbox:a,Select:b,Hidden:p,Slider:e,Drilldown:l}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/ui/ui-options.js b/static/scripts/packed/mvc/ui/ui-options.js index 53a6e1537ea..61cdb01934b 100644 --- a/static/scripts/packed/mvc/ui/ui-options.js +++ b/static/scripts/packed/mvc/ui/ui-options.js @@ -1 +1 @@ -define(["utils/utils"],function(b){var a=Backbone.View.extend({initialize:function(g){this.optionsDefault={visible:true,data:[],id:b.uuid(),error_text:"No data available.",wait_text:"Please wait..."};this.options=b.merge(g,this.optionsDefault);this.setElement("
");this.$message=$("
");this.$options=$(this._template(g));this.$el.append(this.$message);this.$el.append(this.$options);if(!this.options.visible){this.$el.hide()}this.update(this.options.data);if(this.options.value!==undefined){this.value(this.options.value)}var f=this;this.on("change",function(){f._change()})},update:function(g){var j=this._getValue();this.$options.empty();if(this._templateOptions){this.$options.append(this._templateOptions(g))}else{for(var h in g){var i=$(this._templateOption(g[h]));i.addClass("ui-option");this.$options.append(i)}}var f=this;this.$el.find("input").on("change",function(){f.value(f._getValue());f._change()});this._refresh();this.value(j)},value:function(g){if(g!==undefined){if(!(g instanceof Array)){g=[g]}this.$el.find("input").prop("checked",false);for(var f in g){this.$el.find('input[value="'+g[f]+'"]').first().prop("checked",true)}}return this._getValue()},exists:function(g){if(g!==undefined){if(!(g instanceof Array)){g=[g]}for(var f in g){if(this.$el.find('input[value="'+g[f]+'"]').length>0){return true}}}return false},first:function(){var f=this.$el.find("input");if(f.length>0){return f.val()}else{return undefined}},validate:function(){var g=this.value();if(!(g instanceof Array)){g=[g]}for(var f in g){if([null,"null",undefined].indexOf(g[f])>-1){return false}}return true},wait:function(){if(this._size()==0){this._messageShow(this.options.wait_text,"info");this.$options.hide()}},unwait:function(){this._messageHide();this._refresh()},_change:function(){if(this.options.onchange){this.options.onchange(this._getValue())}},_refresh:function(){if(this._size()==0){this._messageShow(this.options.error_text,"danger");this.$options.hide()}else{this._messageHide();this.$options.css("display","inline-block")}},_getValue:function(){var g=this.$el.find(":checked");if(g.length==0){return"null"}if(this.options.multiple){var f=[];g.each(function(){f.push($(this).val())});return f}else{return g.val()}},_size:function(){return this.$el.find(".ui-option").length},_messageShow:function(g,f){this.$message.show();this.$message.removeClass();this.$message.addClass("ui-message alert alert-"+f);this.$message.html(g)},_messageHide:function(){this.$message.hide()},_template:function(){return'
'}});var d={};d.View=a.extend({initialize:function(f){a.prototype.initialize.call(this,f)},_templateOption:function(f){return'
'+f.label+"
"}});var c={};c.View=a.extend({initialize:function(f){f.multiple=true;a.prototype.initialize.call(this,f)},_templateOption:function(f){return'
'+f.label+"
"}});var e={};e.View=a.extend({initialize:function(f){a.prototype.initialize.call(this,f)},value:function(f){if(f!==undefined){this.$el.find("input").prop("checked",false);this.$el.find("label").removeClass("active");this.$el.find('[value="'+f+'"]').prop("checked",true).closest("label").addClass("active")}return this._getValue()},_templateOption:function(g){var f='";return f},_template:function(){return'
'}});return{Base:a,Radio:d,RadioButton:e,Checkbox:c}}); \ No newline at end of file +define(["utils/utils"],function(b){var a=Backbone.View.extend({initialize:function(g){this.optionsDefault={visible:true,data:[],id:b.uuid(),error_text:"No data available.",wait_text:"Please wait..."};this.options=b.merge(g,this.optionsDefault);this.setElement("
");this.$message=$("
");this.$options=$(this._template(g));this.$el.append(this.$message);this.$el.append(this.$options);if(!this.options.visible){this.$el.hide()}this.update(this.options.data);if(this.options.value!==undefined){this.value(this.options.value)}var f=this;this.on("change",function(){f._change()})},update:function(g){var j=this._getValue();this.$options.empty();if(this._templateOptions){this.$options.append(this._templateOptions(g))}else{for(var h in g){var i=$(this._templateOption(g[h]));i.addClass("ui-option");this.$options.append(i)}}var f=this;this.$el.find("input").on("change",function(){f.value(f._getValue());f._change()});this._refresh();this.value(j)},value:function(g){if(g!==undefined){if(!(g instanceof Array)){g=[g]}this.$el.find("input").prop("checked",false);for(var f in g){this.$el.find('input[value="'+g[f]+'"]').first().prop("checked",true)}}return this._getValue()},exists:function(g){if(g!==undefined){if(!(g instanceof Array)){g=[g]}for(var f in g){if(this.$el.find('input[value="'+g[f]+'"]').length>0){return true}}}return false},first:function(){var f=this.$el.find("input");if(f.length>0){return f.val()}else{return undefined}},validate:function(){return b.validate(this.value())},wait:function(){if(this._size()==0){this._messageShow(this.options.wait_text,"info");this.$options.hide()}},unwait:function(){this._messageHide();this._refresh()},_change:function(){if(this.options.onchange){this.options.onchange(this._getValue())}},_refresh:function(){if(this._size()==0){this._messageShow(this.options.error_text,"danger");this.$options.hide()}else{this._messageHide();this.$options.css("display","inline-block")}},_getValue:function(){var g=this.$el.find(":checked");if(g.length==0){return"null"}if(this.options.multiple){var f=[];g.each(function(){f.push($(this).val())});return f}else{return g.val()}},_size:function(){return this.$el.find(".ui-option").length},_messageShow:function(g,f){this.$message.show();this.$message.removeClass();this.$message.addClass("ui-message alert alert-"+f);this.$message.html(g)},_messageHide:function(){this.$message.hide()},_template:function(){return'
'}});var d={};d.View=a.extend({initialize:function(f){a.prototype.initialize.call(this,f)},_templateOption:function(f){return'
'+f.label+"
"}});var c={};c.View=a.extend({initialize:function(f){f.multiple=true;a.prototype.initialize.call(this,f)},_templateOption:function(f){return'
'+f.label+"
"}});var e={};e.View=a.extend({initialize:function(f){a.prototype.initialize.call(this,f)},value:function(f){if(f!==undefined){this.$el.find("input").prop("checked",false);this.$el.find("label").removeClass("active");this.$el.find('[value="'+f+'"]').prop("checked",true).closest("label").addClass("active")}return this._getValue()},_templateOption:function(g){var f='";return f},_template:function(){return'
'}});return{Base:a,Radio:d,RadioButton:e,Checkbox:c}}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/ui/ui-select-default.js b/static/scripts/packed/mvc/ui/ui-select-default.js index 4255908df03..1a69cef9950 100644 --- a/static/scripts/packed/mvc/ui/ui-select-default.js +++ b/static/scripts/packed/mvc/ui/ui-select-default.js @@ -1 +1 @@ -define(["utils/utils"],function(a){var b=Backbone.View.extend({optionsDefault:{id:"",cls:"",error_text:"No data available",visible:true,wait:false,multiple:false,searchable:false},initialize:function(d){this.options=a.merge(d,this.optionsDefault);this.setElement(this._template(this.options));this.$select=this.$el.find(".select");this.$icon=this.$el.find(".icon");this.$button=this.$el.find(".button");if(this.options.multiple){this.$select.prop("multiple",true);this.$select.addClass("ui-select-multiple");this.$icon.remove()}else{this.$el.addClass("ui-select")}this.update(this.options.data);if(this.options.value!==undefined){this.value(this.options.value)}if(!this.options.visible){this.hide()}if(this.options.wait){this.wait()}else{this.show()}var c=this;this.$select.on("change",function(){c._change()});this.on("change",function(){c._change()})},value:function(c){if(c!==undefined){this.$select.val(c);if(this.$select.select2){this.$select.select2("val",c)}}return this.$select.val()},first:function(){var c=this.$select.find("option");if(c.length>0){return c.val()}else{return undefined}},validate:function(){var d=this.value();if(!(d instanceof Array)){d=[d]}for(var c in d){if([null,"null",undefined].indexOf(d[c])>-1){return false}}return true},text:function(){return this.$select.find("option:selected").text()},show:function(){this.unwait();this.$select.show();this.$el.show()},hide:function(){this.$el.hide()},wait:function(){this.$icon.removeClass();this.$icon.addClass("fa fa-spinner fa-spin")},unwait:function(){this.$icon.removeClass();this.$icon.addClass("fa fa-caret-down")},disabled:function(){return this.$select.is(":disabled")},enable:function(){this.$select.prop("disabled",false)},disable:function(){this.$select.prop("disabled",true)},add:function(c){this.$select.append(this._templateOption(c));this._refresh()},del:function(c){this.$select.find("option[value="+c+"]").remove();this.$select.trigger("change");this._refresh()},update:function(c){var e=this.$select.val();this.$select.find("option").remove();for(var d in c){this.$select.append(this._templateOption(c[d]))}this._refresh();this.$select.val(e);if(!this.$select.val()){this.$select.val(this.first())}if(this.options.searchable){this.$button.hide();this.$select.select2("destroy");this.$select.select2()}},setOnChange:function(c){this.options.onchange=c},exists:function(c){return this.$select.find('option[value="'+c+'"]').length>0},_change:function(){if(this.options.onchange){this.options.onchange(this.$select.val())}},_refresh:function(){this.$select.find("option[value=null]").remove();var c=this.$select.find("option").length;if(c==0){this.disable();this.$select.append(this._templateOption({value:"null",label:this.options.error_text}))}else{this.enable()}},_templateOption:function(c){return'"},_template:function(c){return'
'}});return{View:b}}); \ No newline at end of file +define(["utils/utils"],function(a){var b=Backbone.View.extend({optionsDefault:{id:"",cls:"",error_text:"No data available",visible:true,wait:false,multiple:false,searchable:false},initialize:function(d){this.options=a.merge(d,this.optionsDefault);this.setElement(this._template(this.options));this.$select=this.$el.find(".select");this.$icon=this.$el.find(".icon");this.$button=this.$el.find(".button");if(this.options.multiple){this.$select.prop("multiple",true);this.$select.addClass("ui-select-multiple");this.$icon.remove()}else{this.$el.addClass("ui-select")}this.update(this.options.data);if(this.options.value!==undefined){this.value(this.options.value)}if(!this.options.visible){this.hide()}if(this.options.wait){this.wait()}else{this.show()}var c=this;this.$select.on("change",function(){c._change()});this.on("change",function(){c._change()})},value:function(c){if(c!==undefined){this.$select.val(c);if(this.$select.select2){this.$select.select2("val",c)}}return this.$select.val()},first:function(){var c=this.$select.find("option");if(c.length>0){return c.val()}else{return undefined}},validate:function(){return a.validate(this.value())},text:function(){return this.$select.find("option:selected").text()},show:function(){this.unwait();this.$select.show();this.$el.show()},hide:function(){this.$el.hide()},wait:function(){this.$icon.removeClass();this.$icon.addClass("fa fa-spinner fa-spin")},unwait:function(){this.$icon.removeClass();this.$icon.addClass("fa fa-caret-down")},disabled:function(){return this.$select.is(":disabled")},enable:function(){this.$select.prop("disabled",false)},disable:function(){this.$select.prop("disabled",true)},add:function(c){this.$select.append(this._templateOption(c));this._refresh()},del:function(c){this.$select.find("option[value="+c+"]").remove();this.$select.trigger("change");this._refresh()},update:function(c){var e=this.$select.val();this.$select.find("option").remove();for(var d in c){this.$select.append(this._templateOption(c[d]))}this._refresh();this.$select.val(e);if(!this.$select.val()){this.$select.val(this.first())}if(this.options.searchable){this.$button.hide();this.$select.select2("destroy");this.$select.select2()}},setOnChange:function(c){this.options.onchange=c},exists:function(c){return this.$select.find('option[value="'+c+'"]').length>0},_change:function(){if(this.options.onchange){this.options.onchange(this.$select.val())}},_refresh:function(){this.$select.find("option[value=null]").remove();var c=this.$select.find("option").length;if(c==0){this.disable();this.$select.append(this._templateOption({value:"null",label:this.options.error_text}))}else{this.enable()}},_templateOption:function(c){return'"},_template:function(c){return'
'}});return{View:b}}); \ No newline at end of file diff --git a/static/scripts/packed/utils/utils.js b/static/scripts/packed/utils/utils.js index 0a05fe61c62..f0a3c4cd314 100644 --- a/static/scripts/packed/utils/utils.js +++ b/static/scripts/packed/utils/utils.js @@ -1 +1 @@ -define(["libs/underscore"],function(k){function d(l){return $("
").text(l).html()}function e(l){top.__utils__get__=top.__utils__get__||{};if(l.cache&&top.__utils__get__[l.url]){l.success&&l.success(top.__utils__get__[l.url]);console.debug("utils.js::get() - Fetching from cache ["+l.url+"].")}else{h({url:l.url,data:l.data,success:function(m){top.__utils__get__[l.url]=m;l.success&&l.success(m)},error:function(m){l.error&&l.error(m)}})}}function h(m){var l={contentType:"application/json",type:m.type||"GET",data:m.data||{},url:m.url};if(l.type=="GET"||l.type=="DELETE"){if(l.url.indexOf("?")==-1){l.url+="?"}else{l.url+="&"}l.url=l.url+$.param(l.data);l.data=null}else{l.dataType="json";l.url=l.url;l.data=JSON.stringify(l.data)}$.ajax(l).done(function(n){if(typeof n==="string"){try{n=n.replace("Infinity,",'"Infinity",');n=jQuery.parseJSON(n)}catch(o){console.debug(o)}}m.success&&m.success(n)}).fail(function(o){var n=null;try{n=jQuery.parseJSON(o.responseText)}catch(p){n=o.responseText}m.error&&m.error(n,o)})}function i(o,l){var m=$('
');m.appendTo(":eq(0)");var n=m.css(l);m.remove();return n}function g(l){if(!$('link[href^="'+l+'"]').length){$('').appendTo("head")}}function j(l,m){if(l){return k.defaults(l,m)}else{return m}}function b(m,o){var n="";if(m>=100000000000){m=m/100000000000;n="TB"}else{if(m>=100000000){m=m/100000000;n="GB"}else{if(m>=100000){m=m/100000;n="MB"}else{if(m>=100){m=m/100;n="KB"}else{if(m>0){m=m*10;n="b"}else{return"-"}}}}}var l=(Math.round(m)/10);if(o){return l+" "+n}else{return""+l+" "+n}}function a(){return"x"+Math.random().toString(36).substring(2,9)}function c(l){var m=$("

");m.append(l);return m}function f(){var n=new Date();var l=(n.getHours()<10?"0":"")+n.getHours();var m=(n.getMinutes()<10?"0":"")+n.getMinutes();var o=n.getDate()+"/"+(n.getMonth()+1)+"/"+n.getFullYear()+", "+l+":"+m;return o}return{cssLoadFile:g,cssGetAttribute:i,get:e,merge:j,bytesToString:b,uuid:a,time:f,wrap:c,request:h,sanitize:d}}); \ No newline at end of file +define(["libs/underscore"],function(m){function d(n){return $("
").text(n).html()}function l(o){if(!(o instanceof Array)){o=[o]}for(var n in o){if(["None",null,"null",undefined,"undefined"].indexOf(o[n])>-1){return false}}return true}function h(n){var n=n.toString();if(n){n=n.replace(/,/g,", ");var o=n.lastIndexOf(", ");if(o!=-1){n=n.substr(0,o)+" or "+n.substr(o+1)}return n}return""}function e(n){top.__utils__get__=top.__utils__get__||{};if(n.cache&&top.__utils__get__[n.url]){n.success&&n.success(top.__utils__get__[n.url]);console.debug("utils.js::get() - Fetching from cache ["+n.url+"].")}else{i({url:n.url,data:n.data,success:function(o){top.__utils__get__[n.url]=o;n.success&&n.success(o)},error:function(o){n.error&&n.error(o)}})}}function i(o){var n={contentType:"application/json",type:o.type||"GET",data:o.data||{},url:o.url};if(n.type=="GET"||n.type=="DELETE"){if(n.url.indexOf("?")==-1){n.url+="?"}else{n.url+="&"}n.url=n.url+$.param(n.data);n.data=null}else{n.dataType="json";n.url=n.url;n.data=JSON.stringify(n.data)}$.ajax(n).done(function(p){if(typeof p==="string"){try{p=p.replace("Infinity,",'"Infinity",');p=jQuery.parseJSON(p)}catch(q){console.debug(q)}}o.success&&o.success(p)}).fail(function(q){var p=null;try{p=jQuery.parseJSON(q.responseText)}catch(r){p=q.responseText}o.error&&o.error(p,q)})}function j(q,n){var o=$('
');o.appendTo(":eq(0)");var p=o.css(n);o.remove();return p}function g(n){if(!$('link[href^="'+n+'"]').length){$('').appendTo("head")}}function k(n,o){if(n){return m.defaults(n,o)}else{return o}}function b(o,q){var p="";if(o>=100000000000){o=o/100000000000;p="TB"}else{if(o>=100000000){o=o/100000000;p="GB"}else{if(o>=100000){o=o/100000;p="MB"}else{if(o>=100){o=o/100;p="KB"}else{if(o>0){o=o*10;p="b"}else{return"-"}}}}}var n=(Math.round(o)/10);if(q){return n+" "+p}else{return""+n+" "+p}}function a(){return"x"+Math.random().toString(36).substring(2,9)}function c(n){var o=$("

");o.append(n);return o}function f(){var p=new Date();var n=(p.getHours()<10?"0":"")+p.getHours();var o=(p.getMinutes()<10?"0":"")+p.getMinutes();var q=p.getDate()+"/"+(p.getMonth()+1)+"/"+p.getFullYear()+", "+n+":"+o;return q}return{cssLoadFile:g,cssGetAttribute:j,get:e,merge:k,bytesToString:b,uuid:a,time:f,wrap:c,request:i,sanitize:d,textify:h,validate:l}}); \ No newline at end of file diff --git a/static/scripts/utils/utils.js b/static/scripts/utils/utils.js index 86432291d25..be978959a7d 100644 --- a/static/scripts/utils/utils.js +++ b/static/scripts/utils/utils.js @@ -14,6 +14,40 @@ function sanitize(content) { return $('
').text(content).html(); }; +/** + * Validate atomic values or list of values + * usually used for selectable options + * @param{String} value - Value or list to be validated + */ +function validate (value) { + if (!(value instanceof Array)) { + value = [value]; + } + for (var i in value) { + if (['None', null, 'null', undefined, 'undefined'].indexOf(value[i]) > -1) { + return false; + } + } + return true; +}; + +/** + * Convert list to pretty string + * @param{String} lst - List of strings to be converted in human readable list sentence + */ +function textify(lst) { + var lst = lst.toString(); + if (lst) { + lst = lst.replace(/,/g, ', '); + var pos = lst.lastIndexOf(', '); + if (pos != -1) { + lst = lst.substr(0, pos) + ' or ' + lst.substr(pos+1); + } + return lst; + } + return ''; +}; + /** * Request handler for GET * @param{String} url - Url request is made to @@ -212,7 +246,9 @@ return { time: time, wrap: wrap, request: request, - sanitize: sanitize + sanitize: sanitize, + textify: textify, + validate: validate }; }); diff --git a/templates/webapps/galaxy/tool_form.mako b/templates/webapps/galaxy/tool_form.mako index 20ead434dc5..50834049a28 100644 --- a/templates/webapps/galaxy/tool_form.mako +++ b/templates/webapps/galaxy/tool_form.mako @@ -8,12 +8,10 @@ ${h.css('base', 'jquery-ui/smoothness/jquery-ui')} <% ## TEMPORARY: create tool dictionary in mako while both tool forms are in use. ## This avoids making two separate requests since the classic form requires the mako anyway. - from galaxy.webapps.galaxy.api.tools import ToolsController - controller = ToolsController(trans.app) params = dict(trans.request.params) if 'id' in params: params['dataset_id'] = params['id'] - self.form_config = controller._build_dict(trans, tool, params) + self.form_config = tool.to_json(trans, **params) self.form_config.update({ 'id' : tool.id, 'job_id' : trans.security.encode_id( job.id ) if job else None, diff --git a/templates/webapps/galaxy/workflow/editor_tool_form.mako b/templates/webapps/galaxy/workflow/editor_tool_form.mako index 9b47b8e5295..0b94fe270f3 100644 --- a/templates/webapps/galaxy/workflow/editor_tool_form.mako +++ b/templates/webapps/galaxy/workflow/editor_tool_form.mako @@ -1,127 +1,161 @@ -<% -from galaxy.tools.parameters import DataToolParameter, RuntimeValue -from galaxy.tools.parameters import DataCollectionToolParameter -from galaxy.util.expressions import ExpressionContext -%> +## TEMPORARY SWITCH FOR THE NEW TOOL FORM +%if False: + ${h.js("libs/bibtex", "libs/jquery/jquery-ui")} + ${h.css('jquery-ui/smoothness/jquery-ui')} + <% + ## TEMPORARY: create tool dictionary in mako while both tool forms are in use. + ## This avoids making two separate requests since the classic form requires the mako anyway. + from galaxy.tools.parameters import params_to_incoming + incoming = {} + params_to_incoming( incoming, tool.inputs, module.state.inputs, trans.app ) + self.form_config = tool.to_json(trans, **incoming) + self.form_config.update({ + 'id' : tool.id, + 'job_id' : trans.security.encode_id( job.id ) if job else None, + 'history_id' : trans.security.encode_id( trans.history.id ), + 'workflow' : True, + 'container' : '#right-content' + }) + %> + +
+
+ +
+
+%else: + <% + from galaxy.tools.parameters import DataToolParameter, RuntimeValue + from galaxy.tools.parameters import DataCollectionToolParameter + from galaxy.util.expressions import ExpressionContext + %> -<%def name="do_inputs( inputs, values, errors, prefix, ctx=None )"> - <% ctx = ExpressionContext( values, ctx ) %> - %for input_index, input in enumerate( inputs.itervalues() ): - %if input.type == "repeat": -
- - <% repeat_values = values[input.name] %> - %for i in range( len( repeat_values ) ): - <% - if input.name in errors: - rep_errors = errors[input.name][i] - else: - rep_errors = dict() - index = repeat_values[i]['__index__'] - %> -
-
- ${do_inputs( input.inputs, repeat_values[ i ], rep_errors, prefix + input.name + "_" + str(index) + "|", ctx )} -
-
- %endfor -
-
- %elif input.type == "conditional": - %if input.is_job_resource_conditional: - <% continue %> - %endif - <% group_values = values[input.name] %> - <% current_case = group_values['__current_case__'] %> - <% group_prefix = prefix + input.name + "|" %> - <% group_errors = errors.get( input.name, {} ) %> - ${row_for_param( input.test_param, group_values[ input.test_param.name ], group_errors, group_prefix, ctx, allow_runtime=False )} - ${do_inputs( input.cases[ current_case ].inputs, group_values, group_errors, group_prefix, ctx )} - %else: - %if input.name in values: - ${row_for_param( input, values[ input.name ], errors, prefix, ctx )} - %else: - <% errors[ input.name ] = 'Value not stored, displaying default' %> - ${row_for_param( input, input.get_initial_value( trans, values ), errors, prefix, ctx )} - %endif - %endif - %endfor - - -<%def name="row_for_param( param, value, error_dict, prefix, ctx, allow_runtime=True )"> - %if error_dict.has_key( param.name ): - <% cls = "form-row form-row-error" %> - %else: - <% cls = "form-row" %> - %endif -
- ## Data parameters are very special since their value / runtime state - ## comes from connectors - %if type( param ) is DataToolParameter: - -
- Data input '${param.name}' (${" or ".join( param.extensions )}) -
- %elif type( param ) is DataCollectionToolParameter: - -
- Data collection input '${param.name}' -
+ <%def name="do_inputs( inputs, values, errors, prefix, ctx=None )"> + <% ctx = ExpressionContext( values, ctx ) %> + %for input_index, input in enumerate( inputs.itervalues() ): + %if input.type == "repeat": +
+ + <% repeat_values = values[input.name] %> + %for i in range( len( repeat_values ) ): + <% + if input.name in errors: + rep_errors = errors[input.name][i] + else: + rep_errors = dict() + index = repeat_values[i]['__index__'] + %> +
+
+ ${do_inputs( input.inputs, repeat_values[ i ], rep_errors, prefix + input.name + "_" + str(index) + "|", ctx )} +
+
+ %endfor +
+
+ %elif input.type == "conditional": + %if input.is_job_resource_conditional: + <% continue %> + %endif + <% group_values = values[input.name] %> + <% current_case = group_values['__current_case__'] %> + <% group_prefix = prefix + input.name + "|" %> + <% group_errors = errors.get( input.name, {} ) %> + ${row_for_param( input.test_param, group_values[ input.test_param.name ], group_errors, group_prefix, ctx, allow_runtime=False )} + ${do_inputs( input.cases[ current_case ].inputs, group_values, group_errors, group_prefix, ctx )} %else: - %if isinstance( value, RuntimeValue ): + %if input.name in values: + ${row_for_param( input, values[ input.name ], errors, prefix, ctx )} + %else: + <% errors[ input.name ] = 'Value not stored, displaying default' %> + ${row_for_param( input, input.get_initial_value( trans, values ), errors, prefix, ctx )} + %endif + %endif + %endfor + + + <%def name="row_for_param( param, value, error_dict, prefix, ctx, allow_runtime=True )"> + %if error_dict.has_key( param.name ): + <% cls = "form-row form-row-error" %> + %else: + <% cls = "form-row" %> + %endif +
+ ## Data parameters are very special since their value / runtime state + ## comes from connectors + %if type( param ) is DataToolParameter:
- To be set at runtime + Data input '${param.name}' (${" or ".join( param.extensions )}) +
+ %elif type( param ) is DataCollectionToolParameter: + +
+ Data collection input '${param.name}'
%else: -
- - -
- -
-
Tool: ${tool.name}
- %if tool.version: -
Version: ${tool.version}
- %endif -
- - %for i, inputs in enumerate( tool.inputs_by_page ): - %if tool.has_multiple_pages: -
Page ${i+1}
+ +
+ To be set at runtime +
+ %else: + +
+ ${param.get_html_field( trans, value, ctx ).get_html( prefix )} +
%endif - ${do_inputs( inputs, values, errors, "" )} - %endfor + %if error_dict.has_key( param.name ): +
+
 ${error_dict[param.name]}
+
+ %endif + %endif +
-
- + -
+
+ +
+
Tool: ${tool.name}
+ %if tool.version: +
Version: ${tool.version}
+ %endif +
+ + %for i, inputs in enumerate( tool.inputs_by_page ): + %if tool.has_multiple_pages: +
Page ${i+1}
+ %endif + ${do_inputs( inputs, values, errors, "" )} + %endfor +
+
+ + +
+%endif \ No newline at end of file From 76a9820054331fd6d092ca5a415e2204387b40fc Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Sun, 11 Jan 2015 12:08:10 -0500 Subject: [PATCH 148/473] ToolForm/Workflow: Add modules --- .../scripts/mvc/tools/tools-form-base.js | 315 ++++++++++++++++++ .../scripts/mvc/tools/tools-form-workflow.js | 66 ++++ static/scripts/mvc/tools/tools-form-base.js | 315 ++++++++++++++++++ .../scripts/mvc/tools/tools-form-workflow.js | 66 ++++ .../packed/mvc/tools/tools-form-base.js | 1 + .../packed/mvc/tools/tools-form-workflow.js | 1 + 6 files changed, 764 insertions(+) create mode 100644 client/galaxy/scripts/mvc/tools/tools-form-base.js create mode 100644 client/galaxy/scripts/mvc/tools/tools-form-workflow.js create mode 100644 static/scripts/mvc/tools/tools-form-base.js create mode 100644 static/scripts/mvc/tools/tools-form-workflow.js create mode 100644 static/scripts/packed/mvc/tools/tools-form-base.js create mode 100644 static/scripts/packed/mvc/tools/tools-form-workflow.js diff --git a/client/galaxy/scripts/mvc/tools/tools-form-base.js b/client/galaxy/scripts/mvc/tools/tools-form-base.js new file mode 100644 index 00000000000..71eecb21069 --- /dev/null +++ b/client/galaxy/scripts/mvc/tools/tools-form-base.js @@ -0,0 +1,315 @@ +/** + This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. +*/ +define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', + 'mvc/citation/citation-model', 'mvc/citation/citation-view', + 'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree'], + function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView, + Tools, ToolTemplate, ToolContent, ToolSection, ToolTree) { + + // create form view + return Backbone.View.extend({ + // initialize + initialize: function(options) { + // log options + console.debug(options); + + // link galaxy modal or create one + var galaxy = parent.Galaxy; + if (galaxy && galaxy.modal) { + this.modal = galaxy.modal; + } else { + this.modal = new Ui.Modal.View(); + } + + // check if the user is an admin + if (galaxy && galaxy.currUser) { + this.is_admin = galaxy.currUser.get('is_admin'); + } else { + this.is_admin = false; + } + + // link options + this.options = options; + + // link container + this.container = this.options.container || 'body'; + + // create deferred processing queue handler + // this handler reduces the number of requests to the api by filtering redundant requests + this.deferred = new Deferred(); + + // set element + this.setElement('
'); + + // add to main element + $(this.container).append(this.$el); + + // build this form + this._buildForm(); + }, + + /** Shows the final message (usually upon successful job submission) + */ + reciept: function($el) { + $(this.container).empty(); + $(this.container).append($el); + }, + + /** Highlight and scroll to input element (currently only used for error notifications) + */ + highlight: function (input_id, message, silent) { + // get input field + var input_element = this.element_list[input_id]; + + // check input element + if (input_element) { + // mark error + input_element.error(message || 'Please verify this parameter.'); + + // scroll to first input element + if (!silent) { + $(this.container).animate({ + scrollTop: input_element.$el.offset().top - 20 + }, 500); + } + } + }, + + /** Main tool form build function. This function is called once a new model is available. + */ + _buildForm: function() { + // link this + var self = this; + + // reset events + this.off('refresh'); + this.off('reset'); + + // reset field list, which contains the input field elements + this.field_list = {}; + + // reset sequential input definition list, which contains the input definitions as provided from the api + this.input_list = {}; + + // reset input element list, which contains the dom elements of each input element (includes also the input field) + this.element_list = {}; + + // creates a tree/json data structure from the input form + this.tree = new ToolTree(this); + + // request history content and build form + this.content = new ToolContent(this); + + // link model options + var options = this.options; + + // create ui elements + this._renderForm(options); + + // rebuild the underlying data structure + this.tree.finalize(); + + // show errors + if (!this.workflow && options.errors) { + var error_messages = this.tree.matchResponse(options.errors); + for (var input_id in error_messages) { + this.highlight(input_id, error_messages[input_id], true); + } + } + + // add refresh listener + this.on('refresh', function() { + // by using/reseting the deferred ajax queue the number of redundant calls is reduced + self.deferred.reset(); + self.deferred.execute(function(){self._updateModel()}); + }); + + // add reset listener + this.on('reset', function() { + for (var i in this.element_list) { + this.element_list[i].reset(); + } + }); + }, + + /** Renders the UI elements required for the form + */ + _renderForm: function(options) { + // link this + var self = this; + + // create message view + this.message = new Ui.Message(); + + // button for version selection + var requirements_button = new Ui.ButtonIcon({ + icon : 'fa-info-circle', + title : 'Requirements', + tooltip : 'Display tool requirements', + onclick : function() { + if (!this.visible) { + this.visible = true; + self.message.update({ + persistent : true, + message : ToolTemplate.requirements(options), + status : 'info' + }); + } else { + this.visible = false; + self.message.update({ + message : '' + }); + } + } + }); + if (!options.requirements || options.requirements.length == 0) { + requirements_button.$el.hide(); + } + + // button for version selection + var versions_button = new Ui.ButtonMenu({ + icon : 'fa-cubes', + title : 'Versions', + tooltip : 'Select another tool version' + }); + if (options.versions && options.versions.length > 1) { + for (var i in options.versions) { + var version = options.versions[i]; + if (version != options.version) { + versions_button.addMenu({ + title : 'Switch to ' + version, + version : version, + icon : 'fa-cube', + onclick : function() { + // here we update the tool version (some tools encode the version also in the id) + options.id = options.id.replace(options.version, this.version); + options.version = this.version; + + // rebuild the model and form + self.deferred.reset(); + self.deferred.execute(function(){self._buildModel()}); + } + }); + } + } + } else { + versions_button.$el.hide(); + } + + // button menu + var menu_button = new Ui.ButtonMenu({ + icon : 'fa-caret-down', + title : 'Options', + tooltip : 'View available options' + }); + + // configure button selection + if(options.biostar_url) { + // add question option + menu_button.addMenu({ + icon : 'fa-question-circle', + title : 'Question?', + tooltip : 'Ask a question about this tool (Biostar)', + onclick : function() { + window.open(options.biostar_url + '/p/new/post/'); + } + }); + + // create search button + menu_button.addMenu({ + icon : 'fa-search', + title : 'Search', + tooltip : 'Search help for this tool (Biostar)', + onclick : function() { + window.open(options.biostar_url + '/t/' + options.id + '/'); + } + }); + }; + + // create share button + menu_button.addMenu({ + icon : 'fa-share', + title : 'Share', + tooltip : 'Share this tool', + onclick : function() { + prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id); + } + }); + + // add admin operations + if (this.is_admin) { + // create download button + menu_button.addMenu({ + icon : 'fa-download', + title : 'Download', + tooltip : 'Download this tool', + onclick : function() { + window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download'; + } + }); + } + + // create tool form section + this.section = new ToolSection.View(self, { + inputs : options.inputs, + cls : 'ui-table-plain' + }); + + // switch to classic tool form mako if the form definition is incompatible + if (this.incompatible) { + this.$el.hide(); + $('#tool-form-classic').show(); + return; + } + + // create portlet + this.portlet = new Portlet.View({ + icon : 'fa-wrench', + title : '' + options.name + ' ' + options.description + ' (Galaxy Tool Version ' + options.version + ')', + cls : 'ui-portlet-slim', + operations: { + requirements : requirements_button, + menu : menu_button, + versions : versions_button + }, + buttons : this.buttons + }); + + // append message + this.portlet.append(this.message.$el, true); + + // append tool section + this.portlet.append(this.section.$el); + + // start form + this.$el.empty(); + this.$el.append(this.portlet.$el); + + // append help + if (options.help != '') { + this.$el.append(ToolTemplate.help(options.help)); + } + + // append citations + if (options.citations) { + var $citations = $('
'); + var citations = new CitationModel.ToolCitationCollection(); + citations.tool_id = options.id; + var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } ); + citation_list_view.render(); + citations.fetch(); + this.$el.append($citations); + } + + // show message if available in model + if (options.message) { + this.message.update({ + persistent : true, + status : 'warning', + message : options.message + }); + } + } + }); +}); diff --git a/client/galaxy/scripts/mvc/tools/tools-form-workflow.js b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js new file mode 100644 index 00000000000..c9a45ee795e --- /dev/null +++ b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js @@ -0,0 +1,66 @@ +/** + This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. +*/ +define(['utils/utils', 'mvc/tools/tools-form-base'], + function(Utils, ToolFormBase) { + + // create form view + var View = ToolFormBase.extend({ + initialize: function(options) { + this.workflow = true; + ToolFormBase.prototype.initialize.call(this, options); + }, + + /** Builds a new model through api call and recreates the entire form + */ + _buildModel: function() { + }, + + /** Request a new model for an already created tool form and updates the form inputs + */ + _updateModel: function() { + // create the request dictionary + var self = this; + var current_state = this.tree.finalize(); + + // log tool state + console.debug('tools-form-workflow::_refreshForm() - Refreshing states.'); + console.debug(current_state); + + // register process + var process_id = this.deferred.register(); + + // build model url for request + var model_url = galaxy_config.root + 'workflow/editor_form_post?tool_id=' + this.options.id; + + // post job + Utils.request({ + type : 'GET', + url : model_url, + data : current_state, + success : function(node) { + parent.update_node(node); + + // process completed + self.deferred.done(process_id); + + // log success + console.debug('tools-form::_refreshForm() - States refreshed.'); + console.debug(node); + }, + error : function(response) { + // process completed + self.deferred.done(process_id); + + // log error + console.debug('tools-form::_refreshForm() - Refresh request failed.'); + console.debug(response); + } + }); + } + }); + + return { + View: View + }; +}); diff --git a/static/scripts/mvc/tools/tools-form-base.js b/static/scripts/mvc/tools/tools-form-base.js new file mode 100644 index 00000000000..71eecb21069 --- /dev/null +++ b/static/scripts/mvc/tools/tools-form-base.js @@ -0,0 +1,315 @@ +/** + This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. +*/ +define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', + 'mvc/citation/citation-model', 'mvc/citation/citation-view', + 'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree'], + function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView, + Tools, ToolTemplate, ToolContent, ToolSection, ToolTree) { + + // create form view + return Backbone.View.extend({ + // initialize + initialize: function(options) { + // log options + console.debug(options); + + // link galaxy modal or create one + var galaxy = parent.Galaxy; + if (galaxy && galaxy.modal) { + this.modal = galaxy.modal; + } else { + this.modal = new Ui.Modal.View(); + } + + // check if the user is an admin + if (galaxy && galaxy.currUser) { + this.is_admin = galaxy.currUser.get('is_admin'); + } else { + this.is_admin = false; + } + + // link options + this.options = options; + + // link container + this.container = this.options.container || 'body'; + + // create deferred processing queue handler + // this handler reduces the number of requests to the api by filtering redundant requests + this.deferred = new Deferred(); + + // set element + this.setElement('
'); + + // add to main element + $(this.container).append(this.$el); + + // build this form + this._buildForm(); + }, + + /** Shows the final message (usually upon successful job submission) + */ + reciept: function($el) { + $(this.container).empty(); + $(this.container).append($el); + }, + + /** Highlight and scroll to input element (currently only used for error notifications) + */ + highlight: function (input_id, message, silent) { + // get input field + var input_element = this.element_list[input_id]; + + // check input element + if (input_element) { + // mark error + input_element.error(message || 'Please verify this parameter.'); + + // scroll to first input element + if (!silent) { + $(this.container).animate({ + scrollTop: input_element.$el.offset().top - 20 + }, 500); + } + } + }, + + /** Main tool form build function. This function is called once a new model is available. + */ + _buildForm: function() { + // link this + var self = this; + + // reset events + this.off('refresh'); + this.off('reset'); + + // reset field list, which contains the input field elements + this.field_list = {}; + + // reset sequential input definition list, which contains the input definitions as provided from the api + this.input_list = {}; + + // reset input element list, which contains the dom elements of each input element (includes also the input field) + this.element_list = {}; + + // creates a tree/json data structure from the input form + this.tree = new ToolTree(this); + + // request history content and build form + this.content = new ToolContent(this); + + // link model options + var options = this.options; + + // create ui elements + this._renderForm(options); + + // rebuild the underlying data structure + this.tree.finalize(); + + // show errors + if (!this.workflow && options.errors) { + var error_messages = this.tree.matchResponse(options.errors); + for (var input_id in error_messages) { + this.highlight(input_id, error_messages[input_id], true); + } + } + + // add refresh listener + this.on('refresh', function() { + // by using/reseting the deferred ajax queue the number of redundant calls is reduced + self.deferred.reset(); + self.deferred.execute(function(){self._updateModel()}); + }); + + // add reset listener + this.on('reset', function() { + for (var i in this.element_list) { + this.element_list[i].reset(); + } + }); + }, + + /** Renders the UI elements required for the form + */ + _renderForm: function(options) { + // link this + var self = this; + + // create message view + this.message = new Ui.Message(); + + // button for version selection + var requirements_button = new Ui.ButtonIcon({ + icon : 'fa-info-circle', + title : 'Requirements', + tooltip : 'Display tool requirements', + onclick : function() { + if (!this.visible) { + this.visible = true; + self.message.update({ + persistent : true, + message : ToolTemplate.requirements(options), + status : 'info' + }); + } else { + this.visible = false; + self.message.update({ + message : '' + }); + } + } + }); + if (!options.requirements || options.requirements.length == 0) { + requirements_button.$el.hide(); + } + + // button for version selection + var versions_button = new Ui.ButtonMenu({ + icon : 'fa-cubes', + title : 'Versions', + tooltip : 'Select another tool version' + }); + if (options.versions && options.versions.length > 1) { + for (var i in options.versions) { + var version = options.versions[i]; + if (version != options.version) { + versions_button.addMenu({ + title : 'Switch to ' + version, + version : version, + icon : 'fa-cube', + onclick : function() { + // here we update the tool version (some tools encode the version also in the id) + options.id = options.id.replace(options.version, this.version); + options.version = this.version; + + // rebuild the model and form + self.deferred.reset(); + self.deferred.execute(function(){self._buildModel()}); + } + }); + } + } + } else { + versions_button.$el.hide(); + } + + // button menu + var menu_button = new Ui.ButtonMenu({ + icon : 'fa-caret-down', + title : 'Options', + tooltip : 'View available options' + }); + + // configure button selection + if(options.biostar_url) { + // add question option + menu_button.addMenu({ + icon : 'fa-question-circle', + title : 'Question?', + tooltip : 'Ask a question about this tool (Biostar)', + onclick : function() { + window.open(options.biostar_url + '/p/new/post/'); + } + }); + + // create search button + menu_button.addMenu({ + icon : 'fa-search', + title : 'Search', + tooltip : 'Search help for this tool (Biostar)', + onclick : function() { + window.open(options.biostar_url + '/t/' + options.id + '/'); + } + }); + }; + + // create share button + menu_button.addMenu({ + icon : 'fa-share', + title : 'Share', + tooltip : 'Share this tool', + onclick : function() { + prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id); + } + }); + + // add admin operations + if (this.is_admin) { + // create download button + menu_button.addMenu({ + icon : 'fa-download', + title : 'Download', + tooltip : 'Download this tool', + onclick : function() { + window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download'; + } + }); + } + + // create tool form section + this.section = new ToolSection.View(self, { + inputs : options.inputs, + cls : 'ui-table-plain' + }); + + // switch to classic tool form mako if the form definition is incompatible + if (this.incompatible) { + this.$el.hide(); + $('#tool-form-classic').show(); + return; + } + + // create portlet + this.portlet = new Portlet.View({ + icon : 'fa-wrench', + title : '' + options.name + ' ' + options.description + ' (Galaxy Tool Version ' + options.version + ')', + cls : 'ui-portlet-slim', + operations: { + requirements : requirements_button, + menu : menu_button, + versions : versions_button + }, + buttons : this.buttons + }); + + // append message + this.portlet.append(this.message.$el, true); + + // append tool section + this.portlet.append(this.section.$el); + + // start form + this.$el.empty(); + this.$el.append(this.portlet.$el); + + // append help + if (options.help != '') { + this.$el.append(ToolTemplate.help(options.help)); + } + + // append citations + if (options.citations) { + var $citations = $('
'); + var citations = new CitationModel.ToolCitationCollection(); + citations.tool_id = options.id; + var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } ); + citation_list_view.render(); + citations.fetch(); + this.$el.append($citations); + } + + // show message if available in model + if (options.message) { + this.message.update({ + persistent : true, + status : 'warning', + message : options.message + }); + } + } + }); +}); diff --git a/static/scripts/mvc/tools/tools-form-workflow.js b/static/scripts/mvc/tools/tools-form-workflow.js new file mode 100644 index 00000000000..c9a45ee795e --- /dev/null +++ b/static/scripts/mvc/tools/tools-form-workflow.js @@ -0,0 +1,66 @@ +/** + This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules. +*/ +define(['utils/utils', 'mvc/tools/tools-form-base'], + function(Utils, ToolFormBase) { + + // create form view + var View = ToolFormBase.extend({ + initialize: function(options) { + this.workflow = true; + ToolFormBase.prototype.initialize.call(this, options); + }, + + /** Builds a new model through api call and recreates the entire form + */ + _buildModel: function() { + }, + + /** Request a new model for an already created tool form and updates the form inputs + */ + _updateModel: function() { + // create the request dictionary + var self = this; + var current_state = this.tree.finalize(); + + // log tool state + console.debug('tools-form-workflow::_refreshForm() - Refreshing states.'); + console.debug(current_state); + + // register process + var process_id = this.deferred.register(); + + // build model url for request + var model_url = galaxy_config.root + 'workflow/editor_form_post?tool_id=' + this.options.id; + + // post job + Utils.request({ + type : 'GET', + url : model_url, + data : current_state, + success : function(node) { + parent.update_node(node); + + // process completed + self.deferred.done(process_id); + + // log success + console.debug('tools-form::_refreshForm() - States refreshed.'); + console.debug(node); + }, + error : function(response) { + // process completed + self.deferred.done(process_id); + + // log error + console.debug('tools-form::_refreshForm() - Refresh request failed.'); + console.debug(response); + } + }); + } + }); + + return { + View: View + }; +}); diff --git a/static/scripts/packed/mvc/tools/tools-form-base.js b/static/scripts/packed/mvc/tools/tools-form-base.js new file mode 100644 index 00000000000..d3905253c58 --- /dev/null +++ b/static/scripts/packed/mvc/tools/tools-form-base.js @@ -0,0 +1 @@ +define(["utils/utils","utils/deferred","mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/citation/citation-model","mvc/citation/citation-view","mvc/tools","mvc/tools/tools-template","mvc/tools/tools-content","mvc/tools/tools-section","mvc/tools/tools-tree"],function(g,h,f,k,i,a,d,c,e,j,b){return Backbone.View.extend({initialize:function(l){console.debug(l);var m=parent.Galaxy;if(m&&m.modal){this.modal=m.modal}else{this.modal=new k.Modal.View()}if(m&&m.currUser){this.is_admin=m.currUser.get("is_admin")}else{this.is_admin=false}this.options=l;this.container=this.options.container||"body";this.deferred=new h();this.setElement("
");$(this.container).append(this.$el);this._buildForm()},reciept:function(l){$(this.container).empty();$(this.container).append(l)},highlight:function(m,n,l){var o=this.element_list[m];if(o){o.error(n||"Please verify this parameter.");if(!l){$(this.container).animate({scrollTop:o.$el.offset().top-20},500)}}},_buildForm:function(){var l=this;this.off("refresh");this.off("reset");this.field_list={};this.input_list={};this.element_list={};this.tree=new b(this);this.content=new e(this);var n=this.options;this._renderForm(n);this.tree.finalize();if(!this.workflow&&n.errors){var o=this.tree.matchResponse(n.errors);for(var m in o){this.highlight(m,o[m],true)}}this.on("refresh",function(){l.deferred.reset();l.deferred.execute(function(){l._updateModel()})});this.on("reset",function(){for(var p in this.element_list){this.element_list[p].reset()}})},_renderForm:function(u){var t=this;this.message=new k.Message();var q=new k.ButtonIcon({icon:"fa-info-circle",title:"Requirements",tooltip:"Display tool requirements",onclick:function(){if(!this.visible){this.visible=true;t.message.update({persistent:true,message:c.requirements(u),status:"info"})}else{this.visible=false;t.message.update({message:""})}}});if(!u.requirements||u.requirements.length==0){q.$el.hide()}var m=new k.ButtonMenu({icon:"fa-cubes",title:"Versions",tooltip:"Select another tool version"});if(u.versions&&u.versions.length>1){for(var o in u.versions){var r=u.versions[o];if(r!=u.version){m.addMenu({title:"Switch to "+r,version:r,icon:"fa-cube",onclick:function(){u.id=u.id.replace(u.version,this.version);u.version=this.version;t.deferred.reset();t.deferred.execute(function(){t._buildModel()})}})}}}else{m.$el.hide()}var p=new k.ButtonMenu({icon:"fa-caret-down",title:"Options",tooltip:"View available options"});if(u.biostar_url){p.addMenu({icon:"fa-question-circle",title:"Question?",tooltip:"Ask a question about this tool (Biostar)",onclick:function(){window.open(u.biostar_url+"/p/new/post/")}});p.addMenu({icon:"fa-search",title:"Search",tooltip:"Search help for this tool (Biostar)",onclick:function(){window.open(u.biostar_url+"/t/"+u.id+"/")}})}p.addMenu({icon:"fa-share",title:"Share",tooltip:"Share this tool",onclick:function(){prompt("Copy to clipboard: Ctrl+C, Enter",window.location.origin+galaxy_config.root+"root?tool_id="+u.id)}});if(this.is_admin){p.addMenu({icon:"fa-download",title:"Download",tooltip:"Download this tool",onclick:function(){window.location.href=galaxy_config.root+"api/tools/"+u.id+"/download"}})}this.section=new j.View(t,{inputs:u.inputs,cls:"ui-table-plain"});if(this.incompatible){this.$el.hide();$("#tool-form-classic").show();return}this.portlet=new f.View({icon:"fa-wrench",title:""+u.name+" "+u.description+" (Galaxy Tool Version "+u.version+")",cls:"ui-portlet-slim",operations:{requirements:q,menu:p,versions:m},buttons:this.buttons});this.portlet.append(this.message.$el,true);this.portlet.append(this.section.$el);this.$el.empty();this.$el.append(this.portlet.$el);if(u.help!=""){this.$el.append(c.help(u.help))}if(u.citations){var s=$("
");var l=new i.ToolCitationCollection();l.tool_id=u.id;var n=new a.CitationListView({el:s,collection:l});n.render();l.fetch();this.$el.append(s)}if(u.message){this.message.update({persistent:true,status:"warning",message:u.message})}}})}); \ No newline at end of file diff --git a/static/scripts/packed/mvc/tools/tools-form-workflow.js b/static/scripts/packed/mvc/tools/tools-form-workflow.js new file mode 100644 index 00000000000..4bf9dfdf6a0 --- /dev/null +++ b/static/scripts/packed/mvc/tools/tools-form-workflow.js @@ -0,0 +1 @@ +define(["utils/utils","mvc/tools/tools-form-base"],function(b,a){var c=a.extend({initialize:function(d){this.workflow=true;a.prototype.initialize.call(this,d)},_buildModel:function(){},_updateModel:function(){var d=this;var e=this.tree.finalize();console.debug("tools-form-workflow::_refreshForm() - Refreshing states.");console.debug(e);var g=this.deferred.register();var f=galaxy_config.root+"workflow/editor_form_post?tool_id="+this.options.id;b.request({type:"GET",url:f,data:e,success:function(h){parent.update_node(h);d.deferred.done(g);console.debug("tools-form::_refreshForm() - States refreshed.");console.debug(h)},error:function(h){d.deferred.done(g);console.debug("tools-form::_refreshForm() - Refresh request failed.");console.debug(h)}})}});return{View:c}}); \ No newline at end of file From 0681eeeabcb2d4cf06e24b536e8f5f2051391003 Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Sun, 11 Jan 2015 12:59:43 -0500 Subject: [PATCH 149/473] Upload: Show upload button only in analysis view --- client/galaxy/scripts/mvc/upload/upload-view.js | 2 +- static/scripts/mvc/upload/upload-view.js | 2 +- static/scripts/packed/mvc/upload/upload-view.js | 2 +- templates/webapps/galaxy/root/index.mako | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/galaxy/scripts/mvc/upload/upload-view.js b/client/galaxy/scripts/mvc/upload/upload-view.js index 064139f2caa..db33657cf6d 100644 --- a/client/galaxy/scripts/mvc/upload/upload-view.js +++ b/client/galaxy/scripts/mvc/upload/upload-view.js @@ -104,7 +104,7 @@ return Backbone.View.extend({ }); // define location - $('#left .unified-panel-header-inner').append((new UploadButton.View(this.ui_button)).$el); + $('.with-upload-button').append((new UploadButton.View(this.ui_button)).$el); // load extension var self = this; diff --git a/static/scripts/mvc/upload/upload-view.js b/static/scripts/mvc/upload/upload-view.js index 064139f2caa..db33657cf6d 100644 --- a/static/scripts/mvc/upload/upload-view.js +++ b/static/scripts/mvc/upload/upload-view.js @@ -104,7 +104,7 @@ return Backbone.View.extend({ }); // define location - $('#left .unified-panel-header-inner').append((new UploadButton.View(this.ui_button)).$el); + $('.with-upload-button').append((new UploadButton.View(this.ui_button)).$el); // load extension var self = this; diff --git a/static/scripts/packed/mvc/upload/upload-view.js b/static/scripts/packed/mvc/upload/upload-view.js index 20670410b1b..0fe65750254 100644 --- a/static/scripts/packed/mvc/upload/upload-view.js +++ b/static/scripts/packed/mvc/upload/upload-view.js @@ -1 +1 @@ -define(["utils/utils","mvc/upload/upload-button","mvc/upload/upload-model","mvc/upload/upload-row","mvc/upload/upload-ftp","mvc/ui/ui-popover","mvc/ui/ui-modal","utils/uploadbox"],function(f,e,c,b,g,d,a){return Backbone.View.extend({options:{nginx_upload_path:""},modal:null,ui_button:null,uploadbox:null,current_history:null,upload_size:0,list_extensions:[],list_genomes:[],auto:{id:"auto",text:"Auto-detect",description:"This system will try to detect the file type automatically. If your file is not detected properly as one of the known formats, it most likely means that it has some format problems (e.g., different number of columns on different rows). You can still coerce the system to set your data to the format you think it should be. You can also upload compressed files, which will automatically be decompressed."},collection:new c.Collection(),ftp:null,counter:{announce:0,success:0,error:0,running:0,reset:function(){this.announce=this.success=this.error=this.running=0}},initialize:function(i){var h=this;if(i){this.options=_.defaults(i,this.options)}this.ui_button=new e.Model({icon:"fa-upload",tooltip:"Download from URL or upload files from disk",label:"Load Data",onclick:function(j){if(j){j.preventDefault();h.show()}},onunload:function(){if(h.counter.running>0){return"Several uploads are still processing."}}});$("#left .unified-panel-header-inner").append((new e.View(this.ui_button)).$el);var h=this;f.get({url:galaxy_config.root+"api/datatypes?extension_only=False",success:function(j){for(key in j){h.list_extensions.push({id:j[key].extension,text:j[key].extension,description:j[key].description,description_url:j[key].description_url})}h.list_extensions.sort(function(l,k){return l.id>k.id?1:l.idk.id?1:l.id0)){return null}this.uploadbox.configure({url:this.options.nginx_upload_path});if(h=="local"){this.uploadbox.configure({paramname:"files_0|file_data"})}else{this.uploadbox.configure({paramname:null})}tool_input={};if(h=="new"){tool_input["files_0|url_paste"]=q}if(h=="ftp"){tool_input["files_0|ftp_files"]=n}tool_input.dbkey=r;tool_input.file_type=p;tool_input["files_0|type"]="upload_dataset";tool_input.space_to_tabs=l;tool_input.to_posix_lines=i;data={};data.history_id=this.current_history;data.tool_id="upload1";data.inputs=JSON.stringify(tool_input);return data},_eventProgress:function(i,j,h){var k=this.collection.get(i);k.set("percentage",h);this.ui_button.set("percentage",this._upload_percentage(h,j.size))},_eventSuccess:function(i,j,l){var k=this.collection.get(i);k.set("percentage",100);k.set("status","success");var h=k.get("file_size");this.ui_button.set("percentage",this._upload_percentage(100,h));this.upload_completed+=h*100;this.counter.announce--;this.counter.success++;this._updateScreen();Galaxy.currHistoryPanel.refreshContents()},_eventError:function(h,i,k){var j=this.collection.get(h);j.set("percentage",100);j.set("status","error");j.set("info",k);this.ui_button.set("percentage",this._upload_percentage(100,i.size));this.ui_button.set("status","danger");this.upload_completed+=i.size*100;this.counter.announce--;this.counter.error++;this._updateScreen()},_eventComplete:function(){this.collection.each(function(h){if(h.get("status")=="queued"){h.set("status","init")}});this.counter.running=0;this._updateScreen()},_eventFtp:function(){if(!this.ftp.visible){this.ftp.empty();this.ftp.append((new g(this)).$el);this.ftp.show()}else{this.ftp.hide()}},_eventCreate:function(){this.uploadbox.add([{name:"New File",size:0,mode:"new"}])},_eventStart:function(){if(this.counter.announce==0||this.counter.running>0){return}var h=this;this.upload_size=0;this.upload_completed=0;this.collection.each(function(i){if(i.get("status")=="init"){i.set("status","queued");h.upload_size+=i.get("file_size")}});this.ui_button.set("percentage",0);this.ui_button.set("status","success");this.counter.running=this.counter.announce;this._updateScreen();this.uploadbox.start()},_eventStop:function(){if(this.counter.running==0){return}this.ui_button.set("status","info");this.uploadbox.stop();$("#upload-info").html("Queue will pause after completing the current file...")},_eventReset:function(){if(this.counter.running==0){this.collection.reset();this.counter.reset();this._updateScreen();this.uploadbox.reset();this.ui_button.set("percentage",0)}},_updateUser:function(){this.current_user=Galaxy.currUser.get("id");this.current_history=null;if(this.current_user){this.current_history=Galaxy.currHistoryPanel.model.get("id")}},_updateScreen:function(){if(this.counter.announce==0){if(this.uploadbox.compatible()){message="You can Drag & Drop files into this box."}else{message="Unfortunately, your browser does not support multiple file uploads or drag&drop.
Some supported browsers are: Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+."}}else{if(this.counter.running==0){message="You added "+this.counter.announce+" file(s) to the queue. Add more files or click 'Start' to proceed."}else{message="Please wait..."+this.counter.announce+" out of "+this.counter.running+" remaining."}}$("#upload-info").html(message);if(this.counter.running==0&&this.counter.announce+this.counter.success+this.counter.error>0){this.modal.enableButton("Reset")}else{this.modal.disableButton("Reset")}if(this.counter.running==0&&this.counter.announce>0){this.modal.enableButton("Start")}else{this.modal.disableButton("Start")}if(this.counter.running>0){this.modal.enableButton("Pause")}else{this.modal.disableButton("Pause")}if(this.counter.running==0){this.modal.enableButton("Choose local file");this.modal.enableButton("Choose FTP file");this.modal.enableButton("Paste/Fetch data")}else{this.modal.disableButton("Choose local file");this.modal.disableButton("Choose FTP file");this.modal.disableButton("Paste/Fetch data")}if(this.current_user&&this.options.ftp_upload_dir&&this.options.ftp_upload_site){this.modal.showButton("Choose FTP file")}else{this.modal.hideButton("Choose FTP file")}if(this.counter.announce+this.counter.success+this.counter.error>0){$(this.el).find("#upload-table").show()}else{$(this.el).find("#upload-table").hide()}},_upload_percentage:function(h,i){return(this.upload_completed+(h*i))/this.upload_size},_template:function(i,h){return'
'}})}); \ No newline at end of file +define(["utils/utils","mvc/upload/upload-button","mvc/upload/upload-model","mvc/upload/upload-row","mvc/upload/upload-ftp","mvc/ui/ui-popover","mvc/ui/ui-modal","utils/uploadbox"],function(f,e,c,b,g,d,a){return Backbone.View.extend({options:{nginx_upload_path:""},modal:null,ui_button:null,uploadbox:null,current_history:null,upload_size:0,list_extensions:[],list_genomes:[],auto:{id:"auto",text:"Auto-detect",description:"This system will try to detect the file type automatically. If your file is not detected properly as one of the known formats, it most likely means that it has some format problems (e.g., different number of columns on different rows). You can still coerce the system to set your data to the format you think it should be. You can also upload compressed files, which will automatically be decompressed."},collection:new c.Collection(),ftp:null,counter:{announce:0,success:0,error:0,running:0,reset:function(){this.announce=this.success=this.error=this.running=0}},initialize:function(i){var h=this;if(i){this.options=_.defaults(i,this.options)}this.ui_button=new e.Model({icon:"fa-upload",tooltip:"Download from URL or upload files from disk",label:"Load Data",onclick:function(j){if(j){j.preventDefault();h.show()}},onunload:function(){if(h.counter.running>0){return"Several uploads are still processing."}}});$(".with-upload-button").append((new e.View(this.ui_button)).$el);var h=this;f.get({url:galaxy_config.root+"api/datatypes?extension_only=False",success:function(j){for(key in j){h.list_extensions.push({id:j[key].extension,text:j[key].extension,description:j[key].description,description_url:j[key].description_url})}h.list_extensions.sort(function(l,k){return l.id>k.id?1:l.idk.id?1:l.id0)){return null}this.uploadbox.configure({url:this.options.nginx_upload_path});if(h=="local"){this.uploadbox.configure({paramname:"files_0|file_data"})}else{this.uploadbox.configure({paramname:null})}tool_input={};if(h=="new"){tool_input["files_0|url_paste"]=q}if(h=="ftp"){tool_input["files_0|ftp_files"]=n}tool_input.dbkey=r;tool_input.file_type=p;tool_input["files_0|type"]="upload_dataset";tool_input.space_to_tabs=l;tool_input.to_posix_lines=i;data={};data.history_id=this.current_history;data.tool_id="upload1";data.inputs=JSON.stringify(tool_input);return data},_eventProgress:function(i,j,h){var k=this.collection.get(i);k.set("percentage",h);this.ui_button.set("percentage",this._upload_percentage(h,j.size))},_eventSuccess:function(i,j,l){var k=this.collection.get(i);k.set("percentage",100);k.set("status","success");var h=k.get("file_size");this.ui_button.set("percentage",this._upload_percentage(100,h));this.upload_completed+=h*100;this.counter.announce--;this.counter.success++;this._updateScreen();Galaxy.currHistoryPanel.refreshContents()},_eventError:function(h,i,k){var j=this.collection.get(h);j.set("percentage",100);j.set("status","error");j.set("info",k);this.ui_button.set("percentage",this._upload_percentage(100,i.size));this.ui_button.set("status","danger");this.upload_completed+=i.size*100;this.counter.announce--;this.counter.error++;this._updateScreen()},_eventComplete:function(){this.collection.each(function(h){if(h.get("status")=="queued"){h.set("status","init")}});this.counter.running=0;this._updateScreen()},_eventFtp:function(){if(!this.ftp.visible){this.ftp.empty();this.ftp.append((new g(this)).$el);this.ftp.show()}else{this.ftp.hide()}},_eventCreate:function(){this.uploadbox.add([{name:"New File",size:0,mode:"new"}])},_eventStart:function(){if(this.counter.announce==0||this.counter.running>0){return}var h=this;this.upload_size=0;this.upload_completed=0;this.collection.each(function(i){if(i.get("status")=="init"){i.set("status","queued");h.upload_size+=i.get("file_size")}});this.ui_button.set("percentage",0);this.ui_button.set("status","success");this.counter.running=this.counter.announce;this._updateScreen();this.uploadbox.start()},_eventStop:function(){if(this.counter.running==0){return}this.ui_button.set("status","info");this.uploadbox.stop();$("#upload-info").html("Queue will pause after completing the current file...")},_eventReset:function(){if(this.counter.running==0){this.collection.reset();this.counter.reset();this._updateScreen();this.uploadbox.reset();this.ui_button.set("percentage",0)}},_updateUser:function(){this.current_user=Galaxy.currUser.get("id");this.current_history=null;if(this.current_user){this.current_history=Galaxy.currHistoryPanel.model.get("id")}},_updateScreen:function(){if(this.counter.announce==0){if(this.uploadbox.compatible()){message="You can Drag & Drop files into this box."}else{message="Unfortunately, your browser does not support multiple file uploads or drag&drop.
Some supported browsers are: Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+."}}else{if(this.counter.running==0){message="You added "+this.counter.announce+" file(s) to the queue. Add more files or click 'Start' to proceed."}else{message="Please wait..."+this.counter.announce+" out of "+this.counter.running+" remaining."}}$("#upload-info").html(message);if(this.counter.running==0&&this.counter.announce+this.counter.success+this.counter.error>0){this.modal.enableButton("Reset")}else{this.modal.disableButton("Reset")}if(this.counter.running==0&&this.counter.announce>0){this.modal.enableButton("Start")}else{this.modal.disableButton("Start")}if(this.counter.running>0){this.modal.enableButton("Pause")}else{this.modal.disableButton("Pause")}if(this.counter.running==0){this.modal.enableButton("Choose local file");this.modal.enableButton("Choose FTP file");this.modal.enableButton("Paste/Fetch data")}else{this.modal.disableButton("Choose local file");this.modal.disableButton("Choose FTP file");this.modal.disableButton("Paste/Fetch data")}if(this.current_user&&this.options.ftp_upload_dir&&this.options.ftp_upload_site){this.modal.showButton("Choose FTP file")}else{this.modal.hideButton("Choose FTP file")}if(this.counter.announce+this.counter.success+this.counter.error>0){$(this.el).find("#upload-table").show()}else{$(this.el).find("#upload-table").hide()}},_upload_percentage:function(h,i){return(this.upload_completed+(h*i))/this.upload_size},_template:function(i,h){return'
'}})}); \ No newline at end of file diff --git a/templates/webapps/galaxy/root/index.mako b/templates/webapps/galaxy/root/index.mako index 4e36be60ca3..d4514e7c125 100644 --- a/templates/webapps/galaxy/root/index.mako +++ b/templates/webapps/galaxy/root/index.mako @@ -58,7 +58,7 @@ <%def name="left_panel()">
-
+
${n_('Tools')}
From a7bb23e8c847898323516761c069fd5efa8b358d Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Sun, 11 Jan 2015 13:16:00 -0500 Subject: [PATCH 150/473] Masthead: Lower icons by one pixel --- static/style/blue/base.css | 2 +- static/style/src/less/base.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/style/blue/base.css b/static/style/blue/base.css index 5b94e50ff7c..cd41ac92d68 100644 --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1742,7 +1742,7 @@ div.unified-panel-body{position:absolute;top:30px;bottom:0;width:100%} #masthead li.dropdown>a:hover .caret{border-top-color:gold;border-bottom-color:gold} #masthead .navbar-brand{position:absolute;left:0;top:0;font-family:verdana;font-weight:bold;font-size:20px;line-height:1;color:white;padding:5px 20px 12px;margin-left:-15px;z-index:2000}#masthead .navbar-brand img{display:inline;width:26px;vertical-align:top} #masthead .navbar-brand a{color:white;text-decoration:none} -#masthead .iconbar{position:absolute;top:6px;right:110px;cursor:pointer;color:#999;overflow:hidden}#masthead .iconbar .symbol{float:left;margin:0px 10px;height:26px;font-size:11px} +#masthead .iconbar{position:absolute;top:7px;right:110px;cursor:pointer;color:#999;overflow:hidden}#masthead .iconbar .symbol{float:left;margin:0px 10px;height:26px;font-size:11px} #masthead .iconbar .symbol .number{font-weight:bold;font-size:10px;font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;position:relative;left:23px;top:-12px} #masthead .iconbar .toggle{color:gold} .quota-meter-container{position:absolute;top:0;right:0;height:32px} diff --git a/static/style/src/less/base.less b/static/style/src/less/base.less index a1f9b4b3095..076aca221fd 100644 --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -425,7 +425,7 @@ div.unified-panel-body { .iconbar { position : absolute; - top : 6px; + top : 7px; right : 110px; cursor : pointer; color : @gray-light; From 46f2679e9d8209b8becfb0b97060296d2da1fb38 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 11 Jan 2015 21:32:50 -0500 Subject: [PATCH 151/473] Tool Tests - Only set library import dir if it exists. Fixes planemo #41 (https://github.com/galaxyproject/planemo/issues/41). --- scripts/functional_tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/functional_tests.py b/scripts/functional_tests.py index 8196013af9c..a1aff16f495 100644 --- a/scripts/functional_tests.py +++ b/scripts/functional_tests.py @@ -225,7 +225,11 @@ def main(): if not os.path.isabs( galaxy_test_file_dir ): galaxy_test_file_dir = os.path.join( os.getcwd(), galaxy_test_file_dir ) library_import_dir = galaxy_test_file_dir - user_library_import_dir = os.path.join( galaxy_test_file_dir, 'users' ) + import_dir = os.path.join( galaxy_test_file_dir, 'users' ) + if os.path.exists(import_dir): + user_library_import_dir = import_dir + else: + user_library_import_dir = None ignore_files = () start_server = 'GALAXY_TEST_EXTERNAL' not in os.environ From 3c76664ef893d506530b39427578f89922e369b4 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 11 Jan 2015 21:43:13 -0500 Subject: [PATCH 152/473] Source the common startup script for run_tests.sh. Fixes planemo #50. --- run_tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run_tests.sh b/run_tests.sh index 5f5924736e4..91bf74ae532 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,5 +1,7 @@ #!/bin/sh +./scripts/common_startup.sh + # A good place to look for nose info: http://somethingaboutorange.com/mrl/projects/nose/ rm -f run_functional_tests.log From 40427b13644e5b793953e272a79f8b5534e7ba70 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Mon, 12 Jan 2015 10:51:00 -0500 Subject: [PATCH 153/473] Move param parsing down into base controllers, normalize manager object names in api and web controllers --- lib/galaxy/managers/hdas.py | 39 +-------- lib/galaxy/managers/sharable.py | 7 ++ lib/galaxy/managers/users.py | 1 - lib/galaxy/web/base/controller.py | 14 ++++ lib/galaxy/webapps/galaxy/api/annotations.py | 4 +- .../webapps/galaxy/api/dataset_collections.py | 2 +- lib/galaxy/webapps/galaxy/api/datasets.py | 17 ++-- .../webapps/galaxy/api/extended_metadata.py | 2 +- .../webapps/galaxy/api/folder_contents.py | 2 +- lib/galaxy/webapps/galaxy/api/histories.py | 46 ++++------- .../webapps/galaxy/api/history_contents.py | 55 +++++-------- lib/galaxy/webapps/galaxy/api/jobs.py | 6 +- .../webapps/galaxy/api/library_contents.py | 4 +- lib/galaxy/webapps/galaxy/api/provenance.py | 12 +-- lib/galaxy/webapps/galaxy/api/tools.py | 21 +++-- lib/galaxy/webapps/galaxy/api/workflows.py | 23 ++---- .../webapps/galaxy/controllers/dataset.py | 80 +++++++++---------- .../webapps/galaxy/controllers/history.py | 43 ++++------ lib/galaxy/webapps/galaxy/controllers/page.py | 40 +++++----- lib/galaxy/webapps/galaxy/controllers/root.py | 8 +- .../galaxy/controllers/visualization.py | 30 ++++--- 21 files changed, 186 insertions(+), 270 deletions(-) diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 60ef428d2cb..4c5fc4dbc6a 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -251,44 +251,6 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface dataset_data = None return truncated, dataset_data - #def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ): - # """ - # Get an HDA object by id performing security checks using - # the current transaction. - # """ - # try: - # dataset_id = trans.security.decode_id( dataset_id ) - # except ( AttributeError, TypeError ): - # # DEPRECATION: We still support unencoded ids for backward compatibility - # try: - # dataset_id = int( dataset_id ) - # except ValueError: - # raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) ) - # - # try: - # data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) ) - # except: - # raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) ) - # - # if check_ownership: - # # Verify ownership. - # user = trans.get_user() - # if not user: - # error( "Must be logged in to manage Galaxy items" ) - # if data.history.user != user: - # error( "%s is not owned by current user" % data.__class__.__name__ ) - # - # if check_accessible: - # current_user_roles = trans.get_current_user_roles() - # - # if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ): - # error( "You are not allowed to access this dataset" ) - # - # if check_state and data.state == trans.model.Dataset.states.UPLOAD: - # return trans.show_error_message( "Please wait until this dataset finishes uploading " - # + "before attempting to view it." ) - # return data - # this is a weird syntax and return val def data_conversion_status( self, trans, hda ): """ @@ -322,6 +284,7 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface return True return False + # ============================================================================= class HDASerializer( base.ModelSerializer ): #TODO: inherit from datasets.DatasetAssociationSerializer diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index fa4cedb1fb5..cff3c80d328 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -340,6 +340,13 @@ class SharableModelSerializer( base.ModelSerializer ): # user/user_id # user_shares? # username_and_slug? + #def get_name_and_link_async( self, trans, id=None ): + #def published_url( self, trans, item, key ): + # """ + # """ + # url = url_for(controller='history', action="display_by_username_and_slug", + # username=item.user.username, slug=item.slug ) + # return url # ============================================================================= diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 8045375bd23..449e23915ac 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -30,7 +30,6 @@ class UserManager( base.ModelManager ): #TODO: incorp CreatesUsersMixin #TODO: incorp CreatesApiKeysMixin - def create( self, trans, **kwargs ): """ Create a new user. diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index b988674e416..6dd365eb2d3 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -99,6 +99,13 @@ class BaseController( object ): def get_role( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ): return self.get_object( trans, id, 'Role', check_ownership=False, check_accessible=False, deleted=deleted ) + def decode_id( self, id ): + try: + return self.app.security.decode_id( id ) + except: + msg = "Malformed History id ( %s ) specified, unable to decode" % ( str( id ) ) + raise exceptions.MalformedId( msg, id=str( id ) ) + def encode_all_ids( self, trans, rval, recursive=False ): """ Encodes all integer values in the dict rval whose keys are 'id' or end with '_id' @@ -174,6 +181,13 @@ class BaseAPIController( BaseController ): def not_implemented( self, trans, **kwd ): raise HTTPNotImplemented() + def _parse_serialization_params( self, kwd, default_view ): + view = kwd.get( 'view', None ) + keys = kwd.get( 'keys' ) + if isinstance( keys, basestring ): + keys = keys.split( ',' ) + return dict( view=view, keys=keys, default_view=default_view ) + class Datatype( object ): """Used for storing in-memory list of datatypes currently in the datatypes registry.""" diff --git a/lib/galaxy/webapps/galaxy/api/annotations.py b/lib/galaxy/webapps/galaxy/api/annotations.py index a7fcf5f84af..a5a7b6ccd03 100644 --- a/lib/galaxy/webapps/galaxy/api/annotations.py +++ b/lib/galaxy/webapps/galaxy/api/annotations.py @@ -58,7 +58,7 @@ class HistoryAnnotationsController(BaseAnnotationsController): self.history_manager = managers.histories.HistoryManager( app ) def _get_item_from_id(self, trans, idstr): - decoded_idstr = trans.security.decode_id( idstr ) + decoded_idstr = self.decode_id( idstr ) history = self.history_manager.accessible_by_id( trans, decoded_idstr, trans.user ) return history @@ -72,7 +72,7 @@ class HistoryContentAnnotationsController(BaseAnnotationsController): self.hda_manager = managers.hdas.HDAManager( app ) def _get_item_from_id(self, trans, idstr): - decoded_idstr = trans.security.decode_id( idstr ) + decoded_idstr = self.decode_id( idstr ) hda = self.hda_manager.accessible_by_id( trans, decoded_idstr, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) return hda diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index 08193bf4476..76f70d98d1c 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -44,7 +44,7 @@ class DatasetCollectionsController( instance_type = payload.pop( "instance_type", "history" ) if instance_type == "history": history_id = payload.get( 'history_id' ) - history_id = trans.security.decode_id( history_id ) + history_id = self.decode_id( history_id ) history = self.history_manager.owned_by_id( trans, history_id, trans.user ) create_params[ "parent" ] = history elif instance_type == "library": diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index f2eaf1aaba3..e0f2d4beb7a 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -22,10 +22,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): def __init__( self, app ): super( DatasetsController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ), - hdas=managers.hdas.HDAManager( app ) - ) + self.hda_manager = managers.hdas.HDAManager( app ) self.hda_serializer = managers.hdas.HDASerializer( self.app ) @web.expose_api @@ -83,7 +80,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): """ Returns state of dataset. """ - msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( trans, dataset ) if not msg: msg = dataset.conversion_messages.DATA @@ -94,7 +91,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): Init-like method that returns state of dataset's converted datasets. Returns valid chroms for that dataset as well. """ - msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( trans, dataset ) if msg: return msg @@ -143,7 +140,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): return dataset.conversion_messages.NO_DATA # Dataset check. - msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( trans, dataset ) if msg: return msg @@ -236,7 +233,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): be slow because indexes need to be created. """ # Dataset check. - msg = self.mgrs.hdas.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( trans, dataset ) if msg: return msg @@ -278,12 +275,12 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): some point in the future without warning. Generally, data should be processed by its datatype prior to display (the defult if raw is unspecified or explicitly false. """ - decoded_content_id = trans.security.decode_id( history_content_id ) + decoded_content_id = self.decode_id( history_content_id ) raw = util.string_as_bool_or_none( raw ) rval = '' try: - hda = self.mgrs.hdas.accessible_by_id( trans, decoded_content_id, trans.user ) + hda = self.hda_manager.accessible_by_id( trans, decoded_content_id, trans.user ) if raw: if filename and filename != 'index': diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index 63b78cae1ac..bf8684504ae 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -69,7 +69,7 @@ class HistoryDatasetExtendMetadataController(BaseExtendedMetadataController): self.hda_manager = managers.hdas.HDAManager( app ) def _get_item_from_id(self, trans, idstr, check_writable=True): - decoded_idstr = trans.security.decode_id( idstr ) + decoded_idstr = self.decode_id( idstr ) if check_writable: return self.hda_manager.owned_by_id( trans, decoded_idstr, trans.user ) else: diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index ff2815c226c..cbce170d266 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -267,7 +267,7 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary ldda_message = util.sanitize_html.sanitize_html( ldda_message, 'utf-8' ) rval = {} try: - decoded_hda_id = trans.security.decode_id( from_hda_id ) + decoded_hda_id = self.decode_id( from_hda_id ) hda = self.hda_manager.owned_by_id( trans, decoded_hda_id, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) folder = self.get_library_folder( trans, encoded_folder_id_16, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 61181e3f2d7..ca3e0134b86 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -32,26 +32,10 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor def __init__( self, app ): super( HistoriesController, self ).__init__( app ) self.citations_manager = citations.CitationsManager( app ) - self.mgrs = util.bunch.Bunch( - histories=histories.HistoryManager( app ) - ) + self.history_manager = histories.HistoryManager( app ) self.history_serializer = histories.HistorySerializer( app ) self.history_deserializer = histories.HistoryDeserializer( app ) - def _decode_id( self, trans, id ): - try: - return trans.security.decode_id( id ) - except: - raise exceptions.MalformedId( "Malformed History id ( %s ) specified, unable to decode" - % ( str( id ) ), type='error' ) - - def _parse_serialization_params( self, kwd, default_view ): - view = kwd.get( 'view', None ) - keys = kwd.get( 'keys' ) - if isinstance( keys, basestring ): - keys = keys.split( ',' ) - return dict( view=view, keys=keys, default_view=default_view ) - @expose_api_anonymous def index( self, trans, deleted='False', **kwd ): """ @@ -75,7 +59,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor if string_as_bool( deleted ): deleted_filter = ( self.app.model.History.deleted == True ) - histories = self.mgrs.histories.by_user( trans, user=trans.user, filters=deleted_filter ) + histories = self.history_manager.by_user( trans, user=trans.user, filters=deleted_filter ) for history in histories: history_dict = self.history_serializer.serialize_to_view( trans, history, **serialization_params ) rval.append( history_dict ) @@ -105,10 +89,10 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor deleted = string_as_bool( deleted ) if history_id == "most_recently_used": - history = self.mgrs.histories.most_recent( trans, trans.user, + history = self.history_manager.most_recent( trans, trans.user, filters=( self.app.model.History.deleted == False ) ) else: - history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) return self.history_serializer.serialize_to_view( trans, history, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -117,7 +101,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor def citations( self, trans, history_id, **kwd ): """ """ - history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) tool_ids = set([]) for dataset in history.datasets: job = dataset.creating_job @@ -161,8 +145,8 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor new_history = None # if a history id was passed, copy that history if copy_this_history_id: - decoded_id = self._decode_id( trans, copy_this_history_id ) - original_history = self.mgrs.histories.accessible_by_id( trans, decoded_id, trans.user ) + decoded_id = self.decode_id( copy_this_history_id ) + original_history = self.history_manager.accessible_by_id( trans, decoded_id, trans.user ) hist_name = hist_name or ( "Copy of '%s'" % original_history.name ) new_history = original_history.copy( name=hist_name, target_user=trans.user ) @@ -204,10 +188,10 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor if kwd.get( 'payload', None ): purge = string_as_bool( kwd['payload'].get( 'purge', False ) ) - history = self.mgrs.histories.owned_by_id( trans, self._decode_id( trans, history_id ), trans.user ) - self.mgrs.histories.delete( trans, history ) + history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) + self.history_manager.delete( trans, history ) if purge: - self.mgrs.histories.purge( trans, history ) + self.history_manager.purge( trans, history ) return self.history_serializer.serialize_to_view( trans, new_history, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -226,8 +210,8 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor :returns: 'OK' if the history was undeleted """ history_id = id - history = self.mgrs.histories.owned_by_id( trans, self._decode_id( trans, history_id ), trans.user ) - self.mgrs.histories.undelete( trans, history ) + history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) + self.history_manager.undelete( trans, history ) return self.history_serializer.serialize_to_view( trans, history, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -252,7 +236,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor any values that were different from the original and, therefore, updated """ #TODO: PUT /api/histories/{encoded_history_id} payload = { rating: rating } (w/ no security checks) - history = self.mgrs.histories.owned_by_id( trans, self._decode_id( trans, id ), trans.user ) + history = self.history_manager.owned_by_id( trans, self.decode_id( id ), trans.user ) #TODO: flushing in deserialize is an iffy pattern... self.history_deserializer.deserialize( trans, history, payload ) @@ -275,7 +259,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # PUT instead of POST because multiple requests should just result # in one object being created. - history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) + history = self.history_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) jeha = history.latest_export up_to_date = jeha and jeha.up_to_date if 'force' in kwds: @@ -308,7 +292,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # Seems silly to put jeha_id in here, but want GET to be immuatable? # and this is being accomplished this way. - history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) + history = self.history_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) matching_exports = filter( lambda e: trans.security.encode_id( e.id ) == jeha_id, history.exports ) if not matching_exports: raise exceptions.ObjectNotFound() diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 24031600792..4f95c014de1 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -29,20 +29,11 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar def __init__( self, app ): super( HistoryContentsController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=histories.HistoryManager( app ), - hdas=hdas.HDAManager( app ) - ) + self.hda_manager = hdas.HDAManager( app ) + self.history_manager = histories.HistoryManager( app ) self.hda_serializer = hdas.HDASerializer( app ) self.hda_deserializer = hdas.HDADeserializer( app ) - def _decode_id( self, trans, id ): - try: - return trans.security.decode_id( id ) - except: - raise exceptions.MalformedId( "Malformed History id ( %s ) specified, unable to decode" - % ( str( id ) ), type='error' ) - def _parse_serialization_params( self, kwd, default_view ): view = kwd.get( 'view', None ) keys = kwd.get( 'keys' ) @@ -77,13 +68,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar """ rval = [] - # get the history, if anon user and requesting current history - allow it - if( ( trans.user == None ) - and ( history_id == trans.security.encode_id( trans.history.id ) ) ): - history = trans.history - # otherwise, check permissions for the history first - else: - history = self.mgrs.histories.accessible_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) # Allow passing in type or types - for continuity rest of methods # take in type - but this one can be passed multiple types and @@ -96,7 +81,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar contents_kwds = { 'types': types } if ids: - ids = map( lambda id: trans.security.decode_id( id ), ids.split( ',' ) ) + ids = map( lambda id: self.decode_id( id ), ids.split( ',' ) ) contents_kwds[ 'ids' ] = ids # If explicit ids given, always used detailed result. details = 'all' @@ -156,9 +141,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar return self.__handle_unknown_contents_type( trans, contents_type ) def __show_dataset( self, trans, id, **kwd ): - hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) - #if hda.history.id != self._decode_id( trans, history_id ): - # raise exceptions.ObjectNotFound( 'dataset was not found in this history' ) + hda = self.hda_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) return self.hda_serializer.serialize_to_view( trans, hda, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -229,7 +212,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # TODO: Flush out create new collection documentation above, need some # examples. See also bioblend and API tests for specific examples. - history = self.mgrs.histories.owned_by_id( trans, self._decode_id( trans, history_id ), trans.user ) + history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) type = payload.get( 'type', 'dataset' ) if type == 'dataset': @@ -261,8 +244,8 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # copy an existing, accessible hda elif source == 'hda': - unencoded_hda_id = self._decode_id( trans, content ) - original = self.mgrs.hdas.accessible_by_id( trans, unencoded_hda_id, trans.user ) + unencoded_hda_id = self.decode_id( content ) + original = self.hda_manager.accessible_by_id( trans, unencoded_hda_id, trans.user ) data_copy = original.copy( copy_children=True ) hda = history.add_dataset( data_copy ) @@ -332,10 +315,10 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # anon user: ensure that history ids match up and the history is the current, # check for uploading, and use only the subset of attribute keys manipulatable by anon users if trans.user == None: - hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ) ) + hda = self.hda_manager.by_id( trans, self.decode_id( id ) ) if hda.history != trans.history: raise exceptions.AuthenticationRequired( 'You must be logged in to update this dataset' ) - hda = self.mgrs.hdas.error_if_uploading( trans, hda ) + hda = self.hda_manager.error_if_uploading( trans, hda ) anon_allowed_payload = {} if 'deleted' in payload: @@ -346,13 +329,13 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # logged in user: use full payload, check state if deleting, and make sure the history is theirs else: - hda = self.mgrs.hdas.accessible_by_id( trans, self._decode_id( trans, id ), trans.user ) - self.mgrs.histories.error_unless_owner( trans, hda.history, trans.user ) + hda = self.hda_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) + self.history_manager.error_unless_owner( trans, hda.history, trans.user ) # only check_state if not deleting, otherwise cannot delete uploading files check_state = not payload.get( 'deleted', False ) if check_state: - hda = self.mgrs.hdas.error_if_uploading( trans, hda ) + hda = self.hda_manager.error_if_uploading( trans, hda ) # make the actual changes #TODO: is this if still needed? @@ -360,7 +343,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar self.hda_deserializer.deserialize( trans, hda, payload ) #TODO: this should be an effect of deleting the hda if payload.get( 'deleted', False ): - self.mgrs.hdas.stop_creating_job( hda ) + self.hda_manager.stop_creating_job( hda ) return self.hda_serializer.serialize_to_view( trans, hda, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -413,14 +396,14 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # payload takes priority purge = util.string_as_bool( kwd['payload'].get( 'purge', purge ) ) - hda = self.mgrs.hdas.by_id( trans, self._decode_id( trans, id ), trans.user ) - self.mgrs.histories.error_unless_ownership( trans, hda.history, trans.user ) - self.mgrs.hdas.error_if_uploading( trans, hda ) + hda = self.hda_manager.by_id( trans, self.decode_id( id ), trans.user ) + self.history_manager.error_unless_ownership( trans, hda.history, trans.user ) + self.hda_manager.error_if_uploading( trans, hda ) if purge: - self.mgrs.hdas.purge( trans, hda ) + self.hda_manager.purge( trans, hda ) else: - self.mgrs.hdas.delete( trans, hda ) + self.hda_manager.delete( trans, hda ) return self.hda_serializer.serialize_to_view( trans, hda, **self._parse_serialization_params( kwd, 'detailed' ) ) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 6517be6846a..d39531e1d94 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -89,7 +89,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): history_id = kwd.get( 'history_id', None ) if history_id is not None: try: - decoded_history_id = trans.security.decode_id(history_id) + decoded_history_id = self.decode_id(history_id) query = query.filter( trans.app.model.Job.history_id == decoded_history_id ) except: raise exceptions.ObjectAttributeInvalidException() @@ -199,7 +199,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): def __get_job( self, trans, id ): try: - decoded_job_id = trans.security.decode_id( id ) + decoded_job_id = self.decode_id( id ) except Exception: raise exceptions.MalformedId() query = trans.sa_session.query( trans.app.model.Job ) @@ -260,7 +260,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): if isinstance( v, dict ): if 'id' in v: if 'src' not in v or v[ 'src' ] == 'hda': - hda_id = trans.security.decode_id( v['id'] ) + hda_id = self.decode_id( v['id'] ) dataset = self.hda_manager.accessible_by_id( trans, hda_id, trans.user ) else: dataset = self.get_library_dataset_dataset_association( trans, v['id'] ) diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 7c34263835c..692ada19d46 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -72,7 +72,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar rval.append( ld ) return rval try: - decoded_library_id = trans.security.decode_id( library_id ) + decoded_library_id = self.decode_id( library_id ) except Exception: raise exceptions.MalformedId( 'Malformed library id ( %s ) specified, unable to decode.' % library_id ) try: @@ -302,7 +302,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar try: # check permissions on (all three?) resources: hda, library, folder #TODO: do we really need the library?? - from_hda_id = trans.security.decode_id( from_hda_id ) + from_hda_id = self.decode_id( from_hda_id ) hda = self.hda_manager.owned_by_id( trans, from_hda_id, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) # library = self.get_library( trans, library_id, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/provenance.py b/lib/galaxy/webapps/galaxy/api/provenance.py index c42d8e5bdf5..8488540e659 100644 --- a/lib/galaxy/webapps/galaxy/api/provenance.py +++ b/lib/galaxy/webapps/galaxy/api/provenance.py @@ -5,7 +5,7 @@ import logging from galaxy import web from galaxy.web.base.controller import BaseAPIController from paste.httpexceptions import HTTPNotImplemented, HTTPBadRequest -from galaxy.managers import hdas +from galaxy import managers log = logging.getLogger( __name__ ) @@ -15,7 +15,7 @@ class BaseProvenanceController( BaseAPIController ): """ def __init__( self, app ): super( BaseProvenanceController, self ).__init__( app ) - self.hdas = hdas.HDAManager( app ) + self.hda_manager = managers.hdas.HDAManager( app ) @web.expose_api def index( self, trans, **kwd ): @@ -38,13 +38,13 @@ class BaseProvenanceController( BaseAPIController ): def delete( self, trans, tag_name, **kwd ): raise HTTPBadRequest("Cannot Delete Provenance") - def _get_provenance(self, trans, item_class_name, item_id, follow=True): + def _get_provenance( self, trans, item_class_name, item_id, follow=True ): provenance_item = self.get_object( trans, item_id, item_class_name, check_ownership=False, check_accessible=False) if item_class_name == "HistoryDatasetAssociation": - self.hdas.check_accessible(trans, provenance_item) + self.hda_manager.error_unless_accessible( trans, provenance_item ) else: - self.security_check(trans, provenance_item, check_accessible=True) - out = self._get_record(trans, provenance_item, follow) + self.security_check( trans, provenance_item, check_accessible=True ) + out = self._get_record( trans, provenance_item, follow ) return out def _get_record(self, trans, item, follow): diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 68453d656bb..f5ca7c7c93f 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -31,10 +31,8 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): def __init__( self, app ): super( ToolsController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ), - hdas=managers.hdas.HDAManager( app ) - ) + self.history_manager = managers.histories.HistoryManager( app ) + self.hda_manager = managers.hdas.HDAManager( app ) @web.expose_api def index( self, trans, **kwds ): @@ -47,7 +45,6 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): including sections and labels trackster - if true, only tools that are compatible with Trackster are returned - """ # Read params. @@ -144,8 +141,8 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): # dataset upload. history_id = payload.get("history_id", None) if history_id: - decoded_id = trans.security.decode_id( history_id ) - target_history = self.mgrs.histories.owned_by_id( trans, decoded_id, trans.user ) + decoded_id = self.decode_id( history_id ) + target_history = self.history_manager.owned_by_id( trans, decoded_id, trans.user ) else: target_history = None @@ -160,7 +157,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): input_patch = {} for k, v in inputs.iteritems(): if isinstance(v, dict) and v.get('src', '') == 'ldda' and 'id' in v: - ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id(v['id']) ) + ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( self.decode_id(v['id']) ) if trans.user_is_admin() or trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), ldda.dataset ): input_patch[k] = ldda.to_history_dataset_association(target_history, add_to_history=True) @@ -283,10 +280,10 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): run_on_regions = True # Dataset check. - decoded_dataset_id = trans.security.decode_id( payload.get( 'target_dataset_id' ) ) - original_dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_dataset_id, user=trans.user ) + decoded_dataset_id = self.decode_id( payload.get( 'target_dataset_id' ) ) + original_dataset = self.hda_manager.accessible_by_id( trans, decoded_dataset_id, user=trans.user ) original_dataset = self.hda_manager.error_if_uploading( trans, original_dataset ) - msg = self.mgrs.hdas.data_conversion_status( trans, original_dataset ) + msg = self.hda_manager.data_conversion_status( trans, original_dataset ) if msg: return msg @@ -294,7 +291,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): # job's previous parameters and incoming parameters. Incoming parameters # have priority. # - original_job = self.mgrs.hdas.creating_job( original_dataset ) + original_job = self.hda_manager.creating_job( original_dataset ) tool = trans.app.toolbox.get_tool( original_job.tool_id ) if not tool: return trans.app.model.Dataset.conversion_messages.NO_TOOL diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 0dcebd52ae1..bebf183480f 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -148,10 +148,10 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn if 'from_history_id' in payload: from_history_id = payload.get( 'from_history_id' ) - from_history_id = trans.security.decode_id( from_history_id ) + from_history_id = self.decode_id( from_history_id ) history = self.history_manager.accessible_by_id( trans, from_history_id, trans.user ) - job_ids = map( trans.security.decode_id, payload.get( 'job_ids', [] ) ) + job_ids = map( self.decode_id, payload.get( 'job_ids', [] ) ) dataset_ids = payload.get( 'dataset_ids', [] ) dataset_collection_ids = payload.get( 'dataset_collection_ids', [] ) workflow_name = payload[ 'workflow_name' ] @@ -242,7 +242,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn workflow_id = id try: - stored_workflow = trans.sa_session.query(self.app.model.StoredWorkflow).get(trans.security.decode_id(workflow_id)) + stored_workflow = trans.sa_session.query(self.app.model.StoredWorkflow).get(self.decode_id(workflow_id)) except Exception, e: trans.response.status = 400 return ("Workflow with ID='%s' can not be found\n Exception: %s") % (workflow_id, str( e )) @@ -431,7 +431,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_workflow_invocation_id = self.__decode_id( trans, invocation_id ) + decoded_workflow_invocation_id = self.decode_id( invocation_id ) workflow_invocation = self.workflow_manager.get_invocation( trans, decoded_workflow_invocation_id ) if workflow_invocation: return self.__encode_invocation( trans, workflow_invocation ) @@ -451,7 +451,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_workflow_invocation_id = self.__decode_id( trans, invocation_id ) + decoded_workflow_invocation_id = self.decode_id( invocation_id ) workflow_invocation = self.workflow_manager.cancel_invocation( trans, decoded_workflow_invocation_id ) return self.__encode_invocation( trans, workflow_invocation ) @@ -474,7 +474,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_invocation_step_id = self.__decode_id( trans, step_id ) + decoded_invocation_step_id = self.decode_id( step_id ) invocation_step = self.workflow_manager.get_invocation_step( trans, decoded_invocation_step_id @@ -501,7 +501,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn :raises: exceptions.MessageException, exceptions.ObjectNotFound """ - decoded_invocation_step_id = self.__decode_id( trans, step_id ) + decoded_invocation_step_id = self.decode_id( step_id ) action = payload.get( "action", None ) invocation_step = self.workflow_manager.update_invocation_step( @@ -540,7 +540,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn if stored_workflow is None: raise exceptions.ObjectNotFound( "Workflow not found: %s" % workflow_id ) else: - workflow_id = self.__decode_id( trans, workflow_id ) + workflow_id = self.decode_id( workflow_id ) query = trans.sa_session.query( trans.app.model.StoredWorkflow ) stored_workflow = query.get( workflow_id ) if stored_workflow is None: @@ -553,10 +553,3 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn invocation.to_dict( view ), True ) - - def __decode_id( self, trans, workflow_id, model_type="workflow" ): - try: - return trans.security.decode_id( workflow_id ) - except Exception: - message = "Malformed %s id ( %s ) specified, unable to decode" % ( model_type, workflow_id ) - raise exceptions.MalformedId( message ) diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index 225ae28d8fe..bfc464ba8c6 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -92,17 +92,15 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def __init__( self, app ): super( DatasetInterface, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ), - hdas=managers.hdas.HDAManager( app ), - ) + self.history_manager = managers.histories.HistoryManager( app ) + self.hda_manager = managers.hdas.HDAManager( app ) def _get_job_for_dataset( self, trans, dataset_id ): ''' Return the job for the given dataset. This will throw an error if the dataset is either nonexistent or inaccessible to the user. ''' - hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( self.decode_id( dataset_id ) ) assert hda and self._can_access_dataset( trans, hda ) return hda.creating_job @@ -119,7 +117,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses except: hda = None if not hda: - hda = trans.sa_session.query( model.HistoryDatasetAssociation ).get( trans.security.decode_id( id ) ) + hda = trans.sa_session.query( model.HistoryDatasetAssociation ).get( self.decode_id( id ) ) if not hda or not self._can_access_dataset( trans, hda ): return trans.show_error_message( "Either this dataset does not exist or you do not have permission to access it." ) return trans.fill_template( "dataset/errors.mako", hda=hda ) @@ -177,7 +175,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.expose def get_metadata_file(self, trans, hda_id, metadata_name): """ Allows the downloading of metadata files associated with datasets (eg. bai index for bam files) """ - data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( trans.security.decode_id( hda_id ) ) + data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( self.decode_id( hda_id ) ) if not data or not self._can_access_dataset( trans, data ): return trans.show_error_message( "You are not allowed to access this dataset" ) @@ -192,7 +190,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def _check_dataset(self, trans, hda_id): # DEPRECATION: We still support unencoded ids for backward compatibility try: - data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( trans.security.decode_id( hda_id) ) + data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( self.decode_id( hda_id) ) if data is None: raise ValueError( 'Invalid reference dataset id: %s.' % hda_id) except: @@ -463,7 +461,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses # Load the hdas and ensure they all belong to the current user hdas = [] for encoded_hda_id in hda_ids: - hda_id = trans.security.decode_id( encoded_hda_id ) + hda_id = self.decode_id( encoded_hda_id ) hda = trans.sa_session.query( model.HistoryDatasetAssociation ).filter_by( id=hda_id ).first() if hda: # Ensure history is owned by current user @@ -530,18 +528,18 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.require_login( "use Galaxy datasets" ) def get_name_and_link_async( self, trans, id=None ): """ Returns dataset's name and link. """ - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) return_dict = { "name" : dataset.name, "link" : url_for( controller='dataset', action="display_by_username_and_slug", username=dataset.history.user.username, slug=trans.security.encode_id( dataset.id ) ) } return return_dict @web.expose def get_embed_html_async( self, trans, id ): """ Returns HTML for embedding a dataset in a page. """ - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset: return "Embedded Dataset '%s'" % dataset.name @@ -557,9 +555,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def rate_async( self, trans, id, rating ): """ Rate a dataset asynchronously and return updated community data. """ - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: return trans.show_error_message( "The specified dataset does not exist." ) @@ -572,15 +570,15 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def display_by_username_and_slug( self, trans, username, slug, filename=None, preview=True ): """ Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id. """ id = slug - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset: # Filename used for composite types. if filename: return self.display( trans, dataset_id=slug, filename=filename) - truncated, dataset_data = self.mgrs.hdas.text_data( dataset, preview ) + truncated, dataset_data = self.hda_manager.text_data( dataset, preview ) dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) # If dataset is chunkable, get first chunk. @@ -615,12 +613,12 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def get_item_content_async( self, trans, id ): """ Returns item content in HTML format. """ - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset is None: raise web.httpexceptions.HTTPNotFound() - truncated, dataset_data = self.mgrs.hdas.text_data( dataset, preview=True ) + truncated, dataset_data = self.hda_manager.text_data( dataset, preview=True ) # Get annotation. dataset.annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) return trans.stream_template_mako( "/dataset/item_content.mako", item=dataset, item_data=dataset_data, truncated=truncated ) @@ -628,9 +626,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.expose def annotate_async( self, trans, id, new_annotation=None, **kwargs ): #TODO:?? why is this an access check only? - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() if dataset and new_annotation: @@ -642,9 +640,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.expose def get_annotation_async( self, trans, id ): - decoded_id = trans.security.decode_id( id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) @@ -793,7 +791,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses hda.mark_deleted() hda.clear_associated_files() trans.log_event( "Dataset id %s marked as deleted" % str(id) ) - self.mgrs.hdas.stop_creating_job( hda ) + self.hda_manager.stop_creating_job( hda ) trans.sa_session.flush() except Exception, e: msg = 'HDA deletion failed (encoded: %s, decoded: %s)' % ( dataset_id, id ) @@ -959,7 +957,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses Show the parameters used for the job associated with an HDA """ try: - hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( self.decode_id( dataset_id ) ) except ValueError: hda = None if not hda: @@ -1017,8 +1015,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def copy_datasets( self, trans, source_history=None, source_content_ids="", target_history_id=None, target_history_ids="", new_history_name="", do_copy=False, **kwd ): user = trans.get_user() if source_history is not None: - decoded_source_history_id = trans.security.decode_id( source_history ) - history = self.mgrs.histories.owned_by_id( trans, decoded_source_history_id, trans.user ) + decoded_source_history_id = self.decode_id( source_history ) + history = self.history_manager.owned_by_id( trans, decoded_source_history_id, trans.user ) current_history = trans.get_history() else: history = current_history = trans.get_history() @@ -1034,11 +1032,11 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses decoded_dataset_collection_ids = [] decoded_dataset_ids = [] if target_history_id: - target_history_ids = [ trans.security.decode_id(target_history_id) ] + target_history_ids = [ self.decode_id(target_history_id) ] elif target_history_ids: if not isinstance( target_history_ids, list ): target_history_ids = target_history_ids.split(",") - target_history_ids = list(set([ trans.security.decode_id(h) for h in target_history_ids if h ])) + target_history_ids = list(set([ self.decode_id(h) for h in target_history_ids if h ])) else: target_history_ids = [] done_msg = error_msg = "" @@ -1119,9 +1117,9 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses if user != history.user: error_msg = error_msg + "You do not have permission to add datasets to %i requested histories. " % ( len( target_histories ) ) for dataset_id in dataset_ids: - decoded_id = trans.security.decode_id( dataset_id ) - data = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - data = self.mgrs.hdas.error_if_uploading( data ) + decoded_id = self.decode_id( dataset_id ) + data = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + data = self.hda_manager.error_if_uploading( data ) if data is None: error_msg = error_msg + "You tried to copy a dataset that does not exist or that you do not have access to. " diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index ba9c4757d8c..2de77d5de78 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -214,15 +214,14 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI def __init__( self, app ): super( HistoryController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ) - ) + self.history_manager = managers.histories.HistoryManager( app ) self.history_serializer = managers.histories.HistorySerializer( self.app ) def _get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ): """ Get a History from the database by id, verifying ownership. """ + #TODO: moved from the mixin - to manager if trans.user is None and trans.history: if id == trans.security.encode_id( trans.history.id ): return trans.history @@ -495,7 +494,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if id is None: id = trans.history.id else: - id = trans.security.decode_id( id ) + id = self.decode_id( id ) # Expunge history from the session to allow us to force a reload # with a bunch of eager loaded joins trans.sa_session.expunge( trans.history ) @@ -563,17 +562,16 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI def structure( self, trans, id=None, **kwargs ): """ """ - if not id: - id = trans.security.encode_id( trans.history.id ) + id = self.decode_id( id ) if id else trans.history.id + history_to_view = self.history_manager.accessible_by_id( trans, id, trans.user ) - history_to_view = self._get_history( trans, id, check_ownership=False, check_accessible=True ) - history_data = self.mgrs.histories._get_history_data( trans, history_to_view ) + history_data = self.history_manager._get_history_data( trans, history_to_view ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] jobs = ( trans.sa_session.query( trans.app.model.Job ) .filter( trans.app.model.Job.user == trans.user ) - .filter( trans.app.model.Job.history_id == trans.security.decode_id( id ) ) ).all() + .filter( trans.app.model.Job.history_id == self.decode_id( id ) ) ).all() jobs = map( lambda j: self.encode_all_ids( trans, j.to_dict( 'element' ), True ), jobs ) tools = {} @@ -605,22 +603,11 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI hda_dictionaries = [] user_is_owner = False try: - history_to_view = self._get_history( trans, id, check_ownership=False, check_accessible=False ) - if not history_to_view: - return trans.show_error_message( "The specified history does not exist." ) - if history_to_view.user == trans.user: - user_is_owner = True - - if( ( history_to_view.user != trans.user ) - # Admin users can view any history - and ( not trans.user_is_admin() ) - and ( not history_to_view.importable ) - and ( trans.user not in history_to_view.users_shared_with_dot_users ) ): - return trans.show_error_message( "Either you are not allowed to view this history" - + " or the owner of this history has not made it accessible." ) + history_to_view = self.history_manager.accessible_by_id( trans, id, trans.user ) + user_is_owner = history_to_view.user == trans.user # include all datasets: hidden, deleted, and purged - history_data = self.mgrs.histories._get_history_data( trans, history_to_view ) + history_data = self.history_manager._get_history_data( trans, history_to_view ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] @@ -658,7 +645,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI current_history_id = trans.security.encode_id( current_history.id ) if current_history else None history_dictionaries = [] - for history in self.mgrs.histories.by_user( trans, trans.user, filters=deleted_filter ): + for history in self.history_manager.by_user( trans, trans.user, filters=deleted_filter ): history_dictionary = self.history_serializer.serialize_to_view( trans, history, view='detailed' ) history_dictionaries.append( history_dictionary ) @@ -692,7 +679,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # create ownership flag for template, dictify models user_is_owner = trans.user == history.user - history_data = self.mgrs.histories._get_history_data( trans, history ) + history_data = self.history_manager._get_history_data( trans, history ) history_dict = history_data[ 'history' ] hda_dicts = history_data[ 'contents' ] @@ -742,7 +729,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI elif 'disable_link_access_and_unpublish' in kwargs: history.importable = history.published = False elif 'unshare_user' in kwargs: - user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( kwargs[ 'unshare_user' ] ) ) + user = trans.sa_session.query( trans.app.model.User ).get( self.decode_id( kwargs[ 'unshare_user' ] ) ) # Look for and delete sharing relation for history-user. deleted_sharing_relation = False husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() @@ -1130,7 +1117,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI trans.app.job_manager.job_stop_queue.put( job.id ) # Regardless of whether it was previously deleted, get the most recent history or create a new one. - most_recent_history = self.mgrs.histories.most_recent( trans, user=trans.user ) + most_recent_history = self.history_manager.most_recent( trans, user=trans.user ) if most_recent_history: trans.set_history( most_recent_history ) return trans.show_ok_message( "History deleted, your most recent history is now active", @@ -1483,7 +1470,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI """ """ try: - history = self.mgrs.histories.owned_by_id( trans, trans.security.decode_id( id ), trans.user ) + history = self.history_manager.owned_by_id( trans, self.decode_id( id ), trans.user ) trans.set_history( history ) return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) except exceptions.MessageException, msg_exc: diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index a92dae5de09..a4abfd44508 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -289,10 +289,8 @@ class PageController( BaseUIController, SharableMixin, def __init__( self, app ): super( PageController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ), - hdas=managers.hdas.HDAManager( app ), - ) + self.history_manager = managers.histories.HistoryManager( app ) + self.hda_manager = managers.hdas.HDAManager( app ) @web.expose @web.require_login() @@ -304,7 +302,7 @@ class PageController( BaseUIController, SharableMixin, operation = kwargs['operation'].lower() ids = util.listify( kwargs['id'] ) for id in ids: - item = session.query( model.Page ).get( trans.security.decode_id( id ) ) + item = session.query( model.Page ).get( self.decode_id( id ) ) if operation == "delete": item.deleted = True if operation == "share or publish": @@ -397,7 +395,7 @@ class PageController( BaseUIController, SharableMixin, Edit a page's attributes. """ encoded_id = id - id = trans.security.decode_id( id ) + id = self.decode_id( id ) session = trans.sa_session page = session.query( model.Page ).get( id ) user = trans.user @@ -447,7 +445,7 @@ class PageController( BaseUIController, SharableMixin, """ Render the main page editor interface. """ - id = trans.security.decode_id( id ) + id = self.decode_id( id ) page = trans.sa_session.query( model.Page ).get( id ) assert page.user == trans.user return trans.fill_template( "page/editor.mako", page=page ) @@ -459,7 +457,7 @@ class PageController( BaseUIController, SharableMixin, # Get session and page. session = trans.sa_session - page = trans.sa_session.query( model.Page ).get( trans.security.decode_id( id ) ) + page = trans.sa_session.query( model.Page ).get( self.decode_id( id ) ) # Do operation on page. if 'make_accessible_via_link' in kwargs: @@ -476,7 +474,7 @@ class PageController( BaseUIController, SharableMixin, elif 'disable_link_access_and_unpublish' in kwargs: page.importable = page.published = False elif 'unshare_user' in kwargs: - user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) ) + user = session.query( model.User ).get( self.decode_id( kwargs['unshare_user' ] ) ) if not user: error( "User not found for provided id" ) association = session.query( model.PageUserShareAssociation ) \ @@ -493,7 +491,7 @@ class PageController( BaseUIController, SharableMixin, def share( self, trans, id, email="", use_panels=False ): """ Handle sharing with an individual user. """ msg = mtype = None - page = trans.sa_session.query( model.Page ).get( trans.security.decode_id( id ) ) + page = trans.sa_session.query( model.Page ).get( self.decode_id( id ) ) if email: other = trans.sa_session.query( model.User ) \ .filter( and_( model.User.table.c.email==email, @@ -531,7 +529,7 @@ class PageController( BaseUIController, SharableMixin, @web.expose @web.require_login() def save( self, trans, id, content, annotations ): - id = trans.security.decode_id( id ) + id = self.decode_id( id ) page = trans.sa_session.query( model.Page ).get( id ) assert page.user == trans.user @@ -548,7 +546,7 @@ class PageController( BaseUIController, SharableMixin, # Save annotations. annotations = loads( annotations ) for annotation_dict in annotations: - item_id = trans.security.decode_id( annotation_dict[ 'item_id' ] ) + item_id = self.decode_id( annotation_dict[ 'item_id' ] ) item_class = self.get_class( annotation_dict[ 'item_class' ] ) item = trans.sa_session.query( item_class ).filter_by( id=item_id ).first() if not item: @@ -581,7 +579,7 @@ class PageController( BaseUIController, SharableMixin, @web.expose @web.require_login() def display( self, trans, id ): - id = trans.security.decode_id( id ) + id = self.decode_id( id ) page = trans.sa_session.query( model.Page ).get( id ) if not page: raise web.httpexceptions.HTTPNotFound() @@ -715,7 +713,7 @@ class PageController( BaseUIController, SharableMixin, def get_page( self, trans, id, check_ownership=True, check_accessible=False ): """Get a page from the database by id.""" # Load history from database - id = trans.security.decode_id( id ) + id = self.decode_id( id ) page = trans.sa_session.query( model.Page ).get( id ) if not page: error( "Page not found" ) @@ -730,8 +728,8 @@ class PageController( BaseUIController, SharableMixin, Returns html suitable for embedding in another page. """ #TODO: should be moved to history controller and/or called via ajax from the template - decoded_id = trans.security.decode_id( id ) - history = self.mgrs.histories.accessible_by_id( trans, decoded_id, trans.user ) + decoded_id = self.decode_id( id ) + history = self.history_manager.accessible_by_id( trans, decoded_id, trans.user ) if not history: return None @@ -741,7 +739,7 @@ class PageController( BaseUIController, SharableMixin, history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history ) # include all datasets: hidden, deleted, and purged - history_data = self.mgrs.histories._get_history_data( trans, history ) + history_data = self.history_manager._get_history_data( trans, history ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] history_dictionary[ 'annotation' ] = history.annotation @@ -779,13 +777,13 @@ class PageController( BaseUIController, SharableMixin, return self._get_embedded_history_html( trans, item_id ) elif item_class == model.HistoryDatasetAssociation: - decoded_id = trans.security.decode_id( item_id ) - dataset = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.mgrs.hdas.error_if_uploading( dataset ) + decoded_id = self.decode_id( item_id ) + dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) if dataset: - data = self.mgrs.hdas.text_data( dataset ) + data = self.hda_manager.text_data( dataset ) return trans.fill_template( "dataset/embed.mako", item=dataset, item_data=data ) elif item_class == model.StoredWorkflow: diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py index f7c275c5ca1..8ebd8f41cf4 100644 --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -26,9 +26,7 @@ class RootController( BaseUIController, UsesAnnotations ): """ def __init__( self, app ): super( RootController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - histories=managers.histories.HistoryManager( app ) - ) + self.history_manager = managers.histories.HistoryManager( app ) @web.expose def default(self, trans, target1=None, target2=None, **kwd): @@ -128,7 +126,7 @@ class RootController( BaseUIController, UsesAnnotations ): history_dictionary = {} hda_dictionaries = [] try: - history_data = self.mgrs.histories._get_history_data( trans, trans.get_history( create=True ) ) + history_data = self.history_manager._get_history_data( trans, trans.get_history( create=True ) ) history_dictionary = history_data[ 'history' ] hda_dictionaries = history_data[ 'contents' ] @@ -170,7 +168,7 @@ class RootController( BaseUIController, UsesAnnotations ): raise Exception( "No dataset with hid '%d'" % hid ) else: if encoded_id and not id: - id = trans.security.decode_id( encoded_id ) + id = self.decode_id( encoded_id ) try: data = trans.sa_session.query( self.app.model.HistoryDatasetAssociation ).get( id ) except: diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index bdf72e6c0b2..afbe83be333 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -129,7 +129,7 @@ class DbKeyColumn( grids.GridColumn ): class HistoryColumn( grids.GridColumn ): """ Column for filtering by history id. """ def filter( self, trans, user, query, history_id ): - return query.filter( model.History.id==trans.security.decode_id(history_id) ) + return query.filter( model.History.id==self.decode_id(history_id) ) class HistoryDatasetsSelectionGrid( grids.Grid ): # Grid definition. @@ -158,7 +158,7 @@ class HistoryDatasetsSelectionGrid( grids.Grid ): Current item for grid is the history being queried. This is a bit of hack since current_item typically means the current item in the grid. """ - return trans.sa_session.query( model.History ).get( trans.security.decode_id( kwargs[ 'f-history' ] ) ) + return trans.sa_session.query( model.History ).get( self.decode_id( kwargs[ 'f-history' ] ) ) def build_initial_query( self, trans, **kwargs ): return trans.sa_session.query( self.model_class ).join( model.History.table ).join( model.Dataset.table ) def apply_query_filter( self, trans, query, **kwargs ): @@ -283,9 +283,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio def __init__( self, app ): super( VisualizationController, self ).__init__( app ) - self.mgrs = util.bunch.Bunch( - hdas=managers.hdas.HDAManager( app ), - ) + self.hda_manager = managers.hdas.HDAManager( app ) # # -- Functions for listing visualizations. -- @@ -304,7 +302,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio def list_library_datasets( self, trans, **kwargs ): """List a library's datasets that can be added to a visualization.""" - library = trans.sa_session.query( trans.app.model.Library ).get( trans.security.decode_id( kwargs.get('f-library') ) ) + library = trans.sa_session.query( trans.app.model.Library ).get( self.decode_id( kwargs.get('f-library') ) ) return trans.fill_template( '/tracks/library_datasets_select_grid.mako', cntrller="library", use_panels=False, @@ -373,7 +371,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio operation = kwargs['operation'].lower() ids = util.listify( kwargs['id'] ) for id in ids: - item = session.query( model.Visualization ).get( trans.security.decode_id( id ) ) + item = session.query( model.Visualization ).get( self.decode_id( id ) ) if operation == "delete": item.deleted = True if operation == "share or publish": @@ -517,7 +515,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio elif 'disable_link_access_and_unpublish' in kwargs: visualization.importable = visualization.published = False elif 'unshare_user' in kwargs: - user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) ) + user = session.query( model.User ).get( self.decode_id( kwargs['unshare_user' ] ) ) if not user: error( "User not found for provided id" ) association = session.query( model.VisualizationUserShareAssociation ) \ @@ -868,8 +866,8 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # use dbkey from dataset to be added or from incoming parameter dbkey = None if new_dataset_id: - decoded_id = trans.security.decode_id( new_dataset_id ) - hda = self.mgrs.hdas.owned_by_id( trans, decoded_id, trans.user ) + decoded_id = self.decode_id( new_dataset_id ) + hda = self.hda_manager.owned_by_id( trans, decoded_id, trans.user ) dbkey = hda.dbkey if dbkey == '?': dbkey = kwargs.get( "dbkey", None ) @@ -978,12 +976,12 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # Loading a shared visualization. viz = self.get_visualization( trans, id ) viz_config = self.get_visualization_config( trans, viz ) - decoded_id = trans.security.decode_id( viz_config[ 'dataset_id' ] ) - dataset = self.mgrs.hdas.owned_by_id( trans, decoded_id, trans.user ) + decoded_id = self.decode_id( viz_config[ 'dataset_id' ] ) + dataset = self.hda_manager.owned_by_id( trans, decoded_id, trans.user ) else: # Loading new visualization. dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) - job = self.mgrs.hdas.creating_job( dataset ) + job = self.hda_manager.creating_job( dataset ) viz_config = { 'dataset_id': dataset_id, 'tool_id': job.tool_id, @@ -1013,9 +1011,9 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # get the hda if we can, then its data using the phyloviz parsers if dataset_id: - decoded_id = trans.security.decode_id( dataset_id ) - hda = self.mgrs.hdas.accessible_by_id( trans, decoded_id, trans.user ) - hda = self.mgrs.hdas.error_if_uploading( hda ) + decoded_id = self.decode_id( dataset_id ) + hda = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + hda = self.hda_manager.error_if_uploading( hda ) else: return trans.show_message( "Phyloviz couldn't find a dataset_id" ) From 44370ec5ba675c1229bae404030ebc6f1ecd626c Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Mon, 12 Jan 2015 13:01:28 -0500 Subject: [PATCH 154/473] Normalize names from owned/accessible_by_id to get_owned/accessible; fix some history errors --- lib/galaxy/managers/base.py | 23 +++-- lib/galaxy/managers/collections.py | 2 +- lib/galaxy/webapps/galaxy/api/annotations.py | 4 +- .../webapps/galaxy/api/dataset_collections.py | 2 +- lib/galaxy/webapps/galaxy/api/datasets.py | 2 +- .../webapps/galaxy/api/extended_metadata.py | 4 +- .../webapps/galaxy/api/folder_contents.py | 2 +- lib/galaxy/webapps/galaxy/api/histories.py | 16 ++-- .../webapps/galaxy/api/history_contents.py | 10 +-- lib/galaxy/webapps/galaxy/api/jobs.py | 2 +- .../webapps/galaxy/api/library_contents.py | 2 +- lib/galaxy/webapps/galaxy/api/tools.py | 4 +- lib/galaxy/webapps/galaxy/api/workflows.py | 2 +- .../webapps/galaxy/controllers/dataset.py | 34 ++++---- .../webapps/galaxy/controllers/history.py | 87 ++++++++----------- lib/galaxy/webapps/galaxy/controllers/page.py | 6 +- .../galaxy/controllers/visualization.py | 6 +- 17 files changed, 102 insertions(+), 106 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 0280d911bf9..4c9d53c2b00 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -251,12 +251,12 @@ class ModelManager( object ): except exceptions.ObjectNotFound, not_found: return None - def list( self, trans, query=None, **kwargs ): - """ - Sends kwargs to build the query return all models found. - """ - query = query or self.query( trans, **kwargs ) - return query.all() + #def get( self, trans, id, **kwargs ): + # """ + # """ + # #TODO: either remove to prevent confusion with sqlalx get or make it behave *much* more like it: obj/None + # id_filter = self.model_class.id == id + # return self.one( trans, filters=id_filter, **kwargs ) def by_id( self, trans, id, **kwargs ): """ @@ -265,6 +265,13 @@ class ModelManager( object ): id_filter = self.model_class.id == id return self.one( trans, filters=id_filter, **kwargs ) + def list( self, trans, query=None, **kwargs ): + """ + Sends kwargs to build the query return all models found. + """ + query = query or self.query( trans, **kwargs ) + return query.all() + def _query_by_ids( self, trans, ids, filters=None, **kwargs ): """ Builds a query to find a list of models with the given list of `ids`. @@ -436,7 +443,7 @@ class AccessibleModelInterface( object ): raise exceptions.NotImplemented( "Abstract Interface Method" ) #OR? return False - def accessible_by_id( self, trans, id, user, **kwargs ): + def get_accessible( self, trans, id, user, **kwargs ): """ Return the item with the given id if it's accessible to user, otherwise raise an error. @@ -496,7 +503,7 @@ class OwnableModelInterface( object ): raise exceptions.NotImplemented( "Abstract Interface Method" ) #OR? return False - def owned_by_id( self, trans, id, user, **kwargs ): + def get_owned( self, trans, id, user, **kwargs ): """ Return the item with the given id if owned by the user, otherwise raise an error. diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index 4cd6265ccf4..f027fd5fe56 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -248,7 +248,7 @@ class DatasetCollectionManager( object ): if src_type == 'hda': decoded_id = int( trans.app.security.decode_id( encoded_id ) ) - element = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + element = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) elif src_type == 'ldda': element = self.ldda_manager.get( trans, encoded_id ) elif src_type == 'hdca': diff --git a/lib/galaxy/webapps/galaxy/api/annotations.py b/lib/galaxy/webapps/galaxy/api/annotations.py index a5a7b6ccd03..1c797fc9ec4 100644 --- a/lib/galaxy/webapps/galaxy/api/annotations.py +++ b/lib/galaxy/webapps/galaxy/api/annotations.py @@ -59,7 +59,7 @@ class HistoryAnnotationsController(BaseAnnotationsController): def _get_item_from_id(self, trans, idstr): decoded_idstr = self.decode_id( idstr ) - history = self.history_manager.accessible_by_id( trans, decoded_idstr, trans.user ) + history = self.history_manager.get_accessible( trans, decoded_idstr, trans.user ) return history @@ -73,7 +73,7 @@ class HistoryContentAnnotationsController(BaseAnnotationsController): def _get_item_from_id(self, trans, idstr): decoded_idstr = self.decode_id( idstr ) - hda = self.hda_manager.accessible_by_id( trans, decoded_idstr, trans.user ) + hda = self.hda_manager.get_accessible( trans, decoded_idstr, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) return hda diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index 76f70d98d1c..2f489618752 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -45,7 +45,7 @@ class DatasetCollectionsController( if instance_type == "history": history_id = payload.get( 'history_id' ) history_id = self.decode_id( history_id ) - history = self.history_manager.owned_by_id( trans, history_id, trans.user ) + history = self.history_manager.get_owned( trans, history_id, trans.user ) create_params[ "parent" ] = history elif instance_type == "library": folder_id = payload.get( 'folder_id' ) diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index e0f2d4beb7a..54366a7cd3b 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -280,7 +280,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): rval = '' try: - hda = self.hda_manager.accessible_by_id( trans, decoded_content_id, trans.user ) + hda = self.hda_manager.get_accessible( trans, decoded_content_id, trans.user ) if raw: if filename and filename != 'index': diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index bf8684504ae..4d2db859af0 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -71,7 +71,7 @@ class HistoryDatasetExtendMetadataController(BaseExtendedMetadataController): def _get_item_from_id(self, trans, idstr, check_writable=True): decoded_idstr = self.decode_id( idstr ) if check_writable: - return self.hda_manager.owned_by_id( trans, decoded_idstr, trans.user ) + return self.hda_manager.get_owned( trans, decoded_idstr, trans.user ) else: - hda = self.hda_manager.accessible_by_id( trans, decoded_idstr, trans.user ) + hda = self.hda_manager.get_accessible( trans, decoded_idstr, trans.user ) return self.hda_manager.error_if_uploading( trans, hda ) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index cbce170d266..a8ec369b93b 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -268,7 +268,7 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary rval = {} try: decoded_hda_id = self.decode_id( from_hda_id ) - hda = self.hda_manager.owned_by_id( trans, decoded_hda_id, trans.user ) + hda = self.hda_manager.get_owned( trans, decoded_hda_id, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) folder = self.get_library_folder( trans, encoded_folder_id_16, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index ca3e0134b86..6f674e0fac1 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -92,7 +92,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor history = self.history_manager.most_recent( trans, trans.user, filters=( self.app.model.History.deleted == False ) ) else: - history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) return self.history_serializer.serialize_to_view( trans, history, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -101,7 +101,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor def citations( self, trans, history_id, **kwd ): """ """ - history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) tool_ids = set([]) for dataset in history.datasets: job = dataset.creating_job @@ -146,7 +146,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor # if a history id was passed, copy that history if copy_this_history_id: decoded_id = self.decode_id( copy_this_history_id ) - original_history = self.history_manager.accessible_by_id( trans, decoded_id, trans.user ) + original_history = self.history_manager.get_accessible( trans, decoded_id, trans.user ) hist_name = hist_name or ( "Copy of '%s'" % original_history.name ) new_history = original_history.copy( name=hist_name, target_user=trans.user ) @@ -188,7 +188,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor if kwd.get( 'payload', None ): purge = string_as_bool( kwd['payload'].get( 'purge', False ) ) - history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) self.history_manager.delete( trans, history ) if purge: self.history_manager.purge( trans, history ) @@ -210,7 +210,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor :returns: 'OK' if the history was undeleted """ history_id = id - history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) self.history_manager.undelete( trans, history ) return self.history_serializer.serialize_to_view( trans, history, @@ -236,7 +236,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor any values that were different from the original and, therefore, updated """ #TODO: PUT /api/histories/{encoded_history_id} payload = { rating: rating } (w/ no security checks) - history = self.history_manager.owned_by_id( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) #TODO: flushing in deserialize is an iffy pattern... self.history_deserializer.deserialize( trans, history, payload ) @@ -259,7 +259,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # PUT instead of POST because multiple requests should just result # in one object being created. - history = self.history_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) jeha = history.latest_export up_to_date = jeha and jeha.up_to_date if 'force' in kwds: @@ -292,7 +292,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # Seems silly to put jeha_id in here, but want GET to be immuatable? # and this is being accomplished this way. - history = self.history_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) matching_exports = filter( lambda e: trans.security.encode_id( e.id ) == jeha_id, history.exports ) if not matching_exports: raise exceptions.ObjectNotFound() diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 4f95c014de1..28b95204f49 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -68,7 +68,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar """ rval = [] - history = self.history_manager.accessible_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) # Allow passing in type or types - for continuity rest of methods # take in type - but this one can be passed multiple types and @@ -141,7 +141,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar return self.__handle_unknown_contents_type( trans, contents_type ) def __show_dataset( self, trans, id, **kwd ): - hda = self.hda_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) + hda = self.hda_manager.get_accessible( trans, self.decode_id( id ), trans.user ) return self.hda_serializer.serialize_to_view( trans, hda, **self._parse_serialization_params( kwd, 'detailed' ) ) @@ -212,7 +212,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # TODO: Flush out create new collection documentation above, need some # examples. See also bioblend and API tests for specific examples. - history = self.history_manager.owned_by_id( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) type = payload.get( 'type', 'dataset' ) if type == 'dataset': @@ -245,7 +245,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # copy an existing, accessible hda elif source == 'hda': unencoded_hda_id = self.decode_id( content ) - original = self.hda_manager.accessible_by_id( trans, unencoded_hda_id, trans.user ) + original = self.hda_manager.get_accessible( trans, unencoded_hda_id, trans.user ) data_copy = original.copy( copy_children=True ) hda = history.add_dataset( data_copy ) @@ -329,7 +329,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # logged in user: use full payload, check state if deleting, and make sure the history is theirs else: - hda = self.hda_manager.accessible_by_id( trans, self.decode_id( id ), trans.user ) + hda = self.hda_manager.get_accessible( trans, self.decode_id( id ), trans.user ) self.history_manager.error_unless_owner( trans, hda.history, trans.user ) # only check_state if not deleting, otherwise cannot delete uploading files diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index d39531e1d94..14f1d4b03ff 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -261,7 +261,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): if 'id' in v: if 'src' not in v or v[ 'src' ] == 'hda': hda_id = self.decode_id( v['id'] ) - dataset = self.hda_manager.accessible_by_id( trans, hda_id, trans.user ) + dataset = self.hda_manager.get_accessible( trans, hda_id, trans.user ) else: dataset = self.get_library_dataset_dataset_association( trans, v['id'] ) if dataset is None: diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 692ada19d46..2269f7f5f46 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -303,7 +303,7 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # check permissions on (all three?) resources: hda, library, folder #TODO: do we really need the library?? from_hda_id = self.decode_id( from_hda_id ) - hda = self.hda_manager.owned_by_id( trans, from_hda_id, trans.user ) + hda = self.hda_manager.get_owned( trans, from_hda_id, trans.user ) hda = self.hda_manager.error_if_uploading( trans, hda ) # library = self.get_library( trans, library_id, check_accessible=True ) folder = self.get_library_folder( trans, folder_id, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index f5ca7c7c93f..182254ad370 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -142,7 +142,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): history_id = payload.get("history_id", None) if history_id: decoded_id = self.decode_id( history_id ) - target_history = self.history_manager.owned_by_id( trans, decoded_id, trans.user ) + target_history = self.history_manager.get_owned( trans, decoded_id, trans.user ) else: target_history = None @@ -281,7 +281,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): # Dataset check. decoded_dataset_id = self.decode_id( payload.get( 'target_dataset_id' ) ) - original_dataset = self.hda_manager.accessible_by_id( trans, decoded_dataset_id, user=trans.user ) + original_dataset = self.hda_manager.get_accessible( trans, decoded_dataset_id, user=trans.user ) original_dataset = self.hda_manager.error_if_uploading( trans, original_dataset ) msg = self.hda_manager.data_conversion_status( trans, original_dataset ) if msg: diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index bebf183480f..74befbead2b 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -149,7 +149,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn if 'from_history_id' in payload: from_history_id = payload.get( 'from_history_id' ) from_history_id = self.decode_id( from_history_id ) - history = self.history_manager.accessible_by_id( trans, from_history_id, trans.user ) + history = self.history_manager.get_accessible( trans, from_history_id, trans.user ) job_ids = map( self.decode_id, payload.get( 'job_ids', [] ) ) dataset_ids = payload.get( 'dataset_ids', [] ) diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index bfc464ba8c6..66eb7f51227 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -529,8 +529,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def get_name_and_link_async( self, trans, id=None ): """ Returns dataset's name and link. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) return_dict = { "name" : dataset.name, "link" : url_for( controller='dataset', action="display_by_username_and_slug", username=dataset.history.user.username, slug=trans.security.encode_id( dataset.id ) ) } return return_dict @@ -538,8 +538,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def get_embed_html_async( self, trans, id ): """ Returns HTML for embedding a dataset in a page. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if dataset: return "Embedded Dataset '%s'" % dataset.name @@ -556,8 +556,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Rate a dataset asynchronously and return updated community data. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if not dataset: return trans.show_error_message( "The specified dataset does not exist." ) @@ -571,8 +571,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id. """ id = slug decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if dataset: # Filename used for composite types. if filename: @@ -614,8 +614,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Returns item content in HTML format. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if dataset is None: raise web.httpexceptions.HTTPNotFound() truncated, dataset_data = self.hda_manager.text_data( dataset, preview=True ) @@ -627,8 +627,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def annotate_async( self, trans, id, new_annotation=None, **kwargs ): #TODO:?? why is this an access check only? decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if not dataset: web.httpexceptions.HTTPNotFound() if dataset and new_annotation: @@ -641,8 +641,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.expose def get_annotation_async( self, trans, id ): decoded_id = self.decode_id( id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) if not dataset: web.httpexceptions.HTTPNotFound() annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) @@ -1016,7 +1016,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses user = trans.get_user() if source_history is not None: decoded_source_history_id = self.decode_id( source_history ) - history = self.history_manager.owned_by_id( trans, decoded_source_history_id, trans.user ) + history = self.history_manager.get_owned( trans, decoded_source_history_id, trans.user ) current_history = trans.get_history() else: history = current_history = trans.get_history() @@ -1118,8 +1118,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses error_msg = error_msg + "You do not have permission to add datasets to %i requested histories. " % ( len( target_histories ) ) for dataset_id in dataset_ids: decoded_id = self.decode_id( dataset_id ) - data = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - data = self.hda_manager.error_if_uploading( data ) + data = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + data = self.hda_manager.error_if_uploading( trans, data ) if data is None: error_msg = error_msg + "You tried to copy a dataset that does not exist or that you do not have access to. " diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index 2de77d5de78..7b0c98c0b8b 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -209,6 +209,7 @@ class HistoryAllPublishedGrid( grids.Grid ): # A public history is published, has a slug, and is not deleted. return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) + class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesItemRatings, ExportsHistoryMixin, ImportsHistoryMixin ): @@ -217,21 +218,6 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI self.history_manager = managers.histories.HistoryManager( app ) self.history_serializer = managers.histories.HistorySerializer( self.app ) - def _get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ): - """ - Get a History from the database by id, verifying ownership. - """ - #TODO: moved from the mixin - to manager - if trans.user is None and trans.history: - if id == trans.security.encode_id( trans.history.id ): - return trans.history - raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' ) - - history = self.get_object( trans, id, 'History', - check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted ) - history = self.security_check( trans, history, check_ownership, check_accessible ) - return history - @web.expose def index( self, trans ): return "" @@ -257,7 +243,6 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI grid = self.published_list_grid( trans, **kwargs ) return trans.fill_template( "history/list_published.mako", embedded_grid=grid ) - @web.expose @web.require_login( "work with multiple histories" ) def list( self, trans, **kwargs ): @@ -273,14 +258,13 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI del kwargs['name'] # Remove ajax name param that rename method uses return self.rename( trans, **kwargs ) if operation == "view": - history = self._get_history( trans, kwargs.get( 'id', None ) ) - if history: - return trans.response.send_redirect( url_for( controller='history', - action='view', - id=kwargs['id'], - show_deleted=history.deleted, - use_panels=False ) ) - #return self.view( trans, id=kwargs['id'], show_deleted=history.deleted, use_panels=False ) + decoded_id = self.decode_id( kwargs.get( 'id', None ) ) + history = self.history_manager.get_owned( trans, decoded_id, trans.user ) + return trans.response.send_redirect( url_for( controller='history', + action='view', + id=kwargs['id'], + show_deleted=history.deleted, + use_panels=False ) ) if operation == 'copy' and kwargs.get( 'id', None ): return self.copy( trans, id=kwargs.get( 'id', None ) ) history_ids = galaxy.util.listify( kwargs.get( 'id', [] ) ) @@ -290,7 +274,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Load the histories and ensure they all belong to the current user histories = [] for history_id in history_ids: - history = self._get_history( trans, history_id ) + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) if history: # Ensure history is owned by current user if history.user_id != None and trans.user: @@ -333,10 +317,13 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI self._make_item_importable( trans.sa_session, history ) elif operation == "disable import via link": if history_ids: - histories = [ self._get_history( trans, history_id ) for history_id in history_ids ] - for history in histories: + histories = [] + for history_id in history_ids: + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) if history.importable: history.importable = False + histories.append( history ) + trans.sa_session.flush() # Render the list view return self.stored_list_grid( trans, status=status, message=message, **kwargs ) @@ -450,7 +437,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI operation = kwargs['operation'].lower() if operation == "view": # Display history. - history = self._get_history( trans, ids[0], False) + history = self.history_manager.get_accessible( trans, self.decode_id( ids[0] ), trans.user ) return self.display_by_username_and_slug( trans, history.user.username, history.slug ) elif operation == "copy": if not ids: @@ -463,12 +450,13 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if not ids: message = "Select a history to unshare" return self.shared_list_grid( trans, status='error', message=message, **kwargs ) - # No need to check security, association below won't yield a - # hit if this user isn't having the history shared with her. - histories = [ self._get_history( trans, history_id, check_ownership=False ) for history_id in ids ] - for history in histories: + for id in ids: + # No need to check security, association below won't yield a + # hit if this user isn't having the history shared with her. + history = self.history_manager.by_id( trans, self.decode_id( id ) ) # Current user is the user with which the histories were shared - association = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=trans.user, history=history ).one() + association = ( trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) + .filter_by( user=trans.user, history=history ).one() ) trans.sa_session.delete( association ) trans.sa_session.flush() message = "Unshared %d shared histories" % len( ids ) @@ -563,7 +551,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI """ """ id = self.decode_id( id ) if id else trans.history.id - history_to_view = self.history_manager.accessible_by_id( trans, id, trans.user ) + history_to_view = self.history_manager.get_accessible( trans, id, trans.user ) history_data = self.history_manager._get_history_data( trans, history_to_view ) history_dictionary = history_data[ 'history' ] @@ -603,7 +591,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI hda_dictionaries = [] user_is_owner = False try: - history_to_view = self.history_manager.accessible_by_id( trans, id, trans.user ) + history_to_view = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) user_is_owner = history_to_view.user == trans.user # include all datasets: hidden, deleted, and purged @@ -665,7 +653,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if history is None: raise web.httpexceptions.HTTPNotFound() # Security check raises error if user cannot access history. - self.security_check( trans, history, False, True) + self.history_manager.error_unless_accessible( trans, history, trans.user ) # Get rating data. user_item_rating = 0 @@ -705,7 +693,8 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if id: ids = galaxy.util.listify( id ) if ids: - histories = [ self._get_history( trans, history_id ) for history_id in ids ] + histories = [ self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + for history_id in ids ] elif not histories: histories = [ trans.history ] @@ -774,7 +763,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_err = err_msg histories = [] for history_id in id: - histories.append( self._get_history( trans, history_id ) ) + histories.append( self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) ) return trans.fill_template( "/history/share.mako", histories=histories, email=email, @@ -882,7 +871,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_err = "" histories = [] for history_id in id: - histories.append( self._get_history( trans, history_id ) ) + histories.append( self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) ) send_to_users = [] for email_address in galaxy.util.listify( email ): email_address = email_address.strip() @@ -1161,7 +1150,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.json def rate_async( self, trans, id, rating ): """ Rate a history asynchronously and return updated community data. """ - history = self._get_history( trans, id, check_ownership=False, check_accessible=True ) + history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) if not history: return trans.show_error_message( "The specified history does not exist." ) # Rate history. @@ -1205,7 +1194,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Get history to export. # if id: - history = self._get_history( trans, id, check_ownership=False, check_accessible=True ) + history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) else: # Use current history. history = trans.history @@ -1243,7 +1232,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "get history name and link" ) def get_name_and_link_async( self, trans, id=None ): """ Returns history's name and link. """ - history = self._get_history( trans, id, False ) + history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) if self.create_item_slug( trans.sa_session, history ): trans.sa_session.flush() return_dict = { @@ -1257,7 +1246,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "set history's accessible flag" ) def set_accessible_async( self, trans, id=None, accessible=False ): """ Set history's importable attribute and slug. """ - history = self._get_history( trans, id, True ) + history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. importable = accessible in ['True', 'true', 't', 'T']; if history and history.importable != importable: @@ -1306,11 +1295,11 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Do import. if not id: return trans.show_error_message( "You must specify a history you want to import.
You can %s." % referer_message, use_panels=True ) - import_history = self._get_history( trans, id, check_ownership=False, check_accessible=False ) + import_history = self.history_manager.by_id( trans, self.decode_id( id ) ) if not import_history: return trans.show_error_message( "The specified history does not exist.
You can %s." % referer_message, use_panels=True ) # History is importable if user is admin or it's accessible. TODO: probably want to have app setting to enable admin access to histories. - if not trans.user_is_admin() and not self.security_check( trans, import_history, check_ownership=False, check_accessible=True ): + if not self.history_manager.is_accessible( trans, import_history, trans.user ): return trans.show_error_message( "You cannot access this history.
You can %s." % referer_message, use_panels=True ) if user: #dan: I can import my own history. @@ -1376,7 +1365,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI histories = [] cur_names = [] for history_id in id: - history = self._get_history( trans, history_id ) + history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) if history and history.user_id == user.id: histories.append( history ) cur_names.append( history.get_display_name() ) @@ -1418,7 +1407,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI ids = galaxy.util.listify( id ) histories = [] for history_id in ids: - history = self._get_history( trans, history_id, check_ownership=False ) + history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) histories.append( history ) user = trans.get_user() for history in histories: @@ -1455,7 +1444,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return trans.response.send_redirect( url_for( "/" ) ) def get_item( self, trans, id ): - return self._get_history( trans, id ) + return self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) #TODO: override of base ui controller? def history_data( self, trans, history ): @@ -1470,7 +1459,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI """ """ try: - history = self.history_manager.owned_by_id( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) trans.set_history( history ) return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) except exceptions.MessageException, msg_exc: diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index a4abfd44508..2287bbb6173 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -729,7 +729,7 @@ class PageController( BaseUIController, SharableMixin, """ #TODO: should be moved to history controller and/or called via ajax from the template decoded_id = self.decode_id( id ) - history = self.history_manager.accessible_by_id( trans, decoded_id, trans.user ) + history = self.history_manager.get_accessible( trans, decoded_id, trans.user ) if not history: return None @@ -778,8 +778,8 @@ class PageController( BaseUIController, SharableMixin, elif item_class == model.HistoryDatasetAssociation: decoded_id = self.decode_id( item_id ) - dataset = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( dataset ) + dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( trans, dataset ) dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) if dataset: diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index afbe83be333..5b47bc5e2f3 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -867,7 +867,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio dbkey = None if new_dataset_id: decoded_id = self.decode_id( new_dataset_id ) - hda = self.hda_manager.owned_by_id( trans, decoded_id, trans.user ) + hda = self.hda_manager.get_owned( trans, decoded_id, trans.user ) dbkey = hda.dbkey if dbkey == '?': dbkey = kwargs.get( "dbkey", None ) @@ -977,7 +977,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio viz = self.get_visualization( trans, id ) viz_config = self.get_visualization_config( trans, viz ) decoded_id = self.decode_id( viz_config[ 'dataset_id' ] ) - dataset = self.hda_manager.owned_by_id( trans, decoded_id, trans.user ) + dataset = self.hda_manager.get_owned( trans, decoded_id, trans.user ) else: # Loading new visualization. dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) @@ -1012,7 +1012,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # get the hda if we can, then its data using the phyloviz parsers if dataset_id: decoded_id = self.decode_id( dataset_id ) - hda = self.hda_manager.accessible_by_id( trans, decoded_id, trans.user ) + hda = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) hda = self.hda_manager.error_if_uploading( hda ) else: return trans.show_message( "Phyloviz couldn't find a dataset_id" ) From 52ff6ef5d04666b15092e5d9db0e2358a595620f Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Mon, 12 Jan 2015 14:15:42 -0500 Subject: [PATCH 155/473] Remove trans from method bodies where possible --- lib/galaxy/managers/base.py | 18 ++++++------ lib/galaxy/managers/datasets.py | 35 ++++++++++++----------- lib/galaxy/managers/hdas.py | 32 ++++++++++----------- lib/galaxy/managers/histories.py | 29 ++++++++++--------- lib/galaxy/managers/sharable.py | 34 +++++++++++----------- lib/galaxy/managers/users.py | 21 +++++++------- test/unit/managers/test_HDAManager.py | 2 +- test/unit/managers/test_HistoryManager.py | 12 ++++---- test/unit/managers/test_UserManager.py | 3 +- 9 files changed, 96 insertions(+), 90 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 4c9d53c2b00..7b125bc2f2a 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -644,7 +644,7 @@ class ModelSerializer( object ): Serialize an id attribute of `item`. """ id = getattr( item, key ) - return trans.security.encode_id( id ) if id is not None else None + return self.app.security.encode_id( id ) if id is not None else None #TODO: AnnotatableMixin def serialize_annotation( self, trans, item, key ): @@ -654,7 +654,8 @@ class ModelSerializer( object ): #TODO: which is correct here? #user = item.user user = trans.user - return item.get_item_annotation_str( trans.sa_session, user, item ) + sa_session = self.app.model.context + return item.get_item_annotation_str( sa_session, user, item ) #TODO: TaggableMixin def serialize_tags( self, trans, item, key ): @@ -745,6 +746,7 @@ class ModelDeserializer( object ): Convert an incoming serialized dict into values that can be directly assigned to an item's attributes and assign them """ + sa_session = self.app.model.context new_dict = {} for key, val in data.items(): if key in self.deserializers: @@ -753,8 +755,8 @@ class ModelDeserializer( object ): #TODO:?? add and flush here or in manager? if flush and len( new_dict ): - trans.sa_session.add( item ) - trans.sa_session.flush() + sa_session.add( item ) + sa_session.flush() return new_dict @@ -891,10 +893,10 @@ class ModelValidator( object ): #TODO: is this correct? if val is None: return '?' - # wot...why tuples? - if val not in dict( trans.db_builds ): - raise exceptions.RequestParameterInvalidException( "invalid reference", key=key, val=val ) - return val + for genome_build_shortname, longname in self.app.genome_builds.get_genome_build_names( trans=trans ): + if val == genome_build_shortname: + return val + raise exceptions.RequestParameterInvalidException( "invalid reference", key=key, val=val ) #def slug( self, trans, item, key, val ): # """validate slug""" diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 7632c23242a..43ebb4bb8a1 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -21,6 +21,8 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur default_order_by = ( model.Dataset.create_time, ) foreign_key_name = 'dataset' + #TODO:?? get + error_if_uploading is common pattern, should upload check be worked into access/owed? + def __init__( self, app ): super( DatasetManager, self ).__init__( app ) # need for admin test @@ -35,9 +37,9 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) ) dataset = model.Dataset( **kwargs ) - trans.sa_session.add( dataset ) + self.app.model.context.add( dataset ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return dataset #def copy( self, dataset, **kwargs ): @@ -61,9 +63,9 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # ......................................................................... delete #TODO: this may be more conv. somewhere else -#TODO: allow admin bypass +#TODO: how to allow admin bypass? def error_unless_dataset_purge_allowed( self, trans, item, msg=None ): - if not trans.app.config.allow_user_dataset_purge: + if not self.app.config.allow_user_dataset_purge: msg = msg or 'This instance does not allow user dataset purging' raise exceptions.ConfigDoesNotAllowException( msg ) return item @@ -79,9 +81,9 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # the following also marks dataset as purged and deleted dataset.full_delete() - trans.sa_session.add( dataset ) + self.app.model.context.add( dataset ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return dataset # ......................................................................... accessibility @@ -102,20 +104,20 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur Return T/F if the user has role-based access to the dataset. """ roles = user.all_roles() if user else [] - return trans.app.security_agent.can_access_dataset( roles, dataset ) + return self.app.security_agent.can_access_dataset( roles, dataset ) #TODO: these need work def _access_permission( self, trans, dataset, user=None, role=None ): """ Return most recent DatasetPermissions for the dataset and user. """ - security_agent = trans.app.security_agent + security_agent = self.app.security_agent access_action = security_agent.permitted_actions.DATASET_ACCESS.action # get a list of role ids to check access for (defaulting to all_roles) user_roles = [ role ] if role else user.all_roles() user_role_ids = [ role.id for role in user_roles ] - query = ( trans.sa_session.query( model.DatasetPermissions ) + query = ( self.app.model.context.query( model.DatasetPermissions ) .filter( model.DatasetPermissions.action == access_action ) .filter( model.DatasetPermissions.dataset == dataset ) .filter( model.DatasetPermissions.role_id.in_( user_role_ids ) ) ) @@ -126,18 +128,18 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur """ Create a new DatasetPermissions for the given dataset and role. """ - access_action = trans.app.security_agent.permitted_actions.DATASET_ACCESS.action + access_action = self.app.security_agent.permitted_actions.DATASET_ACCESS.action permission = model.DatasetPermissions( access_action, dataset, role ) - trans.sa_session.add( permission ) + self.app.model.context.add( permission ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return permission #def give_access_permission( self, trans, dataset, user, flush=True ): # """ # """ # # for now, use the user's private role - # security_agent = trans.app.security_agent + # security_agent = self.app.security_agent # user_role = security_agent.get_private_user_role( user ) # # existing_permission = self._access_permission( trans, dataset, role=user_role ) @@ -153,9 +155,9 @@ class DatasetManager( base.ModelManager, base.AccessibleModelInterface, base.Pur # #permissions = { access_action : [ user_role ] } # #security_agent.set_dataset_permission( dataset, permissions ) # - # #trans.sa_session.add( dataset ) + # #self.app.model.context.add( dataset ) # #if flush: - # # trans.sa_session.flush() + # # self.app.model.context.flush() # # dbl_chk = self._access_permission( trans, dataset, role=user_role ) # print 'access_perm:', dbl_chk @@ -233,7 +235,8 @@ class DatasetDeserializer( base.ModelDeserializer ): class DatasetAssociationManager( base.ModelManager, base.AccessibleModelInterface, base.PurgableModelInterface ): """ """ - # A dataset association HAS a dataset but contains metadata specific to a library (lda) or user (hda) + # DA's were meant to be proxies - but were never fully implemented as them + # Instead, a dataset association HAS a dataset but contains metadata specific to a library (lda) or user (hda) model_class = model.DatasetInstance default_order_by = ( model.Dataset.create_time, ) diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 4c5fc4dbc6a..12436dbdf50 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -64,16 +64,16 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface if not dataset: kwargs[ 'create_dataset' ] = True hda = super( HDAManager, self ).create( trans, flush=flush, - history=history, dataset=dataset, sa_session=trans.sa_session, **kwargs ) + history=history, dataset=dataset, sa_session=self.app.model.context, **kwargs ) if history: set_hid = not ( 'hid' in kwargs ) history.add_dataset( hda ) #TODO:?? some internal sanity check here (or maybe in add_dataset) to make sure hids are not duped? - trans.sa_session.add( hda ) + self.app.model.context.add( hda ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return hda def copy( self, trans, hda, history=None, **kwargs ): @@ -105,15 +105,15 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface #TODO: update from kwargs? # Need to set after flushed, as MetadataFiles require dataset.id - trans.sa_session.add( copy ) - trans.sa_session.flush() + self.app.model.context.add( copy ) + self.app.model.context.flush() copy.metadata = hda.metadata # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs if not hda.datatype.copy_safe_peek: copy.set_peek() - trans.sa_session.flush() + self.app.model.context.flush() return copy def copy_ldda( self, trans, history, ldda, **kwargs ): @@ -162,7 +162,7 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface """ Raise error if HDA is still uploading. """ - if hda.state == trans.model.Dataset.states.UPLOAD: + if hda.state == model.Dataset.states.UPLOAD: raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) return hda @@ -200,12 +200,12 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface Return dictionary containing old-style display app urls. """ display_apps = [] - if not trans.app.config.enable_old_display_applications: + if not self.app.config.enable_old_display_applications: return display_apps for display_app in hda.datatype.get_display_types(): target_frame, display_links = hda.datatype.get_display_links( hda, - display_app, trans.app, trans.request.base ) + display_app, self.app, trans.request.base ) if len( display_links ) > 0: display_label = hda.datatype.get_display_label( display_app ) @@ -226,9 +226,9 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface """ """ # use older system if registry is off in the config - if not trans.app.visualizations_registry: + if not self.app.visualizations_registry: return hda.get_visualizations() - return trans.app.visualizations_registry.get_visualizations( trans, hda ) + return self.app.visualizations_registry.get_visualizations( trans, hda ) #TODO: to data provider or Text datatype directly def text_data( self, dataset, preview=True ): @@ -258,9 +258,9 @@ class HDAManager( datasets.DatasetAssociationManager, base.OwnableModelInterface """ if not hda: return hda.conversion_messages.NO_DATA - if hda.state == trans.app.model.Job.states.ERROR: + if hda.state == model.Job.states.ERROR: return hda.conversion_messages.ERROR - if hda.state != trans.app.model.Job.states.OK: + if hda.state != model.Job.states.OK: return hda.conversion_messages.PENDING return None @@ -483,7 +483,7 @@ class HDASerializer( base.ModelSerializer ): #NOTE: no files if isinstance( val, model.MetadataFile ): # only when explicitly set: fetching filepaths can be expensive - if not trans.app.config.expose_dataset_path: + if not self.app.config.expose_dataset_path: continue val = val.file_name #TODO:? possibly split this off? @@ -510,7 +510,7 @@ class HDASerializer( base.ModelSerializer ): """ """ #TODO: allow admin - if trans.app.config.expose_dataset_path: + if self.app.config.expose_dataset_path: try: return hda.file_name except objectstore.ObjectNotFound: @@ -521,7 +521,7 @@ class HDASerializer( base.ModelSerializer ): """ """ url_for = galaxy.web.url_for - encoded_id = trans.security.encode_id( hda.id ) + encoded_id = self.security.encode_id( hda.id ) urls = { 'purge' : url_for( controller='dataset', action='purge_async', dataset_id=encoded_id ), 'display' : url_for( controller='dataset', action='display', dataset_id=encoded_id, preview=True ), diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index e7a1f3fe4ab..0806cf8d723 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -44,6 +44,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface def most_recent( self, trans, user, **kwargs ): """ """ +#TODO: normalize this if not user: return None if trans.history.deleted else trans.history desc_update_time = self.model_class.table.c.update_time @@ -127,19 +128,19 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): contents_dict = {} - if isinstance( content, trans.app.model.HistoryDatasetAssociation ): + if isinstance( content, model.HistoryDatasetAssociation ): contents_dict = hda_serializer.serialize_to_view( trans, content, view='detailed' ) - elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): + elif isinstance( content, model.HistoryDatasetCollectionAssociation ): try: - service = trans.app.dataset_collections_service + service = self.app.dataset_collections_service dataset_collection_instance = service.get_dataset_collection_instance( trans=trans, instance_type='history', - id=trans.security.encode_id( content.id ), + id=self.app.security.encode_id( content.id ), ) contents_dict = collection_dictifier( dataset_collection_instance, - security=trans.security, parent=dataset_collection_instance.history, view="element" ) + security=self.app.security, parent=dataset_collection_instance.history, view="element" ) except Exception, exc: log.exception( "Error in history API at listing dataset collection: %s", exc ) @@ -165,7 +166,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface """ #TODO: the default flags above may not make a lot of sense (T,T?) state_counts = {} - for state in trans.app.model.Dataset.states.values(): + for state in model.Dataset.states.values(): state_counts[ state ] = 0 #TODO:?? collections and coll. states? @@ -182,13 +183,13 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface """ """ state_ids = {} - for state in trans.app.model.Dataset.states.values(): + for state in model.Dataset.states.values(): state_ids[ state ] = [] #TODO:?? collections and coll. states? for hda in history.datasets: #TODO: do not encode ids at this layer - encoded_id = trans.security.encode_id( hda.id ) + encoded_id = self.app.security.encode_id( hda.id ) state_ids[ hda.state ].append( encoded_id ) return state_ids @@ -196,7 +197,7 @@ class HistoryManager( sharable.SharableModelManager, base.PurgableModelInterface def get_history_state( self, trans, history ): """Returns the history state based on the states of the HDAs it contains. """ - states = trans.app.model.Dataset.states + states = model.Dataset.states # (default to ERROR) state = states.ERROR @@ -307,21 +308,21 @@ class HistorySerializer( sharable.SharableModelSerializer ): def serialize_contents( self, trans, history, key ): for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): contents_dict = {} - if isinstance( content, trans.app.model.HistoryDatasetAssociation ): + if isinstance( content, model.HistoryDatasetAssociation ): contents_dict = self.hda_serializer.serialize_to_view( trans, hda, view='detailed' ) - elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): + elif isinstance( content, model.HistoryDatasetCollectionAssociation ): contents_dict = self.serialize_collection( trans, collection ) contents_dictionaries.append( contents_dict ) def serialize_collection( self, trans, collection ): - service = trans.app.dataset_collections_service + service = self.app.dataset_collections_service dataset_collection_instance = service.get_dataset_collection_instance( trans=trans, instance_type='history', - id=trans.security.encode_id( collection.id ), + id=self.security.encode_id( collection.id ), ) return collection_dictifier( dataset_collection_instance, - security=trans.security, parent=dataset_collection_instance.history, view="element" ) + security=self.app.security, parent=dataset_collection_instance.history, view="element" ) # ============================================================================= diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index cff3c80d328..fa0614df372 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -89,11 +89,11 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ - trans.sa_session.add( item ) + self.app.model.context.add( item ) item.importable = True self.create_unique_slug( trans, item, flush=False ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item def make_non_importable( self, trans, item, flush=True ): @@ -107,10 +107,10 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # self.unpublish( trans, item, flush=False ) # else: # raise exceptions.BadRequest( 'Item must be non-published to be inaccessible', item=item ) - trans.sa_session.add( item ) + self.app.model.context.add( item ) item.importable = False if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item #def _query_importable( self, trans, filters=None, **kwargs ): @@ -142,20 +142,20 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. """ if not item.importable: self.make_importable( trans, item, flush=False ) - trans.sa_session.add( item ) + self.app.model.context.add( item ) item.published = True if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item def unpublish( self, trans, item, flush=True ): """ Set the published flag on `item` to False. """ - trans.sa_session.add( item ) + self.app.model.context.add( item ) item.published = False if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item #def _query_published( self, trans, filters=None, **kwargs ): @@ -202,7 +202,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. Create a share for the given user. """ user_share_assoc = self.user_share_model() - trans.sa_session.add( user_share_assoc ) + self.app.model.context.add( user_share_assoc ) self.associate( trans, user_share_assoc, item ) user_share_assoc.user = user @@ -210,7 +210,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. self.create_unique_slug( trans, item ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return user_share_assoc def unshare_with( self, trans, item, user, flush=True ): @@ -221,9 +221,9 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. return map( lambda user: self.unshare_with( trans, item, user, flush=False ), user ) # Look for and delete sharing relation for user. user_share_assoc = self.get_share_assocs( trans, item, user=user )[0] - trans.sa_session.delete( user_share_assoc ) + self.app.model.context.delete( user_share_assoc ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return user_share_assoc #def _query_shared_with( self, trans, user, filters=None, **kwargs ): @@ -256,14 +256,14 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. raise exceptions.RequestParameterInvalidException( "Invalid slug", slug=new_slug ) # error if slug is already in use - existing_match = ( trans.sa_session.query( self.model_class ) + existing_match = ( self.app.model.context.query( self.model_class ) .filter_by( user=item.user, slug=new_slug, importable=True ) ) if( existing_match.count() != 0 ): raise exceptions.BadRequest( "Slug already exists", slug=new_slug ) item.slug = new_slug if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item def get_unique_slug( self, trans, item ): @@ -295,7 +295,7 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. # add integer to end. new_slug = slug_base count = 1 - while ( trans.sa_session.query( item.__class__ ) + while ( self.app.model.context.query( item.__class__ ) .filter_by( user=item.user, slug=new_slug, importable=True ) .count() != 0 ): # Slug taken; choose a new slug based on count. This approach can @@ -310,9 +310,9 @@ class SharableModelManager( base.ModelManager, base.OwnableModelInterface, base. Set a new, unique slug on the item. """ item.slug = self.get_unique_slug( trans, item ) - trans.sa_session.add( item ) + self.app.model.context.add( item ) if flush: - trans.sa_session.flush() + self.app.model.context.flush() return item #def by_slug( self, trans, user, **kwargs ): diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 449e23915ac..30d9c07fccc 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -43,26 +43,27 @@ class UserManager( base.ModelManager ): user = model.User( email=email, password=password ) user.username = username - if trans.app.config.user_activation_on: + if self.app.config.user_activation_on: user.active = False else: # Activation is off, every new user is active by default. user.active = True - trans.sa_session.add( user ) + self.app.model.context.add( user ) try: - trans.sa_session.flush() + self.app.model.context.flush() #TODO:?? flush needed for permissions below? If not, make optional except sqlalchemy.exceptions.IntegrityError, db_err: raise exceptions.Conflict( db_err.message ) # can throw an sqlalx.IntegrityError if username not unique - trans.app.security_agent.create_private_user_role( user ) + self.app.security_agent.create_private_user_role( user ) +#TODO: any other route to webapp? if trans.webapp.name == 'galaxy': # We set default user permissions, before we log in and set the default history permissions - permissions = trans.app.config.new_user_dataset_access_role_default_private - trans.app.security_agent.user_set_default_permissions( user, default_access_private=permissions ) + permissions = self.app.config.new_user_dataset_access_role_default_private + self.app.security_agent.user_set_default_permissions( user, default_access_private=permissions ) return user #TODO: incorporate security/validate_user_input.py @@ -107,7 +108,7 @@ class UserManager( base.ModelManager ): """ Return a list of admin email addresses from the config file. """ - return [ x.strip() for x in trans.app.config.get( "admin_users", "" ).split( "," ) ] + return [ x.strip() for x in self.app.config.get( "admin_users", "" ).split( "," ) ] def admins( self, trans, filters=None, **kwargs ): """ @@ -179,14 +180,14 @@ class UserManager( base.ModelManager ): Create and return an API key for `user`. """ #TODO: seems like this should return the model - return api_keys.ApiKeyManager( trans.app ).create_api_key( user ) + return api_keys.ApiKeyManager( self.app ).create_api_key( user ) #TODO: possibly move to ApiKeyManager def valid_api_key( self, trans, user ): """ Return this most recent APIKey for this user or None if none have been created. """ - query = ( trans.sa_session.query( model.APIKeys ) + query = ( self.app.model.context.query( model.APIKeys ) .filter_by( user=user ) .order_by( model.APIKeys.create_time ) ) return query.first() @@ -208,7 +209,7 @@ class UserManager( base.ModelManager ): Return the security agent's private role for `user`. """ #TODO: not sure we need to go through sec agent... it's just the first role of type private - return trans.app.security_agent.get_private_user_role( user ) + return self.app.security_agent.get_private_user_role( user ) # ============================================================================= diff --git a/test/unit/managers/test_HDAManager.py b/test/unit/managers/test_HDAManager.py index b8391cbe9b6..566cd096ada 100644 --- a/test/unit/managers/test_HDAManager.py +++ b/test/unit/managers/test_HDAManager.py @@ -99,7 +99,7 @@ class HDAManagerTestCase( BaseTestCase ): hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) self.log( "should be able to copy an HDA" ) - hda2 = self.hda_mgr.copy_hda( self.trans, hda1, history=history1 ) + hda2 = self.hda_mgr.copy( self.trans, hda1, history=history1 ) self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) self.assertEqual( hda2, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda2.id ) ) self.assertEqual( hda2.name, hda1.name ) diff --git a/test/unit/managers/test_HistoryManager.py b/test/unit/managers/test_HistoryManager.py index de40a433e4d..5e380a5e79e 100644 --- a/test/unit/managers/test_HistoryManager.py +++ b/test/unit/managers/test_HistoryManager.py @@ -107,16 +107,16 @@ class HistoryManagerTestCase( BaseTestCase ): self.assertRaises( exceptions.ItemOwnershipException, self.history_mgr.error_unless_owner, self.trans, item1, non_owner ) self.assertRaises( exceptions.ItemOwnershipException, - self.history_mgr.owned_by_id, self.trans, item1.id, non_owner ) + self.history_mgr.get_owned, self.trans, item1.id, non_owner ) self.log( "should not raise an error when checking ownership with owner" ) self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, owner ), item1 ) - self.assertEqual( self.history_mgr.owned_by_id( self.trans, item1.id, owner ), item1 ) + self.assertEqual( self.history_mgr.get_owned( self.trans, item1.id, owner ), item1 ) self.log( "should not raise an error when checking ownership with admin" ) self.assertTrue( self.history_mgr.is_owner( self.trans, item1, self.admin_user ) ) self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, self.admin_user ), item1 ) - self.assertEqual( self.history_mgr.owned_by_id( self.trans, item1.id, self.admin_user ), item1 ) + self.assertEqual( self.history_mgr.get_owned( self.trans, item1.id, self.admin_user ), item1 ) def test_accessible( self ): owner = self.user_mgr.create( self.trans, **user2_data ) @@ -133,16 +133,16 @@ class HistoryManagerTestCase( BaseTestCase ): self.assertRaises( exceptions.ItemAccessibilityException, self.history_mgr.error_unless_accessible, self.trans, item1, non_owner ) self.assertRaises( exceptions.ItemAccessibilityException, - self.history_mgr.accessible_by_id, self.trans, item1.id, non_owner ) + self.history_mgr.get_accessible, self.trans, item1.id, non_owner ) self.log( "should not raise an error when checking ownership with owner" ) self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, owner ), item1 ) - self.assertEqual( self.history_mgr.accessible_by_id( self.trans, item1.id, owner ), item1 ) + self.assertEqual( self.history_mgr.get_accessible( self.trans, item1.id, owner ), item1 ) self.log( "should not raise an error when checking ownership with admin" ) self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, self.admin_user ) ) self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, self.admin_user ), item1 ) - self.assertEqual( self.history_mgr.accessible_by_id( self.trans, item1.id, self.admin_user ), item1 ) + self.assertEqual( self.history_mgr.get_accessible( self.trans, item1.id, self.admin_user ), item1 ) def test_importable( self ): owner = self.user_mgr.create( self.trans, **user2_data ) diff --git a/test/unit/managers/test_UserManager.py b/test/unit/managers/test_UserManager.py index 4a375d76acd..e410cc2e007 100644 --- a/test/unit/managers/test_UserManager.py +++ b/test/unit/managers/test_UserManager.py @@ -118,8 +118,7 @@ class UserManagerTestCase( BaseTestCase ): user3 = self.user_mgr.create( self.trans, **user3_data ) self.log( "should return None if no APIKey has been created" ) - no_api_key_yet = self.user_mgr.api_key( self.trans, user2 ) - self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, None ) + self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ), None ) self.log( "should be able to generate and retrieve valid api key" ) user2_api_key = self.user_mgr.create_api_key( self.trans, user2 ) From 680f6a06541d0cbf93b00bf67828a7faa6877488 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 12 Jan 2015 15:36:05 -0500 Subject: [PATCH 156/473] Fix typo introduced in 67ae665. --- lib/galaxy/tools/toolbox/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tools/toolbox/base.py b/lib/galaxy/tools/toolbox/base.py index 06f89c2baff..1b20c02b1af 100644 --- a/lib/galaxy/tools/toolbox/base.py +++ b/lib/galaxy/tools/toolbox/base.py @@ -394,7 +394,7 @@ class AbstractToolBox( object, Dictifiable ): section.elems.stub_label( section_id ) self._integrated_tool_panel.append_section( key, section ) elif elem.tag == 'label': - section.stub_label( key ) + self._integrated_tool_panel.stub_label( key ) def _write_integrated_tool_panel_config_file( self ): """ From d26c79c88c5700023a1ac68ed5124eca84f7ba84 Mon Sep 17 00:00:00 2001 From: Carl Eberhard Date: Mon, 12 Jan 2015 16:06:43 -0500 Subject: [PATCH 157/473] Fix to UI modularization & trackster: add mod_icon_btn to the proper closure; Fix to TagManager refactoring: remove trans from tagging_common.mako method calls --- client/galaxy/scripts/viz/trackster.js | 6 ++---- static/scripts/packed/viz/trackster.js | 2 +- static/scripts/viz/trackster.js | 6 ++---- templates/tagging_common.mako | 4 ++-- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/client/galaxy/scripts/viz/trackster.js b/client/galaxy/scripts/viz/trackster.js index 67d8dd07c35..6da2b057a0b 100644 --- a/client/galaxy/scripts/viz/trackster.js +++ b/client/galaxy/scripts/viz/trackster.js @@ -12,7 +12,6 @@ require( [ // load js libraries 'utils/utils', - 'mvc/ui/icon-button', 'libs/jquery/jquery.event.drag', 'libs/jquery/jquery.event.hover', 'libs/jquery/jquery.mousewheel', @@ -32,9 +31,8 @@ require( }); // trackster viewer -define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization"], - function(_, base, tracks, visualization) { - +define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization", "mvc/ui/icon-button" ], + function(_, base, tracks, visualization, mod_icon_btn) { /** * User interface controls for trackster */ diff --git a/static/scripts/packed/viz/trackster.js b/static/scripts/packed/viz/trackster.js index e55bdf6a94c..b7051135501 100644 --- a/static/scripts/packed/viz/trackster.js +++ b/static/scripts/packed/viz/trackster.js @@ -1 +1 @@ -var ui=null;var view=null;var browser_router=null;require(["utils/utils","mvc/ui/icon-button","libs/jquery/jquery.event.drag","libs/jquery/jquery.event.hover","libs/jquery/jquery.mousewheel","libs/jquery/jquery-ui","libs/jquery/select2","libs/farbtastic","libs/jquery/jquery.form","libs/jquery/jquery.rating"],function(b,a){b.cssLoadFile("static/style/jquery.rating.css");b.cssLoadFile("static/style/autocomplete_tagging.css");b.cssLoadFile("static/style/jquery-ui/smoothness/jquery-ui.css");b.cssLoadFile("static/style/library.css");b.cssLoadFile("static/style/trackster.css")});define(["libs/underscore","base","viz/trackster/tracks","viz/visualization"],function(b,e,a,c){var d=e.Base.extend({initialize:function(g){this.baseURL=g},save_viz:function(){Galaxy.modal.show({title:"Saving...",body:"progress"});var g=[];$(".bookmark").each(function(){g.push({position:$(this).children(".position").text(),annotation:$(this).children(".annotation").text()})});var h=(view.overview_drawable?view.overview_drawable.config.get_value("name"):null),j={view:view.to_dict(),viewport:{chrom:view.chrom,start:view.low,end:view.high,overview:h},bookmarks:g};return $.ajax({url:galaxy_config.root+"visualization/save",type:"POST",dataType:"json",data:{id:view.vis_id,title:view.config.get_value("name"),dbkey:view.dbkey,type:"trackster",vis_json:JSON.stringify(j)}}).success(function(k){Galaxy.modal.hide();view.vis_id=k.vis_id;view.has_changes=false;window.history.pushState({},"",k.url+window.location.hash)}).error(function(){Galaxy.modal.show({title:"Could Not Save",body:"Could not save visualization. Please try again later.",buttons:{Cancel:function(){Galaxy.modal.hide()}}})})},createButtonMenu:function(){var g=this,h=mod_icon_btn.create_icon_buttons_menu([{icon_class:"plus-button",title:"Add tracks",on_click:function(){c.select_datasets(galaxy_config.root+"visualization/list_current_history_datasets",galaxy_config.root+"api/datasets",{"f-dbkey":view.dbkey},function(j){b.each(j,function(k){view.add_drawable(a.object_from_template(k,view,view))})})}},{icon_class:"block--plus",title:"Add group",on_click:function(){view.add_drawable(new a.DrawableGroup(view,view,{name:"New Group"}))}},{icon_class:"bookmarks",title:"Bookmarks",on_click:function(){force_right_panel(($("div#right").css("right")=="0px"?"hide":"show"))}},{icon_class:"globe",title:"Circster",on_click:function(){window.location=g.baseURL+"visualization/circster?id="+view.vis_id}},{icon_class:"disk--arrow",title:"Save",on_click:function(){g.save_viz()}},{icon_class:"cross-circle",title:"Close",on_click:function(){g.handle_unsaved_changes(view)}}],{tooltip_config:{placement:"bottom"}});this.buttonMenu=h;return h},add_bookmarks:function(){var g=this,h=this.baseURL;Galaxy.modal.show({title:"Select dataset for new bookmarks",body:"progress"});$.ajax({url:this.baseURL+"/visualization/list_histories",data:{"f-dbkey":view.dbkey},error:function(){alert("Grid failed")},success:function(j){Galaxy.modal.show({title:"Select dataset for new bookmarks",body:j,buttons:{Cancel:function(){Galaxy.modal.hide()},Insert:function(){$("input[name=id]:checked,input[name=ldda_ids]:checked").first().each(function(){var k,l=$(this).val();if($(this).attr("name")==="id"){k={hda_id:l}}else{k={ldda_id:l}}$.ajax({url:this.baseURL+"/visualization/bookmarks_from_dataset",data:k,dataType:"json"}).then(function(m){for(i=0;i").addClass("bookmark").appendTo(n);var q=$("
").addClass("position").appendTo(p),m=$("").text(l).appendTo(q).click(function(){view.go_to(l);return false}),k=$("
").text(j).appendTo(p);if(g){var o=$("
").addClass("delete-icon-container").prependTo(p).click(function(){p.slideUp("fast");p.remove();view.has_changes=true;return false}),h=$("").addClass("icon-button delete").appendTo(o);k.make_text_editable({num_rows:3,use_textarea:true,help_text:"Edit bookmark note"}).addClass("annotation")}view.has_changes=true;return p},create_visualization:function(m,g,l,n,k){var j=this,h=new a.TracksterView(b.extend(m,{header:false}));h.editor=true;$.when(h.load_chroms_deferred).then(function(y){if(g){var w=g.chrom,o=g.start,t=g.end,q=g.overview;if(w&&(o!==undefined)&&t){h.change_chrom(w,o,t)}else{h.change_chrom(y[0].chrom)}}else{h.change_chrom(y[0].chrom)}if(l){var r,p,s;for(var u=0;u");$("#right-border").click(function(){view.resize_window()});force_right_panel("hide");if(galaxy_config.app.id){this.view_existing()}else{this.view_new()}},view_existing:function(){var g=galaxy_config.app.viz_config;view=ui.create_visualization({container:$("#center .unified-panel-body"),name:g.title,vis_id:g.vis_id,dbkey:g.dbkey},g.viewport,g.tracks,g.bookmarks,true);this.init_editor()},view_new:function(){var g=this;$.ajax({url:galaxy_config.root+"api/genomes?chrom_info=True",data:{},error:function(){alert("Couldn't create new browser.")},success:function(h){Galaxy.modal.show({title:"New Visualization",body:g.template_view_new(h),buttons:{Cancel:function(){window.location=galaxy_config.root+"visualization/list"},Create:function(){g.create_browser($("#new-title").val(),$("#new-dbkey").val());Galaxy.modal.hide()}}});if(galaxy_config.app.default_dbkey){$("#new-dbkey").val(galaxy_config.app.default_dbkey)}$("#new-title").focus();$("select[name='dbkey']").select2();$("#overlay").css("overflow","auto")}})},template_view_new:function(g){var j='
Is the build not listed here? Add a Custom Build
';return j},create_browser:function(h,g){$(document).trigger("convert_to_values");view=ui.create_visualization({container:$("#center .unified-panel-body"),name:h,dbkey:g},galaxy_config.app.gene_region);this.init_editor();view.editor=true},init_editor:function(){$("#center .unified-panel-title").text(view.config.get_value("name")+" ("+view.dbkey+")");if(galaxy_config.app.add_dataset){$.ajax({url:galaxy_config.root+"api/datasets/"+galaxy_config.app.add_dataset,data:{hda_ldda:"hda",data_type:"track_config"},dataType:"json",success:function(g){view.add_drawable(a.object_from_template(g,view,view))}})}$("#add-bookmark-button").click(function(){var h=view.chrom+":"+view.low+"-"+view.high,g="Bookmark description";return ui.add_bookmark(h,g,true)});ui.init_keyboard_nav(view);$(window).on("beforeunload",function(){if(view.has_changes){return"There are unsaved changes to your visualization that will be lost if you leave this page."}})}});return{TracksterUI:d,GalaxyApp:f}}); \ No newline at end of file +var ui=null;var view=null;var browser_router=null;require(["utils/utils","libs/jquery/jquery.event.drag","libs/jquery/jquery.event.hover","libs/jquery/jquery.mousewheel","libs/jquery/jquery-ui","libs/jquery/select2","libs/farbtastic","libs/jquery/jquery.form","libs/jquery/jquery.rating"],function(b,a){b.cssLoadFile("static/style/jquery.rating.css");b.cssLoadFile("static/style/autocomplete_tagging.css");b.cssLoadFile("static/style/jquery-ui/smoothness/jquery-ui.css");b.cssLoadFile("static/style/library.css");b.cssLoadFile("static/style/trackster.css")});define(["libs/underscore","base","viz/trackster/tracks","viz/visualization","mvc/ui/icon-button"],function(b,f,a,d,c){var e=f.Base.extend({initialize:function(h){this.baseURL=h},save_viz:function(){Galaxy.modal.show({title:"Saving...",body:"progress"});var h=[];$(".bookmark").each(function(){h.push({position:$(this).children(".position").text(),annotation:$(this).children(".annotation").text()})});var j=(view.overview_drawable?view.overview_drawable.config.get_value("name"):null),k={view:view.to_dict(),viewport:{chrom:view.chrom,start:view.low,end:view.high,overview:j},bookmarks:h};return $.ajax({url:galaxy_config.root+"visualization/save",type:"POST",dataType:"json",data:{id:view.vis_id,title:view.config.get_value("name"),dbkey:view.dbkey,type:"trackster",vis_json:JSON.stringify(k)}}).success(function(l){Galaxy.modal.hide();view.vis_id=l.vis_id;view.has_changes=false;window.history.pushState({},"",l.url+window.location.hash)}).error(function(){Galaxy.modal.show({title:"Could Not Save",body:"Could not save visualization. Please try again later.",buttons:{Cancel:function(){Galaxy.modal.hide()}}})})},createButtonMenu:function(){var h=this,j=c.create_icon_buttons_menu([{icon_class:"plus-button",title:"Add tracks",on_click:function(){d.select_datasets(galaxy_config.root+"visualization/list_current_history_datasets",galaxy_config.root+"api/datasets",{"f-dbkey":view.dbkey},function(k){b.each(k,function(l){view.add_drawable(a.object_from_template(l,view,view))})})}},{icon_class:"block--plus",title:"Add group",on_click:function(){view.add_drawable(new a.DrawableGroup(view,view,{name:"New Group"}))}},{icon_class:"bookmarks",title:"Bookmarks",on_click:function(){force_right_panel(($("div#right").css("right")=="0px"?"hide":"show"))}},{icon_class:"globe",title:"Circster",on_click:function(){window.location=h.baseURL+"visualization/circster?id="+view.vis_id}},{icon_class:"disk--arrow",title:"Save",on_click:function(){h.save_viz()}},{icon_class:"cross-circle",title:"Close",on_click:function(){h.handle_unsaved_changes(view)}}],{tooltip_config:{placement:"bottom"}});this.buttonMenu=j;return j},add_bookmarks:function(){var h=this,j=this.baseURL;Galaxy.modal.show({title:"Select dataset for new bookmarks",body:"progress"});$.ajax({url:this.baseURL+"/visualization/list_histories",data:{"f-dbkey":view.dbkey},error:function(){alert("Grid failed")},success:function(k){Galaxy.modal.show({title:"Select dataset for new bookmarks",body:k,buttons:{Cancel:function(){Galaxy.modal.hide()},Insert:function(){$("input[name=id]:checked,input[name=ldda_ids]:checked").first().each(function(){var l,m=$(this).val();if($(this).attr("name")==="id"){l={hda_id:m}}else{l={ldda_id:m}}$.ajax({url:this.baseURL+"/visualization/bookmarks_from_dataset",data:l,dataType:"json"}).then(function(n){for(i=0;i").addClass("bookmark").appendTo(o);var r=$("
").addClass("position").appendTo(q),n=$("").text(m).appendTo(r).click(function(){view.go_to(m);return false}),l=$("
").text(k).appendTo(q);if(h){var p=$("
").addClass("delete-icon-container").prependTo(q).click(function(){q.slideUp("fast");q.remove();view.has_changes=true;return false}),j=$("").addClass("icon-button delete").appendTo(p);l.make_text_editable({num_rows:3,use_textarea:true,help_text:"Edit bookmark note"}).addClass("annotation")}view.has_changes=true;return q},create_visualization:function(n,h,m,o,l){var k=this,j=new a.TracksterView(b.extend(n,{header:false}));j.editor=true;$.when(j.load_chroms_deferred).then(function(z){if(h){var x=h.chrom,p=h.start,u=h.end,r=h.overview;if(x&&(p!==undefined)&&u){j.change_chrom(x,p,u)}else{j.change_chrom(z[0].chrom)}}else{j.change_chrom(z[0].chrom)}if(m){var s,q,t;for(var v=0;v");$("#right-border").click(function(){view.resize_window()});force_right_panel("hide");if(galaxy_config.app.id){this.view_existing()}else{this.view_new()}},view_existing:function(){var h=galaxy_config.app.viz_config;view=ui.create_visualization({container:$("#center .unified-panel-body"),name:h.title,vis_id:h.vis_id,dbkey:h.dbkey},h.viewport,h.tracks,h.bookmarks,true);this.init_editor()},view_new:function(){var h=this;$.ajax({url:galaxy_config.root+"api/genomes?chrom_info=True",data:{},error:function(){alert("Couldn't create new browser.")},success:function(j){Galaxy.modal.show({title:"New Visualization",body:h.template_view_new(j),buttons:{Cancel:function(){window.location=galaxy_config.root+"visualization/list"},Create:function(){h.create_browser($("#new-title").val(),$("#new-dbkey").val());Galaxy.modal.hide()}}});if(galaxy_config.app.default_dbkey){$("#new-dbkey").val(galaxy_config.app.default_dbkey)}$("#new-title").focus();$("select[name='dbkey']").select2();$("#overlay").css("overflow","auto")}})},template_view_new:function(h){var k='
Is the build not listed here? Add a Custom Build
';return k},create_browser:function(j,h){$(document).trigger("convert_to_values");view=ui.create_visualization({container:$("#center .unified-panel-body"),name:j,dbkey:h},galaxy_config.app.gene_region);this.init_editor();view.editor=true},init_editor:function(){$("#center .unified-panel-title").text(view.config.get_value("name")+" ("+view.dbkey+")");if(galaxy_config.app.add_dataset){$.ajax({url:galaxy_config.root+"api/datasets/"+galaxy_config.app.add_dataset,data:{hda_ldda:"hda",data_type:"track_config"},dataType:"json",success:function(h){view.add_drawable(a.object_from_template(h,view,view))}})}$("#add-bookmark-button").click(function(){var j=view.chrom+":"+view.low+"-"+view.high,h="Bookmark description";return ui.add_bookmark(j,h,true)});ui.init_keyboard_nav(view);$(window).on("beforeunload",function(){if(view.has_changes){return"There are unsaved changes to your visualization that will be lost if you leave this page."}})}});return{TracksterUI:e,GalaxyApp:g}}); \ No newline at end of file diff --git a/static/scripts/viz/trackster.js b/static/scripts/viz/trackster.js index 67d8dd07c35..6da2b057a0b 100644 --- a/static/scripts/viz/trackster.js +++ b/static/scripts/viz/trackster.js @@ -12,7 +12,6 @@ require( [ // load js libraries 'utils/utils', - 'mvc/ui/icon-button', 'libs/jquery/jquery.event.drag', 'libs/jquery/jquery.event.hover', 'libs/jquery/jquery.mousewheel', @@ -32,9 +31,8 @@ require( }); // trackster viewer -define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization"], - function(_, base, tracks, visualization) { - +define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization", "mvc/ui/icon-button" ], + function(_, base, tracks, visualization, mod_icon_btn) { /** * User interface controls for trackster */ diff --git a/templates/tagging_common.mako b/templates/tagging_common.mako index 35ccfb4550a..b85388b793f 100644 --- a/templates/tagging_common.mako +++ b/templates/tagging_common.mako @@ -89,7 +89,7 @@ <%def name="render_tool_tagging_elements()"> <% elt_id = int ( floor ( random()*maxint ) ) - tags = trans.app.tag_handler.get_tool_tags( trans ) + tags = trans.app.tag_handler.get_tool_tags() %> ${self.render_tagging_element_html(elt_id=elt_id, \ tags=tags, \ @@ -105,7 +105,7 @@ ## Build HTML. <% elt_id = int ( floor ( random()*maxint ) ) - community_tags = trans.app.tag_handler.get_community_tags( trans, item=tagged_item, limit=5 ) + community_tags = trans.app.tag_handler.get_community_tags( item=tagged_item, limit=5 ) %> ${self.render_tagging_element_html(elt_id=elt_id, \ tags=community_tags, \ From 5b96f80b48b04bf3391ad7d4719760617f793e93 Mon Sep 17 00:00:00 2001 From: Bjoern Gruening Date: Mon, 12 Jan 2015 22:36:20 +0100 Subject: [PATCH 158/473] Add CompressedArchive as datatype and do not uncomress it during upload. --- config/datatypes_conf.xml.sample | 2 + lib/galaxy/datatypes/binary.py | 24 +++ tools/data_source/upload.py | 277 ++++++++++++++++--------------- 3 files changed, 167 insertions(+), 136 deletions(-) diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index 50b55b28cca..14acb1f6c54 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -140,6 +140,8 @@ + + diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 19669f6dd02..8622f88aa09 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -106,6 +106,30 @@ class Ab1( Binary ): Binary.register_unsniffable_binary_ext("ab1") +class CompressedArchive( Binary ): + """ + Class describing an compressed binary file + This class can be sublass'ed to implement archive filetypes that will not be unpacked by upload.py. + """ + file_ext = "compressed_archive" + compressed = True + + def set_peek( self, dataset, is_multi_byte=False ): + if not dataset.dataset.purged: + dataset.peek = "Compressed binary file" + dataset.blurb = data.nice_size( dataset.get_size() ) + else: + dataset.peek = 'file does not exist' + dataset.blurb = 'file purged from disk' + + def display_peek( self, dataset ): + try: + return dataset.peek + except: + return "Compressed binary file (%s)" % ( data.nice_size( dataset.get_size() ) ) + +Binary.register_unsniffable_binary_ext("compressed_archive") + class GenericAsn1Binary( Binary ): """Class for generic ASN.1 binary format""" diff --git a/tools/data_source/upload.py b/tools/data_source/upload.py index 0c968f1e5ba..c170f124800 100644 --- a/tools/data_source/upload.py +++ b/tools/data_source/upload.py @@ -120,171 +120,176 @@ def add_file( dataset, registry, json_file, output_path ): data_type = type_info[0] ext = type_info[1] if not data_type: - # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress - is_gzipped, is_valid = check_gzip( dataset.path ) - if is_gzipped and not is_valid: - file_err( 'The gzipped uploaded file contains inappropriate content', dataset, json_file ) - return - elif is_gzipped and is_valid: - if link_data_only == 'copy_files': - # We need to uncompress the temp_name file, but BAM files must remain compressed in the BGZF format - CHUNK_SIZE = 2**20 # 1Mb - fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_gunzip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) - gzipped_file = gzip.GzipFile( dataset.path, 'rb' ) - while 1: - try: - chunk = gzipped_file.read( CHUNK_SIZE ) - except IOError: - os.close( fd ) - os.remove( uncompressed ) - file_err( 'Problem decompressing gzipped data', dataset, json_file ) - return - if not chunk: - break - os.write( fd, chunk ) - os.close( fd ) - gzipped_file.close() - # Replace the gzipped file with the decompressed file if it's safe to do so - if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place: - dataset.path = uncompressed - else: - shutil.move( uncompressed, dataset.path ) - os.chmod(dataset.path, 0644) - dataset.name = dataset.name.rstrip( '.gz' ) - data_type = 'gzip' - if not data_type and bz2 is not None: - # See if we have a bz2 file, much like gzip - is_bzipped, is_valid = check_bz2( dataset.path ) - if is_bzipped and not is_valid: + root_datatype = registry.get_datatype_by_extension( dataset.file_type ) + if getattr( root_datatype, 'compressed', False ): + data_type = 'compressed archive' + ext = dataset.file_type + else: + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress + is_gzipped, is_valid = check_gzip( dataset.path ) + if is_gzipped and not is_valid: file_err( 'The gzipped uploaded file contains inappropriate content', dataset, json_file ) return - elif is_bzipped and is_valid: + elif is_gzipped and is_valid: if link_data_only == 'copy_files': - # We need to uncompress the temp_name file + # We need to uncompress the temp_name file, but BAM files must remain compressed in the BGZF format CHUNK_SIZE = 2**20 # 1Mb - fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_bunzip2_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) - bzipped_file = bz2.BZ2File( dataset.path, 'rb' ) + fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_gunzip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) + gzipped_file = gzip.GzipFile( dataset.path, 'rb' ) while 1: try: - chunk = bzipped_file.read( CHUNK_SIZE ) + chunk = gzipped_file.read( CHUNK_SIZE ) except IOError: os.close( fd ) os.remove( uncompressed ) - file_err( 'Problem decompressing bz2 compressed data', dataset, json_file ) + file_err( 'Problem decompressing gzipped data', dataset, json_file ) return if not chunk: break os.write( fd, chunk ) os.close( fd ) - bzipped_file.close() - # Replace the bzipped file with the decompressed file if it's safe to do so + gzipped_file.close() + # Replace the gzipped file with the decompressed file if it's safe to do so if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place: dataset.path = uncompressed else: shutil.move( uncompressed, dataset.path ) os.chmod(dataset.path, 0644) - dataset.name = dataset.name.rstrip( '.bz2' ) - data_type = 'bz2' - if not data_type: - # See if we have a zip archive - is_zipped = check_zip( dataset.path ) - if is_zipped: - if link_data_only == 'copy_files': - CHUNK_SIZE = 2**20 # 1Mb - uncompressed = None - uncompressed_name = None - unzipped = False - z = zipfile.ZipFile( dataset.path ) - for name in z.namelist(): - if name.endswith('/'): - continue - if unzipped: - stdout = 'ZIP file contained more than one file, only the first file was added to Galaxy.' - break - fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_zip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) - if sys.version_info[:2] >= ( 2, 6 ): - zipped_file = z.open( name ) - while 1: - try: - chunk = zipped_file.read( CHUNK_SIZE ) - except IOError: - os.close( fd ) - os.remove( uncompressed ) - file_err( 'Problem decompressing zipped data', dataset, json_file ) - return - if not chunk: - break - os.write( fd, chunk ) - os.close( fd ) - zipped_file.close() - uncompressed_name = name - unzipped = True - else: - # python < 2.5 doesn't have a way to read members in chunks(!) + dataset.name = dataset.name.rstrip( '.gz' ) + data_type = 'gzip' + if not data_type and bz2 is not None: + # See if we have a bz2 file, much like gzip + is_bzipped, is_valid = check_bz2( dataset.path ) + if is_bzipped and not is_valid: + file_err( 'The gzipped uploaded file contains inappropriate content', dataset, json_file ) + return + elif is_bzipped and is_valid: + if link_data_only == 'copy_files': + # We need to uncompress the temp_name file + CHUNK_SIZE = 2**20 # 1Mb + fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_bunzip2_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) + bzipped_file = bz2.BZ2File( dataset.path, 'rb' ) + while 1: try: - outfile = open( uncompressed, 'wb' ) - outfile.write( z.read( name ) ) - outfile.close() - uncompressed_name = name - unzipped = True + chunk = bzipped_file.read( CHUNK_SIZE ) except IOError: os.close( fd ) os.remove( uncompressed ) - file_err( 'Problem decompressing zipped data', dataset, json_file ) + file_err( 'Problem decompressing bz2 compressed data', dataset, json_file ) return - z.close() - # Replace the zipped file with the decompressed file if it's safe to do so - if uncompressed is not None: + if not chunk: + break + os.write( fd, chunk ) + os.close( fd ) + bzipped_file.close() + # Replace the bzipped file with the decompressed file if it's safe to do so if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place: dataset.path = uncompressed else: shutil.move( uncompressed, dataset.path ) os.chmod(dataset.path, 0644) - dataset.name = uncompressed_name - data_type = 'zip' - if not data_type: - # TODO refactor this logic. check_binary isn't guaranteed to be - # correct since it only looks at whether the first 100 chars are - # printable or not. If someone specifies a known unsniffable - # binary datatype and check_binary fails, the file gets mangled. - if check_binary( dataset.path ) or Binary.is_ext_unsniffable(dataset.file_type): - # We have a binary dataset, but it is not Bam, Sff or Pdf - data_type = 'binary' - #binary_ok = False - parts = dataset.name.split( "." ) - if len( parts ) > 1: - ext = parts[-1].strip().lower() - if not Binary.is_ext_unsniffable(ext): - file_err( 'The uploaded binary file contains inappropriate content', dataset, json_file ) - return - elif Binary.is_ext_unsniffable(ext) and dataset.file_type != ext: - err_msg = "You must manually set the 'File Format' to '%s' when uploading %s files." % ( ext.capitalize(), ext ) - file_err( err_msg, dataset, json_file ) - return - if not data_type: - # We must have a text file - if check_html( dataset.path ): - file_err( 'The uploaded file contains inappropriate HTML content', dataset, json_file ) - return - if data_type != 'binary': - if link_data_only == 'copy_files': - if dataset.type in ( 'server_dir', 'path_paste' ) and data_type not in [ 'gzip', 'bz2', 'zip' ]: - in_place = False - # Convert universal line endings to Posix line endings, but allow the user to turn it off, - # so that is becomes possible to upload gzip, bz2 or zip files with binary data without - # corrupting the content of those files. - if dataset.to_posix_lines: - tmpdir = output_adjacent_tmpdir( output_path ) - tmp_prefix = 'data_id_%s_convert_' % dataset.dataset_id - if dataset.space_to_tab: - line_count, converted_path = sniff.convert_newlines_sep2tabs( dataset.path, in_place=in_place, tmp_dir=tmpdir, tmp_prefix=tmp_prefix ) - else: - line_count, converted_path = sniff.convert_newlines( dataset.path, in_place=in_place, tmp_dir=tmpdir, tmp_prefix=tmp_prefix ) - if dataset.file_type == 'auto': - ext = sniff.guess_ext( dataset.path, registry.sniff_order ) - else: - ext = dataset.file_type - data_type = ext + dataset.name = dataset.name.rstrip( '.bz2' ) + data_type = 'bz2' + if not data_type: + # See if we have a zip archive + is_zipped = check_zip( dataset.path ) + if is_zipped: + if link_data_only == 'copy_files': + CHUNK_SIZE = 2**20 # 1Mb + uncompressed = None + uncompressed_name = None + unzipped = False + z = zipfile.ZipFile( dataset.path ) + for name in z.namelist(): + if name.endswith('/'): + continue + if unzipped: + stdout = 'ZIP file contained more than one file, only the first file was added to Galaxy.' + break + fd, uncompressed = tempfile.mkstemp( prefix='data_id_%s_upload_zip_' % dataset.dataset_id, dir=os.path.dirname( output_path ), text=False ) + if sys.version_info[:2] >= ( 2, 6 ): + zipped_file = z.open( name ) + while 1: + try: + chunk = zipped_file.read( CHUNK_SIZE ) + except IOError: + os.close( fd ) + os.remove( uncompressed ) + file_err( 'Problem decompressing zipped data', dataset, json_file ) + return + if not chunk: + break + os.write( fd, chunk ) + os.close( fd ) + zipped_file.close() + uncompressed_name = name + unzipped = True + else: + # python < 2.5 doesn't have a way to read members in chunks(!) + try: + outfile = open( uncompressed, 'wb' ) + outfile.write( z.read( name ) ) + outfile.close() + uncompressed_name = name + unzipped = True + except IOError: + os.close( fd ) + os.remove( uncompressed ) + file_err( 'Problem decompressing zipped data', dataset, json_file ) + return + z.close() + # Replace the zipped file with the decompressed file if it's safe to do so + if uncompressed is not None: + if dataset.type in ( 'server_dir', 'path_paste' ) or not in_place: + dataset.path = uncompressed + else: + shutil.move( uncompressed, dataset.path ) + os.chmod(dataset.path, 0644) + dataset.name = uncompressed_name + data_type = 'zip' + if not data_type: + # TODO refactor this logic. check_binary isn't guaranteed to be + # correct since it only looks at whether the first 100 chars are + # printable or not. If someone specifies a known unsniffable + # binary datatype and check_binary fails, the file gets mangled. + if check_binary( dataset.path ) or Binary.is_ext_unsniffable(dataset.file_type): + # We have a binary dataset, but it is not Bam, Sff or Pdf + data_type = 'binary' + #binary_ok = False + parts = dataset.name.split( "." ) + if len( parts ) > 1: + ext = parts[-1].strip().lower() + if not Binary.is_ext_unsniffable(ext): + file_err( 'The uploaded binary file contains inappropriate content', dataset, json_file ) + return + elif Binary.is_ext_unsniffable(ext) and dataset.file_type != ext: + err_msg = "You must manually set the 'File Format' to '%s' when uploading %s files." % ( ext.capitalize(), ext ) + file_err( err_msg, dataset, json_file ) + return + if not data_type: + # We must have a text file + if check_html( dataset.path ): + file_err( 'The uploaded file contains inappropriate HTML content', dataset, json_file ) + return + if data_type != 'binary': + if link_data_only == 'copy_files': + if dataset.type in ( 'server_dir', 'path_paste' ) and data_type not in [ 'gzip', 'bz2', 'zip' ]: + in_place = False + # Convert universal line endings to Posix line endings, but allow the user to turn it off, + # so that is becomes possible to upload gzip, bz2 or zip files with binary data without + # corrupting the content of those files. + if dataset.to_posix_lines: + tmpdir = output_adjacent_tmpdir( output_path ) + tmp_prefix = 'data_id_%s_convert_' % dataset.dataset_id + if dataset.space_to_tab: + line_count, converted_path = sniff.convert_newlines_sep2tabs( dataset.path, in_place=in_place, tmp_dir=tmpdir, tmp_prefix=tmp_prefix ) + else: + line_count, converted_path = sniff.convert_newlines( dataset.path, in_place=in_place, tmp_dir=tmpdir, tmp_prefix=tmp_prefix ) + if dataset.file_type == 'auto': + ext = sniff.guess_ext( dataset.path, registry.sniff_order ) + else: + ext = dataset.file_type + data_type = ext # Save job info for the framework if ext == 'auto' and dataset.ext: ext = dataset.ext From c147aadcb3a0b625ff76e953c968b9610bcd9e1f Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Mon, 12 Jan 2015 17:47:12 -0500 Subject: [PATCH 159/473] Trailing whitespace in parameters.basic. --- lib/galaxy/tools/parameters/basic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 7773fdd1175..cea2e451109 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -487,7 +487,7 @@ class BooleanToolParameter( ToolParameter ): d['truevalue'] = self.truevalue d['falsevalue'] = self.falsevalue return d - + @property def legal_values( self ): return [ self.truevalue, self.falsevalue ] @@ -670,7 +670,7 @@ class BaseURLToolParameter( ToolParameter ): def get_label( self ): # BaseURLToolParameters are ultimately "hidden" parameters return None - + def to_dict( self, trans, view='collection', value_mapper=None ): d = super( BaseURLToolParameter, self ).to_dict( trans ) d['value'] = self.get_value( trans ) @@ -1033,7 +1033,7 @@ class SelectToolParameter( ToolParameter ): # Found selected option. value = option[1] d[ 'value' ] = value - + d['display'] = self.display d['multiple'] = self.multiple d['is_dynamic'] = self.is_dynamic @@ -1114,7 +1114,7 @@ class GenomeBuildParameter( SelectToolParameter ): 'display' : self.display, 'multiple' : self.multiple }) - + return d def _get_dbkey_names( self, trans=None ): @@ -1316,10 +1316,10 @@ class ColumnListParameter( SelectToolParameter ): # add data reference d['data_ref'] = self.data_ref - + # add numerical flag d['numerical'] = self.numerical - + # return return d @@ -2111,7 +2111,7 @@ class DataToolParameter( BaseDataToolParameter ): d['multiple'] = self.multiple d['is_dynamic'] = True d['options'] = {'hda': [], 'hdca': []} - + # return default content if context is not available if other_values is None: return d @@ -2120,7 +2120,7 @@ class DataToolParameter( BaseDataToolParameter ): dataset_matcher = DatasetMatcher( trans, self, None, other_values ) history = trans.history multiple = self.multiple - + # add datasets for hda in history.active_datasets_children_and_roles: match = dataset_matcher.hda_match( hda ) @@ -2133,7 +2133,7 @@ class DataToolParameter( BaseDataToolParameter ): 'name' : m.name, 'src' : 'hda' }) - + # add dataset collections dataset_collection_matcher = DatasetCollectionMatcher( dataset_matcher ) for hdca in history.active_dataset_collections: @@ -2352,7 +2352,7 @@ class DataCollectionToolParameter( BaseDataToolParameter ): d['multiple'] = self.multiple d['is_dynamic'] = False d['options'] = {'hda': [], 'hdca': []} - + # return default content if context is not available if other_values is None: return d @@ -2360,7 +2360,7 @@ class DataCollectionToolParameter( BaseDataToolParameter ): # prepare dataset/collection matching dataset_matcher = DatasetMatcher( trans, self, None, other_values ) history = trans.history - + # append directly matched collections for hdca in self.match_collections( trans, history, dataset_matcher ): d['options']['hdca'].append({ From 18e0661dd5f2733ada24877e296381c13266c9d1 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Mon, 12 Jan 2015 17:50:10 -0500 Subject: [PATCH 160/473] Make SelectToolParameter behave the same way as DrillDownSelectToolParameter for (deprecated) dynamic_options w/ trans. TODO: Revisit this and figure out what the replacement for dynamic_options is. --- lib/galaxy/tools/parameters/basic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index cea2e451109..9804937075b 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -782,8 +782,11 @@ class SelectToolParameter( ToolParameter ): if self.options: return self.options.get_options( trans, other_values ) elif self.dynamic_options: + call_other_values = {"__trans__": trans} + if other_values: + call_other_values.update( other_values.dict ) try: - return eval( self.dynamic_options, self.tool.code_namespace, other_values ) + return eval( self.dynamic_options, self.tool.code_namespace, call_other_values ) except Exception: return [] else: From adc2d60bedb8276f287d27aab2ed63c55a7625cc Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Tue, 13 Jan 2015 01:09:03 -0500 Subject: [PATCH 161/473] ToolForm/Workflow: Add post job action forms, enable custom tool form sections --- .../scripts/galaxy.workflow_editor.canvas.js | 19 +- .../scripts/mvc/tools/tools-form-base.js | 5 + .../scripts/mvc/tools/tools-form-workflow.js | 200 +++++++++++++++++- .../galaxy/scripts/mvc/tools/tools-section.js | 106 ++++++++-- client/galaxy/scripts/mvc/tools/tools-tree.js | 31 +-- client/galaxy/scripts/mvc/ui/ui-misc.js | 6 + client/galaxy/scripts/mvc/ui/ui-portlet.js | 28 +-- .../visualizations/charts/static/build-app.js | 6 +- lib/galaxy/tools/parameters/basic.py | 3 + .../scripts/galaxy.workflow_editor.canvas.js | 19 +- static/scripts/mvc/tools/tools-form-base.js | 5 + .../scripts/mvc/tools/tools-form-workflow.js | 200 +++++++++++++++++- static/scripts/mvc/tools/tools-section.js | 106 ++++++++-- static/scripts/mvc/tools/tools-tree.js | 31 +-- static/scripts/mvc/ui/ui-misc.js | 6 + static/scripts/mvc/ui/ui-portlet.js | 28 +-- .../packed/galaxy.workflow_editor.canvas.js | 2 +- .../packed/mvc/tools/tools-form-base.js | 2 +- .../packed/mvc/tools/tools-form-workflow.js | 2 +- .../scripts/packed/mvc/tools/tools-section.js | 2 +- static/scripts/packed/mvc/tools/tools-tree.js | 2 +- static/scripts/packed/mvc/ui/ui-misc.js | 2 +- static/scripts/packed/mvc/ui/ui-portlet.js | 2 +- static/style/blue/base.css | 13 +- static/style/src/less/base.less | 1 - static/style/src/less/ui.less | 11 + templates/webapps/galaxy/workflow/editor.mako | 3 + .../galaxy/workflow/editor_tool_form.mako | 6 - 28 files changed, 712 insertions(+), 135 deletions(-) diff --git a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js index 918ecf0d124..a7143adb474 100644 --- a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js +++ b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js @@ -1089,7 +1089,11 @@ $.extend( Workflow.prototype, { if ( this.active_node != node ) { this.check_changes_in_active_form(); this.clear_active_node(); - parent.show_form_for_tool( node.form_html + node.tooltip, node ); + if (parent.__NEWTOOLFORM__) { + parent.show_form_for_tool( node.form_html, node ); + } else { + parent.show_form_for_tool( node.form_html + node.tooltip, node ); + } node.make_active(); this.active_node = node; } @@ -1099,7 +1103,11 @@ $.extend( Workflow.prototype, { if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) { // Reactive with new form_html this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped) - parent.show_form_for_tool( node.form_html + node.tooltip, node ); + if (parent.__NEWTOOLFORM__) { + parent.show_form_for_tool( node.form_html, node ); + } else { + parent.show_form_for_tool( node.form_html + node.tooltip, node ); + } } }, layout : function () { @@ -1305,13 +1313,6 @@ function add_node( type, title_text, tool_id ) { return node; } -function update_node( data ) { - var node = workflow.active_node; - if (node) { - node.update_field_data(data, true); - } -} - var ext_to_type = null; var type_to_type = null; diff --git a/client/galaxy/scripts/mvc/tools/tools-form-base.js b/client/galaxy/scripts/mvc/tools/tools-form-base.js index 71eecb21069..9dd6bf64903 100644 --- a/client/galaxy/scripts/mvc/tools/tools-form-base.js +++ b/client/galaxy/scripts/mvc/tools/tools-form-base.js @@ -276,6 +276,11 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', buttons : this.buttons }); + // remove padding + if (this.options.workflow) { + this.portlet.$content.css('padding', '0px'); + } + // append message this.portlet.append(this.message.$el, true); diff --git a/client/galaxy/scripts/mvc/tools/tools-form-workflow.js b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js index c9a45ee795e..8224fa8f8fa 100644 --- a/client/galaxy/scripts/mvc/tools/tools-form-workflow.js +++ b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js @@ -7,8 +7,199 @@ define(['utils/utils', 'mvc/tools/tools-form-base'], // create form view var View = ToolFormBase.extend({ initialize: function(options) { + // link with node representation in workflow module + this.node = workflow.active_node; + if (!this.node) { + console.debug('FAILED - tools-form-workflow:initialize() - Node not found in workflow.'); + return; + } + + // initialize parameters this.workflow = true; - ToolFormBase.prototype.initialize.call(this, options); + this.options = options; + + // load extension + var self = this; + Utils.get({ + url : galaxy_config.root + 'api/datatypes', + cache : true, + success : function(datatypes) { + self.datatypes = datatypes; + self._makeSections(options.inputs); + ToolFormBase.prototype.initialize.call(self, options); + } + }); + }, + + /** Builds all sub sections + */ + _makeSections: function(inputs){ + // for annotation + inputs[Utils.uuid()] = { + label : 'Edit Step Attributes', + type : 'section', + inputs : [{ + label : 'Annotation / Notes', + name : 'annotation', + type : 'text', + area : true, + help : 'Add an annotation or notes to this step; annotations are available when a workflow is viewed.', + value : this.node.annotation + }] + } + + // for actions + this.post_job_actions = this.node.post_job_actions; + for (var i in this.node.output_terminals) { + inputs[Utils.uuid()] = this._makeSection(i); + } + }, + + /** Builds sub section with step actions/annotation + */ + _makeSection: function(output_id){ + // format datatypes + var extensions = []; + for (key in this.datatypes) { + extensions.push({ + 0 : this.datatypes[key], + 1 : this.datatypes[key] + }); + } + + // sort extensions + extensions.sort(function(a, b) { + return a.label > b.label ? 1 : a.label < b.label ? -1 : 0; + }); + + // add additional options + extensions.unshift({ + 0 : 'Sequences', + 1 : 'Sequences' + }); + extensions.unshift({ + 0 : 'Roadmaps', + 1 : 'Roadmaps' + }); + extensions.unshift({ + 0 : 'Leave unchanged', + 1 : 'None' + }); + + // create custom sub section + var input_config = { + label : 'Edit Step Action: \'' + output_id + '\'', + type : 'section', + inputs : [{ + action : 'RenameDatasetAction', + argument : 'newname', + label : 'Rename dataset', + type : 'text', + value : '', + ignore : '', + help : 'This action will rename the result dataset. Click here for more information.' + },{ + action : 'ChangeDatatypeAction', + argument : 'newtype', + label : 'Change datatype', + type : 'select', + ignore : 'None', + options : extensions, + help : 'This action will change the datatype of the output to the indicated value.' + },{ + action : 'TagDatasetAction', + argument : 'tags', + label : 'Tags', + type : 'text', + value : '', + ignore : '', + help : 'This action will set tags for the dataset.' + },{ + label : 'Assign columns', + type : 'section', + inputs : [{ + action : 'ColumnSetAction', + argument : 'chromCol', + label : 'Chrom column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'startCol', + label : 'Start column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'endCol', + label : 'End column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'strandCol', + label : 'Strand column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'nameCol', + label : 'Name column', + type : 'text', + value : '', + ignore : '' + }], + help : 'This action will set column assignments in the output dataset. Blank fields are ignored.' + },{ + action : 'EmailAction', + label : 'Email notification', + type : 'boolean', + value : 'false', + ignore : 'false', + help : 'This action will send an email notifying you when the job is done.' + },{ + action : 'DeleteIntermediatesAction', + label : 'Delete non-outputs', + type : 'boolean', + value : 'false', + ignore : 'false', + help : 'All non-output steps of this workflow will have datasets deleted if they are no longer being used as job inputs when the job this action is attached to is finished. You *must* be using workflow outputs (the snowflake) in your workflow for this to have any effect.' + }] + }; + + // visit input nodes and enrich by name/value pairs from server data + var self = this; + function visit (inputs) { + for (var i in inputs) { + var input = inputs[i]; + if (input.action) { + input.name = 'pja__' + output_id + '__' + input.action; + if (input.argument) { + input.name += '__' + input.argument; + } + var d = self.post_job_actions[input.action + output_id]; + if (d) { + if (input.argument) { + input.value = d.action_arguments && d.action_arguments[input.argument] || input.value; + } else { + input.value = 'true'; + } + } + } else { + if (input.inputs) { + visit(input.inputs); + } + } + } + } + visit(input_config.inputs); + + // return final configuration + return input_config; }, /** Builds a new model through api call and recreates the entire form @@ -38,15 +229,16 @@ define(['utils/utils', 'mvc/tools/tools-form-base'], type : 'GET', url : model_url, data : current_state, - success : function(node) { - parent.update_node(node); + success : function(data) { + // update node in workflow module + self.node.update_field_data(data, true); // process completed self.deferred.done(process_id); // log success console.debug('tools-form::_refreshForm() - States refreshed.'); - console.debug(node); + console.debug(data); }, error : function(response) { // process completed diff --git a/client/galaxy/scripts/mvc/tools/tools-section.js b/client/galaxy/scripts/mvc/tools/tools-section.js index 8038a403079..5f31bb2850c 100644 --- a/client/galaxy/scripts/mvc/tools/tools-section.js +++ b/client/galaxy/scripts/mvc/tools/tools-section.js @@ -1,8 +1,8 @@ /** This class creates a tool form section and populates it with input elements. It also handles repeat blocks and conditionals by recursively creating new sub sections. New input elements can be plugged in by adding cases to the switch block defined in the _createField() function. */ -define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'], - function(Utils, Table, Ui, Repeat, SelectContent, InputElement) { +define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/ui/ui-portlet', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'], + function(Utils, Table, Ui, Portlet, Repeat, SelectContent, InputElement) { // create form view var View = Backbone.View.extend({ @@ -36,13 +36,13 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // load settings elements into table for (var i in this.inputs) { - this._add(this.inputs[i]); + this.add(this.inputs[i]); } }, /** Add a new input element */ - _add: function(input) { + add: function(input) { // link this var self = this; @@ -66,6 +66,10 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep case 'repeat': this._addRepeat(input_def); break; + // customized section + case 'section': + this._addSection(input_def); + break; // default single element row default: this._addRow(input_def); @@ -103,8 +107,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // check if non-hidden elements exist var nonhidden = false; for (var j in case_def.inputs) { - var type = case_def.inputs[j].type; - if (type && type !== 'hidden') { + if (!case_def.inputs[j].hidden) { nonhidden = true; break; } @@ -244,6 +247,58 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep this.table.append(input_def.id); }, + /** Add a customized section + */ + _addSection: function(input_def) { + // link this + var self = this; + + // create sub section + var sub_section = new View(self.app, { + inputs : input_def.inputs, + cls : 'ui-table-plain' + }); + + // delete button + var button_visible = new Ui.ButtonIcon({ + icon : 'fa-eye-slash', + tooltip : 'Show/hide section', + cls : 'ui-button-icon-plain' + }); + + // create portlet for sub section + var portlet = new Portlet.View({ + title : input_def.label, + cls : 'ui-portlet-section', + operations : { + button_visible: button_visible + } + }); + portlet.append(sub_section.$el); + + // add event handler visibility button + var visible = false; + portlet.$content.hide(); + portlet.$header.css('cursor', 'pointer'); + portlet.$header.on('click', function() { + if (visible) { + visible = false; + portlet.$content.hide(); + button_visible.setIcon('fa-eye-slash'); + } else { + visible = true; + portlet.$content.fadeIn('fast'); + button_visible.setIcon('fa-eye'); + } + }); + + // create table row + this.table.add(portlet.$el); + + // append row to table + this.table.append(input_def.id); + }, + /** Add a single input field element */ _addRow: function(input_def) { @@ -273,6 +328,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // append to table this.table.append(id); + // hide row if neccessary + if (input_def.hidden) { + this.table.get(id).hide(); + } + // return created field return field; }, @@ -316,7 +376,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep field = this._fieldHidden(input_def); break; - // hidden field + // hidden data field case 'hidden_data': field = this._fieldHidden(input_def); break; @@ -465,9 +525,9 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep _fieldText : function(input_def) { var self = this; return new Ui.Input({ - id : 'field-' + input_def.id, - area : input_def.area, - onchange: function() { + id : 'field-' + input_def.id, + area : input_def.area, + onchange : function() { self.app.trigger('refresh'); } }); @@ -476,11 +536,15 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Slider field */ _fieldSlider: function(input_def) { + var self = this; return new Ui.Slider.View({ - id : 'field-' + input_def.id, - precise : input_def.type == 'float', - min : input_def.min, - max : input_def.max + id : 'field-' + input_def.id, + precise : input_def.type == 'float', + min : input_def.min, + max : input_def.max, + onchange : function() { + self.app.trigger('refresh'); + } }); }, @@ -488,18 +552,22 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep */ _fieldHidden : function(input_def) { return new Ui.Hidden({ - id : 'field-' + input_def.id, - info : input_def.info + id : 'field-' + input_def.id, + info : input_def.info }); }, /** Boolean field */ _fieldBoolean : function(input_def) { + var self = this; return new Ui.RadioButton.View({ - id : 'field-' + input_def.id, - data : [ { label : 'Yes', value : 'true' }, - { label : 'No', value : 'false' }] + id : 'field-' + input_def.id, + data : [ { label : 'Yes', value : 'true' }, + { label : 'No', value : 'false' }], + onchange : function() { + self.app.trigger('refresh'); + } }); } }); diff --git a/client/galaxy/scripts/mvc/tools/tools-tree.js b/client/galaxy/scripts/mvc/tools/tools-tree.js index 3a8cde64e78..3525622a987 100644 --- a/client/galaxy/scripts/mvc/tools/tools-tree.js +++ b/client/galaxy/scripts/mvc/tools/tools-tree.js @@ -108,22 +108,29 @@ return Backbone.Model.extend({ convert(job_input_id, head[input.id + '-section-' + selectedCase]); } break; + // handle custom sub sections + case 'section': + convert('', node); + break; default: // get field var field = self.app.field_list[input.id]; - - // get and patch field value - var value = field.value(); - if (patch[input.type]) { - value = patch[input.type](value); - } - - // handle default value - if (!field.skip) { - if (field.validate && !field.validate(value)) { - value = null; + if (field && field.value) { + // get and patch field value + var value = field.value(); + if (patch[input.type]) { + value = patch[input.type](value); + } + + // handle default value + if (!field.skip) { + if (field.validate && !field.validate(value)) { + value = null; + } + if (input.ignore === undefined || (value && input.ignore != value)) { + add (job_input_id, input.id, value); + } } - add (job_input_id, input.id, value); } } } diff --git a/client/galaxy/scripts/mvc/ui/ui-misc.js b/client/galaxy/scripts/mvc/ui/ui-misc.js index efedef2c0c3..83678415490 100644 --- a/client/galaxy/scripts/mvc/ui/ui-misc.js +++ b/client/galaxy/scripts/mvc/ui/ui-misc.js @@ -193,6 +193,12 @@ var ButtonIcon = Backbone.View.extend({ this.disabled = false; }, + // change icon + setIcon: function(icon_cls) { + this.$('i').removeClass(this.options.icon).addClass(icon_cls); + this.options.icon = icon_cls; + }, + // template _template: function(options) { // width diff --git a/client/galaxy/scripts/mvc/ui/ui-portlet.js b/client/galaxy/scripts/mvc/ui/ui-portlet.js index 0dfcbf053be..138113ecec9 100644 --- a/client/galaxy/scripts/mvc/ui/ui-portlet.js +++ b/client/galaxy/scripts/mvc/ui/ui-portlet.js @@ -25,6 +25,7 @@ var View = Backbone.View.extend({ $content : null, $buttons : null, $operations : null, + $header : null, // initialize initialize : function(options) { @@ -35,13 +36,16 @@ var View = Backbone.View.extend({ this.setElement(this._template(this.options)); // link content - this.$content = this.$el.find('#content'); + this.$content = this.$el.find('.content'); // link title - this.$title = this.$el.find('#portlet-title-text'); + this.$title = this.$el.find('.portlet-title-text'); + + // link header + this.$header = this.$el.find('.portlet-header'); // set content format - var $portlet_content = this.$el.find('#portlet-content'); + var $portlet_content = this.$el.find('.portlet-content'); if (!this.options.scrollable) { if (this.options.title) { $portlet_content.addClass('no-scroll'); @@ -59,7 +63,7 @@ var View = Backbone.View.extend({ } // append buttons - this.$buttons = $(this.el).find('#buttons'); + this.$buttons = $(this.el).find('.buttons'); if (this.options.buttons) { // link functions var self = this; @@ -72,7 +76,7 @@ var View = Backbone.View.extend({ } // append operations - this.$operations = $(this.el).find('#operations'); + this.$operations = $(this.el).find('.operations'); if (this.options.operations) { // link functions var self = this; @@ -161,27 +165,27 @@ var View = Backbone.View.extend({ var tmpl = '
'; if (options.title) { - tmpl += '
' + - '
' + + tmpl += '
' + + '
' + '
'; if (options.icon) tmpl += ' '; - tmpl += '' + options.title + '' + + tmpl += '' + options.title + '' + '
' + '
'; } - tmpl += '
'; + tmpl += '
'; if (options.placement == 'top') { - tmpl += '
'; + tmpl += '
'; } - tmpl += '
'; + tmpl += '
'; if (options.placement == 'bottom') { - tmpl += '
'; + tmpl += '
'; } tmpl += '
' + diff --git a/config/plugins/visualizations/charts/static/build-app.js b/config/plugins/visualizations/charts/static/build-app.js index 6674660152c..9a4600e3a1d 100644 --- a/config/plugins/visualizations/charts/static/build-app.js +++ b/config/plugins/visualizations/charts/static/build-app.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.4 +// Underscore.js 1.7.0 // http://underscorejs.org -// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. -define("mvc/ui/ui-modal",[],function(){var e=Backbone.View.extend({elMain:"body",optionsDefault:{title:"ui-modal",body:"",backdrop:!0,height:null,width:null,closing_events:!1,closing_callback:null},buttonList:{},initialize:function(e){e&&this._create(e)},show:function(e){this.initialize(e),this.options.height?(this.$body.css("height",this.options.height),this.$body.css("overflow","hidden")):this.$body.css("max-height",$(window).height()/2),this.options.width&&this.$dialog.css("width",this.options.width),this.visible?this.$el.show():this.$el.fadeIn("fast"),this.visible=!0},hide:function(){this.visible=!1,this.$el.fadeOut("fast"),this.options.closing_callback&&this.options.closing_callback()},enableButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).prop("disabled",!1)},disableButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).prop("disabled",!0)},showButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).show()},hideButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).hide()},getButton:function(e){var t=this.buttonList[e];return this.$buttons.find("#"+t)},scrollTop:function(){return this.$body.scrollTop()},_create:function(e){var t=this;this.options=_.defaults(e,this.optionsDefault),this.options.body=="progress"&&(this.options.body=$('
')),this.$el&&(this.$el.remove(),$(document).off("keyup.ui-modal")),this.setElement(this._template(this.options.title)),this.$dialog=this.$el.find(".modal-dialog"),this.$body=this.$el.find(".modal-body"),this.$footer=this.$el.find(".modal-footer"),this.$buttons=this.$el.find(".buttons"),this.$backdrop=this.$el.find(".modal-backdrop"),this.$body.html(this.options.body),this.options.backdrop||this.$backdrop.removeClass("in");if(this.options.buttons){this.buttonList={};var n=0;$.each(this.options.buttons,function(e,r){var i="button-"+n++;t.$buttons.append($('').text(e).click(r)).append(" "),t.buttonList[e]=i})}else this.$footer.hide();$(this.elMain).append($(this.el)),this.options.closing_events&&($(document).on("keyup.ui-modal",function(e){e.keyCode==27&&t.hide()}),this.$el.find(".modal-backdrop").on("click",function(){t.hide()}))},_template:function(e){return'