← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/bug-793848 into lp:launchpad/db-devel

 

Robert Collins has proposed merging lp:~lifeless/launchpad/bug-793848 into lp:launchpad/db-devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #793848 in Launchpad itself: "Distribution:+bugtarget-portlet-bugfilters-stats timeouts"
  https://bugs.launchpad.net/launchpad/+bug/793848

For more details, see:
https://code.launchpad.net/~lifeless/launchpad/bug-793848/+merge/65158

This branch gets the table scan component of the bug stats portlet using bugsummary.

It needs the new columns only available in db-devel; along the way I saw dead code that confused me, so I deleted it.

I've added a interface for things that are dimensions in the bugsummary, this lets me avoid code duplication between the tags counting code and countBugs. In fact, in a future branch we probably want to delete the special cased tag counting code.
-- 
https://code.launchpad.net/~lifeless/launchpad/bug-793848/+merge/65158
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/bug-793848 into lp:launchpad/db-devel.
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2011-06-07 23:20:43 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2011-06-20 07:11:33 +0000
@@ -24,6 +24,7 @@
 import cgi
 from cStringIO import StringIO
 from datetime import datetime
+from functools import partial
 from operator import itemgetter
 import urllib
 from urlparse import urljoin
@@ -1190,7 +1191,7 @@
         return url
 
 
-class BugTargetBugListingView:
+class BugTargetBugListingView(LaunchpadView):
     """Helper methods for rendering bug listings."""
 
     @property
@@ -1218,7 +1219,7 @@
         elif IProductSeries(self.context, None):
             milestone_resultset = self.context.product.milestones
         else:
-            raise AssertionError("series_list called with illegal context")
+            raise AssertionError("milestones_list called with illegal context")
         return list(milestone_resultset)
 
     @property
@@ -1230,25 +1231,28 @@
         The count only considers bugs that the user would actually be
         able to see in a listing.
         """
+        # Circular fail.
+        from lp.bugs.model.bugsummary import (
+            BugSummary,
+            CombineBugSummaryContraint,
+            )
         series_buglistings = []
         bug_task_set = getUtility(IBugTaskSet)
         series_list = self.series_list
         if not series_list:
             return series_buglistings
-        open_bugs = bug_task_set.open_bugtask_search
-        open_bugs.setTarget(any(*series_list))
         # This would be better as delegation not a case statement.
         if IDistribution(self.context, None):
-            backlink = BugTask.distroseriesID
+            backlink = BugSummary.distroseries_id
         elif IProduct(self.context, None):
-            backlink = BugTask.productseriesID
+            backlink = BugSummary.productseries_id
         elif IDistroSeries(self.context, None):
-            backlink = BugTask.distroseriesID
+            backlink = BugSummary.distroseries_id
         elif IProductSeries(self.context, None):
-            backlink = BugTask.productseriesID
+            backlink = BugSummary.productseries_id
         else:
             raise AssertionError("illegal context %r" % self.context)
-        counts = bug_task_set.countBugs(open_bugs, (backlink,))
+        counts = bug_task_set.countBugs(self.user, series_list, (backlink,))
         for series in series_list:
             series_bug_count = counts.get((series.id,), 0)
             if series_bug_count > 0:
@@ -1263,14 +1267,23 @@
     @property
     def milestone_buglistings(self):
         """Return a buglisting for each milestone."""
+        # Circular fail.
+        from lp.bugs.model.bugsummary import (
+            BugSummary,
+            CombineBugSummaryContraint,
+            )
         milestone_buglistings = []
         bug_task_set = getUtility(IBugTaskSet)
         milestones = self.milestones_list
         if not milestones:
             return milestone_buglistings
-        open_bugs = bug_task_set.open_bugtask_search
-        open_bugs.setTarget(any(*milestones))
-        counts = bug_task_set.countBugs(open_bugs, (BugTask.milestoneID,))
+        # Note: this isn't totally optimal as a query, but its the simplest to
+        # code; we can iterate if needed to provide one complex context to
+        # countBugs.
+        query_milestones = map(partial(
+            CombineBugSummaryContraint, self.context), milestones)
+        counts = bug_task_set.countBugs(
+            self.user, query_milestones, (BugSummary.milestone_id,))
         for milestone in milestones:
             milestone_bug_count = counts.get((milestone.id,), 0)
             if milestone_bug_count > 0:

=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py	2011-06-06 06:44:23 +0000
+++ lib/lp/bugs/browser/bugtask.py	2011-06-20 07:11:33 +0000
@@ -1820,17 +1820,12 @@
 
     @cachedproperty
     def _bug_stats(self):
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
         bug_task_set = getUtility(IBugTaskSet)
-        upstream_open_bugs = bug_task_set.open_bugtask_search
-        upstream_open_bugs.setTarget(self.context)
-        upstream_open_bugs.resolved_upstream = True
-        fixed_upstream_clause = SQL(
-            bug_task_set.buildUpstreamClause(upstream_open_bugs))
-        open_bugs = bug_task_set.open_bugtask_search
-        open_bugs.setTarget(self.context)
-        groups = (BugTask.status, BugTask.importance,
-            Bug.latest_patch_uploaded != None, fixed_upstream_clause)
-        counts = bug_task_set.countBugs(open_bugs, groups)
+        groups = (BugSummary.status, BugSummary.importance,
+            BugSummary.has_patch, BugSummary.fixed_upstream)
+        counts = bug_task_set.countBugs(self.user, [self.context], groups)
         # Sum the split out aggregates.
         new = 0
         open = 0

=== modified file 'lib/lp/bugs/interfaces/bugsummary.py'
--- lib/lp/bugs/interfaces/bugsummary.py	2011-06-14 15:06:36 +0000
+++ lib/lp/bugs/interfaces/bugsummary.py	2011-06-20 07:11:33 +0000
@@ -4,7 +4,10 @@
 """BugSummary interfaces."""
 
 __metaclass__ = type
-__all__ = ['IBugSummary']
+__all__ = [
+    'IBugSummary',
+    'IBugSummaryDimension',
+    ]
 
 
 from zope.interface import Interface
@@ -70,3 +73,16 @@
 
     has_patch = Bool(readonly=True)
     fixed_upstream = Bool(readonly=True)
+
+
+class IBugSummaryDimension(Interface):
+    """Interface for dimensions used in the BugSummary database class."""
+
+    def _getBugSummaryContextWhereClause():
+        """Return a storm clause to filter bugsummaries on this context.
+
+        This method is intentended for in-appserver use only.
+        
+        :return: Either a storm clause to filter bugsummaries, or False if
+            there cannot be any matching bug summaries.
+        """

=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py	2011-06-07 03:30:25 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py	2011-06-20 07:11:33 +0000
@@ -271,15 +271,6 @@
         hardware_is_linked_to_bug to True.
         """
 
