Summary ------- This branch fixes a bunch of tests. Most of the fixes are simple search and replace, but there are a good 731 lines that are more complicated, so they need a review instead of a rubber stamp. I have included those diff lines in this comment so you don't have to read the automated diff. Implementation details ---------------------- Display the correct informational message in the browser corresponding to the new behavior for addMember(). lib/lp/registry/browser/team.py I changed the owners of several teams in this test so that it tests the permissions more appropriately. Previously, it was using foobar for everything, and that is a Launchpad admin, so it effectively had admin access on all the teams. lib/lp/registry/doc/teammembership.txt I changed the user for these tests, since salgado is also a Launchpad admin. lib/lp/registry/stories/team/xx-team-add-my-teams.txt Verify that the former members of a team are now only viewable by the admins of the team on the +members page. lib/lp/registry/stories/teammembership/20-managing-members.txt I changed some of the teams involved in the tests and the logged-in user, since addMember() will fully add a team if the user is an admin on both teams. If the user is only an admin on the team adding another team, then addMember() will set the status to INVITED. lib/lp/registry/stories/teammembership/xx-add-member.txt lib/lp/registry/stories/teammembership/xx-private-membership.txt lib/lp/registry/stories/webservice/xx-private-membership.txt In the REST API, the logged-in user is passed in as the reviewer argument. Therefore, the no_priv user has to be used to create a cyclical invitation as opposed to a cyclical active membership. lib/lp/registry/tests/test_teammembership.py Tests ----- ./bin/test -vv -t registry Pruned incremental diff ----------------------- {{{ === modified file 'lib/lp/registry/browser/team.py' --- lib/lp/registry/browser/team.py 2009-12-10 18:11:38 +0000 +++ lib/lp/registry/browser/team.py 2010-01-04 17:12:48 +0000 @@ -993,9 +993,11 @@ assert newmember != self.context, ( "Can't add team to itself: %s" % newmember) - self.context.addMember(newmember, reviewer=self.user, - status=TeamMembershipStatus.APPROVED) - if newmember.isTeam(): + changed, new_status = self.context.addMember( + newmember, reviewer=self.user, + status=TeamMembershipStatus.APPROVED) + + if new_status == TeamMembershipStatus.INVITED: msg = "%s has been invited to join this team." % ( newmember.unique_displayname) else: === modified file 'lib/lp/registry/doc/teammembership.txt' --- lib/lp/registry/doc/teammembership.txt 2009-12-16 02:22:29 +0000 +++ lib/lp/registry/doc/teammembership.txt 2010-01-04 17:12:47 +0000 @@ -22,23 +22,28 @@ >>> from zope.component import getUtility >>> from canonical.launchpad.interfaces import IPersonSet >>> personset = getUtility(IPersonSet) - >>> foobar = personset.getByName('name16') - >>> reviewer = foobar + >>> jblack = personset.getByName('jblack') + >>> nopriv = personset.getByName('no-priv') + >>> jdub = personset.getByName('jdub') + >>> reviewer = nopriv >>> t1 = personset.newTeam( - ... foobar, 't1', 't1', + ... jblack, 't1', 't1', ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN) >>> t2 = personset.newTeam( - ... foobar, 't2', 't2', + ... nopriv, 't2', 't2', ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN) >>> t3 = personset.newTeam( - ... foobar, 't3', 't3', + ... jdub, 't3', 't3', ... subscriptionpolicy=TeamSubscriptionPolicy.MODERATED) >>> t4 = personset.newTeam( - ... foobar, 't4', 't4', + ... nopriv, 't4', 't4', ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN) >>> t5 = personset.newTeam( - ... foobar, 't5', 't5', + ... nopriv, 't5', 't5', ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN) + >>> t6 = personset.newTeam( + ... jdub, 't6', 't6', + ... subscriptionpolicy=TeamSubscriptionPolicy.MODERATED) # Make sure the teams have predictable (and different) creation dates as # some of our tests depend on that. @@ -66,15 +71,15 @@ member of that team --somebody has to approve his membership first: >>> [m.displayname for m in t4.allmembers] - [u'Foo Bar', u'Guilherme Salgado'] + [u'Guilherme Salgado', u'No Privileges Person'] >>> [m.displayname for m in t3.allmembers] - [u'Foo Bar'] + [u'Jeff Waugh'] >>> login_person(t3.teamowner) >>> t3.setMembershipData(salgado, TeamMembershipStatus.APPROVED, reviewer) >>> flush_database_updates() >>> [m.displayname for m in t3.allmembers] - [u'Foo Bar', u'Guilherme Salgado'] + [u'Guilherme Salgado', u'Jeff Waugh'] The join() method is not allowed for teams whose subscription policy is RESTRICTED. And it'll be a no-op in case the user has already joined the @@ -141,12 +146,12 @@ # Log in as the team owner. >>> login_person(t3.teamowner) -addMember returns True if the member got added (i.e. he wasn't already a -member of the team). +If the member was added (i.e. he wasn't already a member of the team), +addMember returns a tuple with True plus the new membership status. >>> t3.addMember( ... salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) - True + (True, >> from canonical.launchpad.interfaces import ITeamMembershipSet >>> membershipset = getUtility(ITeamMembershipSet) >>> flush_database_updates() @@ -158,26 +163,26 @@ >>> salgado in t3.activemembers True -addMember returns True also when the member is added as a proposed -member. +addMember returns (True, PROPOSED) also when the member is added as a +proposed member. >>> marilize = personset.getByName('marilize') >>> t3.addMember( ... marilize, reviewer=mark, status=TeamMembershipStatus.PROPOSED) - True + (True, >> flush_database_updates() >>> marilize in t3.activemembers False If addMember is called with a person that is already a member, it -returns False. +returns a tuple with False and the current status of the membership. >>> t3.addMember( ... salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) - False + (False, >> t3.addMember( ... marilize, reviewer=mark, status=TeamMembershipStatus.PROPOSED) - False + (False, >> t1.addMember(t2, reviewer) + >>> login_person(t1.teamowner) + + # If the reviewer were also an admin of the team being added, + # the status would go to APPROVED instead of INVITED. + >>> t2.teamowner != t1.teamowner True + >>> t1.addMember(t2, reviewer=t1.teamowner) + (True, >> membership = membershipset.getByPersonAndTeam(t2, t1) >>> membership.status == TeamMembershipStatus.INVITED True >>> [m.displayname for m in t1.allmembers] - [u'Foo Bar'] + [u'James Blackwell'] Once one of the t2 admins approve the membership, t2 is shown as a member -of t1. +of t1 and the owner of t2 is an indirect member. >>> login_person(t2.teamowner) >>> t2.acceptInvitationToBeMemberOf(t1, comment='something') + >>> [m.displayname for m in t1.activemembers] + [u'James Blackwell', u't2'] >>> [m.displayname for m in t1.allmembers] - [u'Foo Bar', u't2'] + [u'James Blackwell', u'No Privileges Person', u't2'] A team admin can also decline an invitation made to his team. >>> t2.addMember(t3, reviewer=mark) - True + (True, >> login_person(t3.teamowner) >>> t3.declineInvitationToBeMemberOf(t2, comment='something') >>> membership = membershipset.getByPersonAndTeam(t3, t2) @@ -222,21 +235,40 @@ force_team_add=True to addMember(). We'll use that to add t3 as a member of t2, thus making all t3 members be considered members of t2 as well. - >>> login_person(t3.teamowner) - >>> t2.addMember(t3, reviewer=mark, force_team_add=True) + >>> login_person(t2.teamowner) + + # If the reviewer is also an admin of the team being added, + # force_team_add is unnecessary, and we can't prove that that + # argument works. + >>> t3.teamowner != t2.teamowner True + >>> t2.addMember(t3, reviewer=t2.teamowner, force_team_add=True) + (True, >> [m.displayname for m in t2.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't3'] + [u'Guilherme Salgado', u'Jeff Waugh', u'No Privileges Person', u't3'] And members of t1 as well, since t2 is a member of t1. >>> [m.displayname for m in t1.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't2', u't3'] + [u'Guilherme Salgado', u'James Blackwell', u'Jeff Waugh', + u'No Privileges Person', u't2', u't3'] + + +Passing in force_team_add=True is not necessary if the reviewer is the +admin of the team being added. + + >>> login_person(t3.teamowner) + >>> t6.addMember(t3, reviewer=t3.teamowner) + (True, >> [m.displayname for m in t6.allmembers] + [u'Guilherme Salgado', u'Jeff Waugh', u't3'] Can we add t2 as a member of t3? No, we prevent this kind of loop, and users can't do this because our vocabularies won't allow members that would cause loops. + >>> foobar = personset.getByName('name16') + >>> login_person(foobar) >>> t3.addMember(t2, reviewer) Traceback (most recent call last): ... @@ -246,19 +278,21 @@ Adding t2 as a member of t5 will add all t2 members as t5 members too. >>> t5.addMember(t2, reviewer, force_team_add=True) - True + (True, >> [m.displayname for m in t5.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't2', u't3'] + [u'Guilherme Salgado', u'Jeff Waugh', u'No Privileges Person', + u't2', u't3'] Adding t5 and t1 as members of t4 will add all t5 and t1 members as t4 members too. >>> t4.addMember(t5, reviewer, force_team_add=True) - True + (True, >> t4.addMember(t1, reviewer, force_team_add=True) - True + (True, >> [m.displayname for m in t4.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't1', u't2', u't3', u't5'] + [u'Guilherme Salgado', u'James Blackwell', u'Jeff Waugh', + u'No Privileges Person', u't1', u't2', u't3', u't5'] >>> flush_database_updates() @@ -305,18 +339,20 @@ member anymore, it doesn't have any members apart from its owner. >>> [m.displayname for m in t5.allmembers] - [u'Foo Bar'] + [u'No Privileges Person'] Removing t2 from t5 won't remove it from t4, because t2 is also a member of t1, which is a member of t4. >>> [m.displayname for m in t4.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't1', u't2', u't3', u't5'] + [u'Guilherme Salgado', u'James Blackwell', u'Jeff Waugh', + u'No Privileges Person', u't1', u't2', u't3', u't5'] Nothing changes in t1, because t5 wasn't one of its members. >>> [m.displayname for m in t1.allmembers] - [u'Foo Bar', u'Guilherme Salgado', u't2', u't3'] + [u'Guilherme Salgado', u'James Blackwell', u'Jeff Waugh', + u'No Privileges Person', u't2', u't3'] If 'Guilherme Salgado' decides to leave t3, he'll also be removed from t1 and t2, but not from t4, because he's a direct member of t4. @@ -350,31 +386,31 @@ >>> cprov = getUtility(IPersonSet).getByName('cprov') >>> t3.addMember(cprov, reviewer) - True + (True, >> [m.displayname for m in t3.allmembers] - [u'Celso Providelo', u'Foo Bar'] + [...u'Celso Providelo'... >>> [m.displayname for m in t2.allmembers] - [u'Celso Providelo', u'Foo Bar', u't3'] + [...u'Celso Providelo'... >>> [m.displayname for m in t1.allmembers] - [u'Celso Providelo', u'Foo Bar', u't2', u't3'] + [...u'Celso Providelo'... >>> [m.displayname for m in t4.allmembers] - [u'Celso Providelo', u'Foo Bar', u'Guilherme Salgado', u't1', u't2', - u't3', u't5'] - - -It's important to note that even if Foo Bar leaves the team he's the owner, -he'll retains his rights over the team because he's the team's owner. This -ensures we'll never have teams which can't be managed. - - >>> login_person(foobar) - >>> foobar.leave(t5) + [...u'Celso Providelo'... + + +It's important to note that even if the owner leaves the team, which +removes his membership, he will still be the team's owner and retain his +rights over it. This ensures we'll never have teams which can't be +managed. + + >>> login_person(t5.teamowner) + >>> t5.teamowner.leave(t5) >>> flush_database_updates() >>> [m.displayname for m in t5.allmembers] [] - >>> foobar.inTeam(t5) + >>> t5.teamowner.inTeam(t5) True @@ -390,7 +426,7 @@ # Foo Bar is a launchpad admin, but even so he can't change a membership's # status/expiry-date by hand. - >>> login('