← Back to team overview

launchpad-reviewers team mailing list archive

lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085 into lp:launchpad with lp:~allenap/launchpad/wire-up-filter-subs-bug-655567 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
  #659085 getBugNotificationsRecipients() has been superceded
  https://bugs.launchpad.net/bugs/659085


This is a big branch, but fear not, most of it is moving code and
deletions. I shall guide you though the dark forest...

- tests/test_bugtask_0.py, tests/test_bugtask_1.py and
  tests/test_bugtask_status.py have been folded into
  tests/test_bugtask.py. The first two were left over from the
  automated tree migration.

- test_bugtask_0.py contained one doctest-in-a-doctring which was
  rewritten as a unittest (it's *very* short). It's still called
  test_open_and_resolved_statuses.

- tests/test_bugtask.py and tests/test_bugtask_status.py have moved
  into model/tests.

- Bug.getStructuralSubscribers() has moved to BugTaskSet. It now
  accepts a list of bugtasks instead of getting them from
  self.bugtasks, and gets the store to use with IStore(Person) rather
  than Store.of(self).

- TestBugStructuralSubscribers has moved from model/tests/test_bug.py
  to model/tests/test_bugtask.py, and is now called.
  TestGetStructuralSubscribers.

- A couple of functions have been changed to use
  BugTaskSet.getStructuralSubscribers() via the utility.

- getBugNotificationRecipients() has been removed, along with its
  associated tests.

I'm sorry it got a bit long. The dependencies of each change cascaded
somewhat.

-- 
https://code.launchpad.net/~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085/+merge/38325
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085 into lp:launchpad.
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2010-10-13 13:11:13 +0000
+++ lib/lp/bugs/configure.zcml	2010-10-06 18:53:53 +0000
@@ -706,7 +706,6 @@
                     getSubscribersForPerson
                     indexed_messages
                     getAlsoNotifiedSubscribers
-                    getStructuralSubscribers
                     getBugWatch
                     canBeNominatedFor
                     getNominationFor

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2010-10-13 13:11:13 +0000
+++ lib/lp/bugs/interfaces/bug.py	2010-10-13 13:11:15 +0000
@@ -491,12 +491,6 @@
         from duplicates.
         """
 
-    def getStructuralSubscribers(recipients=None, level=None):
-        """Return `IPerson`s subscribed to this bug's targets.
-
-        This takes into account bug subscription filters.
-        """
-
     def getSubscriptionsFromDuplicates():
         """Return IBugSubscriptions subscribed from dupes of this bug."""
 

=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py	2010-09-21 09:37:06 +0000
+++ lib/lp/bugs/interfaces/bugtask.py	2010-10-13 13:11:15 +0000
@@ -1523,6 +1523,12 @@
     def getOpenBugTasksPerProduct(user, products):
         """Return open bugtask count for multiple products."""
 
+    def getStructuralSubscribers(bugtasks, recipients=None, level=None):
+        """Return `IPerson`s subscribed to the given bug tasks.
+
+        This takes into account bug subscription filters.
+        """
+
 
 def valid_remote_bug_url(value):
     """Verify that the URL is to a bug to a known bug tracker."""

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2010-10-13 13:11:13 +0000
+++ lib/lp/bugs/model/bug.py	2010-10-13 13:11:15 +0000
@@ -175,9 +175,6 @@
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackage import ISourcePackage
-from lp.registry.interfaces.structuralsubscription import (
-    IStructuralSubscriptionTarget,
-    )
 from lp.registry.model.mentoringoffer import MentoringOffer
 from lp.registry.model.person import (
     Person,
@@ -917,8 +914,9 @@
             # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a
             # hack; it does not seem to be possible to express DISTINCT ON
             # with Storm.
-            (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"),
-             # return people and subscribptions
+            (SQL("DISTINCT ON (Person.name, BugSubscription.person) "
+                 "0 AS ignore"),
+             # Return people and subscriptions
              Person, BugSubscription),
             # For this bug or its duplicates
             Or(
@@ -969,8 +967,8 @@
 
         # Structural subscribers.
         also_notified_subscribers.update(
-            self.getStructuralSubscribers(
-                recipients=recipients, level=level))
+            getUtility(IBugTaskSet).getStructuralSubscribers(
+                self.bugtasks, recipients=recipients, level=level))
 
         # Direct subscriptions always take precedence over indirect
         # subscriptions.
@@ -982,58 +980,6 @@
             (also_notified_subscribers - direct_subscribers),
             key=lambda x: removeSecurityProxy(x).displayname)
 
-    def getStructuralSubscribers(self, recipients=None, level=None):
-        """See `IBug`. """
-        query_arguments = []
-        for bugtask in self.bugtasks:
-            if IStructuralSubscriptionTarget.providedBy(bugtask.target):
-                query_arguments.append((bugtask.target, bugtask))
-                if bugtask.target.parent_subscription_target is not None:
-                    query_arguments.append(
-                        (bugtask.target.parent_subscription_target, bugtask))
-            if ISourcePackage.providedBy(bugtask.target):
-                # Distribution series bug tasks with a package have the source
-                # package set as their target, so we add the distroseries
-                # explicitly to the set of subscription targets.
-                query_arguments.append((bugtask.distroseries, bugtask))
-            if bugtask.milestone is not None:
-                query_arguments.append((bugtask.milestone, bugtask))
-
-        if len(query_arguments) == 0:
-            return EmptyResultSet()
-
-        if level is None:
-            # If level is not specified, default to NOTHING so that all
-            # subscriptions are found. XXX: Perhaps this should go in
-            # getSubscriptionsForBugTask()?
-            level = BugNotificationLevel.NOTHING
-
-        # Build the query.
-        union = lambda left, right: left.union(right)
-        queries = (
-            target.getSubscriptionsForBugTask(bugtask, level)
-            for target, bugtask in query_arguments)
-        subscriptions = reduce(union, queries)
-
-        # Pull all the subscriptions in.
-        subscriptions = list(subscriptions)
-
-        # Prepare a query for the subscribers.
-        subscribers = Store.of(self).find(
-            Person, Person.id.is_in(
-                subscription.subscriberID
-                for subscription in subscriptions))
-
-        if recipients is not None:
-            # We need to process subscriptions, so pull all the subscribes
-            # into the cache, then update recipients with the subscriptions.
-            subscribers = list(subscribers)
-            for subscription in subscriptions:
-                recipients.addStructuralSubscriber(
-                    subscription.subscriber, subscription.target)
-
-        return subscribers
-
     def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
                                      level=None,
                                      include_master_dupe_subscribers=False):

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2010-09-23 10:27:11 +0000
+++ lib/lp/bugs/model/bugtask.py	2010-10-13 13:11:15 +0000
@@ -127,6 +127,7 @@
     )
 from lp.bugs.model.bugnomination import BugNomination
 from lp.bugs.model.bugsubscription import BugSubscription
+from lp.registry.enum import BugNotificationLevel
 from lp.registry.interfaces.distribution import (
     IDistribution,
     IDistributionSet,
@@ -155,6 +156,9 @@
 from lp.registry.interfaces.projectgroup import IProjectGroup
 from lp.registry.interfaces.sourcepackage import ISourcePackage
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.registry.interfaces.structuralsubscription import (
+    IStructuralSubscriptionTarget,
+    )
 from lp.registry.model.pillar import pillar_sort_key
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.propertycache import IPropertyCache
@@ -1947,7 +1951,7 @@
         query = " AND ".join(extra_clauses)
 
         if not decorators:
-            decorator = lambda x:x
+            decorator = lambda x: x
         else:
             def decorator(obj):
                 for decor in decorators:
@@ -2829,3 +2833,55 @@
             counts.append(package_counts)
 
         return counts
+
+    def getStructuralSubscribers(self, bugtasks, recipients=None, level=None):
+        """See `IBugTaskSet`."""
+        query_arguments = []
+        for bugtask in bugtasks:
+            if IStructuralSubscriptionTarget.providedBy(bugtask.target):
+                query_arguments.append((bugtask.target, bugtask))
+                if bugtask.target.parent_subscription_target is not None:
+                    query_arguments.append(
+                        (bugtask.target.parent_subscription_target, bugtask))
+            if ISourcePackage.providedBy(bugtask.target):
+                # Distribution series bug tasks with a package have the source
+                # package set as their target, so we add the distroseries
+                # explicitly to the set of subscription targets.
+                query_arguments.append((bugtask.distroseries, bugtask))
+            if bugtask.milestone is not None:
+                query_arguments.append((bugtask.milestone, bugtask))
+
+        if len(query_arguments) == 0:
+            return EmptyResultSet()
+
+        if level is None:
+            # If level is not specified, default to NOTHING so that all
+            # subscriptions are found.
+            level = BugNotificationLevel.NOTHING
+
+        # Build the query.
+        union = lambda left, right: left.union(right)
+        queries = (
+            target.getSubscriptionsForBugTask(bugtask, level)
+            for target, bugtask in query_arguments)
+        subscriptions = reduce(union, queries)
+
+        # Pull all the subscriptions in.
+        subscriptions = list(subscriptions)
+
+        # Prepare a query for the subscribers.
+        from lp.registry.model.person import Person
+        subscribers = IStore(Person).find(
+            Person, Person.id.is_in(
+                subscription.subscriberID
+                for subscription in subscriptions))
+
+        if recipients is not None:
+            # We need to process subscriptions, so pull all the subscribes into
+            # the cache, then update recipients with the subscriptions.
+            subscribers = list(subscribers)
+            for subscription in subscriptions:
+                recipients.addStructuralSubscriber(
+                    subscription.subscriber, subscription.target)
+
+        return subscribers

=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py	2010-10-13 13:11:13 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2010-10-13 13:11:15 +0000
@@ -5,11 +5,7 @@
 
 __metaclass__ = type
 
-from storm.store import ResultSet
-
 from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
-from lp.registry.enum import BugNotificationLevel
 from lp.registry.interfaces.person import PersonVisibility
 from lp.registry.model.structuralsubscription import StructuralSubscription
 from lp.testing import (
@@ -17,7 +13,6 @@
     person_logged_in,
     TestCaseWithFactory,
     )
-from lp.testing.matchers import StartsWith
 
 
 class TestBug(TestCaseWithFactory):
@@ -130,84 +125,3 @@
             dupe_bug.subscribe(team, member)
             dupe_bug.markAsDuplicate(bug)
         self.assertTrue(team in bug.getSubscribersFromDuplicates())
-
-
-class TestBugStructuralSubscribers(TestCaseWithFactory):
-
-    layer = DatabaseFunctionalLayer
-
-    def test_getStructuralSubscribers_no_subscribers(self):
-        # If there are no subscribers for any of the bug's targets then no
-        # subscribers will be returned by getStructuralSubscribers().
-        product = self.factory.makeProduct()
-        bug = self.factory.makeBug(product=product)
-        subscribers = bug.getStructuralSubscribers()
-        self.assertIsInstance(subscribers, ResultSet)
-        self.assertEqual([], list(subscribers))
-
-    def test_getStructuralSubscribers_single_target(self):
-        # Subscribers for any of the bug's targets are returned.
-        subscriber = self.factory.makePerson()
-        login_person(subscriber)
-        product = self.factory.makeProduct()
-        product.addBugSubscription(subscriber, subscriber)
-        bug = self.factory.makeBug(product=product)
-        self.assertEqual([subscriber], list(bug.getStructuralSubscribers()))
-
-    def test_getStructuralSubscribers_multiple_targets(self):
-        # Subscribers for any of the bug's targets are returned.
-        actor = self.factory.makePerson()
-        login_person(actor)
-
-        subscriber1 = self.factory.makePerson()
-        subscriber2 = self.factory.makePerson()
-
-        product1 = self.factory.makeProduct(owner=actor)
-        product1.addBugSubscription(subscriber1, subscriber1)
-        product2 = self.factory.makeProduct(owner=actor)
-        product2.addBugSubscription(subscriber2, subscriber2)
-
-        bug = self.factory.makeBug(product=product1)
-        bug.addTask(actor, product2)
-
-        subscribers = bug.getStructuralSubscribers()
-        self.assertIsInstance(subscribers, ResultSet)
-        self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
-
-    def test_getStructuralSubscribers_recipients(self):
-        # If provided, getStructuralSubscribers() calls the appropriate
-        # methods on a BugNotificationRecipients object.
-        subscriber = self.factory.makePerson()
-        login_person(subscriber)
-        product = self.factory.makeProduct()
-        product.addBugSubscription(subscriber, subscriber)
-        bug = self.factory.makeBug(product=product)
-        recipients = BugNotificationRecipients()
-        subscribers = bug.getStructuralSubscribers(recipients=recipients)
-        # The return value is a list only when populating recipients.
-        self.assertIsInstance(subscribers, list)
-        self.assertEqual([subscriber], recipients.getRecipients())
-        reason, header = recipients.getReason(subscriber)
-        self.assertThat(
-            reason, StartsWith(
-                u"You received this bug notification because "
-                u"you are subscribed to "))
-        self.assertThat(header, StartsWith(u"Subscriber "))
-
-    def test_getStructuralSubscribers_level(self):
-        # getStructuralSubscribers() respects the given level.
-        subscriber = self.factory.makePerson()
-        login_person(subscriber)
-        product = self.factory.makeProduct()
-        subscription = product.addBugSubscription(subscriber, subscriber)
-        subscription.bug_notification_level = BugNotificationLevel.METADATA
-        bug = self.factory.makeBug(product=product)
-        self.assertEqual(
-            [subscriber], list(
-                bug.getStructuralSubscribers(
-                    level=BugNotificationLevel.METADATA)))
-        subscription.bug_notification_level = BugNotificationLevel.METADATA
-        self.assertEqual(
-            [], list(
-                bug.getStructuralSubscribers(
-                    level=BugNotificationLevel.COMMENTS)))

=== renamed file 'lib/lp/bugs/tests/test_bugtask.py' => 'lib/lp/bugs/model/tests/test_bugtask.py'
--- lib/lp/bugs/tests/test_bugtask.py	2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/model/tests/test_bugtask.py	2010-10-13 13:11:15 +0000
@@ -8,31 +8,55 @@
 import unittest
 
 from lazr.lifecycle.snapshot import Snapshot
+from storm.store import ResultSet
 from zope.component import getUtility
 from zope.interface import providedBy
+from zope.security.proxy import removeSecurityProxy
 
+from canonical.database.sqlbase import flush_database_updates
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.searchbuilder import (
     all,
     any,
     )
+from canonical.launchpad.testing import systemdocs
+from canonical.launchpad.webapp.interfaces import ILaunchBag
 from canonical.testing.layers import (
     DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
     )
+from lp.app.enums import ServiceUsage
+from lp.bugs.interfaces.bug import IBugSet
 from lp.bugs.interfaces.bugtarget import IBugTarget
 from lp.bugs.interfaces.bugtask import (
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
+    IBugTaskSet,
+    IUpstreamBugTask,
+    RESOLVED_BUGTASK_STATUSES,
+    UNRESOLVED_BUGTASK_STATUSES,
     )
+from lp.bugs.interfaces.bugwatch import IBugWatchSet
+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
 from lp.bugs.model.bugtask import build_tag_search_clause
+from lp.bugs.tests.bug import (
+    create_old_bug,
+    sync_bugtasks,
+    )
 from lp.hardwaredb.interfaces.hwdb import (
     HWBus,
     IHWDeviceSet,
     )
+from lp.registry.enum import BugNotificationLevel
 from lp.registry.interfaces.distribution import IDistributionSet
-from lp.registry.interfaces.person import IPerson, IPersonSet
+from lp.registry.interfaces.person import (
+    IPerson,
+    IPersonSet,
+    )
+from lp.registry.interfaces.product import IProductSet
+from lp.registry.interfaces.projectgroup import IProjectGroupSet
 from lp.testing import (
     ANONYMOUS,
     login,
@@ -42,6 +66,8 @@
     TestCase,
     TestCaseWithFactory,
     )
+from lp.testing.factory import LaunchpadObjectFactory
+from lp.testing.matchers import StartsWith
 
 
 class TestBugTaskDelta(TestCaseWithFactory):
@@ -892,7 +918,7 @@
         self.assertEqual(2, tasks.count())
         # Cache in the storm cache the account->person lookup so its not
         # distorting what we're testing.
-        _ = IPerson(person.account, None)
+        IPerson(person.account, None)
         # One query and only one should be issued to get the tasks, bugs and
         # allow access to getConjoinedMaster attribute - an attribute that
         # triggers a permission check (nb: id does not trigger such a check)
@@ -909,8 +935,427 @@
         self.assertEqual([task1], list(default_result))
 
 
+class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
+    """Tests for searching bugs filtering on related bug tasks.
+
+    It also acts as a helper class, which makes related doctests more
+    readable, since they can use methods from this class.
+    """
+    layer = DatabaseFunctionalLayer
+
+    def __init__(self, methodName='runTest', helper_only=False):
+        """If helper_only is True, set up it only as a helper class."""
+        if not helper_only:
+            unittest.TestCase.__init__(self, methodName=methodName)
+
+    def setUp(self):
+        login(ANONYMOUS)
+
+    def tearDown(self):
+        logout()
+
+    def _getBugTaskByTarget(self, bug, target):
+        """Return a bug's bugtask for the given target."""
+        for bugtask in bug.bugtasks:
+            if bugtask.target == target:
+                return bugtask
+        else:
+            raise AssertionError(
+                "Didn't find a %s task on bug %s." % (
+                    target.bugtargetname, bug.id))
+
+    def setUpBugsResolvedUpstreamTests(self):
+        """Modify some bugtasks to match the resolved upstream filter."""
+        bugset = getUtility(IBugSet)
+        productset = getUtility(IProductSet)
+        firefox = productset.getByName("firefox")
+        thunderbird = productset.getByName("thunderbird")
+
+        # Mark an upstream task on bug #1 "Fix Released"
+        bug_one = bugset.get(1)
+        firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
+        self.assertEqual(
+            ServiceUsage.LAUNCHPAD,
+            firefox_upstream.product.bug_tracking_usage)
+        self.old_firefox_status = firefox_upstream.status
+        firefox_upstream.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+        self.firefox_upstream = firefox_upstream
+
+        # Mark an upstream task on bug #9 "Fix Committed"
+        bug_nine = bugset.get(9)
+        thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
+        self.old_thunderbird_status = thunderbird_upstream.status
+        thunderbird_upstream.transitionToStatus(
+            BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
+        self.thunderbird_upstream = thunderbird_upstream
+
+        # Add a watch to a Debian bug for bug #2, and mark the task Fix
+        # Released.
+        bug_two = bugset.get(2)
+        bugwatchset = getUtility(IBugWatchSet)
+
+        # Get a debbugs watch.
+        watch_debbugs_327452 = bugwatchset.get(9)
+        self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
+        self.assertEquals(watch_debbugs_327452.remotebug, "327452")
+
+        # Associate the watch to a Fix Released task.
+        debian = getUtility(IDistributionSet).getByName("debian")
+        debian_firefox = debian.getSourcePackage("mozilla-firefox")
+        bug_two_in_debian_firefox = self._getBugTaskByTarget(
+            bug_two, debian_firefox)
+        bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
+        bug_two_in_debian_firefox.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+
+        flush_database_updates()
+
+    def tearDownBugsElsewhereTests(self):
+        """Resets the modified bugtasks to their original statuses."""
+        self.firefox_upstream.transitionToStatus(
+            self.old_firefox_status, getUtility(ILaunchBag).user)
+        self.thunderbird_upstream.transitionToStatus(
+            self.old_thunderbird_status, getUtility(ILaunchBag).user)
+        flush_database_updates()
+
+    def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
+        """Assert the bugtask is pending a bug watch elsewhere.
+
+        Pending a bugwatch elsewhere means that at least one of the bugtask's
+        related task's target isn't using Malone, and that
+        related_bugtask.bugwatch is None.
+        """
+        non_malone_using_bugtasks = [
+            related_task for related_task in bugtask.related_tasks
+            if not related_task.target_uses_malone]
+        pending_bugwatch_bugtasks = [
+            related_bugtask for related_bugtask in non_malone_using_bugtasks
+            if related_bugtask.bugwatch is None]
+        self.assert_(
+            len(pending_bugwatch_bugtasks) > 0,
+            'Bugtask %s on %s has no related bug watches elsewhere.' % (
+                bugtask.id, bugtask.target.displayname))
+
+    def assertBugTaskIsResolvedUpstream(self, bugtask):
+        """Make sure at least one of the related upstream tasks is resolved.
+
+        "Resolved", for our purposes, means either that one of the related
+        tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
+        it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
+        INVALID state.
+        """
+        resolved_upstream_states = [
+            BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
+        resolved_bugwatch_states = [
+            BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
+            BugTaskStatus.INVALID]
+
+        # Helper functions for the list comprehension below.
+        def _is_resolved_upstream_task(bugtask):
+            return (
+                IUpstreamBugTask.providedBy(bugtask) and
+                bugtask.status in resolved_upstream_states)
+
+        def _is_resolved_bugwatch_task(bugtask):
+            return (
+                bugtask.bugwatch and bugtask.status in
+                resolved_bugwatch_states)
+
+        resolved_related_tasks = [
+            related_task for related_task in bugtask.related_tasks
+            if (_is_resolved_upstream_task(related_task) or
+                _is_resolved_bugwatch_task(related_task))]
+
+        self.assert_(len(resolved_related_tasks) > 0)
+        self.assert_(
+            len(resolved_related_tasks) > 0,
+            'Bugtask %s on %s has no resolved related tasks.' % (
+                bugtask.id, bugtask.target.displayname))
+
+    def assertBugTaskIsOpenUpstream(self, bugtask):
+        """Make sure at least one of the related upstream tasks is open.
+
+        "Open", for our purposes, means either that one of the related
+        tasks is an upstream task or a task with a bugwatch which has
+        one of the states listed in open_states.
+        """
+        open_states = [
+            BugTaskStatus.NEW,
+            BugTaskStatus.INCOMPLETE,
+            BugTaskStatus.CONFIRMED,
+            BugTaskStatus.INPROGRESS,
+            BugTaskStatus.UNKNOWN]
+
+        # Helper functions for the list comprehension below.
+        def _is_open_upstream_task(bugtask):
+            return (
+                IUpstreamBugTask.providedBy(bugtask) and
+                bugtask.status in open_states)
+
+        def _is_open_bugwatch_task(bugtask):
+            return (
+                bugtask.bugwatch and bugtask.status in
+                open_states)
+
+        open_related_tasks = [
+            related_task for related_task in bugtask.related_tasks
+            if (_is_open_upstream_task(related_task) or
+                _is_open_bugwatch_task(related_task))]
+
+        self.assert_(
+            len(open_related_tasks) > 0,
+            'Bugtask %s on %s has no open related tasks.' % (
+                bugtask.id, bugtask.target.displayname))
+
+    def _hasUpstreamTask(self, bug):
+        """Does this bug have an upstream task associated with it?
+
+        Returns True if yes, otherwise False.
+        """
+        for bugtask in bug.bugtasks:
+            if IUpstreamBugTask.providedBy(bugtask):
+                return True
+        return False
+
+    def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
+        """Should the bugtask be shown in the search no upstream task search?
+
+        Returns True if yes, otherwise False.
+        """
+        self.assert_(
+            not self._hasUpstreamTask(bugtask.bug),
+            'Bugtask %s on %s has upstream tasks.' % (
+                bugtask.id, bugtask.target.displayname))
+
+
+class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
+    """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        """Setup the zope interaction and create expirable bugtasks."""
+        login('test@xxxxxxxxxxxxx')
+        self.user = getUtility(ILaunchBag).user
+        self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
+        self.distroseries = self.distribution.getSeries('hoary')
+        self.product = getUtility(IProductSet).getByName('jokosher')
+        self.productseries = self.product.getSeries('trunk')
+        self.bugtaskset = getUtility(IBugTaskSet)
+        bugtasks = []
+        bugtasks.append(
+            create_old_bug("90 days old", 90, self.distribution))
+        bugtasks.append(
+            self.bugtaskset.createTask(
+                bug=bugtasks[-1].bug, owner=self.user,
+                distroseries=self.distroseries))
+        bugtasks.append(
+            create_old_bug("90 days old", 90, self.product))
+        bugtasks.append(
+            self.bugtaskset.createTask(
+                bug=bugtasks[-1].bug, owner=self.user,
+                productseries=self.productseries))
+        sync_bugtasks(bugtasks)
+
+    def tearDown(self):
+        logout()
+
+    def testSupportedTargetParam(self):
+        """The target param supports a limited set of BugTargets.
+
+        Four BugTarget types may passed as the target argument:
+        Distribution, DistroSeries, Product, ProductSeries.
+        """
+        supported_targets = [self.distribution, self.distroseries,
+                             self.product, self.productseries]
+        for target in supported_targets:
+            expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
+                0, self.user, target=target)
+            self.assertNotEqual(expirable_bugtasks.count(), 0,
+                 "%s has %d expirable bugtasks." %
+                 (self.distroseries, expirable_bugtasks.count()))
+
+    def testUnsupportedBugTargetParam(self):
+        """Test that unsupported targets raise errors.
+
+        Three BugTarget types are not supported because the UI does not
+        provide bug-index to link to the 'bugs that can expire' page.
+        ProjectGroup, SourcePackage, and DistributionSourcePackage will
+        raise an NotImplementedError.
+
+        Passing an unknown bugtarget type will raise an AssertionError.
+        """
+        project = getUtility(IProjectGroupSet).getByName('mozilla')
+        distributionsourcepackage = self.distribution.getSourcePackage(
+            'mozilla-firefox')
+        sourcepackage = self.distroseries.getSourcePackage(
+            'mozilla-firefox')
+        unsupported_targets = [project, distributionsourcepackage,
+                               sourcepackage]
+        for target in unsupported_targets:
+            self.assertRaises(
+                NotImplementedError, self.bugtaskset.findExpirableBugTasks,
+                0, self.user, target=target)
+
+        # Objects that are not a known BugTarget type raise an AssertionError.
+        self.assertRaises(
+            AssertionError, self.bugtaskset.findExpirableBugTasks,
+            0, self.user, target=[])
+
+
+class BugTaskSetTest(unittest.TestCase):
+    """Test `BugTaskSet` methods."""
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        login(ANONYMOUS)
+
+    def test_getBugTasks(self):
+        """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
+        bugs to their bugtasks. It does that in a single query, to avoid
+        hitting the DB again when getting the bugs' tasks.
+        """
+        login('no-priv@xxxxxxxxxxxxx')
+        factory = LaunchpadObjectFactory()
+        bug1 = factory.makeBug()
+        factory.makeBugTask(bug1)
+        bug2 = factory.makeBug()
+        factory.makeBugTask(bug2)
+        factory.makeBugTask(bug2)
+
+        bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
+            [bug1.id, bug2.id])
+        # The bugtasks returned by getBugTasks() are exactly the same as the
+        # ones returned by bug.bugtasks, obviously.
+        self.failUnlessEqual(
+            set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
+            set([]))
+        self.failUnlessEqual(
+            set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
+            set([]))
+
+    def test_getBugTasks_with_empty_list(self):
+        # When given an empty list of bug IDs, getBugTasks() will return an
+        # empty dictionary.
+        bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
+        self.failUnlessEqual(bugs_and_tasks, {})
+
+
+class TestBugTaskStatuses(TestCase):
+
+    def test_open_and_resolved_statuses(self):
+        """
+        There are constants that are used to define which statuses are for
+        resolved bugs (`RESOLVED_BUGTASK_STATUSES`), and which are for
+        unresolved bugs (`UNRESOLVED_BUGTASK_STATUSES`). The two constants
+        include all statuses defined in BugTaskStatus, except for Unknown.
+        """
+        self.assertNotIn(BugTaskStatus.UNKNOWN, RESOLVED_BUGTASK_STATUSES)
+        self.assertNotIn(BugTaskStatus.UNKNOWN, UNRESOLVED_BUGTASK_STATUSES)
+
+
+class TestGetStructuralSubscribers(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def make_product_with_bug(self):
+        product = self.factory.makeProduct()
+        bug = self.factory.makeBug(product=product)
+        # getStructuralSubscribers() is intended for use only in model code,
+        # and will not work if the bugtasks passed in are security proxied.
+        return product, removeSecurityProxy(bug)
+
+    def test_getStructuralSubscribers_no_subscribers(self):
+        # If there are no subscribers for any of the bug's targets then no
+        # subscribers will be returned by getStructuralSubscribers().
+        product, bug = self.make_product_with_bug()
+        bug_task_set = getUtility(IBugTaskSet)
+        subscribers = bug_task_set.getStructuralSubscribers(bug.bugtasks)
+        self.assertIsInstance(subscribers, ResultSet)
+        self.assertEqual([], list(subscribers))
+
+    def test_getStructuralSubscribers_single_target(self):
+        # Subscribers for any of the bug's targets are returned.
+        subscriber = self.factory.makePerson()
+        login_person(subscriber)
+        product, bug = self.make_product_with_bug()
+        product.addBugSubscription(subscriber, subscriber)
+        bug_task_set = getUtility(IBugTaskSet)
+        self.assertEqual(
+            [subscriber], list(
+                bug_task_set.getStructuralSubscribers(bug.bugtasks)))
+
+    def test_getStructuralSubscribers_multiple_targets(self):
+        # Subscribers for any of the bug's targets are returned.
+        actor = self.factory.makePerson()
+        login_person(actor)
+
+        subscriber1 = self.factory.makePerson()
+        subscriber2 = self.factory.makePerson()
+
+        product1 = self.factory.makeProduct(owner=actor)
+        product1.addBugSubscription(subscriber1, subscriber1)
+        product2 = self.factory.makeProduct(owner=actor)
+        product2.addBugSubscription(subscriber2, subscriber2)
+
+        bug = self.factory.makeBug(product=product1)
+        bug.addTask(actor, product2)
+
+        # getStructuralSubscribers() is intended for use only in model code,
+        # and will not work if the bugtasks passed in are security proxied.
+        bug = removeSecurityProxy(bug)
+
+        bug_task_set = getUtility(IBugTaskSet)
+        subscribers = bug_task_set.getStructuralSubscribers(bug.bugtasks)
+        self.assertIsInstance(subscribers, ResultSet)
+        self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
+
+    def test_getStructuralSubscribers_recipients(self):
+        # If provided, getStructuralSubscribers() calls the appropriate
+        # methods on a BugNotificationRecipients object.
+        subscriber = self.factory.makePerson()
+        login_person(subscriber)
+        product, bug = self.make_product_with_bug()
+        product.addBugSubscription(subscriber, subscriber)
+        recipients = BugNotificationRecipients()
+        bug_task_set = getUtility(IBugTaskSet)
+        subscribers = bug_task_set.getStructuralSubscribers(
+            bug.bugtasks, recipients=recipients)
+        # The return value is a list only when populating recipients.
+        self.assertIsInstance(subscribers, list)
+        self.assertEqual([subscriber], recipients.getRecipients())
+        reason, header = recipients.getReason(subscriber)
+        self.assertThat(
+            reason, StartsWith(
+                u"You received this bug notification because "
+                u"you are subscribed to "))
+        self.assertThat(header, StartsWith(u"Subscriber "))
+
+    def test_getStructuralSubscribers_level(self):
+        # getStructuralSubscribers() respects the given level.
+        subscriber = self.factory.makePerson()
+        login_person(subscriber)
+        product, bug = self.make_product_with_bug()
+        subscription = product.addBugSubscription(subscriber, subscriber)
+        subscription.bug_notification_level = BugNotificationLevel.METADATA
+        bug_task_set = getUtility(IBugTaskSet)
+        self.assertEqual(
+            [subscriber], list(
+                bug_task_set.getStructuralSubscribers(
+                    bug.bugtasks, level=BugNotificationLevel.METADATA)))
+        subscription.bug_notification_level = BugNotificationLevel.METADATA
+        self.assertEqual(
+            [], list(
+                bug_task_set.getStructuralSubscribers(
+                    bug.bugtasks, level=BugNotificationLevel.COMMENTS)))
+
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
     suite.addTest(DocTestSuite('lp.bugs.model.bugtask'))
+    suite.addTest(
+        systemdocs.LayeredDocFileSuite(
+            'test_bugtask_status.txt', layer=LaunchpadFunctionalLayer,
+            setUp=systemdocs.setUp, tearDown=systemdocs.tearDown))
     return suite

=== renamed file 'lib/lp/bugs/tests/test_bugtask_status.txt' => 'lib/lp/bugs/model/tests/test_bugtask_status.txt'
=== modified file 'lib/lp/bugs/subscribers/bug.py'
--- lib/lp/bugs/subscribers/bug.py	2010-08-23 09:25:17 +0000
+++ lib/lp/bugs/subscribers/bug.py	2010-10-13 13:11:15 +0000
@@ -19,6 +19,8 @@
 import datetime
 from operator import attrgetter
 
+from zope.component import getUtility
+
 from canonical.config import config
 from canonical.database.sqlbase import block_implicit_flushes
 from canonical.launchpad.helpers import get_contact_email_addresses
@@ -34,14 +36,12 @@
     )
 from lp.bugs.adapters.bugdelta import BugDelta
 from lp.bugs.interfaces.bugchange import IBugChange
+from lp.bugs.interfaces.bugtask import IBugTaskSet
 from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
 from lp.bugs.mail.newbug import generate_bug_add_email
 from lp.registry.enum import BugNotificationLevel
 from lp.registry.interfaces.person import IPerson
-from lp.registry.interfaces.structuralsubscription import (
-    IStructuralSubscriptionTarget,
-    )
 
 
 @block_implicit_flushes
@@ -179,15 +179,10 @@
         if recipients is not None:
             recipients.addAssignee(bugtask.assignee)
 
-    if IStructuralSubscriptionTarget.providedBy(bugtask.target):
-        also_notified_subscribers.update(
-            bugtask.target.getBugNotificationsRecipients(
-                recipients, level=level))
-
-    if bugtask.milestone is not None:
-        also_notified_subscribers.update(
-            bugtask.milestone.getBugNotificationsRecipients(
-                recipients, level=level))
+    # Get structural subscribers.
+    also_notified_subscribers.update(
+        getUtility(IBugTaskSet).getStructuralSubscribers(
+            [bugtask], recipients, level))
 
     # If the target's bug supervisor isn't set,
     # we add the owner as a subscriber.

=== removed file 'lib/lp/bugs/tests/test_bugtask_0.py'
--- lib/lp/bugs/tests/test_bugtask_0.py	2010-10-03 15:30:06 +0000
+++ lib/lp/bugs/tests/test_bugtask_0.py	1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for bugtask.py."""
-
-__metaclass__ = type
-
-from doctest import (
-    DocTestSuite,
-    ELLIPSIS,
-    NORMALIZE_WHITESPACE,
-    REPORT_NDIFF,
-    )
-
-
-def test_open_and_resolved_statuses(self):
-    """
-    There are constants that are used to define which statuses are for
-    resolved bugs (RESOLVED_BUGTASK_STATUSES), and which are for
-    unresolved bugs (UNRESOLVED_BUGTASK_STATUSES). The two constants
-    include all statuses defined in BugTaskStatus, except for Unknown.
-
-        >>> from canonical.launchpad.interfaces import (
-        ...     RESOLVED_BUGTASK_STATUSES, UNRESOLVED_BUGTASK_STATUSES)
-        >>> from canonical.launchpad.interfaces import BugTaskStatus
-        >>> not_included_status = set(BugTaskStatus.items).difference(
-        ...     RESOLVED_BUGTASK_STATUSES + UNRESOLVED_BUGTASK_STATUSES)
-        >>> [status.name for status in not_included_status]
-        ['UNKNOWN']
-    """
-
-def test_suite():
-    suite = DocTestSuite(
-        optionflags=REPORT_NDIFF|NORMALIZE_WHITESPACE|ELLIPSIS)
-    return suite
-

=== removed file 'lib/lp/bugs/tests/test_bugtask_1.py'
--- lib/lp/bugs/tests/test_bugtask_1.py	2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/tests/test_bugtask_1.py	1970-01-01 00:00:00 +0000
@@ -1,343 +0,0 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Bugtask related tests that are too complex to be readable as doctests."""
-
-__metaclass__ = type
-
-import unittest
-
-from zope.component import getUtility
-
-from canonical.database.sqlbase import flush_database_updates
-from canonical.launchpad.ftests import (
-    ANONYMOUS,
-    login,
-    logout,
-    )
-from canonical.launchpad.webapp.interfaces import ILaunchBag
-from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.app.enums import ServiceUsage
-from lp.bugs.interfaces.bug import IBugSet
-from lp.bugs.interfaces.bugtask import (
-    BugTaskStatus,
-    IBugTaskSet,
-    IUpstreamBugTask,
-    )
-from lp.bugs.interfaces.bugwatch import IBugWatchSet
-from lp.bugs.tests.bug import (
-    create_old_bug,
-    sync_bugtasks,
-    )
-from lp.registry.interfaces.distribution import IDistributionSet
-from lp.registry.interfaces.product import IProductSet
-from lp.registry.interfaces.projectgroup import IProjectGroupSet
-from lp.testing.factory import LaunchpadObjectFactory
-
-
-class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
-    """Tests for searching bugs filtering on related bug tasks.
-
-    It also acts as a helper class, which makes related doctests more
-    readable, since they can use methods from this class.
-    """
-    layer = DatabaseFunctionalLayer
-
-    def __init__(self, methodName='runTest', helper_only=False):
-        """If helper_only is True, set up it only as a helper class."""
-        if not helper_only:
-            unittest.TestCase.__init__(self, methodName=methodName)
-
-    def setUp(self):
-        login(ANONYMOUS)
-
-    def tearDown(self):
-        logout()
-
-    def _getBugTaskByTarget(self, bug, target):
-        """Return a bug's bugtask for the given target."""
-        for bugtask in bug.bugtasks:
-            if bugtask.target == target:
-                return bugtask
-        else:
-            raise AssertionError(
-                "Didn't find a %s task on bug %s." % (
-                    target.bugtargetname, bug.id))
-
-    def setUpBugsResolvedUpstreamTests(self):
-        """Modify some bugtasks to match the resolved upstream filter."""
-        bugset = getUtility(IBugSet)
-        productset = getUtility(IProductSet)
-        firefox = productset.getByName("firefox")
-        thunderbird = productset.getByName("thunderbird")
-
-        # Mark an upstream task on bug #1 "Fix Released"
-        bug_one = bugset.get(1)
-        firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
-        self.assertEqual(
-            ServiceUsage.LAUNCHPAD,
-            firefox_upstream.product.bug_tracking_usage)
-        self.old_firefox_status = firefox_upstream.status
-        firefox_upstream.transitionToStatus(
-            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
-        self.firefox_upstream = firefox_upstream
-
-        # Mark an upstream task on bug #9 "Fix Committed"
-        bug_nine = bugset.get(9)
-        thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
-        self.old_thunderbird_status = thunderbird_upstream.status
-        thunderbird_upstream.transitionToStatus(
-            BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
-        self.thunderbird_upstream = thunderbird_upstream
-
-        # Add a watch to a Debian bug for bug #2, and mark the task Fix
-        # Released.
-        bug_two = bugset.get(2)
-        current_user = getUtility(ILaunchBag).user
-        bugtaskset = getUtility(IBugTaskSet)
-        bugwatchset = getUtility(IBugWatchSet)
-
-        # Get a debbugs watch.
-        watch_debbugs_327452 = bugwatchset.get(9)
-        self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
-        self.assertEquals(watch_debbugs_327452.remotebug, "327452")
-
-        # Associate the watch to a Fix Released task.
-        debian = getUtility(IDistributionSet).getByName("debian")
-        debian_firefox = debian.getSourcePackage("mozilla-firefox")
-        bug_two_in_debian_firefox = self._getBugTaskByTarget(
-            bug_two, debian_firefox)
-        bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
-        bug_two_in_debian_firefox.transitionToStatus(
-            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
-
-        flush_database_updates()
-
-    def tearDownBugsElsewhereTests(self):
-        """Resets the modified bugtasks to their original statuses."""
-        self.firefox_upstream.transitionToStatus(
-            self.old_firefox_status, getUtility(ILaunchBag).user)
-        self.thunderbird_upstream.transitionToStatus(
-            self.old_thunderbird_status, getUtility(ILaunchBag).user)
-        flush_database_updates()
-
-    def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
-        """Assert the bugtask is pending a bug watch elsewhere.
-
-        Pending a bugwatch elsewhere means that at least one of the bugtask's
-        related task's target isn't using Malone, and that
-        related_bugtask.bugwatch is None.
-        """
-        non_malone_using_bugtasks = [
-            related_task for related_task in bugtask.related_tasks
-            if not related_task.target_uses_malone]
-        pending_bugwatch_bugtasks = [
-            related_bugtask for related_bugtask in non_malone_using_bugtasks
-            if related_bugtask.bugwatch is None]
-        self.assert_(
-            len(pending_bugwatch_bugtasks) > 0,
-            'Bugtask %s on %s has no related bug watches elsewhere.' % (
-                bugtask.id, bugtask.target.displayname))
-
-    def assertBugTaskIsResolvedUpstream(self, bugtask):
-        """Make sure at least one of the related upstream tasks is resolved.
-
-        "Resolved", for our purposes, means either that one of the related
-        tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
-        it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
-        INVALID state.
-        """
-        resolved_upstream_states = [
-            BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
-        resolved_bugwatch_states = [
-            BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
-            BugTaskStatus.INVALID]
-
-        # Helper functions for the list comprehension below.
-        def _is_resolved_upstream_task(bugtask):
-            return (
-                IUpstreamBugTask.providedBy(bugtask) and
-                bugtask.status in resolved_upstream_states)
-
-        def _is_resolved_bugwatch_task(bugtask):
-            return (
-                bugtask.bugwatch and bugtask.status in
-                resolved_bugwatch_states)
-
-        resolved_related_tasks = [
-            related_task for related_task in bugtask.related_tasks
-            if (_is_resolved_upstream_task(related_task) or
-                _is_resolved_bugwatch_task(related_task))]
-
-        self.assert_(len(resolved_related_tasks) > 0)
-        self.assert_(
-            len(resolved_related_tasks) > 0,
-            'Bugtask %s on %s has no resolved related tasks.' % (
-                bugtask.id, bugtask.target.displayname))
-
-    def assertBugTaskIsOpenUpstream(self, bugtask):
-        """Make sure at least one of the related upstream tasks is open.
-
-        "Open", for our purposes, means either that one of the related
-        tasks is an upstream task or a task with a bugwatch which has
-        one of the states listed in open_states.
-        """
-        open_states = [
-            BugTaskStatus.NEW,
-            BugTaskStatus.INCOMPLETE,
-            BugTaskStatus.CONFIRMED,
-            BugTaskStatus.INPROGRESS,
-            BugTaskStatus.UNKNOWN]
-
-        # Helper functions for the list comprehension below.
-        def _is_open_upstream_task(bugtask):
-            return (
-                IUpstreamBugTask.providedBy(bugtask) and
-                bugtask.status in open_states)
-
-        def _is_open_bugwatch_task(bugtask):
-            return (
-                bugtask.bugwatch and bugtask.status in
-                open_states)
-
-        open_related_tasks = [
-            related_task for related_task in bugtask.related_tasks
-            if (_is_open_upstream_task(related_task) or
-                _is_open_bugwatch_task(related_task))]
-
-        self.assert_(
-            len(open_related_tasks) > 0,
-            'Bugtask %s on %s has no open related tasks.' % (
-                bugtask.id, bugtask.target.displayname))
-
-    def _hasUpstreamTask(self, bug):
-        """Does this bug have an upstream task associated with it?
-
-        Returns True if yes, otherwise False.
-        """
-        for bugtask in bug.bugtasks:
-            if IUpstreamBugTask.providedBy(bugtask):
-                return True
-        return False
-
-    def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
-        """Should the bugtask be shown in the search no upstream task search?
-
-        Returns True if yes, otherwise False.
-        """
-        self.assert_(
-            not self._hasUpstreamTask(bugtask.bug),
-            'Bugtask %s on %s has upstream tasks.' % (
-                bugtask.id, bugtask.target.displayname))
-
-
-class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
-    """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        """Setup the zope interaction and create expirable bugtasks."""
-        login('test@xxxxxxxxxxxxx')
-        self.user = getUtility(ILaunchBag).user
-        self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
-        self.distroseries = self.distribution.getSeries('hoary')
-        self.product = getUtility(IProductSet).getByName('jokosher')
-        self.productseries = self.product.getSeries('trunk')
-        self.bugtaskset = getUtility(IBugTaskSet)
-        bugtasks = []
-        bugtasks.append(
-            create_old_bug("90 days old", 90, self.distribution))
-        bugtasks.append(
-            self.bugtaskset.createTask(
-                bug=bugtasks[-1].bug, owner=self.user,
-                distroseries=self.distroseries))
-        bugtasks.append(
-            create_old_bug("90 days old", 90, self.product))
-        bugtasks.append(
-            self.bugtaskset.createTask(
-                bug=bugtasks[-1].bug, owner=self.user,
-                productseries=self.productseries))
-        sync_bugtasks(bugtasks)
-
-    def tearDown(self):
-        logout()
-
-    def testSupportedTargetParam(self):
-        """The target param supports a limited set of BugTargets.
-
-        Four BugTarget types may passed as the target argument:
-        Distribution, DistroSeries, Product, ProductSeries.
-        """
-        supported_targets = [self.distribution, self.distroseries,
-                             self.product, self.productseries]
-        for target in supported_targets:
-            expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
-                0, self.user, target=target)
-            self.assertNotEqual(expirable_bugtasks.count(), 0,
-                 "%s has %d expirable bugtasks." %
-                 (self.distroseries, expirable_bugtasks.count()))
-
-    def testUnsupportedBugTargetParam(self):
-        """Test that unsupported targets raise errors.
-
-        Three BugTarget types are not supported because the UI does not
-        provide bug-index to link to the 'bugs that can expire' page.
-        ProjectGroup, SourcePackage, and DistributionSourcePackage will
-        raise an NotImplementedError.
-
-        Passing an unknown bugtarget type will raise an AssertionError.
-        """
-        project = getUtility(IProjectGroupSet).getByName('mozilla')
-        distributionsourcepackage = self.distribution.getSourcePackage(
-            'mozilla-firefox')
-        sourcepackage = self.distroseries.getSourcePackage(
-            'mozilla-firefox')
-        unsupported_targets = [project, distributionsourcepackage,
-                               sourcepackage]
-        for target in unsupported_targets:
-            self.assertRaises(
-                NotImplementedError, self.bugtaskset.findExpirableBugTasks,
-                0, self.user, target=target)
-
-        # Objects that are not a known BugTarget type raise an AssertionError.
-        self.assertRaises(
-            AssertionError, self.bugtaskset.findExpirableBugTasks,
-            0, self.user, target=[])
-
-
-class BugTaskSetTest(unittest.TestCase):
-    """Test `BugTaskSet` methods."""
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        login(ANONYMOUS)
-
-    def test_getBugTasks(self):
-        """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
-        bugs to their bugtasks. It does that in a single query, to avoid
-        hitting the DB again when getting the bugs' tasks.
-        """
-        login('no-priv@xxxxxxxxxxxxx')
-        factory = LaunchpadObjectFactory()
-        bug1 = factory.makeBug()
-        factory.makeBugTask(bug1)
-        bug2 = factory.makeBug()
-        factory.makeBugTask(bug2)
-        factory.makeBugTask(bug2)
-
-        bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
-            [bug1.id, bug2.id])
-        # The bugtasks returned by getBugTasks() are exactly the same as the
-        # ones returned by bug.bugtasks, obviously.
-        self.failUnlessEqual(
-            set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
-            set([]))
-        self.failUnlessEqual(
-            set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
-            set([]))
-
-    def test_getBugTasks_with_empty_list(self):
-        # When given an empty list of bug IDs, getBugTasks() will return an
-        # empty dictionary.
-        bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
-        self.failUnlessEqual(bugs_and_tasks, {})

=== removed file 'lib/lp/bugs/tests/test_bugtask_status.py'
--- lib/lp/bugs/tests/test_bugtask_status.py	2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/tests/test_bugtask_status.py	1970-01-01 00:00:00 +0000
@@ -1,22 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test for choosing the request and publication."""
-
-__metaclass__ = type
-
-from canonical.launchpad.testing.systemdocs import (
-    LayeredDocFileSuite,
-    setUp,
-    tearDown,
-    )
-from canonical.testing.layers import LaunchpadFunctionalLayer
-
-
-def test_suite():
-    suite = LayeredDocFileSuite(
-            'test_bugtask_status.txt',
-            layer=LaunchpadFunctionalLayer, setUp=setUp, tearDown=tearDown,
-            )
-    return suite
-

=== modified file 'lib/lp/registry/doc/structural-subscriptions.txt'
--- lib/lp/registry/doc/structural-subscriptions.txt	2010-10-13 13:11:13 +0000
+++ lib/lp/registry/doc/structural-subscriptions.txt	2010-10-13 13:11:15 +0000
@@ -85,12 +85,7 @@
 When notifying subscribers of bug activity, both subscribers to the
 target and to the target's parent are notified.
 
-    >>> from canonical.launchpad.ftests import syncUpdate
     >>> from lp.registry.enum import BugNotificationLevel
-    >>> from canonical.launchpad.interfaces import (
-    ...     BlueprintNotificationLevel)
-    >>> from lp.bugs.mail.bugnotificationrecipients import (
-    ...     BugNotificationRecipients)
 
 We define some utility functions for printing out bug subscriptions and
 the recipients for the notifications they generate.
@@ -114,89 +109,6 @@
     >>> ubuntu_sub.bug_notification_level = BugNotificationLevel.COMMENTS
     >>> evolution_sub.bug_notification_level = BugNotificationLevel.COMMENTS
 
-`getBugNotificationsRecipients` returns all the bug subscribers to the
-target and its parent, and adds the rationale for the subscriptions to
-the recipients set. Each subscriber is only added once.
-
-    >>> recipients = BugNotificationRecipients()
-    >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
-    ...     recipients=recipients)
-    >>> print_bug_subscriptions(ubuntu.bug_subscriptions)
-    name12
-    >>> print_bug_subscriptions(evolution_package.bug_subscriptions)
-    name12
-    >>> print_bug_subscribers(bug_subscribers)
-    name12
-    >>> print_bug_recipients(recipients)
-    name12 "Subscriber (evolution in ubuntu)"
-
-Foo Bar subscribes to Ubuntu.
-
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> foobar_subscription = ubuntu.addBugSubscription(foobar, foobar)
-    >>> recipients = BugNotificationRecipients()
-
-The set of subscribers to the evolution package for ubuntu now includes
-both subscribers to the package, and subscribers to the distribution.
-
-    >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
-    ...     recipients=recipients)
-    >>> print_bug_recipients(recipients)
-    name16 "Subscriber (Ubuntu)"
-    name12 "Subscriber (evolution in ubuntu)"
-
-We can pass the parameter `level` to getBugNotificationsRecipients().
-Subscribers whose subscription level is lower than the given parameter
-are not returned.
-
-    >>> foobar_subscription.bug_notification_level = (
-    ...     BugNotificationLevel.METADATA)
-    >>> recipients = BugNotificationRecipients()
-    >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
-    ...     recipients=recipients, level=BugNotificationLevel.COMMENTS)
-    >>> print_bug_recipients(recipients)
-    name12 "Subscriber (evolution in ubuntu)"
-
-We remove Sample Person's bug subscription to the package.
-
-    >>> evolution_sub.blueprint_notification_level = (
-    ...     BlueprintNotificationLevel.METADATA)
-    >>> evolution_package.removeBugSubscription(sampleperson, sampleperson)
-    >>> ubuntu.removeBugSubscription(sampleperson, sampleperson)
-    >>> syncUpdate(evolution_sub)
-
-Sample Person is no longer a subscriber to the package, but Foo Bar
-is still a subscriber, by being subscribed to Ubuntu.
-
-    >>> print_bug_subscribers(
-    ...     evolution_package.getBugNotificationsRecipients(
-    ...         recipients=recipients))
-    name16
-
-A project is the parent of each of its products.
-
-Fireox does not have any subscribers.
-
-    >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
-
-Mozilla is the parent of Fireox.
-
-    >>> from canonical.launchpad.interfaces import IProjectGroupSet
-    >>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
-    >>> print firefox.parent_subscription_target.displayname
-    the Mozilla Project
-
-Foobar subscribes to bug notificatios for Mozilla.
-
-    >>> mozilla.addBugSubscription(foobar, foobar)
-    <StructuralSubscription at ...>
-
-As a result of subscribing to Mozilla, Foobar is now a subscriber of
-Firefox.
-
-    >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
-    name16
-
 
 Target type display
 ===================

=== modified file 'lib/lp/registry/interfaces/structuralsubscription.py'
--- lib/lp/registry/interfaces/structuralsubscription.py	2010-10-13 13:11:13 +0000
+++ lib/lp/registry/interfaces/structuralsubscription.py	2010-10-13 13:11:15 +0000
@@ -181,20 +181,6 @@
     def getSubscription(person):
         """Return the subscription for `person`, if it exists."""
 
-    def getBugNotificationsRecipients(recipients=None, level=None):
-        """Return the set of bug subscribers to this target.
-
-        :param recipients: If recipients is not None, a rationale
-            is added for each subscriber.
-        :type recipients: `INotificationRecipientSet`
-        'param level: If level is not None, only strucutral
-            subscribers with a subscrition level greater or equal
-            to the given value are returned.
-        :type level: `BugNotificationLevel`
-        :return: An `INotificationRecipientSet` instance containing
-            the bug subscribers.
-        """
-
     target_type_display = Attribute("The type of the target, for display.")
 
     def userHasBugSubscriptions(user):

=== modified file 'lib/lp/registry/model/structuralsubscription.py'
--- lib/lp/registry/model/structuralsubscription.py	2010-10-13 13:11:13 +0000
+++ lib/lp/registry/model/structuralsubscription.py	2010-10-13 13:11:15 +0000
@@ -448,24 +448,6 @@
         return StructuralSubscription.select(
             query, orderBy='Person.displayname', clauseTables=['Person'])
 
-    def getBugNotificationsRecipients(self, recipients=None, level=None):
-        """See `IStructuralSubscriptionTarget`."""
-        if level is None:
-            subscriptions = self.bug_subscriptions
-        else:
-            subscriptions = self.getSubscriptions(
-                min_bug_notification_level=level)
-        subscribers = set(
-            subscription.subscriber for subscription in subscriptions)
-        if recipients is not None:
-            for subscriber in subscribers:
-                recipients.addStructuralSubscriber(subscriber, self)
-        parent = self.parent_subscription_target
-        if parent is not None:
-            subscribers.update(
-                parent.getBugNotificationsRecipients(recipients, level))
-        return subscribers
-
     @property
     def bug_subscriptions(self):
         """See `IStructuralSubscriptionTarget`."""


Follow ups