Merge pull request #2340 from carlfeberhard/history.sharing-api

History/Sharable model user sharing API
This commit is contained in:
Dannon Baker
2016-05-16 14:28:05 -04:00
3 changed files with 101 additions and 32 deletions
+45 -32
View File
@@ -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,
+48
View File
@@ -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 ):
+8
View File
@@ -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 ):