-    def getBugCounts(user, statuses=None):
-        """Return a dict with the number of bugs in each possible status.
-
-            :user: Only bugs the user has permission to view will be
-                   counted.
-            :statuses: Only bugs with these statuses will be counted. If
-                       None, all statuses will be included.
-        """
-
     def getBugTaskWeightFunction():
         """Return a function that is used to weight the bug tasks.
 

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2011-06-14 15:06:36 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2011-06-20 07:11:33 +0000
@@ -1519,12 +1519,17 @@
         :param params: the BugTaskSearchParams to search on.
         """
 
-    def countBugs(params, group_on):
-        """Count bugs that match params, grouping by group_on.
-
-        :param param: A BugTaskSearchParams object.
+    def countBugs(user, contexts, group_on):
+        """Count open bugs that match params, grouping by group_on.
+
+        This serves results from the bugsummary fact table: it is fast but not
+        completely precise. See the bug summary documentation for more detail.
+
+        :param user: The user to query on behalf of.
+        :param contexts: A list of contexts to search. Contexts must support
+            the IBugSummaryDimension interface.
         :param group_on: The column(s) group on - .e.g (
-            Bugtask.distroseriesID, BugTask.milestoneID) will cause
+            BugSummary.distroseries_id, BugSummary.milestone_id) will cause
             grouping by distro series and then milestone.
         :return: A dict {group_instance: count, ...}
         """

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-06-16 13:50:58 +0000
+++ lib/lp/bugs/model/bug.py	2011-06-20 07:11:33 +0000
@@ -271,8 +271,9 @@
     The only change is that this function takes a SQL expression for limiting
     the found tags.
     :param context_condition: A Storm SQL expression, limiting the
-        used tags to a specific context. Only the BugTask table may be
-        used to choose the context.
+        used tags to a specific context. Only the BugSummary table may be
+        used to choose the context. If False then no query will be performed
+        (and {} returned).
     """
     # Circular fail.
     from lp.bugs.model.bugsummary import BugSummary

=== modified file 'lib/lp/bugs/model/bugsummary.py'
--- lib/lp/bugs/model/bugsummary.py	2011-06-14 15:06:36 +0000
+++ lib/lp/bugs/model/bugsummary.py	2011-06-20 07:11:33 +0000
@@ -4,9 +4,13 @@
 """BugSummary Storm database classes."""
 
 __metaclass__ = type
-__all__ = ['BugSummary']
+__all__ = [
+    'BugSummary'
+    'CombineBugSummaryContraint',
+    ]
 
 from storm.locals import (
+    And,
     Bool,
     Int,
     Reference,
@@ -14,6 +18,7 @@
     Unicode,
     )
 from zope.interface import implements
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.database.enumcol import EnumCol
 from lp.bugs.interfaces.bugsummary import IBugSummary
@@ -68,3 +73,21 @@
 
     has_patch = Bool()
     fixed_upstream = Bool()
