~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-05-23 18:43:31 UTC
  • mfrom: (13084.2.6 page-match-rewrite-url)
  • Revision ID: launchpad@pqm.canonical.com-20110523184331-dhd2c7cgfuu49epw
[r=sinzui][bug=784273] Adds facility to the PageMatch to handle bad
        URIs

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
    'Bug',
12
12
    'BugAffectsPerson',
13
13
    'BugBecameQuestionEvent',
14
 
    'BugMute',
15
14
    'BugSet',
16
15
    'BugTag',
17
16
    'FileBugData',
30
29
from functools import wraps
31
30
from itertools import chain
32
31
import operator
33
 
import pytz
34
32
import re
35
33
 
36
34
from lazr.lifecycle.event import (
54
52
    Count,
55
53
    Desc,
56
54
    Exists,
57
 
    In,
58
55
    Join,
59
56
    LeftJoin,
60
57
    Max,
63
60
    Select,
64
61
    SQL,
65
62
    SQLRaw,
66
 
    Sum,
67
63
    Union,
68
64
    )
69
65
from storm.info import ClassAlias
70
 
from storm.locals import (
71
 
    DateTime,
72
 
    Int,
73
 
    Reference,
74
 
    )
75
66
from storm.store import (
76
67
    EmptyResultSet,
77
68
    Store,
103
94
    LibraryFileAlias,
104
95
    LibraryFileContent,
105
96
    )
 
97
from lp.services.messages.model.message import (
 
98
    Message,
 
99
    MessageChunk,
 
100
    MessageSet,
 
101
    )
106
102
from canonical.launchpad.helpers import shortlist
107
 
from canonical.launchpad.interfaces.launchpad import IHasBug
 
103
from canonical.launchpad.interfaces.launchpad import (
 
104
    IHasBug,
 
105
    ILaunchpadCelebrities,
 
106
    IPersonRoles,
 
107
    )
108
108
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
109
109
from canonical.launchpad.interfaces.lpstorm import IStore
 
110
from lp.services.messages.interfaces.message import (
 
111
    IMessage,
 
112
    IndexedMessage,
 
113
    )
110
114
from canonical.launchpad.webapp.authorization import check_permission
111
115
from canonical.launchpad.webapp.interfaces import (
112
116
    DEFAULT_FLAVOR,
119
123
    NotFoundError,
120
124
    UserCannotUnsubscribePerson,
121
125
    )
