Merge lp:~stevenk/launchpad/new-branch-search into lp:launchpad
- new-branch-search
- Merge into devel
Status: | Merged |
---|---|
Approved by: | William Grant |
Approved revision: | no longer in the source branch. |
Merged at revision: | 16492 |
Proposed branch: | lp:~stevenk/launchpad/new-branch-search |
Merge into: | lp:launchpad |
Diff against target: |
901 lines (+133/-386) 11 files modified
lib/lp/code/feed/branch.py (+5/-8) lib/lp/code/interfaces/branchcollection.py (+6/-12) lib/lp/code/model/branch.py (+4/-0) lib/lp/code/model/branchcollection.py (+39/-87) lib/lp/code/model/tests/test_branchcollection.py (+26/-96) lib/lp/code/stories/feeds/xx-branch-atom.txt (+5/-4) lib/lp/code/vocabularies/branch.py (+10/-35) lib/lp/code/vocabularies/tests/branch.txt (+0/-13) lib/lp/code/vocabularies/tests/test_branch_vocabularies.py (+36/-127) lib/lp/registry/model/product.py (+0/-2) lib/lp/services/webapp/configure.zcml (+2/-2) |
To merge this branch: | bzr merge lp:~stevenk/launchpad/new-branch-search |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+147840@code.launchpad.net |
Commit message
Rewrite branch search to not look through Product, Person and SourcePackageName, but only match against name or unique_name. Destroy (unused) IBranchCollecti
Description of the change
Destroy the current branch search paradigm. Unfortunately, now I can't finish this description since I've killed myself for using the word 'paradigm'.
IBranchCollecti
IBranchCollecti
I have refactored the branch vocabularies to no longer rely on a useless base
class, and cleaned up some whitespace and such.
Preview Diff
1 | === modified file 'lib/lp/code/feed/branch.py' |
2 | --- lib/lp/code/feed/branch.py 2011-12-30 07:32:58 +0000 |
3 | +++ lib/lp/code/feed/branch.py 2013-02-14 03:56:21 +0000 |
4 | @@ -31,6 +31,7 @@ |
5 | ) |
6 | from lp.code.interfaces.branchcollection import IAllBranches |
7 | from lp.code.interfaces.revisioncache import IRevisionCache |
8 | +from lp.code.model.branch import Branch |
9 | from lp.registry.interfaces.person import IPerson |
10 | from lp.registry.interfaces.product import IProduct |
11 | from lp.registry.interfaces.projectgroup import IProjectGroup |
12 | @@ -162,17 +163,13 @@ |
13 | |
14 | Only `self.quantity` revisions are returned. |
15 | """ |
16 | - from lp.code.model.branch import Branch |
17 | collection = self._getCollection().visibleByUser( |
18 | None).withLifecycleStatus(*DEFAULT_BRANCH_STATUS_IN_LISTING) |
19 | branches = collection.getBranches(eager_load=False) |
20 | - branches.order_by( |
21 | - Desc(Branch.date_last_modified), |
22 | - Asc(Branch.target_suffix), |
23 | - Desc(Branch.lifecycle_status), |
24 | - Asc(Branch.name)) |
25 | - branches.config(limit=self.quantity) |
26 | - return list(branches) |
27 | + return list(branches.order_by( |
28 | + Desc(Branch.date_last_modified), Asc(Branch.target_suffix), |
29 | + Desc(Branch.lifecycle_status), Asc(Branch.name)).config( |
30 | + limit=self.quantity)) |
31 | |
32 | |
33 | class ProductBranchFeed(BranchListingFeed): |
34 | |
35 | === modified file 'lib/lp/code/interfaces/branchcollection.py' |
36 | --- lib/lp/code/interfaces/branchcollection.py 2013-01-07 02:40:55 +0000 |
37 | +++ lib/lp/code/interfaces/branchcollection.py 2013-02-14 03:56:21 +0000 |
38 | @@ -1,4 +1,4 @@ |
39 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
40 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
41 | # GNU Affero General Public License version 3 (see the file LICENSE). |
42 | |
43 | """A collection of branches. |
44 | @@ -178,17 +178,11 @@ |
45 | def registeredBy(person): |
46 | """Restrict the collection to branches registered by 'person'.""" |
47 | |
48 | - def relatedTo(person): |
49 | - """Restrict the collection to branches related to 'person'. |
50 | - |
51 | - That is, branches that 'person' owns, registered or is subscribed to. |
52 | - """ |
53 | - |
54 | - def search(search_term): |
55 | - """Search the collection for branches matching 'search_term'. |
56 | - |
57 | - :param search_term: A string. |
58 | - :return: An `ICountableIterator`. |
59 | + def search(term): |
60 | + """Search the collection for branches matching 'term'. |
61 | + |
62 | + :param term: A string. |
63 | + :return: A `ResultSet` of branches that matched. |
64 | """ |
65 | |
66 | def scanned(): |
67 | |
68 | === modified file 'lib/lp/code/model/branch.py' |
69 | --- lib/lp/code/model/branch.py 2013-01-16 06:41:43 +0000 |
70 | +++ lib/lp/code/model/branch.py 2013-02-14 03:56:21 +0000 |
71 | @@ -147,6 +147,7 @@ |
72 | validate_person, |
73 | validate_public_person, |
74 | ) |
75 | +from lp.registry.interfaces.role import IPersonRoles |
76 | from lp.registry.interfaces.sharingjob import ( |
77 | IRemoveArtifactSubscriptionsJobSource, |
78 | ) |
79 | @@ -1630,6 +1631,9 @@ |
80 | if user is None: |
81 | return [public_branch_filter] |
82 | |
83 | + if IPersonRoles(user).in_admin: |
84 | + return [] |
85 | + |
86 | artifact_grant_query = Coalesce( |
87 | ArrayIntersects( |
88 | SQL('%s.access_grants' % branch_class.__storm_table__), |
89 | |
90 | === modified file 'lib/lp/code/model/branchcollection.py' |
91 | --- lib/lp/code/model/branchcollection.py 2012-10-18 19:06:48 +0000 |
92 | +++ lib/lp/code/model/branchcollection.py 2013-02-14 03:56:21 +0000 |
93 | @@ -1,4 +1,4 @@ |
94 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
95 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
96 | # GNU Affero General Public License version 3 (see the file LICENSE). |
97 | |
98 | """Implementations of `IBranchCollection`.""" |
99 | @@ -13,6 +13,10 @@ |
100 | from operator import attrgetter |
101 | |
102 | from lazr.restful.utils import safe_hasattr |
103 | +from lazr.uri import ( |
104 | + InvalidURIError, |
105 | + URI, |
106 | + ) |
107 | from storm.expr import ( |
108 | And, |
109 | Count, |
110 | @@ -20,10 +24,8 @@ |
111 | In, |
112 | Join, |
113 | LeftJoin, |
114 | - Or, |
115 | Select, |
116 | SQL, |
117 | - Union, |
118 | With, |
119 | ) |
120 | from storm.info import ClassAlias |
121 | @@ -31,10 +33,7 @@ |
122 | from zope.component import getUtility |
123 | from zope.interface import implements |
124 | |
125 | -from lp.app.enums import ( |
126 | - PRIVATE_INFORMATION_TYPES, |
127 | - PUBLIC_INFORMATION_TYPES, |
128 | - ) |
129 | +from lp.app.enums import PRIVATE_INFORMATION_TYPES |
130 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
131 | from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context |
132 | from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
133 | @@ -66,12 +65,8 @@ |
134 | from lp.registry.enums import EXCLUSIVE_TEAM_POLICY |
135 | from lp.registry.model.distribution import Distribution |
136 | from lp.registry.model.distroseries import DistroSeries |
137 | -from lp.registry.model.person import ( |
138 | - Owner, |
139 | - Person, |
140 | - ) |
141 | +from lp.registry.model.person import Person |
142 | from lp.registry.model.product import Product |
143 | -from lp.registry.model.sourcepackagename import SourcePackageName |
144 | from lp.registry.model.teammembership import TeamParticipation |
145 | from lp.services.database.bulk import ( |
146 | load_referencing, |
147 | @@ -87,7 +82,6 @@ |
148 | from lp.services.database.sqlbase import quote |
149 | from lp.services.propertycache import get_property_cache |
150 | from lp.services.searchbuilder import any |
151 | -from lp.services.webapp.vocabulary import CountableIterator |
152 | |
153 | |
154 | class GenericBranchCollection: |
155 | @@ -647,71 +641,35 @@ |
156 | """See `IBranchCollection`.""" |
157 | return self._filterBy([Branch.registrant == person], symmetric=False) |
158 | |
159 | - def relatedTo(self, person): |
160 | - """See `IBranchCollection`.""" |
161 | - return self._filterBy( |
162 | - [Branch.id.is_in( |
163 | - Union( |
164 | - Select(Branch.id, Branch.owner == person), |
165 | - Select(Branch.id, Branch.registrant == person), |
166 | - Select(Branch.id, |
167 | - And(BranchSubscription.person == person, |
168 | - BranchSubscription.branch == Branch.id))))], |
169 | - symmetric=False) |
170 | - |
171 | - def _getExactMatch(self, search_term): |
172 | - """Return the exact branch that 'search_term' matches, or None.""" |
173 | - search_term = search_term.rstrip('/') |
174 | - branch_set = getUtility(IBranchLookup) |
175 | - branch = branch_set.getByUniqueName(search_term) |
176 | - if branch is None: |
177 | - branch = branch_set.getByUrl(search_term) |
178 | - return branch |
179 | - |
180 | - def search(self, search_term): |
181 | - """See `IBranchCollection`.""" |
182 | - # XXX: JonathanLange 2009-02-23 bug 372591: This matches the old |
183 | - # search algorithm that used to live in vocabularies/dbojects.py. It's |
184 | - # not actually very good -- really it should match based on substrings |
185 | - # of the unique name and sort based on relevance. |
186 | - branch = self._getExactMatch(search_term) |
187 | - if branch is not None: |
188 | - if branch in self.getBranches(eager_load=False): |
189 | - return CountableIterator(1, [branch]) |
190 | - else: |
191 | - return CountableIterator(0, []) |
192 | - like_term = '%' + search_term + '%' |
193 | - # Match the Branch name or the URL. |
194 | - queries = [Select(Branch.id, |
195 | - Or(Branch.name.like(like_term), |
196 | - Branch.url == search_term))] |
197 | - # Match the product name. |
198 | - if 'product' not in self._exclude_from_search: |
199 | - queries.append(Select( |
200 | - Branch.id, |
201 | - And(Branch.product == Product.id, |
202 | - Product.name.like(like_term)))) |
203 | - |
204 | - # Match the owner name. |
205 | - queries.append(Select( |
206 | - Branch.id, |
207 | - And(Branch.owner == Owner.id, Owner.name.like(like_term)))) |
208 | - |
209 | - # Match the package bits. |
210 | - queries.append( |
211 | - Select(Branch.id, |
212 | - And(Branch.sourcepackagename == SourcePackageName.id, |
213 | - Branch.distroseries == DistroSeries.id, |
214 | - DistroSeries.distribution == Distribution.id, |
215 | - Or(SourcePackageName.name.like(like_term), |
216 | - DistroSeries.name.like(like_term), |
217 | - Distribution.name.like(like_term))))) |
218 | - |
219 | - # Get the results. |
220 | - collection = self._filterBy([Branch.id.is_in(Union(*queries))]) |
221 | - results = collection.getBranches(eager_load=False).order_by( |
222 | + def _getExactMatch(self, term): |
223 | + # Try and look up the branch by its URL, which handles lp: shortfom. |
224 | + branch_url = getUtility(IBranchLookup).getByUrl(term) |
225 | + if branch_url: |
226 | + return branch_url |
227 | + # Fall back to searching by unique_name, stripping out the path if it's |
228 | + # a URI. |
229 | + try: |
230 | + path = URI(term).path.strip('/') |
231 | + except InvalidURIError: |
232 | + path = term |
233 | + return getUtility(IBranchLookup).getByUniqueName(path) |
234 | + |
235 | + def search(self, term): |
236 | + """See `IBranchCollection`.""" |
237 | + branch = self._getExactMatch(term) |
238 | + if branch: |
239 | + collection = self._filterBy([Branch.id == branch.id]) |
240 | + else: |
241 | + term = unicode(term) |
242 | + # Filter by name. |
243 | + field = Branch.name |
244 | + # Except if the term contains /, when we use unique_name. |
245 | + if '/' in term: |
246 | + field = Branch.unique_name |
247 | + collection = self._filterBy( |
248 | + [field.lower().contains_string(term.lower())]) |
249 | + return collection.getBranches(eager_load=False).order_by( |
250 | Branch.name, Branch.id) |
251 | - return CountableIterator(results.count(), results) |
252 | |
253 | def scanned(self): |
254 | """See `IBranchCollection`.""" |
255 | @@ -790,8 +748,7 @@ |
256 | |
257 | def _getBranchVisibilityExpression(self, branch_class=Branch): |
258 | """Return the where clauses for visibility.""" |
259 | - return [ |
260 | - branch_class.information_type.is_in(PUBLIC_INFORMATION_TYPES)] |
261 | + return get_branch_privacy_filter(None, branch_class=branch_class) |
262 | |
263 | |
264 | class VisibleBranchCollection(GenericBranchCollection): |
265 | @@ -839,13 +796,9 @@ |
266 | if exclude_from_search is None: |
267 | exclude_from_search = [] |
268 | return self.__class__( |
269 | - self._user, |
270 | - self.store, |
271 | - symmetric_expr, |
272 | - tables, |
273 | + self._user, self.store, symmetric_expr, tables, |
274 | self._exclude_from_search + exclude_from_search, |
275 | - asymmetric_expr, |
276 | - asymmetric_tables) |
277 | + asymmetric_expr, asymmetric_tables) |
278 | |
279 | def _getBranchVisibilityExpression(self, branch_class=Branch): |
280 | """Return the where clauses for visibility. |
281 | @@ -853,8 +806,7 @@ |
282 | :param branch_class: The Branch class to use - permits using |
283 | ClassAliases. |
284 | """ |
285 | - return get_branch_privacy_filter( |
286 | - self._user, branch_class=branch_class) |
287 | + return get_branch_privacy_filter(self._user, branch_class=branch_class) |
288 | |
289 | def visibleByUser(self, person): |
290 | """See `IBranchCollection`.""" |
291 | |
292 | === modified file 'lib/lp/code/model/tests/test_branchcollection.py' |
293 | --- lib/lp/code/model/tests/test_branchcollection.py 2012-10-18 19:20:49 +0000 |
294 | +++ lib/lp/code/model/tests/test_branchcollection.py 2013-02-14 03:56:21 +0000 |
295 | @@ -1,4 +1,4 @@ |
296 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
297 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
298 | # GNU Affero General Public License version 3 (see the file LICENSE). |
299 | |
300 | """Tests for branch collections.""" |
301 | @@ -41,6 +41,7 @@ |
302 | IStoreSelector, |
303 | MAIN_STORE, |
304 | ) |
305 | +from lp.services.webapp.publisher import canonical_url |
306 | from lp.testing import ( |
307 | person_logged_in, |
308 | run_with_login, |
309 | @@ -560,34 +561,6 @@ |
310 | collection = self.all_branches.subscribedBy(subscriber) |
311 | self.assertEqual([branch], list(collection.getBranches())) |
312 | |
313 | - def test_relatedTo(self): |
314 | - # 'relatedTo' returns a collection that has all branches that a user |
315 | - # owns, is subscribed to or registered. No other branches are in this |
316 | - # collection. |
317 | - person = self.factory.makePerson() |
318 | - team = self.factory.makeTeam(person) |
319 | - owned_branch = self.factory.makeAnyBranch(owner=person) |
320 | - # Unsubscribe the owner, to demonstrate that we show owned branches |
321 | - # even if they aren't subscribed. |
322 | - owned_branch.unsubscribe(person, person) |
323 | - # Subscribe two other people to the owned branch to make sure |
324 | - # that the BranchSubscription join is doing it right. |
325 | - self.factory.makeBranchSubscription(branch=owned_branch) |
326 | - self.factory.makeBranchSubscription(branch=owned_branch) |
327 | - |
328 | - registered_branch = self.factory.makeAnyBranch( |
329 | - owner=team, registrant=person) |
330 | - subscribed_branch = self.factory.makeAnyBranch() |
331 | - subscribed_branch.subscribe( |
332 | - person, BranchSubscriptionNotificationLevel.NOEMAIL, |
333 | - BranchSubscriptionDiffSize.NODIFF, |
334 | - CodeReviewNotificationLevel.NOEMAIL, |
335 | - person) |
336 | - related_branches = self.all_branches.relatedTo(person) |
337 | - self.assertEqual( |
338 | - sorted([owned_branch, registered_branch, subscribed_branch]), |
339 | - sorted(related_branches.getBranches())) |
340 | - |
341 | def test_withBranchType(self): |
342 | hosted_branch1 = self.factory.makeAnyBranch( |
343 | branch_type=BranchType.HOSTED) |
344 | @@ -1161,6 +1134,21 @@ |
345 | search_results = self.collection.search(branch.codebrowse_url()) |
346 | self.assertEqual([branch], list(search_results)) |
347 | |
348 | + def test_exact_match_with_lp_colon_url(self): |
349 | + branch = self.factory.makeBranch() |
350 | + lp_name = 'lp://dev/' + branch.unique_name |
351 | + search_results = self.collection.search(lp_name) |
352 | + self.assertEqual([branch], list(search_results)) |
353 | + |
354 | + def test_exact_match_full_url(self): |
355 | + branch = self.factory.makeBranch() |
356 | + url = canonical_url(branch) |
357 | + self.assertEqual([branch], list(self.collection.search(url))) |
358 | + |
359 | + def test_exact_match_bad_url(self): |
360 | + search_results = self.collection.search('http:hahafail') |
361 | + self.assertEqual([], list(search_results)) |
362 | + |
363 | def test_exact_match_bzr_identity(self): |
364 | # If you search for the bzr identity of a branch, then you get a |
365 | # single result with that branch. |
366 | @@ -1214,6 +1202,12 @@ |
367 | search_results = self.collection.search('foo') |
368 | self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
369 | |
370 | + def test_match_against_unique_name(self): |
371 | + branch = self.factory.makeAnyBranch(name='fooa') |
372 | + search_term = branch.product.name + '/foo' |
373 | + search_results = self.collection.search(search_term) |
374 | + self.assertEqual([branch], list(search_results)) |
375 | + |
376 | def test_match_sub_branch_name(self): |
377 | # search returns all branches which have a name of which the search |
378 | # term is a substring. |
379 | @@ -1223,73 +1217,9 @@ |
380 | search_results = self.collection.search('foo') |
381 | self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
382 | |
383 | - def test_match_exact_owner_name(self): |
384 | - # search returns all branches which have an owner with a name matching |
385 | - # the server. |
386 | - person = self.factory.makePerson(name='foo') |
387 | - branch1 = self.factory.makeAnyBranch(owner=person) |
388 | - branch2 = self.factory.makeAnyBranch(owner=person) |
389 | - self.factory.makeAnyBranch() |
390 | - search_results = self.collection.search('foo') |
391 | - self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
392 | - |
393 | - def test_match_sub_owner_name(self): |
394 | - # search returns all branches that have an owner name where the search |
395 | - # term is a substring of the owner name. |
396 | - person1 = self.factory.makePerson(name='foom') |
397 | - branch1 = self.factory.makeAnyBranch(owner=person1) |
398 | - person2 = self.factory.makePerson(name='afoo') |
399 | - branch2 = self.factory.makeAnyBranch(owner=person2) |
400 | - self.factory.makeAnyBranch() |
401 | - search_results = self.collection.search('foo') |
402 | - self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
403 | - |
404 | - def test_match_exact_product_name(self): |
405 | - # search returns all branches that have a product name where the |
406 | - # product name is the same as the search term. |
407 | - product = self.factory.makeProduct(name='foo') |
408 | - branch1 = self.factory.makeAnyBranch(product=product) |
409 | - branch2 = self.factory.makeAnyBranch(product=product) |
410 | - self.factory.makeAnyBranch() |
411 | - search_results = self.collection.search('foo') |
412 | - self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
413 | - |
414 | - def test_match_sub_product_name(self): |
415 | - # search returns all branches that have a product name where the |
416 | - # search terms is a substring of the product name. |
417 | - product1 = self.factory.makeProduct(name='foom') |
418 | - branch1 = self.factory.makeProductBranch(product=product1) |
419 | - product2 = self.factory.makeProduct(name='afoo') |
420 | - branch2 = self.factory.makeProductBranch(product=product2) |
421 | - self.factory.makeAnyBranch() |
422 | - search_results = self.collection.search('foo') |
423 | - self.assertEqual(sorted([branch1, branch2]), sorted(search_results)) |
424 | - |
425 | - def test_match_sub_distro_name(self): |
426 | - # search returns all branches that have a distro name where the search |
427 | - # term is a substring of the distro name. |
428 | - branch = self.factory.makePackageBranch() |
429 | - self.factory.makeAnyBranch() |
430 | - search_term = branch.distribution.name[1:] |
431 | - search_results = self.collection.search(search_term) |
432 | - self.assertEqual([branch], list(search_results)) |
433 | - |
434 | - def test_match_sub_distroseries_name(self): |
435 | - # search returns all branches that have a distro series with a name |
436 | - # that the search term is a substring of. |
437 | - branch = self.factory.makePackageBranch() |
438 | - self.factory.makeAnyBranch() |
439 | - search_term = branch.distroseries.name[1:] |
440 | - search_results = self.collection.search(search_term) |
441 | - self.assertEqual([branch], list(search_results)) |
442 | - |
443 | - def test_match_sub_sourcepackage_name(self): |
444 | - # search returns all branches that have a source package with a name |
445 | - # that contains the search term. |
446 | - branch = self.factory.makePackageBranch() |
447 | - self.factory.makeAnyBranch() |
448 | - search_term = branch.sourcepackagename.name[1:] |
449 | - search_results = self.collection.search(search_term) |
450 | + def test_match_ignores_case(self): |
451 | + branch = self.factory.makeAnyBranch(name='foobar') |
452 | + search_results = self.collection.search('FOOBAR') |
453 | self.assertEqual([branch], list(search_results)) |
454 | |
455 | def test_dont_match_product_if_in_product(self): |
456 | |
457 | === modified file 'lib/lp/code/stories/feeds/xx-branch-atom.txt' |
458 | --- lib/lp/code/stories/feeds/xx-branch-atom.txt 2012-07-05 04:59:52 +0000 |
459 | +++ lib/lp/code/stories/feeds/xx-branch-atom.txt 2013-02-14 03:56:21 +0000 |
460 | @@ -99,16 +99,17 @@ |
461 | |
462 | If a branch is marked private it will not be displayed. The Landscape |
463 | developers team has two branches which are both private. |
464 | + |
465 | >>> from zope.component import getUtility |
466 | >>> from zope.security.proxy import removeSecurityProxy |
467 | >>> from lp.code.model.branch import Branch |
468 | >>> from lp.code.interfaces.branchcollection import IAllBranches |
469 | >>> from lp.registry.interfaces.person import IPersonSet |
470 | + >>> from lp.registry.interfaces.product import IProductSet |
471 | >>> login(ANONYMOUS) |
472 | - >>> person_set = getUtility(IPersonSet) |
473 | - >>> landscape_team = person_set.getByName('landscape-developers') |
474 | - >>> test_user = person_set.getByEmail('test@canonical.com') |
475 | - >>> branches = getUtility(IAllBranches).relatedTo(landscape_team) |
476 | + >>> test_user = getUtility(IPersonSet).getByEmail('test@canonical.com') |
477 | + >>> landscape = getUtility(IProductSet)['landscape'] |
478 | + >>> branches = getUtility(IAllBranches).inProduct(landscape) |
479 | >>> branches = branches.visibleByUser( |
480 | ... test_user).getBranches().order_by(Branch.id) |
481 | >>> for branch in branches: |
482 | |
483 | === modified file 'lib/lp/code/vocabularies/branch.py' |
484 | --- lib/lp/code/vocabularies/branch.py 2012-10-18 17:49:39 +0000 |
485 | +++ lib/lp/code/vocabularies/branch.py 2013-02-14 03:56:21 +0000 |
486 | @@ -1,9 +1,8 @@ |
487 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
488 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
489 | # GNU Affero General Public License version 3 (see the file LICENSE). |
490 | |
491 | """Vocabularies that contain branches.""" |
492 | |
493 | - |
494 | __metaclass__ = type |
495 | |
496 | __all__ = [ |
497 | @@ -31,12 +30,8 @@ |
498 | ) |
499 | |
500 | |
501 | -class BranchVocabularyBase(SQLObjectVocabularyBase): |
502 | - """A base class for Branch vocabularies. |
503 | - |
504 | - Override `BranchVocabularyBase._getCollection` to provide the collection |
505 | - of branches which make up the vocabulary. |
506 | - """ |
507 | +class BranchVocabulary(SQLObjectVocabularyBase): |
508 | + """A vocabulary for searching branches.""" |
509 | |
510 | implements(IHugeVocabulary) |
511 | |
512 | @@ -56,17 +51,10 @@ |
513 | return iter(search_results).next() |
514 | raise LookupError(token) |
515 | |
516 | - def _getCollection(self): |
517 | - """Return the collection of branches the vocabulary searches. |
518 | - |
519 | - Subclasses MUST override and implement this. |
520 | - """ |
521 | - raise NotImplementedError(self._getCollection) |
522 | - |
523 | def searchForTerms(self, query=None, vocab_filter=None): |
524 | """See `IHugeVocabulary`.""" |
525 | - logged_in_user = getUtility(ILaunchBag).user |
526 | - collection = self._getCollection().visibleByUser(logged_in_user) |
527 | + user = getUtility(ILaunchBag).user |
528 | + collection = self._getCollection().visibleByUser(user) |
529 | if query is None: |
530 | branches = collection.getBranches(eager_load=False) |
531 | else: |
532 | @@ -77,28 +65,15 @@ |
533 | """See `IVocabulary`.""" |
534 | return self.search().count() |
535 | |
536 | - |
537 | -class BranchVocabulary(BranchVocabularyBase): |
538 | - """A vocabulary for searching branches. |
539 | - |
540 | - The name and URL of the branch, the name of the product, and the |
541 | - name of the registrant of the branches is checked for the entered |
542 | - value. |
543 | - """ |
544 | - |
545 | def _getCollection(self): |
546 | return getUtility(IAllBranches) |
547 | |
548 | |
549 | -class BranchRestrictedOnProductVocabulary(BranchVocabularyBase): |
550 | - """A vocabulary for searching branches restricted on product. |
551 | - |
552 | - The query entered checks the name or URL of the branch, or the |
553 | - name of the registrant of the branch. |
554 | - """ |
555 | +class BranchRestrictedOnProductVocabulary(BranchVocabulary): |
556 | + """A vocabulary for searching branches restricted on product.""" |
557 | |
558 | def __init__(self, context=None): |
559 | - BranchVocabularyBase.__init__(self, context) |
560 | + super(BranchRestrictedOnProductVocabulary, self).__init__(context) |
561 | if IProduct.providedBy(self.context): |
562 | self.product = self.context |
563 | elif IProductSeries.providedBy(self.context): |
564 | @@ -113,7 +88,7 @@ |
565 | return getUtility(IAllBranches).inProduct(self.product).isExclusive() |
566 | |
567 | |
568 | -class HostedBranchRestrictedOnOwnerVocabulary(BranchVocabularyBase): |
569 | +class HostedBranchRestrictedOnOwnerVocabulary(BranchVocabulary): |
570 | """A vocabulary for hosted branches owned by the current user. |
571 | |
572 | These are branches that the user either owns themselves or which are |
573 | @@ -124,7 +99,7 @@ |
574 | """Pass a Person as context, or anything else for the current user.""" |
575 | super(HostedBranchRestrictedOnOwnerVocabulary, self).__init__(context) |
576 | if IPerson.providedBy(self.context): |
577 | - self.user = context |
578 | + self.user = self.context |
579 | else: |
580 | self.user = getUtility(ILaunchBag).user |
581 | |
582 | |
583 | === modified file 'lib/lp/code/vocabularies/tests/branch.txt' |
584 | --- lib/lp/code/vocabularies/tests/branch.txt 2012-04-30 13:53:06 +0000 |
585 | +++ lib/lp/code/vocabularies/tests/branch.txt 2013-02-14 03:56:21 +0000 |
586 | @@ -32,16 +32,6 @@ |
587 | ~stevea/thunderbird/main |
588 | ~vcs-imports/evolution/main |
589 | |
590 | - >>> print_vocab_branches(branch_vocabulary, 'vcs-imports') |
591 | - ~vcs-imports/evolution/import |
592 | - ~vcs-imports/evolution/main |
593 | - ~vcs-imports/gnome-terminal/import |
594 | - |
595 | - >>> print_vocab_branches(branch_vocabulary, 'evolution') |
596 | - ~carlos/evolution/2.0 |
597 | - ~vcs-imports/evolution/import |
598 | - ~vcs-imports/evolution/main |
599 | - |
600 | A search with the full branch unique name should also find the branch. |
601 | |
602 | >>> print_vocab_branches(branch_vocabulary, '~name12/firefox/main') |
603 | @@ -107,9 +97,6 @@ |
604 | >>> print_vocab_branches(branch_vocabulary, 'main') |
605 | ~name12/gnome-terminal/main |
606 | |
607 | - >>> print_vocab_branches(branch_vocabulary, 'vcs-imports') |
608 | - ~vcs-imports/gnome-terminal/import |
609 | - |
610 | If a full unique name is entered that has a different product, the |
611 | branch is not part of the vocabulary. |
612 | |
613 | |
614 | === modified file 'lib/lp/code/vocabularies/tests/test_branch_vocabularies.py' |
615 | --- lib/lp/code/vocabularies/tests/test_branch_vocabularies.py 2012-10-18 17:49:39 +0000 |
616 | +++ lib/lp/code/vocabularies/tests/test_branch_vocabularies.py 2013-02-14 03:56:21 +0000 |
617 | @@ -1,11 +1,10 @@ |
618 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
619 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
620 | # GNU Affero General Public License version 3 (see the file LICENSE). |
621 | |
622 | """Test the branch vocabularies.""" |
623 | |
624 | __metaclass__ = type |
625 | |
626 | -from unittest import TestCase |
627 | |
628 | from zope.component import getUtility |
629 | |
630 | @@ -16,99 +15,36 @@ |
631 | ) |
632 | from lp.registry.enums import TeamMembershipPolicy |
633 | from lp.registry.interfaces.product import IProductSet |
634 | -from lp.testing import ( |
635 | - ANONYMOUS, |
636 | - login, |
637 | - logout, |
638 | - ) |
639 | -from lp.testing.factory import LaunchpadObjectFactory |
640 | +from lp.testing import TestCaseWithFactory |
641 | from lp.testing.layers import DatabaseFunctionalLayer |
642 | |
643 | |
644 | -class BranchVocabTestCase(TestCase): |
645 | - """A base class for the branch vocabulary test cases.""" |
646 | - layer = DatabaseFunctionalLayer |
647 | - |
648 | - def setUp(self): |
649 | - # Set up the anonymous security interaction. |
650 | - login(ANONYMOUS) |
651 | - |
652 | - def tearDown(self): |
653 | - logout() |
654 | - |
655 | - |
656 | -class TestBranchVocabulary(BranchVocabTestCase): |
657 | +class TestBranchVocabulary(TestCaseWithFactory): |
658 | """Test that the BranchVocabulary behaves as expected.""" |
659 | |
660 | + layer = DatabaseFunctionalLayer |
661 | + |
662 | def setUp(self): |
663 | - BranchVocabTestCase.setUp(self) |
664 | + super(TestBranchVocabulary, self).setUp() |
665 | self._createBranches() |
666 | self.vocab = BranchVocabulary(context=None) |
667 | |
668 | def _createBranches(self): |
669 | - factory = LaunchpadObjectFactory() |
670 | - widget = factory.makeProduct(name='widget') |
671 | - sprocket = factory.makeProduct(name='sprocket') |
672 | - # Scotty's branches. |
673 | - scotty = factory.makePerson(name='scotty') |
674 | - factory.makeProductBranch( |
675 | + widget = self.factory.makeProduct(name='widget') |
676 | + sprocket = self.factory.makeProduct(name='sprocket') |
677 | + scotty = self.factory.makePerson(name='scotty') |
678 | + self.factory.makeProductBranch( |
679 | owner=scotty, product=widget, name='fizzbuzz') |
680 | - factory.makeProductBranch( |
681 | + self.factory.makeProductBranch( |
682 | owner=scotty, product=widget, name='mountain') |
683 | - factory.makeProductBranch( |
684 | + self.factory.makeProductBranch( |
685 | owner=scotty, product=sprocket, name='fizzbuzz') |
686 | - # Spotty's branches. |
687 | - spotty = factory.makePerson(name='spotty') |
688 | - factory.makeProductBranch( |
689 | - owner=spotty, product=widget, name='hill') |
690 | - factory.makeProductBranch( |
691 | - owner=spotty, product=widget, name='sprocket') |
692 | - # Sprocket's branches. |
693 | - sprocket_person = factory.makePerson(name='sprocket') |
694 | - factory.makeProductBranch( |
695 | - owner=sprocket_person, product=widget, name='foo') |
696 | |
697 | def test_fizzbuzzBranches(self): |
698 | """Return branches that match the string 'fizzbuzz'.""" |
699 | results = self.vocab.searchForTerms('fizzbuzz') |
700 | expected = [ |
701 | - u'~scotty/sprocket/fizzbuzz', |
702 | - u'~scotty/widget/fizzbuzz', |
703 | - ] |
704 | - branch_names = sorted([branch.token for branch in results]) |
705 | - self.assertEqual(expected, branch_names) |
706 | - |
707 | - def test_widgetBranches(self): |
708 | - """Searches match the product name too.""" |
709 | - results = self.vocab.searchForTerms('widget') |
710 | - expected = [ |
711 | - u'~scotty/widget/fizzbuzz', |
712 | - u'~scotty/widget/mountain', |
713 | - u'~spotty/widget/hill', |
714 | - u'~spotty/widget/sprocket', |
715 | - u'~sprocket/widget/foo', |
716 | - ] |
717 | - branch_names = sorted([branch.token for branch in results]) |
718 | - self.assertEqual(expected, branch_names) |
719 | - |
720 | - def test_spottyBranches(self): |
721 | - """Searches also match the registrant name.""" |
722 | - results = self.vocab.searchForTerms('spotty') |
723 | - expected = [ |
724 | - u'~spotty/widget/hill', |
725 | - u'~spotty/widget/sprocket', |
726 | - ] |
727 | - branch_names = sorted([branch.token for branch in results]) |
728 | - self.assertEqual(expected, branch_names) |
729 | - |
730 | - def test_crossAttributeBranches(self): |
731 | - """The search checks name, product, and person.""" |
732 | - results = self.vocab.searchForTerms('rocket') |
733 | - expected = [ |
734 | - u'~scotty/sprocket/fizzbuzz', |
735 | - u'~spotty/widget/sprocket', |
736 | - u'~sprocket/widget/foo', |
737 | - ] |
738 | + u'~scotty/sprocket/fizzbuzz', u'~scotty/widget/fizzbuzz'] |
739 | branch_names = sorted([branch.token for branch in results]) |
740 | self.assertEqual(expected, branch_names) |
741 | |
742 | @@ -116,20 +52,15 @@ |
743 | # If there is a single search result that matches, use that |
744 | # as the result. |
745 | term = self.vocab.getTermByToken('mountain') |
746 | - self.assertEqual( |
747 | - '~scotty/widget/mountain', |
748 | - term.value.unique_name) |
749 | + self.assertEqual('~scotty/widget/mountain', term.value.unique_name) |
750 | |
751 | def test_multipleQueryResult(self): |
752 | # If there are more than one search result, a LookupError is still |
753 | # raised. |
754 | - self.assertRaises( |
755 | - LookupError, |
756 | - self.vocab.getTermByToken, |
757 | - 'fizzbuzz') |
758 | - |
759 | - |
760 | -class TestRestrictedBranchVocabularyOnProduct(BranchVocabTestCase): |
761 | + self.assertRaises(LookupError, self.vocab.getTermByToken, 'fizzbuzz') |
762 | + |
763 | + |
764 | +class TestRestrictedBranchVocabularyOnProduct(TestCaseWithFactory): |
765 | """Test the BranchRestrictedOnProductVocabulary behaves as expected. |
766 | |
767 | When a BranchRestrictedOnProductVocabulary is used with a product the |
768 | @@ -137,8 +68,10 @@ |
769 | context. |
770 | """ |
771 | |
772 | + layer = DatabaseFunctionalLayer |
773 | + |
774 | def setUp(self): |
775 | - BranchVocabTestCase.setUp(self) |
776 | + super(TestRestrictedBranchVocabularyOnProduct, self).setUp() |
777 | self._createBranches() |
778 | self.vocab = BranchRestrictedOnProductVocabulary( |
779 | context=self._getVocabRestriction()) |
780 | @@ -148,18 +81,17 @@ |
781 | return getUtility(IProductSet).getByName('widget') |
782 | |
783 | def _createBranches(self): |
784 | - factory = LaunchpadObjectFactory() |
785 | - test_product = factory.makeProduct(name='widget') |
786 | - other_product = factory.makeProduct(name='sprocket') |
787 | - person = factory.makePerson(name='scotty') |
788 | - factory.makeProductBranch( |
789 | + test_product = self.factory.makeProduct(name='widget') |
790 | + other_product = self.factory.makeProduct(name='sprocket') |
791 | + person = self.factory.makePerson(name='scotty') |
792 | + self.factory.makeProductBranch( |
793 | owner=person, product=test_product, name='main') |
794 | - factory.makeProductBranch( |
795 | + self.factory.makeProductBranch( |
796 | owner=person, product=test_product, name='mountain') |
797 | - factory.makeProductBranch( |
798 | + self.factory.makeProductBranch( |
799 | owner=person, product=other_product, name='main') |
800 | - person = factory.makePerson(name='spotty') |
801 | - factory.makeProductBranch( |
802 | + person = self.factory.makePerson(name='spotty') |
803 | + self.factory.makeProductBranch( |
804 | owner=person, product=test_product, name='hill') |
805 | self.product = test_product |
806 | |
807 | @@ -169,23 +101,7 @@ |
808 | The result set should not show ~scotty/sprocket/main. |
809 | """ |
810 | results = self.vocab.searchForTerms('main') |
811 | - expected = [ |
812 | - u'~scotty/widget/main', |
813 | - ] |
814 | - branch_names = sorted([branch.token for branch in results]) |
815 | - self.assertEqual(expected, branch_names) |
816 | - |
817 | - def test_ownersBranches(self): |
818 | - """Look for branches owned by scotty. |
819 | - |
820 | - The result set should not show ~scotty/sprocket/main. |
821 | - """ |
822 | - results = self.vocab.searchForTerms('scotty') |
823 | - |
824 | - expected = [ |
825 | - u'~scotty/widget/main', |
826 | - u'~scotty/widget/mountain', |
827 | - ] |
828 | + expected = [u'~scotty/widget/main'] |
829 | branch_names = sorted([branch.token for branch in results]) |
830 | self.assertEqual(expected, branch_names) |
831 | |
832 | @@ -193,26 +109,20 @@ |
833 | # If there is a single search result that matches, use that |
834 | # as the result. |
835 | term = self.vocab.getTermByToken('mountain') |
836 | - self.assertEqual( |
837 | - '~scotty/widget/mountain', |
838 | - term.value.unique_name) |
839 | + self.assertEqual('~scotty/widget/mountain', term.value.unique_name) |
840 | |
841 | def test_multipleQueryResult(self): |
842 | # If there are more than one search result, a LookupError is still |
843 | # raised. |
844 | - self.assertRaises( |
845 | - LookupError, |
846 | - self.vocab.getTermByToken, |
847 | - 'scotty') |
848 | + self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty') |
849 | |
850 | def test_does_not_contain_inclusive_teams(self): |
851 | - factory = LaunchpadObjectFactory() |
852 | - open_team = factory.makeTeam(name='open-team', |
853 | + open_team = self.factory.makeTeam(name='open-team', |
854 | membership_policy=TeamMembershipPolicy.OPEN) |
855 | - delegated_team = factory.makeTeam(name='delegated-team', |
856 | + delegated_team = self.factory.makeTeam(name='delegated-team', |
857 | membership_policy=TeamMembershipPolicy.DELEGATED) |
858 | for team in [open_team, delegated_team]: |
859 | - factory.makeProductBranch( |
860 | + self.factory.makeProductBranch( |
861 | owner=team, product=self.product, name='mountain') |
862 | results = self.vocab.searchForTerms('mountain') |
863 | branch_names = sorted([branch.token for branch in results]) |
864 | @@ -230,5 +140,4 @@ |
865 | |
866 | def _getVocabRestriction(self): |
867 | """Restrict using a branch on widget.""" |
868 | - return getUtility(IBranchLookup).getByUniqueName( |
869 | - '~spotty/widget/hill') |
870 | + return getUtility(IBranchLookup).getByUniqueName('~spotty/widget/hill') |
871 | |
872 | === modified file 'lib/lp/registry/model/product.py' |
873 | --- lib/lp/registry/model/product.py 2013-02-07 06:10:38 +0000 |
874 | +++ lib/lp/registry/model/product.py 2013-02-14 03:56:21 +0000 |
875 | @@ -585,8 +585,6 @@ |
876 | |
877 | @property |
878 | def official_codehosting(self): |
879 | - # XXX Need to remove official_codehosting column from Product |
880 | - # table. |
881 | return self.development_focus.branch is not None |
882 | |
883 | @property |
884 | |
885 | === modified file 'lib/lp/services/webapp/configure.zcml' |
886 | --- lib/lp/services/webapp/configure.zcml 2012-09-28 07:11:24 +0000 |
887 | +++ lib/lp/services/webapp/configure.zcml 2013-02-14 03:56:21 +0000 |
888 | @@ -418,11 +418,11 @@ |
889 | permission="zope.Public" |
890 | /> |
891 | |
892 | - <!-- Define the widget used by fields that use BranchVocabularyBase. --> |
893 | + <!-- Define the widget used by fields that use BranchVocabulary. --> |
894 | <view |
895 | type="zope.publisher.interfaces.browser.IBrowserRequest" |
896 | for="zope.schema.interfaces.IChoice |
897 | - lp.code.vocabularies.branch.BranchVocabularyBase" |
898 | + lp.code.vocabularies.branch.BranchVocabulary" |
899 | provides="zope.app.form.interfaces.IInputWidget" |
900 | factory="lp.code.browser.widgets.branch.BranchPopupWidget" |
901 | permission="zope.Public" |
221 + if term and term.startswith('lp:'):
term should never be None.
229 + path = urlparse( term)[2] [1:]
Perhaps use lazr.uri instead?
+ collection = self._filterBy( [field. contains_ string( term.lower( ))])
The field needs to be lowercased too.
What's the effect of dropping the CountableIterator wrapper?