1034
1030
if level is None:
1035
1031
level = BugNotificationLevel.LIFECYCLE
1036
1032
info = self.getSubscriptionInfo(level)
1037
1034
if recipients is not None:
1038
list(self.duplicates) # Pre-load duplicate bugs.
1039
info.duplicate_only_subscribers # Pre-load subscribers.
1035
# Pre-load duplicate bugs.
1036
list(self.duplicates)
1040
1037
for subscription in info.duplicate_only_subscriptions:
1041
1038
recipients.addDupeSubscriber(
1042
1039
subscription.person, subscription.bug)
1043
return info.duplicate_only_subscribers.sorted
1040
return info.duplicate_only_subscriptions.subscribers.sorted
1045
1042
def getSubscribersForPerson(self, person):
1046
1043
"""See `IBug."""
2392
2389
if IBug.providedBy(bug_or_bugtask):
2393
2390
bug = bug_or_bugtask
2394
2391
bugtasks = bug.bugtasks
2395
info = bug.getSubscriptionInfo(level)
2396
2392
elif IBugTask.providedBy(bug_or_bugtask):
2397
2393
bug = bug_or_bugtask.bug
2398
2394
bugtasks = [bug_or_bugtask]
2399
info = bug.getSubscriptionInfo(level).forTask(bug_or_bugtask)
2401
2396
raise ValueError('First argument must be bug or bugtask')
2403
2398
if bug.private:
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.
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:
2417
2413
recipients.addAssignee(bugtask.assignee)
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)
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)
2425
2425
# This structural subscribers code omits direct subscribers itself.
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)
2426
also_notified_subscribers.update(
2427
get_structural_subscribers(
2428
bug_or_bugtask, recipients, level, direct_subscribers))
2432
return also_notified_subscribers.sorted
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)
2435
2436
def load_people(*where):
2574
2574
def __init__(self, bug, level):
2576
self.bugtask = None # Implies all.
2577
2576
assert level is not None
2578
2577
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))
2620
2579
@cachedproperty
2621
2580
@freeze(BugSubscriptionSet)
2622
def direct_subscriptions(self):
2623
"""The bug's direct subscriptions.
2625
Excludes muted subscriptions.
2581
def old_direct_subscriptions(self):
2582
"""The bug's direct subscriptions."""
2627
2583
return IStore(BugSubscription).find(
2628
2584
BugSubscription,
2629
2585
BugSubscription.bug_notification_level >= self.level,
2631
2587
Not(In(BugSubscription.person_id,
2632
2588
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)
2635
2612
def direct_subscribers(self):
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
2613
return self.direct_subscriptions_and_subscribers[1]
2659
2615
@cachedproperty
2660
@freeze(BugSubscriptionSet)
2661
def duplicate_subscriptions(self):
2662
"""Subscriptions to duplicates of the bug.
2664
Excludes muted subscriptions.
2616
def duplicate_subscriptions_and_subscribers(self):
2617
"""Subscriptions to duplicates of the bug."""
2666
2618
if self.bug.private:
2669
return IStore(BugSubscription).find(
2621
res = IStore(BugSubscription).find(
2622
(BugSubscription, Person),
2671
2623
BugSubscription.bug_notification_level >= self.level,
2672
2624
BugSubscription.bug_id == Bug.id,
2625
BugSubscription.person_id == Person.id,
2673
2626
Bug.duplicateof == self.bug,
2674
2627
Not(In(BugSubscription.person_id,
2675
2628
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)
2678
2640
def duplicate_subscribers(self):
2679
"""Subscribers to duplicates of the bug.
2681
Excludes muted subscribers.
2683
return self.duplicate_subscriptions.subscribers
2641
return self.duplicate_subscriptions_and_subscribers[1]
2685
2643
@cachedproperty
2686
2644
@freeze(BugSubscriptionSet)
2687
2645
def duplicate_only_subscriptions(self):
2688
"""Subscriptions to duplicates of the bug only.
2646
"""Subscriptions to duplicates of the bug.
2690
Excludes muted subscriptions, subscriptions for people who have a
2691
direct subscription, or who are also notified for another reason.
2648
Excludes subscriptions for people who have a direct subscription or
2649
are also notified for another reason.
2693
2651
self.duplicate_subscribers # Pre-load subscribers.
2694
2652
higher_precedence = (
2698
2656
subscription for subscription in self.duplicate_subscriptions
2699
2657
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
2710
2659
@cachedproperty
2711
2660
@freeze(StructuralSubscriptionSet)
2712
2661
def structural_subscriptions(self):
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
2662
"""Structural subscriptions to the bug's targets."""
2663
return list(get_structural_subscriptions_for_bug(self.bug))
2728
2665
@cachedproperty
2729
2666
@freeze(BugSubscriberSet)
2730
2667
def all_assignees(self):
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)
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))
2741
2672
@cachedproperty
2742
2673
@freeze(BugSubscriberSet)
2743
2674
def all_pillar_owners_without_bug_supervisors(self):
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:
2675
"""Owners of pillars for which no Bug supervisor is configured."""
2676
for bugtask in self.bug.bugtasks:
2755
2677
pillar = bugtask.pillar
2756
if pillar.official_malone:
2757
if pillar.bug_supervisor is None:
2678
if pillar.bug_supervisor is None:
2760
2681
@cachedproperty
2761
2682
def also_notified_subscribers(self):
2762
"""All subscribers except direct, dupe, and muted subscribers."""
2683
"""All subscribers except direct and dupe subscribers."""
2763
2684
if self.bug.private:
2764
2685
return BugSubscriberSet()
2766
subscribers = BugSubscriberSet().union(
2767
self.structural_subscribers,
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,
2768
2693
self.all_pillar_owners_without_bug_supervisors,
2770
return subscribers.difference(
2771
self.direct_subscribers_at_all_levels,
2772
self.muted_subscribers)
2694
self.all_assignees).difference(
2695
self.direct_subscribers).difference(muted)
2774
2697
@cachedproperty
2775
2698
def indirect_subscribers(self):
2776
"""All subscribers except direct subscribers.
2778
Excludes muted subscribers.
2699
"""All subscribers except direct subscribers."""
2780
2700
return self.also_notified_subscribers.union(
2781
2701
self.duplicate_subscribers)