← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Tim Penhey has proposed merging lp:~thumper/launchpad/blueprint-dependency-vocabulary into lp:launchpad with lp:~thumper/launchpad/blueprint-dependencies as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #707111 in Launchpad itself: "Blueprint dependency picker doesn't allow searching outside the target"
  https://bugs.launchpad.net/launchpad/+bug/707111

For more details, see:
https://code.launchpad.net/~thumper/launchpad/blueprint-dependency-vocabulary/+merge/53192

Allow the vocabulary to work with any dependency.
-- 
https://code.launchpad.net/~thumper/launchpad/blueprint-dependency-vocabulary/+merge/53192
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/blueprint-dependency-vocabulary into lp:launchpad.
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2011-03-14 03:22:42 +0000
+++ lib/lp/blueprints/model/specification.py	2011-03-14 03:22:43 +0000
@@ -671,6 +671,9 @@
         spec_branch = self.getBranchLink(branch)
         spec_branch.destroySelf()
 
+    def __repr__(self):
+        return '<Specification %r for %r>' % (self.name, self.target.name)
+
 
 class HasSpecificationsMixin:
     """A mixin class that implements many of the common shortcut properties

=== modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py'
--- lib/lp/blueprints/vocabularies/specificationdependency.py	2010-12-01 18:58:44 +0000
+++ lib/lp/blueprints/vocabularies/specificationdependency.py	2011-03-14 03:22:43 +0000
@@ -11,12 +11,12 @@
 
 from operator import attrgetter
 
+from storm.locals import Not, SQL, Store
 from zope.component import getUtility
 from zope.interface import implements
 from zope.schema.vocabulary import SimpleTerm
 
-from canonical.database.sqlbase import quote_like
-from canonical.launchpad.helpers import shortlist
+from canonical.database.sqlbase import quote
 from canonical.launchpad.webapp import (
     canonical_url,
     urlparse,
@@ -27,9 +27,11 @@
     NamedSQLObjectVocabulary,
     SQLObjectVocabularyBase,
     )
-
-from lp.blueprints.enums import SpecificationFilter
-from lp.blueprints.model.specification import Specification
+from lp.blueprints.interfaces.specification import ISpecification
+from lp.blueprints.model.specification import (
+    recursive_blocked_query,
+    Specification,
+    )
 from lp.registry.interfaces.pillar import IPillarNameSet
 
 
@@ -47,10 +49,10 @@
        as the context, or
      - the full URL of the spec, in which case it can be any spec at all.
 
-    For the purposes of enumeration and searching we only consider the first
-    sort of spec for now.  The URL form of token only matches precisely,
-    searching only looks for specs on the current target if the search term is
-    not a URL.
+    For the purposes of enumeration and searching we look at all the possible
+    specifications, but order those of the same target first.  If there is an
+    associated series as well, then those are shown before other matches not
+    linked to the same series.
     """
 
     implements(IHugeVocabulary)
@@ -60,30 +62,51 @@
     displayname = 'Select a blueprint'
     step_title = 'Search'
 
-    def _is_valid_candidate(self, spec, check_target=False):
+    def _is_valid_candidate(self, spec):
         """Is `spec` a valid candidate spec for self.context?
 
         Invalid candidates are:
 
-         * The spec that we're adding a depdency to,
-         * Specs for a different target, and
-         * Specs that depend on this one.
+         * Anything not a specification
+         * The spec that we're adding a depdency to
+         * Specs that depend on this one
 
         Preventing the last category prevents loops in the dependency graph.
         """
-        if check_target and spec.target != self.context.target:
+        if ISpecification(spec, None) is None:
             return False
-        return spec != self.context and spec not in self.context.all_blocked
-
-    def _filter_specs(self, specs, check_target=False):
-        """Filter `specs` to remove invalid candidates.
-
-        See `_is_valid_candidate` for what an invalid candidate is.
-        """
-        # XXX intellectronica 2007-07-05: is 100 a reasonable count before
-        # starting to warn?
-        return [spec for spec in shortlist(specs, 100)
-                if self._is_valid_candidate(spec, check_target)]
+        return spec != self.context and spec not in set(self.context.all_blocked)
+
+    def _order_by(self):
+        """Look at the context to provide grouping."""
+        order_statements = []
+        spec = self.context
+        if spec.product is not None:
+            order_statements.append(
+                "(CASE Specification.product WHEN %s THEN 0 ELSE 1 END)" %
+                spec.product.id)
+            if spec.productseries is not None:
+                order_statements.append(
+                    "(CASE Specification.productseries"
+                    " WHEN %s THEN 0 ELSE 1 END)" %
+                    spec.productseries.id)
+        elif spec.distribution is not None:
+            order_statements.append(
+                "(CASE Specification.distribution WHEN %s THEN 0 ELSE 1 END)" %
+                spec.distribution.id)
+            if spec.distroseries is not None:
+                order_statements.append(
+                    "(CASE Specification.distroseries"
+                    " WHEN %s THEN 0 ELSE 1 END)" %
+                    spec.distroseries.id)
+        order_statements.append("Specification.name")
+        order_statements.append("Specification.id")
+        return SQL(', '.join(order_statements))
+
+    def _blocked_subselect(self):
+        """Return the select statement to exclude already blocked specs."""
+        return SQL("Specification.id not in (WITH %s select id from blocked)"
+                   % recursive_blocked_query(self.context))
 
     def toTerm(self, obj):
         if obj.target == self.context.target:
