Merge lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 14638
Proposed branch: lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email
Merge into: lp:launchpad
Diff against target: 1541 lines (+706/-257)
9 files modified
lib/lp/bugs/configure.zcml (+13/-0)
lib/lp/bugs/doc/bugsubscription.txt (+4/-4)
lib/lp/bugs/interfaces/bug.py (+3/-2)
lib/lp/bugs/interfaces/bugtask.py (+1/-1)
lib/lp/bugs/model/bug.py (+180/-100)
lib/lp/bugs/model/structuralsubscription.py (+84/-41)
lib/lp/bugs/model/tests/test_bug.py (+82/-8)
lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+239/-99)
lib/lp/bugs/tests/test_structuralsubscription.py (+100/-2)
To merge this branch: bzr merge lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+87397@code.launchpad.net

Commit message

Load preferredemail when calculating bug subscribers of any kind via BugSubscriptionInfo.

Description of the change

This branch is long, but is actually quite readable, I think :)

One big problem when updating bugs is sending notifications to
subscribers. It looks like preferred email addresses are being loaded
one by one (though I started this branch long enough ago that the
details have faded).

In getting email addresses preloaded I updated BugSubscriptionInfo,
fixed some issues with it, and gotten get_also_notified_subscribers()
using it.

I think BSI is now a fairly good and comprehensive foundation for any
bug subscription calculation. It may not do everything in the minimum
number of queries possible, but it's does everything in a constant
number of queries, and makes it easy for code that uses it to also do
so.

There are still methods in IBug (and probably elsewhere) that could be
refactored to use BSI more directly, but I'll leave that for another
time.

The changes:

- Updates BugSubscriptionInfo:

  - Adds all_direct_subscriptions, all_direct_subscribers,
    direct_subscribers, muted_subscribers, and structural_subscribers
    properties.

  - Adds support for returning subscription info for only a single
    bugtask as well as all bugtasks of the given bug.

  - Adds forLevel() and forTask() methods.

  - When loading Person records, load preferred email information too.

- Updates get_also_notified_subscribers():

  - Use BugSubscriptionInfo more heavily.

  - Adds tests around performance of this function when passed a
    recipients argument. This was previously poor (potato potato).

- Updates structural subscriptions:

  Split get_structural_subscribers() into two functions -
  get_structural_subscribers() and get_structural_subscriptions() -
  which both back onto query_structural_subscriptions(), a new
  function. This supports the changes to BugSubscriptionInfo and also
  makes the implementation cleaner.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Fwiw, passes in devel:

Tests started at approximately 2012-01-03 16:07:06.335715
Source: bzr+ssh://bazaar.launchpad.net/~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email r14523
Target: bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/ r14623

17326 tests run in 4:21:55.682084, 0 failures, 0 errors

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (22.9 KiB)

Hi Gavin,

This took me a while to review. By and large it looks nice, but I have a few cosmetic issues to rant on. Not all of them are vital, and some will seem more trouble than they're worth — unless they can become habits. It may seem petty of me; but I could give a more effective review without these things in the way.

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2012-01-03 17:52:00 +0000
@@ -99,7 +99,7 @@
> getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates().
>
> >>> linux_source_bug.getAlsoNotifiedSubscribers()
> - [<Person at ...>]
> + (<Person at ...>,)
> >>> linux_source_bug.getSubscribersFromDuplicates()
> ()
>
@@ -109,7 +109,7 @@
> >>> from lp.bugs.model.bug import get_also_notified_subscribers
> >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
> >>> res
> - [<Person at ...>]
> + (<Person at ...>,)

These two original tests were brittle and a bit obscure. They seemed to establish that (1) a list is returned and (2) the list contains one or more Persons. I suspect (1) is of no significance.

Your new tests establish that (1) a tuple is returned and (2) the tuple contains exactly one Person, as implied by the trailing comma. So you test more, but it's still brittle and still a bit obscure.

It's not worth spending too much time on any given instance of these problems, but it's good to have fixes ready when you come across them: listify so that it won't matter what kind of iterable you get, or print just the length, or use a “[foo] = give_me_a_list_or_tuple()” assignment. The big question of course is what the test means to prove and what is incidental. Stay close to the former and away from the latter.

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/bug.py 2012-01-03 17:52:00 +0000

@@ -948,9 +949,10 @@
> BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
> return DecoratedResultSet(results, operator.itemgetter(1))
>
> - def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
> + def getSubscriptionInfo(self, level=None):
> """See `IBug`."""
> - return BugSubscriptionInfo(self, level)
> + return BugSubscriptionInfo(
> + self, BugNotificationLevel.LIFECYCLE if level is None else level)

Found the "if/else" slightly hard to read here. Maybe I'm just not used to the syntax yet, but it made me double-check that nothing else goes on in this line. Please consider putting the if/else on a separate line.

@@ -1002,6 +1004,8 @@
> # the regular proxied object.
 > return sorted(
 > indirect_subscribers,
> + # XXX: GavinPanella 2011-12-12 bug=???: Should probably use
> + # person_sort_key.
> key=lambda x: removeSecurityProxy(x).displayname)

Don't forget to file that bug!

@@ -2389,48 +2392,44 @@

> - # Direct subscriptions always take precedence over indirect
> - # subscriptions.
> - direct_subscriber...

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (28.3 KiB)

> Hi Gavin,
>
> This took me a while to review. By and large it looks nice, but I
> have a few cosmetic issues to rant on. Not all of them are vital,
> and some will seem more trouble than they're worth — unless they can
> become habits. It may seem petty of me; but I could give a more
> effective review without these things in the way.

Thanks for going through this branch!

>
>
> === modified file 'lib/lp/bugs/doc/bugsubscription.txt'
> --- lib/lp/bugs/doc/bugsubscription.txt 2011-12-24 17:49:30 +0000
> +++ lib/lp/bugs/doc/bugsubscription.txt 2012-01-03 17:52:00 +0000
> @@ -99,7 +99,7 @@
> > getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates().
> >
> > >>> linux_source_bug.getAlsoNotifiedSubscribers()
> > - [<Person at ...>]
> > + (<Person at ...>,)
> > >>> linux_source_bug.getSubscribersFromDuplicates()
> > ()
> >
> @@ -109,7 +109,7 @@
> > >>> from lp.bugs.model.bug import get_also_notified_subscribers
> > >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
> > >>> res
> > - [<Person at ...>]
> > + (<Person at ...>,)
>
> These two original tests were brittle and a bit obscure. They
> seemed to establish that (1) a list is returned and (2) the list
> contains one or more Persons. I suspect (1) is of no significance.
>
> Your new tests establish that (1) a tuple is returned and (2) the
> tuple contains exactly one Person, as implied by the trailing comma.
> So you test more, but it's still brittle and still a bit obscure.
>
> It's not worth spending too much time on any given instance of these
> problems, but it's good to have fixes ready when you come across
> them: listify so that it won't matter what kind of iterable you get,
> or print just the length, or use a “[foo] =
> give_me_a_list_or_tuple()” assignment. The big question of course
> is what the test means to prove and what is incidental. Stay close
> to the former and away from the latter.

Good points, well put. I've amended those.

>
>
> === modified file 'lib/lp/bugs/model/bug.py'
> --- lib/lp/bugs/model/bug.py 2011-12-30 06:14:56 +0000
> +++ lib/lp/bugs/model/bug.py 2012-01-03 17:52:00 +0000
>
> @@ -948,9 +949,10 @@
> > BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
> > return DecoratedResultSet(results, operator.itemgetter(1))
> >
> > - def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
> > + def getSubscriptionInfo(self, level=None):
> > """See `IBug`."""
> > - return BugSubscriptionInfo(self, level)
> > + return BugSubscriptionInfo(
> > + self, BugNotificationLevel.LIFECYCLE if level is None else
> level)
>
> Found the "if/else" slightly hard to read here. Maybe I'm just not
> used to the syntax yet, but it made me double-check that nothing
> else goes on in this line. Please consider putting the if/else on a
> separate line.

Done.

