← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/bugs-with-blueprints-bug-707103 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/bugs-with-blueprints-bug-707103 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #707103 Unable to search for bugs that are attached to blueprints
  https://bugs.launchpad.net/bugs/707103

For more details, see:
https://code.launchpad.net/~allenap/launchpad/bugs-with-blueprints-bug-707103/+merge/47866

Make it possible to search for bugs with or without linked Blueprints. I will update the advanced search in a subsequent branch; this branch just deals with the internal and webservice API.
-- 
https://code.launchpad.net/~allenap/launchpad/bugs-with-blueprints-bug-707103/+merge/47866
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/bugs-with-blueprints-bug-707103 into lp:launchpad.
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py	2010-12-02 16:14:30 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py	2011-01-28 21:51:34 +0000
@@ -35,7 +35,6 @@
     )
 from lazr.restful.fields import Reference
 from lazr.restful.interface import copy_field
-
 from zope.interface import (
     Attribute,
     Interface,
@@ -52,6 +51,7 @@
 
 from canonical.launchpad import _
 from lp.bugs.interfaces.bugtask import (
+    BugBlueprintSearch,
     BugBranchSearch,
     BugTagsSearchCombinator,
     IBugTask,
@@ -59,7 +59,8 @@
     )
 from lp.services.fields import Tag
 
-search_tasks_params_for_api_1_0 = {
+
+search_tasks_params_common = {
     "order_by": List(
         title=_('List of fields by which the results are ordered.'),
         value_type=Text(),
@@ -80,7 +81,6 @@
     "tags": copy_field(IBugTaskSearch['tag']),
     "tags_combinator": copy_field(IBugTaskSearch['tags_combinator']),
     "omit_duplicates": copy_field(IBugTaskSearch['omit_dupes']),
-    "omit_targeted": copy_field(IBugTaskSearch['omit_targeted']),
     "status_upstream": copy_field(IBugTaskSearch['status_upstream']),
     "milestone_assignment": copy_field(
         IBugTaskSearch['milestone_assignment']),
@@ -172,9 +172,21 @@
             "date."),
         required=False),
     }
-search_tasks_params_for_api_devel = search_tasks_params_for_api_1_0.copy()
-search_tasks_params_for_api_devel["omit_targeted"] = copy_field(
-    IBugTaskSearch['omit_targeted'], default=False)
+
+search_tasks_params_for_api_default = dict(
+    search_tasks_params_common,
+    omit_targeted=copy_field(
+        IBugTaskSearch['omit_targeted']))
+
+search_tasks_params_for_api_devel = dict(
+    search_tasks_params_common,
+    omit_targeted=copy_field(
+        IBugTaskSearch['omit_targeted'], default=False),
+    linked_blueprints=Choice(
+        title=_(
+            u"Search for bugs that are linked to blueprints or for "
+            u"bugs that are not linked to branches."),
+        vocabulary=BugBlueprintSearch, required=False))
 
 
 class IHasBugs(Interface):
@@ -203,16 +215,21 @@
     has_bugtasks = Attribute(
         "True if a BugTask has ever been reported for this target.")
 
+    # searchTasks devel API declaration.
     @call_with(search_params=None, user=REQUEST_USER)
     @operation_parameters(**search_tasks_params_for_api_devel)
     @operation_returns_collection_of(IBugTask)
     @export_read_operation()
+
+    # Pop the *default* version (decorators are run last to first).
     @operation_removed_in_version('devel')
 
+    # searchTasks default API declaration.
     @call_with(search_params=None, user=REQUEST_USER)
-    @operation_parameters(**search_tasks_params_for_api_1_0)
+    @operation_parameters(**search_tasks_params_for_api_default)
     @operation_returns_collection_of(IBugTask)
     @export_read_operation()
+
     def searchTasks(search_params, user=None,
                     order_by=None, search_text=None,
                     status=None, importance=None,
@@ -232,8 +249,8 @@
                     hardware_owner_is_affected_by_bug=False,
                     hardware_owner_is_subscribed_to_bug=False,
                     hardware_is_linked_to_bug=False, linked_branches=None,
-                    structural_subscriber=None, modified_since=None,
-                    created_since=None, prejoins=[]):
+                    linked_blueprints=None, structural_subscriber=None,
+                    modified_since=None, created_since=None, prejoins=[]):
         """Search the IBugTasks reported on this entity.
 
         :search_params: a BugTaskSearchParams object

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2010-12-17 04:26:11 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2011-01-28 21:51:34 +0000
@@ -9,6 +9,7 @@
 
 __all__ = [
     'BUG_SUPERVISOR_BUGTASK_STATUSES',
+    'BugBlueprintSearch',
     'BugBranchSearch',
     'BugTagsSearchCombinator',
     'BugTaskImportance',
@@ -28,8 +29,6 @@
     'IDistroBugTask',
     'IDistroSeriesBugTask',
     'IFrontPageBugTaskSearch',
-    'IllegalRelatedBugTasksParams',
-    'IllegalTarget',
     'INominationsReviewTableBatchNavigator',
     'INullBugTask',
     'IPersonBugTaskSearch',
@@ -37,13 +36,16 @@
     'IRemoveQuestionFromBugTaskForm',
     'IUpstreamBugTask',
     'IUpstreamProductBugTaskSearch',
+    'IllegalRelatedBugTasksParams',
+    'IllegalTarget',
     'RESOLVED_BUGTASK_STATUSES',
     'UNRESOLVED_BUGTASK_STATUSES',
     'UserCannotEditBugTaskAssignee',
     'UserCannotEditBugTaskImportance',
     'UserCannotEditBugTaskMilestone',
     'UserCannotEditBugTaskStatus',
-    'valid_remote_bug_url']
+    'valid_remote_bug_url',
+    ]
 
 from lazr.enum import (
     DBEnumeratedType,
@@ -340,7 +342,7 @@
     """Bug branch search option.
 
     The possible values to search for bugs having branches attached
