~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

Merge db-devel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
    'Bug',
12
12
    'BugAffectsPerson',
13
13
    'BugBecameQuestionEvent',
 
14
    'BugMute',
14
15
    'BugSet',
15
16
    'BugTag',
16
17
    'FileBugData',
29
30
from functools import wraps
30
31
from itertools import chain
31
32
import operator
 
33
import pytz
32
34
import re
33
35
 
34
36
from lazr.lifecycle.event import (
52
54
    Count,
53
55
    Desc,
54
56
    Exists,
 
57
    In,
55
58
    Join,
56
59
    LeftJoin,
57
60
    Max,
63
66
    Union,
64
67
    )
65
68
from storm.info import ClassAlias
 
69
from storm.locals import (
 
70
    DateTime,
 
71
    Int,
 
72
    Reference,
 
73
    )
66
74
from storm.store import (
67
75
    EmptyResultSet,
68
76
    Store,
138
146
from lp.bugs.interfaces.bug import (
139
147
    IBug,
140
148
    IBugBecameQuestionEvent,
 
149
    IBugMute,
141
150
    IBugSet,
142
151
    IFileBugData,
143
152
    )
188
197
from lp.registry.interfaces.distroseries import IDistroSeries
189
198
from lp.registry.interfaces.person import (
190
199
    IPersonSet,
 
200
    validate_person,
191
201
    validate_public_person,
192
202
    )
193
203
from lp.registry.interfaces.product import IProduct
201
211
    )
202
212
from lp.registry.model.pillar import pillar_sort_key
203
213
from lp.registry.model.teammembership import TeamParticipation
 
214
from lp.services.database.stormbase import StormBase
204
215
from lp.services.fields import DuplicateBug
205
216
from lp.services.propertycache import (
206
217
    cachedproperty,
521
532
                    parent = message_by_id.get(parent.id, parent)
522
533
            else:
523
534
                message, bugmessage = row
524
 
                parent = None # parent attribute is not going to be accessed.
 
535
                parent = None  # parent attribute is not going to be accessed.
525
536
            index = bugmessage.index
526
537
            result = IndexedMessage(message, inside, index, parent)
527
538
            if include_parents:
843
854
        """See `IBug`."""
844
855
        return self.personIsSubscribedToDuplicate(person)
845
856
 
 
857
    def _getMutes(self, person):
 
858
        store = Store.of(self)
 
859
        mutes = store.find(
 
860
            BugMute,
 
861
            BugMute.bug == self,
 
862
            BugMute.person == person)
 
863
        return mutes
 
864
 
846
865
    def isMuted(self, person):
847
866
        """See `IBug`."""
848
 
        store = Store.of(self)
849
 
        subscriptions = store.find(
850
 
            BugSubscription,
851
 
            BugSubscription.bug == self,
852
 
            BugSubscription.person == person,
853
 
            BugSubscription.bug_notification_level ==
854
 
                BugNotificationLevel.NOTHING)
855
 
        return not subscriptions.is_empty()
 
867
        mutes = self._getMutes(person)
 
868
        return not mutes.is_empty()
856
869
 
857
870
    def mute(self, person, muted_by):
858
871
        """See `IBug`."""
859
872
        if person is None:
860
873
            # This may be a webservice request.
861
874
            person = muted_by
862
 
        # If there's an existing subscription, update it.
863
 
        store = Store.of(self)
864
 
        subscriptions = store.find(
865
 
            BugSubscription,
866
 
            BugSubscription.bug == self,
867
 
            BugSubscription.person == person)
868
 
        if subscriptions.is_empty():
869
 
            return self.subscribe(
870
 
                person, muted_by, level=BugNotificationLevel.NOTHING)
 
875
        assert not person.is_team, (
 
876
            "Muting a subscription for entire team is not allowed.")
 
877
 
 
878
        # If it's already muted, ignore the request.
 
879
        mutes = self._getMutes(person)
 
880
        if mutes.is_empty():
 
881
            mute = BugMute(person, self)
 
882
            Store.of(mute).flush()
871
883
        else:
872
 
            subscription = subscriptions.one()
873
 
            subscription.bug_notification_level = (
874
 
                BugNotificationLevel.NOTHING)
875
 
            return subscription
 
884
            # It's already muted, pass.
 
885
            pass
876
886
 
877
887
    def unmute(self, person, unmuted_by):
878
888
        """See `IBug`."""
879
 
        self.unsubscribe(person, unmuted_by)
 
889
        store = Store.of(self)
 
890
        if person is None:
 
891
            # This may be a webservice request.
 
892
            person = unmuted_by
 
893
        mutes = self._getMutes(person)
 
894
        store.remove(mutes.one())
 
895
        return self.getSubscriptionForPerson(person)
880
896
 
881
897
    @property
882
898
    def subscriptions(self):
888
904
            BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
889
905
        return DecoratedResultSet(results, operator.itemgetter(1))
890
906
 
891
 
    def getSubscriptionInfo(self, level=BugNotificationLevel.NOTHING):
 
907
    def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
892
908
        """See `IBug`."""
893
909
        return BugSubscriptionInfo(self, level)
894
910
 
905
921
        it.
906
922
        """
907
923
        if level is None:
908
 
            level = BugNotificationLevel.NOTHING
 
924
            level = BugNotificationLevel.LIFECYCLE
909
925
        subscriptions = self.getSubscriptionInfo(level).direct_subscriptions
910
926
        if recipients is not None:
911
927
            for subscriber in subscriptions.subscribers:
954
970
        recipients argument.
955
971
        """
956
972
        if level is None:
957
 
            level = BugNotificationLevel.NOTHING
 
973
            level = BugNotificationLevel.LIFECYCLE
958
974
        info = self.getSubscriptionInfo(level)
959
975
 
960
976
        if recipients is not None:
2235
2251
        return IStore(BugSubscription).find(
2236
2252
            BugSubscription,
2237
2253
            BugSubscription.bug_notification_level >= self.level,
2238
 
            BugSubscription.bug == self.bug)
 
2254
            BugSubscription.bug == self.bug,
 
2255
            Not(In(BugSubscription.person_id,
 
2256
                   Select(BugMute.person_id, BugMute.bug_id==self.bug.id))))
