~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/model/bug.py

[r=jtv][bug=874250] Load preferredemail when calculating bug
 subscribers of any kind via BugSubscriptionInfo.

Show diffs side-by-side

added added

removed removed

Lines of Context:
158
158
from lp.bugs.model.bugwatch import BugWatch
159
159
from lp.bugs.model.structuralsubscription import (
160
160
    get_structural_subscribers,
 
161
    get_structural_subscriptions,
161
162
    get_structural_subscriptions_for_bug,
162
163
    )
163
164
from lp.code.interfaces.branchcollection import IAllBranches
948
949
            BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
949
950
        return DecoratedResultSet(results, operator.itemgetter(1))
950
951
 
951
 
    def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
 
952
    def getSubscriptionInfo(self, level=None):
952
953
        """See `IBug`."""
 
954
        if level is None:
 
955
            level = BugNotificationLevel.LIFECYCLE
953
956
        return BugSubscriptionInfo(self, level)
954
957
 
955
958
    def getDirectSubscriptions(self):
1002
1005
        # the regular proxied object.
1003
1006
        return sorted(
1004
1007
            indirect_subscribers,
 
1008
            # XXX: GavinPanella 2011-12-12 bug=911752: Use person_sort_key.
1005
1009
            key=lambda x: removeSecurityProxy(x).displayname)
1006
1010
 
1007
1011
    def getSubscriptionsFromDuplicates(self, recipients=None):
1030
1034
        if level is None:
1031
1035
            level = BugNotificationLevel.LIFECYCLE
1032
1036
        info = self.getSubscriptionInfo(level)
1033
 
 
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
1041
1044
 
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)
2395
2400
    else:
2396
2401
        raise ValueError('First argument must be bug or bugtask')
2397
2402
 
2398
2403
    if bug.private:
2399
2404
        return []
2400
2405
 
2401
 
    # Direct subscriptions always take precedence over indirect
2402
 
    # subscriptions.
2403
 
    direct_subscribers = set(bug.getDirectSubscribers())
2404
 
 
2405
 
    also_notified_subscribers = set()
2406
 
 
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
 
2411
 
 
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)
2414
 
 
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)
2424
2424
 
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)
2429
2431
 
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
2434
2433
 
2435
2434
 
2436
2435
def load_people(*where):
2443
2442
        `ValidPersonCache` records are loaded simultaneously.
2444
2443
    """
2445
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)
2447
2447
 
2448
2448
 
2449
2449
class BugSubscriberSet(frozenset):
2573
2573
 
2574
2574
    def __init__(self, bug, level):
2575
2575
        self.bug = bug
 
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()
 
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))
2578
2619
 
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.
 
2624
 
 
2625
        Excludes muted subscriptions.
 
2626
        """
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))))
2589
2633
 
2590
 
    @cachedproperty
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)
 
2634
    @property
2612
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
2614
2658
 
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.
 
2663
 
 
2664
        Excludes muted subscriptions.
 
2665
        """
2618
2666
        if self.bug.private:
2619
 
            return ((), ())
 
2667
            return ()
2620
2668
        else:
2621
 
            res = IStore(BugSubscription).find(
2622
 
                (BugSubscription, Person),
 
2669
            return IStore(BugSubscription).find(
 
2670
                BugSubscription,
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 ((), ())
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)
 
2676
 
 
2677
    @property
2640
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
2642
2684
 
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.
2647
2689
 
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.
2650
2692
        """
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)
2658
2700
 
 
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
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.
 
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
2664
2727
 
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.
 
2732
 
 
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)
2671
2740
 
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.
 
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
2755
            pillar = bugtask.pillar
2678
 
            if pillar.bug_supervisor is None:
2679
 
                yield pillar.owner
 
2756
            if pillar.official_malone:
 
2757
                if pillar.bug_supervisor is None:
 
2758
                    yield pillar.owner
2680
2759
 
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()
2686
2765
        else:
2687
 
            muted = IStore(BugMute).find(
2688
 
                Person,
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)
 
2769
                self.all_assignees)
 
2770
            return subscribers.difference(
 
2771
                self.direct_subscribers_at_all_levels,
 
2772
                self.muted_subscribers)
2696
2773
 
2697
2774
    @cachedproperty
2698
2775
    def indirect_subscribers(self):
2699
 
        """All subscribers except direct subscribers."""
 
2776
        """All subscribers except direct subscribers.
 
2777
 
 
2778
        Excludes muted subscribers.
 
2779
        """
2700
2780
        return self.also_notified_subscribers.union(
2701
2781
            self.duplicate_subscribers)
2702
2782