Merge lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout into lp:launchpad
- bug-664828-teammembership-delete-timeout
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Māris Fogels | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 12183 | ||||
Proposed branch: | lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
681 lines (+261/-191) 5 files modified
lib/lp/registry/browser/tests/test_teammembership.py (+69/-0) lib/lp/registry/doc/teammembership.txt (+2/-0) lib/lp/registry/interfaces/teammembership.py (+5/-1) lib/lp/registry/model/teammembership.py (+78/-160) lib/lp/registry/tests/test_teammembership.py (+107/-30) |
||||
To merge this branch: | bzr merge lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Māris Fogels (community) | Approve | ||
Review via email: mp+45805@code.launchpad.net |
Commit message
[r=mars][ui=none][bug=664828] Fixed timeout when deactivating a member of a team.
Description of the change
Summary
-------
This branch fixes the timeout when deactivating a member of a team that
was caused by the many queries necessary to traverse the TeamMembership
table to determine what entries need to be removed from the
TeamParticipation table.
Implementation details
-------
Used a postgresql 8.4 recursive query to traverse the TeamMembership
table to determine which entries should be kept in TeamParticipation.
lib/
Extended tests to verify that there are no extra deletions from the
TeamParticipation table as a side-effect. Added test to make sure that
the number of queries doesn't grow.
lib/
lib/
Minor edits:
lib/
lib/
Tests
-----
./bin/test -vv -t 'doc/teammember
Demo and Q/A
------------
* Open http://
Lint
----
No lint.
Robert Collins (lifeless) wrote : | # |
Edwin Grubbs (edwin-grubbs) wrote : | # |
> I'm confused - why is a recursive query needed?
>
> Isn't the same thing used to do the +participations able to serve the
> response in ~ 2 queries?
If Team B's membership in Team A is deactivated, then Team B and all of its participants may or may not still participate in Team A and its super teams. The only way to tell if the super teams still have participants via another path is by recursing the membership tree. This could either be done as a recursive query, which I chose, or in python.
Māris Fogels (mars) wrote : | # |
Hi Edwin,
The code in this change looks good to me, and the SQL is nicely documented. My first question is, where does the magic number '12' come from as the query count in test_deactivate
On a related note, is there already a test for the case you mentioned to Robert? "The only way to tell if the super teams still have participants via another path is by recursing the membership tree."
Maris
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Maris,
Thanks for the review. An incremental diff is included at the bottom.
> Hi Edwin,
>
> The code in this change looks good to me, and the SQL is nicely documented.
> My first question is, where does the magic number '12' come from as the query
> count in test_deactivate
> performance testing, but that one test and one number don't detail how the
> query is expected to perform using different data.
I have added a comment explaining the queries. I was able to reduce the number of queries by one by moving create_view() outside of the with-statement.
> On a related note, is there already a test for the case you mentioned to
> Robert? "The only way to tell if the super teams still have participants via
> another path is by recursing the membership tree."
lp/registry/
Incremental diff:
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -32,6 +32,25 @@
def test_deactivate
+ # Only these queries should be run, no matter what the
+ # membership tree looks like, although the number of queries
+ # could change slightly if a different user is logged in.
+ # 1. Check whether the user is the team owner.
+ # 2. Deactivate the membership in the TeamMembership table.
+ # 3. Delete from TeamParticipation table.
+ # (Queries #4, #5, #6, #7, and #10 are run because the storm
+ # objects have been invalidated.)
+ # 4. Get the TeamMembership entry.
+ # 5. Verify that the member exists in the db, but don't load
+ # the refresh the rest of its data, since we just need the id.
+ # 6. Verify that the user exists in the db.
+ # 7. Verify that the team exists in the db.
+ # 8. Insert into Job table.
+ # 9. Insert into PersonTransferJob table to schedule sending
+ # email. (This requires the data from queries #5, #6, and
+ # #7.)
+ # 10.Query the rest of the team data for the invalidated
+ # object in order to generate the canonical url.
form = {
@@ -41,9 +60,10 @@
}
membership = self.membership
+ view = create_view(
+ membership, "+index", method='POST', form=form)
with StormStatementR
- view = create_
- self...
Māris Fogels (mars) wrote : | # |
> Hi Maris,
>
> Thanks for the review. An incremental diff is included at the bottom.
>
Thanks Edwin, your code looks good. r=mars
Preview Diff
1 | === added file 'lib/lp/registry/browser/tests/test_teammembership.py' | |||
2 | --- lib/lp/registry/browser/tests/test_teammembership.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ lib/lp/registry/browser/tests/test_teammembership.py 2011-01-11 20:48:40 +0000 | |||
4 | @@ -0,0 +1,69 @@ | |||
5 | 1 | # Copyright 2011 Canonical Ltd. This software is licensed under the | ||
6 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
7 | 3 | |||
8 | 4 | __metaclass__ = type | ||
9 | 5 | |||
10 | 6 | from testtools.matchers import LessThan | ||
11 | 7 | from zope.component import getUtility | ||
12 | 8 | |||
13 | 9 | from canonical.testing.layers import DatabaseFunctionalLayer | ||
14 | 10 | from lp.testing.matchers import HasQueryCount | ||
15 | 11 | from lp.registry.interfaces.teammembership import ( | ||
16 | 12 | ITeamMembershipSet, | ||
17 | 13 | TeamMembershipStatus, | ||
18 | 14 | ) | ||
19 | 15 | from lp.testing import ( | ||
20 | 16 | login_celebrity, | ||
21 | 17 | StormStatementRecorder, | ||
22 | 18 | TestCaseWithFactory, | ||
23 | 19 | ) | ||
24 | 20 | from lp.testing.views import create_view | ||
25 | 21 | |||
26 | 22 | |||
27 | 23 | class TestTeamMenu(TestCaseWithFactory): | ||
28 | 24 | |||
29 | 25 | layer = DatabaseFunctionalLayer | ||
30 | 26 | |||
31 | 27 | def setUp(self): | ||
32 | 28 | super(TestTeamMenu, self).setUp() | ||
33 | 29 | login_celebrity('admin') | ||
34 | 30 | self.membership_set = getUtility(ITeamMembershipSet) | ||
35 | 31 | self.team = self.factory.makeTeam() | ||
36 | 32 | self.member = self.factory.makeTeam() | ||
37 | 33 | |||
38 | 34 | def test_deactivate_member_query_count(self): | ||
39 | 35 | # Only these queries should be run, no matter what the | ||
40 | 36 | # membership tree looks like, although the number of queries | ||
41 | 37 | # could change slightly if a different user is logged in. | ||
42 | 38 | # 1. Check whether the user is the team owner. | ||
43 | 39 | # 2. Deactivate the membership in the TeamMembership table. | ||
44 | 40 | # 3. Delete from TeamParticipation table. | ||
45 | 41 | # (Queries #4, #5, #6, #7, and #10 are run because the storm | ||
46 | 42 | # objects have been invalidated.) | ||
47 | 43 | # 4. Get the TeamMembership entry. | ||
48 | 44 | # 5. Verify that the member exists in the db, but don't load | ||
49 | 45 | # the refresh the rest of its data, since we just need the id. | ||
50 | 46 | # 6. Verify that the user exists in the db. | ||
51 | 47 | # 7. Verify that the team exists in the db. | ||
52 | 48 | # 8. Insert into Job table. | ||
53 | 49 | # 9. Insert into PersonTransferJob table to schedule sending | ||
54 | 50 | # email. (This requires the data from queries #5, #6, and | ||
55 | 51 | # #7.) | ||
56 | 52 | # 10.Query the rest of the team data for the invalidated | ||
57 | 53 | # object in order to generate the canonical url. | ||
58 | 54 | self.team.addMember( | ||
59 | 55 | self.member, self.team.teamowner, force_team_add=True) | ||
60 | 56 | form = { | ||
61 | 57 | 'editactive': 1, | ||
62 | 58 | 'expires': 'never', | ||
63 | 59 | 'deactivate': 'Deactivate', | ||
64 | 60 | } | ||
65 | 61 | membership = self.membership_set.getByPersonAndTeam( | ||
66 | 62 | self.member, self.team) | ||
67 | 63 | view = create_view( | ||
68 | 64 | membership, "+index", method='POST', form=form) | ||
69 | 65 | with StormStatementRecorder() as recorder: | ||
70 | 66 | view.processForm() | ||
71 | 67 | self.assertEqual('', view.errormessage) | ||
72 | 68 | self.assertEqual(TeamMembershipStatus.DEACTIVATED, membership.status) | ||
73 | 69 | self.assertThat(recorder, HasQueryCount(LessThan(11))) | ||
74 | 0 | 70 | ||
75 | === modified file 'lib/lp/registry/doc/teammembership.txt' | |||
76 | --- lib/lp/registry/doc/teammembership.txt 2010-10-24 12:46:23 +0000 | |||
77 | +++ lib/lp/registry/doc/teammembership.txt 2011-01-11 20:48:40 +0000 | |||
78 | @@ -131,6 +131,8 @@ | |||
79 | 131 | ... ubuntu_team, TeamMembershipStatus.DEACTIVATED, t2.teamowner) | 131 | ... ubuntu_team, TeamMembershipStatus.DEACTIVATED, t2.teamowner) |
80 | 132 | >>> ubuntu_team in t2.activemembers | 132 | >>> ubuntu_team in t2.activemembers |
81 | 133 | False | 133 | False |
82 | 134 | >>> [m.displayname for m in t2.allmembers] | ||
83 | 135 | [u'No Privileges Person'] | ||
84 | 134 | >>> login(ANONYMOUS) | 136 | >>> login(ANONYMOUS) |
85 | 135 | 137 | ||
86 | 136 | Another API for adding members is ITeam.addMember(), which ensures the given | 138 | Another API for adding members is ITeam.addMember(), which ensures the given |
87 | 137 | 139 | ||
88 | === modified file 'lib/lp/registry/interfaces/teammembership.py' | |||
89 | --- lib/lp/registry/interfaces/teammembership.py 2010-12-10 14:58:31 +0000 | |||
90 | +++ lib/lp/registry/interfaces/teammembership.py 2011-01-11 20:48:40 +0000 | |||
91 | @@ -116,7 +116,11 @@ | |||
92 | 116 | 116 | ||
93 | 117 | 117 | ||
94 | 118 | class ITeamMembership(Interface): | 118 | class ITeamMembership(Interface): |
96 | 119 | """TeamMembership for Users""" | 119 | """TeamMembership for Users. |
97 | 120 | |||
98 | 121 | This table includes *direct* team members only. Indirect memberships are | ||
99 | 122 | handled by the TeamParticipation table. | ||
100 | 123 | """ | ||
101 | 120 | export_as_webservice_entry() | 124 | export_as_webservice_entry() |
102 | 121 | 125 | ||
103 | 122 | id = Int(title=_('ID'), required=True, readonly=True) | 126 | id = Int(title=_('ID'), required=True, readonly=True) |
104 | 123 | 127 | ||
105 | === modified file 'lib/lp/registry/model/teammembership.py' | |||
106 | --- lib/lp/registry/model/teammembership.py 2010-12-10 14:58:31 +0000 | |||
107 | +++ lib/lp/registry/model/teammembership.py 2011-01-11 20:48:40 +0000 | |||
108 | @@ -21,7 +21,7 @@ | |||
109 | 21 | ForeignKey, | 21 | ForeignKey, |
110 | 22 | StringCol, | 22 | StringCol, |
111 | 23 | ) | 23 | ) |
113 | 24 | from storm.locals import Store | 24 | from storm.store import Store |
114 | 25 | from zope.component import getUtility | 25 | from zope.component import getUtility |
115 | 26 | from zope.interface import implements | 26 | from zope.interface import implements |
116 | 27 | 27 | ||
117 | @@ -498,167 +498,85 @@ | |||
118 | 498 | person = ForeignKey(dbName='person', foreignKey='Person', notNull=True) | 498 | person = ForeignKey(dbName='person', foreignKey='Person', notNull=True) |
119 | 499 | 499 | ||
120 | 500 | 500 | ||
228 | 501 | def _cleanTeamParticipation(person, team): | 501 | def _cleanTeamParticipation(child, parent): |
229 | 502 | """Remove relevant entries in TeamParticipation for <person> and <team>. | 502 | """Remove child from team and clean up child's subteams. |
230 | 503 | 503 | ||
231 | 504 | Remove all tuples "person, team" from TeamParticipation for the given | 504 | A participant of child is removed from parent's TeamParticipation |
232 | 505 | person and team (together with all its superteams), unless this person is | 505 | entries if the only path from the participant to parent is via |
233 | 506 | an indirect member of the given team. More information on how to use the | 506 | child. |
234 | 507 | TeamParticipation table can be found in the TeamParticipationUsage spec or | 507 | """ |
235 | 508 | the teammembership.txt system doctest. | 508 | # Delete participation entries for the child and the child's |
236 | 509 | """ | 509 | # direct/indirect members in other ancestor teams, unless those |
237 | 510 | query = """ | 510 | # ancestor teams have another path the the child besides the |
238 | 511 | SELECT EXISTS( | 511 | # membership that has just been deactivated. |
239 | 512 | SELECT 1 FROM TeamParticipation | 512 | store = Store.of(parent) |
240 | 513 | WHERE person = %(person_id)s AND team IN ( | 513 | store.execute(""" |
134 | 514 | SELECT person | ||
135 | 515 | FROM TeamParticipation JOIN Person ON | ||
136 | 516 | (person = Person.id) | ||
137 | 517 | WHERE team = %(team_id)s | ||
138 | 518 | AND person NOT IN (%(team_id)s, %(person_id)s) | ||
139 | 519 | AND teamowner IS NOT NULL | ||
140 | 520 | ) | ||
141 | 521 | ) | ||
142 | 522 | """ % dict(team_id=team.id, person_id=person.id) | ||
143 | 523 | store = Store.of(person) | ||
144 | 524 | (result, ) = store.execute(query).get_one() | ||
145 | 525 | if result: | ||
146 | 526 | # The person is a participant in this team by virtue of a membership | ||
147 | 527 | # in another one, so don't attempt to remove anything. | ||
148 | 528 | return | ||
149 | 529 | |||
150 | 530 | # First of all, we remove <person> from <team> (and its superteams). | ||
151 | 531 | _removeParticipantFromTeamAndSuperTeams(person, team) | ||
152 | 532 | if not person.is_team: | ||
153 | 533 | # Nothing else to do. | ||
154 | 534 | return | ||
155 | 535 | |||
156 | 536 | store = Store.of(person) | ||
157 | 537 | |||
158 | 538 | # Clean the participation of all our participant subteams, that are | ||
159 | 539 | # not a direct members of the target team. | ||
160 | 540 | query = """ | ||
161 | 541 | -- All of my participant subteams... | ||
162 | 542 | SELECT person | ||
163 | 543 | FROM TeamParticipation JOIN Person ON (person = Person.id) | ||
164 | 544 | WHERE team = %(person_id)s AND person != %(person_id)s | ||
165 | 545 | AND teamowner IS NOT NULL | ||
166 | 546 | EXCEPT | ||
167 | 547 | -- that aren't a direct member of the team. | ||
168 | 548 | SELECT person | ||
169 | 549 | FROM TeamMembership | ||
170 | 550 | WHERE team = %(team_id)s AND status IN %(active_states)s | ||
171 | 551 | """ % dict( | ||
172 | 552 | person_id=person.id, team_id=team.id, | ||
173 | 553 | active_states=sqlvalues(ACTIVE_STATES)[0]) | ||
174 | 554 | |||
175 | 555 | # Avoid circular import. | ||
176 | 556 | from lp.registry.model.person import Person | ||
177 | 557 | for subteam in store.find(Person, "id IN (%s)" % query): | ||
178 | 558 | _cleanTeamParticipation(subteam, team) | ||
179 | 559 | |||
180 | 560 | # Then clean-up all the non-team participants. We can remove those | ||
181 | 561 | # in a single query when the team graph is up to date. | ||
182 | 562 | _removeAllIndividualParticipantsFromTeamAndSuperTeams(person, team) | ||
183 | 563 | |||
184 | 564 | |||
185 | 565 | def _removeParticipantFromTeamAndSuperTeams(person, team): | ||
186 | 566 | """Remove participation of person in team. | ||
187 | 567 | |||
188 | 568 | If <person> is a participant (that is, has a TeamParticipation entry) | ||
189 | 569 | of any team that is a subteam of <team>, then <person> should be kept as | ||
190 | 570 | a participant of <team> and (as a consequence) all its superteams. | ||
191 | 571 | Otherwise, <person> is removed from <team> and we repeat this process for | ||
192 | 572 | each superteam of <team>. | ||
193 | 573 | """ | ||
194 | 574 | # Check if the person is a member of the given team through another team. | ||
195 | 575 | query = """ | ||
196 | 576 | SELECT EXISTS( | ||
197 | 577 | SELECT 1 | ||
198 | 578 | FROM TeamParticipation, TeamMembership | ||
199 | 579 | WHERE | ||
200 | 580 | TeamMembership.team = %(team_id)s AND | ||
201 | 581 | TeamMembership.person = TeamParticipation.team AND | ||
202 | 582 | TeamParticipation.person = %(person_id)s AND | ||
203 | 583 | TeamMembership.status IN %(active_states)s) | ||
204 | 584 | """ % dict(team_id=team.id, person_id=person.id, | ||
205 | 585 | active_states=sqlvalues(ACTIVE_STATES)[0]) | ||
206 | 586 | store = Store.of(person) | ||
207 | 587 | (result, ) = store.execute(query).get_one() | ||
208 | 588 | if result: | ||
209 | 589 | # The person is a participant by virtue of a membership on another | ||
210 | 590 | # team, so don't remove. | ||
211 | 591 | return | ||
212 | 592 | store.find(TeamParticipation, ( | ||
213 | 593 | (TeamParticipation.team == team) & | ||
214 | 594 | (TeamParticipation.person == person))).remove() | ||
215 | 595 | |||
216 | 596 | for superteam in _getSuperTeamsExcludingDirectMembership(person, team): | ||
217 | 597 | _removeParticipantFromTeamAndSuperTeams(person, superteam) | ||
218 | 598 | |||
219 | 599 | |||
220 | 600 | def _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, target_team): | ||
221 | 601 | """Remove all non-team participants in <team> from <target_team>. | ||
222 | 602 | |||
223 | 603 | All the non-team participants of <team> are removed from <target_team> | ||
224 | 604 | and its super teams, unless they participate in <target_team> also from | ||
225 | 605 | one of its sub team. | ||
226 | 606 | """ | ||
227 | 607 | query = """ | ||
241 | 608 | DELETE FROM TeamParticipation | 514 | DELETE FROM TeamParticipation |
250 | 609 | WHERE team = %(target_team_id)s AND person IN ( | 515 | USING ( |
251 | 610 | -- All the individual participants. | 516 | /* Get all the participation entries that might need to be |
252 | 611 | SELECT person | 517 | * deleted, i.e. all the entries where the |
253 | 612 | FROM TeamParticipation JOIN Person ON (person = Person.id) | 518 | * TeamParticipation.person is a participant of child, and |
254 | 613 | WHERE team = %(team_id)s AND teamowner IS NULL | 519 | * where child participated in TeamParticipation.team until |
255 | 614 | EXCEPT | 520 | * child was removed from parent. |
256 | 615 | -- people participating through a subteam of target_team; | 521 | */ |
257 | 616 | SELECT person | 522 | SELECT person, team |
258 | 617 | FROM TeamParticipation | 523 | FROM TeamParticipation |
303 | 618 | WHERE team IN ( | 524 | WHERE person IN ( |
304 | 619 | -- The subteams of target_team. | 525 | SELECT person |
305 | 620 | SELECT person | 526 | FROM TeamParticipation |
306 | 621 | FROM TeamParticipation JOIN Person ON (person = Person.id) | 527 | WHERE team = %(child)s |
307 | 622 | WHERE team = %(target_team_id)s | 528 | ) |
308 | 623 | AND person NOT IN (%(target_team_id)s, %(team_id)s) | 529 | AND team IN ( |
309 | 624 | AND teamowner IS NOT NULL | 530 | SELECT team |
310 | 625 | ) | 531 | FROM TeamParticipation |
311 | 626 | -- or people directly a member of the target team. | 532 | WHERE person = %(child)s |
312 | 627 | EXCEPT | 533 | AND team != %(child)s |
313 | 628 | SELECT person | 534 | ) |
314 | 629 | FROM TeamMembership | 535 | |
315 | 630 | WHERE team = %(target_team_id)s AND status IN %(active_states)s | 536 | |
316 | 631 | ) | 537 | EXCEPT ( |
317 | 632 | """ % dict( | 538 | |
318 | 633 | team_id=team.id, target_team_id=target_team.id, | 539 | /* Compute the TeamParticipation entries that we need to |
319 | 634 | active_states=sqlvalues(ACTIVE_STATES)[0]) | 540 | * keep by walking the tree in the TeamMembership table. |
320 | 635 | store = Store.of(team) | 541 | */ |
321 | 636 | store.execute(query) | 542 | WITH RECURSIVE parent(person, team) AS ( |
322 | 637 | 543 | /* Start by getting all the ancestors of the child | |
323 | 638 | super_teams = _getSuperTeamsExcludingDirectMembership(team, target_team) | 544 | * from the TeamParticipation table, then get those |
324 | 639 | for superteam in super_teams: | 545 | * ancestors' direct members to recurse through the |
325 | 640 | _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, superteam) | 546 | * tree from the top. |
326 | 641 | 547 | */ | |
327 | 642 | 548 | SELECT ancestor.person, ancestor.team | |
328 | 643 | def _getSuperTeamsExcludingDirectMembership(person, team): | 549 | FROM TeamMembership ancestor |
329 | 644 | """Return all the super teams of <team> where person isn't a member.""" | 550 | WHERE ancestor.status IN %(active_states)s |
330 | 645 | query = """ | 551 | AND ancestor.team IN ( |
331 | 646 | -- All the super teams... | 552 | SELECT team |
332 | 647 | SELECT team | 553 | FROM TeamParticipation |
333 | 648 | FROM TeamParticipation | 554 | WHERE person = %(child)s |
334 | 649 | WHERE person = %(team_id)s AND team != %(team_id)s | 555 | ) |
335 | 650 | EXCEPT | 556 | |
336 | 651 | -- The one where person has an active membership. | 557 | UNION |
337 | 652 | SELECT team | 558 | |
338 | 653 | FROM TeamMembership | 559 | /* Find the next level of direct members, but hold |
339 | 654 | WHERE person = %(person_id)s AND status IN %(active_states)s | 560 | * onto the parent.team, since we want the top and |
340 | 655 | """ % dict( | 561 | * bottom of the hierarchy to calculate the |
341 | 656 | person_id=person.id, team_id=team.id, | 562 | * TeamParticipation. The query above makes sure |
342 | 657 | active_states=sqlvalues(ACTIVE_STATES)[0]) | 563 | * that we do this for all the ancestors. |
343 | 658 | 564 | */ | |
344 | 659 | # Avoid circular import. | 565 | SELECT child.person, parent.team |
345 | 660 | from lp.registry.model.person import Person | 566 | FROM TeamMembership child |
346 | 661 | return Store.of(person).find(Person, "id IN (%s)" % query) | 567 | JOIN parent ON child.team = parent.person |
347 | 568 | WHERE child.status IN %(active_states)s | ||
348 | 569 | ) | ||
349 | 570 | SELECT person, team | ||
350 | 571 | FROM parent | ||
351 | 572 | ) | ||
352 | 573 | ) AS keeping | ||
353 | 574 | WHERE TeamParticipation.person = keeping.person | ||
354 | 575 | AND TeamParticipation.team = keeping.team | ||
355 | 576 | """ % sqlvalues( | ||
356 | 577 | child=child.id, | ||
357 | 578 | active_states=ACTIVE_STATES)) | ||
358 | 579 | store.invalidate() | ||
359 | 662 | 580 | ||
360 | 663 | 581 | ||
361 | 664 | def _fillTeamParticipation(member, accepting_team): | 582 | def _fillTeamParticipation(member, accepting_team): |
362 | 665 | 583 | ||
363 | === modified file 'lib/lp/registry/tests/test_teammembership.py' | |||
364 | --- lib/lp/registry/tests/test_teammembership.py 2010-12-02 16:13:51 +0000 | |||
365 | +++ lib/lp/registry/tests/test_teammembership.py 2011-01-11 20:48:40 +0000 | |||
366 | @@ -9,7 +9,10 @@ | |||
367 | 9 | ) | 9 | ) |
368 | 10 | import re | 10 | import re |
369 | 11 | import subprocess | 11 | import subprocess |
371 | 12 | import unittest | 12 | from unittest import ( |
372 | 13 | TestCase, | ||
373 | 14 | TestLoader, | ||
374 | 15 | ) | ||
375 | 13 | 16 | ||
376 | 14 | import pytz | 17 | import pytz |
377 | 15 | from zope.component import getUtility | 18 | from zope.component import getUtility |
378 | @@ -24,6 +27,7 @@ | |||
379 | 24 | login, | 27 | login, |
380 | 25 | login_person, | 28 | login_person, |
381 | 26 | ) | 29 | ) |
382 | 30 | from canonical.launchpad.interfaces.lpstorm import IStore | ||
383 | 27 | from canonical.launchpad.testing.systemdocs import ( | 31 | from canonical.launchpad.testing.systemdocs import ( |
384 | 28 | default_optionflags, | 32 | default_optionflags, |
385 | 29 | LayeredDocFileSuite, | 33 | LayeredDocFileSuite, |
386 | @@ -40,11 +44,14 @@ | |||
387 | 40 | ITeamMembershipSet, | 44 | ITeamMembershipSet, |
388 | 41 | TeamMembershipStatus, | 45 | TeamMembershipStatus, |
389 | 42 | ) | 46 | ) |
395 | 43 | from lp.registry.model.teammembership import TeamMembership | 47 | from lp.registry.model.teammembership import ( |
396 | 44 | from lp.testing.factory import LaunchpadObjectFactory | 48 | TeamMembership, |
397 | 45 | 49 | TeamParticipation, | |
398 | 46 | 50 | ) | |
399 | 47 | class TestTeamMembershipSet(unittest.TestCase): | 51 | from lp.testing import TestCaseWithFactory |
400 | 52 | |||
401 | 53 | |||
402 | 54 | class TestTeamMembershipSet(TestCase): | ||
403 | 48 | layer = DatabaseFunctionalLayer | 55 | layer = DatabaseFunctionalLayer |
404 | 49 | 56 | ||
405 | 50 | def setUp(self): | 57 | def setUp(self): |
406 | @@ -155,11 +162,12 @@ | |||
407 | 155 | self.failIf(sample_person.inTeam(motu)) | 162 | self.failIf(sample_person.inTeam(motu)) |
408 | 156 | 163 | ||
409 | 157 | 164 | ||
411 | 158 | class TeamParticipationTestCase(unittest.TestCase): | 165 | class TeamParticipationTestCase(TestCaseWithFactory): |
412 | 159 | """Tests for team participation using 5 teams.""" | 166 | """Tests for team participation using 5 teams.""" |
413 | 160 | layer = DatabaseFunctionalLayer | 167 | layer = DatabaseFunctionalLayer |
414 | 161 | 168 | ||
415 | 162 | def setUp(self): | 169 | def setUp(self): |
416 | 170 | super(TeamParticipationTestCase, self).setUp() | ||
417 | 163 | login('foo.bar@canonical.com') | 171 | login('foo.bar@canonical.com') |
418 | 164 | person_set = getUtility(IPersonSet) | 172 | person_set = getUtility(IPersonSet) |
419 | 165 | self.foo_bar = person_set.getByEmail('foo.bar@canonical.com') | 173 | self.foo_bar = person_set.getByEmail('foo.bar@canonical.com') |
420 | @@ -177,6 +185,9 @@ | |||
421 | 177 | sorted(participant_names), | 185 | sorted(participant_names), |
422 | 178 | sorted([participant.name for participant in team.allmembers])) | 186 | sorted([participant.name for participant in team.allmembers])) |
423 | 179 | 187 | ||
424 | 188 | def getTeamParticipationCount(self): | ||
425 | 189 | return IStore(TeamParticipation).find(TeamParticipation).count() | ||
426 | 190 | |||
427 | 180 | 191 | ||
428 | 181 | class TestTeamParticipationHierarchy(TeamParticipationTestCase): | 192 | class TestTeamParticipationHierarchy(TeamParticipationTestCase): |
429 | 182 | """Participation management tests using 5 nested teams. | 193 | """Participation management tests using 5 nested teams. |
430 | @@ -221,6 +232,7 @@ | |||
431 | 221 | 232 | ||
432 | 222 | This is similar to what was experienced in bug 261915. | 233 | This is similar to what was experienced in bug 261915. |
433 | 223 | """ | 234 | """ |
434 | 235 | previous_count = self.getTeamParticipationCount() | ||
435 | 224 | self.team2.setMembershipData( | 236 | self.team2.setMembershipData( |
436 | 225 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 237 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
437 | 226 | self.assertParticipantsEquals(['name16', 'team2'], self.team1) | 238 | self.assertParticipantsEquals(['name16', 'team2'], self.team1) |
438 | @@ -230,11 +242,15 @@ | |||
439 | 230 | self.assertParticipantsEquals( | 242 | self.assertParticipantsEquals( |
440 | 231 | ['name16', 'no-priv', 'team5'], self.team4) | 243 | ['name16', 'no-priv', 'team5'], self.team4) |
441 | 232 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 244 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
442 | 245 | self.assertEqual( | ||
443 | 246 | previous_count-8, | ||
444 | 247 | self.getTeamParticipationCount()) | ||
445 | 233 | 248 | ||
446 | 234 | def testRemovingLeafTeam(self): | 249 | def testRemovingLeafTeam(self): |
447 | 235 | """Make sure that participations are updated correctly when removing | 250 | """Make sure that participations are updated correctly when removing |
448 | 236 | the leaf team. | 251 | the leaf team. |
449 | 237 | """ | 252 | """ |
450 | 253 | previous_count = self.getTeamParticipationCount() | ||
451 | 238 | self.team4.setMembershipData( | 254 | self.team4.setMembershipData( |
452 | 239 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 255 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
453 | 240 | self.assertParticipantsEquals( | 256 | self.assertParticipantsEquals( |
454 | @@ -244,6 +260,9 @@ | |||
455 | 244 | self.assertParticipantsEquals(['name16', 'team4'], self.team3) | 260 | self.assertParticipantsEquals(['name16', 'team4'], self.team3) |
456 | 245 | self.assertParticipantsEquals(['name16'], self.team4) | 261 | self.assertParticipantsEquals(['name16'], self.team4) |
457 | 246 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 262 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
458 | 263 | self.assertEqual( | ||
459 | 264 | previous_count-8, | ||
460 | 265 | self.getTeamParticipationCount()) | ||
461 | 247 | 266 | ||
462 | 248 | 267 | ||
463 | 249 | class TestTeamParticipationTree(TeamParticipationTestCase): | 268 | class TestTeamParticipationTree(TeamParticipationTestCase): |
464 | @@ -252,9 +271,10 @@ | |||
465 | 252 | Create a team hierarchy looking like this: | 271 | Create a team hierarchy looking like this: |
466 | 253 | team1 | 272 | team1 |
467 | 254 | team2 | 273 | team2 |
471 | 255 | team5 team3 | 274 | team5 |
472 | 256 | team4 | 275 | team3 |
473 | 257 | team5 | 276 | team4 |
474 | 277 | team5 | ||
475 | 258 | no-priv | 278 | no-priv |
476 | 259 | """ | 279 | """ |
477 | 260 | layer = DatabaseFunctionalLayer | 280 | layer = DatabaseFunctionalLayer |
478 | @@ -269,6 +289,10 @@ | |||
479 | 269 | self.team3.addMember(self.team4, self.foo_bar, force_team_add=True) | 289 | self.team3.addMember(self.team4, self.foo_bar, force_team_add=True) |
480 | 270 | self.team4.addMember(self.team5, self.foo_bar, force_team_add=True) | 290 | self.team4.addMember(self.team5, self.foo_bar, force_team_add=True) |
481 | 271 | 291 | ||
482 | 292 | def tearDown(self): | ||
483 | 293 | super(TestTeamParticipationTree, self).tearDown() | ||
484 | 294 | self.layer.force_dirty_database() | ||
485 | 295 | |||
486 | 272 | def testTeamParticipationSetUp(self): | 296 | def testTeamParticipationSetUp(self): |
487 | 273 | """Make sure that the TeamParticipation are sane after setUp.""" | 297 | """Make sure that the TeamParticipation are sane after setUp.""" |
488 | 274 | self.assertParticipantsEquals( | 298 | self.assertParticipantsEquals( |
489 | @@ -284,6 +308,7 @@ | |||
490 | 284 | ['name16', 'no-priv'], self.team5) | 308 | ['name16', 'no-priv'], self.team5) |
491 | 285 | 309 | ||
492 | 286 | def testRemoveTeam3FromTeam2(self): | 310 | def testRemoveTeam3FromTeam2(self): |
493 | 311 | previous_count = self.getTeamParticipationCount() | ||
494 | 287 | self.team2.setMembershipData( | 312 | self.team2.setMembershipData( |
495 | 288 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 313 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
496 | 289 | self.assertParticipantsEquals( | 314 | self.assertParticipantsEquals( |
497 | @@ -295,8 +320,12 @@ | |||
498 | 295 | self.assertParticipantsEquals( | 320 | self.assertParticipantsEquals( |
499 | 296 | ['name16', 'no-priv', 'team5'], self.team4) | 321 | ['name16', 'no-priv', 'team5'], self.team4) |
500 | 297 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 322 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
501 | 323 | self.assertEqual( | ||
502 | 324 | previous_count-4, | ||
503 | 325 | self.getTeamParticipationCount()) | ||
504 | 298 | 326 | ||
505 | 299 | def testRemoveTeam5FromTeam4(self): | 327 | def testRemoveTeam5FromTeam4(self): |
506 | 328 | previous_count = self.getTeamParticipationCount() | ||
507 | 300 | self.team4.setMembershipData( | 329 | self.team4.setMembershipData( |
508 | 301 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 330 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
509 | 302 | self.assertParticipantsEquals( | 331 | self.assertParticipantsEquals( |
510 | @@ -308,6 +337,40 @@ | |||
511 | 308 | ['name16', 'team4'], self.team3) | 337 | ['name16', 'team4'], self.team3) |
512 | 309 | self.assertParticipantsEquals(['name16'], self.team4) | 338 | self.assertParticipantsEquals(['name16'], self.team4) |
513 | 310 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 339 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
514 | 340 | self.assertEqual( | ||
515 | 341 | previous_count-4, | ||
516 | 342 | self.getTeamParticipationCount()) | ||
517 | 343 | |||
518 | 344 | |||
519 | 345 | class TestParticipationCleanup(TeamParticipationTestCase): | ||
520 | 346 | """Test deletion of a member from a team with many superteams. | ||
521 | 347 | Create a team hierarchy looking like this: | ||
522 | 348 | team1 | ||
523 | 349 | team2 | ||
524 | 350 | team3 | ||
525 | 351 | team4 | ||
526 | 352 | team5 | ||
527 | 353 | no-priv | ||
528 | 354 | """ | ||
529 | 355 | |||
530 | 356 | def setUp(self): | ||
531 | 357 | """Setup the team hierarchy.""" | ||
532 | 358 | super(TestParticipationCleanup, self).setUp() | ||
533 | 359 | self.team1.addMember(self.team2, self.foo_bar, force_team_add=True) | ||
534 | 360 | self.team2.addMember(self.team3, self.foo_bar, force_team_add=True) | ||
535 | 361 | self.team3.addMember(self.team4, self.foo_bar, force_team_add=True) | ||
536 | 362 | self.team4.addMember(self.team5, self.foo_bar, force_team_add=True) | ||
537 | 363 | self.team5.addMember(self.no_priv, self.foo_bar) | ||
538 | 364 | |||
539 | 365 | def testMemberRemoval(self): | ||
540 | 366 | """Remove the member from the last team. | ||
541 | 367 | |||
542 | 368 | The number of db queries should be constant not O(depth). | ||
543 | 369 | """ | ||
544 | 370 | self.assertStatementCount( | ||
545 | 371 | 7, | ||
546 | 372 | self.team5.setMembershipData, self.no_priv, | ||
547 | 373 | TeamMembershipStatus.DEACTIVATED, self.team5.teamowner) | ||
548 | 311 | 374 | ||
549 | 312 | 375 | ||
550 | 313 | class TestTeamParticipationMesh(TeamParticipationTestCase): | 376 | class TestTeamParticipationMesh(TeamParticipationTestCase): |
551 | @@ -315,12 +378,15 @@ | |||
552 | 315 | branches. | 378 | branches. |
553 | 316 | 379 | ||
554 | 317 | Create a team hierarchy looking like this: | 380 | Create a team hierarchy looking like this: |
561 | 318 | team1 /--team6 | 381 | team1 team6 |
562 | 319 | team2 \ | 382 | \ / | |
563 | 320 | | team3 | | 383 | team2 | |
564 | 321 | \--- team4-/ | 384 | / | | |
565 | 322 | team5 | 385 | team3 | | |
566 | 323 | no-priv | 386 | \ | / |
567 | 387 | team4 | ||
568 | 388 | team5 | ||
569 | 389 | no-priv | ||
570 | 324 | """ | 390 | """ |
571 | 325 | layer = DatabaseFunctionalLayer | 391 | layer = DatabaseFunctionalLayer |
572 | 326 | 392 | ||
573 | @@ -338,6 +404,10 @@ | |||
574 | 338 | self.team6.addMember(self.team2, self.foo_bar, force_team_add=True) | 404 | self.team6.addMember(self.team2, self.foo_bar, force_team_add=True) |
575 | 339 | self.team6.addMember(self.team4, self.foo_bar, force_team_add=True) | 405 | self.team6.addMember(self.team4, self.foo_bar, force_team_add=True) |
576 | 340 | 406 | ||
577 | 407 | def tearDown(self): | ||
578 | 408 | super(TestTeamParticipationMesh, self).tearDown() | ||
579 | 409 | self.layer.force_dirty_database() | ||
580 | 410 | |||
581 | 341 | def testTeamParticipationSetUp(self): | 411 | def testTeamParticipationSetUp(self): |
582 | 342 | """Make sure that the TeamParticipation are sane after setUp.""" | 412 | """Make sure that the TeamParticipation are sane after setUp.""" |
583 | 343 | self.assertParticipantsEquals( | 413 | self.assertParticipantsEquals( |
584 | @@ -355,6 +425,7 @@ | |||
585 | 355 | self.team6) | 425 | self.team6) |
586 | 356 | 426 | ||
587 | 357 | def testRemoveTeam3FromTeam2(self): | 427 | def testRemoveTeam3FromTeam2(self): |
588 | 428 | previous_count = self.getTeamParticipationCount() | ||
589 | 358 | self.team2.setMembershipData( | 429 | self.team2.setMembershipData( |
590 | 359 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 430 | self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
591 | 360 | self.assertParticipantsEquals( | 431 | self.assertParticipantsEquals( |
592 | @@ -368,8 +439,12 @@ | |||
593 | 368 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 439 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
594 | 369 | self.assertParticipantsEquals( | 440 | self.assertParticipantsEquals( |
595 | 370 | ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6) | 441 | ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6) |
596 | 442 | self.assertEqual( | ||
597 | 443 | previous_count-3, | ||
598 | 444 | self.getTeamParticipationCount()) | ||
599 | 371 | 445 | ||
600 | 372 | def testRemoveTeam5FromTeam4(self): | 446 | def testRemoveTeam5FromTeam4(self): |
601 | 447 | previous_count = self.getTeamParticipationCount() | ||
602 | 373 | self.team4.setMembershipData( | 448 | self.team4.setMembershipData( |
603 | 374 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) | 449 | self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar) |
604 | 375 | self.assertParticipantsEquals( | 450 | self.assertParticipantsEquals( |
605 | @@ -381,9 +456,12 @@ | |||
606 | 381 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) | 456 | self.assertParticipantsEquals(['name16', 'no-priv'], self.team5) |
607 | 382 | self.assertParticipantsEquals( | 457 | self.assertParticipantsEquals( |
608 | 383 | ['name16', 'team2', 'team3', 'team4'], self.team6) | 458 | ['name16', 'team2', 'team3', 'team4'], self.team6) |
612 | 384 | 459 | self.assertEqual( | |
613 | 385 | 460 | previous_count-10, | |
614 | 386 | class TestTeamMembership(unittest.TestCase): | 461 | self.getTeamParticipationCount()) |
615 | 462 | |||
616 | 463 | |||
617 | 464 | class TestTeamMembership(TestCaseWithFactory): | ||
618 | 387 | layer = DatabaseFunctionalLayer | 465 | layer = DatabaseFunctionalLayer |
619 | 388 | 466 | ||
620 | 389 | def test_teams_not_kicked_from_themselves_bug_248498(self): | 467 | def test_teams_not_kicked_from_themselves_bug_248498(self): |
621 | @@ -400,12 +478,11 @@ | |||
622 | 400 | This test will make sure that doesn't happen in the future. | 478 | This test will make sure that doesn't happen in the future. |
623 | 401 | """ | 479 | """ |
624 | 402 | login('test@canonical.com') | 480 | login('test@canonical.com') |
627 | 403 | factory = LaunchpadObjectFactory() | 481 | person = self.factory.makePerson() |
626 | 404 | person = factory.makePerson() | ||
628 | 405 | login_person(person) # Now login with the future owner of the teams. | 482 | login_person(person) # Now login with the future owner of the teams. |
630 | 406 | teamA = factory.makeTeam( | 483 | teamA = self.factory.makeTeam( |
631 | 407 | person, subscription_policy=TeamSubscriptionPolicy.MODERATED) | 484 | person, subscription_policy=TeamSubscriptionPolicy.MODERATED) |
633 | 408 | teamB = factory.makeTeam( | 485 | teamB = self.factory.makeTeam( |
634 | 409 | person, subscription_policy=TeamSubscriptionPolicy.MODERATED) | 486 | person, subscription_policy=TeamSubscriptionPolicy.MODERATED) |
635 | 410 | self.failUnless( | 487 | self.failUnless( |
636 | 411 | teamA.inTeam(teamA), "teamA is not a participant of itself") | 488 | teamA.inTeam(teamA), "teamA is not a participant of itself") |
637 | @@ -444,21 +521,21 @@ | |||
638 | 444 | self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value) | 521 | self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value) |
639 | 445 | 522 | ||
640 | 446 | 523 | ||
642 | 447 | class TestTeamMembershipSetStatus(unittest.TestCase): | 524 | class TestTeamMembershipSetStatus(TestCaseWithFactory): |
643 | 448 | """Test the behaviour of TeamMembership's setStatus().""" | 525 | """Test the behaviour of TeamMembership's setStatus().""" |
644 | 449 | layer = DatabaseFunctionalLayer | 526 | layer = DatabaseFunctionalLayer |
645 | 450 | 527 | ||
646 | 451 | def setUp(self): | 528 | def setUp(self): |
647 | 529 | super(TestTeamMembershipSetStatus, self).setUp() | ||
648 | 452 | login('foo.bar@canonical.com') | 530 | login('foo.bar@canonical.com') |
649 | 453 | self.foobar = getUtility(IPersonSet).getByName('name16') | 531 | self.foobar = getUtility(IPersonSet).getByName('name16') |
650 | 454 | self.no_priv = getUtility(IPersonSet).getByName('no-priv') | 532 | self.no_priv = getUtility(IPersonSet).getByName('no-priv') |
651 | 455 | self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team') | 533 | self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team') |
652 | 456 | self.admins = getUtility(IPersonSet).getByName('admins') | 534 | self.admins = getUtility(IPersonSet).getByName('admins') |
653 | 457 | # Create a bunch of arbitrary teams to use in the tests. | 535 | # Create a bunch of arbitrary teams to use in the tests. |
658 | 458 | factory = LaunchpadObjectFactory() | 536 | self.team1 = self.factory.makeTeam(self.foobar) |
659 | 459 | self.team1 = factory.makeTeam(self.foobar) | 537 | self.team2 = self.factory.makeTeam(self.foobar) |
660 | 460 | self.team2 = factory.makeTeam(self.foobar) | 538 | self.team3 = self.factory.makeTeam(self.foobar) |
657 | 461 | self.team3 = factory.makeTeam(self.foobar) | ||
661 | 462 | 539 | ||
662 | 463 | def test_proponent_is_stored(self): | 540 | def test_proponent_is_stored(self): |
663 | 464 | for status in [TeamMembershipStatus.DEACTIVATED, | 541 | for status in [TeamMembershipStatus.DEACTIVATED, |
664 | @@ -698,7 +775,7 @@ | |||
665 | 698 | self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status) | 775 | self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status) |
666 | 699 | 776 | ||
667 | 700 | 777 | ||
669 | 701 | class TestCheckTeamParticipationScript(unittest.TestCase): | 778 | class TestCheckTeamParticipationScript(TestCase): |
670 | 702 | layer = DatabaseFunctionalLayer | 779 | layer = DatabaseFunctionalLayer |
671 | 703 | 780 | ||
672 | 704 | def _runScript(self, expected_returncode=0): | 781 | def _runScript(self, expected_returncode=0): |
673 | @@ -803,7 +880,7 @@ | |||
674 | 803 | 880 | ||
675 | 804 | 881 | ||
676 | 805 | def test_suite(): | 882 | def test_suite(): |
678 | 806 | suite = unittest.TestLoader().loadTestsFromName(__name__) | 883 | suite = TestLoader().loadTestsFromName(__name__) |
679 | 807 | bug_249185 = LayeredDocFileSuite( | 884 | bug_249185 = LayeredDocFileSuite( |
680 | 808 | 'bug-249185.txt', optionflags=default_optionflags, | 885 | 'bug-249185.txt', optionflags=default_optionflags, |
681 | 809 | layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown) | 886 | layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown) |
I'm confused - why is a recursive query needed?
Isn't the same thing used to do the +participations able to serve the
response in ~ 2 queries?