-    or not having branches attched.
+    or not having branches attached.
     """
 
     ALL = Item("Show all bugs")
@@ -350,6 +352,20 @@
     BUGS_WITHOUT_BRANCHES = Item("Show only Bugs without linked Branches")
 
 
+class BugBlueprintSearch(EnumeratedType):
+    """Bug blueprint search option.
+
+    The possible values to search for bugs having blueprints attached
+    or not having blueprints attached.
+    """
+
+    ALL = Item("Show all bugs")
+
+    BUGS_WITH_BLUEPRINTS = Item("Show only Bugs with linked Blueprints")
+
+    BUGS_WITHOUT_BLUEPRINTS = Item("Show only Bugs without linked Blueprints")
+
+
 # XXX: Brad Bollenbach 2005-12-02 bugs=5320:
 # In theory, INCOMPLETE belongs in UNRESOLVED_BUGTASK_STATUSES, but the
 # semantics of our current reports would break if it were added to the
@@ -1144,9 +1160,9 @@
                  hardware_owner_is_affected_by_bug=False,
                  hardware_owner_is_subscribed_to_bug=False,
                  hardware_is_linked_to_bug=False,
-                 linked_branches=None, structural_subscriber=None,
-                 modified_since=None, created_since=None,
-                 exclude_conjoined_tasks=False):
+                 linked_branches=None, linked_blueprints=None,
+                 structural_subscriber=None, modified_since=None,
+                 created_since=None, exclude_conjoined_tasks=False):
 
         self.bug = bug
         self.searchtext = searchtext
@@ -1189,6 +1205,7 @@
             hardware_owner_is_subscribed_to_bug)
         self.hardware_is_linked_to_bug = hardware_is_linked_to_bug
         self.linked_branches = linked_branches
+        self.linked_blueprints = linked_blueprints
         self.structural_subscriber = structural_subscriber
         self.modified_since = modified_since
         self.created_since = created_since
@@ -1265,8 +1282,8 @@
                        hardware_owner_is_affected_by_bug=False,
                        hardware_owner_is_subscribed_to_bug=False,
                        hardware_is_linked_to_bug=False, linked_branches=None,
-                       structural_subscriber=None, modified_since=None,
-                       created_since=None):
+                       linked_blueprints=None, structural_subscriber=None,
+                       modified_since=None, created_since=None):
         """Create and return a new instance using the parameter list."""
         search_params = cls(user=user, orderby=order_by)
 
@@ -1332,7 +1349,8 @@
             hardware_owner_is_subscribed_to_bug)
         search_params.hardware_is_linked_to_bug = (
             hardware_is_linked_to_bug)
-        search_params.linked_branches=linked_branches
+        search_params.linked_branches = linked_branches
+        search_params.linked_blueprints = linked_blueprints
         search_params.structural_subscriber = structural_subscriber
         search_params.modified_since = modified_since
         search_params.created_since = created_since

=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py	2010-11-30 21:39:25 +0000
+++ lib/lp/bugs/model/bugtarget.py	2011-01-28 21:51:34 +0000
@@ -94,7 +94,8 @@
                     hardware_owner_is_affected_by_bug=False,
                     hardware_owner_is_subscribed_to_bug=False,
                     hardware_is_linked_to_bug=False, linked_branches=None,
-                    modified_since=None, created_since=None, prejoins=[]):
+                    linked_blueprints=None, modified_since=None,
+                    created_since=None, prejoins=[]):
         """See `IHasBugs`."""
         if status is None:
             # If no statuses are supplied, default to the

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2011-01-20 04:50:35 +0000
+++ lib/lp/bugs/model/bugtask.py	2011-01-28 21:51:34 +0000
@@ -103,6 +103,7 @@
 from lp.bugs.interfaces.bugtask import (
     BUG_SUPERVISOR_BUGTASK_STATUSES,
     BugBranchSearch,
+    BugBlueprintSearch,
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
@@ -2066,6 +2067,10 @@
             # we don't need to add any clause.
             pass
 
+        linked_blueprints_clause = self._buildBlueprintRelatedClause(params)
+        if linked_blueprints_clause is not None:
+            extra_clauses.append(linked_blueprints_clause)
+
         if params.modified_since:
             extra_clauses.append(
                 "Bug.date_last_updated > %s" % (
@@ -2318,6 +2323,21 @@
             ', '.join(tables), ' AND '.join(clauses))
         return clause
 
+    def _buildBlueprintRelatedClause(self, params):
+        """Find bugs related to Blueprints, or not."""
+        if params.linked_blueprints == (
+            BugBlueprintSearch.BUGS_WITH_BLUEPRINTS):
+            return "EXISTS (%s)" % (
+                "SELECT 1 FROM SpecificationBug"
+                " WHERE SpecificationBug.bug = Bug.id")
+        elif params.linked_blueprints == (
+            BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS):
+            return "NOT EXISTS (%s)" % (
+                "SELECT 1 FROM SpecificationBug"
+                " WHERE SpecificationBug.bug = Bug.id")
+        else:
+            return None
+
     def buildOrigin(self, join_tables, prejoin_tables, clauseTables):
         """Build the parameter list for Store.using().
 

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2011-01-13 17:00:41 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2011-01-28 21:51:34 +0000
@@ -31,6 +31,7 @@
 from lp.bugs.interfaces.bugattachment import BugAttachmentType
 from lp.bugs.interfaces.bugtask import (
     BugBranchSearch,
+    BugBlueprintSearch,
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
@@ -445,6 +446,23 @@
             user=None, linked_branches=BugBranchSearch.BUGS_WITHOUT_BRANCHES)
         self.assertSearchFinds(params, self.bugtasks[1:])
 
