Merge lp:~stevenk/launchpad/new-branch-search into lp:launchpad

Proposed by Steve Kowalik
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
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) IBranchCollection.relatedTo() by accident.

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'.

IBranchCollection.search(), and its exact match friend have been torched and sadly burnt to the ground. I have rewritten them to only match against Branch.name, with a few shortcuts that will return singular branches if it matches a URL (any of Branch.url, https://code.launchpad... or http://bazaar.launchpad...), lp: shortform URL or unique_name.

IBranchCollection.relatedTo() was caught by a backdraft and also burnt to the ground. Since only one test used it, I left it as a pile of cinders, and fixed the test.

I have refactored the branch vocabularies to no longer rely on a useless base
class, and cleaned up some whitespace and such.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

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?

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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"