Merge lp:~jcsackett/launchpad/plus-participation-additional-fixes into lp:launchpad
- plus-participation-additional-fixes
- Merge into devel
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 | ||||
Related bugs: |
|
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 myactivemembers
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 TestPersonParti
On launchpad.dev, checkout https:/
lint
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
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/
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.
Robert Collins (lifeless) wrote : | # |
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:/
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:/
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_
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
graph_edges = list(team_
graph = dict(graph_edges)
graph.update(
# now render
...
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_
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_
graph = dict(team_
# now render
...
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.
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(
• 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.
I am marking this as Needs Information for now. When the IPerson interface questions are answered then we can approve this to land.
Maris
Māris Fogels (mars) wrote : | # |
Looking at the test commands, I see that only the TestPersonTeams and TestPersonParti
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/
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.
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.
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.
Edwin Grubbs (edwin-grubbs) wrote : | # |
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/
>--- lib/lp/
>+++ lib/lp/
>@@ -3078,31 +3078,34 @@
> def label(self):
> return 'Team participation for ' + self.context.
>
>- def _asParticipatio
>+ def _asParticipatio
> """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.
>+ 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.
>
> 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...
j.c.sackett (jcsackett) wrote : | # |
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/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -3078,31 +3078,34 @@
>> def label(self):
>> return 'Team participation for ' + self.context.
>>
>> - def _asParticipatio
>> + def _asParticipatio
>> """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.
>> + 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.
>>
>> 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 ...
j.c.sackett (jcsackett) wrote : | # |
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 _getDirectMembe
>> # never return None
>> assert self.hasPartici
>> @@ -1687,19 +1699,16 @@
>> self.invited_
>> orderBy=
>>
>> - # 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 myactivemembers
>> + def team_membership
>> """See `IPerson`."""
>> - return TeamMembership.
>> - TeamMembership.
>> - Person.id = TeamMembership.team
>> - """ % sqlvalues(self.id, [TeamMembership
>> - TeamMembershipS
>> - clauseTables=
>> - orderBy=
>> + Team = ClassAlias(Person, "Team")
>> + store = Store.of(self)
>> + return store.find(
>> + And(TeamMembers
>> + Team.id == TeamMembership.
>> + TeamMembership.
>> + ...
Edwin Grubbs (edwin-grubbs) wrote : | # |
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 = [
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 _getDirectMembe
> >> # never return None
> >> assert self.hasPartici
> >> @@ -1687,19 +1699,16 @@
> >> self.invited_
> >> orderBy=
> >>
> >> - # 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 myactivemembers
> >> + def team_membership
> >> """See `IPerson`."""
> >> - return TeamMembership.
> >> - TeamMembership.
> >> - Person.id = TeamMembership.team
> >> - """ % sqlvalues(self.id, [TeamMembership
> >> - TeamMembershipS
> >> - clauseTables=
> >> - orderBy=
> >> + Team = ClassAlias(Person, "Team")
> >> + store = Store.of(self)
> >> + return store.find(
> >> + And(TeamMembers
> >> + ...
Edwin Grubbs (edwin-grubbs) wrote : | # |
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/
--- lib/lp/
+++ lib/lp/
@@ -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.
else:
# The member is a direct member; use the membership data.
@@ -3133,8 +3133,8 @@
paths, memberships = self.context.
- team for team in paths.keys() if
- team not in direct_teams]
+ team for team in paths.keys()
+ if team not in direct_teams]
# First, create a participation for all direct memberships.
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -1712,7 +1712,6 @@
store = Store.of(self)
return store.find(
- Team.id == TeamMembership.
@@ -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.
store = Store.of(self)
@@ -2114,7 +2113,7 @@
- return (paths, user_memberships)
+ return (paths, user_memberships)
@property
def teams_participa
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -75,7 +75,7 @@
- def test_path_
+ def test_path_
path_to_a = self.user.
path_to_b = self.user.
path_to_c = self.user.
@@ -84,15 +84,6 @@
...
Edwin Grubbs (edwin-grubbs) : | # |
j.c.sackett (jcsackett) wrote : | # |
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -2203,6 +2203,10 @@
# Get all of the teams this person participates in.
teams = list(self.
+ # 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.
store = Store.of(self)
- TeamMembership.
- [team.id for team in teams] + [self.id]
- Desc(TeamMember
+ And(
+ TeamMembership.
+ [team.id for team in teams] + [self.id]),
+ TeamMembership.
+ TeamMembership.
+ TeamMembershipS
+ TeamMembershipS
+ ]))).order_by(
+ Desc(TeamMember
+ Desc(TeamMember
# Cast the results to list now, because they will be iterated over
# several times.
Robert Collins (lifeless) wrote : | # |
On Sun, Aug 22, 2010 at 4:46 PM, j.c.sackett
<email address hidden> wrote:
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -2203,6 +2203,10 @@
> # Get all of the teams this person participates in.
> teams = list(self.
>
> + # 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
Jelmer Vernooij (jelmer) wrote : | # |
+1 on the improvements after the ec2 test run.
Preview Diff
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 |
Could you explain why the indirect team path calculations were a problem?