2239
2257
 
2240
2258
    @cachedproperty
2241
2259
    @freeze(BugSubscriptionSet)
2248
2266
                BugSubscription,
2249
2267
                BugSubscription.bug_notification_level >= self.level,
2250
2268
                BugSubscription.bug_id == Bug.id,
2251
 
                Bug.duplicateof == self.bug)
 
2269
                Bug.duplicateof == self.bug,
 
2270
                Not(In(BugSubscription.person_id,
 
2271
                       Select(BugMute.person_id, BugMute.bug_id==Bug.id))))
2252
2272
 
2253
2273
    @cachedproperty
2254
2274
    @freeze(BugSubscriptionSet)
2255
2275
    def duplicate_only_subscriptions(self):
2256
 
        """Subscripitions to duplicates of the bug.
 
2276
        """Subscriptions to duplicates of the bug.
2257
2277
 
2258
2278
        Excludes subscriptions for people who have a direct subscription or
2259
2279
        are also notified for another reason.
2294
2314
        if self.bug.private:
2295
2315
            return BugSubscriberSet()
2296
2316
        else:
 
2317
            muted = IStore(BugMute).find(
 
2318
                Person,
 
2319
                BugMute.person_id==Person.id,
 
2320
                BugMute.bug==self.bug)
2297
2321
            return BugSubscriberSet().union(
2298
2322
                self.structural_subscriptions.subscribers,
2299
2323
                self.all_pillar_owners_without_bug_supervisors,
2300
2324
                self.all_assignees).difference(
2301
 
                self.direct_subscriptions.subscribers)
 
2325
                self.direct_subscriptions.subscribers).difference(muted)
2302
2326
 
2303
2327
    @cachedproperty
2304
2328
    def indirect_subscribers(self):
2637
2661
    def asDict(self):
2638
2662
        """Return the FileBugData instance as a dict."""
2639
2663
        return self.__dict__.copy()
 
2664
 
 
2665
 
 
2666
class BugMute(StormBase):
 
2667
    """Contains bugs a person has decided to block notifications from."""
 
2668
 
 
2669
    implements(IBugMute)
 
2670
 
 
2671
    __storm_table__ = "BugMute"
 
2672
 
 
2673
    def __init__(self, person=None, bug=None):
 
2674
        if person is not None:
 
2675
            self.person = person
 
2676
        if bug is not None:
 
2677
            self.bug_id = bug.id
 
2678
 
 
2679
    person_id = Int("person", allow_none=False, validator=validate_person)
 
2680
    person = Reference(person_id, "Person.id")
 
2681
 
 
2682
    bug_id = Int("bug", allow_none=False)
 
2683
    bug = Reference(bug_id, "Bug.id")
 
2684
 
 
2685
    __storm_primary__ = 'person_id', 'bug_id'
 
2686
 
 
2687
    date_created = DateTime(
 
2688
        "date_created", allow_none=False, default=UTC_NOW,
 
2689
        tzinfo=pytz.UTC)