← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/wire-up-filter-subs-bug-655567 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/wire-up-filter-subs-bug-655567 into lp:launchpad with lp:~allenap/launchpad/subs-for-bugtask-bug-656194 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #655567 Wire up getSubscriptionsForBug() into the notifications machinery
  https://bugs.launchpad.net/bugs/655567


Replace the use of PersonSet.getSubscribersForTargets() with
bug.getStructuralSubscribers(), and remove the former from the
code-base entirely.

There are also a small sprinkling of lint fixes. One I feel I should
comment on:

{{{
-            def to_messages(rows):
-                return [row[0] for row in rows]
+            to_messages = lambda rows: [row[0] for row in rows]
         else:
-            def to_messages(rows):
-                return rows
+            to_messages = lambda rows: rows
}}}

While it's equivalent code, this silences a warning in pyflakes. While
we probably should fix pyflakes, that's out of scope for now, and
keeping lint warnings to a minimum is useful for collaboration.

I have also fixed a small bug in format-new-and-modified-imports which
caused it to fail when files are renamed.

-- 
https://code.launchpad.net/~allenap/launchpad/wire-up-filter-subs-bug-655567/+merge/37857
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/wire-up-filter-subs-bug-655567 into lp:launchpad.
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2010-10-06 18:53:53 +0000
+++ lib/lp/bugs/configure.zcml	2010-10-07 15:02:46 +0000
@@ -706,6 +706,7 @@
                     getSubscribersForPerson
                     indexed_messages
                     getAlsoNotifiedSubscribers
+                    getStructuralSubscribers
                     getBugWatch
                     canBeNominatedFor
                     getNominationFor

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2010-10-03 15:30:06 +0000
+++ lib/lp/bugs/interfaces/bug.py	2010-10-07 15:02:46 +0000
@@ -491,6 +491,12 @@
         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."""
 
@@ -499,9 +505,9 @@
 
     def getSubscribersForPerson(person):
         """Find the persons or teams by which person is subscribed.
-        
+
         This call should be quite cheap to make and performs a single query.
