Merge lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout into lp:launchpad

Proposed by Edwin Grubbs
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
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/lp/registry/model/teammembership.py

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/lp/registry/tests/test_teammembership.py
    lib/lp/registry/browser/tests/test_teammembership.py

Minor edits:
    lib/lp/registry/doc/teammembership.txt
    lib/lp/registry/interfaces/teammembership.py

Tests
-----

./bin/test -vv -t 'doc/teammembership.txt|registry.tests.test_teammembership'

Demo and Q/A
------------

* Open http://launchpad.dev/...

Lint
----

No lint.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) 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?

Revision history for this message
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.

Revision history for this message
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_member_query_count? I know the test is for performance testing, but that one test and one number don't detail how the query is expected to perform using different data.

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

review: Needs Fixing
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (3.2 KiB)

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_member_query_count? I know the test is for
> 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/tests/test_teammembership.py already has the TestTeamParticipationMesh, TestTeamParticipationTable, and TestTeamParticipationHierarchy tests that verify that the TeamParticipation table is updated correctly.

Incremental diff:

=== modified file 'lib/lp/registry/browser/tests/test_teammembership.py'
--- lib/lp/registry/browser/tests/test_teammembership.py 2011-01-11 01:40:09 +0000
+++ lib/lp/registry/browser/tests/test_teammembership.py 2011-01-11 20:45:04 +0000
@@ -32,6 +32,25 @@
         self.member = self.factory.makeTeam()

     def test_deactivate_member_query_count(self):
+ # 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.
         self.team.addMember(
             self.member, self.team.teamowner, force_team_add=True)
         form = {
@@ -41,9 +60,10 @@
             }
         membership = self.membership_set.getByPersonAndTeam(
             self.member, self.team)
+ view = create_view(
+ membership, "+index", method='POST', form=form)
         with StormStatementRecorder() as recorder:
- view = create_view(membership, "+index", method='POST', form=form)
             view.processForm()
         self.assertEqual('', view.errormessage)
         self.assertEqual(TeamMembershipStatus.DEACTIVATED, membership.status)
- self...

Read more...

