Merge lp:~abentley/launchpad/team-transition-error into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 13816
Proposed branch: lp:~abentley/launchpad/team-transition-error
Merge into: lp:launchpad
Diff against target: 233 lines (+100/-12)
4 files modified
lib/lp/registry/browser/tests/test_team.py (+18/-1)
lib/lp/registry/model/person.py (+18/-3)
lib/lp/registry/model/teammembership.py (+1/-1)
lib/lp/registry/tests/test_teammembership.py (+63/-7)
To merge this branch: bzr merge lp:~abentley/launchpad/team-transition-error
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+73103@code.launchpad.net

Commit message

Fix TeamMembershipTransitionError moving from DECLINED to INVITED

Description of the change

= Summary =
Fix bug #820077: TeamMembershipTransitionError: Bad state transition from DECLINED to INVITED

== Proposed fix ==
Bless transition from DECLINED to INVITED

== Pre-implementation notes ==
Discussed with sinzui

== Implementation details ==
As well as fixing the particular error, I've tested that TeamMemberAddView.add_action cannot raise TeamMembershipTransitionError at all, so there's no need to handle that exception in browser code.

This meant turning PROPOSED to APPROVED if the reviewer was the an admin of the team, instead of changing it to INVITED.

It also meant preventing addMember from downgrading APPROVED/ADMIN to PROPOSED. And while I was there, I prevented downgrading ADMIN to APPROVED.

== Tests ==
bin/test -t test_no_TeamMembershipTransitionError -t test_declined_member_can_be_invited -t test_add_approved -t test_add_admin -t test_implicit_approval

== Demo and Q/A ==
Create team A and team B with different owners. As the owner of team A, attempt to join team B. As the owner of team B, decline the membership. As the owner of team B, attempt to add team A as a member. This should not OOPS.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/tests/test_team.py
  lib/lp/registry/tests/test_teammembership.py
  lib/lp/registry/model/person.py
  lib/lp/registry/model/teammembership.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