122
 
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
123
126
from lp.app.validators import LaunchpadValidationError
124
127
from lp.bugs.adapters.bugchange import (
125
128
    BranchLinkedToBug,
135
138
from lp.bugs.interfaces.bug import (
136
139
    IBug,
137
140
    IBugBecameQuestionEvent,
138
 
    IBugMute,
139
141
    IBugSet,
140
142
    IFileBugData,
141
143
    )
186
188
from lp.registry.interfaces.distroseries import IDistroSeries
187
189
from lp.registry.interfaces.person import (
188
190
    IPersonSet,
189
 
    validate_person,
190
191
    validate_public_person,
191
192
    )
192
193
from lp.registry.interfaces.product import IProduct
193
194
from lp.registry.interfaces.productseries import IProductSeries
194
 
from lp.registry.interfaces.role import IPersonRoles
195
195
from lp.registry.interfaces.series import SeriesStatus
196
196
from lp.registry.interfaces.sourcepackage import ISourcePackage
197
197
from lp.registry.model.person import (
201
201
    )
202
202
from lp.registry.model.pillar import pillar_sort_key
203
203
from lp.registry.model.teammembership import TeamParticipation
204
 
from lp.services.database.stormbase import StormBase
205
204
from lp.services.fields import DuplicateBug
206
 
from lp.services.messages.interfaces.message import (
207
 
    IMessage,
208
 
    IndexedMessage,
209
 
    )
210
 
from lp.services.messages.model.message import (
211
 
    Message,
212
 
    MessageChunk,
213
 
    MessageSet,
214
 
    )
215
205
from lp.services.propertycache import (
216
206
    cachedproperty,
217
207
    clear_property_cache,
243
233
    tag = StringCol(notNull=True)
244
234
 
245
235
 
 
236
# We need to always use the same Count instance or the
 
237
# get_bug_tags_open_count is not UNIONable.
 
238
tag_count_columns = (BugTag.tag, Count())
 
239
 
 
240
 
246
241
def get_bug_tags(context_clause):
247
242
    """Return all the bug tags as a list of strings.
248
243
 
262
257
    return shortlist([row[0] for row in cur.fetchall()])
263
258
 
264
259
 
265
 
def get_bug_tags_open_count(context_condition, user, tag_limit=0,
266
 
    include_tags=None):
267
 
    """Worker for IBugTarget.getUsedBugTagsWithOpenCounts.
268
 
 
269
 
    See `IBugTarget` for details.
270
 
 
271
 
    The only change is that this function takes a SQL expression for limiting
272
 
    the found tags.
 
260
def get_bug_tags_open_count(context_condition, user, wanted_tags=None):
 
261
    """Return all the used bug tags with their open bug count.
 
262
 
273
263
    :param context_condition: A Storm SQL expression, limiting the
274
264
        used tags to a specific context. Only the BugTask table may be
275
265
        used to choose the context.
 
266
    :param user: The user performing the search.
 
267
    :param wanted_tags: A set of tags within which to restrict the search.
 
268
 
 
269
    :return: A list of tuples, (tag name, open bug count).
276
270
    """
277
 
    # Circular fail.
278
 
    from lp.bugs.model.bugsummary import BugSummary
279
 
    tags = {}
280
 
    if include_tags:
281
 
        tags = dict((tag, 0) for tag in include_tags)
282
 
    store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
283
 
    admin_team = getUtility(ILaunchpadCelebrities).admin
284
 
    if user is not None and not user.inTeam(admin_team):
285
 
        store = store.with_(SQL(
286
 
            "teams AS ("
287
 
            "SELECT team from TeamParticipation WHERE person=?)", (user.id,)))
 
271
    tables = (
 
272
        BugTag,
 
273
        Join(BugTask, BugTask.bugID == BugTag.bugID),
 
274
        )
288
275
    where_conditions = [
289
 
        BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
290
 
        BugSummary.tag != None,
 
276
        BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
291
277
        context_condition,
292
278
        ]
293
 
    if user is None:
294
 
        where_conditions.append(BugSummary.viewed_by_id == None)
295
 
    elif not user.inTeam(admin_team):
 
279
    if wanted_tags is not None:
 
280
        where_conditions.append(BugTag.tag.is_in(wanted_tags))
 
281
    privacy_filter = get_bug_privacy_filter(user)
 
282
    if privacy_filter:
 
283
        # The EXISTS sub-select avoids a join against Bug, improving
 
284
        # performance significantly.
296
285
        where_conditions.append(
297
 
            Or(
298
 
                BugSummary.viewed_by_id == None,
299
 
                BugSummary.viewed_by_id.is_in(SQL("SELECT team FROM teams"))
300
 
                ))
301
 
    sum_count = Sum(BugSummary.count)
302
 
    tag_count_columns = (BugSummary.tag, sum_count)
303
 
    # Always query for used
304
 
    def _query(*args):
305
 
        return store.find(tag_count_columns, *(where_conditions + list(args))
306
 
            ).group_by(BugSummary.tag).having(sum_count != 0).order_by(
307
 
            Desc(Sum(BugSummary.count)), BugSummary.tag)
308
 
    used = _query()
309
 
    if tag_limit:
310
 
        used = used[:tag_limit]
311
 
    if include_tags:
312
 
        # Union in a query for just include_tags.
313
 
        used = used.union(_query(BugSummary.tag.is_in(include_tags)))
314
 
    tags.update(dict(used))
315
 
    return tags
 
286
            Exists(Select(
 
287
                columns=[True], tables=[Bug],
 
288
                where=And(Bug.id == BugTag.bugID, SQLRaw(privacy_filter)))))
 
289
    store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
 
290
    return store.using(*tables).find(
 
291
        tag_count_columns, *where_conditions).group_by(BugTag.tag).order_by(
 
292
            Desc(Count()), BugTag.tag)
316
293
 
317
294
 
318
295
class BugBecameQuestionEvent:
544
521
                    parent = message_by_id.get(parent.id, parent)
545
522
            else:
546
523
                message, bugmessage = row
547
 
                parent = None  # parent attribute is not going to be accessed.
 
524
                parent = None # parent attribute is not going to be accessed.
548
525
            index = bugmessage.index
549
526
            result = IndexedMessage(message, inside, index, parent)
550
527
            if include_parents:
584
561
        """See `IBug`."""
585
562
        dn = 'Bug #%d' % self.id
586
563
        if self.name:
587
 
            dn += ' (' + self.name + ')'
 
564
            dn += ' ('+self.name+')'
588
565
        return dn
589
566
 
590
567
    @cachedproperty
756
733
        table = LeftJoin(
757
734
            table,
758
735
            Distribution,
759
 
            OfficialBugTag.distribution_id == Distribution.id)
 
736
            OfficialBugTag.distribution_id==Distribution.id)
