Merge lp:~jcsackett/launchpad/plus-participation-additional-fixes into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: 11408
Proposed branch: lp:~jcsackett/launchpad/plus-participation-additional-fixes
Merge into: lp:launchpad
Diff against target: 656 lines (+238/-87)
13 files modified
lib/canonical/launchpad/doc/sample-data-assertions.txt (+2/-2)
lib/lp/bugs/doc/initial-bug-contacts.txt (+1/-1)
lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt (+1/-1)
lib/lp/registry/browser/person.py (+34/-21)
lib/lp/registry/browser/tests/test_person_view.py (+2/-2)
lib/lp/registry/doc/person-account.txt (+2/-2)
lib/lp/registry/doc/private-team-visibility.txt (+1/-1)
lib/lp/registry/doc/teammembership.txt (+5/-5)
lib/lp/registry/doc/vocabularies.txt (+1/-1)
lib/lp/registry/interfaces/person.py (+7/-7)
lib/lp/registry/model/person.py (+77/-15)
lib/lp/registry/tests/test_person.py (+95/-7)
lib/lp/registry/vocabularies.py (+10/-22)
To merge this branch: bzr merge lp:~jcsackett/launchpad/plus-participation-additional-fixes
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) code Approve
Edwin Grubbs (community) code Approve
Māris Fogels (community) Needs Information
Review via email: mp+32820@code.launchpad.net

Commit message

Attacks the +participation problem by stormifying the team_memberships method (previously myactivememberships) and using a graph traversal to determine team paths rather than multiple look ups using findPathToTeam.

Description of the change

Summary

Contributes to fixing the +participation time outs by converting team path look ups into one large SQL lookup followed by some graph traversal.

Previously the mp showed an implementation that largely cut-off the team_path lookup; the changes for it are still useful, so have not been removed from the branch, but the solution changed in the last few revisions.

Proposed fix

Stormifying where possible to assist db load; more importantly, using a graph of all teams the person participates in to create the team_paths for indirect teams as displayed.

Pre-implementation notes

Chatted with Curtis Hovey (sinzui)

Got schooled by Robert Collins (lifeless) in the ways of the graph, and agreed it was a much better solution.

Implementation details

Renames myactivememberships to team_memberships, per an XXX from kiko in the codebase. Updates call sites.

Stormifies team_memberships and updates callsites.

Uses a graph traversal in getPathsToTeams to find the path between the person and all teams, then filters out direct memberships from that to get indirect teams and their team_paths.

Demo and Q/A
bin/test -vvc -t TestPersonTeams
bin/test -vvc -t TestPersonParticipationView

On launchpad.dev, checkout https://launchpad.dev/~mark/+participation. The via column should show the team_path for indirect teams. I haven't found anyone with longer team_paths than one indirection in the sample data, but if you know of one you should see the full team_path.

lint
= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt
  lib/lp/registry/vocabularies.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/browser/tests/test_person_view.py
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_person.py

./lib/lp/registry/vocabularies.py
     180: E302 expected 2 blank lines, found 1
     230: E302 expected 2 blank lines, found 1
     625: E302 expected 2 blank lines, found 1
    1495: E301 expected 1 blank line, found 0
./lib/lp/registry/interfaces/person.py
     448: E302 expected 2 blank lines, found 1

The "expected n blank lines" were explained to me as a factor of lint having issues around comments and class definitions.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Could you explain why the indirect team path calculations were a problem?

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

The findPathToTeam calculations were a problem b/c in the cases causing the timeout, the query gets called hundreds of times, leading to the largest set of time in the process. Because of the iteration through an unknown series of team relations, there's no easy way to predict how this will work.

As an example, in https://lp-oops.canonical.com/oops.py/?oopsid=OOPS-1688ED2377 you can see the query used in findPathToTeam being called 261 times, for a total time of 4610. The other +participation OOPS reports show the same thing.

Revision history for this message
Robert Collins (lifeless) wrote :

On Wed, Aug 18, 2010 at 1:01 AM, j.c.sackett
<email address hidden> wrote:
> The findPathToTeam calculations were a problem b/c in the cases causing the timeout, the query gets called hundreds of times, leading to the largest set of time in the process. Because of the iteration through an unknown series of team relations, there's no easy way to predict how this will work.
>
> As an example, in https://lp-oops.canonical.com/oops.py/?oopsid=OOPS-1688ED2377 you can see the query used in findPathToTeam being called 261 times, for a total time of 4610. The other +participation OOPS reports show the same thing.

Ok, so theres basic reason for it to be slow.

Have you considered this algorithm (psuedo code):
# 1 query, we want all the teams he participates in
# (that is, all rows that have the member == user, and the team == anything
# should return a list of tuples [(user, t1), (user, t2)] or something like that
memberships = list(get_memberships(person==user))
teams = [team for _, team in memberships]
# Get all the memberships of those teams themselves; because memberships
# are transitive, this will be a subset of the first result, and lets us infer
# a valid graph cheaply (there can be multiple paths to a team, we only
# need to show one)
team_temberships = get_memberships(person in teams)
graph_edges = list(team_memberships)
graph = dict(graph_edges)
graph.update(memberships)
# now render
...

Revision history for this message
Robert Collins (lifeless) wrote :

Bah, I got the direct and indirect person memberships the wrong way around.

Have you considered this algorithm (psuedo code):
# 1 query, we want all the teams he participates via the expanded memo:
# (that is, all rows that have the member == user, and the team == anything
# that is directly or indirectly in. This should return a list of tuples
# [(user, t1), (user, t2)] or something like that
memberships = list(get_expanded_memberships(Class.person==user))
known_teams = [team for _, team in memberships]
known_members = known_teams + [user]
# Get all the memberships of those teams themselves; because memberships
# are transitive, we can query for the entire graph directly.
# Note that here we query direct memberships only
team_temberships = get_direct_memberships(Class.person in known_members)
graph = dict(team_memberships)
# now render
...

Revision history for this message
Māris Fogels (mars) wrote :

Hi Jonathan,

I reviewed the branch and both the test and implementation of the changes look good. However, I did come upon one large question: did the IPerson.findPathToTeam() interface definition change with this patch, or does this patch only change the Person class itself? If so, then the IPerson interface class will have to be updated. If you want to verify this, I suggest writing a test in test_person.py that calls verifyObject() on a Person instance - that will immediately reveal any interface errors.

