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
=== modified file 'lib/lp/code/feed/branch.py'
--- lib/lp/code/feed/branch.py 2011-12-30 07:32:58 +0000
+++ lib/lp/code/feed/branch.py 2013-02-14 03:56:21 +0000
@@ -31,6 +31,7 @@
31 )31 )
32from lp.code.interfaces.branchcollection import IAllBranches32from lp.code.interfaces.branchcollection import IAllBranches
33from lp.code.interfaces.revisioncache import IRevisionCache33from lp.code.interfaces.revisioncache import IRevisionCache
34from lp.code.model.branch import Branch
34from lp.registry.interfaces.person import IPerson35from lp.registry.interfaces.person import IPerson
35from lp.registry.interfaces.product import IProduct36from lp.registry.interfaces.product import IProduct
36from lp.registry.interfaces.projectgroup import IProjectGroup37from lp.registry.interfaces.projectgroup import IProjectGroup
@@ -162,17 +163,13 @@
162163
163 Only `self.quantity` revisions are returned.164 Only `self.quantity` revisions are returned.
164 """165 """
165 from lp.code.model.branch import Branch
166 collection = self._getCollection().visibleByUser(166 collection = self._getCollection().visibleByUser(
167 None).withLifecycleStatus(*DEFAULT_BRANCH_STATUS_IN_LISTING)167 None).withLifecycleStatus(*DEFAULT_BRANCH_STATUS_IN_LISTING)
168 branches = collection.getBranches(eager_load=False)168 branches = collection.getBranches(eager_load=False)
169 branches.order_by(169 return list(branches.order_by(
170 Desc(Branch.date_last_modified),170 Desc(Branch.date_last_modified), Asc(Branch.target_suffix),
171 Asc(Branch.target_suffix),171 Desc(Branch.lifecycle_status), Asc(Branch.name)).config(
172 Desc(Branch.lifecycle_status),172 limit=self.quantity))
173 Asc(Branch.name))
174 branches.config(limit=self.quantity)
175 return list(branches)
176173
177174
178class ProductBranchFeed(BranchListingFeed):175class ProductBranchFeed(BranchListingFeed):
179176
=== modified file 'lib/lp/code/interfaces/branchcollection.py'
--- lib/lp/code/interfaces/branchcollection.py 2013-01-07 02:40:55 +0000
+++ lib/lp/code/interfaces/branchcollection.py 2013-02-14 03:56:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""A collection of branches.4"""A collection of branches.
@@ -178,17 +178,11 @@
178 def registeredBy(person):178 def registeredBy(person):
179 """Restrict the collection to branches registered by 'person'."""179 """Restrict the collection to branches registered by 'person'."""
180180
181 def relatedTo(person):181 def search(term):
182 """Restrict the collection to branches related to 'person'.182 """Search the collection for branches matching 'term'.
183183
184 That is, branches that 'person' owns, registered or is subscribed to.184 :param term: A string.
185 """185 :return: A `ResultSet` of branches that matched.
186
187 def search(search_term):
188 """Search the collection for branches matching 'search_term'.
189
190 :param search_term: A string.
191 :return: An `ICountableIterator`.
192 """186 """
193187
194 def scanned():188 def scanned():
195189
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2013-01-16 06:41:43 +0000
+++ lib/lp/code/model/branch.py 2013-02-14 03:56:21 +0000
@@ -147,6 +147,7 @@
147 validate_person,147 validate_person,
148 validate_public_person,148 validate_public_person,
149 )149 )
150from lp.registry.interfaces.role import IPersonRoles
150from lp.registry.interfaces.sharingjob import (151from lp.registry.interfaces.sharingjob import (
151 IRemoveArtifactSubscriptionsJobSource,152 IRemoveArtifactSubscriptionsJobSource,
152 )153 )
@@ -1630,6 +1631,9 @@
1630 if user is None:1631 if user is None:
1631 return [public_branch_filter]1632 return [public_branch_filter]
16321633
1634 if IPersonRoles(user).in_admin:
1635 return []
1636
1633 artifact_grant_query = Coalesce(1637 artifact_grant_query = Coalesce(
1634 ArrayIntersects(1638 ArrayIntersects(
1635 SQL('%s.access_grants' % branch_class.__storm_table__),1639 SQL('%s.access_grants' % branch_class.__storm_table__),
16361640
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2012-10-18 19:06:48 +0000
+++ lib/lp/code/model/branchcollection.py 2013-02-14 03:56:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Implementations of `IBranchCollection`."""4"""Implementations of `IBranchCollection`."""
@@ -13,6 +13,10 @@
13from operator import attrgetter13from operator import attrgetter
1414
15from lazr.restful.utils import safe_hasattr15from lazr.restful.utils import safe_hasattr
16from lazr.uri import (
17 InvalidURIError,
18 URI,
19 )
16from storm.expr import (20from storm.expr import (
17 And,21 And,
18 Count,22 Count,
@@ -20,10 +24,8 @@
20 In,24 In,
21 Join,25 Join,
22 LeftJoin,26 LeftJoin,
23 Or,
24 Select,27 Select,
25 SQL,28 SQL,
26 Union,
27 With,29 With,
28 )30 )
29from storm.info import ClassAlias31from storm.info import ClassAlias
@@ -31,10 +33,7 @@
31from zope.component import getUtility33from zope.component import getUtility
32from zope.interface import implements34from zope.interface import implements
3335
34from lp.app.enums import (36from lp.app.enums import PRIVATE_INFORMATION_TYPES
35 PRIVATE_INFORMATION_TYPES,
36 PUBLIC_INFORMATION_TYPES,
37 )
38from lp.bugs.interfaces.bugtask import IBugTaskSet37from lp.bugs.interfaces.bugtask import IBugTaskSet
39from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context38from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
40from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams39from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
@@ -66,12 +65,8 @@
66from lp.registry.enums import EXCLUSIVE_TEAM_POLICY65from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
67from lp.registry.model.distribution import Distribution66from lp.registry.model.distribution import Distribution
68from lp.registry.model.distroseries import DistroSeries67from lp.registry.model.distroseries import DistroSeries
69from lp.registry.model.person import (68from lp.registry.model.person import Person
70 Owner,
71 Person,
72 )
73from lp.registry.model.product import Product69from lp.registry.model.product import Product
74from lp.registry.model.sourcepackagename import SourcePackageName
75from lp.registry.model.teammembership import TeamParticipation70from lp.registry.model.teammembership import TeamParticipation
76from lp.services.database.bulk import (71from lp.services.database.bulk import (
77 load_referencing,72 load_referencing,
@@ -87,7 +82,6 @@
87from lp.services.database.sqlbase import quote82from lp.services.database.sqlbase import quote
88from lp.services.propertycache import get_property_cache83from lp.services.propertycache import get_property_cache
89from lp.services.searchbuilder import any84from lp.services.searchbuilder import any
90from lp.services.webapp.vocabulary import CountableIterator
9185
9286
93class GenericBranchCollection:87class GenericBranchCollection:
@@ -647,71 +641,35 @@
647 """See `IBranchCollection`."""641 """See `IBranchCollection`."""
648 return self._filterBy([Branch.registrant == person], symmetric=False)642 return self._filterBy([Branch.registrant == person], symmetric=False)
649643
650 def relatedTo(self, person):644 def _getExactMatch(self, term):
651 """See `IBranchCollection`."""645 # Try and look up the branch by its URL, which handles lp: shortfom.
652 return self._filterBy(646 branch_url = getUtility(IBranchLookup).getByUrl(term)
653 [Branch.id.is_in(647 if branch_url:
654 Union(648 return branch_url
655 Select(Branch.id, Branch.owner == person),649 # Fall back to searching by unique_name, stripping out the path if it's
656 Select(Branch.id, Branch.registrant == person),650 # a URI.
657 Select(Branch.id,651 try:
658 And(BranchSubscription.person == person,652 path = URI(term).path.strip('/')
659 BranchSubscription.branch == Branch.id))))],653 except InvalidURIError:
660 symmetric=False)654 path = term
661655 return getUtility(IBranchLookup).getByUniqueName(path)
662 def _getExactMatch(self, search_term):656
663 """Return the exact branch that 'search_term' matches, or None."""657 def search(self, term):
664 search_term = search_term.rstrip('/')658 """See `IBranchCollection`."""
665 branch_set = getUtility(IBranchLookup)659 branch = self._getExactMatch(term)
666 branch = branch_set.getByUniqueName(search_term)660 if branch:
667 if branch is None:661 collection = self._filterBy([Branch.id == branch.id])
668 branch = branch_set.getByUrl(search_term)662 else:
669 return branch663 term = unicode(term)
670664 # Filter by name.
671 def search(self, search_term):665 field = Branch.name
672 """See `IBranchCollection`."""666 # Except if the term contains /, when we use unique_name.
673 # XXX: JonathanLange 2009-02-23 bug 372591: This matches the old667 if '/' in term:
674 # search algorithm that used to live in vocabularies/dbojects.py. It's668 field = Branch.unique_name
675 # not actually very good -- really it should match based on substrings669 collection = self._filterBy(
676 # of the unique name and sort based on relevance.670 [field.lower().contains_string(term.lower())])
677 branch = self._getExactMatch(search_term)671 return collection.getBranches(eager_load=False).order_by(
678 if branch is not None:
679 if branch in self.getBranches(eager_load=False):
680 return CountableIterator(1, [branch])
681 else:
682 return CountableIterator(0, [])
683 like_term = '%' + search_term + '%'
684 # Match the Branch name or the URL.
685 queries = [Select(Branch.id,
686 Or(Branch.name.like(like_term),
687 Branch.url == search_term))]
688 # Match the product name.
689 if 'product' not in self._exclude_from_search:
690 queries.append(Select(
691 Branch.id,
692 And(Branch.product == Product.id,
693 Product.name.like(like_term))))
694
695 # Match the owner name.
696 queries.append(Select(
697 Branch.id,
698 And(Branch.owner == Owner.id, Owner.name.like(like_term))))
699
700 # Match the package bits.
701 queries.append(
702 Select(Branch.id,
703 And(Branch.sourcepackagename == SourcePackageName.id,
704 Branch.distroseries == DistroSeries.id,
705 DistroSeries.distribution == Distribution.id,
706 Or(SourcePackageName.name.like(like_term),
707 DistroSeries.name.like(like_term),
708 Distribution.name.like(like_term)))))
709
710 # Get the results.
711 collection = self._filterBy([Branch.id.is_in(Union(*queries))])
712 results = collection.getBranches(eager_load=False).order_by(
713 Branch.name, Branch.id)672 Branch.name, Branch.id)
714 return CountableIterator(results.count(), results)
715673
716 def scanned(self):674 def scanned(self):
717 """See `IBranchCollection`."""675 """See `IBranchCollection`."""
@@ -790,8 +748,7 @@
790748
791 def _getBranchVisibilityExpression(self, branch_class=Branch):749 def _getBranchVisibilityExpression(self, branch_class=Branch):
792 """Return the where clauses for visibility."""750 """Return the where clauses for visibility."""
793 return [751 return get_branch_privacy_filter(None, branch_class=branch_class)
794 branch_class.information_type.is_in(PUBLIC_INFORMATION_TYPES)]
795752
796753
797class VisibleBranchCollection(GenericBranchCollection):754class VisibleBranchCollection(GenericBranchCollection):
@@ -839,13 +796,9 @@
839 if exclude_from_search is None:796 if exclude_from_search is None:
840 exclude_from_search = []797 exclude_from_search = []
841 return self.__class__(798 return self.__class__(
842 self._user,799 self._user, self.store, symmetric_expr, tables,
843 self.store,
844 symmetric_expr,
845 tables,
846 self._exclude_from_search + exclude_from_search,800 self._exclude_from_search + exclude_from_search,
847 asymmetric_expr,801 asymmetric_expr, asymmetric_tables)
848 asymmetric_tables)
849802
850 def _getBranchVisibilityExpression(self, branch_class=Branch):803 def _getBranchVisibilityExpression(self, branch_class=Branch):
851 """Return the where clauses for visibility.804 """Return the where clauses for visibility.
@@ -853,8 +806,7 @@
853 :param branch_class: The Branch class to use - permits using806 :param branch_class: The Branch class to use - permits using
854 ClassAliases.807 ClassAliases.
855 """808 """
856 return get_branch_privacy_filter(809 return get_branch_privacy_filter(self._user, branch_class=branch_class)
857 self._user, branch_class=branch_class)
858810
859 def visibleByUser(self, person):811 def visibleByUser(self, person):
860 """See `IBranchCollection`."""812 """See `IBranchCollection`."""
861813
=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py 2012-10-18 19:20:49 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py 2013-02-14 03:56:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for branch collections."""4"""Tests for branch collections."""
@@ -41,6 +41,7 @@
41 IStoreSelector,41 IStoreSelector,
42 MAIN_STORE,42 MAIN_STORE,
43 )43 )
44from lp.services.webapp.publisher import canonical_url
44from lp.testing import (45from lp.testing import (
45 person_logged_in,46 person_logged_in,
46 run_with_login,47 run_with_login,
@@ -560,34 +561,6 @@
560 collection = self.all_branches.subscribedBy(subscriber)561 collection = self.all_branches.subscribedBy(subscriber)
561 self.assertEqual([branch], list(collection.getBranches()))562 self.assertEqual([branch], list(collection.getBranches()))
562563
563 def test_relatedTo(self):
564 # 'relatedTo' returns a collection that has all branches that a user
565 # owns, is subscribed to or registered. No other branches are in this
566 # collection.
567 person = self.factory.makePerson()
568 team = self.factory.makeTeam(person)
569 owned_branch = self.factory.makeAnyBranch(owner=person)
570 # Unsubscribe the owner, to demonstrate that we show owned branches
571 # even if they aren't subscribed.
572 owned_branch.unsubscribe(person, person)
573 # Subscribe two other people to the owned branch to make sure
574 # that the BranchSubscription join is doing it right.
575 self.factory.makeBranchSubscription(branch=owned_branch)
576 self.factory.makeBranchSubscription(branch=owned_branch)
577
578 registered_branch = self.factory.makeAnyBranch(
579 owner=team, registrant=person)
580 subscribed_branch = self.factory.makeAnyBranch()
581 subscribed_branch.subscribe(
582 person, BranchSubscriptionNotificationLevel.NOEMAIL,
583 BranchSubscriptionDiffSize.NODIFF,
584 CodeReviewNotificationLevel.NOEMAIL,
585 person)
586 related_branches = self.all_branches.relatedTo(person)
587 self.assertEqual(
588 sorted([owned_branch, registered_branch, subscribed_branch]),
589 sorted(related_branches.getBranches()))
590
591 def test_withBranchType(self):564 def test_withBranchType(self):
592 hosted_branch1 = self.factory.makeAnyBranch(565 hosted_branch1 = self.factory.makeAnyBranch(
593 branch_type=BranchType.HOSTED)566 branch_type=BranchType.HOSTED)
@@ -1161,6 +1134,21 @@
1161 search_results = self.collection.search(branch.codebrowse_url())1134 search_results = self.collection.search(branch.codebrowse_url())
1162 self.assertEqual([branch], list(search_results))1135 self.assertEqual([branch], list(search_results))
11631136
1137 def test_exact_match_with_lp_colon_url(self):
1138 branch = self.factory.makeBranch()
1139 lp_name = 'lp://dev/' + branch.unique_name
1140 search_results = self.collection.search(lp_name)
1141 self.assertEqual([branch], list(search_results))
1142
1143 def test_exact_match_full_url(self):
1144 branch = self.factory.makeBranch()
1145 url = canonical_url(branch)
1146 self.assertEqual([branch], list(self.collection.search(url)))
1147
1148 def test_exact_match_bad_url(self):
1149 search_results = self.collection.search('http:hahafail')
1150 self.assertEqual([], list(search_results))
1151
1164 def test_exact_match_bzr_identity(self):1152 def test_exact_match_bzr_identity(self):
1165 # If you search for the bzr identity of a branch, then you get a1153 # If you search for the bzr identity of a branch, then you get a
1166 # single result with that branch.1154 # single result with that branch.
@@ -1214,6 +1202,12 @@
1214 search_results = self.collection.search('foo')1202 search_results = self.collection.search('foo')
1215 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))1203 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
12161204
1205 def test_match_against_unique_name(self):
1206 branch = self.factory.makeAnyBranch(name='fooa')
1207 search_term = branch.product.name + '/foo'
1208 search_results = self.collection.search(search_term)
1209 self.assertEqual([branch], list(search_results))
1210
1217 def test_match_sub_branch_name(self):1211 def test_match_sub_branch_name(self):
1218 # search returns all branches which have a name of which the search1212 # search returns all branches which have a name of which the search
1219 # term is a substring.1213 # term is a substring.
@@ -1223,73 +1217,9 @@
1223 search_results = self.collection.search('foo')1217 search_results = self.collection.search('foo')
1224 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))1218 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
12251219
1226 def test_match_exact_owner_name(self):1220 def test_match_ignores_case(self):
1227 # search returns all branches which have an owner with a name matching1221 branch = self.factory.makeAnyBranch(name='foobar')
1228 # the server.1222 search_results = self.collection.search('FOOBAR')
1229 person = self.factory.makePerson(name='foo')
1230 branch1 = self.factory.makeAnyBranch(owner=person)
1231 branch2 = self.factory.makeAnyBranch(owner=person)
1232 self.factory.makeAnyBranch()
1233 search_results = self.collection.search('foo')
1234 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
1235
1236 def test_match_sub_owner_name(self):
1237 # search returns all branches that have an owner name where the search
1238 # term is a substring of the owner name.
1239 person1 = self.factory.makePerson(name='foom')
1240 branch1 = self.factory.makeAnyBranch(owner=person1)
1241 person2 = self.factory.makePerson(name='afoo')
1242 branch2 = self.factory.makeAnyBranch(owner=person2)
1243 self.factory.makeAnyBranch()
1244 search_results = self.collection.search('foo')
1245 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
1246
1247 def test_match_exact_product_name(self):
1248 # search returns all branches that have a product name where the
1249 # product name is the same as the search term.
1250 product = self.factory.makeProduct(name='foo')
1251 branch1 = self.factory.makeAnyBranch(product=product)
1252 branch2 = self.factory.makeAnyBranch(product=product)
1253 self.factory.makeAnyBranch()
1254 search_results = self.collection.search('foo')
1255 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
1256
1257 def test_match_sub_product_name(self):
1258 # search returns all branches that have a product name where the
1259 # search terms is a substring of the product name.
1260 product1 = self.factory.makeProduct(name='foom')
1261 branch1 = self.factory.makeProductBranch(product=product1)
1262 product2 = self.factory.makeProduct(name='afoo')
1263 branch2 = self.factory.makeProductBranch(product=product2)
1264 self.factory.makeAnyBranch()
1265 search_results = self.collection.search('foo')
1266 self.assertEqual(sorted([branch1, branch2]), sorted(search_results))
1267
1268 def test_match_sub_distro_name(self):
1269 # search returns all branches that have a distro name where the search
1270 # term is a substring of the distro name.
1271 branch = self.factory.makePackageBranch()
1272 self.factory.makeAnyBranch()
1273 search_term = branch.distribution.name[1:]
1274 search_results = self.collection.search(search_term)
1275 self.assertEqual([branch], list(search_results))
1276
1277 def test_match_sub_distroseries_name(self):
1278 # search returns all branches that have a distro series with a name
1279 # that the search term is a substring of.
1280 branch = self.factory.makePackageBranch()
1281 self.factory.makeAnyBranch()
1282 search_term = branch.distroseries.name[1:]
1283 search_results = self.collection.search(search_term)
1284 self.assertEqual([branch], list(search_results))
1285
1286 def test_match_sub_sourcepackage_name(self):
1287 # search returns all branches that have a source package with a name
1288 # that contains the search term.
1289 branch = self.factory.makePackageBranch()
1290 self.factory.makeAnyBranch()
1291 search_term = branch.sourcepackagename.name[1:]
1292 search_results = self.collection.search(search_term)
1293 self.assertEqual([branch], list(search_results))1223 self.assertEqual([branch], list(search_results))
12941224
1295 def test_dont_match_product_if_in_product(self):1225 def test_dont_match_product_if_in_product(self):
12961226
=== modified file 'lib/lp/code/stories/feeds/xx-branch-atom.txt'
--- lib/lp/code/stories/feeds/xx-branch-atom.txt 2012-07-05 04:59:52 +0000
+++ lib/lp/code/stories/feeds/xx-branch-atom.txt 2013-02-14 03:56:21 +0000
@@ -99,16 +99,17 @@
9999
100If a branch is marked private it will not be displayed. The Landscape100If a branch is marked private it will not be displayed. The Landscape
101developers team has two branches which are both private.101developers team has two branches which are both private.
102
102 >>> from zope.component import getUtility103 >>> from zope.component import getUtility
103 >>> from zope.security.proxy import removeSecurityProxy104 >>> from zope.security.proxy import removeSecurityProxy
104 >>> from lp.code.model.branch import Branch105 >>> from lp.code.model.branch import Branch
105 >>> from lp.code.interfaces.branchcollection import IAllBranches106 >>> from lp.code.interfaces.branchcollection import IAllBranches
106 >>> from lp.registry.interfaces.person import IPersonSet107 >>> from lp.registry.interfaces.person import IPersonSet
108 >>> from lp.registry.interfaces.product import IProductSet
107 >>> login(ANONYMOUS)109 >>> login(ANONYMOUS)
108 >>> person_set = getUtility(IPersonSet)110 >>> test_user = getUtility(IPersonSet).getByEmail('test@canonical.com')
109 >>> landscape_team = person_set.getByName('landscape-developers')111 >>> landscape = getUtility(IProductSet)['landscape']
110 >>> test_user = person_set.getByEmail('test@canonical.com')112 >>> branches = getUtility(IAllBranches).inProduct(landscape)
111 >>> branches = getUtility(IAllBranches).relatedTo(landscape_team)
112 >>> branches = branches.visibleByUser(113 >>> branches = branches.visibleByUser(
113 ... test_user).getBranches().order_by(Branch.id)114 ... test_user).getBranches().order_by(Branch.id)
114 >>> for branch in branches:115 >>> for branch in branches:
115116
=== modified file 'lib/lp/code/vocabularies/branch.py'
--- lib/lp/code/vocabularies/branch.py 2012-10-18 17:49:39 +0000
+++ lib/lp/code/vocabularies/branch.py 2013-02-14 03:56:21 +0000
@@ -1,9 +1,8 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Vocabularies that contain branches."""4"""Vocabularies that contain branches."""
55
6
7__metaclass__ = type6__metaclass__ = type
87
9__all__ = [8__all__ = [
@@ -31,12 +30,8 @@
31 )30 )
3231
3332
34class BranchVocabularyBase(SQLObjectVocabularyBase):33class BranchVocabulary(SQLObjectVocabularyBase):
35 """A base class for Branch vocabularies.34 """A vocabulary for searching branches."""
36
37 Override `BranchVocabularyBase._getCollection` to provide the collection
38 of branches which make up the vocabulary.
39 """
4035
41 implements(IHugeVocabulary)36 implements(IHugeVocabulary)
4237
@@ -56,17 +51,10 @@
56 return iter(search_results).next()51 return iter(search_results).next()
57 raise LookupError(token)52 raise LookupError(token)
5853
59 def _getCollection(self):
60 """Return the collection of branches the vocabulary searches.
61
62 Subclasses MUST override and implement this.
63 """
64 raise NotImplementedError(self._getCollection)
65
66 def searchForTerms(self, query=None, vocab_filter=None):54 def searchForTerms(self, query=None, vocab_filter=None):
67 """See `IHugeVocabulary`."""55 """See `IHugeVocabulary`."""
68 logged_in_user = getUtility(ILaunchBag).user56 user = getUtility(ILaunchBag).user
69 collection = self._getCollection().visibleByUser(logged_in_user)57 collection = self._getCollection().visibleByUser(user)
70 if query is None:58 if query is None:
71 branches = collection.getBranches(eager_load=False)59 branches = collection.getBranches(eager_load=False)
72 else:60 else:
@@ -77,28 +65,15 @@
77 """See `IVocabulary`."""65 """See `IVocabulary`."""
78 return self.search().count()66 return self.search().count()
7967
80
81class BranchVocabulary(BranchVocabularyBase):
82 """A vocabulary for searching branches.
83
84 The name and URL of the branch, the name of the product, and the
85 name of the registrant of the branches is checked for the entered
86 value.
87 """
88
89 def _getCollection(self):68 def _getCollection(self):
90 return getUtility(IAllBranches)69 return getUtility(IAllBranches)
9170
9271
93class BranchRestrictedOnProductVocabulary(BranchVocabularyBase):72class BranchRestrictedOnProductVocabulary(BranchVocabulary):
94 """A vocabulary for searching branches restricted on product.73 """A vocabulary for searching branches restricted on product."""
95
96 The query entered checks the name or URL of the branch, or the
97 name of the registrant of the branch.
98 """
9974
100 def __init__(self, context=None):75 def __init__(self, context=None):
101 BranchVocabularyBase.__init__(self, context)76 super(BranchRestrictedOnProductVocabulary, self).__init__(context)
102 if IProduct.providedBy(self.context):77 if IProduct.providedBy(self.context):
103 self.product = self.context78 self.product = self.context
104 elif IProductSeries.providedBy(self.context):79 elif IProductSeries.providedBy(self.context):
@@ -113,7 +88,7 @@
113 return getUtility(IAllBranches).inProduct(self.product).isExclusive()88 return getUtility(IAllBranches).inProduct(self.product).isExclusive()
11489
11590
116class HostedBranchRestrictedOnOwnerVocabulary(BranchVocabularyBase):91class HostedBranchRestrictedOnOwnerVocabulary(BranchVocabulary):
117 """A vocabulary for hosted branches owned by the current user.92 """A vocabulary for hosted branches owned by the current user.
11893
119 These are branches that the user either owns themselves or which are94 These are branches that the user either owns themselves or which are
@@ -124,7 +99,7 @@
124 """Pass a Person as context, or anything else for the current user."""99 """Pass a Person as context, or anything else for the current user."""
125 super(HostedBranchRestrictedOnOwnerVocabulary, self).__init__(context)100 super(HostedBranchRestrictedOnOwnerVocabulary, self).__init__(context)
126 if IPerson.providedBy(self.context):101 if IPerson.providedBy(self.context):
127 self.user = context102 self.user = self.context
128 else:103 else:
129 self.user = getUtility(ILaunchBag).user104 self.user = getUtility(ILaunchBag).user
130105
131106
=== modified file 'lib/lp/code/vocabularies/tests/branch.txt'
--- lib/lp/code/vocabularies/tests/branch.txt 2012-04-30 13:53:06 +0000
+++ lib/lp/code/vocabularies/tests/branch.txt 2013-02-14 03:56:21 +0000
@@ -32,16 +32,6 @@
32 ~stevea/thunderbird/main32 ~stevea/thunderbird/main
33 ~vcs-imports/evolution/main33 ~vcs-imports/evolution/main
3434
35 >>> print_vocab_branches(branch_vocabulary, 'vcs-imports')
36 ~vcs-imports/evolution/import
37 ~vcs-imports/evolution/main
38 ~vcs-imports/gnome-terminal/import
39
40 >>> print_vocab_branches(branch_vocabulary, 'evolution')
41 ~carlos/evolution/2.0
42 ~vcs-imports/evolution/import
43 ~vcs-imports/evolution/main
44
45A search with the full branch unique name should also find the branch.35A search with the full branch unique name should also find the branch.
4636
47 >>> print_vocab_branches(branch_vocabulary, '~name12/firefox/main')37 >>> print_vocab_branches(branch_vocabulary, '~name12/firefox/main')
@@ -107,9 +97,6 @@
107 >>> print_vocab_branches(branch_vocabulary, 'main')97 >>> print_vocab_branches(branch_vocabulary, 'main')
108 ~name12/gnome-terminal/main98 ~name12/gnome-terminal/main
10999
110 >>> print_vocab_branches(branch_vocabulary, 'vcs-imports')
111 ~vcs-imports/gnome-terminal/import
112
113If a full unique name is entered that has a different product, the100If a full unique name is entered that has a different product, the
114branch is not part of the vocabulary.101branch is not part of the vocabulary.
115102
116103
=== modified file 'lib/lp/code/vocabularies/tests/test_branch_vocabularies.py'
--- lib/lp/code/vocabularies/tests/test_branch_vocabularies.py 2012-10-18 17:49:39 +0000
+++ lib/lp/code/vocabularies/tests/test_branch_vocabularies.py 2013-02-14 03:56:21 +0000
@@ -1,11 +1,10 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test the branch vocabularies."""4"""Test the branch vocabularies."""
55
6__metaclass__ = type6__metaclass__ = type
77
8from unittest import TestCase
98
10from zope.component import getUtility9from zope.component import getUtility
1110
@@ -16,99 +15,36 @@
16 )15 )
17from lp.registry.enums import TeamMembershipPolicy16from lp.registry.enums import TeamMembershipPolicy
18from lp.registry.interfaces.product import IProductSet17from lp.registry.interfaces.product import IProductSet
19from lp.testing import (18from lp.testing import TestCaseWithFactory
20 ANONYMOUS,
21 login,
22 logout,
23 )
24from lp.testing.factory import LaunchpadObjectFactory
25from lp.testing.layers import DatabaseFunctionalLayer19from lp.testing.layers import DatabaseFunctionalLayer
2620
2721
28class BranchVocabTestCase(TestCase):22class TestBranchVocabulary(TestCaseWithFactory):
29 """A base class for the branch vocabulary test cases."""
30 layer = DatabaseFunctionalLayer
31
32 def setUp(self):
33 # Set up the anonymous security interaction.
34 login(ANONYMOUS)
35
36 def tearDown(self):
37 logout()
38
39
40class TestBranchVocabulary(BranchVocabTestCase):
41 """Test that the BranchVocabulary behaves as expected."""23 """Test that the BranchVocabulary behaves as expected."""
4224
25 layer = DatabaseFunctionalLayer
26
43 def setUp(self):27 def setUp(self):
44 BranchVocabTestCase.setUp(self)28 super(TestBranchVocabulary, self).setUp()
45 self._createBranches()29 self._createBranches()
46 self.vocab = BranchVocabulary(context=None)30 self.vocab = BranchVocabulary(context=None)
4731
48 def _createBranches(self):32 def _createBranches(self):
49 factory = LaunchpadObjectFactory()33 widget = self.factory.makeProduct(name='widget')
50 widget = factory.makeProduct(name='widget')34 sprocket = self.factory.makeProduct(name='sprocket')
51 sprocket = factory.makeProduct(name='sprocket')35 scotty = self.factory.makePerson(name='scotty')
52 # Scotty's branches.36 self.factory.makeProductBranch(
53 scotty = factory.makePerson(name='scotty')
54 factory.makeProductBranch(
55 owner=scotty, product=widget, name='fizzbuzz')37 owner=scotty, product=widget, name='fizzbuzz')
56 factory.makeProductBranch(38 self.factory.makeProductBranch(
57 owner=scotty, product=widget, name='mountain')39 owner=scotty, product=widget, name='mountain')
58 factory.makeProductBranch(40 self.factory.makeProductBranch(
59 owner=scotty, product=sprocket, name='fizzbuzz')41 owner=scotty, product=sprocket, name='fizzbuzz')
60 # Spotty's branches.
61 spotty = factory.makePerson(name='spotty')
62 factory.makeProductBranch(
63 owner=spotty, product=widget, name='hill')
64 factory.makeProductBranch(
65 owner=spotty, product=widget, name='sprocket')
66 # Sprocket's branches.
67 sprocket_person = factory.makePerson(name='sprocket')
68 factory.makeProductBranch(
69 owner=sprocket_person, product=widget, name='foo')
7042
71 def test_fizzbuzzBranches(self):43 def test_fizzbuzzBranches(self):
72 """Return branches that match the string 'fizzbuzz'."""44 """Return branches that match the string 'fizzbuzz'."""
73 results = self.vocab.searchForTerms('fizzbuzz')45 results = self.vocab.searchForTerms('fizzbuzz')
74 expected = [46 expected = [
75 u'~scotty/sprocket/fizzbuzz',47 u'~scotty/sprocket/fizzbuzz', u'~scotty/widget/fizzbuzz']
76 u'~scotty/widget/fizzbuzz',
77 ]
78 branch_names = sorted([branch.token for branch in results])
79 self.assertEqual(expected, branch_names)
80
81 def test_widgetBranches(self):
82 """Searches match the product name too."""
83 results = self.vocab.searchForTerms('widget')
84 expected = [
85 u'~scotty/widget/fizzbuzz',
86 u'~scotty/widget/mountain',
87 u'~spotty/widget/hill',
88 u'~spotty/widget/sprocket',
89 u'~sprocket/widget/foo',
90 ]
91 branch_names = sorted([branch.token for branch in results])
92 self.assertEqual(expected, branch_names)
93
94 def test_spottyBranches(self):
95 """Searches also match the registrant name."""
96 results = self.vocab.searchForTerms('spotty')
97 expected = [
98 u'~spotty/widget/hill',
99 u'~spotty/widget/sprocket',
100 ]
101 branch_names = sorted([branch.token for branch in results])
102 self.assertEqual(expected, branch_names)
103
104 def test_crossAttributeBranches(self):
105 """The search checks name, product, and person."""
106 results = self.vocab.searchForTerms('rocket')
107 expected = [
108 u'~scotty/sprocket/fizzbuzz',
109 u'~spotty/widget/sprocket',
110 u'~sprocket/widget/foo',
111 ]
112 branch_names = sorted([branch.token for branch in results])48 branch_names = sorted([branch.token for branch in results])
113 self.assertEqual(expected, branch_names)49 self.assertEqual(expected, branch_names)
11450
@@ -116,20 +52,15 @@
116 # If there is a single search result that matches, use that52 # If there is a single search result that matches, use that
117 # as the result.53 # as the result.
118 term = self.vocab.getTermByToken('mountain')54 term = self.vocab.getTermByToken('mountain')
119 self.assertEqual(55 self.assertEqual('~scotty/widget/mountain', term.value.unique_name)
120 '~scotty/widget/mountain',
121 term.value.unique_name)
12256
123 def test_multipleQueryResult(self):57 def test_multipleQueryResult(self):
124 # If there are more than one search result, a LookupError is still58 # If there are more than one search result, a LookupError is still
125 # raised.59 # raised.
126 self.assertRaises(60 self.assertRaises(LookupError, self.vocab.getTermByToken, 'fizzbuzz')
127 LookupError,61
128 self.vocab.getTermByToken,62
129 'fizzbuzz')63class TestRestrictedBranchVocabularyOnProduct(TestCaseWithFactory):
130
131
132class TestRestrictedBranchVocabularyOnProduct(BranchVocabTestCase):
133 """Test the BranchRestrictedOnProductVocabulary behaves as expected.64 """Test the BranchRestrictedOnProductVocabulary behaves as expected.
13465
135 When a BranchRestrictedOnProductVocabulary is used with a product the66 When a BranchRestrictedOnProductVocabulary is used with a product the
@@ -137,8 +68,10 @@
137 context.68 context.
138 """69 """
13970
71 layer = DatabaseFunctionalLayer
72
140 def setUp(self):73 def setUp(self):
141 BranchVocabTestCase.setUp(self)74 super(TestRestrictedBranchVocabularyOnProduct, self).setUp()
142 self._createBranches()75 self._createBranches()
143 self.vocab = BranchRestrictedOnProductVocabulary(76 self.vocab = BranchRestrictedOnProductVocabulary(
144 context=self._getVocabRestriction())77 context=self._getVocabRestriction())
@@ -148,18 +81,17 @@
148 return getUtility(IProductSet).getByName('widget')81 return getUtility(IProductSet).getByName('widget')
14982
150 def _createBranches(self):83 def _createBranches(self):
151 factory = LaunchpadObjectFactory()84 test_product = self.factory.makeProduct(name='widget')
152 test_product = factory.makeProduct(name='widget')85 other_product = self.factory.makeProduct(name='sprocket')
153 other_product = factory.makeProduct(name='sprocket')86 person = self.factory.makePerson(name='scotty')
154 person = factory.makePerson(name='scotty')87 self.factory.makeProductBranch(
155 factory.makeProductBranch(
156 owner=person, product=test_product, name='main')88 owner=person, product=test_product, name='main')
157 factory.makeProductBranch(89 self.factory.makeProductBranch(
158 owner=person, product=test_product, name='mountain')90 owner=person, product=test_product, name='mountain')
159 factory.makeProductBranch(91 self.factory.makeProductBranch(
160 owner=person, product=other_product, name='main')92 owner=person, product=other_product, name='main')
161 person = factory.makePerson(name='spotty')93 person = self.factory.makePerson(name='spotty')
162 factory.makeProductBranch(94 self.factory.makeProductBranch(
163 owner=person, product=test_product, name='hill')95 owner=person, product=test_product, name='hill')
164 self.product = test_product96 self.product = test_product
16597
@@ -169,23 +101,7 @@
169 The result set should not show ~scotty/sprocket/main.101 The result set should not show ~scotty/sprocket/main.
170 """102 """
171 results = self.vocab.searchForTerms('main')103 results = self.vocab.searchForTerms('main')
172 expected = [104 expected = [u'~scotty/widget/main']
173 u'~scotty/widget/main',
174 ]
175 branch_names = sorted([branch.token for branch in results])
176 self.assertEqual(expected, branch_names)
177
178 def test_ownersBranches(self):
179 """Look for branches owned by scotty.
180
181 The result set should not show ~scotty/sprocket/main.
182 """
183 results = self.vocab.searchForTerms('scotty')
184
185 expected = [
186 u'~scotty/widget/main',
187 u'~scotty/widget/mountain',
188 ]
189 branch_names = sorted([branch.token for branch in results])105 branch_names = sorted([branch.token for branch in results])
190 self.assertEqual(expected, branch_names)106 self.assertEqual(expected, branch_names)
191107
@@ -193,26 +109,20 @@
193 # If there is a single search result that matches, use that109 # If there is a single search result that matches, use that
194 # as the result.110 # as the result.
195 term = self.vocab.getTermByToken('mountain')111 term = self.vocab.getTermByToken('mountain')
196 self.assertEqual(112 self.assertEqual('~scotty/widget/mountain', term.value.unique_name)
197 '~scotty/widget/mountain',
198 term.value.unique_name)
199113
200 def test_multipleQueryResult(self):114 def test_multipleQueryResult(self):
201 # If there are more than one search result, a LookupError is still115 # If there are more than one search result, a LookupError is still
202 # raised.116 # raised.
203 self.assertRaises(117 self.assertRaises(LookupError, self.vocab.getTermByToken, 'scotty')
204 LookupError,
205 self.vocab.getTermByToken,
206 'scotty')
207118
208 def test_does_not_contain_inclusive_teams(self):119 def test_does_not_contain_inclusive_teams(self):
209 factory = LaunchpadObjectFactory()120 open_team = self.factory.makeTeam(name='open-team',
210 open_team = factory.makeTeam(name='open-team',
211 membership_policy=TeamMembershipPolicy.OPEN)121 membership_policy=TeamMembershipPolicy.OPEN)
212 delegated_team = factory.makeTeam(name='delegated-team',122 delegated_team = self.factory.makeTeam(name='delegated-team',
213 membership_policy=TeamMembershipPolicy.DELEGATED)123 membership_policy=TeamMembershipPolicy.DELEGATED)
214 for team in [open_team, delegated_team]:124 for team in [open_team, delegated_team]:
215 factory.makeProductBranch(125 self.factory.makeProductBranch(
216 owner=team, product=self.product, name='mountain')126 owner=team, product=self.product, name='mountain')
217 results = self.vocab.searchForTerms('mountain')127 results = self.vocab.searchForTerms('mountain')
218 branch_names = sorted([branch.token for branch in results])128 branch_names = sorted([branch.token for branch in results])
@@ -230,5 +140,4 @@
230140
231 def _getVocabRestriction(self):141 def _getVocabRestriction(self):
232 """Restrict using a branch on widget."""142 """Restrict using a branch on widget."""
233 return getUtility(IBranchLookup).getByUniqueName(143 return getUtility(IBranchLookup).getByUniqueName('~spotty/widget/hill')
234 '~spotty/widget/hill')
235144
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2013-02-07 06:10:38 +0000
+++ lib/lp/registry/model/product.py 2013-02-14 03:56:21 +0000
@@ -585,8 +585,6 @@
585585
586 @property586 @property
587 def official_codehosting(self):587 def official_codehosting(self):
588 # XXX Need to remove official_codehosting column from Product
589 # table.
590 return self.development_focus.branch is not None588 return self.development_focus.branch is not None
591589
592 @property590 @property
593591
=== modified file 'lib/lp/services/webapp/configure.zcml'
--- lib/lp/services/webapp/configure.zcml 2012-09-28 07:11:24 +0000
+++ lib/lp/services/webapp/configure.zcml 2013-02-14 03:56:21 +0000
@@ -418,11 +418,11 @@
418 permission="zope.Public"418 permission="zope.Public"
419 />419 />
420420
421 <!-- Define the widget used by fields that use BranchVocabularyBase. -->421 <!-- Define the widget used by fields that use BranchVocabulary. -->
422 <view422 <view
423 type="zope.publisher.interfaces.browser.IBrowserRequest"423 type="zope.publisher.interfaces.browser.IBrowserRequest"
424 for="zope.schema.interfaces.IChoice424 for="zope.schema.interfaces.IChoice
425 lp.code.vocabularies.branch.BranchVocabularyBase"425 lp.code.vocabularies.branch.BranchVocabulary"
426 provides="zope.app.form.interfaces.IInputWidget"426 provides="zope.app.form.interfaces.IInputWidget"
427 factory="lp.code.browser.widgets.branch.BranchPopupWidget"427 factory="lp.code.browser.widgets.branch.BranchPopupWidget"
428 permission="zope.Public"428 permission="zope.Public"