760
737
        table = LeftJoin(
761
738
            table,
762
739
            Product,
763
 
            OfficialBugTag.product_id == Product.id)
 
740
            OfficialBugTag.product_id==Product.id)
764
741
        # When this method is typically called it already has the necessary
765
742
        # info in memory, so rather than rejoin with Product etc, we do this
766
743
        # bit in Python. If reviewing performance here feel free to change.
775
752
 
776
753
    def followup_subject(self):
777
754
        """See `IBug`."""
778
 
        return 'Re: ' + self.title
 
755
        return 'Re: '+ self.title
779
756
 
780
757
    @property
781
758
    def has_patches(self):
866
843
        """See `IBug`."""
867
844
        return self.personIsSubscribedToDuplicate(person)
868
845
 
869
 
    def _getMutes(self, person):
870
 
        store = Store.of(self)
871
 
        mutes = store.find(
872
 
            BugMute,
873
 
            BugMute.bug == self,
874
 
            BugMute.person == person)
875
 
        return mutes
876
 
 
877
846
    def isMuted(self, person):
878
847
        """See `IBug`."""
879
 
        mutes = self._getMutes(person)
880
 
        return not mutes.is_empty()
 
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()
881
856
 
882
857
    def mute(self, person, muted_by):
883
858
        """See `IBug`."""
884
859
        if person is None:
885
860
            # This may be a webservice request.
886
861
            person = muted_by
887
 
        assert not person.is_team, (
888
 
            "Muting a subscription for entire team is not allowed.")
889
 
 
890
 
        # If it's already muted, ignore the request.
891
 
        mutes = self._getMutes(person)
892
 
        if mutes.is_empty():
893
 
            mute = BugMute(person, self)
894
 
            Store.of(mute).flush()
 
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)
895
871
        else:
896
 
            # It's already muted, pass.
897
 
            pass
 
872
            subscription = subscriptions.one()
 
873
            subscription.bug_notification_level = (
 
874
                BugNotificationLevel.NOTHING)
 
875
            return subscription
898
876
 
899
877
    def unmute(self, person, unmuted_by):
900
878
        """See `IBug`."""
901
 
        store = Store.of(self)
902
 
        if person is None:
903
 
            # This may be a webservice request.
904
 
            person = unmuted_by
905
 
        mutes = self._getMutes(person)
906
 
        store.remove(mutes.one())
907
 
        return self.getSubscriptionForPerson(person)
 
879
        self.unsubscribe(person, unmuted_by)
908
880
 
909
881
    @property
910
882
    def subscriptions(self):
916
888
            BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
917
889
        return DecoratedResultSet(results, operator.itemgetter(1))
918
890
 
919
 
    def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE):
 
891
    def getSubscriptionInfo(self, level=BugNotificationLevel.NOTHING):
920
892
        """See `IBug`."""
921
893
        return BugSubscriptionInfo(self, level)
922
894
 
933
905
        it.
934
906
        """
935
907
        if level is None:
936
 
            level = BugNotificationLevel.LIFECYCLE
 
908
            level = BugNotificationLevel.NOTHING
937
909
        subscriptions = self.getSubscriptionInfo(level).direct_subscriptions
938
910
        if recipients is not None:
939
911
            for subscriber in subscriptions.subscribers:
982
954
        recipients argument.
983
955
        """