Besides that, I only found minor points in the diff:

 • Does the new 'via' argument to _asParticipation() need a epydoc stanza in the method's docstring? Otherwise the argument type is not obvious.

 • Should there be a comment on the line 53 of the diff, before calling findPathToTeam(limit=1), to make the purpose of the limit argument clear? Without that 'limit=1' appears to be a magic number.

 • It looks like the 'from storm.expr' import in model/person.py was ordered alphabetically, but Desc isn't in order.

 • You probably want to use an epydoc :param: stanza for the new limit argument in the IPerson.findPathToTeam() docstring.

I am marking this as Needs Information for now. When the IPerson interface questions are answered then we can approve this to land.

Maris

review: Needs Information
Revision history for this message
Māris Fogels (mars) wrote :

Looking at the test commands, I see that only the TestPersonTeams and TestPersonParticipationView cases are run. It is possible that 'bin/test -t test_person' will have a test that calls verifyObject().

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

Maris--

You were right, I had borked the interface. I've since updated the interface to show the new parameters.

Per suggestion from Robert, a lot has changed in this code; I'm unsure if you want to continue this MP or kill it and have me submit another. All the code reviewed is still there, as the changes were still good ones, but many of the changes are no longer taken advantage of by person/+participation, in favor of Robert's suggestion.

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

Robert--

I've pushed up changes that I believe implement your idea. Rather than picking the shortest path though, it uses the oldest path to maintain consistency with the data previously presented (save of course those occasions when the shortest path is a direct_membership).

Take a look and let me know what you think.

Revision history for this message
Robert Collins (lifeless) wrote :

129 """Validate the person is a real person with no other restrictions."""
130 +
131 def validate(person):

That new vertical whitespace is non-PEP8 - this is a curried function, so its appropriate to have it adjacent and not break the eye's flow reading past it - please don't do that. If your editor is telling you to do it, give it a good spanking.

You also have some list reflows that make things less readable - our style guide encourages readability over sheer compactness; I'm on the fence myself, but wanted you to be aware that you have discretion there - what the code had was fine, with the lines and indentation clearly grouping the query clauses.

The implementation of a 2-query look up looks pretty nice, I'm glad it turned out well. I can't really judge the ui result trivially, perhaps a screenshot of a complexish situation?

I haven't done a full code review, I think you should carry on with Maris or whomever is reviewer-de-jour.

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

Robert--

Thanks, I'll look into the reflows and that line break.

I agree on pursuing the review with Maris or whomever; I was interested in your view of my implementation of the two query graph thing. Really great idea, btw.

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (14.6 KiB)

Hi JC,

The main thing I learned from reviewing this branch is that we should
not give new employees programming tasks concerning graph traversal
unless we want to scare them away. I think that is the most confusing
code I've ever seen in the registry.

I am impressed with all that you have accomplished in this branch. Most
of my comments are related to formatting, which is to be expected with
new employees. I felt your pain.

You really need to run "make lint" against the branch, especially
test_person.py.

-Edwin

>=== modified file 'lib/lp/registry/browser/person.py'
>--- lib/lp/registry/browser/person.py 2010-08-18 03:12:35 +0000
>+++ lib/lp/registry/browser/person.py 2010-08-18 21:23:34 +0000
>@@ -3078,31 +3078,34 @@
> def label(self):
> return 'Team participation for ' + self.context.displayname
>
>- def _asParticipation(self, membership=None, team=None, team_path=None):
>+ def _asParticipation(self, membership=None, team=None, via=None):
> """Return a dict of participation information for the membership.
>
> Method requires membership or team, not both.
>+ :param via: The team through which the membership in the indirect
>+ is established.

"in the indirect" seems to be missing a noun.

> """
> if ((membership is None and team is None) or
> (membership is not None and team is not None)):
> raise AssertionError(
> "The membership or team argument must be provided, not both.")
>
>- if team_path is None:
>- via = None
>- else:
>- via = COMMASPACE.join(team.displayname for team in team_path)
>+ if via is not None:
>+ # When showing the path, it's unnecessary to show the team in
>+ # question at the beginning of the path, or the user at the
>+ # end of the path.
>+ via = COMMASPACE.join(
>+ [via_team.displayname for via_team in via[1:-1]])
>
> if membership is None:
>- #we have membership via an indirect team, and can use
>- #sane defaults
>+ # Membership is via an indirect team; sane defaults exist.
>
>- #the user can only be a member of this team
>+ # The user can only be a member of this team.

The space between these two comments makes it look like a line of
code disappeared. "sane defaults exist" kinda confuses me. I see what
you are going for with a comment for the block and a separate comment
for the first statement. It also took a second to understand what "can
only be a member" meant. Maybe, the two comments can be combined as:
  # An indirect member cannot be the Owner or Admin of the team.

> role = 'Member'
>- #the user never joined, and can't have a join date
>+ # The user never joined, and can't have a join date.

"user" almost always refers to the logged-in user, so it is better
to say "person" if the function is not dealing with the logged-in user.

> datejoined = None
> else:
>- #the member is a direct member, so we can use membership data
>+ # The member is a direct memb...

review: Needs Fixing
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (15.6 KiB)

Edwin--

Thanks for all the comments; I'll make those changes and push them up soon.

On Aug 18, 2010, at 9:48 PM, Edwin Grubbs wrote:

> Review: Needs Fixing
> Hi JC,
>
> The main thing I learned from reviewing this branch is that we should
> not give new employees programming tasks concerning graph traversal
> unless we want to scare them away. I think that is the most confusing
> code I've ever seen in the registry.
>
> I am impressed with all that you have accomplished in this branch. Most
> of my comments are related to formatting, which is to be expected with
> new employees. I felt your pain.
>
> You really need to run "make lint" against the branch, especially
> test_person.py.
>
>
> -Edwin
>
>
>> === modified file 'lib/lp/registry/browser/person.py'
>> --- lib/lp/registry/browser/person.py 2010-08-18 03:12:35 +0000
>> +++ lib/lp/registry/browser/person.py 2010-08-18 21:23:34 +0000
>> @@ -3078,31 +3078,34 @@
>> def label(self):
>> return 'Team participation for ' + self.context.displayname
>>
>> - def _asParticipation(self, membership=None, team=None, team_path=None):
>> + def _asParticipation(self, membership=None, team=None, via=None):
>> """Return a dict of participation information for the membership.
>>
>> Method requires membership or team, not both.
>> + :param via: The team through which the membership in the indirect
>> + is established.
>
>
> "in the indirect" seems to be missing a noun.
>
>
>> """
>> if ((membership is None and team is None) or
>> (membership is not None and team is not None)):
>> raise AssertionError(
>> "The membership or team argument must be provided, not both.")
>>
>> - if team_path is None:
>> - via = None
>> - else:
>> - via = COMMASPACE.join(team.displayname for team in team_path)
>> + if via is not None:
>> + # When showing the path, it's unnecessary to show the team in
>> + # question at the beginning of the path, or the user at the
>> + # end of the path.
>> + via = COMMASPACE.join(
>> + [via_team.displayname for via_team in via[1:-1]])
>>
>> if membership is None:
>> - #we have membership via an indirect team, and can use
>> - #sane defaults
>> + # Membership is via an indirect team; sane defaults exist.
>>
>> - #the user can only be a member of this team
>> + # The user can only be a member of this team.
>
>
>
> The space between these two comments makes it look like a line of
> code disappeared. "sane defaults exist" kinda confuses me. I see what
> you are going for with a comment for the block and a separate comment
> for the first statement. It also took a second to understand what "can
> only be a member" meant. Maybe, the two comments can be combined as:
> # An indirect member cannot be the Owner or Admin of the team.
>
>
>> role = 'Member'
>> - #the user never joined, and can't have a join date
>> + # The user never joined, and can't have a join date.
>
>
> "user" almost always ...

Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (6.0 KiB)

