~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Curtis Hovey
  • Date: 2011-12-24 17:49:30 UTC
  • mto: This revision was merged to the branch mainline in revision 14602.
  • Revision ID: curtis.hovey@canonical.com-20111224174930-xk1d5cvhyxq46ctf
Moved webapp to lp.services.

Show diffs side-by-side

added added

removed removed

Lines of Context:
86
86
    removeSecurityProxy,
87
87
    )
88
88
 
 
89
from canonical.config import config
 
90
from canonical.database.constants import UTC_NOW
 
91
from canonical.database.datetimecol import UtcDateTimeCol
 
92
from canonical.database.sqlbase import (
 
93
    cursor,
 
94
    SQLBase,
 
95
    sqlvalues,
 
96
    )
 
97
from lp.services.helpers import shortlist
 
98
from lp.services.webapp.authorization import check_permission
 
99
from lp.services.webapp.interfaces import (
 
100
    DEFAULT_FLAVOR,
 
101
    ILaunchBag,
 
102
    IStoreSelector,
 
103
    MAIN_STORE,
 
104
    )
89
105
from lp.answers.interfaces.questiontarget import IQuestionTarget
90
106
from lp.app.enums import ServiceUsage
91
107
from lp.app.errors import (
158
174
from lp.bugs.model.bugwatch import BugWatch
159
175
from lp.bugs.model.structuralsubscription import (
160
176
    get_structural_subscribers,
161
 
    get_structural_subscriptions,
162
177
    get_structural_subscriptions_for_bug,
163
178
    )
164
179
from lp.code.interfaces.branchcollection import IAllBranches
186
201
    )
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 (
195
 
    cursor,
196
 
    SQLBase,
197
 
    sqlvalues,
198
 
    )
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,
219
225
    clear_property_cache,
220
226
    get_property_cache,
221
227
    )
222
 
from lp.services.webapp.authorization import check_permission
223
 
from lp.services.webapp.interfaces import (
224
 
    DEFAULT_FLAVOR,
225
 
    ILaunchBag,
226
 
    IStoreSelector,
227
 
    MAIN_STORE,
228
 
    )
229
228
 
230
229
 
231
230
_bug_tag_query_template = """
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