@@ -127,7 +150,7 @@
         spec = self._spec_from_url(token)
         if spec is None:
             spec = self.context.target.getSpecification(token)
-        if spec and self._is_valid_candidate(spec):
+        if self._is_valid_candidate(spec):
             return self.toTerm(spec)
         raise LookupError(token)
 
@@ -141,27 +164,18 @@
         if not query:
             return CountableIterator(0, [])
         spec = self._spec_from_url(query)
-        if spec is not None and self._is_valid_candidate(spec):
+        if self._is_valid_candidate(spec):
             return CountableIterator(1, [spec])
-        quoted_query = quote_like(query)
-        sql_query = ("""
-            (Specification.name LIKE %s OR
-             Specification.title LIKE %s OR
-             fti @@ ftq(%s))
-            """
-            % (quoted_query, quoted_query, quoted_query))
-        all_specs = Specification.select(sql_query, orderBy=self._orderBy)
-        candidate_specs = self._filter_specs(all_specs, check_target=True)
-        return CountableIterator(len(candidate_specs), candidate_specs)
 
-    @property
-    def _all_specs(self):
-        return self.context.target.specifications(
-            filter=[SpecificationFilter.ALL], prejoin_people=False)
+        return Store.of(self.context).find(
+            Specification,
+            SQL('Specification.fti @@ ftq(%s)' % quote(query)),
+            self._blocked_subselect(),
+            ).order_by(self._order_by())
 
     def __iter__(self):
-        return (
-            self.toTerm(spec) for spec in self._filter_specs(self._all_specs))
+        # We don't ever want to iterate over everything.
+        raise NotImplementedError()
 
     def __contains__(self, obj):
         return self._is_valid_candidate(obj)

=== 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-14 03:22:43 +0000
@@ -176,3 +176,72 @@
         vocab = self.getVocabularyForSpec(spec)
         term = vocab.getTermByToken(canonical_url(candidate))
         self.assertEqual(term.token, canonical_url(candidate))
+
+    def test_searchForTerms_ordering_different_targets_by_name(self):
+        # If the searched for specs are on different targets, the ordering is
+        # by name.
+        spec = self.factory.makeSpecification()
+        foo_b = self.factory.makeSpecification(name='foo-b')
+        foo_a = self.factory.makeSpecification(name='foo-a')
+        vocab = self.getVocabularyForSpec(spec)
+        results = vocab.searchForTerms('foo')
+        self.assertEqual(2, len(results))
+        found = [item.value for item in results]
+        self.assertEqual([foo_a, foo_b], found)
+
+    def test_searchForTerms_ordering_same_product_first(self):
+        # Specs on the same product are returned first.
+        widget = self.factory.makeProduct()
+        spec = self.factory.makeSpecification(product=widget)
+        foo_b = self.factory.makeSpecification(name='foo-b', product=widget)
+        foo_a = self.factory.makeSpecification(name='foo-a')
+        vocab = self.getVocabularyForSpec(spec)
+        results = vocab.searchForTerms('foo')
+        self.assertEqual(2, len(results))
+        found = [item.value for item in results]
+        self.assertEqual([foo_b, foo_a], found)
+
+    def test_searchForTerms_ordering_same_productseries_first(self):
+        # Specs on the same product series are returned before the same
+        # products, and those before others.
+        widget = self.factory.makeProduct()
+        spec = self.factory.makeSpecification(product=widget)
+        spec.proposeGoal(widget.development_focus, widget.owner)
+        foo_c = self.factory.makeSpecification(name='foo-c', product=widget)
+        foo_c.proposeGoal(widget.development_focus, widget.owner)
+        foo_b = self.factory.makeSpecification(name='foo-b', product=widget)
+        foo_a = self.factory.makeSpecification(name='foo-a')
+        vocab = self.getVocabularyForSpec(spec)
+        results = vocab.searchForTerms('foo')
+        self.assertEqual(3, len(results))
+        found = [item.value for item in results]
+        self.assertEqual([foo_c, foo_b, foo_a], found)
+
+    def test_searchForTerms_ordering_same_distribution_first(self):
+        # Specs on the same distribution are returned first.
+        mint = self.factory.makeDistribution()
+        spec = self.factory.makeSpecification(distribution=mint)
+        foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint)
+        foo_a = self.factory.makeSpecification(name='foo-a')
+        vocab = self.getVocabularyForSpec(spec)
+        results = vocab.searchForTerms('foo')
+        self.assertEqual(2, len(results))
+        found = [item.value for item in results]
+        self.assertEqual([foo_b, foo_a], found)
+
+    def test_searchForTerms_ordering_same_distroseries_first(self):
+        # Specs on the same distroseries are returned before the same
+        # distribution, and those before others.
+        mint = self.factory.makeDistribution()
+        next = self.factory.makeDistroSeries(mint)
+        spec = self.factory.makeSpecification(distribution=mint)
+        spec.proposeGoal(next, mint.owner)
+        foo_c = self.factory.makeSpecification(name='foo-c', distribution=mint)
+        foo_c.proposeGoal(next, mint.owner)
+        foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint)
+        foo_a = self.factory.makeSpecification(name='foo-a')
+        vocab = self.getVocabularyForSpec(spec)
+        results = vocab.searchForTerms('foo')
+        self.assertEqual(3, len(results))
+        found = [item.value for item in results]
+        self.assertEqual([foo_c, foo_b, foo_a], found)