As we discussed on IRC, this branch also fixes bug 480157.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py 2011-04-09 02:23:46 +0000
+++ lib/lp/registry/browser/tests/test_team.py 2011-08-29 13:54:49 +0000
@@ -4,11 +4,16 @@
4__metaclass__ = type4__metaclass__ = type
55
6from zope.component import getUtility6from zope.component import getUtility
7from zope.security.proxy import removeSecurityProxy
78
8from canonical.launchpad.webapp.publisher import canonical_url9from canonical.launchpad.webapp.publisher import canonical_url
9from canonical.testing.layers import DatabaseFunctionalLayer10from canonical.testing.layers import DatabaseFunctionalLayer
10from lp.registry.browser.person import TeamOverviewMenu11from lp.registry.browser.person import TeamOverviewMenu
11from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource12from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
13from lp.registry.interfaces.teammembership import (
14 ITeamMembershipSet,
15 TeamMembershipStatus,
16 )
12from lp.testing import (17from lp.testing import (
13 login_person,18 login_person,
14 TestCaseWithFactory,19 TestCaseWithFactory,
@@ -42,7 +47,7 @@
42 self.assertEqual('Create a mailing list', link.text)47 self.assertEqual('Create a mailing list', link.text)
4348
44 def test_TeamOverviewMenu_check_menu_links_with_mailing(self):49 def test_TeamOverviewMenu_check_menu_links_with_mailing(self):
45 mailing_list = self.factory.makeMailingList(50 self.factory.makeMailingList(
46 self.team, self.team.teamowner)51 self.team, self.team.teamowner)
47 menu = TeamOverviewMenu(self.team)52 menu = TeamOverviewMenu(self.team)
48 self.assertEqual(True, check_menu_links(menu))53 self.assertEqual(True, check_menu_links(menu))
@@ -139,6 +144,18 @@
139 "You can't add a team that doesn't have any active members.",144 "You can't add a team that doesn't have any active members.",
140 view.errors[0])145 view.errors[0])
141146
147 def test_no_TeamMembershipTransitionError(self):
148 # Attempting to add a team never triggers a
149 # TeamMembershipTransitionError
150 member_team = self.factory.makeTeam()
151 self.team.addMember(member_team, self.team.teamowner)
152 tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
153 member_team, self.team)
154 for status in TeamMembershipStatus.items:
155 removeSecurityProxy(tm).status = status
156 view = create_initialized_view(self.team, "+addmember")
157 view.add_action.success(data={'newmember': member_team})
158
142159
143class TestTeamIndexView(TestCaseWithFactory):160class TestTeamIndexView(TestCaseWithFactory):
144161
145162
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-08-25 03:30:40 +0000
+++ lib/lp/registry/model/person.py 2011-08-29 13:54:49 +0000
@@ -1455,6 +1455,12 @@
1455 "You can't add a member with this status: %s." % status.name)1455 "You can't add a member with this status: %s." % status.name)
14561456
1457 event = JoinTeamEvent1457 event = JoinTeamEvent
1458 tm = TeamMembership.selectOneBy(person=person, team=self)
1459 if tm is not None:
1460 if tm.status == TeamMembershipStatus.ADMIN or (
1461 tm.status == TeamMembershipStatus.APPROVED and status ==
1462 TeamMembershipStatus.PROPOSED):
1463 status = tm.status
1458 if person.is_team:1464 if person.is_team:
1459 assert not self.hasParticipationEntryFor(person), (1465 assert not self.hasParticipationEntryFor(person), (
1460 "Team '%s' is a member of '%s'. As a consequence, '%s' can't "1466 "Team '%s' is a member of '%s'. As a consequence, '%s' can't "
@@ -1468,12 +1474,21 @@
1468 is_reviewer_admin_of_new_member = (1474 is_reviewer_admin_of_new_member = (
1469 person in reviewer.getAdministratedTeams())1475 person in reviewer.getAdministratedTeams())
1470 if not force_team_add and not is_reviewer_admin_of_new_member:1476 if not force_team_add and not is_reviewer_admin_of_new_member:
1471 status = TeamMembershipStatus.INVITED1477 if tm is None or tm.status not in (
1472 event = TeamInvitationEvent1478 TeamMembershipStatus.PROPOSED,
1479 TeamMembershipStatus.APPROVED,
1480 TeamMembershipStatus.ADMIN,
1481 ):
1482 status = TeamMembershipStatus.INVITED
1483 event = TeamInvitationEvent
1484 else:
1485 if tm.status == TeamMembershipStatus.PROPOSED:
1486 status = TeamMembershipStatus.APPROVED
1487 else:
1488 status = tm.status
14731489
1474 status_changed = True1490 status_changed = True
1475 expires = self.defaultexpirationdate1491 expires = self.defaultexpirationdate
1476 tm = TeamMembership.selectOneBy(person=person, team=self)
1477 if tm is None:1492 if tm is None:
1478 tm = TeamMembershipSet().new(1493 tm = TeamMembershipSet().new(
1479 person, self, status, reviewer, dateexpires=expires,1494 person, self, status, reviewer, dateexpires=expires,
14801495
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2011-08-12 14:39:51 +0000
+++ lib/lp/registry/model/teammembership.py 2011-08-29 13:54:49 +0000
@@ -338,7 +338,7 @@
338 deactivated: [proposed, approved, admin, invited],338 deactivated: [proposed, approved, admin, invited],
339 expired: [proposed, approved, admin, invited],339 expired: [proposed, approved, admin, invited],
340 proposed: [approved, admin, declined],340 proposed: [approved, admin, declined],
341 declined: [proposed, approved, admin],341 declined: [proposed, approved, admin, invited],
342 invited: [approved, admin, invitation_declined],342 invited: [approved, admin, invitation_declined],
343 invitation_declined: [invited, approved, admin]}343 invitation_declined: [invited, approved, admin]}
344344
345345
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2011-05-27 21:12:25 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2011-08-29 13:54:49 +0000
@@ -315,7 +315,7 @@
315 ['name16', 'no-priv', 'team5'], self.team4)315 ['name16', 'no-priv', 'team5'], self.team4)
316 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)316 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
317 self.assertEqual(317 self.assertEqual(
318 previous_count-8,318 previous_count - 8,
319 self.getTeamParticipationCount())319 self.getTeamParticipationCount())
320320
321 def testRemovingLeafTeam(self):321 def testRemovingLeafTeam(self):
@@ -333,7 +333,7 @@
333 self.assertParticipantsEquals(['name16'], self.team4)333 self.assertParticipantsEquals(['name16'], self.team4)
334 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)334 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
335 self.assertEqual(335 self.assertEqual(
336 previous_count-8,336 previous_count - 8,
337 self.getTeamParticipationCount())337 self.getTeamParticipationCount())
338338
339339
@@ -393,7 +393,7 @@
393 ['name16', 'no-priv', 'team5'], self.team4)393 ['name16', 'no-priv', 'team5'], self.team4)
394 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)394 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
395 self.assertEqual(395 self.assertEqual(
396 previous_count-4,396 previous_count - 4,
397 self.getTeamParticipationCount())397 self.getTeamParticipationCount())
398398
399 def testRemoveTeam5FromTeam4(self):399 def testRemoveTeam5FromTeam4(self):
@@ -410,7 +410,7 @@
410 self.assertParticipantsEquals(['name16'], self.team4)410 self.assertParticipantsEquals(['name16'], self.team4)
411 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)411 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
412 self.assertEqual(412 self.assertEqual(
413 previous_count-4,413 previous_count - 4,
414 self.getTeamParticipationCount())414 self.getTeamParticipationCount())
415415
416416
@@ -512,7 +512,7 @@
512 self.assertParticipantsEquals(512 self.assertParticipantsEquals(
513 ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)513 ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)
514 self.assertEqual(514 self.assertEqual(
515 previous_count-3,515 previous_count - 3,
516 self.getTeamParticipationCount())516 self.getTeamParticipationCount())
517517
518 def testRemoveTeam5FromTeam4(self):518 def testRemoveTeam5FromTeam4(self):
@@ -529,7 +529,7 @@
529 self.assertParticipantsEquals(529 self.assertParticipantsEquals(
530 ['name16', 'team2', 'team3', 'team4'], self.team6)530 ['name16', 'team2', 'team3', 'team4'], self.team6)
531 self.assertEqual(531 self.assertEqual(
532 previous_count-10,532 previous_count - 10,
533 self.getTeamParticipationCount())533 self.getTeamParticipationCount())
534534
535 def testTeam3_deactivateActiveMemberships(self):535 def testTeam3_deactivateActiveMemberships(self):
@@ -576,7 +576,7 @@
576 """576 """
577 login('test@canonical.com')577 login('test@canonical.com')
578 person = self.factory.makePerson()578 person = self.factory.makePerson()
579 login_person(person) # Now login with the future owner of the teams.579 login_person(person) # Now login with the future owner of the teams.
580 teamA = self.factory.makeTeam(580 teamA = self.factory.makeTeam(
581 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)581 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
582 teamB = self.factory.makeTeam(582 teamB = self.factory.makeTeam(
@@ -837,6 +837,62 @@
837 TeamMembershipStatus.INVITATION_DECLINED, self.team2.teamowner)837 TeamMembershipStatus.INVITATION_DECLINED, self.team2.teamowner)
838 self.assertEqual(TeamMembershipStatus.INVITATION_DECLINED, tm.status)838 self.assertEqual(TeamMembershipStatus.INVITATION_DECLINED, tm.status)
839839
840 def test_declined_member_can_be_invited(self):
841 # A team can re-invite a declined member.
842 self.team2.addMember(
843 self.team1, self.no_priv, status=TeamMembershipStatus.PROPOSED,
844 force_team_add=True)
845 tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
846 self.team1, self.team2)
847 tm.setStatus(
848 TeamMembershipStatus.DECLINED, self.team1.teamowner)
849 tm.setStatus(
850 TeamMembershipStatus.INVITED, self.team1.teamowner)
851 self.assertEqual(TeamMembershipStatus.INVITED, tm.status)
852
853 def test_add_approved(self):
854 # Adding an approved team is a no-op.
855 member_team = self.factory.makeTeam()
856 self.team1.addMember(
857 member_team, self.team1.teamowner)
858 with person_logged_in(member_team.teamowner):
859 member_team.acceptInvitationToBeMemberOf(self.team1, 'alright')
860 self.team1.addMember(
861 member_team, self.team1.teamowner)
862 tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
863 member_team, self.team1)
864 self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
865 self.team1.addMember(
866 member_team, member_team.teamowner)
867 self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
868
869 def test_add_admin(self):
870 # Adding an admin team is a no-op.
871 member_team = self.factory.makeTeam()
872 self.team1.addMember(
873 member_team, self.team1.teamowner,
874 status=TeamMembershipStatus.ADMIN, force_team_add=True)
875 self.team1.addMember(
876 member_team, self.team1.teamowner)
877 tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
878 member_team, self.team1)
879 self.assertEqual(TeamMembershipStatus.ADMIN, tm.status)
880 self.team1.addMember(
881 member_team, member_team.teamowner)
882 self.assertEqual(TeamMembershipStatus.ADMIN, tm.status)
883
884 def test_implicit_approval(self):
885 # Inviting a proposed person is an implicit approval.
886 member_team = self.factory.makeTeam()
887 self.team1.addMember(
888 member_team, self.team1.teamowner,
889 status=TeamMembershipStatus.PROPOSED, force_team_add=True)
890 self.team1.addMember(
891 member_team, self.team1.teamowner)
892 tm = getUtility(ITeamMembershipSet).getByPersonAndTeam(
893 member_team, self.team1)
894 self.assertEqual(TeamMembershipStatus.APPROVED, tm.status)
895
840 def test_retractTeamMembership_invited(self):896 def test_retractTeamMembership_invited(self):
841 # A team can retract a membership invitation.897 # A team can retract a membership invitation.
842 self.team2.addMember(self.team1, self.no_priv)898 self.team2.addMember(self.team1, self.no_priv)