1030
1034
if level is None:
1031
1035
level = BugNotificationLevel.LIFECYCLE
1032
1036
info = self.getSubscriptionInfo(level)
1034
1037
if recipients is not None:
1035
# Pre-load duplicate bugs.
1036
list(self.duplicates)
1038
list(self.duplicates) # Pre-load duplicate bugs.
1039
info.duplicate_only_subscribers # Pre-load subscribers.
1037
1040
for subscription in info.duplicate_only_subscriptions:
1038
1041
recipients.addDupeSubscriber(
1039
1042
subscription.person, subscription.bug)
1040
return info.duplicate_only_subscriptions.subscribers.sorted
1043
return info.duplicate_only_subscribers.sorted
1042
1045
def getSubscribersForPerson(self, person):
1043
1046
"""See `IBug."""
2389
2392
if IBug.providedBy(bug_or_bugtask):
2390
2393
bug = bug_or_bugtask
2391
2394
bugtasks = bug.bugtasks
2395
info = bug.getSubscriptionInfo(level)
2392
2396
elif IBugTask.providedBy(bug_or_bugtask):
2393
2397
bug = bug_or_bugtask.bug
2394
2398
bugtasks = [bug_or_bugtask]
2399
info = bug.getSubscriptionInfo(level).forTask(bug_or_bugtask)
2396
2401
raise ValueError('First argument must be bug or bugtask')
2398
2403
if bug.private:
2401
# Direct subscriptions always take precedence over indirect
2403
direct_subscribers = set(bug.getDirectSubscribers())
2405
also_notified_subscribers = set()
2407
for bugtask in bugtasks:
2408
if (bugtask.assignee and
2409
bugtask.assignee not in direct_subscribers):
2410
# We have an assignee that is not a direct subscriber.
2411
also_notified_subscribers.add(bugtask.assignee)
2412
if recipients is not None:
2406
# Subscribers to exclude.
2407
exclude_subscribers = frozenset().union(
2408
info.direct_subscribers_at_all_levels, info.muted_subscribers)
2409
# Get also-notified subscribers at the given level for the given tasks.
2410
also_notified_subscribers = info.also_notified_subscribers
2412
if recipients is not None:
2413
for bugtask in bugtasks:
2414
assignee = bugtask.assignee
2415
if assignee in also_notified_subscribers:
2416
# We have an assignee that is not a direct subscriber.
2413
2417
recipients.addAssignee(bugtask.assignee)
2415
# If the target's bug supervisor isn't set...
2416
pillar = bugtask.pillar
2417
if (pillar.bug_supervisor is None and
2418
pillar.official_malone and
2419
pillar.owner not in direct_subscribers):
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)
2418
# If the target's bug supervisor isn't set...
2419
pillar = bugtask.pillar
2420
if pillar.official_malone and pillar.bug_supervisor is None:
2421
if pillar.owner in also_notified_subscribers:
2422
# ...we add the owner as a subscriber.
2423
recipients.addRegistrant(pillar.owner, pillar)
2425
2425
# This structural subscribers code omits direct subscribers itself.
2426
also_notified_subscribers.update(
2427
get_structural_subscribers(
2428
bug_or_bugtask, recipients, level, direct_subscribers))
2426
# TODO: Pass the info object into get_structural_subscribers for
2427
# efficiency... or do the recipients stuff here.
2428
structural_subscribers = get_structural_subscribers(
2429
bug_or_bugtask, recipients, level, exclude_subscribers)
2430
assert also_notified_subscribers.issuperset(structural_subscribers)
2430
# Remove security proxy for the sort key, but return
2431
# the regular proxied object.
2432
return sorted(also_notified_subscribers,
2433
key=lambda x: removeSecurityProxy(x).displayname)
2432
return also_notified_subscribers.sorted
2436
2435
def load_people(*where):
2574
2574
def __init__(self, bug, level):
2576
self.bugtask = None # Implies all.
2576
2577
assert level is not None
2577
2578
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()
2588
def cache_key(self):
2589
"""A (bug ID, bugtask ID, level) tuple for use as a hash key.
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.
2595
bugtask_id = None if self.bugtask is None else self.bugtask.id
2596
return self.bug.id, bugtask_id, self.level
2598
def forTask(self, bugtask):
2599
"""Create a new `BugSubscriptionInfo` limited to `bugtask`.
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.
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)
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)
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))
2579
2620
@cachedproperty
2580
2621
@freeze(BugSubscriptionSet)
2581
def old_direct_subscriptions(self):
2582
"""The bug's direct subscriptions."""
2622
def direct_subscriptions(self):
2623
"""The bug's direct subscriptions.
2625
Excludes muted subscriptions.
2583
2627
return IStore(BugSubscription).find(
2584
2628
BugSubscription,
2585
2629
BugSubscription.bug_notification_level >= self.level,
2587
2631
Not(In(BugSubscription.person_id,
2588
2632
Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))
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 ((), ())
2606
@freeze(BugSubscriptionSet)
2607
def direct_subscriptions(self):
2608
return self.direct_subscriptions_and_subscribers[0]
2611
@freeze(BugSubscriberSet)
2612
2635
def direct_subscribers(self):
2613
return self.direct_subscriptions_and_subscribers[1]
2636
"""The bug's direct subscriptions.
2638
Excludes muted subscribers.
2640
return self.direct_subscriptions.subscribers
2643
def direct_subscriptions_at_all_levels(self):
2644
"""The bug's direct subscriptions at all levels.
2646
Excludes muted subscriptions.
2648
return self.forLevel(
2649
BugNotificationLevel.LIFECYCLE).direct_subscriptions
2652
def direct_subscribers_at_all_levels(self):
2653
"""The bug's direct subscribers at all levels.
2655
Excludes muted subscribers.
2657
return self.direct_subscriptions_at_all_levels.subscribers
2615
2659
@cachedproperty
2616
def duplicate_subscriptions_and_subscribers(self):
2617
"""Subscriptions to duplicates of the bug."""
2660
@freeze(BugSubscriptionSet)
2661
def duplicate_subscriptions(self):
2662
"""Subscriptions to duplicates of the bug.
2664
Excludes muted subscriptions.
2618
2666
if self.bug.private:
2621
res = IStore(BugSubscription).find(
2622
(BugSubscription, Person),
2669
return IStore(BugSubscription).find(
2623
2671
BugSubscription.bug_notification_level >= self.level,
2624
2672
BugSubscription.bug_id == Bug.id,
2625
BugSubscription.person_id == Person.id,
2626
2673
Bug.duplicateof == self.bug,
2627
2674
Not(In(BugSubscription.person_id,
2628
2675
Select(BugMute.person_id, BugMute.bug_id == Bug.id))))
2629
# Here we could test for res.count() but that will execute another
2630
# query. This structure avoids the extra query.
2631
return zip(*res) or ((), ())
2634
@freeze(BugSubscriptionSet)
2635
def duplicate_subscriptions(self):
2636
return self.duplicate_subscriptions_and_subscribers[0]
2639
@freeze(BugSubscriberSet)
2640
2678
def duplicate_subscribers(self):
2641
return self.duplicate_subscriptions_and_subscribers[1]
2679
"""Subscribers to duplicates of the bug.
2681
Excludes muted subscribers.
2683
return self.duplicate_subscriptions.subscribers
2643
2685
@cachedproperty
2644
2686
@freeze(BugSubscriptionSet)
2645
2687
def duplicate_only_subscriptions(self):
2646
"""Subscriptions to duplicates of the bug.
2688
"""Subscriptions to duplicates of the bug only.
2648
Excludes subscriptions for people who have a direct subscription or
2649
are also notified for another reason.
2690
Excludes muted subscriptions, subscriptions for people who have a
2691
direct subscription, or who are also notified for another reason.
2651
2693
self.duplicate_subscribers # Pre-load subscribers.
2652
2694
higher_precedence = (
2656
2698
subscription for subscription in self.duplicate_subscriptions
2657
2699
if subscription.person not in higher_precedence)
2702
def duplicate_only_subscribers(self):
2703
"""Subscribers to duplicates of the bug only.
2705
Excludes muted subscribers, subscribers who have a direct
2706
subscription, or who are also notified for another reason.
2708
return self.duplicate_only_subscriptions.subscribers
2659
2710
@cachedproperty
2660
2711
@freeze(StructuralSubscriptionSet)
2661
2712
def structural_subscriptions(self):
2662
"""Structural subscriptions to the bug's targets."""
2663
return list(get_structural_subscriptions_for_bug(self.bug))
2713
"""Structural subscriptions to the bug's targets.
2715
Excludes direct subscriptions.
2717
subject = self.bug if self.bugtask is None else self.bugtask
2718
return get_structural_subscriptions(subject, self.level)
2721
def structural_subscribers(self):
2722
"""Structural subscribers to the bug's targets.
2724
Excludes direct subscribers.
2726
return self.structural_subscriptions.subscribers
2665
2728
@cachedproperty
2666
2729
@freeze(BugSubscriberSet)
2667
2730
def all_assignees(self):
2668
"""Assignees of the bug's tasks."""
2669
assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug)
2670
return load_people(Person.id.is_in(assignees))
2731
"""Assignees of the bug's tasks.
2733
*Does not* exclude muted subscribers.
2735
if self.bugtask is None:
2736
assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug)
2737
return load_people(Person.id.is_in(assignees))
2739
return load_people(Person.id == self.bugtask.assigneeID)
2672
2741
@cachedproperty
2673
2742
@freeze(BugSubscriberSet)
2674
2743
def all_pillar_owners_without_bug_supervisors(self):
2675
"""Owners of pillars for which no Bug supervisor is configured."""
2676
for bugtask in self.bug.bugtasks:
2744
"""Owners of pillars for which there is no bug supervisor.
2746
The pillars must also use Launchpad for bug tracking.
2748
*Does not* exclude muted subscribers.
2750
if self.bugtask is None:
2751
bugtasks = self.bug.bugtasks
2753
bugtasks = [self.bugtask]
2754
for bugtask in bugtasks:
2677
2755
pillar = bugtask.pillar
2678
if pillar.bug_supervisor is None:
2756
if pillar.official_malone:
2757
if pillar.bug_supervisor is None:
2681
2760
@cachedproperty
2682
2761
def also_notified_subscribers(self):
2683
"""All subscribers except direct and dupe subscribers."""
2762
"""All subscribers except direct, dupe, and muted subscribers."""
2684
2763
if self.bug.private:
2685
2764
return BugSubscriberSet()
2687
muted = IStore(BugMute).find(
2689
BugMute.person_id == Person.id,
2690
BugMute.bug == self.bug)
2691
return BugSubscriberSet().union(
2692
self.structural_subscriptions.subscribers,
2766
subscribers = BugSubscriberSet().union(
2767
self.structural_subscribers,
2693
2768
self.all_pillar_owners_without_bug_supervisors,
2694
self.all_assignees).difference(
2695
self.direct_subscribers).difference(muted)
2770
return subscribers.difference(
2771
self.direct_subscribers_at_all_levels,
2772
self.muted_subscribers)
2697
2774
@cachedproperty
2698
2775
def indirect_subscribers(self):
2699
"""All subscribers except direct subscribers."""
2776
"""All subscribers except direct subscribers.
2778
Excludes muted subscribers.
2700
2780
return self.also_notified_subscribers.union(
2701
2781
self.duplicate_subscribers)