Merge lp:~thumper/launchpad/blueprint-dependency-vocabulary into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: 12598
Proposed branch: lp:~thumper/launchpad/blueprint-dependency-vocabulary
Merge into: lp:launchpad
Prerequisite: lp:~thumper/launchpad/blueprint-dependencies
Diff against target: 370 lines (+134/-156)
4 files modified
lib/lp/blueprints/vocabularies/specificationdependency.py (+65/-42)
lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt (+0/-97)
lib/lp/blueprints/vocabularies/tests/test_doc.py (+0/-17)
lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py (+69/-0)
To merge this branch: bzr merge lp:~thumper/launchpad/blueprint-dependency-vocabulary
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+53192@code.launchpad.net

Commit message

[r=mwhudson][bug=707111] Allow any blueprint to be searched for when adding dependencies.

Description of the change

Allow the vocabulary to work with any dependency.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi Tim,

This basically looks fine. The only comments I have are:

 * I think the ISpecification check is a bit over the top (already said this on IRC),
 * I think _order_by.__doc__ could do with expansion (or deletion -- the current docstring doesn't really add anything)
 * I think "_exclude_blocked_query" would be a better name than "_blocked_subselect" (the latter sounds a bit like it returns the blocked specs)
 * "Not" is imported but not used in ./lib/lp/blueprints/vocabularies/specificationdependency.py

"make lint" also reports a few long lines, up to you if you care :)

review: Approve
Revision history for this message
Tim Penhey (thumper) wrote :

On Tue, 15 Mar 2011 12:13:40 you wrote:
> Review: Approve
> Hi Tim,
>
> This basically looks fine. The only comments I have are:
>
> * I think the ISpecification check is a bit over the top (already said
> this on IRC), * I think _order_by.__doc__ could do with expansion (or
> deletion -- the current docstring doesn't really add anything) * I think
> "_exclude_blocked_query" would be a better name than "_blocked_subselect"
> (the latter sounds a bit like it returns the blocked specs) * "Not" is
> imported but not used in
> ./lib/lp/blueprints/vocabularies/specificationdependency.py
>
> "make lint" also reports a few long lines, up to you if you care :)