-        
+
         :return: An IResultSet.
         """
 

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2010-10-04 06:20:04 +0000
+++ lib/lp/bugs/model/bug.py	2010-10-07 15:02:46 +0000
@@ -51,7 +51,6 @@
     In,
     LeftJoin,
     Max,
-    Min,
     Not,
     Or,
     Select,
@@ -156,14 +155,13 @@
 from lp.bugs.model.bugnomination import BugNomination
 from lp.bugs.model.bugnotification import BugNotification
 from lp.bugs.model.bugsubscription import BugSubscription
+from lp.bugs.model.bugtarget import OfficialBugTag
 from lp.bugs.model.bugtask import (
     BugTask,
     bugtask_sort_key,
-    BugTaskSet,
     get_bug_privacy_filter,
     NullBugTask,
     )
-from lp.bugs.model.bugtarget import OfficialBugTag
 from lp.bugs.model.bugwatch import BugWatch
 from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
 from lp.registry.enum import BugNotificationLevel
@@ -172,10 +170,7 @@
     IDistributionSourcePackage,
     )
 from lp.registry.interfaces.distroseries import IDistroSeries
-from lp.registry.interfaces.person import (
-    IPersonSet,
-    validate_public_person,
-    )
+from lp.registry.interfaces.person import validate_public_person
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.series import SeriesStatus
@@ -458,11 +453,9 @@
         store = Store.of(self)
         message_by_id = {}
         if include_parents:
-            def to_messages(rows):
-                return [row[0] for row in rows]
+            to_messages = lambda rows: [row[0] for row in rows]
         else:
-            def to_messages(rows):
-                return rows
+            to_messages = lambda rows: rows
         def eager_load_owners(messages):
             # Because we may have multiple owners, we spend less time in storm
             # with very large bugs by not joining and instead querying a second
@@ -960,31 +953,12 @@
 
         also_notified_subscribers = set()
 
-        structural_subscription_targets = set()
-
         for bugtask in self.bugtasks:
             if bugtask.assignee:
                 also_notified_subscribers.add(bugtask.assignee)
                 if recipients is not None:
                     recipients.addAssignee(bugtask.assignee)
 
-            if IStructuralSubscriptionTarget.providedBy(bugtask.target):
-                structural_subscription_targets.add(bugtask.target)
-                if bugtask.target.parent_subscription_target is not None:
-                    structural_subscription_targets.add(
-                        bugtask.target.parent_subscription_target)
-
-            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.
-                structural_subscription_targets.add(
-                    bugtask.distroseries)
-
-            if bugtask.milestone is not None:
-                structural_subscription_targets.add(bugtask.milestone)
-
             # If the target's bug supervisor isn't set,
             # we add the owner as a subscriber.
             pillar = bugtask.pillar
@@ -993,22 +967,73 @@
                 if recipients is not None:
                     recipients.addRegistrant(pillar.owner, pillar)
 
-        person_set = getUtility(IPersonSet)
-        target_subscribers = person_set.getSubscribersForTargets(
-            structural_subscription_targets, recipients=recipients,
-            level=level)
-
-        also_notified_subscribers.update(target_subscribers)
+        # Structural subscribers.
+        also_notified_subscribers.update(
+            self.getStructuralSubscribers(
+                recipients=recipients, level=level))
 
         # Direct subscriptions always take precedence over indirect
         # subscriptions.
         direct_subscribers = set(self.getDirectSubscribers())
+
         # Remove security proxy for the sort key, but return
         # the regular proxied object.
         return sorted(
             (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):

=== renamed file 'lib/lp/bugs/tests/test_bug.py' => 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/tests/test_bug.py	2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2010-10-07 15:02:46 +0000
@@ -5,7 +5,11 @@
 
 __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 (
@@ -13,6 +17,7 @@
     person_logged_in,
     TestCaseWithFactory,
     )
+from lp.testing.matchers import StartsWith
 
 
 class TestBug(TestCaseWithFactory):
@@ -34,8 +39,9 @@
     def test_get_subscribers_for_person_indirect_subscription(self):
         bug = self.factory.makeBug()
         person = self.factory.makePerson()
-        team1 = self.factory.makeTeam(members=[person])
-        team2 = self.factory.makeTeam(members=[person])
+        [team1, team2] = (
+            self.factory.makeTeam(members=[person]),
+            self.factory.makeTeam(members=[person]))
         with person_logged_in(person):
             bug.subscribe(team1, person)
         self.assertEqual([team1], list(bug.getSubscribersForPerson(person)))
@@ -125,3 +131,84 @@
             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)))

=== modified file 'lib/lp/registry/doc/structural-subscriptions.txt'
--- lib/lp/registry/doc/structural-subscriptions.txt	2010-10-06 18:53:53 +0000
+++ lib/lp/registry/doc/structural-subscriptions.txt	2010-10-07 15:02:46 +0000
@@ -198,61 +198,6 @@
     name16
 
 
-Retrieving structural subscribers of targets
-============================================
-
-Subscribers of one or more targets are retrieved by
-PersonSet.getSubscribersForTargets.
-
-XXX Abel Deuring 2008-07-11 We must remove the security proxy, because
-PersonSet.getSubscribersForTargets() accesses the private attribute
-firefox._targets_args. This attribute should be renamed. Bug #247525.
-
-    >>> from zope.security.proxy import removeSecurityProxy
-    >>> firefox_no_proxy = removeSecurityProxy(firefox)
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> person_set = getUtility(IPersonSet)
-    >>> firefox_subscribers = person_set.getSubscribersForTargets(
-    ...     [firefox_no_proxy])
-    >>> print_bug_subscribers(firefox_subscribers)
-    name12
-
-If PersonSet.getSubscribersForTargets() is passed a
-BugNotificationLevel in its `level` parameter, only structural
-subscribers with that notification level or higher will be returned.
-The subscription level of ff_sub, the only structural subscription
-to firefox, is NOTHING, hence we get an empty list if we pass any
-larger bug notification level.
-
-    >>> firefox_subscribers = person_set.getSubscribersForTargets(
-    ...     [firefox_no_proxy], level=BugNotificationLevel.LIFECYCLE)
-    >>> print len(firefox_subscribers)
-    0
-
-If the bug notification level of ff_sub is increased, the subscription
-is included in the result of PersonSet.getSubscribersForTarget().
-
-    >>> ff_sub.level = BugNotificationLevel.LIFECYCLE
-    >>> firefox_subscribers = person_set.getSubscribersForTargets(
-    ...     [firefox_no_proxy])
-    >>> print_bug_subscribers(firefox_subscribers)
-    name12
-    >>> ff_sub.level = BugNotificationLevel.METADATA
-    >>> firefox_subscribers = person_set.getSubscribersForTargets(
-    ...     [firefox_no_proxy])
-    >>> print_bug_subscribers(firefox_subscribers)
-    name12
-
-More than one target can be passed to PersonSet.getSubscribersForTargets().
-
-    >>> ubuntu_no_proxy = removeSecurityProxy(ubuntu)
-    >>> ubuntu_or_firefox_subscribers = person_set.getSubscribersForTargets(
-    ...     [firefox_no_proxy, ubuntu_no_proxy])
-    >>> print_bug_subscribers(ubuntu_or_firefox_subscribers)
-    name12
-    name16
-
-
 Target type display
 ===================
 

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2010-09-29 20:05:17 +0000
+++ lib/lp/registry/interfaces/person.py	2010-10-07 15:02:46 +0000
@@ -2006,17 +2006,6 @@
             specified product are returned.
         """
 