More detailed reply with updated code...

> I am impressed with all that you have accomplished in this branch. Most
> of my comments are related to formatting, which is to be expected with
> new employees. I felt your pain.

Actually, once I ran through some experiments in a terminal, it wasn't *that* painful. But thanks. :-)

> You really need to run "make lint" against the branch, especially
> test_person.py.

I thought I had, but I think I ran make lint before committing some files, and so only got the issues with those files.

> The style guide doesn't explicitely mention this, but I think the
> formatting choices should be similar to a function call. Each line of
> the list comprehension should line up so they are easier to read:
> Either:
> result = [foo
> bar
> baz]
> Or:
> result = [
> foo
> bar
> baz]
>
> For actual lists, the ending square bracket has to be on a line by
> itself. There isn't an exact rule for list comprehensions, but
> pocketlint will complain if you put the square bracket on a line by
> itself without a comma on the previous line, and of course, that only
> works for lists.

I've broken up the list comprehension as

indirect_teams = [
            team for team in paths.keys() if
            team not in direct_teams]

It seems clean to break the conditional filter onto its own line.

> I don't see the limit being used anywhere? Is that feature still needed?
>
> If it's needed the docstring should describe what limit is. Look in other
> docstrings, which use the convention:
> :param PARAMNAME: DESCRIPTION

The limit isn't being used anywhere, but I had left it in b/c I could see use of this method
being a problem for someone else down the road. However, I can see it's presence now
is just confusing, and it's a sufficiently obvious solution that if it's actually needed down
the road it can be redone then.