Addressed, and thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py'
--- lib/lp/blueprints/vocabularies/specificationdependency.py 2011-03-14 21:25:44 +0000
+++ lib/lp/blueprints/vocabularies/specificationdependency.py 2011-03-15 08:46:24 +0000
@@ -11,12 +11,12 @@
1111
12from operator import attrgetter12from operator import attrgetter
1313
14from storm.locals import SQL, Store
14from zope.component import getUtility15from zope.component import getUtility
15from zope.interface import implements16from zope.interface import implements
16from zope.schema.vocabulary import SimpleTerm17from zope.schema.vocabulary import SimpleTerm
1718
18from canonical.database.sqlbase import quote_like19from canonical.database.sqlbase import quote
19from canonical.launchpad.helpers import shortlist
20from canonical.launchpad.webapp import (20from canonical.launchpad.webapp import (
21 canonical_url,21 canonical_url,
22 urlparse,22 urlparse,
@@ -27,9 +27,11 @@
27 NamedSQLObjectVocabulary,27 NamedSQLObjectVocabulary,
28 SQLObjectVocabularyBase,28 SQLObjectVocabularyBase,
29 )29 )
3030from lp.blueprints.interfaces.specification import ISpecification
31from lp.blueprints.enums import SpecificationFilter31from lp.blueprints.model.specification import (
32from lp.blueprints.model.specification import Specification32 recursive_blocked_query,
33 Specification,
34 )
33from lp.registry.interfaces.pillar import IPillarNameSet35from lp.registry.interfaces.pillar import IPillarNameSet
3436
3537
@@ -47,10 +49,10 @@
47 as the context, or49 as the context, or
48 - the full URL of the spec, in which case it can be any spec at all.50 - the full URL of the spec, in which case it can be any spec at all.
4951
50 For the purposes of enumeration and searching we only consider the first52 For the purposes of enumeration and searching we look at all the possible
51 sort of spec for now. The URL form of token only matches precisely,53 specifications, but order those of the same target first. If there is an
52 searching only looks for specs on the current target if the search term is54 associated series as well, then those are shown before other matches not
53 not a URL.55 linked to the same series.
54 """56 """
5557
56 implements(IHugeVocabulary)58 implements(IHugeVocabulary)
@@ -60,30 +62,60 @@
60 displayname = 'Select a blueprint'62 displayname = 'Select a blueprint'
61 step_title = 'Search'63 step_title = 'Search'
6264
63 def _is_valid_candidate(self, spec, check_target=False):65 def _is_valid_candidate(self, spec):
64 """Is `spec` a valid candidate spec for self.context?66 """Is `spec` a valid candidate spec for self.context?
6567
66 Invalid candidates are:68 Invalid candidates are:
6769
68 * The spec that we're adding a depdency to,70 * None
69 * Specs for a different target, and71 * The spec that we're adding a depdency to
70 * Specs that depend on this one.72 * Specs that depend on this one
7173
72 Preventing the last category prevents loops in the dependency graph.74 Preventing the last category prevents loops in the dependency graph.
73 """75 """
74 if check_target and spec.target != self.context.target:76 if spec is None or spec == self.context:
75 return False77 return False
76 return spec != self.context and spec not in set(self.context.all_blocked)78 return spec not in set(self.context.all_blocked)
7779
78 def _filter_specs(self, specs, check_target=False):80 def _order_by(self):
79 """Filter `specs` to remove invalid candidates.81 """Look at the context to provide grouping.
8082
81 See `_is_valid_candidate` for what an invalid candidate is.83 If the blueprint is for a project, then matching results for that
84 project should be first. If the blueprint is set for a series, then
85 that series should come before others for the project. Similarly for
86 the distribution, and the series goal for the distribution.
87
88 If all else is equal, the ordering is by name, then database id as a
89 final uniqueness resolver.
82 """90 """
83 # XXX intellectronica 2007-07-05: is 100 a reasonable count before91 order_statements = []
84 # starting to warn?92 spec = self.context
85 return [spec for spec in shortlist(specs, 100)93 if spec.product is not None:
86 if self._is_valid_candidate(spec, check_target)]94 order_statements.append(
95 "(CASE Specification.product WHEN %s THEN 0 ELSE 1 END)" %
96 spec.product.id)
97 if spec.productseries is not None:
98 order_statements.append(
99 "(CASE Specification.productseries"
100 " WHEN %s THEN 0 ELSE 1 END)" %
101 spec.productseries.id)
102 elif spec.distribution is not None:
103 order_statements.append(
104 "(CASE Specification.distribution WHEN %s THEN 0 ELSE 1 END)"
105 % spec.distribution.id)
106 if spec.distroseries is not None:
107 order_statements.append(
108 "(CASE Specification.distroseries"
109 " WHEN %s THEN 0 ELSE 1 END)" %
110 spec.distroseries.id)
111 order_statements.append("Specification.name")
112 order_statements.append("Specification.id")
113 return SQL(', '.join(order_statements))
114
115 def _exclude_blocked_query(self):
116 """Return the select statement to exclude already blocked specs."""
117 return SQL("Specification.id not in (WITH %s select id from blocked)"
118 % recursive_blocked_query(self.context))
87119
88 def toTerm(self, obj):120 def toTerm(self, obj):
89 if obj.target == self.context.target:121 if obj.target == self.context.target:
@@ -127,7 +159,7 @@
127 spec = self._spec_from_url(token)159 spec = self._spec_from_url(token)
128 if spec is None:160 if spec is None:
129 spec = self.context.target.getSpecification(token)161 spec = self.context.target.getSpecification(token)
130 if spec and self._is_valid_candidate(spec):162 if self._is_valid_candidate(spec):
131 return self.toTerm(spec)163 return self.toTerm(spec)
132 raise LookupError(token)164 raise LookupError(token)
133165
@@ -141,27 +173,18 @@
141 if not query:173 if not query:
142 return CountableIterator(0, [])174 return CountableIterator(0, [])
143 spec = self._spec_from_url(query)175 spec = self._spec_from_url(query)
144 if spec is not None and self._is_valid_candidate(spec):176 if self._is_valid_candidate(spec):
145 return CountableIterator(1, [spec])177 return CountableIterator(1, [spec])
146 quoted_query = quote_like(query)
147 sql_query = ("""
148 (Specification.name LIKE %s OR
149 Specification.title LIKE %s OR
150 fti @@ ftq(%s))
151 """
152 % (quoted_query, quoted_query, quoted_query))
153 all_specs = Specification.select(sql_query, orderBy=self._orderBy)
154 candidate_specs = self._filter_specs(all_specs, check_target=True)
155 return CountableIterator(len(candidate_specs), candidate_specs)
156178
157 @property179 return Store.of(self.context).find(
158 def _all_specs(self):180 Specification,
159 return self.context.target.specifications(181 SQL('Specification.fti @@ ftq(%s)' % quote(query)),
160 filter=[SpecificationFilter.ALL], prejoin_people=False)182 self._exclude_blocked_query(),
183 ).order_by(self._order_by())
161184
162 def __iter__(self):185 def __iter__(self):
163 return (186 # We don't ever want to iterate over everything.
164 self.toTerm(spec) for spec in self._filter_specs(self._all_specs))187 raise NotImplementedError()
165188
166 def __contains__(self, obj):189 def __contains__(self, obj):
167 return self._is_valid_candidate(obj)190 return self._is_valid_candidate(obj)
168191
=== removed file 'lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt'
--- lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 2011-03-15 02:39:51 +0000
+++ lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 1970-01-01 00:00:00 +0000
@@ -1,97 +0,0 @@
1SpecificationDepCandidatesVocabulary
2====================================
3
4All blueprints that can be added as a dependency of the context
5blueprint.
6
7 >>> from zope.schema.vocabulary import getVocabularyRegistry
8 >>> vocabulary_registry = getVocabularyRegistry()
9
10First, we set up a product with three blueprints.
11
12 >>> specced_product = factory.makeProduct()
13 >>> spec_a = factory.makeSpecification(
14 ... name='spec-a', summary='The first spec',
15 ... product=specced_product)
16 >>> spec_b = factory.makeSpecification(
17 ... name='spec-b', summary='The second spec',
18 ... product=specced_product)
19 >>> spec_c = factory.makeSpecification(
20 ... name='spec-c', summary='The third spec',
21 ... product=specced_product)
22 >>> sorted([spec.name for spec in specced_product.specifications()])
23 [u'spec-a', u'spec-b', u'spec-c']
24
25
26Iterating over the vocabulary gives all blueprints for specced_product
27except for spec_a itself.
28
29 >>> vocab = vocabulary_registry.get(
30 ... spec_a, "SpecificationDepCandidates")
31 >>> sorted([term.value.name for term in vocab])
32 [u'spec-b', u'spec-c']
33
34Blueprints for other targets are considered to be 'in' the vocabulary
35though.
36
37 >>> unrelated_spec = factory.makeSpecification(
38 ... product=factory.makeProduct())
39 >>> vocab = vocabulary_registry.get(
40 ... spec_a, "SpecificationDepCandidates")
41 >>> unrelated_spec in vocab
42 True
43
44We mark spec_b as a dependency of spec_a and spec_c as a dependency of
45spec_b.
46
47 >>> spec_a.createDependency(spec_b)
48 <SpecificationDependency at ...>
49 >>> [spec.name for spec in spec_a.dependencies]
50 [u'spec-b']
51
52 >>> spec_b.createDependency(spec_c)
53 <SpecificationDependency at ...>
54 >>> [spec.name for spec in spec_b.dependencies]
55 [u'spec-c']
56
57No circular dependencies - the vocabulary excludes specifications that
58are a dependency of the context spec.
59
60 >>> spec_a in set(spec_b.all_blocked)
61 True
62 >>> spec_b in set(spec_c.all_blocked)
63 True
64 >>> vocab = vocabulary_registry.get(
65 ... spec_c, "SpecificationDepCandidates")
66 >>> spec_a in [term.value for term in vocab]
67 False
68
69This vocabulary provides the IHugeVocabulary interface.
70
71 >>> from canonical.launchpad.webapp.testing import verifyObject
72 >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
73 >>> verifyObject(IHugeVocabulary, vocab)
74 True
75
76The search() method returns specifications within the vocabulary that
77matches the search string. The string is matched against the name, or
78fallbacks to a full text search.
79
80 >>> from zope.security.proxy import removeSecurityProxy
81 >>> naked_vocab = removeSecurityProxy(
82 ... vocabulary_registry.get(
83 ... spec_a, "SpecificationDepCandidates"))
84 >>> list(naked_vocab.search('spec-b')) == [spec_b]
85 True
86 >>> list(naked_vocab.search('third')) == [spec_c]
87 True
88
89The search method uses the SQL `LIKE` operator, with the values quoted
90appropriately. Queries conataining regual expression operators, for
91example, will simply look for the respective characters within the
92vocabulary's item (this used to be the cause of an OOPS, see
93https://bugs.launchpad.net/blueprint/+bug/139385 for more
94details).
95
96 >>> list(naked_vocab.search('*'))
97 []
980
=== removed file 'lib/lp/blueprints/vocabularies/tests/test_doc.py'
--- lib/lp/blueprints/vocabularies/tests/test_doc.py 2010-08-25 05:17:20 +0000
+++ lib/lp/blueprints/vocabularies/tests/test_doc.py 1970-01-01 00:00:00 +0000
@@ -1,17 +0,0 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""
5Run the doctests.
6"""
7
8import os
9
10from lp.services.testing import build_doctest_suite
11
12
13here = os.path.dirname(os.path.realpath(__file__))
14
15
16def test_suite():
17 return build_doctest_suite(here, '')
180
=== modified file 'lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py'
--- lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2010-10-04 19:50:45 +0000
+++ lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2011-03-15 08:46:24 +0000
@@ -176,3 +176,72 @@
176 vocab = self.getVocabularyForSpec(spec)176 vocab = self.getVocabularyForSpec(spec)
177 term = vocab.getTermByToken(canonical_url(candidate))177 term = vocab.getTermByToken(canonical_url(candidate))
178 self.assertEqual(term.token, canonical_url(candidate))178 self.assertEqual(term.token, canonical_url(candidate))
179
180 def test_searchForTerms_ordering_different_targets_by_name(self):
181 # If the searched for specs are on different targets, the ordering is
182 # by name.
183 spec = self.factory.makeSpecification()
184 foo_b = self.factory.makeSpecification(name='foo-b')
185 foo_a = self.factory.makeSpecification(name='foo-a')
186 vocab = self.getVocabularyForSpec(spec)
187 results = vocab.searchForTerms('foo')
188 self.assertEqual(2, len(results))
189 found = [item.value for item in results]
190 self.assertEqual([foo_a, foo_b], found)
191
192 def test_searchForTerms_ordering_same_product_first(self):
193 # Specs on the same product are returned first.
194 widget = self.factory.makeProduct()
195 spec = self.factory.makeSpecification(product=widget)
196 foo_b = self.factory.makeSpecification(name='foo-b', product=widget)
197 foo_a = self.factory.makeSpecification(name='foo-a')
198 vocab = self.getVocabularyForSpec(spec)
199 results = vocab.searchForTerms('foo')
200 self.assertEqual(2, len(results))
201 found = [item.value for item in results]
202 self.assertEqual([foo_b, foo_a], found)
203
204 def test_searchForTerms_ordering_same_productseries_first(self):
205 # Specs on the same product series are returned before the same
206 # products, and those before others.
207 widget = self.factory.makeProduct()
208 spec = self.factory.makeSpecification(product=widget)
209 spec.proposeGoal(widget.development_focus, widget.owner)
210 foo_c = self.factory.makeSpecification(name='foo-c', product=widget)
211 foo_c.proposeGoal(widget.development_focus, widget.owner)
212 foo_b = self.factory.makeSpecification(name='foo-b', product=widget)
213 foo_a = self.factory.makeSpecification(name='foo-a')
214 vocab = self.getVocabularyForSpec(spec)
215 results = vocab.searchForTerms('foo')
216 self.assertEqual(3, len(results))
217 found = [item.value for item in results]
218 self.assertEqual([foo_c, foo_b, foo_a], found)
219
220 def test_searchForTerms_ordering_same_distribution_first(self):
221 # Specs on the same distribution are returned first.
222 mint = self.factory.makeDistribution()
223 spec = self.factory.makeSpecification(distribution=mint)
224 foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint)
225 foo_a = self.factory.makeSpecification(name='foo-a')
226 vocab = self.getVocabularyForSpec(spec)
227 results = vocab.searchForTerms('foo')
228 self.assertEqual(2, len(results))
229 found = [item.value for item in results]
230 self.assertEqual([foo_b, foo_a], found)
231
232 def test_searchForTerms_ordering_same_distroseries_first(self):
233 # Specs on the same distroseries are returned before the same
234 # distribution, and those before others.
235 mint = self.factory.makeDistribution()
236 next = self.factory.makeDistroSeries(mint)
237 spec = self.factory.makeSpecification(distribution=mint)
238 spec.proposeGoal(next, mint.owner)
239 foo_c = self.factory.makeSpecification(name='foo-c', distribution=mint)
240 foo_c.proposeGoal(next, mint.owner)
241 foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint)
242 foo_a = self.factory.makeSpecification(name='foo-a')
243 vocab = self.getVocabularyForSpec(spec)
244 results = vocab.searchForTerms('foo')
245 self.assertEqual(3, len(results))
246 found = [item.value for item in results]
247 self.assertEqual([foo_c, foo_b, foo_a], found)