+    def test_blueprints_linked(self):
+        # Search results can be limited to bugs with or without linked
+        # blueprints.
+        with person_logged_in(self.owner):
+            blueprint = self.factory.makeSpecification()
+            blueprint.linkBug(self.bugtasks[0].bug)
+        params = self.getBugTaskSearchParams(
+            user=None, linked_blueprints=(
+                BugBlueprintSearch.BUGS_WITH_BLUEPRINTS))
+        self.assertSearchFinds(params, self.bugtasks[:1])
+        params = self.getBugTaskSearchParams(
+            user=None, linked_blueprints=(
+                BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS))
+        self.assertSearchFinds(params, self.bugtasks[1:])
+        params = self.getBugTaskSearchParams(user=None)
+        self.assertSearchFinds(params, self.bugtasks)
+
     def test_limit_search_to_one_bug(self):
         # Search results can be limited to a given bug.
         params = self.getBugTaskSearchParams(

=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
--- lib/lp/bugs/tests/test_searchtasks_webservice.py	2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/tests/test_searchtasks_webservice.py	2011-01-28 21:51:34 +0000
@@ -6,25 +6,28 @@
 __metaclass__ = type
 
 from canonical.launchpad.ftests import login
-from lp.testing import TestCaseWithFactory
 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
 from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.testing import (
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 
 
 class TestOmitTargetedParameter(TestCaseWithFactory):
     """Test all values for the omit_targeted search parameter."""
+
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
         TestCaseWithFactory.setUp(self)
         login('foo.bar@xxxxxxxxxxxxx')
         self.distro = self.factory.makeDistribution(name='mebuntu')
-        self.release = self.factory.makeDistroRelease(name='inkanyamba',
-            distribution=self.distro)
+        self.release = self.factory.makeDistroRelease(
+            name='inkanyamba', distribution=self.distro)
         self.bug = self.factory.makeBugTask(target=self.release)
-
-        self.webservice = LaunchpadWebServiceCaller('launchpad-library',
-            'salgado-change-anything')
+        self.webservice = LaunchpadWebServiceCaller(
+            'launchpad-library', 'salgado-change-anything')
 
     def test_omit_targeted_old_default_true(self):
         response = self.webservice.named_get('/mebuntu/inkanyamba',
@@ -35,3 +38,50 @@
         response = self.webservice.named_get('/mebuntu/inkanyamba',
             'searchTasks', api_version='devel').jsonBody()
         self.assertEqual(response['total_size'], 1)
+
+    def test_linked_blueprints_in_devel(self):
+        response = self.webservice.named_get('/mebuntu/inkanyamba',
+            'searchTasks', api_version='devel').jsonBody()
+        self.assertEqual(response['total_size'], 1)
+
+
+class TestLinkedBlueprintsParameter(TestCaseWithFactory):
+    """Tests for the linked_blueprints parameter."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestLinkedBlueprintsParameter, self).setUp()
+        TestCaseWithFactory.setUp(self)
+        self.owner = self.factory.makePerson()
+        with person_logged_in(self.owner):
+            self.product = self.factory.makeProduct()
+        self.bug = self.factory.makeBugTask(target=self.product)
+        self.webservice = LaunchpadWebServiceCaller(
+            'launchpad-library', 'salgado-change-anything')
+
+    def search(self, api_version, **kwargs):
+        return self.webservice.named_get(
+            '/%s' % self.product.name, 'searchTasks',
+            api_version=api_version, **kwargs).jsonBody()
+
+    def test_linked_blueprints_in_devel(self):
+        # Searching for linked Blueprints works in the devel API.
+        self.search("devel", linked_blueprints="Show all bugs")
+        # If linked_blueprints is not a member of BugBlueprintSearch an
+        # error is returned.
+        self.assertRaises(
+            ValueError, self.search, "devel",
+            linked_blueprints="Teabags!")
+
+    def test_linked_blueprints_not_in_1_0(self):
+        # Searching for linked Blueprints does not work in the 1.0 API. No
+        # validation is performed for the linked_blueprints parameter, and
+        # thus no error is returned when we pass rubbish.
+        self.search("1.0", linked_blueprints="Teabags!")
+
+    def test_linked_blueprints_not_in_beta(self):
+        # Searching for linked Blueprints does not work in the beta API. No
+        # validation is performed for the linked_blueprints parameter, and
+        # thus no error is returned when we pass rubbish.
+        self.search("beta", linked_blueprints="Teabags!")