+
+
+class CombineBugSummaryContraint(object):
+    """A class to combine two separate bug summary constraints.
+
+    This is useful for querying on multiple related dimensions (e.g. milestone
+    + sourcepackage) - and essential when a dimension is not unique to a
+    context.
+    """
+
+    def __init__(self, *dimensions):
+        self.dimensions = map(
+            lambda x:removeSecurityProxy(x._getBugSummaryContextWhereClause()),
+            dimensions)
+
+    def _getBugSummaryContextWhereClause(self):
+        """See `IBugSummaryDimension`."""
+        return And(*self.dimensions)

=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py	2011-03-29 15:59:22 +0000
+++ lib/lp/bugs/model/bugtarget.py	2011-06-20 07:11:33 +0000
@@ -119,15 +119,15 @@
 
     def _customizeSearchParams(self, search_params):
         """Customize `search_params` for a specific target."""
-        raise NotImplementedError
-
-    def _getBugTaskContextWhereClause(self):
-        """Return an SQL snippet to filter bugtasks on this context."""
-        raise NotImplementedError
-
-    def _getBugTaskContextClause(self):
-        """Return a SQL clause for selecting this target's bugtasks."""
-        raise NotImplementedError(self._getBugTaskContextClause)
+        raise NotImplementedError(self._customizeSearchParams)
+
+    def _getBugSummaryContextWhereClause(self):
+        """Return a storm clause to filter bugsummaries on this context.
+        
+        :return: Either a storm clause to filter bugsummaries, or False if
+            there cannot be any matching bug summaries.
+        """
+        raise NotImplementedError(self._getBugSummaryContextWhereClause)
 
     @property
     def closed_bugtasks(self):
@@ -208,32 +208,6 @@
         """See `IHasBugs`."""
         return not self.all_bugtasks.is_empty()
 
-    def getBugCounts(self, user, statuses=None):
-        """See `IHasBugs`."""
-        if statuses is None:
-            statuses = BugTaskStatus.items
-        statuses = list(statuses)
-
-        count_column = """
-            COUNT (CASE WHEN BugTask.status = %s
-                        THEN BugTask.id ELSE NULL END)"""
-        select_columns = [count_column % sqlvalues(status)
-                          for status in statuses]
-        conditions = [
-            '(%s)' % self._getBugTaskContextClause(),
-            'BugTask.bug = Bug.id',
-            'Bug.duplicateof is NULL']
-        privacy_filter = get_bug_privacy_filter(user)
-        if privacy_filter:
-            conditions.append(privacy_filter)
-
-        cur = cursor()
-        cur.execute(
-            "SELECT %s FROM BugTask, Bug WHERE %s" % (
-                ', '.join(select_columns), ' AND '.join(conditions)))
-        counts = cur.fetchone()
-        return dict(zip(statuses, counts))
-
     def getBugTaskWeightFunction(self):
         """Default weight function is the simple one."""
         return simple_weight_calculator
@@ -249,6 +223,13 @@
     # IDistribution, IDistroSeries, IProjectGroup.
     enable_bugfiling_duplicate_search = True
 
+    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
+        """See IBugTarget."""
+        from lp.bugs.model.bug import get_bug_tags_open_count
+        return get_bug_tags_open_count(
+            self._getBugSummaryContextWhereClause(),
+            user, tag_limit=tag_limit, include_tags=include_tags)
+
 
 class HasBugHeatMixin:
     """Standard functionality for objects implementing IHasBugHeat."""

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2011-05-28 04:09:11 +0000
+++ lib/lp/bugs/model/bugtask.py	2011-06-20 07:11:33 +0000
@@ -45,6 +45,7 @@
     Or,
     Select,
     SQL,
+    Sum,
     )
 from storm.info import ClassAlias
 from storm.store import (
@@ -2525,13 +2526,56 @@
         """See `IBugTaskSet`."""
         return self._search(BugTask.bugID, [], None, params).result_set
 
-    def countBugs(self, params, group_on):
+    def countBugs(self, user, contexts, group_on):
         """See `IBugTaskSet`."""
-        resultset = self._search(
-            group_on + (SQL("COUNT(Distinct BugTask.bug)"),),
-            [], None, params).result_set
-        # We group on the related field:
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        assert user is not None
+        conditions = []
+        # Open bug statuses
+        conditions.append(BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES))
+        # BugSummary does not include duplicates so no need to exclude.
+        context_conditions = []
+        for context in contexts:
+            condition = removeSecurityProxy(
+                context._getBugSummaryContextWhereClause())
+            if condition is not False:
+                context_conditions.append(condition)
+        if not context_conditions:
+            return {}
+        conditions.append(Or(*context_conditions))
+        # bugsummary by design requires either grouping by tag or excluding
+        # non-null tags.
+        # This is an awkward way of saying
+        # if BugSummary.tag not in group_on:
+        # - see bug 799602
+        group_on_tag = False
+        for column in group_on:
+            if column is BugSummary.tag:
+                group_on_tag = True
+        if not group_on_tag:
+            conditions.append(BugSummary.tag == None)
+        else:
+            conditions.append(BugSummary.tag != None)
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        admin_team = getUtility(ILaunchpadCelebrities).admin
+        if user is not None and not user.inTeam(admin_team):
+            store = store.with_(SQL(
+                "teams AS ("
+                "SELECT team from TeamParticipation WHERE person=?)",
+                (user.id,)))
+        # Note that because admins can see every bug regardless of subscription
+        # they will see rather inflated counts. Admins get to deal.
+        if not user.inTeam(admin_team):
+            conditions.append(
+                Or(
+                    BugSummary.viewed_by_id == None,
+                    BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM teams"))
+                    ))
+        sum_count = Sum(BugSummary.count)
+        resultset = store.find(group_on + (sum_count,), *conditions)
         resultset.group_by(*group_on)