-    def getSubscribersForTargets(targets, recipients=None):
-        """Return the set of subscribers for `targets`.
-
-        :param targets: The sequence of targets for which to get the
-                        subscribers.
-        :param recipients: An optional instance of
-                           `BugNotificationRecipients`.
-                           If present, all found subscribers will be
-                           added to it.
-        """
-
     def updatePersonalStandings():
         """Update the personal standings of some people.
 

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2010-09-29 14:38:30 +0000
+++ lib/lp/registry/model/person.py	2010-10-07 15:02:46 +0000
@@ -4060,59 +4060,6 @@
             Person.id in (%s)
             ''' % branch_clause)
 
-    def getSubscribersForTargets(self, targets, recipients=None, level=None):
-        """See `IPersonSet`. """
-        if len(targets) == 0:
-            return set()
-        target_criteria = []
-        for target in targets:
-            # target_args is a mapping from query arguments
-            # to query values.
-            target_args = target._target_args
-            target_criteria_clauses = []
-            for key, value in target_args.items():
-                if value is not None:
-                    target_criteria_clauses.append(
-                        '%s = %s' % (key, quote(value)))
-                else:
-                    target_criteria_clauses.append(
-                        '%s IS NULL' % key)
-            if level is not None:
-                target_criteria_clauses.append('bug_notification_level >= %s'
-                    % quote(level.value))
-
-            target_criteria.append(
-                '(%s)' % ' AND '.join(target_criteria_clauses))
-
-        # Build a UNION query, since using OR slows down the query a lot.
-        subscriptions = StructuralSubscription.select(target_criteria[0])
-        for target_criterion in target_criteria[1:]:
-            subscriptions = subscriptions.union(
-                StructuralSubscription.select(target_criterion))
-
-        # Listify the result, since we want to loop over it multiple times.
-        subscriptions = list(subscriptions)
-
-        # We can't use prejoins in UNION queries, so populate the cache
-        # by getting all the subscribers.
-        subscriber_ids = [
-            subscription.subscriberID for subscription in subscriptions]
-        if len(subscriber_ids) > 0:
-            # Pull in ValidPersonCache records in addition to Person
-            # records to warm up the cache.
-            list(IStore(Person).find(
-                    (Person, ValidPersonCache),
-                    In(Person.id, subscriber_ids),
-                    ValidPersonCache.id == Person.id))
-
-        subscribers = set()
-        for subscription in subscriptions:
-            subscribers.add(subscription.subscriber)
-            if recipients is not None:
-                recipients.addStructuralSubscriber(
-                    subscription.subscriber, subscription.target)
-        return subscribers
-
     def updatePersonalStandings(self):
         """See `IPersonSet`."""
         cur = cursor()

=== modified file 'utilities/format-new-and-modified-imports'
--- utilities/format-new-and-modified-imports	2010-09-28 09:50:49 +0000
+++ utilities/format-new-and-modified-imports	2010-10-07 15:02:46 +0000
@@ -9,5 +9,5 @@
 #
 
 bzr status --short "$@" | \
-    awk '/^.[MN] .*[.]py$/ { print $2 }' | \
+    awk '/^.[MN] .*[.]py$/ { print $NF }' | \
     xargs -r "$(dirname "$0")/format-imports"