984
956
        if level is None:
985
 
            level = BugNotificationLevel.LIFECYCLE
 
957
            level = BugNotificationLevel.NOTHING
986
958
        info = self.getSubscriptionInfo(level)
987
959
 
988
960
        if recipients is not None:
1464
1436
        # 1 bugmessage -> 1 message -> small N chunks. For now, using a wide
1465
1437
        # query seems fine as we have to join out from bugmessage anyway.
1466
1438
        result = Store.of(self).find((BugMessage, Message, MessageChunk),
1467
 
            Message.id == MessageChunk.messageID,
1468
 
            BugMessage.messageID == Message.id,
1469
 
            BugMessage.bug == self.id,
 
1439
            Message.id==MessageChunk.messageID,
 
1440
            BugMessage.messageID==Message.id,
 
1441
            BugMessage.bug==self.id,
1470
1442
            *ranges)
1471
1443
        result.order_by(BugMessage.index, MessageChunk.sequence)
1472
1444
 
1677
1649
            # Correct the heat for the bug immediately, so that we don't have
1678
1650
            # to wait for the next calculation job for the adjusted heat.
1679
1651
            self.updateHeat()
1680
 
            return True  # Changed.
 
1652
            return True # Changed.
1681
1653
        else:
1682
 
            return False  # Not changed.
 
1654
            return False # Not changed.
1683
1655
 
1684
1656
    def setSecurityRelated(self, security_related):
1685
1657
        """Setter for the `security_related` property."""
1690
1662
            # to wait for the next calculation job for the adjusted heat.
1691
1663
            self.updateHeat()
1692
1664
 
1693
 
            return True  # Changed
 
1665
            return True # Changed
1694
1666
        else:
1695
 
            return False  # Unchanged
 
1667
            return False # Unchanged
1696
1668
 
1697
1669
    def getBugTask(self, target):
1698
1670
        """See `IBug`."""
1710
1682
    def _cached_tags(self):
1711
1683
        return list(Store.of(self).find(
1712
1684
            BugTag.tag,
1713
 
            BugTag.bugID == self.id).order_by(BugTag.tag))
 
1685
            BugTag.bugID==self.id).order_by(BugTag.tag))
1714
1686
 
1715
1687
    def _setTags(self, tags):
1716
1688
        """Set the tags from a list of strings."""
1861
1833
    def userCanView(self, user):
1862
1834
        """See `IBug`.
1863
1835
 
1864
 
        This method is called by security adapters but only in the case for
1865
 
        authenticated users.  It is also called in other contexts where the
1866
 
        user may be anonymous.
 
1836
        Note that Editing is also controlled by this check,
 
1837
        because we permit editing of any bug one can see.
1867
1838
 
1868
1839
        If bug privacy rights are changed here, corresponding changes need
1869
1840
        to be made to the queries which screen for privacy.  See
1870
1841
        Bug.searchAsUser and BugTask.get_bug_privacy_filter_with_decorator.
1871
1842
        """
 
1843
        assert user is not None, "User may not be None"
 
1844
 
 
1845
        if user.id in self._known_viewers:
 
1846
            return True
1872
1847
        if not self.private:
1873
1848
            # This is a public bug.
1874
1849
            return True
1875
 
        # This method may be called for anonymous users.  For private bugs
1876
 
        # always return false for anonymous.
1877
 
        if user is None:
1878
 
            return False
1879
 
        if user.id in self._known_viewers:
1880
 
            return True
1881
 
 
1882
1850
        elif IPersonRoles(user).in_admin:
1883
1851
            # Admins can view all bugs.
1884
1852
            return True
2267
2235
        return IStore(BugSubscription).find(
2268
2236
            BugSubscription,
2269
2237
            BugSubscription.bug_notification_level >= self.level,
2270
 
            BugSubscription.bug == self.bug,
2271
 
            Not(In(BugSubscription.person_id,
2272
 
                   Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))
 
2238
            BugSubscription.bug == self.bug)
2273
2239
 
2274
2240
    @cachedproperty
2275
2241
    @freeze(BugSubscriptionSet)