+        resultset.having(sum_count != 0)
         resultset.order_by()
         result = {}
         for row in resultset:

=== removed file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
--- lib/lp/bugs/tests/bugtarget-bugcount.txt	2010-10-09 16:36:22 +0000
+++ lib/lp/bugs/tests/bugtarget-bugcount.txt	1970-01-01 00:00:00 +0000
@@ -1,137 +0,0 @@
-= Bug Counts for IBugTarget =
-
-A IBugTarget has a method for getting the number of bugs in different
-statuses, getBugCounts. The method has one required parameter; the user
-used when searching for bugs (to ensure that private bugs the user
-doesn't have permission to view are excluded). None acts as the
-anonymous user, i.e. no private bugs will be included in the counts.
-
-    >>> bug_counts = bugtarget.getBugCounts(None)
-    >>> for status in sorted(bug_counts.keys()):
-    ...     print status.name
-    NEW
-    INCOMPLETE
-    OPINION
-    INVALID
-    WONTFIX
-    EXPIRED
-    CONFIRMED
-    TRIAGED
-    INPROGRESS
-    FIXCOMMITTED
-    FIXRELEASED
-    UNKNOWN
-
-It's possible to limit which statuses to return.
-
-    >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
-    >>> bug_counts = bugtarget.getBugCounts(
-    ...     None, [BugTaskStatus.NEW, BugTaskStatus.CONFIRMED])
-    >>> for status in sorted(bug_counts.keys()):
-    ...     print status.name
-    NEW
-    CONFIRMED
-
-If we file a few bugs in each status, the counts will increase.
-
-    >>> def print_count_difference(new_counts, old_counts, status):
-    ...     difference = new_counts[status] - old_counts[status]
-    ...     if difference > 0:
-    ...         diff_message = "%s bug(s) more" % difference
-    ...     elif difference < 0:
-    ...         diff_message = "%s bug(s) less" % -difference
-    ...     else:
-    ...         diff_message = "no difference"
-    ...     print "%s: %s" % (status.name, diff_message)
-
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> old_counts = bugtarget.getBugCounts(None)
-    >>> from storm.store import Store
-    >>> for status in sorted(BugTaskStatus.items):
-    ...     for index in range(5):
-    ...         bug = filebug(bugtarget, "Test Bug", status=status)
-    ...         Store.of(bug).flush()
-    ...     new_bug_counts = bugtarget.getBugCounts(None)
-    ...     print_count_difference(new_bug_counts, old_counts, status)
-    NEW: 5 bug(s) more
-    INCOMPLETE: 5 bug(s) more
-    OPINION: 5 bug(s) more
-    INVALID: 5 bug(s) more
-    WONTFIX: 5 bug(s) more
-    EXPIRED: 5 bug(s) more
-    CONFIRMED: 5 bug(s) more
-    TRIAGED: 5 bug(s) more
-    INPROGRESS: 5 bug(s) more
-    FIXCOMMITTED: 5 bug(s) more
-    FIXRELEASED: 5 bug(s) more
-    UNKNOWN: 5 bug(s) more
-
-If we take a bug that is in one status and changes it to another, the
-former status' count will of course decrease, and the latter's increase.
-
-    >>> from canonical.launchpad.ftests import syncUpdate
-    >>> bug = filebug(bugtarget, "Test Bug", status=BugTaskStatus.CONFIRMED)
-    >>> Store.of(bug).flush()
-    >>> old_counts = bugtarget.getBugCounts(None)
-    >>> for bugtask in bug.bugtasks:
-    ...     if bugtask.target == bugtarget:
-    ...         break
-    >>> bugtask.transitionToStatus(
-    ...     BugTaskStatus.INCOMPLETE, getUtility(ILaunchBag).user)
-    >>> syncUpdate(bugtask)
-    >>> new_bug_counts = bugtarget.getBugCounts(None)
-    >>> print_count_difference(
-    ...     new_bug_counts, old_counts, BugTaskStatus.CONFIRMED)
-    CONFIRMED: 1 bug(s) less
-    >>> print_count_difference(
-    ...     new_bug_counts, old_counts, BugTaskStatus.INCOMPLETE)
-    INCOMPLETE: 1 bug(s) more
-
-== Private Bugs ==
-
-Private bugs are included in the counts, only if the user has permission
-to view the bug. So if we set the INCOMPLETE bug above to be private,
-Sample Person (the bug reporter) will still get the bug included in the
-count, but not No Privileges Person or the anonymous user.
-
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> old_counts = bugtarget.getBugCounts(None)
-    >>> bug.setPrivate(True, getUtility(ILaunchBag).user)
-    True
-    >>> syncUpdate(bug)
-    >>> sample_person_counts = bugtarget.getBugCounts(
-    ...     getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx'))
-    >>> nopriv_counts = bugtarget.getBugCounts(
-    ...     getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx'))
-    >>> anon_counts = bugtarget.getBugCounts(None)
-
-    >>> print_count_difference(
-    ...     sample_person_counts, old_counts, BugTaskStatus.INCOMPLETE)
-    INCOMPLETE: no difference
-
-    >>> print_count_difference(
-    ...     nopriv_counts, old_counts, BugTaskStatus.INCOMPLETE)
-    INCOMPLETE: 1 bug(s) less
-
-    >>> print_count_difference(anon_counts, old_counts, BugTaskStatus.INCOMPLETE)
-    INCOMPLETE: 1 bug(s) less
-
-    >>> bug.setPrivate(False, getUtility(ILaunchBag).user)
-    True
-    >>> syncUpdate(bug)
-
-== Duplicate Bugs ==
-
-Duplicate bugs are not included in the counts, so if some bug is marked
-as a duplicate of another bug, the count will decrease.
-
-    >>> another_bug =  filebug(
-    ...     bugtarget, "Another Test Bug", status=BugTaskStatus.NEW)
-    >>> Store.of(another_bug).flush()
-    >>> old_counts = bugtarget.getBugCounts(None)
-    >>> another_bug.markAsDuplicate(bug)
-    >>> syncUpdate(another_bug)
-    >>> new_counts = bugtarget.getBugCounts(None)
-    >>> print_count_difference(
-    ...     new_counts, old_counts, BugTaskStatus.NEW)
-    NEW: 1 bug(s) less

=== modified file 'lib/lp/bugs/tests/test_bugtarget.py'
--- lib/lp/bugs/tests/test_bugtarget.py	2011-05-12 14:55:54 +0000
+++ lib/lp/bugs/tests/test_bugtarget.py	2011-06-20 07:11:33 +0000
@@ -5,8 +5,7 @@
 
 This module runs the interface test against the Product, ProductSeries
 ProjectGroup, DistributionSourcePackage, and DistroSeries implementations
-IBugTarget. It runs the bugtarget-bugcount.txt, and
-bugtarget-questiontarget.txt tests.
+IBugTarget. It runs the bugtarget-questiontarget.txt test.
 """
 # pylint: disable-msg=C0103
 
@@ -291,14 +290,5 @@
             layer=DatabaseFunctionalLayer)
         suite.addTest(test)
 
-    setUpMethods.append(sourcePackageSetUp)
-    setUpMethods.append(projectSetUp)
-
-    for setUpMethod in setUpMethods:
-        test = LayeredDocFileSuite('bugtarget-bugcount.txt',
-            setUp=setUpMethod, tearDown=tearDown,
-            layer=DatabaseFunctionalLayer)
-        suite.addTest(test)
-
     suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
     return suite

=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py	2011-03-23 16:28:51 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py	2011-06-20 07:11:33 +0000
@@ -35,6 +35,7 @@
     BugTaskStatus,
     IBugTaskSet,
     )
+from lp.bugs.model.bugsummary import BugSummary
 from lp.bugs.model.bugtask import BugTask
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distributionsourcepackage import (
@@ -76,11 +77,13 @@
         if self.group_on is None:
             # Not a useful/valid permutation.
             return
-        params = self.getBugTaskSearchParams(user=None, multitarget=True)
+        self.getBugTaskSearchParams(user=None, multitarget=True)
         # The test data has 3 bugs for searchtarget and 6 for searchtarget2.
         expected = {(self.targetToGroup(self.searchtarget),): 3,
             (self.targetToGroup(self.searchtarget2),): 6}
-        self.assertEqual(expected, self.bugtask_set.countBugs(params,
+        user = self.factory.makePerson()
+        self.assertEqual(expected, self.bugtask_set.countBugs(user,
+            (self.searchtarget, self.searchtarget2),
             group_on=self.group_on))
 
     def test_search_all_bugtasks_for_target(self):
@@ -696,7 +699,7 @@
 
     def setUp(self):
         super(ProductTarget, self).setUp()
-        self.group_on = (BugTask.productID,)
+        self.group_on = (BugSummary.product_id,)
         self.searchtarget = self.factory.makeProduct()
         self.owner = self.searchtarget.owner
         self.makeBugTasks()
@@ -727,7 +730,7 @@
 
     def setUp(self):
         super(ProductSeriesTarget, self).setUp()
-        self.group_on = (BugTask.productseriesID,)
+        self.group_on = (BugSummary.productseries_id,)
         self.searchtarget = self.factory.makeProductSeries()
         self.owner = self.searchtarget.owner
         self.makeBugTasks()
@@ -884,7 +887,7 @@
     def setUp(self):
         super(MilestoneTarget, self).setUp()
         self.product = self.factory.makeProduct()
-        self.group_on = (BugTask.milestoneID,)
+        self.group_on = (BugSummary.milestone_id,)
         self.searchtarget = self.factory.makeMilestone(product=self.product)
         self.owner = self.product.owner
         self.makeBugTasks(bugtarget=self.product)
@@ -930,7 +933,7 @@
 
     def setUp(self):
         super(DistributionTarget, self).setUp()
-        self.group_on = (BugTask.distributionID,)
+        self.group_on = (BugSummary.distribution_id,)
         self.searchtarget = self.factory.makeDistribution()
         self.owner = self.searchtarget.owner
         self.makeBugTasks()
@@ -968,7 +971,7 @@
 
     def setUp(self):
         super(DistroseriesTarget, self).setUp()
-        self.group_on = (BugTask.distroseriesID,)
+        self.group_on = (BugSummary.distroseries_id,)
         self.searchtarget = self.factory.makeDistroSeries()
         self.owner = self.searchtarget.owner
         self.makeBugTasks()
@@ -991,7 +994,7 @@
 
     def setUp(self):
         super(SourcePackageTarget, self).setUp()
-        self.group_on = (BugTask.sourcepackagenameID,)
+        self.group_on = (BugSummary.sourcepackagename_id,)
         self.searchtarget = self.factory.makeSourcePackage()
         self.owner = self.searchtarget.distroseries.owner
         self.makeBugTasks()
@@ -1028,7 +1031,7 @@
 
     def setUp(self):
         super(DistributionSourcePackageTarget, self).setUp()
-        self.group_on = (BugTask.sourcepackagenameID,)
+        self.group_on = (BugSummary.sourcepackagename_id,)
         self.searchtarget = self.factory.makeDistributionSourcePackage()
         self.owner = self.searchtarget.distribution.owner
         self.makeBugTasks()

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2011-06-06 20:56:43 +0000
+++ lib/lp/registry/configure.zcml	2011-06-20 07:11:33 +0000
@@ -201,6 +201,7 @@
         class="lp.registry.model.distroseries.DistroSeries">
         <allow
             interface="lp.registry.interfaces.distroseries.IDistroSeriesPublic"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <allow
@@ -360,6 +361,7 @@
         class="lp.registry.model.projectgroup.ProjectGroup">
         <allow
             interface="lp.registry.interfaces.projectgroup.IProjectGroupPublic"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <allow
@@ -448,6 +450,7 @@
 
     <class
         class="lp.registry.model.distributionsourcepackage.DistributionSourcePackage">
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <allow
@@ -477,7 +480,6 @@
                 enable_bugfiling_duplicate_search
                 findRelatedArchivePublications
                 findRelatedArchives
-                getBugCounts
                 getPersonsByEmail
                 getReleasesAndPublishingHistory
                 getUsedBugTags
@@ -986,6 +988,7 @@
                 createProductRelease
                 closeBugsAndBlueprints
                 destroySelf"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugs"/>
         <allow
@@ -1143,6 +1146,7 @@
         class="lp.registry.model.product.Product">
         <allow
             interface="lp.registry.interfaces.product.IProductPublic"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <allow
@@ -1392,6 +1396,7 @@
         <require
             permission="launchpad.Edit"
             interface="lp.registry.interfaces.productseries.IProductSeriesEditRestricted"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <require
@@ -1460,6 +1465,7 @@
         class="lp.registry.model.distribution.Distribution">
         <allow
             interface="lp.registry.interfaces.distribution.IDistributionPublic"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <require
@@ -1592,6 +1598,7 @@
         class="lp.registry.model.sourcepackage.SourcePackage">
         <allow
             interface="lp.registry.interfaces.sourcepackage.ISourcePackagePublic"/>
+        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <allow
             interface="lp.bugs.interfaces.bugtarget.IHasBugHeat"/>
         <allow

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2011-06-08 23:31:41 +0000
+++ lib/lp/registry/model/distribution.py	2011-06-20 07:11:33 +0000
@@ -112,7 +112,6 @@
 from lp.bugs.model.bug import (
     BugSet,
     get_bug_tags,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -637,9 +636,13 @@
         """See `IBugTarget`."""
         return self.name
 
-    def _getBugTaskContextWhereClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
-        return "BugTask.distribution = %d" % self.id
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return And(
+                BugSummary.distribution_id == self.id,
+                BugSummary.sourcepackagename_id == None)
 
     def _customizeSearchParams(self, search_params):
         """Customize `search_params` for this distribution."""
@@ -649,15 +652,6 @@
         """See `IBugTarget`."""
         return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            And(BugSummary.distribution_id == self.id,
-                BugSummary.sourcepackagename_id == None),
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
     def getMirrorByName(self, name):
         """See `IDistribution`."""
         return Store.of(self).find(
@@ -720,10 +714,6 @@
         bug_params.setBugTarget(distribution=self)
         return BugSet().createBug(bug_params)
 
-    def _getBugTaskContextClause(self):
-        """See BugTargetBase."""
-        return 'BugTask.distribution = %s' % sqlvalues(self)
-
     @property
     def currentseries(self):
         """See `IDistribution`."""

=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py	2011-06-07 03:30:25 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py	2011-06-20 07:11:33 +0000
@@ -43,7 +43,6 @@
 from lp.bugs.model.bug import (
     Bug,
     BugSet,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -472,11 +471,13 @@
         """See `IDistributionSourcePackage`."""
         return not self.__eq__(other)
 
-    def _getBugTaskContextWhereClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See `BugTargetBase`."""
-        return (
-            "BugTask.distribution = %d AND BugTask.sourcepackagename = %d" % (
-            self.distribution.id, self.sourcepackagename.id))
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return And(
+            BugSummary.distribution == self.distribution,
+            BugSummary.sourcepackagename == self.sourcepackagename),
 
     def _customizeSearchParams(self, search_params):
         """Customize `search_params` for this distribution source package."""
@@ -486,15 +487,6 @@
         """See `IBugTarget`."""
         return self.distribution.getUsedBugTags()
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            And(BugSummary.distribution == self.distribution,
-                BugSummary.sourcepackagename == self.sourcepackagename),
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
     def _getOfficialTagClause(self):
         return self.distribution._getOfficialTagClause()
 
@@ -510,12 +502,6 @@
             sourcepackagename=self.sourcepackagename)
         return BugSet().createBug(bug_params)
 
-    def _getBugTaskContextClause(self):
-        """See `BugTargetBase`."""
-        return (
-            'BugTask.distribution = %s AND BugTask.sourcepackagename = %s' %
-                sqlvalues(self.distribution, self.sourcepackagename))
-
     def composeCustomLanguageCodeMatch(self):
         """See `HasCustomLanguageCodesMixin`."""
         return And(

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2011-06-16 10:38:21 +0000
+++ lib/lp/registry/model/distroseries.py	2011-06-20 07:11:33 +0000
@@ -84,7 +84,6 @@
 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
 from lp.bugs.model.bug import (
     get_bug_tags,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -841,18 +840,6 @@
         """See `IHasBugs`."""
         return get_bug_tags("BugTask.distroseries = %s" % sqlvalues(self))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0,
-                                     include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            And(
-                BugSummary.distroseries_id == self.id,
-                BugSummary.sourcepackagename_id == None
-                ),
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
     @property
     def has_any_specifications(self):
         """See IHasSpecifications."""
@@ -1664,9 +1651,14 @@
             "not-too-distant future. For now, you probably meant to file "
             "the bug on the distribution instead.")
 
-    def _getBugTaskContextClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
-        return 'BugTask.distroseries = %s' % sqlvalues(self)
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return And(
+                BugSummary.distroseries_id == self.id,
+                BugSummary.sourcepackagename_id == None
+                )
 
     def copyTranslationsFromParent(self, transaction, logger=None):
         """See `IDistroSeries`."""

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2011-03-23 16:28:51 +0000
+++ lib/lp/registry/model/milestone.py	2011-06-20 07:11:33 +0000
@@ -40,6 +40,7 @@
 from canonical.launchpad.webapp.sorting import expand_numbers
 from lp.app.errors import NotFoundError
 from lp.blueprints.model.specification import Specification
+from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
 from lp.bugs.interfaces.bugtarget import IHasBugs
 from lp.bugs.interfaces.bugtask import (
     BugTaskSearchParams,
@@ -128,7 +129,7 @@
 
 
 class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
-    implements(IHasBugs, IMilestone)
+    implements(IHasBugs, IMilestone, IBugSummaryDimension)
 
     # XXX: Guilherme Salgado 2007-03-27 bug=40978:
     # Milestones should be associated with productseries/distroseriess
@@ -247,6 +248,12 @@
             "associated with it.")
         SQLBase.destroySelf(self)
 
+    def _getBugSummaryContextWhereClause(self):
+        """See BugTargetBase."""
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return BugSummary.milestone_id == self.id
+
 
 class MilestoneSet:
     implements(IMilestoneSet)

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2011-06-07 03:30:25 +0000
+++ lib/lp/registry/model/product.py	2011-06-20 07:11:33 +0000
@@ -114,7 +114,6 @@
 from lp.bugs.model.bug import (
     BugSet,
     get_bug_tags,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -799,14 +798,6 @@
         """See `IBugTarget`."""
         return get_bug_tags("BugTask.product = %s" % sqlvalues(self))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            BugSummary.product_id == self.id,
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
     series = SQLMultipleJoin('ProductSeries', joinColumn='product',
         orderBy='name')
 
@@ -957,9 +948,11 @@
         bug_params.setBugTarget(product=self)
         return BugSet().createBug(bug_params)
 
-    def _getBugTaskContextClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
-        return 'BugTask.product = %s' % sqlvalues(self)
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return BugSummary.product_id == self.id
 
     def searchQuestions(self, search_text=None,
                         status=QUESTION_STATUS_DEFAULT_SEARCH,

=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py	2011-06-07 03:30:25 +0000
+++ lib/lp/registry/model/productseries.py	2011-06-20 07:11:33 +0000
@@ -66,7 +66,6 @@
 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
 from lp.bugs.model.bug import (
     get_bug_tags,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -446,21 +445,15 @@
         """See IBugTarget."""
         return get_bug_tags("BugTask.productseries = %s" % sqlvalues(self))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            BugSummary.productseries_id == self.id, user, tag_limit=tag_limit,
-            include_tags=include_tags)
-
     def createBug(self, bug_params):
         """See IBugTarget."""
         raise NotImplementedError('Cannot file a bug against a productseries')
 
-    def _getBugTaskContextClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
-        return 'BugTask.productseries = %s' % sqlvalues(self)
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return BugSummary.productseries_id == self.id
 
     def getSpecification(self, name):
         """See ISpecificationTarget."""

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2011-06-07 03:30:25 +0000
+++ lib/lp/registry/model/projectgroup.py	2011-06-20 07:11:33 +0000
@@ -69,7 +69,6 @@
 from lp.bugs.interfaces.bugtarget import IHasBugHeat
 from lp.bugs.model.bug import (
     get_bug_tags,
-    get_bug_tags_open_count,
     )
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -343,20 +342,14 @@
         return get_bug_tags(
             "BugTask.product IN (%s)" % ",".join(product_ids))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
+    def _getBugSummaryContextWhereClause(self):
+        """See BugTargetBase."""
         # Circular fail.
         from lp.bugs.model.bugsummary import BugSummary
         product_ids = [product.id for product in self.products]
         if not product_ids:
-            return {}
-        return get_bug_tags_open_count(
-            BugSummary.product_id.is_in(product_ids),
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
-    def _getBugTaskContextClause(self):
-        """See `HasBugsBase`."""
-        return 'BugTask.product IN (%s)' % ','.join(sqlvalues(*self.products))
+            return False
+        return BugSummary.product_id.is_in(product_ids)
 
     # IQuestionCollection
     def searchQuestions(self, search_text=None,

=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py	2011-06-08 15:00:44 +0000
+++ lib/lp/registry/model/sourcepackage.py	2011-06-20 07:11:33 +0000
@@ -39,7 +39,6 @@
     )
 from lp.bugs.interfaces.bugtarget import IHasBugHeat
 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
-from lp.bugs.model.bug import get_bug_tags_open_count
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
     HasBugHeatMixin,
@@ -500,15 +499,6 @@
         """See `IBugTarget`."""
         return self.distroseries.getUsedBugTags()
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
-        """See IBugTarget."""
-        # Circular fail.
-        from lp.bugs.model.bugsummary import BugSummary
-        return get_bug_tags_open_count(
-            And(BugSummary.distroseries == self.distroseries,
-                BugSummary.sourcepackagename == self.sourcepackagename),
-            user, tag_limit=tag_limit, include_tags=include_tags)
-
     @property
     def max_bug_heat(self):
         """See `IHasBugs`."""
@@ -528,11 +518,13 @@
             "future. For now, you probably meant to file the bug on the "
             "distro-wide (i.e. not series-specific) source package.")
 
-    def _getBugTaskContextClause(self):
+    def _getBugSummaryContextWhereClause(self):
         """See BugTargetBase."""
-        return (
-            'BugTask.distroseries = %s AND BugTask.sourcepackagename = %s' %
-                sqlvalues(self.distroseries, self.sourcepackagename))
+        # Circular fail.
+        from lp.bugs.model.bugsummary import BugSummary
+        return And(
+                BugSummary.distroseries == self.distroseries,
+                BugSummary.sourcepackagename == self.sourcepackagename)
 
     def setPackaging(self, productseries, owner):
         """See `ISourcePackage`."""


Follow ups