diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index 5826202ebf0..a091881fcaa 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -147,6 +147,7 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu """ Get or create a share for the given user (or users if `user` is a list). """ + # precondition: user has been validated # allow user to be a list and call recursivly if isinstance( user, list ): return map( lambda user: self.share_with( item, user, flush=False ), user ) @@ -307,19 +308,7 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu self.session().flush() return item - # def by_slug( self, user, **kwargs ): - # """ - # """ - # pass - - # .... display - # def display_by_username_and_slug( self, username, slug ): - # """ Display item by username and slug. """ - - # def set_public_username( self, id, username, **kwargs ): - # """ Set user's public username and delegate to sharing() """ - # def sharing( self, id, **kwargs ): - # """ Handle item sharing. """ + # TODO: def by_slug( self, user, **kwargs ): class SharableModelSerializer( base.ModelSerializer, @@ -335,7 +324,8 @@ class SharableModelSerializer( base.ModelSerializer, self.serializers.update({ 'user_id' : self.serialize_id, - 'username_and_slug' : self.serialize_username_and_slug + 'username_and_slug' : self.serialize_username_and_slug, + 'users_shared_with' : self.serialize_users_shared_with }) # these use the default serializer but must still be white-listed self.serializable_keyset.update([ @@ -347,19 +337,24 @@ class SharableModelSerializer( base.ModelSerializer, return None return ( '/' ).join(( 'u', item.user.username, self.SINGLE_CHAR_ABBR, item.slug ) ) - # def serialize_username_and_slug( self, item, key, **context ): - # """ - # """ - # #TODO: replace the above with this - # url = self.url_for(controller='history', action="display_by_username_and_slug", - # username=item.user.username, slug=item.slug ) - # return url - # the only ones that needs any fns: # user/user_id - # user_shares? # username_and_slug? + def serialize_users_shared_with( self, item, key, user=None, **context ): + """ + Returns a list of encoded ids for users the item has been shared. + + Skipped if the requesting user is not the owner. + """ + # TODO: still an open question as to whether key removal based on user + # should be handled here or at a higher level (even if we didn't have to pass user (via thread context, etc.)) + if not self.manager.is_owner( item, user ): + self.skip() + + share_assocs = self.manager.get_share_assocs( item ) + return [ self.serialize_id( share, 'user_id' ) for share in share_assocs ] + class SharableModelDeserializer( base.ModelDeserializer, taggable.TaggableDeserializerMixin, annotatable.AnnotatableDeserializerMixin, ratable.RatableDeserializerMixin ): @@ -371,8 +366,9 @@ class SharableModelDeserializer( base.ModelDeserializer, ratable.RatableDeserializerMixin.add_deserializers( self ) self.deserializers.update({ - 'published' : self.deserialize_published, - 'importable' : self.deserialize_importable, + 'published' : self.deserialize_published, + 'importable' : self.deserialize_importable, + 'users_shared_with' : self.deserialize_users_shared_with, }) def deserialize_published( self, item, key, val, **context ): @@ -399,15 +395,32 @@ class SharableModelDeserializer( base.ModelDeserializer, self.manager.make_importable( item, flush=False ) else: self.manager.make_non_importable( item, flush=False ) - return item.published + return item.importable - # def deserialize_slug( self, item, val, **context ): - # """ - # """ - # #TODO: call manager.set_slug - # pass + # TODO: def deserialize_slug( self, item, val, **context ): - # def deserialize_user_shares(): + def deserialize_users_shared_with( self, item, key, val, **context ): + """ + Accept a list of encoded user_ids, validate them as users, and then + add or remove user shares in order to update the users_shared_with to + match the given list finally returning the new list of shares. + """ + unencoded_ids = [ self.app.security.decode_id( id_ ) for id_ in val ] + new_users_shared_with = set( self.manager.user_manager.by_ids( unencoded_ids ) ) + current_shares = self.manager.get_share_assocs( item ) + currently_shared_with = set([ share.user for share in current_shares ]) + + needs_adding = new_users_shared_with - currently_shared_with + for user in needs_adding: + current_shares.append( self.manager.share_with( item, user, flush=False ) ) + + needs_removing = currently_shared_with - new_users_shared_with + for user in needs_removing: + current_shares.remove( self.manager.unshare_with( item, user, flush=False ) ) + + self.manager.session().flush() + # TODO: or should this return the list of ids? + return current_shares class SharableModelFilters( base.ModelFilterParser, diff --git a/test/unit/managers/test_HistoryManager.py b/test/unit/managers/test_HistoryManager.py index b98ec9f6809..8c0b0046ff6 100644 --- a/test/unit/managers/test_HistoryManager.py +++ b/test/unit/managers/test_HistoryManager.py @@ -437,6 +437,18 @@ class HistorySerializerTestCase( BaseTestCase ): serialized = self.history_serializer.serialize( history1, sharable_attrs ) self.assertKeys( serialized, sharable_attrs ) + self.log( 'should return user_id for user with whom it\'s been shared if the requester is the owner' ) + non_owner = self.user_manager.create( **user3_data ) + self.history_manager.share_with( history1, non_owner ) + serialized = self.history_serializer.serialize( history1, [ 'users_shared_with' ], user=user2 ) + self.assertKeys( serialized, [ 'users_shared_with' ] ) + self.assertIsInstance( serialized[ 'users_shared_with' ], list ) + self.assertEqual( serialized[ 'users_shared_with' ][0], self.app.security.encode_id( non_owner.id ) ) + + self.log( 'should not return users_shared_with if the requester is not the owner' ) + serialized = self.history_serializer.serialize( history1, [ 'users_shared_with' ], user=non_owner ) + self.assertFalse( hasattr( serialized, 'users_shared_with' ) ) + def test_purgable( self ): user2 = self.user_manager.create( **user2_data ) history1 = self.history_manager.create( name='history1', user=user2 ) @@ -595,6 +607,42 @@ class HistoryDeserializerTestCase( BaseTestCase ): deserializer.deserialize( item, { 'community_rating' : 4 }, user=user2 ) self.assertEqual( manager.ratings_count( item ), 1 ) + def test_sharable( self ): + manager = self.history_manager + deserializer = self.history_deserializer + + user2 = self.user_manager.create( **user2_data ) + item = manager.create( name='history1', user=user2 ) + non_owner = self.user_manager.create( **user3_data ) + + self.log( 'should allow adding a share by adding a user id to users_shared_with' ) + non_owner_id = self.app.security.encode_id( non_owner.id ) + deserializer.deserialize( item, { 'users_shared_with' : [ non_owner_id ] }, user=user2 ) + user_shares = manager.get_share_assocs( item ) + self.assertEqual( len( user_shares ), 1 ) + self.assertEqual( user_shares[0].user_id, non_owner.id ) + + self.log( 're-adding an existing user id should do nothing' ) + deserializer.deserialize( item, { 'users_shared_with' : [ non_owner_id, non_owner_id ] }, user=user2 ) + user_shares = manager.get_share_assocs( item ) + self.assertEqual( len( user_shares ), 1 ) + self.assertEqual( user_shares[0].user_id, non_owner.id ) + + self.log( 'should allow removing a share by not having it in users_shared_with' ) + deserializer.deserialize( item, { 'users_shared_with' : [] }, user=user2 ) + user_shares = manager.get_share_assocs( item ) + self.assertEqual( len( user_shares ), 0 ) + + self.log( 'adding a bad user id should error' ) + self.assertRaises( AttributeError, + deserializer.deserialize, item, { 'users_shared_with' : [ None ] }, user=user2 ) + + self.log( 'adding a non-existing user id should do nothing' ) + non_user_id = self.app.security.encode_id( 99 ) + deserializer.deserialize( item, { 'users_shared_with' : [ non_user_id ] }, user=user2 ) + user_shares = manager.get_share_assocs( item ) + self.assertEqual( len( user_shares ), 0 ) + # ============================================================================= class HistoryFiltersTestCase( BaseTestCase ): diff --git a/test/unit/unittest_utils/galaxy_mock.py b/test/unit/unittest_utils/galaxy_mock.py index 48dbedb7f01..bc64ab976d1 100644 --- a/test/unit/unittest_utils/galaxy_mock.py +++ b/test/unit/unittest_utils/galaxy_mock.py @@ -7,6 +7,8 @@ import shutil from galaxy.web import security from galaxy import objectstore +from galaxy import model +from galaxy.datatypes import registry from galaxy.model import mapping from galaxy.util.bunch import Bunch @@ -59,6 +61,12 @@ class MockApp( object ): self.visualizations_registry = MockVisualizationsRegistry() self.tag_handler = tags.GalaxyTagManager( self ) self.quota_agent = quota.QuotaAgent( self.model ) + self.init_datatypes() + + def init_datatypes( self ): + datatypes_registry = registry.Registry() + datatypes_registry.load_datatypes() + model.set_datatypes_registry( datatypes_registry ) class MockAppConfig( Bunch ):