2282
2248
                BugSubscription,
2283
2249
                BugSubscription.bug_notification_level >= self.level,
2284
2250
                BugSubscription.bug_id == Bug.id,
2285
 
                Bug.duplicateof == self.bug,
2286
 
                Not(In(BugSubscription.person_id,
2287
 
                       Select(BugMute.person_id, BugMute.bug_id == Bug.id))))
 
2251
                Bug.duplicateof == self.bug)
2288
2252
 
2289
2253
    @cachedproperty
2290
2254
    @freeze(BugSubscriptionSet)
2291
2255
    def duplicate_only_subscriptions(self):
2292
 
        """Subscriptions to duplicates of the bug.
 
2256
        """Subscripitions to duplicates of the bug.
2293
2257
 
2294
2258
        Excludes subscriptions for people who have a direct subscription or
2295
2259
        are also notified for another reason.
2296
2260
        """
2297
 
        self.duplicate_subscriptions.subscribers  # Pre-load subscribers.
 
2261
        self.duplicate_subscriptions.subscribers # Pre-load subscribers.
2298
2262
        higher_precedence = (
2299
2263
            self.direct_subscriptions.subscribers.union(
2300
2264
                self.also_notified_subscribers))
2330
2294
        if self.bug.private:
2331
2295
            return BugSubscriberSet()
2332
2296
        else:
2333
 
            muted = IStore(BugMute).find(
2334
 
                Person,
2335
 
                BugMute.person_id == Person.id,
2336
 
                BugMute.bug == self.bug)
2337
2297
            return BugSubscriberSet().union(
2338
2298
                self.structural_subscriptions.subscribers,
2339
2299
                self.all_pillar_owners_without_bug_supervisors,
2340
2300
                self.all_assignees).difference(
2341
 
                self.direct_subscriptions.subscribers).difference(muted)
 
2301
                self.direct_subscriptions.subscribers)
2342
2302
 
2343
2303
    @cachedproperty
2344
2304
    def indirect_subscribers(self):
2586
2546
        #      Transaction.iterSelect() will try to listify the results.
2587
2547
        #      This can be fixed by selecting from Bugs directly, but
2588
2548
        #      that's non-trivial.
2589
 
        # ---: Robert Collins 2010-08-18: if bug_tasks implements IResultSet
 
2549
        # ---: Robert Collins 20100818: if bug_tasks implements IResultSset
2590
2550
        #      then it should be very possible to improve on it, though
2591
2551
        #      DecoratedResultSets would need careful handling (e.g. type
2592
2552
        #      driven callbacks on columns)
2635
2595
            Bug.heat_last_updated == None)
2636
2596
 
2637
2597
        return store.find(
2638
 
            Bug, Bug.duplicateof == None, last_updated_clause).order_by('id')
 
2598
            Bug, Bug.duplicateof==None, last_updated_clause).order_by('id')
2639
2599
 
2640
2600
 
2641
2601
class BugAffectsPerson(SQLBase):
2677
2637
    def asDict(self):
2678
2638
        """Return the FileBugData instance as a dict."""
2679
2639
        return self.__dict__.copy()
2680
 
 
2681
 
 
2682
 
class BugMute(StormBase):
2683
 
    """Contains bugs a person has decided to block notifications from."""
2684
 
 
2685
 
    implements(IBugMute)
2686
 
 
2687
 
    __storm_table__ = "BugMute"
2688
 
 
2689
 
    def __init__(self, person=None, bug=None):
2690
 
        if person is not None:
2691
 
            self.person = person
2692
 
        if bug is not None:
2693
 
            self.bug_id = bug.id
2694
 
 
2695
 
    person_id = Int("person", allow_none=False, validator=validate_person)
2696
 
    person = Reference(person_id, "Person.id")
2697
 
 
2698
 
    bug_id = Int("bug", allow_none=False)
2699
 
    bug = Reference(bug_id, "Bug.id")
2700
 
 
2701
 
    __storm_primary__ = 'person_id', 'bug_id'
2702
 
 
2703
 
    date_created = DateTime(
2704
 
        "date_created", allow_none=False, default=UTC_NOW,
2705
 
        tzinfo=pytz.UTC)