>
>
> @@ -1002,6 +1004,8 @@
> > # the regular proxied object.
> > return sorted(
> > indirect_subscribers,
> > + # XXX: GavinPanella 2011-12-12 bug=???: Should probably use
> > + # person_sort_key.
> ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2011-12-30 08:03:42 +0000
+++ lib/lp/bugs/configure.zcml 2012-01-04 18:01:30 +0000
@@ -650,6 +650,7 @@
650 permission="launchpad.View"650 permission="launchpad.View"
651 attributes="651 attributes="
652 bug652 bug
653 bugtask
653 level654 level
654 " />655 " />
655 <!-- Properties -->656 <!-- Properties -->
@@ -659,12 +660,24 @@
659 all_assignees660 all_assignees
660 all_pillar_owners_without_bug_supervisors661 all_pillar_owners_without_bug_supervisors
661 also_notified_subscribers662 also_notified_subscribers
663 direct_subscribers
664 direct_subscribers_at_all_levels
662 direct_subscriptions665 direct_subscriptions
666 direct_subscriptions_at_all_levels
663 duplicate_only_subscriptions667 duplicate_only_subscriptions
664 duplicate_subscriptions668 duplicate_subscriptions
665 indirect_subscribers669 indirect_subscribers
670 muted_subscribers
671 structural_subscribers
666 structural_subscriptions672 structural_subscriptions
667 " />673 " />
674 <!-- Methods -->
675 <require
676 permission="launchpad.View"
677 attributes="
678 forLevel
679 forTask
680 " />
668 </class>681 </class>
669682
670 <!-- PersonSubscriptionInfo -->683 <!-- PersonSubscriptionInfo -->
671684
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2012-01-04 18:01:30 +0000
@@ -98,17 +98,17 @@
98Finer-grained access to indirect subscribers is provided by98Finer-grained access to indirect subscribers is provided by
99getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates().99getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates().
100100
101 >>> linux_source_bug.getAlsoNotifiedSubscribers()101 >>> list(linux_source_bug.getAlsoNotifiedSubscribers())
102 [<Person at ...>]102 [<Person at ...>]
103 >>> linux_source_bug.getSubscribersFromDuplicates()103 >>> list(linux_source_bug.getSubscribersFromDuplicates())
104 ()104 []
105105
106It is also possible to get the list of indirect subscribers for an106It is also possible to get the list of indirect subscribers for an
107individual bug task.107individual bug task.
108108
109 >>> from lp.bugs.model.bug import get_also_notified_subscribers109 >>> from lp.bugs.model.bug import get_also_notified_subscribers
110 >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])110 >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
111 >>> res111 >>> list(res)
112 [<Person at ...>]112 [<Person at ...>]
113113
114These are security proxied.114These are security proxied.
115115
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/interfaces/bug.py 2012-01-04 18:01:30 +0000
@@ -578,10 +578,11 @@
578 If no such `BugSubscription` exists, return None.578 If no such `BugSubscription` exists, return None.
579 """579 """
580580
581 def getSubscriptionInfo(level):581 def getSubscriptionInfo(level=None):
582 """Return a `BugSubscriptionInfo` at the given `level`.582 """Return a `BugSubscriptionInfo` at the given `level`.
583583
584 :param level: A member of `BugNotificationLevel`.584 :param level: A member of `BugNotificationLevel`. Defaults to
585 `BugSubscriptionLevel.LIFECYCLE` if unspecified.
585 """586 """
586587
587 def getBugNotificationRecipients(duplicateof=None, old_bug=None,588 def getBugNotificationRecipients(duplicateof=None, old_bug=None,
588589
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2012-01-04 18:01:30 +0000
@@ -560,7 +560,7 @@
560 title=_('Assigned to'), required=False,560 title=_('Assigned to'), required=False,
561 vocabulary='ValidAssignee',561 vocabulary='ValidAssignee',
562 readonly=True))562 readonly=True))
563 assigneeID = Attribute('The assignee ID (for eager loading)')563 assigneeID = Int(title=_('The assignee ID (for eager loading)'))
564 bugtargetdisplayname = exported(564 bugtargetdisplayname = exported(
565 Text(title=_("The short, descriptive name of the target"),565 Text(title=_("The short, descriptive name of the target"),
566 readonly=True),566 readonly=True),
567567
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/bug.py 2012-01-04 18:01:30 +0000
@@ -158,6 +158,7 @@
158from lp.bugs.model.bugwatch import BugWatch158from lp.bugs.model.bugwatch import BugWatch
159from lp.bugs.model.structuralsubscription import (159from lp.bugs.model.structuralsubscription import (
160 get_structural_subscribers,160 get_structural_subscribers,
161 get_structural_subscriptions,
161 get_structural_subscriptions_for_bug,162 get_structural_subscriptions_for_bug,
162 )163 )
163from lp.code.interfaces.branchcollection import IAllBranches164from lp.code.interfaces.branchcollection import IAllBranches
@@ -948,8 +949,10 @@
948 BugSubscription.bug_id == self.id).order_by(BugSubscription.id)949 BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
949 return DecoratedResultSet(results, operator.itemgetter(1))950 return DecoratedResultSet(results, operator.itemgetter(1))
950951
951 def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):952 def getSubscriptionInfo(self, level=None):
952 """See `IBug`."""953 """See `IBug`."""
954 if level is None:
955 level = BugNotificationLevel.LIFECYCLE
953 return BugSubscriptionInfo(self, level)956 return BugSubscriptionInfo(self, level)
954957
955 def getDirectSubscriptions(self):958 def getDirectSubscriptions(self):
@@ -1002,6 +1005,7 @@
1002 # the regular proxied object.1005 # the regular proxied object.
1003 return sorted(1006 return sorted(
1004 indirect_subscribers,1007 indirect_subscribers,
1008 # XXX: GavinPanella 2011-12-12 bug=911752: Use person_sort_key.
1005 key=lambda x: removeSecurityProxy(x).displayname)1009 key=lambda x: removeSecurityProxy(x).displayname)
10061010
1007 def getSubscriptionsFromDuplicates(self, recipients=None):1011 def getSubscriptionsFromDuplicates(self, recipients=None):
@@ -1030,14 +1034,13 @@
1030 if level is None:1034 if level is None:
1031 level = BugNotificationLevel.LIFECYCLE1035 level = BugNotificationLevel.LIFECYCLE
1032 info = self.getSubscriptionInfo(level)1036 info = self.getSubscriptionInfo(level)
1033
1034 if recipients is not None:1037 if recipients is not None:
1035 # Pre-load duplicate bugs.1038 list(self.duplicates) # Pre-load duplicate bugs.
1036 list(self.duplicates)1039 info.duplicate_only_subscribers # Pre-load subscribers.
1037 for subscription in info.duplicate_only_subscriptions:1040 for subscription in info.duplicate_only_subscriptions:
1038 recipients.addDupeSubscriber(1041 recipients.addDupeSubscriber(
1039 subscription.person, subscription.bug)1042 subscription.person, subscription.bug)
1040 return info.duplicate_only_subscriptions.subscribers.sorted1043 return info.duplicate_only_subscribers.sorted
10411044
1042 def getSubscribersForPerson(self, person):1045 def getSubscribersForPerson(self, person):
1043 """See `IBug."""1046 """See `IBug."""
@@ -2389,48 +2392,44 @@
2389 if IBug.providedBy(bug_or_bugtask):2392 if IBug.providedBy(bug_or_bugtask):
2390 bug = bug_or_bugtask2393 bug = bug_or_bugtask
2391 bugtasks = bug.bugtasks2394 bugtasks = bug.bugtasks
2395 info = bug.getSubscriptionInfo(level)
2392 elif IBugTask.providedBy(bug_or_bugtask):2396 elif IBugTask.providedBy(bug_or_bugtask):
2393 bug = bug_or_bugtask.bug2397 bug = bug_or_bugtask.bug
2394 bugtasks = [bug_or_bugtask]2398 bugtasks = [bug_or_bugtask]
2399 info = bug.getSubscriptionInfo(level).forTask(bug_or_bugtask)
2395 else:2400 else:
2396 raise ValueError('First argument must be bug or bugtask')2401 raise ValueError('First argument must be bug or bugtask')
23972402
2398 if bug.private:2403 if bug.private:
2399 return []2404 return []
24002405
2401 # Direct subscriptions always take precedence over indirect2406 # Subscribers to exclude.
2402 # subscriptions.2407 exclude_subscribers = frozenset().union(
2403 direct_subscribers = set(bug.getDirectSubscribers())2408 info.direct_subscribers_at_all_levels, info.muted_subscribers)
24042409 # Get also-notified subscribers at the given level for the given tasks.
2405 also_notified_subscribers = set()2410 also_notified_subscribers = info.also_notified_subscribers
24062411
2407 for bugtask in bugtasks:2412 if recipients is not None:
2408 if (bugtask.assignee and2413 for bugtask in bugtasks:
2409 bugtask.assignee not in direct_subscribers):2414 assignee = bugtask.assignee
2410 # We have an assignee that is not a direct subscriber.2415 if assignee in also_notified_subscribers:
2411 also_notified_subscribers.add(bugtask.assignee)2416 # We have an assignee that is not a direct subscriber.
2412 if recipients is not None:
2413 recipients.addAssignee(bugtask.assignee)2417 recipients.addAssignee(bugtask.assignee)
24142418 # If the target's bug supervisor isn't set...
2415 # If the target's bug supervisor isn't set...2419 pillar = bugtask.pillar
2416 pillar = bugtask.pillar2420 if pillar.official_malone and pillar.bug_supervisor is None:
2417 if (pillar.bug_supervisor is None and2421 if pillar.owner in also_notified_subscribers:
2418 pillar.official_malone and2422 # ...we add the owner as a subscriber.
2419 pillar.owner not in direct_subscribers):2423 recipients.addRegistrant(pillar.owner, pillar)
2420 # ...we add the owner as a subscriber.
2421 also_notified_subscribers.add(pillar.owner)
2422 if recipients is not None:
2423 recipients.addRegistrant(pillar.owner, pillar)
24242424
2425 # This structural subscribers code omits direct subscribers itself.2425 # This structural subscribers code omits direct subscribers itself.
2426 also_notified_subscribers.update(2426 # TODO: Pass the info object into get_structural_subscribers for
2427 get_structural_subscribers(2427 # efficiency... or do the recipients stuff here.
2428 bug_or_bugtask, recipients, level, direct_subscribers))2428 structural_subscribers = get_structural_subscribers(
2429 bug_or_bugtask, recipients, level, exclude_subscribers)
2430 assert also_notified_subscribers.issuperset(structural_subscribers)
24292431
2430 # Remove security proxy for the sort key, but return2432 return also_notified_subscribers.sorted
2431 # the regular proxied object.
2432 return sorted(also_notified_subscribers,
2433 key=lambda x: removeSecurityProxy(x).displayname)
24342433
24352434
2436def load_people(*where):2435def load_people(*where):
@@ -2443,7 +2442,8 @@
2443 `ValidPersonCache` records are loaded simultaneously.2442 `ValidPersonCache` records are loaded simultaneously.
2444 """2443 """
2445 return PersonSet()._getPrecachedPersons(2444 return PersonSet()._getPrecachedPersons(
2446 origin=[Person], conditions=where, need_validity=True)2445 origin=[Person], conditions=where, need_validity=True,
2446 need_preferred_email=True)
24472447
24482448
2449class BugSubscriberSet(frozenset):2449class BugSubscriberSet(frozenset):
@@ -2573,13 +2573,57 @@
25732573
2574 def __init__(self, bug, level):2574 def __init__(self, bug, level):
2575 self.bug = bug2575 self.bug = bug
2576 self.bugtask = None # Implies all.
2576 assert level is not None2577 assert level is not None
2577 self.level = level2578 self.level = level
2579 # This cache holds related `BugSubscriptionInfo` instances relating to
2580 # the same bug but with different levels and/or choice of bugtask.
2581 self.cache = {self.cache_key: self}
2582 # This is often used in event handlers, many of which block implicit
2583 # flushes. However, the data needs to be in the database for the
2584 # queries herein to give correct answers.
2585 Store.of(bug).flush()
2586
2587 @property
2588 def cache_key(self):
2589 """A (bug ID, bugtask ID, level) tuple for use as a hash key.
2590
2591 This helps `forTask()` and `forLevel()` to be more efficient,
2592 returning previously populated instances to avoid running the same
2593 queries against the database again and again.
2594 """
2595 bugtask_id = None if self.bugtask is None else self.bugtask.id
2596 return self.bug.id, bugtask_id, self.level
2597
2598 def forTask(self, bugtask):
2599 """Create a new `BugSubscriptionInfo` limited to `bugtask`.
2600
2601 The given task must refer to this object's bug. If `None` is passed a
2602 new `BugSubscriptionInfo` instance is returned with no limit.
2603 """
2604 info = self.__class__(self.bug, self.level)
2605 info.bugtask, info.cache = bugtask, self.cache
2606 return self.cache.setdefault(info.cache_key, info)
2607
2608 def forLevel(self, level):
2609 """Create a new `BugSubscriptionInfo` limited to `level`."""
2610 info = self.__class__(self.bug, level)
2611 info.bugtask, info.cache = self.bugtask, self.cache
2612 return self.cache.setdefault(info.cache_key, info)
2613
2614 @cachedproperty
2615 @freeze(BugSubscriberSet)
2616 def muted_subscribers(self):
2617 muted_people = Select(BugMute.person_id, BugMute.bug == self.bug)
2618 return load_people(Person.id.is_in(muted_people))
25782619
2579 @cachedproperty2620 @cachedproperty
2580 @freeze(BugSubscriptionSet)2621 @freeze(BugSubscriptionSet)
2581 def old_direct_subscriptions(self):2622 def direct_subscriptions(self):
2582 """The bug's direct subscriptions."""2623 """The bug's direct subscriptions.
2624
2625 Excludes muted subscriptions.
2626 """
2583 return IStore(BugSubscription).find(2627 return IStore(BugSubscription).find(
2584 BugSubscription,2628 BugSubscription,
2585 BugSubscription.bug_notification_level >= self.level,2629 BugSubscription.bug_notification_level >= self.level,
@@ -2587,66 +2631,64 @@
2587 Not(In(BugSubscription.person_id,2631 Not(In(BugSubscription.person_id,
2588 Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))2632 Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))
25892633
2590 @cachedproperty2634 @property
2591 def direct_subscriptions_and_subscribers(self):
2592 """The bug's direct subscriptions."""
2593 res = IStore(BugSubscription).find(
2594 (BugSubscription, Person),
2595 BugSubscription.bug_notification_level >= self.level,
2596 BugSubscription.bug == self.bug,
2597 BugSubscription.person_id == Person.id,
2598 Not(In(BugSubscription.person_id,
2599 Select(BugMute.person_id,
2600 BugMute.bug_id == self.bug.id))))
2601 # Here we could test for res.count() but that will execute another
2602 # query. This structure avoids the extra query.
2603 return zip(*res) or ((), ())
2604
2605 @cachedproperty
2606 @freeze(BugSubscriptionSet)
2607 def direct_subscriptions(self):
2608 return self.direct_subscriptions_and_subscribers[0]
2609
2610 @cachedproperty
2611 @freeze(BugSubscriberSet)
2612 def direct_subscribers(self):2635 def direct_subscribers(self):
2613 return self.direct_subscriptions_and_subscribers[1]2636 """The bug's direct subscriptions.
2637
2638 Excludes muted subscribers.
2639 """
2640 return self.direct_subscriptions.subscribers
2641
2642 @property
2643 def direct_subscriptions_at_all_levels(self):
2644 """The bug's direct subscriptions at all levels.
2645
2646 Excludes muted subscriptions.
2647 """
2648 return self.forLevel(
2649 BugNotificationLevel.LIFECYCLE).direct_subscriptions
2650
2651 @property
2652 def direct_subscribers_at_all_levels(self):
2653 """The bug's direct subscribers at all levels.
2654
2655 Excludes muted subscribers.
2656 """
2657 return self.direct_subscriptions_at_all_levels.subscribers
26142658
2615 @cachedproperty2659 @cachedproperty
2616 def duplicate_subscriptions_and_subscribers(self):2660 @freeze(BugSubscriptionSet)
2617 """Subscriptions to duplicates of the bug."""2661 def duplicate_subscriptions(self):
2662 """Subscriptions to duplicates of the bug.
2663
2664 Excludes muted subscriptions.
2665 """
2618 if self.bug.private:2666 if self.bug.private:
2619 return ((), ())2667 return ()
2620 else:2668 else:
2621 res = IStore(BugSubscription).find(2669 return IStore(BugSubscription).find(
2622 (BugSubscription, Person),2670 BugSubscription,
2623 BugSubscription.bug_notification_level >= self.level,2671 BugSubscription.bug_notification_level >= self.level,
2624 BugSubscription.bug_id == Bug.id,2672 BugSubscription.bug_id == Bug.id,
2625 BugSubscription.person_id == Person.id,
2626 Bug.duplicateof == self.bug,2673 Bug.duplicateof == self.bug,
2627 Not(In(BugSubscription.person_id,2674 Not(In(BugSubscription.person_id,
2628 Select(BugMute.person_id, BugMute.bug_id == Bug.id))))2675 Select(BugMute.person_id, BugMute.bug_id == Bug.id))))
2629 # Here we could test for res.count() but that will execute another2676
2630 # query. This structure avoids the extra query.2677 @property
2631 return zip(*res) or ((), ())
2632
2633 @cachedproperty
2634 @freeze(BugSubscriptionSet)
2635 def duplicate_subscriptions(self):
2636 return self.duplicate_subscriptions_and_subscribers[0]
2637
2638 @cachedproperty
2639 @freeze(BugSubscriberSet)
2640 def duplicate_subscribers(self):2678 def duplicate_subscribers(self):
2641 return self.duplicate_subscriptions_and_subscribers[1]2679 """Subscribers to duplicates of the bug.
2680
2681 Excludes muted subscribers.
2682 """
2683 return self.duplicate_subscriptions.subscribers
26422684
2643 @cachedproperty2685 @cachedproperty
2644 @freeze(BugSubscriptionSet)2686 @freeze(BugSubscriptionSet)
2645 def duplicate_only_subscriptions(self):2687 def duplicate_only_subscriptions(self):
2646 """Subscriptions to duplicates of the bug.2688 """Subscriptions to duplicates of the bug only.
26472689
2648 Excludes subscriptions for people who have a direct subscription or2690 Excludes muted subscriptions, subscriptions for people who have a
2649 are also notified for another reason.2691 direct subscription, or who are also notified for another reason.
2650 """2692 """
2651 self.duplicate_subscribers # Pre-load subscribers.2693 self.duplicate_subscribers # Pre-load subscribers.
2652 higher_precedence = (2694 higher_precedence = (
@@ -2656,47 +2698,85 @@
2656 subscription for subscription in self.duplicate_subscriptions2698 subscription for subscription in self.duplicate_subscriptions
2657 if subscription.person not in higher_precedence)2699 if subscription.person not in higher_precedence)
26582700
2701 @property
2702 def duplicate_only_subscribers(self):
2703 """Subscribers to duplicates of the bug only.
2704
2705 Excludes muted subscribers, subscribers who have a direct
2706 subscription, or who are also notified for another reason.
2707 """
2708 return self.duplicate_only_subscriptions.subscribers
2709
2659 @cachedproperty2710 @cachedproperty
2660 @freeze(StructuralSubscriptionSet)2711 @freeze(StructuralSubscriptionSet)
2661 def structural_subscriptions(self):2712 def structural_subscriptions(self):
2662 """Structural subscriptions to the bug's targets."""2713 """Structural subscriptions to the bug's targets.
2663 return list(get_structural_subscriptions_for_bug(self.bug))2714
2715 Excludes direct subscriptions.
2716 """
2717 subject = self.bug if self.bugtask is None else self.bugtask
2718 return get_structural_subscriptions(subject, self.level)
2719
2720 @property
2721 def structural_subscribers(self):
2722 """Structural subscribers to the bug's targets.
2723
2724 Excludes direct subscribers.
2725 """
2726 return self.structural_subscriptions.subscribers
26642727
2665 @cachedproperty2728 @cachedproperty
2666 @freeze(BugSubscriberSet)2729 @freeze(BugSubscriberSet)
2667 def all_assignees(self):2730 def all_assignees(self):
2668 """Assignees of the bug's tasks."""2731 """Assignees of the bug's tasks.
2669 assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug)2732
2670 return load_people(Person.id.is_in(assignees))2733 *Does not* exclude muted subscribers.
2734 """
2735 if self.bugtask is None:
2736 assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug)
2737 return load_people(Person.id.is_in(assignees))
2738 else:
2739 return load_people(Person.id == self.bugtask.assigneeID)
26712740
2672 @cachedproperty2741 @cachedproperty
2673 @freeze(BugSubscriberSet)2742 @freeze(BugSubscriberSet)
2674 def all_pillar_owners_without_bug_supervisors(self):2743 def all_pillar_owners_without_bug_supervisors(self):
2675 """Owners of pillars for which no Bug supervisor is configured."""2744 """Owners of pillars for which there is no bug supervisor.
2676 for bugtask in self.bug.bugtasks:2745
2746 The pillars must also use Launchpad for bug tracking.
2747
2748 *Does not* exclude muted subscribers.
2749 """
2750 if self.bugtask is None:
2751 bugtasks = self.bug.bugtasks
2752 else:
2753 bugtasks = [self.bugtask]
2754 for bugtask in bugtasks:
2677 pillar = bugtask.pillar2755 pillar = bugtask.pillar
2678 if pillar.bug_supervisor is None:2756 if pillar.official_malone:
2679 yield pillar.owner2757 if pillar.bug_supervisor is None:
2758 yield pillar.owner
26802759
2681 @cachedproperty2760 @cachedproperty
2682 def also_notified_subscribers(self):2761 def also_notified_subscribers(self):
2683 """All subscribers except direct and dupe subscribers."""2762 """All subscribers except direct, dupe, and muted subscribers."""
2684 if self.bug.private:2763 if self.bug.private:
2685 return BugSubscriberSet()2764 return BugSubscriberSet()
2686 else:2765 else:
2687 muted = IStore(BugMute).find(2766 subscribers = BugSubscriberSet().union(
2688 Person,2767 self.structural_subscribers,
2689 BugMute.person_id == Person.id,
2690 BugMute.bug == self.bug)
2691 return BugSubscriberSet().union(
2692 self.structural_subscriptions.subscribers,
2693 self.all_pillar_owners_without_bug_supervisors,2768 self.all_pillar_owners_without_bug_supervisors,
2694 self.all_assignees).difference(2769 self.all_assignees)
2695 self.direct_subscribers).difference(muted)2770 return subscribers.difference(
2771 self.direct_subscribers_at_all_levels,
2772 self.muted_subscribers)
26962773
2697 @cachedproperty2774 @cachedproperty
2698 def indirect_subscribers(self):2775 def indirect_subscribers(self):
2699 """All subscribers except direct subscribers."""2776 """All subscribers except direct subscribers.
2777
2778 Excludes muted subscribers.
2779 """
2700 return self.also_notified_subscribers.union(2780 return self.also_notified_subscribers.union(
2701 self.duplicate_subscribers)2781 self.duplicate_subscribers)
27022782
27032783
=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
--- lib/lp/bugs/model/structuralsubscription.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/structuralsubscription.py 2012-01-04 18:01:30 +0000
@@ -3,10 +3,11 @@
33
4__metaclass__ = type4__metaclass__ = type
5__all__ = [5__all__ = [
6 'get_structural_subscribers',
7 'get_structural_subscription_targets',
8 'get_structural_subscriptions',
6 'get_structural_subscriptions_for_bug',9 'get_structural_subscriptions_for_bug',
7 'get_structural_subscriptions_for_target',10 'get_structural_subscriptions_for_target',
8 'get_structural_subscribers',
9 'get_structural_subscription_targets',
10 'StructuralSubscription',11 'StructuralSubscription',
11 'StructuralSubscriptionTargetMixin',12 'StructuralSubscriptionTargetMixin',
12 ]13 ]
@@ -478,14 +479,14 @@
478 def getSubscriptions(self, subscriber=None):479 def getSubscriptions(self, subscriber=None):
479 """See `IStructuralSubscriptionTarget`."""480 """See `IStructuralSubscriptionTarget`."""
480 from lp.registry.model.person import Person481 from lp.registry.model.person import Person
481 clauses = [StructuralSubscription.subscriberID==Person.id]482 clauses = [StructuralSubscription.subscriberID == Person.id]
482 for key, value in self._target_args.iteritems():483 for key, value in self._target_args.iteritems():
483 clauses.append(484 clauses.append(
484 getattr(StructuralSubscription, key)==value)485 getattr(StructuralSubscription, key) == value)
485486
486 if subscriber is not None:487 if subscriber is not None:
487 clauses.append(488 clauses.append(
488 StructuralSubscription.subscriberID==subscriber.id)489 StructuralSubscription.subscriberID == subscriber.id)
489490
490 store = Store.of(self.__helper.pillar)491 store = Store.of(self.__helper.pillar)
491 return store.find(492 return store.find(
@@ -588,54 +589,96 @@
588 *conditions)589 *conditions)
589590
590591
591def get_structural_subscribers(592def query_structural_subscriptions(
592 bug_or_bugtask, recipients, level, direct_subscribers=None):593 what, bug, bugtasks, level, exclude=None):
593 """Return subscribers for bug or bugtask at level.594 """Query into structural subscriptions for a given bug.
594595
595 :param bug: a bug.596 :param what: The fields to fetch. Choose from `Person`,
596 :param recipients: a BugNotificationRecipients object or None.597 `StructuralSubscription`, `BugSubscriptionFilter`, or a combo.
597 Populates if given.598 :param bug: An `IBug`
598 :param level: a level from lp.bugs.enum.BugNotificationLevel.599 :param bugtasks: An iterable of `IBugTask`.
599 :param direct_subscribers: a collection of Person objects who are600 :param level: A level from `BugNotificationLevel`. Filters below this
600 directly subscribed to the bug.601 level will be excluded.
601602 :param exclude: `Person`s to exclude (e.g. direct subscribers).
602 Excludes structural subscriptions for people who are directly subscribed603 """
603 to the bug."""604 from lp.registry.model.person import Person # Circular.
604 if IBug.providedBy(bug_or_bugtask):
605 bug = bug_or_bugtask
606 bugtasks = bug.bugtasks
607 elif IBugTask.providedBy(bug_or_bugtask):
608 bug = bug_or_bugtask.bug
609 bugtasks = [bug_or_bugtask]
610 else:
611 raise ValueError('First argument must be bug or bugtask')
612 filter_id_query = (605 filter_id_query = (
613 _get_structural_subscription_filter_id_query(606 _get_structural_subscription_filter_id_query(
614 bug, bugtasks, level, direct_subscribers))607 bug, bugtasks, level, exclude))
615 if not filter_id_query:608 if not filter_id_query:
616 return EmptyResultSet()609 return EmptyResultSet()
617 # This is here because of a circular import.
618 from lp.registry.model.person import Person
619 source = IStore(StructuralSubscription).using(610 source = IStore(StructuralSubscription).using(
620 StructuralSubscription,611 StructuralSubscription,
621 Join(BugSubscriptionFilter,612 Join(BugSubscriptionFilter,
622 BugSubscriptionFilter.structural_subscription_id ==613 BugSubscriptionFilter.structural_subscription_id ==
623 StructuralSubscription.id),614 StructuralSubscription.id),
624 Join(Person,615 Join(Person,
625 Person.id == StructuralSubscription.subscriberID),616 Person.id == StructuralSubscription.subscriberID))
626 )617 conditions = In(
618 BugSubscriptionFilter.id, filter_id_query)
619 return source.find(what, conditions)
620
621
622def get_bug_and_bugtasks(bug_or_bugtask):
623 """Return a bug and a list of bugtasks given a bug or a bugtask.
624
625 :param bug_or_bugtask: An `IBug` or `IBugTask`.
626 :raises ValueError: If `bug_or_bugtask` does not provide `IBug` or
627 `IBugTask`.
628 """
629 if IBug.providedBy(bug_or_bugtask):
630 return bug_or_bugtask, bug_or_bugtask.bugtasks
631 elif IBugTask.providedBy(bug_or_bugtask):
632 return bug_or_bugtask.bug, [bug_or_bugtask]
633 else:
634 raise ValueError(
635 "Expected bug or bugtask, got %r" % (bug_or_bugtask,))
636
637
638def get_structural_subscriptions(bug_or_bugtask, level, exclude=None):
639 """Return subscriptions for bug or bugtask at level.
640
641 :param bug_or_bugtask: An `IBug` or `IBugTask`.
642 :param level: A level from `BugNotificationLevel`. Filters below this
643 level will be excluded.
644 :param exclude: `Person`s to exclude (e.g. direct subscribers).
645 """
646 from lp.registry.model.person import Person # Circular.
647 bug, bugtasks = get_bug_and_bugtasks(bug_or_bugtask)
648 subscriptions = query_structural_subscriptions(
649 StructuralSubscription, bug, bugtasks, level, exclude)
650 # Return only the first subscription and filter per subscriber.
651 subscriptions.config(distinct=(Person.id,))
652 subscriptions.order_by(
653 Person.id, StructuralSubscription.id,
654 BugSubscriptionFilter.id)
655 return subscriptions
656
657
658def get_structural_subscribers(
659 bug_or_bugtask, recipients, level, exclude=None):
660 """Return subscribers for bug or bugtask at level.
661
662 :param bug_or_bugtask: An `IBug` or `IBugTask`.
663 :param recipients: A `BugNotificationRecipients` object or
664 `None`, which will be populated if provided.
665 :param level: A level from `BugNotificationLevel`. Filters below this
666 level will be excluded.
667 :param exclude: `Person`s to exclude (e.g. direct subscribers).
668 """
669 from lp.registry.model.person import Person # Circular.
670 bug, bugtasks = get_bug_and_bugtasks(bug_or_bugtask)
627 if recipients is None:671 if recipients is None:
628 return source.find(672 subscribers = query_structural_subscriptions(
629 Person,673 Person, bug, bugtasks, level, exclude)
630 In(BugSubscriptionFilter.id,674 subscribers.config(distinct=True)
631 filter_id_query)).config(distinct=True).order_by()675 return subscribers.order_by()
632 else:676 else:
677 results = query_structural_subscriptions(
678 (Person, StructuralSubscription, BugSubscriptionFilter),
679 bug, bugtasks, level, exclude)
633 subscribers = []680 subscribers = []
634 query_results = source.find(681 for person, subscription, filter in results:
635 (Person, StructuralSubscription, BugSubscriptionFilter),
636 In(BugSubscriptionFilter.id, filter_id_query))
637 for person, subscription, filter in query_results:
638 # Set up results.
639 if person not in recipients:682 if person not in recipients:
640 subscribers.append(person)683 subscribers.append(person)
641 recipients.addStructuralSubscriber(684 recipients.addStructuralSubscriber(
@@ -839,7 +882,7 @@
839 group_by=(BugSubscriptionFilter.id,),882 group_by=(BugSubscriptionFilter.id,),
840 having=Count(883 having=Count(
841 SQL('CASE WHEN BugSubscriptionFilterTag.include '884 SQL('CASE WHEN BugSubscriptionFilterTag.include '
842 'THEN BugSubscriptionFilterTag.tag END'))==0)885 'THEN BugSubscriptionFilterTag.tag END')) == 0)
843 else:886 else:
844 # The bug has some tags. This will require a bit of fancy887 # The bug has some tags. This will require a bit of fancy
845 # footwork. First, though, we will simply want to leave out888 # footwork. First, though, we will simply want to leave out
846889
=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/model/tests/test_bug.py 2012-01-04 18:01:30 +0000
@@ -22,6 +22,7 @@
22from lp.bugs.errors import BugCannotBePrivate22from lp.bugs.errors import BugCannotBePrivate
23from lp.bugs.interfaces.bugnotification import IBugNotificationSet23from lp.bugs.interfaces.bugnotification import IBugNotificationSet
24from lp.bugs.interfaces.bugtask import BugTaskStatus24from lp.bugs.interfaces.bugtask import BugTaskStatus
25from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
25from lp.bugs.model.bug import (26from lp.bugs.model.bug import (
26 BugNotification,27 BugNotification,
27 BugSubscriptionInfo,28 BugSubscriptionInfo,
@@ -35,6 +36,7 @@
35 feature_flags,36 feature_flags,
36 login_person,37 login_person,
37 person_logged_in,38 person_logged_in,
39 record_two_runs,
38 set_feature_flag,40 set_feature_flag,
39 StormStatementRecorder,41 StormStatementRecorder,
40 TestCaseWithFactory,42 TestCaseWithFactory,
@@ -166,7 +168,7 @@
166 with StormStatementRecorder() as recorder:168 with StormStatementRecorder() as recorder:
167 subscribers = list(bug.getDirectSubscribers())169 subscribers = list(bug.getDirectSubscribers())
168 self.assertThat(len(subscribers), Equals(10 + 1))170 self.assertThat(len(subscribers), Equals(10 + 1))
169 self.assertThat(recorder, HasQueryCount(Equals(1)))171 self.assertThat(recorder, HasQueryCount(Equals(2)))
170172
171 def test_mark_as_duplicate_query_count(self):173 def test_mark_as_duplicate_query_count(self):
172 bug = self.factory.makeBug()174 bug = self.factory.makeBug()
@@ -476,6 +478,78 @@
476 self.assertContentEqual(public_branches, linked_branches)478 self.assertContentEqual(public_branches, linked_branches)
477 self.assertNotIn(private_branch, linked_branches)479 self.assertNotIn(private_branch, linked_branches)
478480
481 def test_getDirectSubscribers_with_recipients_query_count(self):
482 # getDirectSubscribers() uses a constant number of queries when given
483 # a recipients argument regardless of the number of subscribers.
484 bug = self.factory.makeBug()
485
486 def create_subscriber():
487 subscriber = self.factory.makePerson()
488 with person_logged_in(subscriber):
489 bug.subscribe(subscriber, subscriber)
490
491 def get_subscribers():
492 recipients = BugNotificationRecipients()
493 subs = bug.getDirectSubscribers(recipients=recipients)
494 list(subs) # Ensure they're pulled.
495
496 recorder1, recorder2 = record_two_runs(
497 get_subscribers, create_subscriber, 3)
498 self.assertThat(
499 recorder2, HasQueryCount(Equals(recorder1.count)))
500
501 def test_getSubscribersFromDuplicates_with_recipients_query_count(self):
502 # getSubscribersFromDuplicates() uses a constant number of queries
503 # when given a recipients argument regardless of the number of
504 # subscribers.
505 bug = self.factory.makeBug()
506 duplicate_bug = self.factory.makeBug()
507 with person_logged_in(duplicate_bug.owner):
508 duplicate_bug.markAsDuplicate(bug)
509
510 def create_subscriber():
511 subscriber = self.factory.makePerson()
512 with person_logged_in(subscriber):
513 duplicate_bug.subscribe(subscriber, subscriber)
514
515 def get_subscribers():
516 recipients = BugNotificationRecipients()
517 subs = bug.getSubscribersFromDuplicates(recipients=recipients)
518 list(subs) # Ensure they're pulled.
519
520 recorder1, recorder2 = record_two_runs(
521 get_subscribers, create_subscriber, 3)
522 self.assertThat(
523 recorder2, HasQueryCount(Equals(recorder1.count)))
524
525 def test_getAlsoNotifiedSubscribers_with_recipients_query_count(self):
526 # getAlsoNotifiedSubscribers() uses a constant number of queries when
527 # given a recipients argument regardless of the number of subscribers.
528 bug = self.factory.makeBug()
529
530 def create_stuff():
531 # Create a new bugtask, set its assignee, set its pillar's
532 # official_malone=True, and subscribe someone to its target.
533 bugtask = self.factory.makeBugTask(bug=bug)
534 with person_logged_in(bugtask.owner):
535 bugtask.transitionToAssignee(bugtask.owner)
536 with person_logged_in(bugtask.pillar.owner):
537 bugtask.pillar.official_malone = True
538 subscriber = self.factory.makePerson()
539 with person_logged_in(subscriber):
540 bugtask.target.addSubscription(
541 subscriber, subscriber)
542
543 def get_subscribers():
544 recipients = BugNotificationRecipients()
545 subs = bug.getAlsoNotifiedSubscribers(recipients=recipients)
546 list(subs) # Ensure they're pulled.
547
548 recorder1, recorder2 = record_two_runs(
549 get_subscribers, create_stuff, 3)
550 self.assertThat(
551 recorder2, HasQueryCount(Equals(recorder1.count)))
552
479553
480class TestBugPrivateAndSecurityRelatedUpdatesMixin:554class TestBugPrivateAndSecurityRelatedUpdatesMixin:
481555
@@ -553,7 +627,7 @@
553 # If the bug is for a private project, then other direct subscribers627 # If the bug is for a private project, then other direct subscribers
554 # should be unsubscribed.628 # should be unsubscribed.
555629
556 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (630 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
557 self.createBugTasksAndSubscribers())631 self.createBugTasksAndSubscribers())
558 initial_subscribers = set((632 initial_subscribers = set((
559 self.factory.makePerson(), bugtask_a.owner, bug_owner,633 self.factory.makePerson(), bugtask_a.owner, bug_owner,
@@ -586,7 +660,7 @@
586 # If the bug is for a private project, then other direct subscribers660 # If the bug is for a private project, then other direct subscribers
587 # should be unsubscribed.661 # should be unsubscribed.
588662
589 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (663 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
590 self.createBugTasksAndSubscribers(private_security_related=True))664 self.createBugTasksAndSubscribers(private_security_related=True))
591 initial_subscribers = set((665 initial_subscribers = set((
592 self.factory.makePerson(), bug_owner,666 self.factory.makePerson(), bug_owner,
@@ -617,10 +691,10 @@
617 # If the bug is for a private project, then other direct subscribers691 # If the bug is for a private project, then other direct subscribers
618 # should be unsubscribed.692 # should be unsubscribed.
619693
620 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (694 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
621 self.createBugTasksAndSubscribers(private_security_related=True))695 self.createBugTasksAndSubscribers(private_security_related=True))
622 initial_subscribers = set((696 initial_subscribers = set((
623 self.factory.makePerson(), bug_owner,697 self.factory.makePerson(), bug_owner,
624 bugtask_a.pillar.security_contact, bugtask_a.pillar.driver,698 bugtask_a.pillar.security_contact, bugtask_a.pillar.driver,
625 bugtask_a.pillar.bug_supervisor))699 bugtask_a.pillar.bug_supervisor))
626700
@@ -644,7 +718,7 @@
644 # When a bug is marked as private=false and security_related=false,718 # When a bug is marked as private=false and security_related=false,
645 # any existing subscriptions are left alone.719 # any existing subscriptions are left alone.
646720
647 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (721 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
648 self.createBugTasksAndSubscribers(private_security_related=True))722 self.createBugTasksAndSubscribers(private_security_related=True))
649 initial_subscribers = set((723 initial_subscribers = set((
650 self.factory.makePerson(), bug_owner,724 self.factory.makePerson(), bug_owner,
@@ -751,7 +825,7 @@
751 # The bug supervisors are unsubscribed if a bug is made public and an825 # The bug supervisors are unsubscribed if a bug is made public and an
752 # email is sent telling them they have been unsubscribed.826 # email is sent telling them they have been unsubscribed.
753827
754 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (828 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
755 self.createBugTasksAndSubscribers(private_security_related=True))829 self.createBugTasksAndSubscribers(private_security_related=True))
756830
757 with person_logged_in(bug_owner):831 with person_logged_in(bug_owner):
@@ -796,7 +870,7 @@
796 # set to false and an email is sent telling them they have been870 # set to false and an email is sent telling them they have been
797 # unsubscribed.871 # unsubscribed.
798872
799 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (873 (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = (
800 self.createBugTasksAndSubscribers(private_security_related=True))874 self.createBugTasksAndSubscribers(private_security_related=True))
801875
802 with person_logged_in(bug_owner):876 with person_logged_in(bug_owner):
803877
=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py'
--- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-01-04 18:01:30 +0000
@@ -46,7 +46,7 @@
46 ]46 ]
47 observed = load_people(47 observed = load_people(
48 Person.id.is_in(person.id for person in expected))48 Person.id.is_in(person.id for person in expected))
49 self.assertEqual(set(expected), set(observed))49 self.assertContentEqual(expected, observed)
5050
5151
52class TestSubscriptionRelatedSets(TestCaseWithFactory):52class TestSubscriptionRelatedSets(TestCaseWithFactory):
@@ -128,9 +128,8 @@
128 with person_logged_in(self.bug.owner):128 with person_logged_in(self.bug.owner):
129 self.bug.unsubscribe(self.bug.owner, self.bug.owner)129 self.bug.unsubscribe(self.bug.owner, self.bug.owner)
130130
131 def getInfo(self):131 def getInfo(self, level=BugNotificationLevel.LIFECYCLE):
132 return BugSubscriptionInfo(132 return BugSubscriptionInfo(self.bug, level)
133 self.bug, BugNotificationLevel.LIFECYCLE)
134133
135 def _create_direct_subscriptions(self):134 def _create_direct_subscriptions(self):
136 subscribers = (135 subscribers = (
@@ -142,22 +141,93 @@
142 for subscriber in subscribers)141 for subscriber in subscribers)
143 return subscribers, subscriptions142 return subscribers, subscriptions
144143
144 def test_forTask(self):
145 # `forTask` returns a new `BugSubscriptionInfo` narrowed to the given
146 # bugtask.
147 info = self.getInfo()
148 self.assertIs(None, info.bugtask)
149 # If called with the current bugtask the same `BugSubscriptionInfo`
150 # instance is returned.
151 self.assertIs(info, info.forTask(info.bugtask))
152 # If called with a different bugtask a new `BugSubscriptionInfo` is
153 # created.
154 bugtask = self.bug.default_bugtask
155 info_for_task = info.forTask(bugtask)
156 self.assertIs(bugtask, info_for_task.bugtask)
157 self.assertIsNot(info, info_for_task)
158 # The instances share a cache of `BugSubscriptionInfo` instances.
159 expected_cache = {
160 info.cache_key: info,
161 info_for_task.cache_key: info_for_task,
162 }
163 self.assertEqual(expected_cache, info.cache)
164 self.assertIs(info.cache, info_for_task.cache)
165 # Calling `forTask` again looks in the cache first.
166 self.assertIs(info, info_for_task.forTask(info.bugtask))
167 self.assertIs(info_for_task, info.forTask(info_for_task.bugtask))
168 # The level is the same.
169 self.assertEqual(info.level, info_for_task.level)
170
171 def test_forLevel(self):
172 # `forLevel` returns a new `BugSubscriptionInfo` narrowed to the given
173 # subscription level.
174 info = self.getInfo(BugNotificationLevel.LIFECYCLE)
175 # If called with the current level the same `BugSubscriptionInfo`
176 # instance is returned.
177 self.assertIs(info, info.forLevel(info.level))
178 # If called with a different level a new `BugSubscriptionInfo` is
179 # created.
180 level = BugNotificationLevel.METADATA
181 info_for_level = info.forLevel(level)
182 self.assertEqual(level, info_for_level.level)
183 self.assertIsNot(info, info_for_level)
184 # The instances share a cache of `BugSubscriptionInfo` instances.
185 expected_cache = {
186 info.cache_key: info,
187 info_for_level.cache_key: info_for_level,
188 }
189 self.assertEqual(expected_cache, info.cache)
190 self.assertIs(info.cache, info_for_level.cache)
191 # Calling `forLevel` again looks in the cache first.
192 self.assertIs(info, info_for_level.forLevel(info.level))
193 self.assertIs(info_for_level, info.forLevel(info_for_level.level))
194 # The bugtask is the same.
195 self.assertIs(info.bugtask, info_for_level.bugtask)
196
197 def test_muted(self):
198 # The set of muted subscribers for the bug.
199 subscribers, subscriptions = self._create_direct_subscriptions()
200 sub1, sub2 = subscribers
201 with person_logged_in(sub1):
202 self.bug.mute(sub1, sub1)
203 self.assertContentEqual([sub1], self.getInfo().muted_subscribers)
204
145 def test_direct(self):205 def test_direct(self):
146 # The set of direct subscribers.206 # The set of direct subscribers.
147 subscribers, subscriptions = self._create_direct_subscriptions()207 subscribers, subscriptions = self._create_direct_subscriptions()
148 found_subscriptions = self.getInfo().direct_subscriptions208 found_subscriptions = self.getInfo().direct_subscriptions
149 self.assertEqual(set(subscriptions), found_subscriptions)209 self.assertContentEqual(subscriptions, found_subscriptions)
150 self.assertEqual(subscriptions, found_subscriptions.sorted)210 self.assertContentEqual(subscribers, found_subscriptions.subscribers)
151 self.assertEqual(set(subscribers), found_subscriptions.subscribers)
152 self.assertEqual(subscribers, found_subscriptions.subscribers.sorted)
153211
154 def test_muted_direct(self):212 def test_direct_muted(self):
155 # If a direct is muted, it is not listed.213 # If a direct is muted, it is not listed.
156 subscribers, subscriptions = self._create_direct_subscriptions()214 subscribers, subscriptions = self._create_direct_subscriptions()
157 with person_logged_in(subscribers[0]):215 with person_logged_in(subscribers[0]):
158 self.bug.mute(subscribers[0], subscribers[0])216 self.bug.mute(subscribers[0], subscribers[0])
159 found_subscriptions = self.getInfo().direct_subscriptions217 found_subscriptions = self.getInfo().direct_subscriptions
160 self.assertEqual(set([subscriptions[1]]), found_subscriptions)218 self.assertContentEqual([subscriptions[1]], found_subscriptions)
219
220 def test_all_direct(self):
221 # The set of all direct subscribers, regardless of level.
222 subscribers, subscriptions = self._create_direct_subscriptions()
223 # Change the first subscription to be for comments only.
224 sub1, sub2 = subscriptions
225 with person_logged_in(sub1.person):
226 sub1.bug_notification_level = BugNotificationLevel.LIFECYCLE
227 info = self.getInfo(BugNotificationLevel.COMMENTS)
228 self.assertContentEqual([sub2], info.direct_subscriptions)
229 self.assertContentEqual(
230 [sub1, sub2], info.direct_subscriptions_at_all_levels)
161231
162 def _create_duplicate_subscription(self):232 def _create_duplicate_subscription(self):
163 duplicate_bug = self.factory.makeBug(product=self.target)233 duplicate_bug = self.factory.makeBug(product=self.target)
@@ -171,34 +241,26 @@
171 def test_duplicate(self):241 def test_duplicate(self):
172 # The set of subscribers from duplicate bugs.242 # The set of subscribers from duplicate bugs.
173 found_subscriptions = self.getInfo().duplicate_subscriptions243 found_subscriptions = self.getInfo().duplicate_subscriptions
174 self.assertEqual(set(), found_subscriptions)244 self.assertContentEqual([], found_subscriptions)
175 self.assertEqual((), found_subscriptions.sorted)245 self.assertContentEqual([], found_subscriptions.subscribers)
176 self.assertEqual(set(), found_subscriptions.subscribers)
177 self.assertEqual((), found_subscriptions.subscribers.sorted)
178 duplicate_bug, duplicate_bug_subscription = (246 duplicate_bug, duplicate_bug_subscription = (
179 self._create_duplicate_subscription())247 self._create_duplicate_subscription())
180 found_subscriptions = self.getInfo().duplicate_subscriptions248 found_subscriptions = self.getInfo().duplicate_subscriptions
181 self.assertEqual(249 self.assertContentEqual(
182 set([duplicate_bug_subscription]),250 [duplicate_bug_subscription],
183 found_subscriptions)251 found_subscriptions)
184 self.assertEqual(252 self.assertContentEqual(
185 (duplicate_bug_subscription,),253 [duplicate_bug.owner],
186 found_subscriptions.sorted)
187 self.assertEqual(
188 set([duplicate_bug.owner]),
189 found_subscriptions.subscribers)254 found_subscriptions.subscribers)
190 self.assertEqual(
191 (duplicate_bug.owner,),
192 found_subscriptions.subscribers.sorted)
193255
194 def test_muted_duplicate(self):256 def test_duplicate_muted(self):
195 # If a duplicate is muted, it is not listed.257 # If a duplicate is muted, it is not listed.
196 duplicate_bug, duplicate_bug_subscription = (258 duplicate_bug, duplicate_bug_subscription = (
197 self._create_duplicate_subscription())259 self._create_duplicate_subscription())
198 with person_logged_in(duplicate_bug.owner):260 with person_logged_in(duplicate_bug.owner):
199 self.bug.mute(duplicate_bug.owner, duplicate_bug.owner)261 self.bug.mute(duplicate_bug.owner, duplicate_bug.owner)
200 found_subscriptions = self.getInfo().duplicate_subscriptions262 found_subscriptions = self.getInfo().duplicate_subscriptions
201 self.assertEqual(set(), found_subscriptions)263 self.assertContentEqual([], found_subscriptions)
202264
203 def test_duplicate_for_private_bug(self):265 def test_duplicate_for_private_bug(self):
204 # The set of subscribers from duplicate bugs is always empty when the266 # The set of subscribers from duplicate bugs is always empty when the
@@ -209,10 +271,8 @@
209 with person_logged_in(self.bug.owner):271 with person_logged_in(self.bug.owner):
210 self.bug.setPrivate(True, self.bug.owner)272 self.bug.setPrivate(True, self.bug.owner)
211 found_subscriptions = self.getInfo().duplicate_subscriptions273 found_subscriptions = self.getInfo().duplicate_subscriptions
212 self.assertEqual(set(), found_subscriptions)274 self.assertContentEqual([], found_subscriptions)
213 self.assertEqual((), found_subscriptions.sorted)275 self.assertContentEqual([], found_subscriptions.subscribers)
214 self.assertEqual(set(), found_subscriptions.subscribers)
215 self.assertEqual((), found_subscriptions.subscribers.sorted)
216276
217 def test_duplicate_only(self):277 def test_duplicate_only(self):
218 # The set of duplicate subscriptions where the subscriber has no other278 # The set of duplicate subscriptions where the subscriber has no other
@@ -224,8 +284,8 @@
224 duplicate_bug.getSubscriptionForPerson(284 duplicate_bug.getSubscriptionForPerson(
225 duplicate_bug.owner))285 duplicate_bug.owner))
226 found_subscriptions = self.getInfo().duplicate_only_subscriptions286 found_subscriptions = self.getInfo().duplicate_only_subscriptions
227 self.assertEqual(287 self.assertContentEqual(
228 set([duplicate_bug_subscription]),288 [duplicate_bug_subscription],
229 found_subscriptions)289 found_subscriptions)
230 # If a user is subscribed to a duplicate bug and is a bugtask290 # If a user is subscribed to a duplicate bug and is a bugtask
231 # assignee, for example, their duplicate subscription will not be291 # assignee, for example, their duplicate subscription will not be
@@ -234,71 +294,136 @@
234 self.bug.default_bugtask.transitionToAssignee(294 self.bug.default_bugtask.transitionToAssignee(
235 duplicate_bug_subscription.person)295 duplicate_bug_subscription.person)
236 found_subscriptions = self.getInfo().duplicate_only_subscriptions296 found_subscriptions = self.getInfo().duplicate_only_subscriptions
237 self.assertEqual(set(), found_subscriptions)297 self.assertContentEqual([], found_subscriptions)
238298
239 def test_structural(self):299 def test_structural_subscriptions(self):
300 # The set of structural subscriptions.
301 subscribers = (
302 self.factory.makePerson(),
303 self.factory.makePerson())
304 with person_logged_in(self.bug.owner):
305 subscriptions = tuple(
306 self.target.addBugSubscription(subscriber, subscriber)
307 for subscriber in subscribers)
308 found_subscriptions = self.getInfo().structural_subscriptions
309 self.assertContentEqual(subscriptions, found_subscriptions)
310
311 def test_structural_subscriptions_muted(self):
312 # The set of structural subscriptions DOES NOT exclude muted
313 # subscriptions.
314 subscriber = self.factory.makePerson()
315 with person_logged_in(subscriber):
316 self.bug.mute(subscriber, subscriber)
317 with person_logged_in(self.bug.owner):
318 subscription = self.target.addBugSubscription(
319 subscriber, subscriber)
320 found_subscriptions = self.getInfo().structural_subscriptions
321 self.assertContentEqual([subscription], found_subscriptions)
322
323 def test_structural_subscribers(self):
240 # The set of structural subscribers.324 # The set of structural subscribers.
241 subscribers = (325 subscribers = (
242 self.factory.makePerson(),326 self.factory.makePerson(),
243 self.factory.makePerson())327 self.factory.makePerson())
244 with person_logged_in(self.bug.owner):328 with person_logged_in(self.bug.owner):
245 subscriptions = tuple(329 for subscriber in subscribers:
246 self.target.addBugSubscription(subscriber, subscriber)330 self.target.addBugSubscription(subscriber, subscriber)
247 for subscriber in subscribers)331 found_subscribers = self.getInfo().structural_subscribers
248 found_subscriptions = self.getInfo().structural_subscriptions332 self.assertContentEqual(subscribers, found_subscribers)
249 self.assertEqual(set(subscriptions), found_subscriptions)333
250 self.assertEqual(subscriptions, found_subscriptions.sorted)334 def test_structural_subscribers_muted(self):
251 self.assertEqual(set(subscribers), found_subscriptions.subscribers)335 # The set of structural subscribers DOES NOT exclude muted
252 self.assertEqual(subscribers, found_subscriptions.subscribers.sorted)336 # subscribers.
337 subscriber = self.factory.makePerson()
338 with person_logged_in(subscriber):
339 self.bug.mute(subscriber, subscriber)
340 with person_logged_in(self.bug.owner):
341 self.target.addBugSubscription(subscriber, subscriber)
342 found_subscribers = self.getInfo().structural_subscribers
343 self.assertContentEqual([subscriber], found_subscribers)
253344
254 def test_all_assignees(self):345 def test_all_assignees(self):
255 # The set of bugtask assignees for bugtasks that have been assigned.346 # The set of bugtask assignees for bugtasks that have been assigned.
256 found_assignees = self.getInfo().all_assignees347 found_assignees = self.getInfo().all_assignees
257 self.assertEqual(set(), found_assignees)348 self.assertContentEqual([], found_assignees)
258 self.assertEqual((), found_assignees.sorted)
259 bugtask = self.bug.default_bugtask349 bugtask = self.bug.default_bugtask
260 with person_logged_in(bugtask.pillar.bug_supervisor):350 with person_logged_in(bugtask.pillar.bug_supervisor):
261 bugtask.transitionToAssignee(self.bug.owner)351 bugtask.transitionToAssignee(self.bug.owner)
262 found_assignees = self.getInfo().all_assignees352 found_assignees = self.getInfo().all_assignees
263 self.assertEqual(set([self.bug.owner]), found_assignees)353 self.assertContentEqual([self.bug.owner], found_assignees)
264 self.assertEqual((self.bug.owner,), found_assignees.sorted)
265 bugtask2 = self.factory.makeBugTask(bug=self.bug)354 bugtask2 = self.factory.makeBugTask(bug=self.bug)
266 with person_logged_in(bugtask2.pillar.owner):355 with person_logged_in(bugtask2.pillar.owner):
267 bugtask2.transitionToAssignee(bugtask2.owner)356 bugtask2.transitionToAssignee(bugtask2.owner)
268 found_assignees = self.getInfo().all_assignees357 found_assignees = self.getInfo().all_assignees
269 self.assertEqual(358 self.assertContentEqual(
270 set([self.bug.owner, bugtask2.owner]),359 [self.bug.owner, bugtask2.owner],
271 found_assignees)360 found_assignees)
272 self.assertEqual(361 # Getting info for a specific bugtask will return the assignee for
273 (self.bug.owner, bugtask2.owner),362 # that bugtask only.
274 found_assignees.sorted)363 self.assertContentEqual(
364 [bugtask2.owner],
365 self.getInfo().forTask(bugtask2).all_assignees)
275366
276 def test_all_pillar_owners_without_bug_supervisors(self):367 def test_all_pillar_owners_without_bug_supervisors(self):
277 # The set of owners of pillars for which no bug supervisor is368 # The set of owners of pillars for which no bug supervisor is
278 # configured.369 # configured and which use Launchpad for bug tracking.
279 [bugtask] = self.bug.bugtasks370 [bugtask] = self.bug.bugtasks
280 found_owners = (371 found_owners = (
281 self.getInfo().all_pillar_owners_without_bug_supervisors)372 self.getInfo().all_pillar_owners_without_bug_supervisors)
282 self.assertEqual(set(), found_owners)373 self.assertContentEqual([], found_owners)
283 self.assertEqual((), found_owners.sorted)374 # Clear the supervisor for the bugtask's target and ensure that the
284 # Clear the supervisor for the first bugtask's target.375 # project uses Launchpad Bugs.
285 with person_logged_in(bugtask.target.owner):376 with person_logged_in(bugtask.target.owner):
286 bugtask.target.setBugSupervisor(None, bugtask.owner)377 bugtask.target.setBugSupervisor(None, bugtask.owner)
378 bugtask.pillar.official_malone = True
379 # The collection includes the pillar's owner.
287 found_owners = (380 found_owners = (
288 self.getInfo().all_pillar_owners_without_bug_supervisors)381 self.getInfo().all_pillar_owners_without_bug_supervisors)
289 self.assertEqual(set([bugtask.pillar.owner]), found_owners)382 self.assertContentEqual([bugtask.pillar.owner], found_owners)
290 self.assertEqual((bugtask.pillar.owner,), found_owners.sorted)383 # Add another bugtask for a pillar that uses Launchpad but does not
291 # Add another bugtask with a bug supervisor.384 # have a bug supervisor.
292 target2 = self.factory.makeProduct(bug_supervisor=None)385 target2 = self.factory.makeProduct(
386 bug_supervisor=None, official_malone=True)
293 bugtask2 = self.factory.makeBugTask(bug=self.bug, target=target2)387 bugtask2 = self.factory.makeBugTask(bug=self.bug, target=target2)
294 found_owners = (388 found_owners = (
295 self.getInfo().all_pillar_owners_without_bug_supervisors)389 self.getInfo().all_pillar_owners_without_bug_supervisors)
296 self.assertEqual(390 self.assertContentEqual(
297 set([bugtask.pillar.owner, bugtask2.pillar.owner]),391 [bugtask.pillar.owner, bugtask2.pillar.owner],
298 found_owners)392 found_owners)
299 self.assertEqual(393
300 (bugtask.pillar.owner, bugtask2.pillar.owner),394 def test_all_pillar_owners_without_bug_supervisors_not_using_malone(self):
301 found_owners.sorted)395 # The set of owners of pillars for which no bug supervisor is
396 # configured and which do not use Launchpad for bug tracking is empty.
397 [bugtask] = self.bug.bugtasks
398 # Clear the supervisor for the first bugtask's target and ensure the
399 # project does not use Launchpad Bugs.
400 with person_logged_in(bugtask.target.owner):
401 bugtask.target.setBugSupervisor(None, bugtask.owner)
402 bugtask.pillar.official_malone = False
403 found_owners = (
404 self.getInfo().all_pillar_owners_without_bug_supervisors)
405 self.assertContentEqual([], found_owners)
406
407 def test_all_pillar_owners_without_bug_supervisors_for_bugtask(self):
408 # The set of the owner of the chosen bugtask's pillar when no bug
409 # supervisor is configured and which uses Launchpad for bug tracking.
410 [bugtask] = self.bug.bugtasks
411 # Clear the supervisor for the bugtask's target and ensure that the
412 # project uses Launchpad Bugs.
413 with person_logged_in(bugtask.target.owner):
414 bugtask.target.setBugSupervisor(None, bugtask.owner)
415 bugtask.pillar.official_malone = True
416 # Add another bugtask for a pillar that uses Launchpad but does not
417 # have a bug supervisor.
418 target2 = self.factory.makeProduct(
419 bug_supervisor=None, official_malone=True)
420 bugtask2 = self.factory.makeBugTask(bug=self.bug, target=target2)
421 # Getting subscription info for just a specific bugtask will yield
422 # owners for only the pillar associated with that bugtask.
423 info_for_bugtask2 = self.getInfo().forTask(bugtask2)
424 self.assertContentEqual(
425 [bugtask2.pillar.owner],
426 info_for_bugtask2.all_pillar_owners_without_bug_supervisors)
302427
303 def _create_also_notified_subscribers(self):428 def _create_also_notified_subscribers(self):
304 # Add an assignee, a bug supervisor and a structural subscriber.429 # Add an assignee, a bug supervisor and a structural subscriber.
@@ -318,8 +443,7 @@
318 def test_also_notified_subscribers(self):443 def test_also_notified_subscribers(self):
319 # The set of also notified subscribers.444 # The set of also notified subscribers.
320 found_subscribers = self.getInfo().also_notified_subscribers445 found_subscribers = self.getInfo().also_notified_subscribers
321 self.assertEqual(set(), found_subscribers)446 self.assertContentEqual([], found_subscribers)
322 self.assertEqual((), found_subscribers.sorted)
323 assignee, supervisor, structural_subscriber = (447 assignee, supervisor, structural_subscriber = (
324 self._create_also_notified_subscribers())448 self._create_also_notified_subscribers())
325 # Add a direct subscription.449 # Add a direct subscription.
@@ -329,14 +453,11 @@
329 # The direct subscriber does not appear in the also notified set, but453 # The direct subscriber does not appear in the also notified set, but
330 # the assignee, supervisor and structural subscriber do.454 # the assignee, supervisor and structural subscriber do.
331 found_subscribers = self.getInfo().also_notified_subscribers455 found_subscribers = self.getInfo().also_notified_subscribers
332 self.assertEqual(456 self.assertContentEqual(
333 set([assignee, supervisor, structural_subscriber]),457 [assignee, supervisor, structural_subscriber],
334 found_subscribers)458 found_subscribers)
335 self.assertEqual(
336 (assignee, supervisor, structural_subscriber),
337 found_subscribers.sorted)
338459
339 def test_muted_also_notified_subscribers(self):460 def test_also_notified_subscribers_muted(self):
340 # If someone is muted, they are not listed in the461 # If someone is muted, they are not listed in the
341 # also_notified_subscribers.462 # also_notified_subscribers.
342 assignee, supervisor, structural_subscriber = (463 assignee, supervisor, structural_subscriber = (
@@ -345,8 +466,8 @@
345 # the assignee, supervisor and structural subscriber do show up466 # the assignee, supervisor and structural subscriber do show up
346 # when they are not muted.467 # when they are not muted.
347 found_subscribers = self.getInfo().also_notified_subscribers468 found_subscribers = self.getInfo().also_notified_subscribers
348 self.assertEqual(469 self.assertContentEqual(
349 set([assignee, supervisor, structural_subscriber]),470 [assignee, supervisor, structural_subscriber],
350 found_subscribers)471 found_subscribers)
351 # Now we mute all of the subscribers.472 # Now we mute all of the subscribers.
352 with person_logged_in(assignee):473 with person_logged_in(assignee):
@@ -357,8 +478,7 @@
357 self.bug.mute(structural_subscriber, structural_subscriber)478 self.bug.mute(structural_subscriber, structural_subscriber)
358 # Now we don't see them.479 # Now we don't see them.
359 found_subscribers = self.getInfo().also_notified_subscribers480 found_subscribers = self.getInfo().also_notified_subscribers
360 self.assertEqual(481 self.assertContentEqual([], found_subscribers)
361 set(), found_subscribers)
362482
363 def test_also_notified_subscribers_for_private_bug(self):483 def test_also_notified_subscribers_for_private_bug(self):
364 # The set of also notified subscribers is always empty when the master484 # The set of also notified subscribers is always empty when the master
@@ -370,8 +490,7 @@
370 with person_logged_in(self.bug.owner):490 with person_logged_in(self.bug.owner):
371 self.bug.setPrivate(True, self.bug.owner)491 self.bug.setPrivate(True, self.bug.owner)
372 found_subscribers = self.getInfo().also_notified_subscribers492 found_subscribers = self.getInfo().also_notified_subscribers
373 self.assertEqual(set(), found_subscribers)493 self.assertContentEqual([], found_subscribers)
374 self.assertEqual((), found_subscribers.sorted)
375494
376 def test_indirect_subscribers(self):495 def test_indirect_subscribers(self):
377 # The set of indirect subscribers is the union of also notified496 # The set of indirect subscribers is the union of also notified
@@ -384,12 +503,9 @@
384 with person_logged_in(duplicate_bug.owner):503 with person_logged_in(duplicate_bug.owner):
385 duplicate_bug.markAsDuplicate(self.bug)504 duplicate_bug.markAsDuplicate(self.bug)
386 found_subscribers = self.getInfo().indirect_subscribers505 found_subscribers = self.getInfo().indirect_subscribers
387 self.assertEqual(506 self.assertContentEqual(
388 set([assignee, duplicate_bug.owner]),507 [assignee, duplicate_bug.owner],
389 found_subscribers)508 found_subscribers)
390 self.assertEqual(
391 (assignee, duplicate_bug.owner),
392 found_subscribers.sorted)
393509
394510
395class TestBugSubscriptionInfoPermissions(TestCaseWithFactory):511class TestBugSubscriptionInfoPermissions(TestCaseWithFactory):
@@ -406,7 +522,7 @@
406522
407 # All attributes require launchpad.View.523 # All attributes require launchpad.View.
408 permissions = set(checker.get_permissions.itervalues())524 permissions = set(checker.get_permissions.itervalues())
409 self.assertEqual(set(["launchpad.View"]), permissions)525 self.assertContentEqual(["launchpad.View"], permissions)
410526
411 # The security adapter for launchpad.View lets anyone reference the527 # The security adapter for launchpad.View lets anyone reference the
412 # attributes unless the bug is private, in which case only explicit528 # attributes unless the bug is private, in which case only explicit
@@ -444,27 +560,46 @@
444 yield recorder560 yield recorder
445 self.assertThat(recorder, condition)561 self.assertThat(recorder, condition)
446562
447 def exercise_subscription_set(self, set_name):563 def exercise_subscription_set(self, set_name, counts=(1, 1, 0)):
564 """Test the number of queries it takes to inspect a subscription set.
565
566 :param set_name: The name of the set, e.g. "direct_subscriptions".
567 :param counts: A triple of the expected query counts for each of three
568 operations: get the set, get the set's subscribers, get the set's
569 subscribers in order.
570 """
448 # Looking up subscriptions takes a single query.571 # Looking up subscriptions takes a single query.
449 with self.exactly_x_queries(1):572 with self.exactly_x_queries(counts[0]):
450 getattr(self.info, set_name)573 getattr(self.info, set_name)
451 # Getting the subscribers results in one additional query.574 # Getting the subscribers results in one additional query.
452 with self.exactly_x_queries(1):575 with self.exactly_x_queries(counts[1]):
453 getattr(self.info, set_name).subscribers576 getattr(self.info, set_name).subscribers
454 # Everything is now cached so no more queries are needed.577 # Everything is now cached so no more queries are needed.
455 with self.exactly_x_queries(0):578 with self.exactly_x_queries(counts[2]):
456 getattr(self.info, set_name).subscribers579 getattr(self.info, set_name).subscribers
457 getattr(self.info, set_name).subscribers.sorted580 getattr(self.info, set_name).subscribers.sorted
458581
459 def exercise_subscription_set_sorted_first(self, set_name):582 def exercise_subscription_set_sorted_first(
583 self, set_name, counts=(1, 1, 0)):
584 """Test the number of queries it takes to inspect a subscription set.
585
586 This differs from `exercise_subscription_set` in its second step, when
587 it looks at the sorted subscription list instead of the subscriber
588 set.
589
590 :param set_name: The name of the set, e.g. "direct_subscriptions".
591 :param counts: A triple of the expected query counts for each of three
592 operations: get the set, get the set in order, get the set's
593 subscribers in order.
594 """
460 # Looking up subscriptions takes a single query.595 # Looking up subscriptions takes a single query.
461 with self.exactly_x_queries(1):596 with self.exactly_x_queries(counts[0]):
462 getattr(self.info, set_name)597 getattr(self.info, set_name)
463 # Getting the sorted subscriptions takes one additional query.598 # Getting the sorted subscriptions takes one additional query.
464 with self.exactly_x_queries(1):599 with self.exactly_x_queries(counts[1]):
465 getattr(self.info, set_name).sorted600 getattr(self.info, set_name).sorted
466 # Everything is now cached so no more queries are needed.601 # Everything is now cached so no more queries are needed.
467 with self.exactly_x_queries(0):602 with self.exactly_x_queries(counts[2]):
468 getattr(self.info, set_name).subscribers603 getattr(self.info, set_name).subscribers
469 getattr(self.info, set_name).subscribers.sorted604 getattr(self.info, set_name).subscribers.sorted
470605
@@ -476,6 +611,10 @@
476 self.exercise_subscription_set_sorted_first(611 self.exercise_subscription_set_sorted_first(
477 "direct_subscriptions")612 "direct_subscriptions")
478613
614 def test_direct_subscriptions_at_all_levels(self):
615 self.exercise_subscription_set(
616 "direct_subscriptions_at_all_levels")
617
479 def make_duplicate_bug(self):618 def make_duplicate_bug(self):
480 duplicate_bug = self.factory.makeBug(product=self.target)619 duplicate_bug = self.factory.makeBug(product=self.target)
481 with person_logged_in(duplicate_bug.owner):620 with person_logged_in(duplicate_bug.owner):
@@ -501,18 +640,19 @@
501 self.info.duplicate_subscriptions.subscribers640 self.info.duplicate_subscriptions.subscribers
502641
503 def add_structural_subscriber(self):642 def add_structural_subscriber(self):
504 with person_logged_in(self.bug.owner):643 subscriber = self.factory.makePerson()
505 self.target.addSubscription(self.bug.owner, self.bug.owner)644 with person_logged_in(subscriber):
645 self.target.addSubscription(subscriber, subscriber)
506646
507 def test_structural_subscriptions(self):647 def test_structural_subscriptions(self):
508 self.add_structural_subscriber()648 self.add_structural_subscriber()
509 self.exercise_subscription_set(649 self.exercise_subscription_set(
510 "structural_subscriptions")650 "structural_subscriptions", (2, 1, 0))
511651
512 def test_structural_subscriptions_sorted_first(self):652 def test_structural_subscriptions_sorted_first(self):
513 self.add_structural_subscriber()653 self.add_structural_subscriber()
514 self.exercise_subscription_set_sorted_first(654 self.exercise_subscription_set_sorted_first(
515 "structural_subscriptions")655 "structural_subscriptions", (2, 1, 0))
516656
517 def test_all_assignees(self):657 def test_all_assignees(self):
518 with self.exactly_x_queries(1):658 with self.exactly_x_queries(1):
@@ -522,8 +662,8 @@
522 # Getting all bug supervisors and pillar owners can take several662 # Getting all bug supervisors and pillar owners can take several
523 # queries. However, there are typically few tasks so the trade for663 # queries. However, there are typically few tasks so the trade for
524 # simplicity of implementation is acceptable. Only the simplest case664 # simplicity of implementation is acceptable. Only the simplest case
525 # is tested here.665 # is tested here (everything is already cached).
526 with self.exactly_x_queries(1):666 with self.exactly_x_queries(0):
527 self.info.all_pillar_owners_without_bug_supervisors667 self.info.all_pillar_owners_without_bug_supervisors
528668
529 def test_also_notified_subscribers(self):669 def test_also_notified_subscribers(self):
@@ -536,7 +676,7 @@
536 self.info.all_assignees676 self.info.all_assignees
537 self.info.all_pillar_owners_without_bug_supervisors677 self.info.all_pillar_owners_without_bug_supervisors
538 self.info.direct_subscriptions.subscribers678 self.info.direct_subscriptions.subscribers
539 self.info.structural_subscriptions.subscribers679 self.info.structural_subscribers
540 with self.exactly_x_queries(1):680 with self.exactly_x_queries(1):
541 self.info.also_notified_subscribers681 self.info.also_notified_subscribers
542682
543683
=== modified file 'lib/lp/bugs/tests/test_structuralsubscription.py'
--- lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-04 18:01:30 +0000
@@ -27,8 +27,10 @@
27from lp.bugs.model.structuralsubscription import (27from lp.bugs.model.structuralsubscription import (
28 get_structural_subscribers,28 get_structural_subscribers,
29 get_structural_subscription_targets,29 get_structural_subscription_targets,
30 get_structural_subscriptions,
30 get_structural_subscriptions_for_bug,31 get_structural_subscriptions_for_bug,
31 )32 )
33from lp.services.database.decoratedresultset import DecoratedResultSet
32from lp.testing import (34from lp.testing import (
33 anonymous_logged_in,35 anonymous_logged_in,
34 login_person,36 login_person,
@@ -42,6 +44,9 @@
42 )44 )
4345
4446
47RESULT_SETS = ResultSet, EmptyResultSet, DecoratedResultSet
48
49
45class TestStructuralSubscription(TestCaseWithFactory):50class TestStructuralSubscription(TestCaseWithFactory):
4651
47 layer = DatabaseFunctionalLayer52 layer = DatabaseFunctionalLayer
@@ -634,6 +639,99 @@
634 self.assertEqual(set([self_sub]), set(subscriptions))639 self.assertEqual(set([self_sub]), set(subscriptions))
635640
636641
642class TestGetStructuralSubscriptions(TestCaseWithFactory):
643
644 layer = DatabaseFunctionalLayer
645
646 def make_product_with_bug(self):
647 product = self.factory.makeProduct()
648 bug = self.factory.makeBug(product=product)
649 return product, bug
650
651 def test_get_structural_subscriptions_no_subscriptions(self):
652 # If there are no subscriptions for any of the bug's targets then no
653 # subscriptions will be returned by get_structural_subscriptions().
654 product, bug = self.make_product_with_bug()
655 subscriptions = get_structural_subscriptions(bug, None)
656 self.assertIsInstance(subscriptions, RESULT_SETS)
657 self.assertEqual([], list(subscriptions))
658
659 def test_get_structural_subscriptions_single_target(self):
660 # Subscriptions for any of the bug's targets are returned.
661 subscriber = self.factory.makePerson()
662 login_person(subscriber)
663 product, bug = self.make_product_with_bug()
664 subscription = product.addBugSubscription(subscriber, subscriber)
665 self.assertContentEqual(
666 [subscription], get_structural_subscriptions(bug, None))
667
668 def test_get_structural_subscriptions_multiple_targets(self):
669 # Subscriptions for any of the bug's targets are returned.
670 actor = self.factory.makePerson()
671 login_person(actor)
672
673 subscriber1 = self.factory.makePerson()
674 subscriber2 = self.factory.makePerson()
675
676 product1 = self.factory.makeProduct(owner=actor)
677 subscription1 = product1.addBugSubscription(subscriber1, subscriber1)
678 product2 = self.factory.makeProduct(owner=actor)
679 subscription2 = product2.addBugSubscription(subscriber2, subscriber2)
680
681 bug = self.factory.makeBug(product=product1)
682 bug.addTask(actor, product2)
683
684 subscriptions = get_structural_subscriptions(bug, None)
685 self.assertIsInstance(subscriptions, RESULT_SETS)
686 self.assertContentEqual(
687 [subscription1, subscription2], subscriptions)
688
689 def test_get_structural_subscriptions_multiple_targets_2(self):
690 # Only the first of multiple subscriptions for a person is returned
691 # when they have multiple matching subscriptions.
692 actor = self.factory.makePerson()
693 login_person(actor)
694
695 subscriber = self.factory.makePerson()
696 product1 = self.factory.makeProduct(owner=actor)
697 subscription1 = product1.addBugSubscription(subscriber, subscriber)
698 product2 = self.factory.makeProduct(owner=actor)
699 product2.addBugSubscription(subscriber, subscriber)
700
701 bug = self.factory.makeBug(product=product1)
702 bug.addTask(actor, product2)
703
704 subscriptions = get_structural_subscriptions(bug, None)
705 self.assertIsInstance(subscriptions, RESULT_SETS)
706 self.assertContentEqual([subscription1], subscriptions)
707
708 def test_get_structural_subscriptions_level(self):
709 # get_structural_subscriptions() respects the given level.
710 subscriber = self.factory.makePerson()
711 login_person(subscriber)
712 product, bug = self.make_product_with_bug()
713 subscription = product.addBugSubscription(subscriber, subscriber)
714 filter = subscription.bug_filters.one()
715 filter.bug_notification_level = BugNotificationLevel.METADATA
716 self.assertContentEqual(
717 [subscription], get_structural_subscriptions(
718 bug, BugNotificationLevel.METADATA))
719 self.assertContentEqual(
720 [], get_structural_subscriptions(
721 bug, BugNotificationLevel.COMMENTS))
722
723 def test_get_structural_subscriptions_exclude(self):
724 # Subscriptions for any of the given excluded subscribers are not
725 # returned.
726 subscriber = self.factory.makePerson()
727 login_person(subscriber)
728 product, bug = self.make_product_with_bug()
729 product.addBugSubscription(subscriber, subscriber)
730 self.assertContentEqual(
731 [], get_structural_subscriptions(
732 bug, None, exclude=[subscriber]))
733
734
637class TestGetStructuralSubscribers(TestCaseWithFactory):735class TestGetStructuralSubscribers(TestCaseWithFactory):
638736
639 layer = DatabaseFunctionalLayer737 layer = DatabaseFunctionalLayer
@@ -648,7 +746,7 @@
648 # subscribers will be returned by get_structural_subscribers().746 # subscribers will be returned by get_structural_subscribers().
649 product, bug = self.make_product_with_bug()747 product, bug = self.make_product_with_bug()
650 subscribers = get_structural_subscribers(bug, None, None, None)748 subscribers = get_structural_subscribers(bug, None, None, None)
651 self.assertIsInstance(subscribers, (ResultSet, EmptyResultSet))749 self.assertIsInstance(subscribers, RESULT_SETS)
652 self.assertEqual([], list(subscribers))750 self.assertEqual([], list(subscribers))
653751
654 def test_getStructuralSubscribers_single_target(self):752 def test_getStructuralSubscribers_single_target(self):
@@ -678,7 +776,7 @@
678 bug.addTask(actor, product2)776 bug.addTask(actor, product2)
679777
680 subscribers = get_structural_subscribers(bug, None, None, None)778 subscribers = get_structural_subscribers(bug, None, None, None)
681 self.assertIsInstance(subscribers, ResultSet)779 self.assertIsInstance(subscribers, RESULT_SETS)
682 self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))780 self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
683781
684 def test_getStructuralSubscribers_recipients(self):782 def test_getStructuralSubscribers_recipients(self):