Merge lp:~jcsackett/launchpad/merge-person-participation-676494 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12016
Proposed branch: lp:~jcsackett/launchpad/merge-person-participation-676494
Merge into: lp:launchpad
Diff against target: 229 lines (+125/-11)
6 files modified
lib/lp/registry/browser/peoplemerge.py (+5/-1)
lib/lp/registry/browser/tests/test_peoplemerge.py (+25/-7)
lib/lp/registry/doc/person-merge.txt (+4/-2)
lib/lp/registry/model/person.py (+35/-0)
lib/lp/registry/tests/test_person.py (+31/-1)
lib/lp/registry/tests/test_team.py (+25/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/merge-person-participation-676494
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+41932@code.launchpad.net

Commit message

[r=bac][ui=none][bug=676494] Fixes bad execution path that leads to extra teams being assigned to ~registry on merge and bad entries in TeamParticipation.

Description of the change

Summary

=======

A long execution path on merging teams as part of a delete leads to corruption of the TeamParticipation table and the addition of teams to ~registry that should never be part of it.

To remedy this, this branch adds an extra guard to prevent super teams from making it through the team merge, as well as cleaning up team participation for indirect entries.
Additional, use of flush_db_updates lower in the call stack helps sync team removal.

Proposed fix

============

Add additional guards to the process of merging teams, add additional db flushes, and make sure to delete all the team participation entries for members when deactivateAllMembers is called.

Preimplementation talk

======================

Talked with and explored the execution path with Curtis Hovey.

Implementation details

======================

As in proposed. The cleanup for team participation is a particularly complicated sql query--I'm very open to discussing better implementations if you have an idea. This branch presents the best I could come up with.

Tests

=====

bin/test -t test_peoplemerge.py

bin/test -t test_person.py

bin/test -t test_team.py

Lint

====

make lint output:

= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:

  lib/lp/registry/browser/peoplemerge.py

  lib/lp/registry/browser/tests/test_peoplemerge.py

  lib/lp/registry/model/person.py

  lib/lp/registry/tests/test_person.py

  lib/lp/registry/tests/test_team.py

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Jon,

Thanks for this fix. I hope it completely solves the problem.

In test_team_with_super_teams_raises_error the doMerge method could be shared with test_team_without_super_teams_is_fine.

The comment in test_team_without_super_teams_is_fine should end with punctuation.

In test_deactivateAllMembers_cleans_up_team_participation you create a bunch of new objects that are created as objects on the instance but they are not used outside the method. Please change them to just be local variables.

I've gone over the changes carefully and they all make sense. I'm very happy you and Curtis worked this out. I think this fix will solve the other TeamParticipation problem (bug 651210).

review: Approve (code)
Revision history for this message
j.c.sackett (jcsackett) wrote :

> In test_team_with_super_teams_raises_error the doMerge method could be shared with test_team_without_super_teams_is_fine.

I've restructured so they share a method, _doMerge.

> The comment in test_team_without_super_teams_is_fine should end with punctuation.

Done.

> In test_deactivateAllMembers_cleans_up_team_participation you create a bunch of new objects that are created as objects on the instance but they are not used outside the method. Please change them to just be local variables.

Good catch. That's taken care of.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/peoplemerge.py'
2--- lib/lp/registry/browser/peoplemerge.py 2010-11-23 23:22:27 +0000
3+++ lib/lp/registry/browser/peoplemerge.py 2010-12-02 15:08:17 +0000
4@@ -242,9 +242,13 @@
5 self.dupe_person.setContactAddress(None)
6 # The registry experts does not want to acquire super teams from a
7 # merge.
8- if self.target_person is self.registry_experts:
9+ if self.target_person == self.registry_experts:
10 for team in self.dupe_person.teams_participated_in:
11 self.dupe_person.retractTeamMembership(team, self.user)
12+ # We have sent another series of calls to the db, potentially a long
13+ # sequence depending on the merge. We want everything synced up
14+ # before proceeding.
15+ flush_database_updates()
16 super(AdminTeamMergeView, self).doMerge(data)
17
18 def validate(self, data):
19
20=== modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
21--- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-11-12 05:36:22 +0000
22+++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-02 15:08:17 +0000
23@@ -95,12 +95,13 @@
24 self.target_team = self.factory.makeTeam()
25 login_celebrity('registry_experts')
26
27- def getView(self):
28- form = {
29- 'field.dupe_person': self.dupe_team.name,
30- 'field.target_person': self.target_team.name,
31- 'field.actions.deactivate_members_and_merge': 'Merge',
32- }
33+ def getView(self, form=None):
34+ if form is None:
35+ form = {
36+ 'field.dupe_person': self.dupe_team.name,
37+ 'field.target_person': self.target_team.name,
38+ 'field.actions.deactivate_members_and_merge': 'Merge',
39+ }
40 return create_initialized_view(
41 self.person_set, '+adminteammerge', form=form)
42
43@@ -130,7 +131,24 @@
44 super_team = self.factory.makeTeam()
45 login_celebrity('admin')
46 self.dupe_team.join(super_team, self.dupe_team.teamowner)
47- login_celebrity('registry_experts')
48+ login_person(self.dupe_team.teamowner)
49+ form = {
50+ 'field.dupe_person': self.dupe_team.name,
51+ 'field.target_person': self.target_team.name,
52+ 'field.actions.merge': 'Merge',
53+ }
54+ view = self.getView()
55+ self.assertEqual([], view.errors)
56+ self.assertEqual(self.target_team, self.dupe_team.merged)
57+
58+ def test_merge_team_as_delete_with_super_teams_into_registry_experts(
59+ self):
60+ # Super team memberships are removed.
61+ self.target_team = getUtility(ILaunchpadCelebrities).registry_experts
62+ super_team = self.factory.makeTeam()
63+ login_celebrity('admin')
64+ self.dupe_team.join(super_team, self.dupe_team.teamowner)
65+ login_person(self.dupe_team.teamowner)
66 view = self.getView()
67 self.assertEqual([], view.errors)
68 self.assertEqual(self.target_team, self.dupe_team.merged)
69
70=== modified file 'lib/lp/registry/doc/person-merge.txt'
71--- lib/lp/registry/doc/person-merge.txt 2010-10-10 15:39:28 +0000
72+++ lib/lp/registry/doc/person-merge.txt 2010-12-02 15:08:17 +0000
73@@ -423,9 +423,11 @@
74
75 >>> comment = "We're merging this team into another one."
76 >>> test_team.deactivateAllMembers(comment, personset.getByName('name16'))
77+ >>> for team in test_team.super_teams:
78+ ... test_team.retractTeamMembership(team, test_team.teamowner)
79 >>> personset.merge(test_team, landscape)
80
81- # The resulting Landscape-developers has now a super team, but has
82+ # The resulting Landscape-developers no new super teams, has
83 # no polls and its members are still the same two from before the
84 # merge.
85 >>> landscape.teamowner.name
86@@ -433,6 +435,6 @@
87 >>> [member.name for member in landscape.allmembers]
88 [u'salgado', u'name12']
89 >>> [team.name for team in landscape.super_teams]
90- [u'launchpad']
91+ []
92 >>> list(IPollSubset(landscape).getAll())
93 []
94
95=== modified file 'lib/lp/registry/model/person.py'
96--- lib/lp/registry/model/person.py 2010-11-24 10:10:07 +0000
97+++ lib/lp/registry/model/person.py 2010-12-02 15:08:17 +0000
98@@ -1473,6 +1473,38 @@
99 # Since we've updated the database behind Storm's back,
100 # flush its caches.
101 store.invalidate()
102+
103+
104+ # Remove all indirect TeamParticipation entries resulting from this
105+ # team. If this were just a select, it would be a complicated but
106+ # feasible set of joins. Since it's a delete, we have to use
107+ # some sub selects.
108+ cur.execute('''
109+ DELETE FROM TeamParticipation
110+ WHERE
111+ -- The person needs to be a member of the team in question
112+ person IN
113+ (SELECT person from TeamParticipation WHERE
114+ team = %(team)s) AND
115+
116+ -- The teams being deleted should be teams that this team
117+ -- is a member of.
118+ team IN
119+ (SELECT team from TeamMembership WHERE
120+ person = %(team)s) AND
121+
122+ -- The person needs to not have direct membership in the
123+ -- team.
124+ NOT EXISTS
125+ (SELECT tm1.person from TeamMembership tm1
126+ WHERE
127+ tm1.person = TeamParticipation.person and
128+ tm1.team = TeamParticipation.team);
129+ ''', dict(team=self.id))
130+
131+ # Since we've updated the database behind Storm's back yet again,
132+ # we need to flush its caches, again.
133+ store.invalidate()
134
135 # Remove all members from the TeamParticipation table
136 # except for the team, itself.
137@@ -3816,6 +3848,9 @@
138 raise AssertionError(
139 "Only teams without active members can be merged")
140
141+ if from_person.is_team and from_person.super_teams.count() > 0:
142+ raise AssertionError(
143+ "Only teams without super teams can be merged.")
144 # since we are doing direct SQL manipulation, make sure all
145 # changes have been flushed to the database
146 store = Store.of(from_person)
147
148=== modified file 'lib/lp/registry/tests/test_person.py'
149--- lib/lp/registry/tests/test_person.py 2010-11-08 01:08:15 +0000
150+++ lib/lp/registry/tests/test_person.py 2010-12-02 15:08:17 +0000
151@@ -226,7 +226,7 @@
152 def test_inTeam_person_string_missing_team(self):
153 # If a check against a string is done, the team lookup is implicit:
154 # treat a missing team as an empty team so that any pages that choose
155- # to do this don't blow up unnecessarily. Similarly feature flags
156+ # to do this don't blow up unnecessarily. Similarly feature flags
157 # team: scopes depend on this.
158 self.assertFalse(self.user.inTeam('does-not-exist'))
159
160@@ -599,6 +599,36 @@
161 self.person_set.merge(duplicate, person)
162 self.assertEqual(oldest_date, person.datecreated)
163
164+ def _doMerge(self, test_team, target_team):
165+ test_team.deactivateAllMembers(
166+ comment='',
167+ reviewer=test_team.teamowner)
168+ self.person_set.merge(test_team, target_team)
169+
170+ def test_team_without_super_teams_is_fine(self):
171+ # A team with no members and no super teams
172+ # merges without errors.
173+ test_team = self.factory.makeTeam()
174+ target_team = self.factory.makeTeam()
175+
176+ login_person(test_team.teamowner)
177+ self._doMerge(test_team, target_team)
178+
179+ def test_team_with_super_teams_raises_error(self):
180+ # A team with no members but with superteams
181+ # raises an assertion error.
182+ test_team = self.factory.makeTeam()
183+ super_team = self.factory.makeTeam()
184+ target_team = self.factory.makeTeam()
185+
186+ login_person(test_team.teamowner)
187+ test_team.join(super_team, test_team.teamowner)
188+ self.assertRaises(
189+ AssertionError,
190+ self._doMerge,
191+ test_team,
192+ target_team)
193+
194
195 class TestPersonSetCreateByOpenId(TestCaseWithFactory):
196 layer = DatabaseFunctionalLayer
197
198=== modified file 'lib/lp/registry/tests/test_team.py'
199--- lib/lp/registry/tests/test_team.py 2010-11-23 16:16:20 +0000
200+++ lib/lp/registry/tests/test_team.py 2010-12-02 15:08:17 +0000
201@@ -210,3 +210,28 @@
202 self.assertEqual(
203 None,
204 self.team.visibilityConsistencyWarning(PersonVisibility.PRIVATE))
205+
206+
207+class TestMembershipManagement(TestCaseWithFactory):
208+
209+ layer = DatabaseFunctionalLayer
210+
211+ def test_deactivateAllMembers_cleans_up_team_participation(self):
212+ superteam = self.factory.makeTeam(name='super')
213+ sharedteam = self.factory.makeTeam(name='shared')
214+ anotherteam = self.factory.makeTeam(name='another')
215+ targetteam = self.factory.makeTeam(name='target')
216+ person = self.factory.makePerson()
217+ login_celebrity('admin')
218+ person.join(targetteam)
219+ person.join(sharedteam)
220+ person.join(anotherteam)
221+ targetteam.join(superteam, targetteam.teamowner)
222+ targetteam.join(sharedteam, targetteam.teamowner)
223+ self.assertTrue(superteam in person.teams_participated_in)
224+ targetteam.deactivateAllMembers(
225+ comment='test',
226+ reviewer=targetteam.teamowner)
227+ self.assertEqual(
228+ sorted([sharedteam, anotherteam]),
229+ sorted([team for team in person.teams_participated_in]))