~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2012-01-03 07:30:21 UTC
  • mto: This revision was merged to the branch mainline in revision 14621.
  • Revision ID: william.grant@canonical.com-20120103073021-qvprj6kbnpg0h1qv
Update a few templates.

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,
162
161
    get_structural_subscriptions_for_bug,
163
162
    )
164
163
from lp.code.interfaces.branchcollection import IAllBranches
949
948
            BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
950
949
        return DecoratedResultSet(results, operator.itemgetter(1))
951
950
 
952
 
    def getSubscriptionInfo(self, level=None):
 
951
    def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
953
952
        """See `IBug`."""
954
 
        if level is None:
955
 
            level = BugNotificationLevel.LIFECYCLE
956
953
        return BugSubscriptionInfo(self, level)
957
954
 
958
955
    def getDirectSubscriptions(self):
1005
1002
        # the regular proxied object.
1006
1003
        return sorted(
1007
1004
            indirect_subscribers,
1008
 
            # XXX: GavinPanella 2011-12-12 bug=911752: Use person_sort_key.
1009
1005
            key=lambda x: removeSecurityProxy(x).displayname)
1010
1006
 
1011
1007
    def getSubscriptionsFromDuplicates(self, recipients=None):
1034
1030
        if level is None:
1035
1031
            level = BugNotificationLevel.LIFECYCLE
1036
1032
        info = self.getSubscriptionInfo(level)
 
1033
 
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
1044
1041
 
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)
2400
2395
    else:
2401
2396
        raise ValueError('First argument must be bug or bugtask')
2402
2397
 
2403
2398
    if bug.private:
2404
2399
        return []
2405
2400
 
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.
 
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:
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)
 
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)
2424
2424
 
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))
2431
2429
 
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)
2433
2434
 
2434
2435
 
2435
2436
def load_people(*where):
2442
2443
        `ValidPersonCache` records are loaded simultaneously.
2443
2444
    """
2444
2445
    return PersonSet()._getPrecachedPersons(
2445
 
        origin=[Person], conditions=where, need_validity=True,
2446
 
        need_preferred_email=True)
 
2446
        origin=[Person], conditions=where, need_validity=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.
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()
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))
2619
2578
 
2620
2579
    @cachedproperty
2621
2580
    @freeze(BugSubscriptionSet)
2622
 
    def direct_subscriptions(self):
2623
 
        """The bug's direct subscriptions.
2624
 
 
2625
 
        Excludes muted subscriptions.
2626
 
        """
 
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))))
2633
2589
 
2634
 
    @property
 
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)
2635
2612
    def direct_subscribers(self):
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
 
2613
        return self.direct_subscriptions_and_subscribers[1]
2658
2614
 
2659
2615
    @cachedproperty
2660
 
    @freeze(BugSubscriptionSet)
2661
 
    def duplicate_subscriptions(self):
2662
 
        """Subscriptions to duplicates of the bug.
2663
 
 
2664
 
        Excludes muted subscriptions.
2665
 
        """
 
2616
    def duplicate_subscriptions_and_subscribers(self):
 
2617
        """Subscriptions to duplicates of the bug."""
2666
2618
        if self.bug.private:
2667
 
            return ()
 
2619
            return ((), ())
2668
2620
        else:
2669
 
            return IStore(BugSubscription).find(
2670
 
                BugSubscription,
 
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))))
2676
 
 
2677
 
    @property
 
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)
2678
2640
    def duplicate_subscribers(self):
2679
 
        """Subscribers to duplicates of the bug.
2680
 
 
2681
 
        Excludes muted subscribers.
2682
 
        """
2683
 
        return self.duplicate_subscriptions.subscribers
 
2641
        return self.duplicate_subscriptions_and_subscribers[1]
2684
2642
 
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.
2689
2647
 
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.
2692
2650
        """
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)
2700
2658
 
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
 
 
2710
2659
    @cachedproperty
2711
2660
    @freeze(StructuralSubscriptionSet)
2712
2661
    def structural_subscriptions(self):
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
 
2662
        """Structural subscriptions to the bug's targets."""
 
2663
        return list(get_structural_subscriptions_for_bug(self.bug))
2727
2664
 
2728
2665
    @cachedproperty
2729
2666
    @freeze(BugSubscriberSet)
2730
2667
    def all_assignees(self):
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)
 
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))
2740
2671
 
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.
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:
 
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:
2758
 
                    yield pillar.owner
 
2678
            if pillar.bug_supervisor is None:
 
2679
                yield pillar.owner
2759
2680
 
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()
2765
2686
        else:
2766
 
            subscribers = BugSubscriberSet().union(
2767
 
                self.structural_subscribers,
 
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,
2768
2693
                self.all_pillar_owners_without_bug_supervisors,
2769
 
                self.all_assignees)
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)
2773
2696
 
2774
2697
    @cachedproperty
2775
2698
    def indirect_subscribers(self):
2776
 
        """All subscribers except direct subscribers.
2777
 
 
2778
 
        Excludes muted subscribers.
2779
 
        """
 
2699
        """All subscribers except direct subscribers."""
2780
2700
        return self.also_notified_subscribers.union(
2781
2701
            self.duplicate_subscribers)
2782
2702