187
202
from lp.registry.model.pillar import pillar_sort_key
188
203
from lp.registry.model.teammembership import TeamParticipation
189
from lp.services.config import config
190
from lp.services.database.constants import UTC_NOW
191
from lp.services.database.datetimecol import UtcDateTimeCol
192
204
from lp.services.database.decoratedresultset import DecoratedResultSet
193
205
from lp.services.database.lpstorm import IStore
194
from lp.services.database.sqlbase import (
199
206
from lp.services.database.stormbase import StormBase
200
207
from lp.services.features import getFeatureFlag
201
208
from lp.services.fields import DuplicateBug
202
from lp.services.helpers import shortlist
203
209
from lp.services.librarian.interfaces import ILibraryFileAliasSet
204
210
from lp.services.librarian.model import (
205
211
LibraryFileAlias,
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)