>
>
>> # This is our guarantee that _getDirectMemberIParticipateIn() will
>> # never return None
>> assert self.hasParticipationEntryFor(team), (
>> @@ -1687,19 +1699,16 @@
>> self.invited_members,
>> orderBy=self._sortingColumnsForSetOperations)
>>
>> - # XXX: kiko 2005-10-07:
>> - # myactivememberships should be renamed to team_memberships and be
>> - # described as the set of memberships for the object's teams.
>> @property
>> - def myactivememberships(self):
>> + def team_memberships(self):
>> """See `IPerson`."""
>> - return TeamMembership.select("""
>> - TeamMembership.person = %s AND status in %s AND
>> - Person.id = TeamMembership.team
>> - """ % sqlvalues(self.id, [TeamMembershipStatus.APPROVED,
>> - TeamMembershipStatus.ADMIN]),
>> - clauseTables=['Person'],
>> - orderBy=Person.sortingColumns)
>> + Team = ClassAlias(Person, "Team")
>> + store = Store.of(self)
>> + return store.find(TeamMembership,
>> + And(TeamMembership.personID == self.id,
>> + Team.id == TeamMembership.teamID,
>> + TeamMembership.status.is_in(
>> + ...

Read more...

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (9.4 KiB)

Hi JC,

I just have a few comments more before it's ready to land.

-Edwin

> > The style guide doesn't explicitely mention this, but I think the
> > formatting choices should be similar to a function call. Each line of
> > the list comprehension should line up so they are easier to read:
> > Either:
> > result = [foo
> > bar
> > baz]
> > Or:
> > result = [
> > foo
> > bar
> > baz]
> >
> > For actual lists, the ending square bracket has to be on a line by
> > itself. There isn't an exact rule for list comprehensions, but
> > pocketlint will complain if you put the square bracket on a line by
> > itself without a comma on the previous line, and of course, that only
> > works for lists.
>
> I've broken up the list comprehension as
>
> indirect_teams = [
> team for team in paths.keys() if
> team not in direct_teams]
>
> It seems clean to break the conditional filter onto its own line.

When scanning code, a person often doesn't notice the very end of the line. Here is an extreme example to illustrate my point:
   result = [
       some_big_complicated_function(a) for a in alist if
       a in blist]
The user might think you are iterating over blist instead of alist. While your list comprehension isn't nearly as hazardous, I think putting the "if" at the start of the line will help readers parse it quickly.

> > I don't see the limit being used anywhere? Is that feature still needed?
> >
> > If it's needed the docstring should describe what limit is. Look in other
> > docstrings, which use the convention:
> > :param PARAMNAME: DESCRIPTION
>
> The limit isn't being used anywhere, but I had left it in b/c I could see use
> of this method
> being a problem for someone else down the road. However, I can see it's
> presence now
> is just confusing, and it's a sufficiently obvious solution that if it's
> actually needed down
> the road it can be redone then.
>
>
> >
> >
> >> # This is our guarantee that _getDirectMemberIParticipateIn() will
> >> # never return None
> >> assert self.hasParticipationEntryFor(team), (
> >> @@ -1687,19 +1699,16 @@
> >> self.invited_members,
> >> orderBy=self._sortingColumnsForSetOperations)
> >>
> >> - # XXX: kiko 2005-10-07:
> >> - # myactivememberships should be renamed to team_memberships and be
> >> - # described as the set of memberships for the object's teams.
> >> @property
> >> - def myactivememberships(self):
> >> + def team_memberships(self):
> >> """See `IPerson`."""
> >> - return TeamMembership.select("""
> >> - TeamMembership.person = %s AND status in %s AND
> >> - Person.id = TeamMembership.team
> >> - """ % sqlvalues(self.id, [TeamMembershipStatus.APPROVED,
> >> - TeamMembershipStatus.ADMIN]),
> >> - clauseTables=['Person'],
> >> - orderBy=Person.sortingColumns)
> >> + Team = ClassAlias(Person, "Team")
> >> + store = Store.of(self)
> >> + return store.find(TeamMembership,
> >> + And(TeamMembership.personID == self.id,
> >> + ...

Read more...

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (4.2 KiB)

Hi JC,

This looks good. BTW, it is customary to add a comment with the incremental diff of changes. This way the reviewer gets all the changes they need to look at in an email. I've included them below in case anybody else is following the progress on this.

-Edwin

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-08-19 14:52:19 +0000
+++ lib/lp/registry/browser/person.py 2010-08-19 17:39:30 +0000
@@ -3101,7 +3101,7 @@
             # Membership is via an indirect team so sane defaults exist.
             # An indirect member cannot be an Owner or Admin of a team.
             role = 'Member'
- # The Person joined, and can't have a join date.
+ # The Person never joined, and can't have a join date.
             datejoined = None
         else:
             # The member is a direct member; use the membership data.
@@ -3133,8 +3133,8 @@
         paths, memberships = self.context.getPathsToTeams()
         direct_teams = [membership.team for membership in memberships]
         indirect_teams = [
- team for team in paths.keys() if
- team not in direct_teams]
+ team for team in paths.keys()
+ if team not in direct_teams]
         participations = []

         # First, create a participation for all direct memberships.

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-08-19 17:29:38 +0000
+++ lib/lp/registry/model/person.py 2010-08-19 17:39:30 +0000
@@ -1712,7 +1712,6 @@
         store = Store.of(self)
         return store.find(TeamMembership,
             And(TeamMembership.personID == self.id,
- Team.id == TeamMembership.teamID,
                 TeamMembership.status.is_in(
                 [TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN])))

@@ -2080,7 +2079,7 @@

         # Get all of the memberships for any of the teams this person is
         # a participant of. This must be ordered by date and id because
- # because the graph the results will create needs to contain
+ # because the graph of the results will create needs to contain
         # the oldest path information to be consistent with results from
         # IPerson.findPathToTeam.
         store = Store.of(self)
@@ -2114,7 +2113,7 @@
                 step = graph[step]
                 path.append(step)
             paths[team] = path
- return (paths, user_memberships)
+ return (paths, user_memberships)

     @property
     def teams_participated_in(self):

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2010-08-19 14:48:21 +0000
+++ lib/lp/registry/tests/test_person.py 2010-08-19 17:39:30 +0000
@@ -75,7 +75,7 @@
         memberships = [(m.person, m.team) for m in memberships]
         self.assertEqual([(self.user, self.a_team)], memberships)

- def test_path_to_team_no_limit(self):
+ def test_path_to_team(self):
         path_to_a = self.user.findPathToTeam(self.a_team)
         path_to_b = self.user.findPathToTeam(self.b_team)
         path_to_c = self.user.findPathToTeam(self.c_team)
@@ -84,15 +84,6 @@
         ...

Read more...

Revision history for this message
Edwin Grubbs (edwin-grubbs) :
review: Approve (code)
Revision history for this message
j.c.sackett (jcsackett) wrote :

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-08-22 03:09:51 +0000
+++ lib/lp/registry/model/person.py 2010-08-22 04:16:05 +0000
@@ -2203,6 +2203,10 @@
         # Get all of the teams this person participates in.
         teams = list(self.teams_participated_in)

+ # For cases where self is a team, we don't need self as a team
+ # participated in.
+ teams = [team for team in teams if team != self]
+
         # Get all of the memberships for any of the teams this person is
         # a participant of. This must be ordered by date and id because
         # because the graph of the results will create needs to contain
@@ -2210,9 +2214,16 @@
         # IPerson.findPathToTeam.
         store = Store.of(self)
         all_direct_memberships = store.find(TeamMembership,
- TeamMembership.personID.is_in(
- [team.id for team in teams] + [self.id])).order_by(
- Desc(TeamMembership.datejoined), Desc(TeamMembership.id))
+ And(
+ TeamMembership.personID.is_in(
+ [team.id for team in teams] + [self.id]),
+ TeamMembership.teamID != self.id,
+ TeamMembership.status.is_in([
+ TeamMembershipStatus.APPROVED,
+ TeamMembershipStatus.ADMIN,
+ ]))).order_by(
+ Desc(TeamMembership.datejoined),
+ Desc(TeamMembership.id))
         # Cast the results to list now, because they will be iterated over
         # several times.
         all_direct_memberships = list(all_direct_memberships)

Revision history for this message
Robert Collins (lifeless) wrote :

On Sun, Aug 22, 2010 at 4:46 PM, j.c.sackett
<email address hidden> wrote:
> === modified file 'lib/lp/registry/model/person.py'
> --- lib/lp/registry/model/person.py     2010-08-22 03:09:51 +0000
> +++ lib/lp/registry/model/person.py     2010-08-22 04:16:05 +0000
> @@ -2203,6 +2203,10 @@
>         # Get all of the teams this person participates in.
>         teams = list(self.teams_participated_in)
>
> +        # For cases where self is a team, we don't need self as a team
> +        # participated in.
> +        teams = [team for team in teams if team != self]

This is not idiomatic python. Does the following work?
teams = [team for team in teams if team is not self]

It may not due to security proxies; worth a comment if it doesn't to
avoid cleanups taking someones time.

-Rob

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

+1 on the improvements after the ec2 test run.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/sample-data-assertions.txt'
2--- lib/canonical/launchpad/doc/sample-data-assertions.txt 2007-02-21 15:55:46 +0000
3+++ lib/canonical/launchpad/doc/sample-data-assertions.txt 2010-08-22 17:16:44 +0000
4@@ -22,14 +22,14 @@
5 This user is not supposed to be a member of any teams.
6
7 >>> no_team_memberships = personset.getByName('no-team-memberships')
8- >>> no_team_memberships.myactivememberships.count()
9+ >>> no_team_memberships.team_memberships.count()
10 0
11
12 * One Team Membership
13 This user is supposed to be a member of only one team, the "Simple Team".
14
15 >>> one_membership = personset.getByName('one-membership')
16- >>> for t in one_membership.myactivememberships: print t.team.displayname
17+ >>> for t in one_membership.team_memberships: print t.team.displayname
18 Simple Team
19
20
21
22=== modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt'
23--- lib/lp/bugs/doc/initial-bug-contacts.txt 2009-08-25 11:21:05 +0000
24+++ lib/lp/bugs/doc/initial-bug-contacts.txt 2010-08-22 17:16:44 +0000
25@@ -403,7 +403,7 @@
26
27 >>> sample_person = view.user
28 >>> ["%s: %s" % (membership.team.displayname, membership.status.name)
29- ... for membership in sample_person.myactivememberships]
30+ ... for membership in sample_person.team_memberships]
31 [u'HWDB Team: APPROVED',
32 u'Landscape Developers: ADMIN',
33 u'Launchpad Users: ADMIN',
34
35=== modified file 'lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt'
36--- lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt 2009-07-17 17:59:07 +0000
37+++ lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt 2010-08-22 17:16:44 +0000
38@@ -5,7 +5,7 @@
39 omit-tag="">
40
41 <div class="portlet" id="portlet-team-assigned-bugs"
42- tal:condition="context/myactivememberships">
43+ tal:condition="context/team_memberships">
44
45 <h2><span tal:replace="context/title" />'s teams</h2>
46
47
48=== modified file 'lib/lp/registry/browser/person.py'
49--- lib/lp/registry/browser/person.py 2010-08-20 20:31:18 +0000
50+++ lib/lp/registry/browser/person.py 2010-08-22 17:16:44 +0000
51@@ -3162,31 +3162,33 @@
52 def label(self):
53 return 'Team participation for ' + self.context.displayname
54
55- def _asParticipation(self, membership=None, team=None, team_path=None):
56+ def _asParticipation(self, membership=None, team=None, via=None):
57 """Return a dict of participation information for the membership.
58
59 Method requires membership or team, not both.
60+ :param via: The team through which the membership in the indirect
61+ team is established.
62 """
63 if ((membership is None and team is None) or
64 (membership is not None and team is not None)):
65 raise AssertionError(
66 "The membership or team argument must be provided, not both.")
67
68- if team_path is None:
69- via = None
70- else:
71- via = COMMASPACE.join(team.displayname for team in team_path)
72+ if via is not None:
73+ # When showing the path, it's unnecessary to show the team in
74+ # question at the beginning of the path, or the user at the
75+ # end of the path.
76+ via = COMMASPACE.join(
77+ [via_team.displayname for via_team in via[1:-1]])
78
79 if membership is None:
80- #we have membership via an indirect team, and can use
81- #sane defaults
82-
83- #the user can only be a member of this team
84+ # Membership is via an indirect team so sane defaults exist.
85+ # An indirect member cannot be an Owner or Admin of a team.
86 role = 'Member'
87- #the user never joined, and can't have a join date
88+ # The Person never joined, and can't have a join date.
89 datejoined = None
90 else:
91- #the member is a direct member, so we can use membership data
92+ # The member is a direct member; use the membership data.
93 team = membership.team
94 datejoined = membership.datejoined
95 if membership.person == team.teamowner:
96@@ -3212,18 +3214,29 @@
97 @cachedproperty
98 def active_participations(self):
99 """Return the participation information for active memberships."""
100- participations = [self._asParticipation(membership=membership)
101- for membership in self.context.myactivememberships
102- if check_permission('launchpad.View', membership.team)]
103- membership_set = getUtility(ITeamMembershipSet)
104- for team in self.context.teams_indirectly_participated_in:
105- if not check_permission('launchpad.View', team):
106+ paths, memberships = self.context.getPathsToTeams()
107+ direct_teams = [membership.team for membership in memberships]
108+ indirect_teams = [
109+ team for team in paths.keys()
110+ if team not in direct_teams]
111+ participations = []
112+
113+ # First, create a participation for all direct memberships.
114+ for membership in memberships:
115+ # Add a participation record for the membership if allowed.
116+ if check_permission('launchpad.View', membership.team):
117+ participations.append(
118+ self._asParticipation(membership=membership))
119+
120+ # Second, create a participation for all indirect memberships,
121+ # using the remaining paths.
122+ for indirect_team in indirect_teams:
123+ if not check_permission('launchpad.View', indirect_team):
124 continue
125- # The key points of the path for presentation are:
126- # [-?] indirect memberships, [-2] direct membership, [-1] team.
127- team_path = self.context.findPathToTeam(team)
128 participations.append(
129- self._asParticipation(team_path=team_path[:-1], team=team))
130+ self._asParticipation(
131+ via=paths[indirect_team],
132+ team=indirect_team))
133 return sorted(participations, key=itemgetter('displayname'))
134
135 @cachedproperty
136
137=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
138--- lib/lp/registry/browser/tests/test_person_view.py 2010-08-20 20:31:18 +0000
139+++ lib/lp/registry/browser/tests/test_person_view.py 2010-08-22 17:16:44 +0000
140@@ -268,7 +268,7 @@
141 team = self.factory.makeTeam()
142 login_person(team.teamowner)
143 team.addMember(self.user, team.teamowner)
144- for membership in self.user.myactivememberships:
145+ for membership in self.user.team_memberships:
146 membership.setStatus(
147 TeamMembershipStatus.ADMIN, team.teamowner)
148 [participation] = self.view.active_participations
149@@ -360,7 +360,7 @@
150 self.assertEqual(['A', 'B', 'C'], display_names)
151 self.assertEqual(None, participations[0]['via'])
152 self.assertEqual('A', participations[1]['via'])
153- self.assertEqual('A, B', participations[2]['via'])
154+ self.assertEqual('B, A', participations[2]['via'])
155
156 def test_has_participations_false(self):
157 participations = self.view.active_participations
158
159=== modified file 'lib/lp/registry/doc/person-account.txt'
160--- lib/lp/registry/doc/person-account.txt 2010-08-17 13:41:56 +0000
161+++ lib/lp/registry/doc/person-account.txt 2010-08-22 17:16:44 +0000
162@@ -80,7 +80,7 @@
163 >>> spec = Specification.selectFirst("assignee IS NULL", orderBy='id')
164 >>> spec.assignee = foobar
165
166- >>> [membership.team.name for membership in foobar.myactivememberships]
167+ >>> [membership.team.name for membership in foobar.team_memberships]
168 [u'canonical-partner-dev', u'guadamen', u'hwdb-team', u'admins',
169 u'launchpad-buildd-admins', u'launchpad', u'testing-spanish-team',
170 u'name18', u'ubuntu-team', u'vcs-imports']
171@@ -152,7 +152,7 @@
172
173 ...to have no team memberships...
174
175- >>> [membership.team.name for membership in foobar.myactivememberships]
176+ >>> [membership.team.name for membership in foobar.team_memberships]
177 []
178
179 ...and no validated/preferred email addresses...
180
181=== modified file 'lib/lp/registry/doc/private-team-visibility.txt'
182--- lib/lp/registry/doc/private-team-visibility.txt 2010-08-13 01:43:26 +0000
183+++ lib/lp/registry/doc/private-team-visibility.txt 2010-08-22 17:16:44 +0000
184@@ -40,7 +40,7 @@
185 >>> commercial_admin = login_celebrity('commercial_admin')
186 >>> check_permission('launchpad.View', priv_team)
187 True
188- >>> team_membership = priv_member.myactivememberships[0]
189+ >>> team_membership = priv_member.team_memberships[0]
190 >>> check_permission('launchpad.View', team_membership)
191 True
192
193
194=== modified file 'lib/lp/registry/doc/teammembership.txt'
195--- lib/lp/registry/doc/teammembership.txt 2010-08-17 11:50:43 +0000
196+++ lib/lp/registry/doc/teammembership.txt 2010-08-22 17:16:44 +0000
197@@ -427,7 +427,7 @@
198 # Foo Bar is a launchpad admin, but even so he can't change a membership's
199 # status/expiry-date by hand.
200 >>> login_person(foobar)
201- >>> membership = foobar.myactivememberships[0]
202+ >>> membership = foobar.team_memberships[0]
203 >>> membership.status = None
204 Traceback (most recent call last):
205 ...
206@@ -638,13 +638,13 @@
207 >>> autorenewal_team.defaultrenewalperiod = 73
208 >>> otto = factory.makePerson(name='otto')
209 >>> ignore = autorenewal_team.addMember(otto, salgado)
210- >>> otto_membership = otto.myactivememberships[0]
211+ >>> otto_membership = otto.team_memberships[0]
212 >>> otto_membership.setExpirationDate(utc_now, salgado)
213 >>> original_otto_date_expires = otto_membership.dateexpires
214
215 >>> do_not_warn = factory.makePerson(name='do-not-warn')
216 >>> ignore = autorenewal_team.addMember(do_not_warn, salgado)
217- >>> do_not_warn.myactivememberships[0].setExpirationDate(
218+ >>> do_not_warn.team_memberships[0].setExpirationDate(
219 ... tomorrow, salgado)
220
221 # Not excluding teams with automatic renewals.
222@@ -788,10 +788,10 @@
223
224 == Querying team memberships ==
225
226-You can check a person's direct memberships by using myactivememberships:
227+You can check a person's direct memberships by using team_memberships:
228
229 >>> [(membership.team.name, membership.status.title)
230- ... for membership in salgado.myactivememberships]
231+ ... for membership in salgado.team_memberships]
232 [(u'hwdb-team', 'Approved'), (u'landscape-developers', 'Approved'),
233 (u'admins', 'Administrator'), (u't4', 'Approved')]
234
235
236=== modified file 'lib/lp/registry/doc/vocabularies.txt'
237--- lib/lp/registry/doc/vocabularies.txt 2010-08-13 21:30:24 +0000
238+++ lib/lp/registry/doc/vocabularies.txt 2010-08-22 17:16:44 +0000
239@@ -320,7 +320,7 @@
240
241 >>> sample_person = person_set.getByName('name12')
242 >>> [membership.team.name
243- ... for membership in sample_person.myactivememberships]
244+ ... for membership in sample_person.team_memberships]
245 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']
246 >>> [team.name for team in sample_person.teams_participated_in]
247 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',
248
249=== modified file 'lib/lp/registry/interfaces/person.py'
250--- lib/lp/registry/interfaces/person.py 2010-08-20 20:31:18 +0000
251+++ lib/lp/registry/interfaces/person.py 2010-08-22 17:16:44 +0000
252@@ -189,10 +189,8 @@
253
254 def validate_public_person(obj, attr, value):
255 """Validate that the person identified by value is public."""
256-
257 def validate(person):
258 return is_public_person(person)
259-
260 return validate_person_common(obj, attr, value, validate)
261
262
263@@ -661,9 +659,9 @@
264 readonly=True, required=False,
265 value_type=Reference(schema=IJabberID)),
266 exported_as='jabber_ids')
267- myactivememberships = exported(
268+ team_memberships = exported(
269 CollectionField(
270- title=_("All TeamMemberships for Teams this Person is an "
271+ title=_("All TeamMemberships for Teams this Team or Person is an "
272 "active member of."),
273 value_type=Reference(schema=ITeamMembership),
274 readonly=True, required=False),
275@@ -979,8 +977,7 @@
276 # @operation_returns_collection_of(Interface) # Really IPerson
277 # @export_read_operation()
278 def findPathToTeam(team):
279- """Return the teams that cause this person to be a participant of the
280- given team.
281+ """Return the teams providing membership to the given team.
282
283 If there is more than one path leading this person to the given team,
284 only the one with the oldest teams is returned.
285@@ -1157,6 +1154,9 @@
286 def getLatestApprovedMembershipsForPerson(limit=5):
287 """Return the <limit> latest approved membrships for this person."""
288
289+ def getPathsToTeams():
290+ """Return the paths to all teams related to this person."""
291+
292 def addLanguage(language):
293 """Add a language to this person's preferences.
294
295@@ -1383,7 +1383,7 @@
296
297 def getMembersWithPreferredEmailsCount():
298 """Returns the count of persons/teams with preferred emails.
299-
300+
301 See also getMembersWithPreferredEmails.
302 """
303
304
305=== modified file 'lib/lp/registry/model/person.py'
306--- lib/lp/registry/model/person.py 2010-08-20 20:31:18 +0000
307+++ lib/lp/registry/model/person.py 2010-08-22 17:16:44 +0000
308@@ -55,6 +55,7 @@
309 from storm.expr import (
310 Alias,
311 And,
312+ Desc,
313 Exists,
314 In,
315 Join,
316@@ -65,6 +66,7 @@
317 Or,
318 Select,
319 SQL,
320+ Upper,
321 )
322 from storm.info import ClassAlias
323 from storm.store import (
324@@ -1618,7 +1620,8 @@
325 if need_location:
326 # New people have no location rows
327 origin.append(
328- LeftJoin(PersonLocation, PersonLocation.person == Person.id))
329+ LeftJoin(PersonLocation,
330+ PersonLocation.person == Person.id))
331 columns.append(PersonLocation)
332 if need_archive:
333 # Not everyone has PPA's
334@@ -1821,19 +1824,22 @@
335 self.invited_members,
336 orderBy=self._sortingColumnsForSetOperations)
337
338- # XXX: kiko 2005-10-07:
339- # myactivememberships should be renamed to team_memberships and be
340- # described as the set of memberships for the object's teams.
341 @property
342- def myactivememberships(self):
343+ def team_memberships(self):
344 """See `IPerson`."""
345- return TeamMembership.select("""
346- TeamMembership.person = %s AND status in %s AND
347- Person.id = TeamMembership.team
348- """ % sqlvalues(self.id, [TeamMembershipStatus.APPROVED,
349- TeamMembershipStatus.ADMIN]),
350- clauseTables=['Person'],
351- orderBy=Person.sortingColumns)
352+ Team = ClassAlias(Person, "Team")
353+ store = Store.of(self)
354+ # Join on team to sort by team names. Upper is used in the sort so
355+ # sorting works as is user expected, e.g. (A b C) not (A C b).
356+ return store.find(TeamMembership,
357+ And(TeamMembership.personID == self.id,
358+ TeamMembership.teamID == Team.id,
359+ TeamMembership.status.is_in([
360+ TeamMembershipStatus.APPROVED,
361+ TeamMembershipStatus.ADMIN,
362+ ]))).order_by(
363+ Upper(Team.displayname),
364+ Upper(Team.name))
365
366 def _getMappedParticipantsLocations(self, limit=None):
367 """See `IPersonViewRestricted`."""
368@@ -1939,7 +1945,7 @@
369 assert self.is_valid_person, (
370 "You can only deactivate an account of a valid person.")
371
372- for membership in self.myactivememberships:
373+ for membership in self.team_memberships:
374 self.leave(membership.team)
375
376 # Deactivate CoC signatures, invalidate email addresses, unassign bug
377@@ -2186,10 +2192,66 @@
378
379 def getLatestApprovedMembershipsForPerson(self, limit=5):
380 """See `IPerson`."""
381- result = self.myactivememberships
382- result = result.orderBy(['-date_joined', '-id'])
383+ result = self.team_memberships
384+ result = result.order_by(
385+ Desc(TeamMembership.datejoined),
386+ Desc(TeamMembership.id))
387 return result[:limit]
388
389+ def getPathsToTeams(self):
390+ """See `Iperson`."""
391+ # Get all of the teams this person participates in.
392+ teams = list(self.teams_participated_in)
393+
394+ # For cases where self is a team, we don't need self as a team
395+ # participated in.
396+ teams = [team for team in teams if team is not self]
397+
398+ # Get all of the memberships for any of the teams this person is
399+ # a participant of. This must be ordered by date and id because
400+ # because the graph of the results will create needs to contain
401+ # the oldest path information to be consistent with results from
402+ # IPerson.findPathToTeam.
403+ store = Store.of(self)
404+ all_direct_memberships = store.find(TeamMembership,
405+ And(
406+ TeamMembership.personID.is_in(
407+ [team.id for team in teams] + [self.id]),
408+ TeamMembership.teamID != self.id,
409+ TeamMembership.status.is_in([
410+ TeamMembershipStatus.APPROVED,
411+ TeamMembershipStatus.ADMIN,
412+ ]))).order_by(
413+ Desc(TeamMembership.datejoined),
414+ Desc(TeamMembership.id))
415+ # Cast the results to list now, because they will be iterated over
416+ # several times.
417+ all_direct_memberships = list(all_direct_memberships)
418+
419+ # Pull out the memberships directly used by this person.
420+ user_memberships = [
421+ membership for membership in
422+ all_direct_memberships
423+ if membership.person == self]
424+
425+ all_direct_memberships = [
426+ (membership.team, membership.person) for membership in
427+ all_direct_memberships]
428+
429+ # Create a graph from the edges provided by the other data sets.
430+ graph = dict(all_direct_memberships)
431+
432+ # Build the teams paths from that graph.
433+ paths = {}
434+ for team in teams:
435+ path = [team]
436+ step = team
437+ while path[-1] != self:
438+ step = graph[step]
439+ path.append(step)
440+ paths[team] = path
441+ return (paths, user_memberships)
442+
443 @property
444 def teams_participated_in(self):
445 """See `IPerson`."""
446
447=== modified file 'lib/lp/registry/tests/test_person.py'
448--- lib/lp/registry/tests/test_person.py 2010-08-20 20:31:18 +0000
449+++ lib/lp/registry/tests/test_person.py 2010-08-22 17:16:44 +0000
450@@ -67,19 +67,107 @@
451
452 layer = DatabaseFunctionalLayer
453
454+ def setUp(self):
455+ super(TestPersonTeams, self).setUp()
456+ self.user = self.factory.makePerson()
457+ self.a_team = self.factory.makeTeam(name='a')
458+ self.b_team = self.factory.makeTeam(name='b', owner=self.a_team)
459+ self.c_team = self.factory.makeTeam(name='c', owner=self.b_team)
460+ login_person(self.a_team.teamowner)
461+ self.a_team.addMember(self.user, self.a_team.teamowner)
462+
463 def test_teams_indirectly_participated_in(self):
464- self.user = self.factory.makePerson()
465- a_team = self.factory.makeTeam(name='a')
466- b_team = self.factory.makeTeam(name='b', owner=a_team)
467- c_team = self.factory.makeTeam(name='c', owner=b_team)
468- login_person(a_team.teamowner)
469- a_team.addMember(self.user, a_team.teamowner)
470 indirect_teams = self.user.teams_indirectly_participated_in
471- expected_teams = [b_team, c_team]
472+ expected_teams = [self.b_team, self.c_team]
473 test_teams = sorted(indirect_teams,
474 key=lambda team: team.displayname)
475 self.assertEqual(expected_teams, test_teams)
476
477+ def test_team_memberships(self):
478+ memberships = self.user.team_memberships
479+ memberships = [(m.person, m.team) for m in memberships]
480+ self.assertEqual([(self.user, self.a_team)], memberships)
481+
482+ def test_path_to_team(self):
483+ path_to_a = self.user.findPathToTeam(self.a_team)
484+ path_to_b = self.user.findPathToTeam(self.b_team)
485+ path_to_c = self.user.findPathToTeam(self.c_team)
486+
487+ self.assertEqual([self.a_team], path_to_a)
488+ self.assertEqual([self.a_team, self.b_team], path_to_b)
489+ self.assertEqual([self.a_team, self.b_team, self.c_team], path_to_c)
490+
491+ def test_teams_participated_in(self):
492+ teams = self.user.teams_participated_in
493+ teams = sorted(list(teams), key=lambda x: x.displayname)
494+ expected_teams = [self.a_team, self.b_team, self.c_team]
495+ self.assertEqual(expected_teams, teams)
496+
497+ def test_getPathsToTeams(self):
498+ paths, memberships = self.user.getPathsToTeams()
499+ expected_paths = {self.a_team: [self.a_team, self.user],
500+ self.b_team: [self.b_team, self.a_team, self.user],
501+ self.c_team: [self.c_team, self.b_team, self.a_team, self.user]}
502+ self.assertEqual(expected_paths, paths)
503+
504+ expected_memberships = [(self.a_team, self.user)]
505+ memberships = [
506+ (membership.team, membership.person) for membership
507+ in memberships]
508+ self.assertEqual(expected_memberships, memberships)
509+
510+ def test_getPathsToTeamsComplicated(self):
511+ d_team = self.factory.makeTeam(name='d', owner=self.b_team)
512+ e_team = self.factory.makeTeam(name='e')
513+ f_team = self.factory.makeTeam(name='f', owner=e_team)
514+ unrelated_team = self.factory.makeTeam(name='unrelated')
515+ login_person(self.a_team.teamowner)
516+ d_team.addMember(self.user, d_team.teamowner)
517+ login_person(e_team.teamowner)
518+ e_team.addMember(self.user, e_team.teamowner)
519+
520+ paths, memberships = self.user.getPathsToTeams()
521+ expected_paths = {
522+ self.a_team: [self.a_team, self.user],
523+ self.b_team: [self.b_team, self.a_team, self.user],
524+ self.c_team: [self.c_team, self.b_team, self.a_team, self.user],
525+ d_team: [d_team, self.b_team, self.a_team, self.user],
526+ e_team: [e_team, self.user],
527+ f_team: [f_team, e_team, self.user]}
528+ self.assertEqual(expected_paths, paths)
529+
530+ expected_memberships = [
531+ (e_team, self.user),
532+ (d_team, self.user),
533+ (self.a_team, self.user),
534+ ]
535+ memberships = [
536+ (membership.team, membership.person) for membership
537+ in memberships]
538+ self.assertEqual(expected_memberships, memberships)
539+
540+ def test_getPathsToTeamsMultiplePaths(self):
541+ d_team = self.factory.makeTeam(name='d', owner=self.b_team)
542+ login_person(self.a_team.teamowner)
543+ self.c_team.addMember(d_team, self.c_team.teamowner)
544+
545+ paths, memberships = self.user.getPathsToTeams()
546+ # getPathsToTeams should not randomly pick one path or another
547+ # when multiples exist; it sorts to use the oldest path, so
548+ # the expected paths below should be the returned result.
549+ expected_paths = {
550+ self.a_team: [self.a_team, self.user],
551+ self.b_team: [self.b_team, self.a_team, self.user],
552+ self.c_team: [self.c_team, self.b_team, self.a_team, self.user],
553+ d_team: [d_team, self.b_team, self.a_team, self.user]}
554+ self.assertEqual(expected_paths, paths)
555+
556+ expected_memberships = [(self.a_team, self.user)]
557+ memberships = [
558+ (membership.team, membership.person) for membership
559+ in memberships]
560+ self.assertEqual(expected_memberships, memberships)
561+
562
563 class TestPerson(TestCaseWithFactory):
564
565
566=== modified file 'lib/lp/registry/vocabularies.py'
567--- lib/lp/registry/vocabularies.py 2010-08-20 20:31:18 +0000
568+++ lib/lp/registry/vocabularies.py 2010-08-22 17:16:44 +0000
569@@ -267,8 +267,7 @@
570 like_query = "'%%' || %s || '%%'" % quote_like(query)
571 fti_query = quote(query)
572 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
573- like_query, fti_query
574- )
575+ like_query, fti_query)
576 return self._table.select(sql, orderBy=self._orderBy)
577 return self.emptySelectResults()
578
579@@ -315,8 +314,7 @@
580 like_query = "'%%' || %s || '%%'" % quote_like(query)
581 fti_query = quote(query)
582 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
583- like_query, fti_query
584- )
585+ like_query, fti_query)
586 return self._table.select(sql)
587 return self.emptySelectResults()
588
589@@ -592,11 +590,8 @@
590 # Or a person who has an active account and a working
591 # email address.
592 And(Account.status == AccountStatus.ACTIVE,
593- EmailAddress.status.is_in(valid_email_statuses))
594- ),
595- self.extra_clause
596- )
597- )
598+ EmailAddress.status.is_in(valid_email_statuses))),
599+ self.extra_clause))
600 # The public query doesn't need to be ordered as it will be done
601 # at the end.
602 public_result.order_by()
603@@ -836,7 +831,7 @@
604 def _get_teams(self):
605 """The teams that the vocabulary is built from."""
606 return [membership.team for membership
607- in self.context.myactivememberships
608+ in self.context.team_memberships
609 if membership.team.visibility == PersonVisibility.PUBLIC]
610
611 def __len__(self):
612@@ -1027,9 +1022,7 @@
613 Milestone.q.productseriesID == ProductSeries.q.id,
614 ProductSeries.q.productID == Product.q.id,
615 Product.q.name == productname,
616- ProductSeries.q.name == productseriesname
617- )
618- )
619+ ProductSeries.q.name == productseriesname))
620 try:
621 return self.toTerm(obj)
622 except IndexError:
623@@ -1315,8 +1308,7 @@
624 return [
625 project for project in sorted(projects,
626 key=attrgetter('displayname'))
627- if not project.qualifies_for_free_hosting
628- ]
629+ if not project.qualifies_for_free_hosting]
630
631 def _doSearch(self, query=None):
632 """Return terms where query is in the text of name
633@@ -1454,11 +1446,8 @@
634 Distribution.q.id == DistroSeries.q.distributionID,
635 OR(
636 CONTAINSSTRING(Distribution.q.name, query),
637- CONTAINSSTRING(DistroSeries.q.name, query)
638- )
639- ),
640- orderBy=self._orderBy
641- )
642+ CONTAINSSTRING(DistroSeries.q.name, query))),
643+ orderBy=self._orderBy)
644 return objs
645
646
647@@ -1472,8 +1461,7 @@
648 """See `IVocabulary`."""
649 if IPillarName.providedBy(obj):
650 assert obj.active, 'Inactive object %s %d' % (
651- obj.__class__.__name__, obj.id
652- )
653+ obj.__class__.__name__, obj.id)
654 obj = obj.pillar
655
656 # It is a hack using the class name here, but it works