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
=== modified file 'lib/canonical/launchpad/doc/sample-data-assertions.txt'
--- lib/canonical/launchpad/doc/sample-data-assertions.txt 2007-02-21 15:55:46 +0000
+++ lib/canonical/launchpad/doc/sample-data-assertions.txt 2010-08-22 17:16:44 +0000
@@ -22,14 +22,14 @@
22 This user is not supposed to be a member of any teams.22 This user is not supposed to be a member of any teams.
2323
24 >>> no_team_memberships = personset.getByName('no-team-memberships')24 >>> no_team_memberships = personset.getByName('no-team-memberships')
25 >>> no_team_memberships.myactivememberships.count()25 >>> no_team_memberships.team_memberships.count()
26 026 0
2727
28* One Team Membership28* One Team Membership
29 This user is supposed to be a member of only one team, the "Simple Team".29 This user is supposed to be a member of only one team, the "Simple Team".
3030
31 >>> one_membership = personset.getByName('one-membership')31 >>> one_membership = personset.getByName('one-membership')
32 >>> for t in one_membership.myactivememberships: print t.team.displayname32 >>> for t in one_membership.team_memberships: print t.team.displayname
33 Simple Team33 Simple Team
3434
3535
3636
=== modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt'
--- lib/lp/bugs/doc/initial-bug-contacts.txt 2009-08-25 11:21:05 +0000
+++ lib/lp/bugs/doc/initial-bug-contacts.txt 2010-08-22 17:16:44 +0000
@@ -403,7 +403,7 @@
403403
404 >>> sample_person = view.user404 >>> sample_person = view.user
405 >>> ["%s: %s" % (membership.team.displayname, membership.status.name)405 >>> ["%s: %s" % (membership.team.displayname, membership.status.name)
406 ... for membership in sample_person.myactivememberships]406 ... for membership in sample_person.team_memberships]
407 [u'HWDB Team: APPROVED',407 [u'HWDB Team: APPROVED',
408 u'Landscape Developers: ADMIN',408 u'Landscape Developers: ADMIN',
409 u'Launchpad Users: ADMIN',409 u'Launchpad Users: ADMIN',
410410
=== modified file 'lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt'
--- lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt 2009-07-17 17:59:07 +0000
+++ lib/lp/bugs/templates/person-portlet-team-assignedbugs.pt 2010-08-22 17:16:44 +0000
@@ -5,7 +5,7 @@
5 omit-tag="">5 omit-tag="">
66
7<div class="portlet" id="portlet-team-assigned-bugs"7<div class="portlet" id="portlet-team-assigned-bugs"
8 tal:condition="context/myactivememberships">8 tal:condition="context/team_memberships">
99
10 <h2><span tal:replace="context/title" />'s teams</h2>10 <h2><span tal:replace="context/title" />'s teams</h2>
1111
1212
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/person.py 2010-08-22 17:16:44 +0000
@@ -3162,31 +3162,33 @@
3162 def label(self):3162 def label(self):
3163 return 'Team participation for ' + self.context.displayname3163 return 'Team participation for ' + self.context.displayname
31643164
3165 def _asParticipation(self, membership=None, team=None, team_path=None):3165 def _asParticipation(self, membership=None, team=None, via=None):
3166 """Return a dict of participation information for the membership.3166 """Return a dict of participation information for the membership.
31673167
3168 Method requires membership or team, not both.3168 Method requires membership or team, not both.
3169 :param via: The team through which the membership in the indirect
3170 team is established.
3169 """3171 """
3170 if ((membership is None and team is None) or3172 if ((membership is None and team is None) or
3171 (membership is not None and team is not None)):3173 (membership is not None and team is not None)):
3172 raise AssertionError(3174 raise AssertionError(
3173 "The membership or team argument must be provided, not both.")3175 "The membership or team argument must be provided, not both.")
31743176
3175 if team_path is None:3177 if via is not None:
3176 via = None3178 # When showing the path, it's unnecessary to show the team in
3177 else:3179 # question at the beginning of the path, or the user at the
3178 via = COMMASPACE.join(team.displayname for team in team_path)3180 # end of the path.
3181 via = COMMASPACE.join(
3182 [via_team.displayname for via_team in via[1:-1]])
31793183
3180 if membership is None:3184 if membership is None:
3181 #we have membership via an indirect team, and can use3185 # Membership is via an indirect team so sane defaults exist.
3182 #sane defaults3186 # An indirect member cannot be an Owner or Admin of a team.
3183
3184 #the user can only be a member of this team
3185 role = 'Member'3187 role = 'Member'
3186 #the user never joined, and can't have a join date3188 # The Person never joined, and can't have a join date.
3187 datejoined = None3189 datejoined = None
3188 else:3190 else:
3189 #the member is a direct member, so we can use membership data3191 # The member is a direct member; use the membership data.
3190 team = membership.team3192 team = membership.team
3191 datejoined = membership.datejoined3193 datejoined = membership.datejoined
3192 if membership.person == team.teamowner:3194 if membership.person == team.teamowner:
@@ -3212,18 +3214,29 @@
3212 @cachedproperty3214 @cachedproperty
3213 def active_participations(self):3215 def active_participations(self):
3214 """Return the participation information for active memberships."""3216 """Return the participation information for active memberships."""
3215 participations = [self._asParticipation(membership=membership)3217 paths, memberships = self.context.getPathsToTeams()
3216 for membership in self.context.myactivememberships3218 direct_teams = [membership.team for membership in memberships]
3217 if check_permission('launchpad.View', membership.team)]3219 indirect_teams = [
3218 membership_set = getUtility(ITeamMembershipSet)3220 team for team in paths.keys()
3219 for team in self.context.teams_indirectly_participated_in:3221 if team not in direct_teams]
3220 if not check_permission('launchpad.View', team):3222 participations = []
3223
3224 # First, create a participation for all direct memberships.
3225 for membership in memberships:
3226 # Add a participation record for the membership if allowed.
3227 if check_permission('launchpad.View', membership.team):
3228 participations.append(
3229 self._asParticipation(membership=membership))
3230
3231 # Second, create a participation for all indirect memberships,
3232 # using the remaining paths.
3233 for indirect_team in indirect_teams:
3234 if not check_permission('launchpad.View', indirect_team):
3221 continue3235 continue
3222 # The key points of the path for presentation are:
3223 # [-?] indirect memberships, [-2] direct membership, [-1] team.
3224 team_path = self.context.findPathToTeam(team)
3225 participations.append(3236 participations.append(
3226 self._asParticipation(team_path=team_path[:-1], team=team))3237 self._asParticipation(
3238 via=paths[indirect_team],
3239 team=indirect_team))
3227 return sorted(participations, key=itemgetter('displayname'))3240 return sorted(participations, key=itemgetter('displayname'))
32283241
3229 @cachedproperty3242 @cachedproperty
32303243
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-08-22 17:16:44 +0000
@@ -268,7 +268,7 @@
268 team = self.factory.makeTeam()268 team = self.factory.makeTeam()
269 login_person(team.teamowner)269 login_person(team.teamowner)
270 team.addMember(self.user, team.teamowner)270 team.addMember(self.user, team.teamowner)
271 for membership in self.user.myactivememberships:271 for membership in self.user.team_memberships:
272 membership.setStatus(272 membership.setStatus(
273 TeamMembershipStatus.ADMIN, team.teamowner)273 TeamMembershipStatus.ADMIN, team.teamowner)
274 [participation] = self.view.active_participations274 [participation] = self.view.active_participations
@@ -360,7 +360,7 @@
360 self.assertEqual(['A', 'B', 'C'], display_names)360 self.assertEqual(['A', 'B', 'C'], display_names)
361 self.assertEqual(None, participations[0]['via'])361 self.assertEqual(None, participations[0]['via'])
362 self.assertEqual('A', participations[1]['via'])362 self.assertEqual('A', participations[1]['via'])
363 self.assertEqual('A, B', participations[2]['via'])363 self.assertEqual('B, A', participations[2]['via'])
364364
365 def test_has_participations_false(self):365 def test_has_participations_false(self):
366 participations = self.view.active_participations366 participations = self.view.active_participations
367367
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2010-08-17 13:41:56 +0000
+++ lib/lp/registry/doc/person-account.txt 2010-08-22 17:16:44 +0000
@@ -80,7 +80,7 @@
80 >>> spec = Specification.selectFirst("assignee IS NULL", orderBy='id')80 >>> spec = Specification.selectFirst("assignee IS NULL", orderBy='id')
81 >>> spec.assignee = foobar81 >>> spec.assignee = foobar
8282
83 >>> [membership.team.name for membership in foobar.myactivememberships]83 >>> [membership.team.name for membership in foobar.team_memberships]
84 [u'canonical-partner-dev', u'guadamen', u'hwdb-team', u'admins',84 [u'canonical-partner-dev', u'guadamen', u'hwdb-team', u'admins',
85 u'launchpad-buildd-admins', u'launchpad', u'testing-spanish-team',85 u'launchpad-buildd-admins', u'launchpad', u'testing-spanish-team',
86 u'name18', u'ubuntu-team', u'vcs-imports']86 u'name18', u'ubuntu-team', u'vcs-imports']
@@ -152,7 +152,7 @@
152152
153...to have no team memberships...153...to have no team memberships...
154154
155 >>> [membership.team.name for membership in foobar.myactivememberships]155 >>> [membership.team.name for membership in foobar.team_memberships]
156 []156 []
157157
158...and no validated/preferred email addresses...158...and no validated/preferred email addresses...
159159
=== modified file 'lib/lp/registry/doc/private-team-visibility.txt'
--- lib/lp/registry/doc/private-team-visibility.txt 2010-08-13 01:43:26 +0000
+++ lib/lp/registry/doc/private-team-visibility.txt 2010-08-22 17:16:44 +0000
@@ -40,7 +40,7 @@
40 >>> commercial_admin = login_celebrity('commercial_admin')40 >>> commercial_admin = login_celebrity('commercial_admin')
41 >>> check_permission('launchpad.View', priv_team)41 >>> check_permission('launchpad.View', priv_team)
42 True42 True
43 >>> team_membership = priv_member.myactivememberships[0]43 >>> team_membership = priv_member.team_memberships[0]
44 >>> check_permission('launchpad.View', team_membership)44 >>> check_permission('launchpad.View', team_membership)
45 True45 True
4646
4747
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2010-08-17 11:50:43 +0000
+++ lib/lp/registry/doc/teammembership.txt 2010-08-22 17:16:44 +0000
@@ -427,7 +427,7 @@
427 # Foo Bar is a launchpad admin, but even so he can't change a membership's427 # Foo Bar is a launchpad admin, but even so he can't change a membership's
428 # status/expiry-date by hand.428 # status/expiry-date by hand.
429 >>> login_person(foobar)429 >>> login_person(foobar)
430 >>> membership = foobar.myactivememberships[0]430 >>> membership = foobar.team_memberships[0]
431 >>> membership.status = None431 >>> membership.status = None
432 Traceback (most recent call last):432 Traceback (most recent call last):
433 ...433 ...
@@ -638,13 +638,13 @@
638 >>> autorenewal_team.defaultrenewalperiod = 73638 >>> autorenewal_team.defaultrenewalperiod = 73
639 >>> otto = factory.makePerson(name='otto')639 >>> otto = factory.makePerson(name='otto')
640 >>> ignore = autorenewal_team.addMember(otto, salgado)640 >>> ignore = autorenewal_team.addMember(otto, salgado)
641 >>> otto_membership = otto.myactivememberships[0]641 >>> otto_membership = otto.team_memberships[0]
642 >>> otto_membership.setExpirationDate(utc_now, salgado)642 >>> otto_membership.setExpirationDate(utc_now, salgado)
643 >>> original_otto_date_expires = otto_membership.dateexpires643 >>> original_otto_date_expires = otto_membership.dateexpires
644644
645 >>> do_not_warn = factory.makePerson(name='do-not-warn')645 >>> do_not_warn = factory.makePerson(name='do-not-warn')
646 >>> ignore = autorenewal_team.addMember(do_not_warn, salgado)646 >>> ignore = autorenewal_team.addMember(do_not_warn, salgado)
647 >>> do_not_warn.myactivememberships[0].setExpirationDate(647 >>> do_not_warn.team_memberships[0].setExpirationDate(
648 ... tomorrow, salgado)648 ... tomorrow, salgado)
649649
650 # Not excluding teams with automatic renewals.650 # Not excluding teams with automatic renewals.
@@ -788,10 +788,10 @@
788788
789== Querying team memberships ==789== Querying team memberships ==
790790
791You can check a person's direct memberships by using myactivememberships:791You can check a person's direct memberships by using team_memberships:
792792
793 >>> [(membership.team.name, membership.status.title)793 >>> [(membership.team.name, membership.status.title)
794 ... for membership in salgado.myactivememberships]794 ... for membership in salgado.team_memberships]
795 [(u'hwdb-team', 'Approved'), (u'landscape-developers', 'Approved'),795 [(u'hwdb-team', 'Approved'), (u'landscape-developers', 'Approved'),
796 (u'admins', 'Administrator'), (u't4', 'Approved')]796 (u'admins', 'Administrator'), (u't4', 'Approved')]
797797
798798
=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt 2010-08-13 21:30:24 +0000
+++ lib/lp/registry/doc/vocabularies.txt 2010-08-22 17:16:44 +0000
@@ -320,7 +320,7 @@
320320
321 >>> sample_person = person_set.getByName('name12')321 >>> sample_person = person_set.getByName('name12')
322 >>> [membership.team.name322 >>> [membership.team.name
323 ... for membership in sample_person.myactivememberships]323 ... for membership in sample_person.team_memberships]
324 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']324 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name20']
325 >>> [team.name for team in sample_person.teams_participated_in]325 >>> [team.name for team in sample_person.teams_participated_in]
326 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',326 [u'hwdb-team', u'landscape-developers', u'launchpad-users', u'name18',
327327
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/interfaces/person.py 2010-08-22 17:16:44 +0000
@@ -189,10 +189,8 @@
189189
190def validate_public_person(obj, attr, value):190def validate_public_person(obj, attr, value):
191 """Validate that the person identified by value is public."""191 """Validate that the person identified by value is public."""
192
193 def validate(person):192 def validate(person):
194 return is_public_person(person)193 return is_public_person(person)
195
196 return validate_person_common(obj, attr, value, validate)194 return validate_person_common(obj, attr, value, validate)
197195
198196
@@ -661,9 +659,9 @@
661 readonly=True, required=False,659 readonly=True, required=False,
662 value_type=Reference(schema=IJabberID)),660 value_type=Reference(schema=IJabberID)),
663 exported_as='jabber_ids')661 exported_as='jabber_ids')
664 myactivememberships = exported(662 team_memberships = exported(
665 CollectionField(663 CollectionField(
666 title=_("All TeamMemberships for Teams this Person is an "664 title=_("All TeamMemberships for Teams this Team or Person is an "
667 "active member of."),665 "active member of."),
668 value_type=Reference(schema=ITeamMembership),666 value_type=Reference(schema=ITeamMembership),
669 readonly=True, required=False),667 readonly=True, required=False),
@@ -979,8 +977,7 @@
979 # @operation_returns_collection_of(Interface) # Really IPerson977 # @operation_returns_collection_of(Interface) # Really IPerson
980 # @export_read_operation()978 # @export_read_operation()
981 def findPathToTeam(team):979 def findPathToTeam(team):
982 """Return the teams that cause this person to be a participant of the980 """Return the teams providing membership to the given team.
983 given team.
984981
985 If there is more than one path leading this person to the given team,982 If there is more than one path leading this person to the given team,
986 only the one with the oldest teams is returned.983 only the one with the oldest teams is returned.
@@ -1157,6 +1154,9 @@
1157 def getLatestApprovedMembershipsForPerson(limit=5):1154 def getLatestApprovedMembershipsForPerson(limit=5):
1158 """Return the <limit> latest approved membrships for this person."""1155 """Return the <limit> latest approved membrships for this person."""
11591156
1157 def getPathsToTeams():
1158 """Return the paths to all teams related to this person."""
1159
1160 def addLanguage(language):1160 def addLanguage(language):
1161 """Add a language to this person's preferences.1161 """Add a language to this person's preferences.
11621162
@@ -1383,7 +1383,7 @@
13831383
1384 def getMembersWithPreferredEmailsCount():1384 def getMembersWithPreferredEmailsCount():
1385 """Returns the count of persons/teams with preferred emails.1385 """Returns the count of persons/teams with preferred emails.
1386 1386
1387 See also getMembersWithPreferredEmails.1387 See also getMembersWithPreferredEmails.
1388 """1388 """
13891389
13901390
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/person.py 2010-08-22 17:16:44 +0000
@@ -55,6 +55,7 @@
55from storm.expr import (55from storm.expr import (
56 Alias,56 Alias,
57 And,57 And,
58 Desc,
58 Exists,59 Exists,
59 In,60 In,
60 Join,61 Join,
@@ -65,6 +66,7 @@
65 Or,66 Or,
66 Select,67 Select,
67 SQL,68 SQL,
69 Upper,
68 )70 )
69from storm.info import ClassAlias71from storm.info import ClassAlias
70from storm.store import (72from storm.store import (
@@ -1618,7 +1620,8 @@
1618 if need_location:1620 if need_location:
1619 # New people have no location rows1621 # New people have no location rows
1620 origin.append(1622 origin.append(
1621 LeftJoin(PersonLocation, PersonLocation.person == Person.id))1623 LeftJoin(PersonLocation,
1624 PersonLocation.person == Person.id))
1622 columns.append(PersonLocation)1625 columns.append(PersonLocation)
1623 if need_archive:1626 if need_archive:
1624 # Not everyone has PPA's1627 # Not everyone has PPA's
@@ -1821,19 +1824,22 @@
1821 self.invited_members,1824 self.invited_members,
1822 orderBy=self._sortingColumnsForSetOperations)1825 orderBy=self._sortingColumnsForSetOperations)
18231826
1824 # XXX: kiko 2005-10-07:
1825 # myactivememberships should be renamed to team_memberships and be
1826 # described as the set of memberships for the object's teams.
1827 @property1827 @property
1828 def myactivememberships(self):1828 def team_memberships(self):
1829 """See `IPerson`."""1829 """See `IPerson`."""
1830 return TeamMembership.select("""1830 Team = ClassAlias(Person, "Team")
1831 TeamMembership.person = %s AND status in %s AND1831 store = Store.of(self)
1832 Person.id = TeamMembership.team1832 # Join on team to sort by team names. Upper is used in the sort so
1833 """ % sqlvalues(self.id, [TeamMembershipStatus.APPROVED,1833 # sorting works as is user expected, e.g. (A b C) not (A C b).
1834 TeamMembershipStatus.ADMIN]),1834 return store.find(TeamMembership,
1835 clauseTables=['Person'],1835 And(TeamMembership.personID == self.id,
1836 orderBy=Person.sortingColumns)1836 TeamMembership.teamID == Team.id,
1837 TeamMembership.status.is_in([
1838 TeamMembershipStatus.APPROVED,
1839 TeamMembershipStatus.ADMIN,
1840 ]))).order_by(
1841 Upper(Team.displayname),
1842 Upper(Team.name))
18371843
1838 def _getMappedParticipantsLocations(self, limit=None):1844 def _getMappedParticipantsLocations(self, limit=None):
1839 """See `IPersonViewRestricted`."""1845 """See `IPersonViewRestricted`."""
@@ -1939,7 +1945,7 @@
1939 assert self.is_valid_person, (1945 assert self.is_valid_person, (
1940 "You can only deactivate an account of a valid person.")1946 "You can only deactivate an account of a valid person.")
19411947
1942 for membership in self.myactivememberships:1948 for membership in self.team_memberships:
1943 self.leave(membership.team)1949 self.leave(membership.team)
19441950
1945 # Deactivate CoC signatures, invalidate email addresses, unassign bug1951 # Deactivate CoC signatures, invalidate email addresses, unassign bug
@@ -2186,10 +2192,66 @@
21862192
2187 def getLatestApprovedMembershipsForPerson(self, limit=5):2193 def getLatestApprovedMembershipsForPerson(self, limit=5):
2188 """See `IPerson`."""2194 """See `IPerson`."""
2189 result = self.myactivememberships2195 result = self.team_memberships
2190 result = result.orderBy(['-date_joined', '-id'])2196 result = result.order_by(
2197 Desc(TeamMembership.datejoined),
2198 Desc(TeamMembership.id))
2191 return result[:limit]2199 return result[:limit]
21922200
2201 def getPathsToTeams(self):
2202 """See `Iperson`."""
2203 # Get all of the teams this person participates in.
2204 teams = list(self.teams_participated_in)
2205
2206 # For cases where self is a team, we don't need self as a team
2207 # participated in.
2208 teams = [team for team in teams if team is not self]
2209
2210 # Get all of the memberships for any of the teams this person is
2211 # a participant of. This must be ordered by date and id because
2212 # because the graph of the results will create needs to contain
2213 # the oldest path information to be consistent with results from
2214 # IPerson.findPathToTeam.
2215 store = Store.of(self)
2216 all_direct_memberships = store.find(TeamMembership,
2217 And(
2218 TeamMembership.personID.is_in(
2219 [team.id for team in teams] + [self.id]),
2220 TeamMembership.teamID != self.id,
2221 TeamMembership.status.is_in([
2222 TeamMembershipStatus.APPROVED,
2223 TeamMembershipStatus.ADMIN,
2224 ]))).order_by(
2225 Desc(TeamMembership.datejoined),
2226 Desc(TeamMembership.id))
2227 # Cast the results to list now, because they will be iterated over
2228 # several times.
2229 all_direct_memberships = list(all_direct_memberships)
2230
2231 # Pull out the memberships directly used by this person.
2232 user_memberships = [
2233 membership for membership in
2234 all_direct_memberships
2235 if membership.person == self]
2236
2237 all_direct_memberships = [
2238 (membership.team, membership.person) for membership in
2239 all_direct_memberships]
2240
2241 # Create a graph from the edges provided by the other data sets.
2242 graph = dict(all_direct_memberships)
2243
2244 # Build the teams paths from that graph.
2245 paths = {}
2246 for team in teams:
2247 path = [team]
2248 step = team
2249 while path[-1] != self:
2250 step = graph[step]
2251 path.append(step)
2252 paths[team] = path
2253 return (paths, user_memberships)
2254
2193 @property2255 @property
2194 def teams_participated_in(self):2256 def teams_participated_in(self):
2195 """See `IPerson`."""2257 """See `IPerson`."""
21962258
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_person.py 2010-08-22 17:16:44 +0000
@@ -67,19 +67,107 @@
6767
68 layer = DatabaseFunctionalLayer68 layer = DatabaseFunctionalLayer
6969
70 def setUp(self):
71 super(TestPersonTeams, self).setUp()
72 self.user = self.factory.makePerson()
73 self.a_team = self.factory.makeTeam(name='a')
74 self.b_team = self.factory.makeTeam(name='b', owner=self.a_team)
75 self.c_team = self.factory.makeTeam(name='c', owner=self.b_team)
76 login_person(self.a_team.teamowner)
77 self.a_team.addMember(self.user, self.a_team.teamowner)
78
70 def test_teams_indirectly_participated_in(self):79 def test_teams_indirectly_participated_in(self):
71 self.user = self.factory.makePerson()
72 a_team = self.factory.makeTeam(name='a')
73 b_team = self.factory.makeTeam(name='b', owner=a_team)
74 c_team = self.factory.makeTeam(name='c', owner=b_team)
75 login_person(a_team.teamowner)
76 a_team.addMember(self.user, a_team.teamowner)
77 indirect_teams = self.user.teams_indirectly_participated_in80 indirect_teams = self.user.teams_indirectly_participated_in
78 expected_teams = [b_team, c_team]81 expected_teams = [self.b_team, self.c_team]
79 test_teams = sorted(indirect_teams,82 test_teams = sorted(indirect_teams,
80 key=lambda team: team.displayname)83 key=lambda team: team.displayname)
81 self.assertEqual(expected_teams, test_teams)84 self.assertEqual(expected_teams, test_teams)
8285
86 def test_team_memberships(self):
87 memberships = self.user.team_memberships
88 memberships = [(m.person, m.team) for m in memberships]
89 self.assertEqual([(self.user, self.a_team)], memberships)
90
91 def test_path_to_team(self):
92 path_to_a = self.user.findPathToTeam(self.a_team)
93 path_to_b = self.user.findPathToTeam(self.b_team)
94 path_to_c = self.user.findPathToTeam(self.c_team)
95
96 self.assertEqual([self.a_team], path_to_a)
97 self.assertEqual([self.a_team, self.b_team], path_to_b)
98 self.assertEqual([self.a_team, self.b_team, self.c_team], path_to_c)
99
100 def test_teams_participated_in(self):
101 teams = self.user.teams_participated_in
102 teams = sorted(list(teams), key=lambda x: x.displayname)
103 expected_teams = [self.a_team, self.b_team, self.c_team]
104 self.assertEqual(expected_teams, teams)
105
106 def test_getPathsToTeams(self):
107 paths, memberships = self.user.getPathsToTeams()
108 expected_paths = {self.a_team: [self.a_team, self.user],
109 self.b_team: [self.b_team, self.a_team, self.user],
110 self.c_team: [self.c_team, self.b_team, self.a_team, self.user]}
111 self.assertEqual(expected_paths, paths)
112
113 expected_memberships = [(self.a_team, self.user)]
114 memberships = [
115 (membership.team, membership.person) for membership
116 in memberships]
117 self.assertEqual(expected_memberships, memberships)
118
119 def test_getPathsToTeamsComplicated(self):
120 d_team = self.factory.makeTeam(name='d', owner=self.b_team)
121 e_team = self.factory.makeTeam(name='e')
122 f_team = self.factory.makeTeam(name='f', owner=e_team)
123 unrelated_team = self.factory.makeTeam(name='unrelated')
124 login_person(self.a_team.teamowner)
125 d_team.addMember(self.user, d_team.teamowner)
126 login_person(e_team.teamowner)
127 e_team.addMember(self.user, e_team.teamowner)
128
129 paths, memberships = self.user.getPathsToTeams()
130 expected_paths = {
131 self.a_team: [self.a_team, self.user],
132 self.b_team: [self.b_team, self.a_team, self.user],
133 self.c_team: [self.c_team, self.b_team, self.a_team, self.user],
134 d_team: [d_team, self.b_team, self.a_team, self.user],
135 e_team: [e_team, self.user],
136 f_team: [f_team, e_team, self.user]}
137 self.assertEqual(expected_paths, paths)
138
139 expected_memberships = [
140 (e_team, self.user),
141 (d_team, self.user),
142 (self.a_team, self.user),
143 ]
144 memberships = [
145 (membership.team, membership.person) for membership
146 in memberships]
147 self.assertEqual(expected_memberships, memberships)
148
149 def test_getPathsToTeamsMultiplePaths(self):
150 d_team = self.factory.makeTeam(name='d', owner=self.b_team)
151 login_person(self.a_team.teamowner)
152 self.c_team.addMember(d_team, self.c_team.teamowner)
153
154 paths, memberships = self.user.getPathsToTeams()
155 # getPathsToTeams should not randomly pick one path or another
156 # when multiples exist; it sorts to use the oldest path, so
157 # the expected paths below should be the returned result.
158 expected_paths = {
159 self.a_team: [self.a_team, self.user],
160 self.b_team: [self.b_team, self.a_team, self.user],
161 self.c_team: [self.c_team, self.b_team, self.a_team, self.user],
162 d_team: [d_team, self.b_team, self.a_team, self.user]}
163 self.assertEqual(expected_paths, paths)
164
165 expected_memberships = [(self.a_team, self.user)]
166 memberships = [
167 (membership.team, membership.person) for membership
168 in memberships]
169 self.assertEqual(expected_memberships, memberships)
170
83171
84class TestPerson(TestCaseWithFactory):172class TestPerson(TestCaseWithFactory):
85173
86174
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/vocabularies.py 2010-08-22 17:16:44 +0000
@@ -267,8 +267,7 @@
267 like_query = "'%%' || %s || '%%'" % quote_like(query)267 like_query = "'%%' || %s || '%%'" % quote_like(query)
268 fti_query = quote(query)268 fti_query = quote(query)
269 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (269 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
270 like_query, fti_query270 like_query, fti_query)
271 )
272 return self._table.select(sql, orderBy=self._orderBy)271 return self._table.select(sql, orderBy=self._orderBy)
273 return self.emptySelectResults()272 return self.emptySelectResults()
274273
@@ -315,8 +314,7 @@
315 like_query = "'%%' || %s || '%%'" % quote_like(query)314 like_query = "'%%' || %s || '%%'" % quote_like(query)
316 fti_query = quote(query)315 fti_query = quote(query)
317 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (316 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
318 like_query, fti_query317 like_query, fti_query)
319 )
320 return self._table.select(sql)318 return self._table.select(sql)
321 return self.emptySelectResults()319 return self.emptySelectResults()
322320
@@ -592,11 +590,8 @@
592 # Or a person who has an active account and a working590 # Or a person who has an active account and a working
593 # email address.591 # email address.
594 And(Account.status == AccountStatus.ACTIVE,592 And(Account.status == AccountStatus.ACTIVE,
595 EmailAddress.status.is_in(valid_email_statuses))593 EmailAddress.status.is_in(valid_email_statuses))),
596 ),594 self.extra_clause))
597 self.extra_clause
598 )
599 )
600 # The public query doesn't need to be ordered as it will be done595 # The public query doesn't need to be ordered as it will be done
601 # at the end.596 # at the end.
602 public_result.order_by()597 public_result.order_by()
@@ -836,7 +831,7 @@
836 def _get_teams(self):831 def _get_teams(self):
837 """The teams that the vocabulary is built from."""832 """The teams that the vocabulary is built from."""
838 return [membership.team for membership833 return [membership.team for membership
839 in self.context.myactivememberships834 in self.context.team_memberships
840 if membership.team.visibility == PersonVisibility.PUBLIC]835 if membership.team.visibility == PersonVisibility.PUBLIC]
841836
842 def __len__(self):837 def __len__(self):
@@ -1027,9 +1022,7 @@
1027 Milestone.q.productseriesID == ProductSeries.q.id,1022 Milestone.q.productseriesID == ProductSeries.q.id,
1028 ProductSeries.q.productID == Product.q.id,1023 ProductSeries.q.productID == Product.q.id,
1029 Product.q.name == productname,1024 Product.q.name == productname,
1030 ProductSeries.q.name == productseriesname1025 ProductSeries.q.name == productseriesname))
1031 )
1032 )
1033 try:1026 try:
1034 return self.toTerm(obj)1027 return self.toTerm(obj)
1035 except IndexError:1028 except IndexError:
@@ -1315,8 +1308,7 @@
1315 return [1308 return [
1316 project for project in sorted(projects,1309 project for project in sorted(projects,
1317 key=attrgetter('displayname'))1310 key=attrgetter('displayname'))
1318 if not project.qualifies_for_free_hosting1311 if not project.qualifies_for_free_hosting]
1319 ]
13201312
1321 def _doSearch(self, query=None):1313 def _doSearch(self, query=None):
1322 """Return terms where query is in the text of name1314 """Return terms where query is in the text of name
@@ -1454,11 +1446,8 @@
1454 Distribution.q.id == DistroSeries.q.distributionID,1446 Distribution.q.id == DistroSeries.q.distributionID,
1455 OR(1447 OR(
1456 CONTAINSSTRING(Distribution.q.name, query),1448 CONTAINSSTRING(Distribution.q.name, query),
1457 CONTAINSSTRING(DistroSeries.q.name, query)1449 CONTAINSSTRING(DistroSeries.q.name, query))),
1458 )1450 orderBy=self._orderBy)
1459 ),
1460 orderBy=self._orderBy
1461 )
1462 return objs1451 return objs
14631452
14641453
@@ -1472,8 +1461,7 @@
1472 """See `IVocabulary`."""1461 """See `IVocabulary`."""
1473 if IPillarName.providedBy(obj):1462 if IPillarName.providedBy(obj):
1474 assert obj.active, 'Inactive object %s %d' % (1463 assert obj.active, 'Inactive object %s %d' % (
1475 obj.__class__.__name__, obj.id1464 obj.__class__.__name__, obj.id)
1476 )
1477 obj = obj.pillar1465 obj = obj.pillar
14781466
1479 # It is a hack using the class name here, but it works1467 # It is a hack using the class name here, but it works