Revision history for this message
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

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/registry/browser/tests/test_teammembership.py'
--- lib/lp/registry/browser/tests/test_teammembership.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_teammembership.py 2011-01-11 20:48:40 +0000
@@ -0,0 +1,69 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from testtools.matchers import LessThan
7from zope.component import getUtility
8
9from canonical.testing.layers import DatabaseFunctionalLayer
10from lp.testing.matchers import HasQueryCount
11from lp.registry.interfaces.teammembership import (
12 ITeamMembershipSet,
13 TeamMembershipStatus,
14 )
15from lp.testing import (
16 login_celebrity,
17 StormStatementRecorder,
18 TestCaseWithFactory,
19 )
20from lp.testing.views import create_view
21
22
23class TestTeamMenu(TestCaseWithFactory):
24
25 layer = DatabaseFunctionalLayer
26
27 def setUp(self):
28 super(TestTeamMenu, self).setUp()
29 login_celebrity('admin')
30 self.membership_set = getUtility(ITeamMembershipSet)
31 self.team = self.factory.makeTeam()
32 self.member = self.factory.makeTeam()
33
34 def test_deactivate_member_query_count(self):
35 # Only these queries should be run, no matter what the
36 # membership tree looks like, although the number of queries
37 # could change slightly if a different user is logged in.
38 # 1. Check whether the user is the team owner.
39 # 2. Deactivate the membership in the TeamMembership table.
40 # 3. Delete from TeamParticipation table.
41 # (Queries #4, #5, #6, #7, and #10 are run because the storm
42 # objects have been invalidated.)
43 # 4. Get the TeamMembership entry.
44 # 5. Verify that the member exists in the db, but don't load
45 # the refresh the rest of its data, since we just need the id.
46 # 6. Verify that the user exists in the db.
47 # 7. Verify that the team exists in the db.
48 # 8. Insert into Job table.
49 # 9. Insert into PersonTransferJob table to schedule sending
50 # email. (This requires the data from queries #5, #6, and
51 # #7.)
52 # 10.Query the rest of the team data for the invalidated
53 # object in order to generate the canonical url.
54 self.team.addMember(
55 self.member, self.team.teamowner, force_team_add=True)
56 form = {
57 'editactive': 1,
58 'expires': 'never',
59 'deactivate': 'Deactivate',
60 }
61 membership = self.membership_set.getByPersonAndTeam(
62 self.member, self.team)
63 view = create_view(
64 membership, "+index", method='POST', form=form)
65 with StormStatementRecorder() as recorder:
66 view.processForm()
67 self.assertEqual('', view.errormessage)
68 self.assertEqual(TeamMembershipStatus.DEACTIVATED, membership.status)
69 self.assertThat(recorder, HasQueryCount(LessThan(11)))
070
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2010-10-24 12:46:23 +0000
+++ lib/lp/registry/doc/teammembership.txt 2011-01-11 20:48:40 +0000
@@ -131,6 +131,8 @@
131 ... ubuntu_team, TeamMembershipStatus.DEACTIVATED, t2.teamowner)131 ... ubuntu_team, TeamMembershipStatus.DEACTIVATED, t2.teamowner)
132 >>> ubuntu_team in t2.activemembers132 >>> ubuntu_team in t2.activemembers
133 False133 False
134 >>> [m.displayname for m in t2.allmembers]
135 [u'No Privileges Person']
134 >>> login(ANONYMOUS)136 >>> login(ANONYMOUS)
135137
136Another API for adding members is ITeam.addMember(), which ensures the given138Another API for adding members is ITeam.addMember(), which ensures the given
137139
=== modified file 'lib/lp/registry/interfaces/teammembership.py'
--- lib/lp/registry/interfaces/teammembership.py 2010-12-10 14:58:31 +0000
+++ lib/lp/registry/interfaces/teammembership.py 2011-01-11 20:48:40 +0000
@@ -116,7 +116,11 @@
116116
117117
118class ITeamMembership(Interface):118class ITeamMembership(Interface):
119 """TeamMembership for Users"""119 """TeamMembership for Users.
120
121 This table includes *direct* team members only. Indirect memberships are
122 handled by the TeamParticipation table.
123 """
120 export_as_webservice_entry()124 export_as_webservice_entry()
121125
122 id = Int(title=_('ID'), required=True, readonly=True)126 id = Int(title=_('ID'), required=True, readonly=True)
123127
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2010-12-10 14:58:31 +0000
+++ lib/lp/registry/model/teammembership.py 2011-01-11 20:48:40 +0000
@@ -21,7 +21,7 @@
21 ForeignKey,21 ForeignKey,
22 StringCol,22 StringCol,
23 )23 )
24from storm.locals import Store24from storm.store import Store
25from zope.component import getUtility25from zope.component import getUtility
26from zope.interface import implements26from zope.interface import implements
2727
@@ -498,167 +498,85 @@
498 person = ForeignKey(dbName='person', foreignKey='Person', notNull=True)498 person = ForeignKey(dbName='person', foreignKey='Person', notNull=True)
499499
500500
501def _cleanTeamParticipation(person, team):501def _cleanTeamParticipation(child, parent):
502 """Remove relevant entries in TeamParticipation for <person> and <team>.502 """Remove child from team and clean up child's subteams.
503503
504 Remove all tuples "person, team" from TeamParticipation for the given504 A participant of child is removed from parent's TeamParticipation
505 person and team (together with all its superteams), unless this person is505 entries if the only path from the participant to parent is via
506 an indirect member of the given team. More information on how to use the506 child.
507 TeamParticipation table can be found in the TeamParticipationUsage spec or507 """
508 the teammembership.txt system doctest.508 # Delete participation entries for the child and the child's
509 """509 # direct/indirect members in other ancestor teams, unless those
510 query = """510 # ancestor teams have another path the the child besides the
511 SELECT EXISTS(511 # membership that has just been deactivated.
512 SELECT 1 FROM TeamParticipation512 store = Store.of(parent)
513 WHERE person = %(person_id)s AND team IN (513 store.execute("""
514 SELECT person
515 FROM TeamParticipation JOIN Person ON
516 (person = Person.id)
517 WHERE team = %(team_id)s
518 AND person NOT IN (%(team_id)s, %(person_id)s)
519 AND teamowner IS NOT NULL
520 )
521 )
522 """ % dict(team_id=team.id, person_id=person.id)
523 store = Store.of(person)
524 (result, ) = store.execute(query).get_one()
525 if result:
526 # The person is a participant in this team by virtue of a membership
527 # in another one, so don't attempt to remove anything.
528 return
529
530 # First of all, we remove <person> from <team> (and its superteams).
531 _removeParticipantFromTeamAndSuperTeams(person, team)
532 if not person.is_team:
533 # Nothing else to do.
534 return
535
536 store = Store.of(person)
537
538 # Clean the participation of all our participant subteams, that are
539 # not a direct members of the target team.
540 query = """
541 -- All of my participant subteams...
542 SELECT person
543 FROM TeamParticipation JOIN Person ON (person = Person.id)
544 WHERE team = %(person_id)s AND person != %(person_id)s
545 AND teamowner IS NOT NULL
546 EXCEPT
547 -- that aren't a direct member of the team.
548 SELECT person
549 FROM TeamMembership
550 WHERE team = %(team_id)s AND status IN %(active_states)s
551 """ % dict(
552 person_id=person.id, team_id=team.id,
553 active_states=sqlvalues(ACTIVE_STATES)[0])
554
555 # Avoid circular import.
556 from lp.registry.model.person import Person
557 for subteam in store.find(Person, "id IN (%s)" % query):
558 _cleanTeamParticipation(subteam, team)
559
560 # Then clean-up all the non-team participants. We can remove those
561 # in a single query when the team graph is up to date.
562 _removeAllIndividualParticipantsFromTeamAndSuperTeams(person, team)
563
564
565def _removeParticipantFromTeamAndSuperTeams(person, team):
566 """Remove participation of person in team.
567
568 If <person> is a participant (that is, has a TeamParticipation entry)
569 of any team that is a subteam of <team>, then <person> should be kept as
570 a participant of <team> and (as a consequence) all its superteams.
571 Otherwise, <person> is removed from <team> and we repeat this process for
572 each superteam of <team>.
573 """
574 # Check if the person is a member of the given team through another team.
575 query = """
576 SELECT EXISTS(
577 SELECT 1
578 FROM TeamParticipation, TeamMembership
579 WHERE
580 TeamMembership.team = %(team_id)s AND
581 TeamMembership.person = TeamParticipation.team AND
582 TeamParticipation.person = %(person_id)s AND
583 TeamMembership.status IN %(active_states)s)
584 """ % dict(team_id=team.id, person_id=person.id,
585 active_states=sqlvalues(ACTIVE_STATES)[0])
586 store = Store.of(person)
587 (result, ) = store.execute(query).get_one()
588 if result:
589 # The person is a participant by virtue of a membership on another
590 # team, so don't remove.
591 return
592 store.find(TeamParticipation, (
593 (TeamParticipation.team == team) &
594 (TeamParticipation.person == person))).remove()
595
596 for superteam in _getSuperTeamsExcludingDirectMembership(person, team):
597 _removeParticipantFromTeamAndSuperTeams(person, superteam)
598
599
600def _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, target_team):
601 """Remove all non-team participants in <team> from <target_team>.
602
603 All the non-team participants of <team> are removed from <target_team>
604 and its super teams, unless they participate in <target_team> also from
605 one of its sub team.
606 """
607 query = """
608 DELETE FROM TeamParticipation514 DELETE FROM TeamParticipation
609 WHERE team = %(target_team_id)s AND person IN (515 USING (
610 -- All the individual participants.516 /* Get all the participation entries that might need to be
611 SELECT person517 * deleted, i.e. all the entries where the
612 FROM TeamParticipation JOIN Person ON (person = Person.id)518 * TeamParticipation.person is a participant of child, and
613 WHERE team = %(team_id)s AND teamowner IS NULL519 * where child participated in TeamParticipation.team until
614 EXCEPT520 * child was removed from parent.
615 -- people participating through a subteam of target_team;521 */
616 SELECT person522 SELECT person, team
617 FROM TeamParticipation523 FROM TeamParticipation
618 WHERE team IN (524 WHERE person IN (
619 -- The subteams of target_team.525 SELECT person
620 SELECT person526 FROM TeamParticipation
621 FROM TeamParticipation JOIN Person ON (person = Person.id)527 WHERE team = %(child)s
622 WHERE team = %(target_team_id)s528 )
623 AND person NOT IN (%(target_team_id)s, %(team_id)s)529 AND team IN (
624 AND teamowner IS NOT NULL530 SELECT team
625 )531 FROM TeamParticipation
626 -- or people directly a member of the target team.532 WHERE person = %(child)s
627 EXCEPT533 AND team != %(child)s
628 SELECT person534 )
629 FROM TeamMembership535
630 WHERE team = %(target_team_id)s AND status IN %(active_states)s536
631 )537 EXCEPT (
632 """ % dict(538
633 team_id=team.id, target_team_id=target_team.id,539 /* Compute the TeamParticipation entries that we need to
634 active_states=sqlvalues(ACTIVE_STATES)[0])540 * keep by walking the tree in the TeamMembership table.
635 store = Store.of(team)541 */
636 store.execute(query)542 WITH RECURSIVE parent(person, team) AS (
637543 /* Start by getting all the ancestors of the child
638 super_teams = _getSuperTeamsExcludingDirectMembership(team, target_team)544 * from the TeamParticipation table, then get those
639 for superteam in super_teams:545 * ancestors' direct members to recurse through the
640 _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, superteam)546 * tree from the top.
641547 */
642548 SELECT ancestor.person, ancestor.team
643def _getSuperTeamsExcludingDirectMembership(person, team):549 FROM TeamMembership ancestor
644 """Return all the super teams of <team> where person isn't a member."""550 WHERE ancestor.status IN %(active_states)s
645 query = """551 AND ancestor.team IN (
646 -- All the super teams...552 SELECT team
647 SELECT team553 FROM TeamParticipation
648 FROM TeamParticipation554 WHERE person = %(child)s
649 WHERE person = %(team_id)s AND team != %(team_id)s555 )
650 EXCEPT556
651 -- The one where person has an active membership.557 UNION
652 SELECT team558
653 FROM TeamMembership559 /* Find the next level of direct members, but hold
654 WHERE person = %(person_id)s AND status IN %(active_states)s560 * onto the parent.team, since we want the top and
655 """ % dict(561 * bottom of the hierarchy to calculate the
656 person_id=person.id, team_id=team.id,562 * TeamParticipation. The query above makes sure
657 active_states=sqlvalues(ACTIVE_STATES)[0])563 * that we do this for all the ancestors.
658564 */
659 # Avoid circular import.565 SELECT child.person, parent.team
660 from lp.registry.model.person import Person566 FROM TeamMembership child
661 return Store.of(person).find(Person, "id IN (%s)" % query)567 JOIN parent ON child.team = parent.person
568 WHERE child.status IN %(active_states)s
569 )
570 SELECT person, team
571 FROM parent
572 )
573 ) AS keeping
574 WHERE TeamParticipation.person = keeping.person
575 AND TeamParticipation.team = keeping.team
576 """ % sqlvalues(
577 child=child.id,
578 active_states=ACTIVE_STATES))
579 store.invalidate()
662580
663581
664def _fillTeamParticipation(member, accepting_team):582def _fillTeamParticipation(member, accepting_team):
665583
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2010-12-02 16:13:51 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2011-01-11 20:48:40 +0000
@@ -9,7 +9,10 @@
9 )9 )
10import re10import re
11import subprocess11import subprocess
12import unittest12from unittest import (
13 TestCase,
14 TestLoader,
15 )
1316
14import pytz17import pytz
15from zope.component import getUtility18from zope.component import getUtility
@@ -24,6 +27,7 @@
24 login,27 login,
25 login_person,28 login_person,
26 )29 )
30from canonical.launchpad.interfaces.lpstorm import IStore
27from canonical.launchpad.testing.systemdocs import (31from canonical.launchpad.testing.systemdocs import (
28 default_optionflags,32 default_optionflags,
29 LayeredDocFileSuite,33 LayeredDocFileSuite,
@@ -40,11 +44,14 @@
40 ITeamMembershipSet,44 ITeamMembershipSet,
41 TeamMembershipStatus,45 TeamMembershipStatus,
42 )46 )
43from lp.registry.model.teammembership import TeamMembership47from lp.registry.model.teammembership import (
44from lp.testing.factory import LaunchpadObjectFactory48 TeamMembership,
4549 TeamParticipation,
4650 )
47class TestTeamMembershipSet(unittest.TestCase):51from lp.testing import TestCaseWithFactory
52
53
54class TestTeamMembershipSet(TestCase):
48 layer = DatabaseFunctionalLayer55 layer = DatabaseFunctionalLayer
4956
50 def setUp(self):57 def setUp(self):
@@ -155,11 +162,12 @@
155 self.failIf(sample_person.inTeam(motu))162 self.failIf(sample_person.inTeam(motu))
156163
157164
158class TeamParticipationTestCase(unittest.TestCase):165class TeamParticipationTestCase(TestCaseWithFactory):
159 """Tests for team participation using 5 teams."""166 """Tests for team participation using 5 teams."""
160 layer = DatabaseFunctionalLayer167 layer = DatabaseFunctionalLayer
161168
162 def setUp(self):169 def setUp(self):
170 super(TeamParticipationTestCase, self).setUp()
163 login('foo.bar@canonical.com')171 login('foo.bar@canonical.com')
164 person_set = getUtility(IPersonSet)172 person_set = getUtility(IPersonSet)
165 self.foo_bar = person_set.getByEmail('foo.bar@canonical.com')173 self.foo_bar = person_set.getByEmail('foo.bar@canonical.com')
@@ -177,6 +185,9 @@
177 sorted(participant_names),185 sorted(participant_names),
178 sorted([participant.name for participant in team.allmembers]))186 sorted([participant.name for participant in team.allmembers]))
179187
188 def getTeamParticipationCount(self):
189 return IStore(TeamParticipation).find(TeamParticipation).count()
190
180191
181class TestTeamParticipationHierarchy(TeamParticipationTestCase):192class TestTeamParticipationHierarchy(TeamParticipationTestCase):
182 """Participation management tests using 5 nested teams.193 """Participation management tests using 5 nested teams.
@@ -221,6 +232,7 @@
221232
222 This is similar to what was experienced in bug 261915.233 This is similar to what was experienced in bug 261915.
223 """234 """
235 previous_count = self.getTeamParticipationCount()
224 self.team2.setMembershipData(236 self.team2.setMembershipData(
225 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)237 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
226 self.assertParticipantsEquals(['name16', 'team2'], self.team1)238 self.assertParticipantsEquals(['name16', 'team2'], self.team1)
@@ -230,11 +242,15 @@
230 self.assertParticipantsEquals(242 self.assertParticipantsEquals(
231 ['name16', 'no-priv', 'team5'], self.team4)243 ['name16', 'no-priv', 'team5'], self.team4)
232 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)244 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
245 self.assertEqual(
246 previous_count-8,
247 self.getTeamParticipationCount())
233248
234 def testRemovingLeafTeam(self):249 def testRemovingLeafTeam(self):
235 """Make sure that participations are updated correctly when removing250 """Make sure that participations are updated correctly when removing
236 the leaf team.251 the leaf team.
237 """252 """
253 previous_count = self.getTeamParticipationCount()
238 self.team4.setMembershipData(254 self.team4.setMembershipData(
239 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)255 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
240 self.assertParticipantsEquals(256 self.assertParticipantsEquals(
@@ -244,6 +260,9 @@
244 self.assertParticipantsEquals(['name16', 'team4'], self.team3)260 self.assertParticipantsEquals(['name16', 'team4'], self.team3)
245 self.assertParticipantsEquals(['name16'], self.team4)261 self.assertParticipantsEquals(['name16'], self.team4)
246 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)262 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
263 self.assertEqual(
264 previous_count-8,
265 self.getTeamParticipationCount())
247266
248267
249class TestTeamParticipationTree(TeamParticipationTestCase):268class TestTeamParticipationTree(TeamParticipationTestCase):
@@ -252,9 +271,10 @@
252 Create a team hierarchy looking like this:271 Create a team hierarchy looking like this:
253 team1272 team1
254 team2273 team2
255 team5 team3274 team5
256 team4275 team3
257 team5276 team4
277 team5
258 no-priv278 no-priv
259 """279 """
260 layer = DatabaseFunctionalLayer280 layer = DatabaseFunctionalLayer
@@ -269,6 +289,10 @@
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)
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)
271291
292 def tearDown(self):
293 super(TestTeamParticipationTree, self).tearDown()
294 self.layer.force_dirty_database()
295
272 def testTeamParticipationSetUp(self):296 def testTeamParticipationSetUp(self):
273 """Make sure that the TeamParticipation are sane after setUp."""297 """Make sure that the TeamParticipation are sane after setUp."""
274 self.assertParticipantsEquals(298 self.assertParticipantsEquals(
@@ -284,6 +308,7 @@
284 ['name16', 'no-priv'], self.team5)308 ['name16', 'no-priv'], self.team5)
285309
286 def testRemoveTeam3FromTeam2(self):310 def testRemoveTeam3FromTeam2(self):
311 previous_count = self.getTeamParticipationCount()
287 self.team2.setMembershipData(312 self.team2.setMembershipData(
288 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)313 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
289 self.assertParticipantsEquals(314 self.assertParticipantsEquals(
@@ -295,8 +320,12 @@
295 self.assertParticipantsEquals(320 self.assertParticipantsEquals(
296 ['name16', 'no-priv', 'team5'], self.team4)321 ['name16', 'no-priv', 'team5'], self.team4)
297 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)322 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
323 self.assertEqual(
324 previous_count-4,
325 self.getTeamParticipationCount())
298326
299 def testRemoveTeam5FromTeam4(self):327 def testRemoveTeam5FromTeam4(self):
328 previous_count = self.getTeamParticipationCount()
300 self.team4.setMembershipData(329 self.team4.setMembershipData(
301 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)330 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
302 self.assertParticipantsEquals(331 self.assertParticipantsEquals(
@@ -308,6 +337,40 @@
308 ['name16', 'team4'], self.team3)337 ['name16', 'team4'], self.team3)
309 self.assertParticipantsEquals(['name16'], self.team4)338 self.assertParticipantsEquals(['name16'], self.team4)
310 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)339 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
340 self.assertEqual(
341 previous_count-4,
342 self.getTeamParticipationCount())
343
344
345class TestParticipationCleanup(TeamParticipationTestCase):
346 """Test deletion of a member from a team with many superteams.
347 Create a team hierarchy looking like this:
348 team1
349 team2
350 team3
351 team4
352 team5
353 no-priv
354 """
355
356 def setUp(self):
357 """Setup the team hierarchy."""
358 super(TestParticipationCleanup, self).setUp()
359 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
360 self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
361 self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
362 self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
363 self.team5.addMember(self.no_priv, self.foo_bar)
364
365 def testMemberRemoval(self):
366 """Remove the member from the last team.
367
368 The number of db queries should be constant not O(depth).
369 """
370 self.assertStatementCount(
371 7,
372 self.team5.setMembershipData, self.no_priv,
373 TeamMembershipStatus.DEACTIVATED, self.team5.teamowner)
311374
312375
313class TestTeamParticipationMesh(TeamParticipationTestCase):376class TestTeamParticipationMesh(TeamParticipationTestCase):
@@ -315,12 +378,15 @@
315 branches.378 branches.
316379
317 Create a team hierarchy looking like this:380 Create a team hierarchy looking like this:
318 team1 /--team6381 team1 team6
319 team2 \382 \ / |
320 | team3 |383 team2 |
321 \--- team4-/384 / | |
322 team5385 team3 | |
323 no-priv386 \ | /
387 team4
388 team5
389 no-priv
324 """390 """
325 layer = DatabaseFunctionalLayer391 layer = DatabaseFunctionalLayer
326392
@@ -338,6 +404,10 @@
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)
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)
340406
407 def tearDown(self):
408 super(TestTeamParticipationMesh, self).tearDown()
409 self.layer.force_dirty_database()
410
341 def testTeamParticipationSetUp(self):411 def testTeamParticipationSetUp(self):
342 """Make sure that the TeamParticipation are sane after setUp."""412 """Make sure that the TeamParticipation are sane after setUp."""
343 self.assertParticipantsEquals(413 self.assertParticipantsEquals(
@@ -355,6 +425,7 @@
355 self.team6)425 self.team6)
356426
357 def testRemoveTeam3FromTeam2(self):427 def testRemoveTeam3FromTeam2(self):
428 previous_count = self.getTeamParticipationCount()
358 self.team2.setMembershipData(429 self.team2.setMembershipData(
359 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)430 self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
360 self.assertParticipantsEquals(431 self.assertParticipantsEquals(
@@ -368,8 +439,12 @@
368 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)439 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
369 self.assertParticipantsEquals(440 self.assertParticipantsEquals(
370 ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)441 ['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)
442 self.assertEqual(
443 previous_count-3,
444 self.getTeamParticipationCount())
371445
372 def testRemoveTeam5FromTeam4(self):446 def testRemoveTeam5FromTeam4(self):
447 previous_count = self.getTeamParticipationCount()
373 self.team4.setMembershipData(448 self.team4.setMembershipData(
374 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)449 self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
375 self.assertParticipantsEquals(450 self.assertParticipantsEquals(
@@ -381,9 +456,12 @@
381 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)456 self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
382 self.assertParticipantsEquals(457 self.assertParticipantsEquals(
383 ['name16', 'team2', 'team3', 'team4'], self.team6)458 ['name16', 'team2', 'team3', 'team4'], self.team6)
384459 self.assertEqual(
385460 previous_count-10,
386class TestTeamMembership(unittest.TestCase):461 self.getTeamParticipationCount())
462
463
464class TestTeamMembership(TestCaseWithFactory):
387 layer = DatabaseFunctionalLayer465 layer = DatabaseFunctionalLayer
388466
389 def test_teams_not_kicked_from_themselves_bug_248498(self):467 def test_teams_not_kicked_from_themselves_bug_248498(self):
@@ -400,12 +478,11 @@
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.
401 """479 """
402 login('test@canonical.com')480 login('test@canonical.com')
403 factory = LaunchpadObjectFactory()481 person = self.factory.makePerson()
404 person = factory.makePerson()
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.
406 teamA = factory.makeTeam(483 teamA = self.factory.makeTeam(
407 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)484 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
408 teamB = factory.makeTeam(485 teamB = self.factory.makeTeam(
409 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)486 person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
410 self.failUnless(487 self.failUnless(
411 teamA.inTeam(teamA), "teamA is not a participant of itself")488 teamA.inTeam(teamA), "teamA is not a participant of itself")
@@ -444,21 +521,21 @@
444 self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value)521 self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value)
445522
446523
447class TestTeamMembershipSetStatus(unittest.TestCase):524class TestTeamMembershipSetStatus(TestCaseWithFactory):
448 """Test the behaviour of TeamMembership's setStatus()."""525 """Test the behaviour of TeamMembership's setStatus()."""
449 layer = DatabaseFunctionalLayer526 layer = DatabaseFunctionalLayer
450527
451 def setUp(self):528 def setUp(self):
529 super(TestTeamMembershipSetStatus, self).setUp()
452 login('foo.bar@canonical.com')530 login('foo.bar@canonical.com')
453 self.foobar = getUtility(IPersonSet).getByName('name16')531 self.foobar = getUtility(IPersonSet).getByName('name16')
454 self.no_priv = getUtility(IPersonSet).getByName('no-priv')532 self.no_priv = getUtility(IPersonSet).getByName('no-priv')
455 self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')533 self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
456 self.admins = getUtility(IPersonSet).getByName('admins')534 self.admins = getUtility(IPersonSet).getByName('admins')
457 # Create a bunch of arbitrary teams to use in the tests.535 # Create a bunch of arbitrary teams to use in the tests.
458 factory = LaunchpadObjectFactory()536 self.team1 = self.factory.makeTeam(self.foobar)
459 self.team1 = factory.makeTeam(self.foobar)537 self.team2 = self.factory.makeTeam(self.foobar)
460 self.team2 = factory.makeTeam(self.foobar)538 self.team3 = self.factory.makeTeam(self.foobar)
461 self.team3 = factory.makeTeam(self.foobar)
462539
463 def test_proponent_is_stored(self):540 def test_proponent_is_stored(self):
464 for status in [TeamMembershipStatus.DEACTIVATED,541 for status in [TeamMembershipStatus.DEACTIVATED,
@@ -698,7 +775,7 @@
698 self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)775 self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)
699776
700777
701class TestCheckTeamParticipationScript(unittest.TestCase):778class TestCheckTeamParticipationScript(TestCase):
702 layer = DatabaseFunctionalLayer779 layer = DatabaseFunctionalLayer
703780
704 def _runScript(self, expected_returncode=0):781 def _runScript(self, expected_returncode=0):
@@ -803,7 +880,7 @@
803880
804881
805def test_suite():882def test_suite():
806 suite = unittest.TestLoader().loadTestsFromName(__name__)883 suite = TestLoader().loadTestsFromName(__name__)
807 bug_249185 = LayeredDocFileSuite(884 bug_249185 = LayeredDocFileSuite(
808 'bug-249185.txt', optionflags=default_optionflags,885 'bug-249185.txt', optionflags=default_optionflags,
809 layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown)886 layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown)