Merge lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 11764
Proposed branch: lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout
Merge into: lp:launchpad
Diff against target: 185 lines (+66/-30)
2 files modified
lib/lp/registry/tests/test_person_vocabularies.py (+46/-0)
lib/lp/registry/vocabularies.py (+20/-30)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-655802-person-vocabulary-timeout
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+38905@code.launchpad.net

Description of the change

Summary
-------

Fixes soft timeouts when the AJAX Picker searches the
ValidPersonOrTeamVocabulary or its subclasses.

Implementation details
----------------------

Rewrote query to remove private_inner_select subquery, which sped up the
query. Don't check the Account table, since the person record is
considered broken if the account status is active, and it doesn't have a
preferred email address. The EmailAddress is actually queried twice.
First, to search all VALIDATED and PREFERRED email addresses for certain
text. Secondly, to verify that any non-team has a PREFERRED email.
    lib/lp/registry/vocabularies.py

Added test to verify that the extra_clause is getting applied to the
query in _doSearch() when that method is passed in a search term.
Previously, the the picker would allow you to select a private team
as a member of itself. Later, it would fail because the __contains__()
method constructs a different query without a search term.
    lib/lp/registry/tests/test_person_vocabularies.py

Tests
-----

./bin/test -vv -t test_person_vocabularies

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

* Open http://launchpad.dev/~launchpad-beta-testers
  * Click on the "Add member" and search for "John Arbash Meinel".
  * It should not timeout.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thanks for working on this. I think this is good to land, but I have a one question/remark. I do not understand why the test is on the LaunchpadFunctionalLayer? I see database objects, but no library objects in the test. I think the test will run on the DatabaseFunctionalLayer.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/registry/tests/test_person_vocabularies.py'
2--- lib/lp/registry/tests/test_person_vocabularies.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/registry/tests/test_person_vocabularies.py 2010-10-20 15:48:52 +0000
4@@ -0,0 +1,46 @@
5+# Copyright 2010 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Test the person vocabularies."""
9+
10+__metaclass__ = type
11+
12+from storm.store import Store
13+from zope.schema.vocabulary import getVocabularyRegistry
14+from zope.security.proxy import removeSecurityProxy
15+
16+from canonical.launchpad.ftests import login_person
17+from canonical.testing.layers import DatabaseFunctionalLayer
18+from lp.registry.interfaces.person import PersonVisibility
19+from lp.testing import TestCaseWithFactory
20+
21+
22+class TestValidTeamMemberVocabulary(TestCaseWithFactory):
23+ """Test that the ValidTeamMemberVocabulary behaves as expected."""
24+
25+ layer = DatabaseFunctionalLayer
26+
27+ def searchVocabulary(self, team, text):
28+ vocabulary_registry = getVocabularyRegistry()
29+ naked_vocabulary = removeSecurityProxy(
30+ vocabulary_registry.get(team, 'ValidTeamMember'))
31+ return naked_vocabulary._doSearch(text)
32+
33+ def test_public_team_cannot_be_a_member_of_itself(self):
34+ # A public team should be filtered by the vocab.extra_clause
35+ # when provided a search term.
36+ team_owner = self.factory.makePerson()
37+ login_person(team_owner)
38+ team = self.factory.makeTeam(owner=team_owner)
39+ Store.of(team).flush()
40+ self.assertFalse(team in self.searchVocabulary(team, team.name))
41+
42+ def test_private_team_cannot_be_a_member_of_itself(self):
43+ # A private team should be filtered by the vocab.extra_clause
44+ # when provided a search term.
45+ team_owner = self.factory.makePerson()
46+ login_person(team_owner)
47+ team = self.factory.makeTeam(
48+ owner=team_owner, visibility=PersonVisibility.PRIVATE)
49+ Store.of(team).flush()
50+ self.assertFalse(team in self.searchVocabulary(team, team.name))
51
52=== modified file 'lib/lp/registry/vocabularies.py'
53--- lib/lp/registry/vocabularies.py 2010-09-27 20:47:58 +0000
54+++ lib/lp/registry/vocabularies.py 2010-10-20 15:48:52 +0000
55@@ -94,11 +94,9 @@
56 SQLBase,
57 sqlvalues,
58 )
59-from canonical.launchpad.database.account import Account
60 from canonical.launchpad.database.emailaddress import EmailAddress
61 from canonical.launchpad.database.stormsugar import StartsWith
62 from canonical.launchpad.helpers import shortlist
63-from canonical.launchpad.interfaces.account import AccountStatus
64 from canonical.launchpad.interfaces.emailaddress import EmailAddressStatus
65 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
66 from canonical.launchpad.interfaces.lpstorm import IStore
67@@ -109,7 +107,6 @@
68 IStoreSelector,
69 MAIN_STORE,
70 )
71-from lp.app.browser.tales import DateTimeFormatterAPI
72 from canonical.launchpad.webapp.vocabulary import (
73 BatchedCountableIterator,
74 CountableIterator,
75@@ -118,6 +115,7 @@
76 NamedSQLObjectVocabulary,
77 SQLObjectVocabularyBase,
78 )
79+from lp.app.browser.tales import DateTimeFormatterAPI
80 from lp.blueprints.interfaces.specification import ISpecification
81 from lp.bugs.interfaces.bugtask import (
82 IBugTask,
83@@ -524,16 +522,11 @@
84 # TeamParticipation table, which is very expensive. The search
85 # for private teams does need that table but the number of private
86 # teams is very small so the cost is not great.
87- valid_email_statuses = (
88- EmailAddressStatus.VALIDATED,
89- EmailAddressStatus.PREFERRED,
90- )
91
92 # First search for public persons and teams that match the text.
93 public_tables = [
94 Person,
95 LeftJoin(EmailAddress, EmailAddress.person == Person.id),
96- LeftJoin(Account, EmailAddress.account == Account.id),
97 ]
98
99 # Create an inner query that will match public persons and teams
100@@ -565,10 +558,15 @@
101 FROM Person, EmailAddress
102 WHERE EmailAddress.person = Person.id
103 AND lower(email) LIKE ? || '%%'
104+ AND EmailAddress.status IN (?, ?)
105 ) AS public_subquery
106 ORDER BY rank DESC
107 LIMIT ?
108- """, (text, text, text, text, text, self.LIMIT))
109+ """, (text, text, text, text, text,
110+ EmailAddressStatus.VALIDATED.value,
111+ EmailAddressStatus.PREFERRED.value,
112+ self.LIMIT))
113+
114
115 public_result = self.store.using(*public_tables).find(
116 Person,
117@@ -579,12 +577,9 @@
118 Or(# A valid person-or-team is either a team...
119 # Note: 'Not' due to Bug 244768.
120 Not(Person.teamowner == None),
121-
122- # Or a person who has an active account and a working
123- # email address.
124- And(Account.status == AccountStatus.ACTIVE,
125- EmailAddress.status.is_in(valid_email_statuses))),
126- self.extra_clause))
127+ # Or a person who has a preferred email address.
128+ EmailAddress.status == EmailAddressStatus.PREFERRED),
129+ ))
130 # The public query doesn't need to be ordered as it will be done
131 # at the end.
132 public_result.order_by()
133@@ -596,33 +591,28 @@
134 # Searching for private teams that match can be easier since we
135 # are only interested in teams. Teams can have email addresses
136 # but we're electing to ignore them here.
137- private_inner_select = SQL("""
138- SELECT Person.id
139- FROM Person
140- WHERE Person.fti @@ ftq(?)
141- LIMIT ?
142- """, (text, self.LIMIT))
143 private_result = self.store.using(*private_tables).find(
144 Person,
145 And(
146- Person.id.is_in(private_inner_select),
147+ SQL('Person.fti @@ ftq(?)', [text]),
148 private_query,
149 )
150 )
151
152- # The private query doesn't need to be ordered as it will be done
153- # at the end, and we need to eliminate the default ordering.
154- private_result.order_by()
155+ private_result.order_by(SQL('rank(fti, ftq(?)) DESC', [text]))
156+ private_result.config(limit=self.LIMIT)
157
158 combined_result = public_result.union(private_result)
159 # Eliminate default ordering.
160 combined_result.order_by()
161 # XXX: BradCrittenden 2009-04-26 bug=217644: The use of Alias and
162- # is a work-around for .count() not working with the 'distinct'
163- # option.
164+ # _get_select() is a work-around for .count() not working
165+ # with the 'distinct' option.
166 subselect = Alias(combined_result._get_select(), 'Person')
167 exact_match = (Person.name == text)
168- result = self.store.using(subselect).find((Person, exact_match))
169+ result = self.store.using(subselect).find(
170+ (Person, exact_match),
171+ self.extra_clause)
172 # XXX: BradCrittenden 2009-05-07 bug=373228: A bug in Storm prevents
173 # setting the 'distinct' and 'limit' options in a single call to
174 # .config(). The work-around is to split them up. Note the limit has
175@@ -755,8 +745,8 @@
176 Person.id NOT IN (
177 SELECT team FROM TeamParticipation
178 WHERE person = %d
179- ) AND Person.id != %d
180- """ % (self.team.id, self.team.id)
181+ )
182+ """ % self.team.id
183
184
185 class ValidTeamOwnerVocabulary(ValidPersonOrTeamVocabulary):