~launchpad-pqm/launchpad/devel

18114.1.2 by Colin Watson
Implement the basics of bug linking for Git-based merge proposals.
1
# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
1564 by Canonical.com Patch Queue Manager
refactor BugFactory to have an API that communicates what it does (fixes https://launchpad.ubuntu.com/malone/bugs/133). rip out a big chunk of search code that was behind the anorak search screen. this screen now just redirects to the Malone homepage, of course, but if the redirect doesn't happen in time, code was being hit in the search() method of its view that was raising errors. the view class itself will be entirely ripped out in another round of refactoring (noted in XXX's).
4
"""Launchpad bug-related database table classes."""
5
6
__metaclass__ = type
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
7
4755.1.43 by Curtis Hovey
Revisions pre review.
8
__all__ = [
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
9
    'Bug',
8451.2.1 by Abel Deuring
Method added to retrieve a list of device owners who are affected by bugs
10
    'BugAffectsPerson',
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
11
    'BugBecameQuestionEvent',
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
12
    'BugMute',
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
13
    'BugSet',
8451.2.1 by Abel Deuring
Method added to retrieve a list of device owners who are affected by bugs
14
    'BugTag',
7675.553.35 by Deryck Hodge
Fix some import warnings.
15
    'FileBugData',
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
16
    'generate_subscription_with',
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
17
    'get_also_notified_subscribers',
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
18
    'get_bug_tags_open_count',
19
    ]
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
20
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
21
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
from cStringIO import StringIO
17390.2.2 by Colin Watson
Use the modern names for email subpackages, introduced in Python 2.5.
23
from email.utils import make_msgid
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
24
from functools import wraps
11869.18.11 by Gavin Panella
Merged subscribe-to-tag-bug-151129-5 into subscribe-to-tag-bug-151129-6, resolving 1 conflict.
25
from itertools import chain
3691.151.5 by kiko
Order sets, and remove obsolete use of sets.Set. Clean up some checks in dbschema.
26
import operator
27
import re
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
28
29
from lazr.lifecycle.event import (
30
    ObjectCreatedEvent,
31
    ObjectModifiedEvent,
32
    )
33
from lazr.lifecycle.snapshot import Snapshot
14027.3.2 by Jeroen Vermeulen
Merge devel, resolve conflicts.
34
import pytz
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
35
from sqlobject import (
36
    BoolCol,
37
    ForeignKey,
38
    IntCol,
39
    SQLMultipleJoin,
40
    SQLObjectNotFound,
41
    SQLRelatedJoin,
42
    StringCol,
43
    )
44
from storm.expr import (
45
    And,
17241.1.8 by William Grant
Bug.default_bugtask avoids inactive products where possible, fixing the /bugs/1234 redirect for normal users.
46
    Coalesce,
12775.3.1 by William Grant
Fix get_bug_tags_open_count to not retrieve EVERYTHING. Now returns a less unpleasant resultset instead.
47
    Desc,
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
48
    In,
14175.1.1 by William Grant
Use nested joins rather than subselects for preloading message parents. Fixes timeouts. Also removes nasty literal SQL strings. Because ew.
49
    Join,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
50
    LeftJoin,
51
    Max,
52
    Not,
53
    Or,
54
    Select,
55
    SQL,
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
56
    Sum,
13445.1.11 by Gary Poster
revert SQL change
57
    Union,
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
58
    With,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
59
    )
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
60
from storm.info import ClassAlias
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
61
from storm.locals import (
62
    DateTime,
63
    Int,
64
    Reference,
65
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
66
from storm.store import (
67
    EmptyResultSet,
68
    Store,
69
    )
70
from zope.component import getUtility
6061.2.6 by Maris Fogels
Updated more deprecated Zope interface references.
71
from zope.contenttype import guess_content_type
2454 by Canonical.com Patch Queue Manager
[r=stevea]. make bug notifictions concerning the same bug be part of the same email thread.
72
from zope.event import notify
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
73
from zope.interface import (
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
74
    implementer,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
75
    providedBy,
76
    )
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
77
from zope.security.interfaces import Unauthorized
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
78
from zope.security.proxy import (
79
    ProxyFactory,
80
    removeSecurityProxy,
81
    )
7876.3.5 by Francis J. Lacoste
Snapshot moved to lazr.lifecycle.
82
16048.1.1 by William Grant
format-imports
83
from lp.answers.interfaces.questiontarget import IQuestionTarget
15968.5.9 by Rick Harding
More file updates missed
84
from lp.app.enums import (
15968.5.4 by Rick Harding
more moving information type to app instead of registry since it's gone universal
85
    InformationType,
86
    PRIVATE_INFORMATION_TYPES,
87
    PROPRIETARY_INFORMATION_TYPES,
88
    SECURITY_INFORMATION_TYPES,
89
    ServiceUsage,
90
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
91
from lp.app.errors import (
92
    NotFoundError,
15365.1.1 by Steve Kowalik
Forbid open and delegated teams to be subscribed to private branches, and
93
    SubscriptionPrivacyViolation,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
94
    UserCannotUnsubscribePerson,
95
    )
16084.2.19 by Deryck Hodge
Bug and Branch implement IInformationType, so update the model
96
from lp.app.interfaces.informationtype import IInformationType
13130.1.6 by Curtis Hovey
Move ILaunchpadCelebrity to lp.app.
97
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
15162.1.28 by Ian Booth
Add job to remove subscriptions for users who can't see the bug after it changes info_type or is retargetted
98
from lp.app.interfaces.services import IService
7675.1347.4 by Aaron Bentley
Use InformationTypeMixin for Bug.
99
from lp.app.model.launchpad import InformationTypeMixin
12442.2.9 by j.c.sackett
Ran import reformatter per review.
100
from lp.app.validators import LaunchpadValidationError
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
101
from lp.bugs.adapters.bug import convert_to_information_type
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
102
from lp.bugs.adapters.bugchange import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
103
    BranchLinkedToBug,
104
    BranchUnlinkedFromBug,
105
    BugConvertedToQuestion,
14027.3.2 by Jeroen Vermeulen
Merge devel, resolve conflicts.
106
    BugDuplicateChange,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
107
    BugWatchAdded,
108
    BugWatchRemoved,
109
    SeriesNominated,
110
    UnsubscribedFromBug,
111
    )
14937.1.1 by Steve Kowalik
Every other enums module contains the s, force bugs into line.
112
from lp.bugs.enums import BugNotificationLevel
15866.1.2 by William Grant
Bug.transitionToInformationType now uses CannotChangeInformationType, rather than the misleading BugCannotBePrivate.
113
from lp.bugs.errors import InvalidDuplicateValue
14174.2.2 by Ian Booth
Lint
114
from lp.bugs.interfaces.bug import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
115
    IBug,
116
    IBugBecameQuestionEvent,
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
117
    IBugMute,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
118
    IBugSet,
119
    IFileBugData,
120
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
121
from lp.bugs.interfaces.bugactivity import IBugActivitySet
122
from lp.bugs.interfaces.bugattachment import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
123
    BugAttachmentType,
124
    IBugAttachmentSet,
125
    )
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
126
from lp.bugs.interfaces.bugmessage import IBugMessageSet
127
from lp.bugs.interfaces.bugnomination import (
14540.3.1 by Ian Booth
Allow a declined bug nomination to be re-nominated
128
    BugNominationStatus,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
129
    NominationError,
130
    NominationSeriesObsoleteError,
131
    )
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
132
from lp.bugs.interfaces.bugnotification import IBugNotificationSet
15761.2.9 by William Grant
Let CreateBugParams take an IBugTarget, rather than a product, distribution and sourcepackagename.
133
from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
134
from lp.bugs.interfaces.bugtask import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
135
    BugTaskStatus,
12845.2.6 by Robert Collins
Failing test for garbo migration to INCOMPLETE_WITH/WITHOUT_RESPONSE.
136
    BugTaskStatusSearch,
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
137
    IBugTask,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
138
    IBugTaskSet,
15761.2.11 by William Grant
Use IllegalTarget instead of AssertionError when calling createBug with an ISeriesBugTarget. It'll be exposed through the API shortly.
139
    IllegalTarget,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
140
    UNRESOLVED_BUGTASK_STATUSES,
141
    )
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
142
from lp.bugs.interfaces.bugtracker import BugTrackerType
143
from lp.bugs.interfaces.bugwatch import IBugWatchSet
144
from lp.bugs.interfaces.cve import ICveSet
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
145
from lp.bugs.interfaces.hasbug import IHasBug
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
146
from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
13955.1.1 by Graham Binns
Batched activity now no longer pulls in results that have already been shown.
147
from lp.bugs.model.bugactivity import BugActivity
7675.489.1 by Graham Binns
Bug.has_patches now uses a straight Storm query rather than looping over Bug.attachments.
148
from lp.bugs.model.bugattachment import BugAttachment
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
149
from lp.bugs.model.bugbranch import BugBranch
17910.1.8 by William Grant
Replace IObject(Created|Deleted)Events on IBugBranch with IObjectLinkedEvents on Bug.
150
from lp.bugs.model.buglinktarget import (
151
    ObjectLinkedEvent,
152
    ObjectUnlinkedEvent,
153
    )
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
154
from lp.bugs.model.bugmessage import BugMessage
155
from lp.bugs.model.bugnomination import BugNomination
156
from lp.bugs.model.bugnotification import BugNotification
157
from lp.bugs.model.bugsubscription import BugSubscription
7675.879.1 by Gavin Panella
Fix lint.
158
from lp.bugs.model.bugtarget import OfficialBugTag
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
159
from lp.bugs.model.bugtask import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
160
    BugTask,
161
    bugtask_sort_key,
162
    )
8631.1.1 by Gavin Panella
Fix up the messy imports before attemtping anything else.
163
from lp.bugs.model.bugwatch import BugWatch
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
164
from lp.bugs.model.structuralsubscription import (
14291.1.2 by Jeroen Vermeulen
Lint.
165
    get_structural_subscribers,
14494.6.23 by Gavin Panella
Update BugSubscriptionInfo.structural_* to use new function get_structural_subscriptions().
166
    get_structural_subscriptions,
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
167
    )
13277.4.6 by Graham Binns
Updated Bug.linked_branches to use the new linkedToBug() filter of BranchCollection.
168
from lp.code.interfaces.branchcollection import IAllBranches
18114.1.2 by Colin Watson
Implement the basics of bug linking for Git-based merge proposals.
169
from lp.code.interfaces.gitcollection import IAllGitRepositories
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
170
from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
15866.1.2 by William Grant
Bug.transitionToInformationType now uses CannotChangeInformationType, rather than the misleading BugCannotBePrivate.
171
from lp.registry.errors import CannotChangeInformationType
15162.1.20 by Ian Booth
Add feature flag and functionality to mirror legacy bug access
172
from lp.registry.interfaces.accesspolicy import (
173
    IAccessArtifactGrantSource,
174
    IAccessArtifactSource,
175
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
176
from lp.registry.interfaces.distribution import IDistribution
10054.26.1 by Adi Roiban
Refactor DistroSeriesStatus to SeriesStatus; Don't prompt for setting up translations for obsolete product series.
177
from lp.registry.interfaces.distroseries import IDistroSeries
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
178
from lp.registry.interfaces.person import (
179
    IPersonSet,
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
180
    validate_person,
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
181
    validate_public_person,
182
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
183
from lp.registry.interfaces.product import IProduct
184
from lp.registry.interfaces.productseries import IProductSeries
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
185
from lp.registry.interfaces.role import IPersonRoles
15365.1.1 by Steve Kowalik
Forbid open and delegated teams to be subscribed to private branches, and
186
from lp.registry.interfaces.series import SeriesStatus
15502.2.1 by Steve Kowalik
Brutally rename RemoveBugSubscriptionsJob and related gubbins to a more generic
187
from lp.registry.interfaces.sharingjob import (
188
    IRemoveArtifactSubscriptionsJobSource,
189
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
190
from lp.registry.interfaces.sourcepackage import ISourcePackage
15685.1.6 by William Grant
Drop unused imports.
191
from lp.registry.model.accesspolicy import reconcile_access_for_artifact
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
192
from lp.registry.model.person import (
193
    Person,
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
194
    person_sort_key,
11869.17.25 by Gavin Panella
Use PersonSet._getPrecachedPersons() because it's awesome.
195
    PersonSet,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
196
    )
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
197
from lp.registry.model.pillar import pillar_sort_key
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
198
from lp.registry.model.teammembership import TeamParticipation
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
199
from lp.services.config import config
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
200
from lp.services.database import bulk
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
201
from lp.services.database.constants import UTC_NOW
202
from lp.services.database.datetimecol import UtcDateTimeCol
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
203
from lp.services.database.decoratedresultset import DecoratedResultSet
14937.2.1 by Steve Kowalik
Add information_type to Bug, and set it for new and changed bugs.
204
from lp.services.database.enumcol import EnumCol
16675.1.1 by Steve Kowalik
Destroy all the callsites I could of IStoreSelector.get(), and kill lp.services.database.lpstorm, moving all of the interfaces to interfaces.
205
from lp.services.database.interfaces import IStore
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
206
from lp.services.database.sqlbase import (
207
    SQLBase,
208
    sqlvalues,
209
    )
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
210
from lp.services.database.stormbase import StormBase
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
211
from lp.services.fields import DuplicateBug
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
212
from lp.services.helpers import shortlist
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
213
from lp.services.librarian.interfaces import ILibraryFileAliasSet
214
from lp.services.librarian.model import (
215
    LibraryFileAlias,
216
    LibraryFileContent,
217
    )
13130.1.12 by Curtis Hovey
Sorted imports.
218
from lp.services.messages.interfaces.message import (
219
    IMessage,
220
    IndexedMessage,
221
    )
222
from lp.services.messages.model.message import (
223
    Message,
224
    MessageChunk,
225
    MessageSet,
226
    )
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
227
from lp.services.propertycache import (
228
    cachedproperty,
11789.2.3 by Gavin Panella
Remove all use of IPropertyCacheManager.
229
    clear_property_cache,
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
230
    get_property_cache,
11382.6.34 by Gavin Panella
Reformat imports in all files touched so far.
231
    )
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
232
from lp.services.webapp.authorization import check_permission
15685.1.6 by William Grant
Drop unused imports.
233
from lp.services.webapp.interfaces import ILaunchBag
16586.2.2 by Steve Kowalik
format-imports, and make SQL() no longer use string interpolation.
234
from lp.services.webapp.publisher import (
235
    get_raw_form_value_from_current_request,
236
    )
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
237
from lp.services.xref.interfaces import IXRefSet
3691.104.1 by Bjorn Tillenius
make it possible to show tags on open bugs, and to get the number of bugs using the tag, through getUsedBugTags.
238
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
239
12775.3.9 by William Grant
Document tag_count_columns hack.
240
def snapshot_bug_params(bug_params):
241
    """Return a snapshot of a `CreateBugParams` object."""
242
    return Snapshot(
243
        bug_params, names=[
244
            "owner", "title", "comment", "description", "msg",
15761.2.19 by William Grant
Add target to the CreateBugParams constructor, and remove the product/distribution/sourcepackagename attributes.
245
            "datecreated", "information_type", "target", "status",
246
            "subscribers", "tags", "subscribe_owner", "filed_by",
247
            "importance", "milestone", "assignee", "cve"])
12775.3.9 by William Grant
Document tag_count_columns hack.
248
249
250
class BugTag(SQLBase):
251
    """A tag belonging to a bug."""
252
253
    bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
254
    tag = StringCol(notNull=True)
255
256
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
257
def get_bug_tags_open_count(context_condition, user, tag_limit=0,
258
    include_tags=None):
259
    """Worker for IBugTarget.getUsedBugTagsWithOpenCounts.
260
261
    See `IBugTarget` for details.
262
263
    The only change is that this function takes a SQL expression for limiting
264
    the found tags.
7030.1.7 by Bjorn Tillenius
clean up.
265
    :param context_condition: A Storm SQL expression, limiting the
7675.1204.4 by Robert Collins
Repurpose unused _getBugTaskContectWhereClause to be a helper for linking to bugsummaries.
266
        used tags to a specific context. Only the BugSummary table may be
267
        used to choose the context. If False then no query will be performed
268
        (and {} returned).
3691.104.3 by Bjorn Tillenius
include the open bug counts in the bug tags portlet.
269
    """
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
270
    # Circular fail.
15685.1.4 by William Grant
Factor out bugsummary privacy filtering.
271
    from lp.bugs.model.bugsummary import (
272
        BugSummary,
273
        get_bugsummary_filter_for_user,
274
        )
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
275
    tags = {}
276
    if include_tags:
277
        tags = dict((tag, 0) for tag in include_tags)
7030.1.4 by Bjorn Tillenius
rewrite query generation.
278
    where_conditions = [
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
279
        BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
280
        BugSummary.tag != None,
7030.1.7 by Bjorn Tillenius
clean up.
281
        context_condition,
7030.1.4 by Bjorn Tillenius
rewrite query generation.
282
        ]
15685.1.4 by William Grant
Factor out bugsummary privacy filtering.
283
284
    # Apply the privacy filter.
285
    store = IStore(BugSummary)
286
    user_with, user_where = get_bugsummary_filter_for_user(user)
287
    if user_with:
288
        store = store.with_(user_with)
289
    where_conditions.extend(user_where)
290
13175.3.20 by Robert Collins
And exclude rows cancelled out by the journal.
291
    sum_count = Sum(BugSummary.count)
292
    tag_count_columns = (BugSummary.tag, sum_count)
13155.2.15 by Francis J. Lacoste
Lint blows but hoover sucks.
293
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
294
    # Always query for used
295
    def _query(*args):
296
        return store.find(tag_count_columns, *(where_conditions + list(args))
13175.3.20 by Robert Collins
And exclude rows cancelled out by the journal.
297
            ).group_by(BugSummary.tag).having(sum_count != 0).order_by(
13165.2.1 by Robert Collins
Change getUsedBugTagsWithOpenCounts to fit our usage better and teach it to use BugSummary.
298
            Desc(Sum(BugSummary.count)), BugSummary.tag)
299
    used = _query()
300
    if tag_limit:
301
        used = used[:tag_limit]
302
    if include_tags:
303
        # Union in a query for just include_tags.
304
        used = used.union(_query(BugSummary.tag.is_in(include_tags)))
305
    tags.update(dict(used))
306
    return tags
3691.40.10 by Bjorn Tillenius
add IBugTarget.getUsedBugTags.
307
3691.40.17 by Bjorn Tillenius
apply review comments.
308
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
309
@implementer(IBugBecameQuestionEvent)
4755.1.43 by Curtis Hovey
Revisions pre review.
310
class BugBecameQuestionEvent:
311
    """See `IBugBecameQuestionEvent`."""
312
313
    def __init__(self, bug, question, user):
314
        self.bug = bug
315
        self.question = question
316
        self.user = user
317
318
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
319
def update_bug_heat(bug_ids):
320
    """Update the heat for the specified bugs."""
321
    # We need to flush the store first to ensure that changes are
322
    # reflected in the new bug heat total.
323
    if not bug_ids:
324
        return
325
    store = IStore(Bug)
326
    store.find(
327
        Bug, Bug.id.is_in(bug_ids)).set(
328
            heat=SQL('calculate_bug_heat(Bug.id)'),
329
            heat_last_updated=UTC_NOW)
330
331
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
332
@implementer(IBug, IInformationType)
7675.1347.4 by Aaron Bentley
Use InformationTypeMixin for Bug.
333
class Bug(SQLBase, InformationTypeMixin):
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
334
    """A bug."""
335
336
    _defaultOrder = '-id'
337
338
    # db field names
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
339
    name = StringCol(unique=True, default=None)
340
    title = StringCol(notNull=True)
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
341
    description = StringCol(notNull=False, default=None)
5485.1.17 by Edwin Grubbs
Fixed indentation
342
    owner = ForeignKey(
343
        dbName='owner', foreignKey='Person',
5821.2.40 by James Henstridge
* Move all the uses of public_person_validator over to the Storm
344
        storm_validator=validate_public_person, notNull=True)
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
345
    duplicateof = ForeignKey(
346
        dbName='duplicateof', foreignKey='Bug', default=None)
1650.1.2 by James Henstridge
commit the first part of the timezone awareness code
347
    datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
3496.2.1 by Brad Bollenbach
checkpoint
348
    date_last_updated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
4240.1.1 by Gavin Panella
Add new columns to bug table to record who and when made the bug private, and corresponding SQLObject descriptors
349
    date_made_private = UtcDateTimeCol(notNull=False, default=None)
350
    who_made_private = ForeignKey(
5485.1.13 by Edwin Grubbs
Sorta working
351
        dbName='who_made_private', foreignKey='Person',
5821.2.40 by James Henstridge
* Move all the uses of public_person_validator over to the Storm
352
        storm_validator=validate_public_person, default=None)
14937.2.1 by Steve Kowalik
Add information_type to Bug, and set it for new and changed bugs.
353
    information_type = EnumCol(
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
354
        enum=InformationType, notNull=True, default=InformationType.PUBLIC)
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
355
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
356
    # useful Joins
3226.2.1 by Diogo Matsubara
Fix https://launchpad.net/products/launchpad/+bug/33625 (Change MultipleJoin to use the new SQLMultipleJoin.)
357
    activity = SQLMultipleJoin('BugActivity', joinColumn='bug', orderBy='id')
3504.1.13 by kiko
Implement initial SQLRelatedJoin migration across Launchpad tree. Still needs to reconsider the Snapshot approach which will be a big performance hit.
358
    messages = SQLRelatedJoin('Message', joinColumn='bug',
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
359
                           otherColumn='message',
2225 by Canonical.com Patch Queue Manager
SMASH bug page fixes into rocketfuel [r=stevea]
360
                           intermediateTable='BugMessage',
3504.1.28 by kiko
Remove two XXXs in bug.py that referred to already-fixed SQLObject bugs, and prejoin owner for messages to avoid us hitting the database for each message. The latter should help with bug 42755: Optimization needed for bug comments queries -- though probably not fix it.
361
                           prejoins=['owner'],
3691.320.3 by Bjorn Tillenius
support more than one inline part, which will be added as comments.
362
                           orderBy=['datecreated', 'id'])
8137.17.24 by Barry Warsaw
thread merge
363
    bug_messages = SQLMultipleJoin(
12346.2.2 by Robert Collins
Change all BugMessage object creation to set the index. This involved
364
        'BugMessage', joinColumn='bug', orderBy='index')
3226.2.1 by Diogo Matsubara
Fix https://launchpad.net/products/launchpad/+bug/33625 (Change MultipleJoin to use the new SQLMultipleJoin.)
365
    watches = SQLMultipleJoin(
3063.2.4 by Bjorn Tillenius
initial support for adding a bug watch together with an upstream task.
366
        'BugWatch', joinColumn='bug', orderBy=['bugtracker', 'remotebug'])
16501.1.1 by Steve Kowalik
Teach IBug.specifications about visible specifications, which can be done with impunity because the API does not know about it.
367
    duplicates = SQLMultipleJoin('Bug', joinColumn='duplicateof', orderBy='id')
17910.1.1 by William Grant
Rename Bug.linked_branches to linked_bugbranches.
368
    linked_bugbranches = SQLMultipleJoin(
13277.4.18 by Graham Binns
Added accessor_for goodness.
369
        'BugBranch', joinColumn='bug', orderBy='id')
4895.3.1 by Tom Berger
an optimization for the incomplete bug search - cache the last message date for each bug in the column `date_last_message`.
370
    date_last_message = UtcDateTimeCol(default=None)
5238.2.1 by Tom Berger
allow sorting bugtask search results by the number of duplicates
371
    number_of_duplicates = IntCol(notNull=True, default=0)
5453.4.8 by Tom Berger
merge changes from rocketfuel and number_of_comments --> message_count
372
    message_count = IntCol(notNull=True, default=0)
7030.5.1 by Tom Berger
model for bug affects user
373
    users_affected_count = IntCol(notNull=True, default=0)
7106.1.1 by Tom Berger
record both affected an unaffected users
374
    users_unaffected_count = IntCol(notNull=True, default=0)
7675.465.4 by Karl Fogel
* lib/lp/bugs/model/bug.py (Bug.heat): Refer to correct column name 'heat'
375
    heat = IntCol(notNull=True, default=0)
7675.582.4 by Graham Binns
Updated tests for heat_last_updated.
376
    heat_last_updated = UtcDateTimeCol(default=None)
10224.18.1 by Abel Deuring
Property latest_patch_uploaded added to classes IBug and Bug; doc tests of this property
377
    latest_patch_uploaded = UtcDateTimeCol(default=None)
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
378
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
379
    @property
17910.1.1 by William Grant
Rename Bug.linked_branches to linked_bugbranches.
380
    def linked_branches(self):
381
        return [link.branch for link in self.linked_bugbranches]
382
383
    @property
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
384
    def cves(self):
385
        from lp.bugs.model.cve import Cve
17746.4.1 by William Grant
Remove non-XRef buglink model code.
386
        xref_cve_sequences = [
387
            sequence for _, sequence in getUtility(IXRefSet).findFrom(
388
                (u'bug', unicode(self.id)), types=[u'cve'])]
389
        expr = Cve.sequence.is_in(xref_cve_sequences)
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
390
        return list(sorted(
17746.3.7 by William Grant
Only read bug links from XRef when a feature flag is enabled.
391
            IStore(Cve).find(Cve, expr), key=operator.attrgetter('sequence')))
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
392
393
    @property
394
    def questions(self):
395
        from lp.answers.model.question import Question
17746.4.1 by William Grant
Remove non-XRef buglink model code.
396
        question_ids = [
397
            int(id) for _, id in getUtility(IXRefSet).findFrom(
398
                (u'bug', unicode(self.id)), types=[u'question'])]
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
399
        return list(sorted(
17746.3.7 by William Grant
Only read bug links from XRef when a feature flag is enabled.
400
            bulk.load(Question, question_ids), key=operator.attrgetter('id')))
17746.3.1 by William Grant
Basic port of QuestionBug/BugCve to XRef.
401
17746.3.2 by William Grant
SpecificationBugs create XRefs too.
402
    @property
403
    def specifications(self):
404
        from lp.blueprints.model.specification import Specification
17746.4.1 by William Grant
Remove non-XRef buglink model code.
405
        spec_ids = [
406
            int(id) for _, id in getUtility(IXRefSet).findFrom(
407
                (u'bug', unicode(self.id)), types=[u'specification'])]
17746.3.2 by William Grant
SpecificationBugs create XRefs too.
408
        return list(sorted(
17746.3.7 by William Grant
Only read bug links from XRef when a feature flag is enabled.
409
            bulk.load(Specification, spec_ids), key=operator.attrgetter('id')))
17746.3.2 by William Grant
SpecificationBugs create XRefs too.
410
16501.1.2 by Steve Kowalik
Rename IBug.specifications to IBug.getSpecifications(), actually teach the interface about it, add another test after refactoring.
411
    def getSpecifications(self, user):
412
        """See `IBug`."""
17746.3.2 by William Grant
SpecificationBugs create XRefs too.
413
        from lp.blueprints.model.specification import Specification
414
        from lp.blueprints.model.specificationsearch import (
415
            get_specification_privacy_filter,
416
            )
417
        return IStore(Specification).find(
16501.1.1 by Steve Kowalik
Teach IBug.specifications about visible specifications, which can be done with impunity because the API does not know about it.
418
            Specification,
17746.3.2 by William Grant
SpecificationBugs create XRefs too.
419
            Specification.id.is_in(spec.id for spec in self.specifications),
16501.1.3 by Steve Kowalik
Rewrite IBug.getSpecifications() to not use a Join.
420
            *get_specification_privacy_filter(user))
16501.1.1 by Steve Kowalik
Teach IBug.specifications about visible specifications, which can be done with impunity because the API does not know about it.
421
15008.1.1 by Steve Kowalik
Resurrect the changes to use IBug.information_type, set information_type in
422
    @property
423
    def security_related(self):
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
424
        return self.information_type in SECURITY_INFORMATION_TYPES
15008.1.1 by Steve Kowalik
Resurrect the changes to use IBug.information_type, set information_type in
425
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
426
    @cachedproperty
427
    def _subscriber_cache(self):
428
        """Caches known subscribers."""
429
        return set()
430
431
    @cachedproperty
432
    def _subscriber_dups_cache(self):
433
        """Caches known subscribers to dupes."""
434
        return set()
435
436
    @cachedproperty
437
    def _unsubscribed_cache(self):
438
        """Cache known non-subscribers."""
439
        return set()
440
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
441
    @property
7675.517.1 by Abel Deuring
property Bug.latest_patch added; property used to display details abotu a patch on the +patches view.
442
    def latest_patch(self):
443
        """See `IBug`."""
444
        # We want to retrieve the most recently added bug attachment
445
        # that is of type BugAttachmentType.PATCH. In order to find
446
        # this attachment, we should in theory sort by
447
        # BugAttachment.message.datecreated. Since we don't have
448
        # an index for Message.datecreated, such a query would be
449
        # quite slow. We search instead for the BugAttachment with
450
        # the largest ID for a given bug. This is "nearly" equivalent
451
        # to searching the record with the maximum value of
452
        # message.datecreated: The only exception is the rare case when
453
        # two BugAttachment records are simultaneuosly added to the same
454
        # bug, where bug_attachment_1.id < bug_attachment_2.id, while
455
        # the Message record for bug_attachment_2 is created before
456
        # the Message record for bug_attachment_1. The difference of
7675.517.2 by Abel Deuring
improved tests of Bug.latest_patch; renamed one-character TAL variable; fixed a few typos
457
        # the datecreated values of the Message records is in this case
7675.517.1 by Abel Deuring
property Bug.latest_patch added; property used to display details abotu a patch on the +patches view.
458
        # probably smaller than one second and the selection of the
459
        # "most recent" patch anyway somewhat arbitrary.
460
        return Store.of(self).find(
461
            BugAttachment, BugAttachment.id == Select(
462
                Max(BugAttachment.id),
463
                And(BugAttachment.bug == self.id,
464
                    BugAttachment.type == BugAttachmentType.PATCH))).one()
465
466
    @property
8279.3.12 by Graham Binns
Fixed comment counts so that initial comments aren't included.
467
    def comment_count(self):
468
        """See `IBug`."""
469
        return self.message_count - 1
470
471
    @property
7881.5.1 by Tom Berger
make it possible to get the collection of users affected by a bug
472
    def users_affected(self):
473
        """See `IBug`."""
10015.2.2 by Gavin Panella
Get the list of affected users with a single query.
474
        return Store.of(self).find(
475
            Person, BugAffectsPerson.person == Person.id,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
476
            BugAffectsPerson.affected, BugAffectsPerson.bug == self)
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
477
478
    @property
479
    def users_unaffected(self):
480
        """See `IBug`."""
481
        return Store.of(self).find(
482
            Person, BugAffectsPerson.person == Person.id,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
483
            Not(BugAffectsPerson.affected), BugAffectsPerson.bug == self)
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
484
10193.3.2 by Karl Fogel
Some tweaks resulting from informal review during Bugs Sprint.
485
    @property
486
    def user_ids_affected_with_dupes(self):
487
        """Return all IDs of Persons affected by this bug and its dupes.
13445.1.11 by Gary Poster
revert SQL change
488
        The return value is a Storm expression.  Running a query with
489
        this expression returns a result that may contain the same ID
490
        multiple times, for example if that person is affected via
491
        more than one duplicate."""
492
        return Union(
493
            Select(Person.id,
494
                   And(BugAffectsPerson.person == Person.id,
495
                       BugAffectsPerson.affected,
496
                       BugAffectsPerson.bug == self)),
497
            Select(Person.id,
498
                   And(BugAffectsPerson.person == Person.id,
499
                       BugAffectsPerson.bug == Bug.id,
500
                       BugAffectsPerson.affected,
501
                       Bug.duplicateof == self.id)))
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
502
503
    @property
10193.3.2 by Karl Fogel
Some tweaks resulting from informal review during Bugs Sprint.
504
    def users_affected_with_dupes(self):
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
505
        """See `IBug`."""
506
        return Store.of(self).find(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
507
            Person, Person.id.is_in(self.user_ids_affected_with_dupes))
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
508
509
    @property
10193.3.2 by Karl Fogel
Some tweaks resulting from informal review during Bugs Sprint.
510
    def users_affected_count_with_dupes(self):
10193.3.1 by Karl Fogel
Fix bug #505845 ("Affected users should be carried through from
511
        """See `IBug`."""
10193.3.3 by Karl Fogel
With Abel, take care of an "XXX" item about scalability.
512
        return self.users_affected_with_dupes.count()
7881.5.1 by Tom Berger
make it possible to get the collection of users affected by a bug
513
514
    @property
14189.6.9 by mbp at canonical
Add other_users_affected_count_with_dupes to get the right answers when the current user is affected by a dupe
515
    def other_users_affected_count_with_dupes(self):
516
        """See `IBug`."""
517
        current_user = getUtility(ILaunchBag).user
518
        if not current_user:
519
            return self.users_affected_count_with_dupes
520
        return self.users_affected_with_dupes.find(
521
            Person.id != current_user.id).count()
522
523
    @property
7029.4.1 by Tom Berger
provide an efficient implementation of the canonical url for messages by decorating messages with their index and context.
524
    def indexed_messages(self):
525
        """See `IMessageTarget`."""
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
526
        # Note that this is a decorated result set, so will cache its
527
        # value (in the absence of slices)
11544.1.6 by Robert Collins
review feedback.
528
        return self._indexed_messages(include_content=True)
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
529
12756.1.1 by William Grant
Revert r12754. It causes parent_link to be empty in the API, apparently untested.
530
    def _indexed_messages(self, include_content=False, include_parents=True):
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
531
        """Get the bugs messages, indexed.
532
11544.1.6 by Robert Collins
review feedback.
533
        :param include_content: If True retrieve the content for the messages
534
            too.
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
535
        :param include_parents: If True retrieve the object for parent
536
            messages too. If False the parent attribute will be *forced* to
537
            None to reduce database lookups.
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
538
        """
539
        # Make all messages be 'in' the main bugtask.
7675.282.6 by Gavin Panella
Make tests pass.
540
        inside = self.default_bugtask
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
541
        store = Store.of(self)
542
        message_by_id = {}
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
543
        to_messages = lambda rows: [row[0] for row in rows]
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
544
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
545
        def eager_load_owners(messages):
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
546
            # Because we may have multiple owners, we spend less time
547
            # in storm with very large bugs by not joining and instead
548
            # querying a second time. If this starts to show high db
549
            # time, we can left outer join instead.
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
550
            owner_ids = set(message.ownerID for message in messages)
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
551
            owner_ids.discard(None)
552
            if not owner_ids:
553
                return
554
            list(store.find(Person, Person.id.is_in(owner_ids)))
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
555
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
556
        def eager_load_content(messages):
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
557
            # To avoid the complexity of having multiple rows per
558
            # message, or joining in the database (though perhaps in
559
            # future we should do that), we do a single separate query
560
            # for the message content.
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
561
            message_ids = set(message.id for message in messages)
11544.1.6 by Robert Collins
review feedback.
562
            chunks = store.find(
563
                MessageChunk, MessageChunk.messageID.is_in(message_ids))
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
564
            chunks.order_by(MessageChunk.id)
565
            chunk_map = {}
566
            for chunk in chunks:
567
                message_chunks = chunk_map.setdefault(chunk.messageID, [])
568
                message_chunks.append(chunk)
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
569
            for message in messages:
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
570
                if message.id not in chunk_map:
571
                    continue
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
572
                cache = get_property_cache(message)
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
573
                cache.text_contents = Message.chunks_text(
574
                    chunk_map[message.id])
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
575
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
576
        def eager_load(rows):
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
577
            messages = to_messages(rows)
578
            eager_load_owners(messages)
11544.1.6 by Robert Collins
review feedback.
579
            if include_content:
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
580
                eager_load_content(messages)
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
581
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
582
        def index_message(row):
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
583
            # convert row to an IndexedMessage
11544.1.6 by Robert Collins
review feedback.
584
            if include_parents:
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
585
                message, parent, bugmessage = row
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
586
                if parent is not None:
587
                    # If there is an IndexedMessage available as parent, use
588
                    # that to reduce on-demand parent lookups.
589
                    parent = message_by_id.get(parent.id, parent)
590
            else:
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
591
                message, bugmessage = row
13023.7.12 by Danilo Segan
Lint fixes.
592
                parent = None  # parent attribute is not going to be accessed.
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
593
            index = bugmessage.index
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
594
            result = IndexedMessage(message, inside, index, parent)
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
595
            if include_parents:
596
                # This message may be the parent for another: stash it to
597
                # permit use.
598
                message_by_id[message.id] = result
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
599
            return result
11544.1.6 by Robert Collins
review feedback.
600
        if include_parents:
14175.1.1 by William Grant
Use nested joins rather than subselects for preloading message parents. Fixes timeouts. Also removes nasty literal SQL strings. Because ew.
601
            ParentMessage = ClassAlias(Message)
602
            ParentBugMessage = ClassAlias(BugMessage)
603
            tables = [
604
                Message,
605
                Join(
606
                    BugMessage,
607
                    BugMessage.messageID == Message.id),
608
                LeftJoin(
609
                    Join(
610
                        ParentMessage,
611
                        ParentBugMessage,
612
                        ParentMessage.id == ParentBugMessage.messageID),
613
                    And(
614
                        Message.parent == ParentMessage.id,
615
                        ParentBugMessage.bugID == self.id)),
616
                ]
617
            results = store.using(*tables).find(
618
                (Message, ParentMessage, BugMessage),
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
619
                BugMessage.bugID == self.id,
620
                )
621
        else:
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
622
            lookup = Message, BugMessage
12262.2.1 by Robert Collins
Add a garbo job to populate BugMessage.index, fixing bug 704446.
623
            results = store.find(lookup,
11544.1.5 by Robert Collins
Skip parents for the attachments use of _indexed_messages - its not needed and saves some time.
624
                BugMessage.bugID == self.id,
625
                BugMessage.messageID == Message.id,
626
                )
12415.5.1 by Robert Collins
Use simpler sort in Bug._indexed_messages now that index is fully populated. Saves 90% on some queries.
627
        results.order_by(BugMessage.index)
11544.1.4 by Robert Collins
Remove listification from bugs/messages API call, so slicing actually can do less work.
628
        return DecoratedResultSet(results, index_message,
12366.4.1 by Robert Collins
Remove the temporary bug message indexing code and simplify the message indexing logic as a result.
629
            pre_iter_hook=eager_load)
7029.4.1 by Tom Berger
provide an efficient implementation of the canonical url for messages by decorating messages with their index and context.
630
631
    @property
2450 by Canonical.com Patch Queue Manager
[r=jamesh] rework cve structure, and general polish
632
    def displayname(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
633
        """See `IBug`."""
2450 by Canonical.com Patch Queue Manager
[r=jamesh] rework cve structure, and general polish
634
        dn = 'Bug #%d' % self.id
635
        if self.name:
13163.1.2 by Brad Crittenden
Fixed lint
636
            dn += ' (' + self.name + ')'
2450 by Canonical.com Patch Queue Manager
[r=jamesh] rework cve structure, and general polish
637
        return dn
553 by Canonical.com Patch Queue Manager
renaming phase 2
638
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
639
    @cachedproperty
2252 by Canonical.com Patch Queue Manager
add cve report on distribution [r=stevea]
640
    def bugtasks(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
641
        """See `IBug`."""
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
642
        # \o/ circular imports.
643
        from lp.registry.model.distribution import Distribution
644
        from lp.registry.model.distroseries import DistroSeries
645
        from lp.registry.model.product import Product
646
        from lp.registry.model.productseries import ProductSeries
647
        from lp.registry.model.sourcepackagename import SourcePackageName
648
        store = Store.of(self)
649
        tasks = list(store.find(BugTask, BugTask.bugID == self.id))
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
650
        # The bugtasks attribute is iterated in the API and web
651
        # services, so it needs to preload all related data otherwise
652
        # late evaluation is triggered in both places. Separately,
653
        # bugtask_sort_key requires the related products, series,
654
        # distros, distroseries and source package names to be loaded.
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
655
        ids = set(map(operator.attrgetter('assigneeID'), tasks))
656
        ids.update(map(operator.attrgetter('ownerID'), tasks))
657
        ids.discard(None)
658
        if ids:
12482.1.3 by Robert Collins
Eager load bugwatches and validity for assignees.
659
            list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
660
                ids, need_validity=True))
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
661
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
662
        def load_something(attrname, klass):
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
663
            ids = set(map(operator.attrgetter(attrname), tasks))
664
            ids.discard(None)
665
            if not ids:
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
666
                return
12482.1.6 by Robert Collins
IDS -> ids and drop unneeded ILaunchBag import.
667
            list(store.find(klass, klass.id.is_in(ids)))
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
668
        load_something('productID', Product)
669
        load_something('productseriesID', ProductSeries)
670
        load_something('distributionID', Distribution)
671
        load_something('distroseriesID', DistroSeries)
672
        load_something('sourcepackagenameID', SourcePackageName)
12482.1.3 by Robert Collins
Eager load bugwatches and validity for assignees.
673
        list(store.find(BugWatch, BugWatch.bugID == self.id))
12482.1.1 by Robert Collins
Eager load related fields for bugs when executing bug.bugtasks.
674
        return sorted(tasks, key=bugtask_sort_key)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
675
2454 by Canonical.com Patch Queue Manager
[r=stevea]. make bug notifictions concerning the same bug be part of the same email thread.
676
    @property
6887.5.12 by Gavin Panella
Rename IBug.first_bugtask to default_bugtask.
677
    def default_bugtask(self):
6887.5.11 by Gavin Panella
New IBug.first_bugtask attribute.
678
        """See `IBug`."""
17241.1.8 by William Grant
Bug.default_bugtask avoids inactive products where possible, fixing the /bugs/1234 redirect for normal users.
679
        from lp.registry.model.product import Product
680
        return Store.of(self).using(
681
                BugTask,
682
                LeftJoin(Product, Product.id == BugTask.productID)
683
            ).find(
684
                BugTask, bug=self
685
            ).order_by(
686
                Desc(Coalesce(Product.active, True)), BugTask.id).first()
6887.5.11 by Gavin Panella
New IBug.first_bugtask attribute.
687
688
    @property
3691.436.22 by Mark Shuttleworth
Clean up mentoring text and templates for 1.0 UI
689
    def is_complete(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
690
        """See `IBug`."""
3691.436.22 by Mark Shuttleworth
Clean up mentoring text and templates for 1.0 UI
691
        for task in self.bugtasks:
3691.436.31 by Mark Shuttleworth
Fix implementation of bug completeness test
692
            if not task.is_complete:
693
                return False
694
        return True
3691.436.22 by Mark Shuttleworth
Clean up mentoring text and templates for 1.0 UI
695
3691.436.58 by Mark Shuttleworth
Test fixes
696
    @property
3847.2.30 by Mark Shuttleworth
Eliminate components/bugtask.py and polish bug listing portlets
697
    def affected_pillars(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
698
        """See `IBug`."""
3847.2.30 by Mark Shuttleworth
Eliminate components/bugtask.py and polish bug listing portlets
699
        result = set()
700
        for task in self.bugtasks:
701
            result.add(task.pillar)
702
        return sorted(result, key=pillar_sort_key)
3847.2.1 by Mark Shuttleworth
Neaten up bug listing portlets
703
704
    @property
5020.3.9 by Curtis Hovey
Revisions per review.
705
    def permits_expiration(self):
706
        """See `IBug`.
707
708
        This property checks the general state of the bug to determine if
709
        expiration is permitted *if* a bugtask were to qualify for expiration.
710
        This property does not check the bugtask preconditions to identify
711
        a specific bugtask that can expire.
712
713
        :See: `IBug.can_expire` or `BugTaskSet.findExpirableBugTasks` to
714
            check or get a list of bugs that can expire.
715
        """
716
        # Bugs cannot be expired if any bugtask is valid.
717
        expirable_status_list = [
718
            BugTaskStatus.INCOMPLETE, BugTaskStatus.INVALID,
719
            BugTaskStatus.WONTFIX]
5020.3.10 by Curtis Hovey
Changes per review.
720
        has_an_expirable_bugtask = False
721
        for bugtask in self.bugtasks:
722
            if bugtask.status not in expirable_status_list:
723
                # We found an unexpirable bugtask; the bug cannot expire.
724
                return False
725
            if (bugtask.status == BugTaskStatus.INCOMPLETE
5283.1.2 by Curtis Hovey
Revised the can_expire code parts to honor enable_bug_expiration. Added
726
                and bugtask.pillar.enable_bug_expiration):
5020.3.10 by Curtis Hovey
Changes per review.
727
                # This bugtasks meets the basic conditions to expire.
728
                has_an_expirable_bugtask = True
729
730
        return has_an_expirable_bugtask
5020.3.9 by Curtis Hovey
Revisions per review.
731
732
    @property
5020.3.1 by Curtis Hovey
Basic can_expire property is added. bugtask-expiration needs revision to show it off.
733
    def can_expire(self):
734
        """See `IBug`.
735
736
        Only Incomplete bug reports that affect a single pillar with
5020.3.9 by Curtis Hovey
Revisions per review.
737
        enabled_bug_expiration set to True can be expired. To qualify for
738
        expiration, the bug and its bugtasks meet the follow conditions:
739
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
740
        1. The bug is inactive; the last update of the bug is older than
5020.3.9 by Curtis Hovey
Revisions per review.
741
            Launchpad expiration age.
742
        2. The bug is not a duplicate.
743
        3. The bug has at least one message (a request for more information).
744
        4. The bug does not have any other valid bugtasks.
5283.1.2 by Curtis Hovey
Revised the can_expire code parts to honor enable_bug_expiration. Added
745
        5. The bugtask belongs to a project with enable_bug_expiration set
746
           to True.
5020.3.9 by Curtis Hovey
Revisions per review.
747
        6. The bugtask has the status Incomplete.
748
        7. The bugtask is not assigned to anyone.
749
        8. The bugtask does not have a milestone.
5020.3.1 by Curtis Hovey
Basic can_expire property is added. bugtask-expiration needs revision to show it off.
750
        """
5020.3.6 by Curtis Hovey
Added pagetest and UI for expiration notices. We *really* need to replace the
751
        # IBugTaskSet.findExpirableBugTasks() is the authoritative determiner
5020.3.9 by Curtis Hovey
Revisions per review.
752
        # if a bug can expire, but it is expensive. We do a general check
753
        # to verify the bug permits expiration before using IBugTaskSet to
754
        # determine if a bugtask can cause expiration.
755
        if not self.permits_expiration:
5020.3.1 by Curtis Hovey
Basic can_expire property is added. bugtask-expiration needs revision to show it off.
756
            return False
5020.3.9 by Curtis Hovey
Revisions per review.
757
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
758
        days_old = config.malone.days_before_expiration
5565.6.5 by Bjorn Tillenius
clarify comment.
759
        # Do the search as the Janitor, to ensure that this bug can be
760
        # found, even if it's private. We don't have access to the user
761
        # calling this property. If the user has access to view this
17903.1.1 by Colin Watson
Use gender-neutral pronouns where appropriate.
762
        # property, they have permission to see the bug, so we're not
5565.6.5 by Bjorn Tillenius
clarify comment.
763
        # exposing something we shouldn't. The Janitor has access to
764
        # view all bugs.
5565.6.3 by Bjorn Tillenius
make the user parameter to findExpirableBugtasks() required. make all callsites specify it.
765
        bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks(
11057.8.2 by Brian Murray
modify can_expire to use the days_before_expiration config option
766
            days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self)
16597.1.1 by Steve Kowalik
Dump .count() > 0 for the new black of .is_empty().
767
        return not bugtasks.is_empty()
5020.3.1 by Curtis Hovey
Basic can_expire property is added. bugtask-expiration needs revision to show it off.
768
11057.8.1 by Brian Murray
create IBug.isExpirable() which is hopefully clearer than IBug.can_expire and export it via the API
769
    def isExpirable(self, days_old=None):
770
        """See `IBug`."""
771
772
        # If days_old is None read it from the Launchpad configuration
773
        # and use that value
774
        if days_old is None:
775
            days_old = config.malone.days_before_expiration
776
777
        # IBugTaskSet.findExpirableBugTasks() is the authoritative determiner
778
        # if a bug can expire, but it is expensive. We do a general check
779
        # to verify the bug permits expiration before using IBugTaskSet to
780
        # determine if a bugtask can cause expiration.
781
        if not self.permits_expiration:
782
            return False
783
784
        # Do the search as the Janitor, to ensure that this bug can be
785
        # found, even if it's private. We don't have access to the user
786
        # calling this property. If the user has access to view this
17903.1.1 by Colin Watson
Use gender-neutral pronouns where appropriate.
787
        # property, they have permission to see the bug, so we're not
11057.8.1 by Brian Murray
create IBug.isExpirable() which is hopefully clearer than IBug.can_expire and export it via the API
788
        # exposing something we shouldn't. The Janitor has access to
789
        # view all bugs.
790
        bugtasks = getUtility(IBugTaskSet).findExpirableBugTasks(
791
            days_old, getUtility(ILaunchpadCelebrities).janitor, bug=self)
16597.1.1 by Steve Kowalik
Dump .count() > 0 for the new black of .is_empty().
792
        return not bugtasks.is_empty()
11057.8.1 by Brian Murray
create IBug.isExpirable() which is hopefully clearer than IBug.can_expire and export it via the API
793
12655.6.2 by Gary Poster
readd the bug and person optimizations, trying to follow the advice Robert gave; cache the initial_message because we were getting it from the SQL twice.
794
    @cachedproperty
2454 by Canonical.com Patch Queue Manager
[r=stevea]. make bug notifictions concerning the same bug be part of the same email thread.
795
    def initial_message(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
796
        """See `IBug`."""
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
797
        return Store.of(self).find(
798
            Message, BugMessage.bug == self,
799
            BugMessage.message == Message.id).order_by('id').first()
2454 by Canonical.com Patch Queue Manager
[r=stevea]. make bug notifictions concerning the same bug be part of the same email thread.
800
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
801
    @cachedproperty
802
    def official_tags(self):
803
        """See `IBug`."""
804
        # Da circle of imports forces the locals.
805
        from lp.registry.model.distribution import Distribution
806
        from lp.registry.model.product import Product
807
        table = OfficialBugTag
808
        table = LeftJoin(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
809
            table, Distribution,
13163.1.2 by Brad Crittenden
Fixed lint
810
            OfficialBugTag.distribution_id == Distribution.id)
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
811
        table = LeftJoin(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
812
            table, Product, OfficialBugTag.product_id == Product.id)
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
813
        # When this method is typically called it already has the necessary
814
        # info in memory, so rather than rejoin with Product etc, we do this
815
        # bit in Python. If reviewing performance here feel free to change.
816
        clauses = []
817
        for task in self.bugtasks:
11582.2.5 by Robert Collins
Fix up test fallout.
818
            clauses.append(
819
                # Storm cannot compile proxied objects.
820
                removeSecurityProxy(task.target._getOfficialTagClause()))
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
821
        clause = Or(*clauses)
822
        return list(Store.of(self).using(table).find(OfficialBugTag.tag,
823
            clause).order_by(OfficialBugTag.tag).config(distinct=True))
824
2070 by Canonical.com Patch Queue Manager
[r=salgado] FormattingBugNotifications implementation. requires some
825
    def followup_subject(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
826
        """See `IBug`."""
13163.1.2 by Brad Crittenden
Fixed lint
827
        return 'Re: ' + self.title
1228 by Canonical.com Patch Queue Manager
small bugfixes and a first go at a db schema patch for bug group
828
10189.4.1 by Tom Berger
interim commit, so that i can merge in another branch
829
    @property
830
    def has_patches(self):
831
        """See `IBug`."""
10304.6.2 by Tom Berger
Use the new Bug.latest_patch_uploaded column to optimize searching for bugs with patches.
832
        return self.latest_patch_uploaded is not None
10189.4.1 by Tom Berger
interim commit, so that i can merge in another branch
833
11688.1.3 by Graham Binns
It's now possible to subscribe at a given BugNotificationLevel.
834
    def subscribe(self, person, subscribed_by, suppress_notify=True,
11688.1.9 by Graham Binns
Minor tweak.
835
                  level=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
836
        """See `IBug`."""
14449.6.1 by Curtis Hovey
Remove isTeam(). Replace calls with .is_team.
837
        if person.is_team and self.private and person.anyone_can_join():
14188.2.10 by j.c.sackett
Added method to check if team is open to person class.
838
            error_msg = ("Open and delegated teams cannot be subscribed "
839
                "to private bugs.")
840
            raise SubscriptionPrivacyViolation(error_msg)
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
841
        # first look for an existing subscription
842
        for sub in self.subscriptions:
843
            if sub.person.id == person.id:
12556.11.1 by Gary Poster
initial cut of direct actions
844
                if level is not None:
845
                    sub.bug_notification_level = level
846
                    # Should subscribed_by be changed in this case?  Until
847
                    # proven otherwise, we will answer with "no."
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
848
                return sub
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
849
12556.11.1 by Gary Poster
initial cut of direct actions
850
        if level is None:
851
            level = BugNotificationLevel.COMMENTS
852
6105.11.1 by Tom Berger
notify users when they are being subscribed to a bug
853
        sub = BugSubscription(
11688.1.3 by Graham Binns
It's now possible to subscribe at a given BugNotificationLevel.
854
            bug=self, person=person, subscribed_by=subscribed_by,
855
            bug_notification_level=level)
10606.7.4 by Deryck Hodge
Make sending notifications configurable via a parameter.
856
5821.2.47 by James Henstridge
make sure bug subscribe/unsubscribe gets flushed to the DB
857
        # Ensure that the subscription has been flushed.
5821.11.13 by James Henstridge
Do an explicit flush in Bug.subscribe() to fix doc/security-teams.txt.
858
        Store.of(sub).flush()
10795.5.1 by Deryck Hodge
Merging in work from production-devel branch to prevent
859
15424.1.2 by Ian Booth
New findPeopleWithoutAccess service methods, ensure all subscribers are granted access to a bug when it becomes private
860
        # Grant the subscriber access if they can't see the bug but only if
861
        # there is at least one bugtask for which access can be checked.
862
        if self.default_bugtask:
863
            service = getUtility(IService, 'sharing')
17332.6.19 by Colin Watson
Add sharing service support for Git repositories.
864
            bugs, _, _, _ = service.getVisibleArtifacts(
15907.2.2 by Ian Booth
Fix permissions on exported methods
865
                person, bugs=[self], ignore_permissions=True)
15424.1.2 by Ian Booth
New findPeopleWithoutAccess service methods, ensure all subscribers are granted access to a bug when it becomes private
866
            if not bugs:
867
                service.ensureAccessGrants(
868
                    [person], subscribed_by, bugs=[self],
869
                    ignore_permissions=True)
15162.1.24 by Ian Booth
Add model code to grant access to bugs when adding a subscriber and add required sharing service methods
870
10898.4.14 by Deryck Hodge
Add a comment.
871
        # In some cases, a subscription should be created without
872
        # email notifications.  suppress_notify determines if
873
        # notifications are sent.
10795.5.1 by Deryck Hodge
Merging in work from production-devel branch to prevent
874
        if suppress_notify is False:
875
            notify(ObjectCreatedEvent(sub, user=subscribed_by))
876
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
877
        update_bug_heat([self.id])
6105.11.1 by Tom Berger
notify users when they are being subscribed to a bug
878
        return sub
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
879
13994.2.1 by Ian Booth
Implement new subscription behaviour
880
    def unsubscribe(self, person, unsubscribed_by, **kwargs):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
881
        """See `IBug`."""
12607.6.8 by Ian Booth
Test fix
882
        # Drop cached subscription info.
883
        clear_property_cache(self)
12607.6.2 by Ian Booth
Rework implementation
884
        # Ensure the unsubscriber is in the _known_viewer cache for the bug so
885
        # that the permissions are such that the operation can succeed.
886
        get_property_cache(self)._known_viewers = set([unsubscribed_by.id])
8426.5.1 by Deryck Hodge
Update the API to allow IBug.unsubscribe to take a person argument.
887
        if person is None:
8615.4.1 by Deryck Hodge
Remove use of ILaunchBag from Bug.unsubscribe.
888
            person = unsubscribed_by
8426.5.1 by Deryck Hodge
Update the API to allow IBug.unsubscribe to take a person argument.
889
13994.2.1 by Ian Booth
Implement new subscription behaviour
890
        ignore_permissions = kwargs.get('ignore_permissions', False)
13994.2.8 by Ian Booth
Send emails when bug supervisor or security contact unsubscribed
891
        recipients = kwargs.get('recipients')
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
892
        for sub in self.subscriptions:
893
            if sub.person.id == person.id:
13994.2.1 by Ian Booth
Implement new subscription behaviour
894
                if (not ignore_permissions
895
                        and not sub.canBeUnsubscribedByUser(unsubscribed_by)):
8384.1.1 by Deryck Hodge
Add a check against canBeUnsubscribedByUser in Bug.unsubscribe.
896
                    raise UserCannotUnsubscribePerson(
897
                        '%s does not have permission to unsubscribe %s.' % (
898
                            unsubscribed_by.displayname,
899
                            person.displayname))
13994.2.2 by Ian Booth
Extract out functionality for bug 672596 into a new branch
900
901
                self.addChange(UnsubscribedFromBug(
13994.2.8 by Ian Booth
Send emails when bug supervisor or security contact unsubscribed
902
                        when=UTC_NOW, person=unsubscribed_by,
13994.2.9 by Ian Booth
Tweak kwargs
903
                        unsubscribed_user=person, **kwargs),
13994.2.8 by Ian Booth
Send emails when bug supervisor or security contact unsubscribed
904
                    recipients=recipients)
8384.1.5 by Deryck Hodge
Raise an error earlier to make the code easier to read.
905
                store = Store.of(sub)
906
                store.remove(sub)
907
                # Make sure that the subscription removal has been
908
                # flushed so that code running with implicit flushes
909
                # disabled see the change.
910
                store.flush()
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
911
                update_bug_heat([self.id])
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
912
                del get_property_cache(self)._known_viewers
15162.1.20 by Ian Booth
Add feature flag and functionality to mirror legacy bug access
913
15587.4.3 by William Grant
Drop the disclosure.legacy_subscription_visibility.enabled feature flag -- unsubscribe now revokes unconditionally.
914
                # Revoke access to bug
915
                artifacts_to_delete = getUtility(
916
                    IAccessArtifactSource).find([self])
917
                getUtility(IAccessArtifactGrantSource).revokeByArtifact(
918
                    artifacts_to_delete, [person])
8384.1.5 by Deryck Hodge
Raise an error earlier to make the code easier to read.
919
                return
920
8656.1.1 by Deryck Hodge
Make unsubscribeFromDupes behave like unsubscribe to all
921
    def unsubscribeFromDupes(self, person, unsubscribed_by):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
922
        """See `IBug`."""
8656.1.1 by Deryck Hodge
Make unsubscribeFromDupes behave like unsubscribe to all
923
        if person is None:
924
            person = unsubscribed_by
925
3691.163.4 by Brad Bollenbach
checkpoint
926
        bugs_unsubscribed = []
927
        for dupe in self.duplicates:
928
            if dupe.isSubscribed(person):
8656.1.1 by Deryck Hodge
Make unsubscribeFromDupes behave like unsubscribe to all
929
                dupe.unsubscribe(person, unsubscribed_by)
3691.163.4 by Brad Bollenbach
checkpoint
930
                bugs_unsubscribed.append(dupe)
931
932
        return bugs_unsubscribed
933
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
934
    def isSubscribed(self, person):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
935
        """See `IBug`."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
936
        return self.personIsDirectSubscriber(person)
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
937
3691.163.2 by Brad Bollenbach
checkpoint
938
    def isSubscribedToDupes(self, person):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
939
        """See `IBug`."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
940
        return self.personIsSubscribedToDuplicate(person)
8620.4.8 by Deryck Hodge
Don't depend on the user being logged in,
941
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
942
    def _getMutes(self, person):
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
943
        return Store.of(self).find(
944
            BugMute, BugMute.bug == self, BugMute.person == person)
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
945
12526.2.1 by Graham Binns
Added an isMuted() method to IBug.
946
    def isMuted(self, person):
947
        """See `IBug`."""
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
948
        return not self._getMutes(person).is_empty()
12526.2.1 by Graham Binns
Added an isMuted() method to IBug.
949
12599.1.1 by Graham Binns
Added an IBug.mute() method.
950
    def mute(self, person, muted_by):
951
        """See `IBug`."""
12783.2.2 by Gary Poster
try to remove all message queries
952
        if person is None:
953
            # This may be a webservice request.
954
            person = muted_by
7675.1138.13 by Danilo Segan
Remove XXXes and add an assertion stopping team mutes as suggested by Graham and Stuart.
955
        assert not person.is_team, (
956
            "Muting a subscription for entire team is not allowed.")
957
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
958
        # If it's already muted, ignore the request.
959
        mutes = self._getMutes(person)
960
        if mutes.is_empty():
7675.1138.14 by Danilo Segan
Fix test failures.
961
            mute = BugMute(person, self)
962
            Store.of(mute).flush()
12599.1.1 by Graham Binns
Added an IBug.mute() method.
963
12599.1.2 by Graham Binns
Added a stubby unmute method.
964
    def unmute(self, person, unmuted_by):
965
        """See `IBug`."""
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
966
        if person is None:
967
            # This may be a webservice request.
968
            person = unmuted_by
969
        mutes = self._getMutes(person)
15809.2.1 by Steve Kowalik
IBug.unmute can now deal with being called when there is no mute.
970
        if not mutes.is_empty():
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
971
            Store.of(self).remove(mutes.one())
13023.7.2 by Danilo Segan
Split Gary's server-side changes.
972
        return self.getSubscriptionForPerson(person)
12599.1.2 by Graham Binns
Added a stubby unmute method.
973
11536.1.4 by Gavin Panella
Ensure that the output of getSubscriptionsFromDuplicates is stable, and change the subscriptions ReferenceSet into a property because the web API can't adapt what ReferenceSet returns.
974
    @property
975
    def subscriptions(self):
11536.1.5 by Gavin Panella
Use a DecoratedResultSet instead of a list comprehension.
976
        """The set of `BugSubscriptions` for this bug."""
11536.1.4 by Gavin Panella
Ensure that the output of getSubscriptionsFromDuplicates is stable, and change the subscriptions ReferenceSet into a property because the web API can't adapt what ReferenceSet returns.
977
        # XXX: kiko 2006-09-23: Why is subscriptions ordered by ID?
978
        results = Store.of(self).find(
979
            (Person, BugSubscription),
980
            BugSubscription.person_id == Person.id,
981
            BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
11536.1.5 by Gavin Panella
Use a DecoratedResultSet instead of a list comprehension.
982
        return DecoratedResultSet(results, operator.itemgetter(1))
11536.1.4 by Gavin Panella
Ensure that the output of getSubscriptionsFromDuplicates is stable, and change the subscriptions ReferenceSet into a property because the web API can't adapt what ReferenceSet returns.
983
14494.6.9 by Gavin Panella
Some clean-ups.
984
    def getSubscriptionInfo(self, level=None):
11869.18.1 by Gavin Panella
New method IBug.getSubscriptionInfo(), and security definitions around BugSubscriptionInfo objects.
985
        """See `IBug`."""
14494.6.33 by Gavin Panella
Remove confusing conditional expression.
986
        if level is None:
987
            level = BugNotificationLevel.LIFECYCLE
988
        return BugSubscriptionInfo(self, level)
11869.18.1 by Gavin Panella
New method IBug.getSubscriptionInfo(), and security definitions around BugSubscriptionInfo objects.
989
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
990
    def getDirectSubscriptions(self):
991
        """See `IBug`."""
11869.18.3 by Gavin Panella
Use getSubscriptionInfo() in getDirectSub*.
992
        return self.getSubscriptionInfo().direct_subscriptions
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
993
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
994
    def getDirectSubscribers(self, recipients=None, level=None,
995
                             filter_visible=False):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
996
        """See `IBug`.
3945.2.30 by kiko
Move recipients out of the interface description and into the docstrings of the database class' methods.
997
998
        The recipients argument is private and not exposed in the
6493.3.1 by Guilherme Salgado
Rename IPerson.timezone to IPerson.time_zone
999
        interface. If a BugNotificationRecipients instance is supplied,
3945.2.30 by kiko
Move recipients out of the interface description and into the docstrings of the database class' methods.
1000
        the relevant subscribers and rationales will be registered on
1001
        it.
1002
        """
11688.1.4 by Graham Binns
getDirectSubscribers() now returns subscribers for a given BugNotificationLevel.
1003
        if level is None:
7675.1139.1 by Danilo Segan
Get rid of all NOTHING usage.
1004
            level = BugNotificationLevel.LIFECYCLE
13627.2.10 by Brad Crittenden
Fixed lint
1005
        direct_subscribers = (
1006
            self.getSubscriptionInfo(level).direct_subscribers)
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
1007
        if filter_visible:
1008
            filtered_subscribers = IStore(Person).find(Person,
1009
                Person.id.is_in([s.id for s in direct_subscribers]),
1010
                self.getSubscriptionInfo().visible_recipients_filter(
1011
                    Person.id))
1012
            direct_subscribers = BugSubscriberSet(
1013
                direct_subscribers.intersection(filtered_subscribers))
4231.1.16 by Francis J. Lacoste
Compare explicitely to None, since a NotificationRecipientSet evaluates to False when empty.
1014
        if recipients is not None:
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
1015
            for subscriber in direct_subscribers:
3945.2.27 by kiko
Replace rationale for recipients everywhere
1016
                recipients.addDirectSubscriber(subscriber)
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
1017
        return direct_subscribers.sorted
3485.6.7 by Brad Bollenbach
Fix bug 29752 (If a bug is marked as a duplicate, its subscribers should be notified when the duplicate bug changes)
1018
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1019
    def getDirectSubscribersWithDetails(self):
1020
        """See `IBug`."""
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
1021
        SubscribedBy = ClassAlias(Person, name="subscribed_by")
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1022
        results = Store.of(self).find(
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
1023
            (Person, SubscribedBy, BugSubscription),
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1024
            BugSubscription.person_id == Person.id,
1025
            BugSubscription.bug_id == self.id,
13469.2.5 by Brad Crittenden
Precache the 'subscribed_by' person for performance. Add tests showing expected query count of 1.
1026
            BugSubscription.subscribed_by_id == SubscribedBy.id,
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1027
            Not(In(BugSubscription.person_id,
1028
                   Select(BugMute.person_id, BugMute.bug_id == self.id)))
17773.1.1 by Colin Watson
Rename Person.displayname to Person.display_name, leaving a property behind for compatibility.
1029
            ).order_by(Person.display_name)
13247.1.1 by Danilo Segan
Reapply bug 772754 fix with packagecopyjob changes removed.
1030
        return results
1031
6676.4.1 by Abel Deuring
filtering of bug notifications for structural subscriptions for BugNotificationlevel.COMMENTS
1032
    def getIndirectSubscribers(self, recipients=None, level=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1033
        """See `IBug`.
3945.2.30 by kiko
Move recipients out of the interface description and into the docstrings of the database class' methods.
1034
1035
        See the comment in getDirectSubscribers for a description of the
1036
        recipients argument.
1037
        """
3553.3.73 by Brad Bollenbach
code review fixes
1038
        # "Also notified" and duplicate subscribers are mutually
1039
        # exclusive, so return both lists.
11869.18.7 by Gavin Panella
Migrate getSubscribersFromDuplicates() to use BugSubscriptionInfo.
1040
        indirect_subscribers = chain(
1041
            self.getAlsoNotifiedSubscribers(recipients, level),
6676.4.1 by Abel Deuring
filtering of bug notifications for structural subscriptions for BugNotificationlevel.COMMENTS
1042
            self.getSubscribersFromDuplicates(recipients, level))
3691.209.6 by Bjorn Tillenius
review comments.
1043
11545.5.10 by Deryck Hodge
Remove proxy on the object to provide sort key for indirect subscribers.
1044
        # Remove security proxy for the sort key, but return
1045
        # the regular proxied object.
3553.3.73 by Brad Bollenbach
code review fixes
1046
        return sorted(
11545.5.10 by Deryck Hodge
Remove proxy on the object to provide sort key for indirect subscribers.
1047
            indirect_subscribers,
14494.6.34 by Gavin Panella
Update XXX for bug 911752.
1048
            # XXX: GavinPanella 2011-12-12 bug=911752: Use person_sort_key.
11545.5.10 by Deryck Hodge
Remove proxy on the object to provide sort key for indirect subscribers.
1049
            key=lambda x: removeSecurityProxy(x).displayname)
3553.3.71 by Brad Bollenbach
Attempt to fix bug 66562 (BugSubscriberPortletView.getSubscribersFromDupes seems to cause timeouts)
1050
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
1051
    def getSubscriptionsFromDuplicates(self, recipients=None):
1052
        """See `IBug`."""
1053
        if self.private:
1054
            return []
11536.1.8 by Gavin Panella
In getSubscriptionsFromDuplicates(), move the subscription selection logic into the database.
1055
        # For each subscription to each duplicate of this bug, find the
11536.1.9 by Gavin Panella
Change the sub-query to a DISTINCT ON clause, as suggested by lifeless. Uses a hack to work around bug 374777.
1056
        # earliest subscription for each subscriber. Eager load the
1057
        # subscribers.
11536.1.8 by Gavin Panella
In getSubscriptionsFromDuplicates(), move the subscription selection logic into the database.
1058
        return DecoratedResultSet(
11536.1.4 by Gavin Panella
Ensure that the output of getSubscriptionsFromDuplicates is stable, and change the subscriptions ReferenceSet into a property because the web API can't adapt what ReferenceSet returns.
1059
            IStore(BugSubscription).find(
13052.1.2 by William Grant
Use Storm DISTINCT ON instead of the ignore hack.
1060
                (Person, BugSubscription),
11536.1.9 by Gavin Panella
Change the sub-query to a DISTINCT ON clause, as suggested by lifeless. Uses a hack to work around bug 374777.
1061
                Bug.duplicateof == self,
1062
                BugSubscription.bug_id == Bug.id,
11536.1.8 by Gavin Panella
In getSubscriptionsFromDuplicates(), move the subscription selection logic into the database.
1063
                BugSubscription.person_id == Person.id).order_by(
14913.1.1 by William Grant
Fix Bug.getSubscriptionsFromDuplicates to specify a deterministic sort order. Its tests break occasionally otherwise.
1064
                    BugSubscription.person_id,
1065
                    BugSubscription.date_created,
1066
                    BugSubscription.id
1067
                    ).config(distinct=(BugSubscription.person_id,)),
13052.1.2 by William Grant
Use Storm DISTINCT ON instead of the ignore hack.
1068
            operator.itemgetter(1))
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
1069
6676.4.1 by Abel Deuring
filtering of bug notifications for structural subscriptions for BugNotificationlevel.COMMENTS
1070
    def getSubscribersFromDuplicates(self, recipients=None, level=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1071
        """See `IBug`.
3945.2.30 by kiko
Move recipients out of the interface description and into the docstrings of the database class' methods.
1072
1073
        See the comment in getDirectSubscribers for a description of the
1074
        recipients argument.
1075
        """
11869.18.17 by Gavin Panella
Make test_subscribers_from_dupes_uses_level() fail when it should.
1076
        if level is None:
7675.1139.1 by Danilo Segan
Get rid of all NOTHING usage.
1077
            level = BugNotificationLevel.LIFECYCLE
11869.18.17 by Gavin Panella
Make test_subscribers_from_dupes_uses_level() fail when it should.
1078
        info = self.getSubscriptionInfo(level)
3945.2.27 by kiko
Replace rationale for recipients everywhere
1079
        if recipients is not None:
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
1080
            list(self.duplicates)  # Pre-load duplicate bugs.
1081
            info.duplicate_only_subscribers  # Pre-load subscribers.
11869.18.7 by Gavin Panella
Migrate getSubscribersFromDuplicates() to use BugSubscriptionInfo.
1082
            for subscription in info.duplicate_only_subscriptions:
12338.3.3 by Gary Poster
the test was a false alarm. remove it.
1083
                recipients.addDupeSubscriber(
1084
                    subscription.person, subscription.bug)
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
1085
        return info.duplicate_only_subscribers.sorted
3691.209.6 by Bjorn Tillenius
review comments.
1086
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
1087
    def getSubscribersForPerson(self, person):
1088
        """See `IBug."""
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
1089
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
1090
        assert person is not None
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
1091
11582.2.5 by Robert Collins
Fix up test fallout.
1092
        def cache_unsubscribed(rows):
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1093
            if not rows:
1094
                self._unsubscribed_cache.add(person)
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
1095
11582.2.5 by Robert Collins
Fix up test fallout.
1096
        def cache_subscriber(row):
13052.1.2 by William Grant
Use Storm DISTINCT ON instead of the ignore hack.
1097
            subscriber, subscription = row
11536.1.17 by Gavin Panella
Fix regression introduced from merge.
1098
            if subscription.bug_id == self.id:
11582.2.5 by Robert Collins
Fix up test fallout.
1099
                self._subscriber_cache.add(subscriber)
1100
            else:
1101
                self._subscriber_dups_cache.add(subscriber)
1102
            return subscriber
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
1103
        with_statement = generate_subscription_with(self, person)
1104
        store = Store.of(self).with_(with_statement)
1105
        return DecoratedResultSet(store.find(
11789.3.10 by Gavin Panella
Fix some lint and remove some vestigial test narrative.
1106
             # Return people and subscriptions
13052.1.2 by William Grant
Use Storm DISTINCT ON instead of the ignore hack.
1107
            (Person, BugSubscription),
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
1108
            BugSubscription.id.is_in(
1109
                SQL('SELECT bugsubscriptions.id FROM bugsubscriptions')),
1110
            Person.id == BugSubscription.person_id,
13052.1.2 by William Grant
Use Storm DISTINCT ON instead of the ignore hack.
1111
            ).order_by(Person.name).config(
1112
                distinct=(Person.name, BugSubscription.person_id)),
11582.2.5 by Robert Collins
Fix up test fallout.
1113
            cache_subscriber, pre_iter_hook=cache_unsubscribed)
11474.2.3 by Robert Collins
Add getSubscribersForPerson to IBug, permitting single query setup of bug index pages, which according to OOPS is about 1.2 seconds (but if we're underestimating could be more) and will make analysing performance on the page easier.
1114
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
1115
    def getSubscriptionForPerson(self, person):
1116
        """See `IBug`."""
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
1117
        return Store.of(self).find(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1118
            BugSubscription, BugSubscription.person == person,
11843.1.3 by Graham Binns
It's now possible to update your subscription. Hurrah.
1119
            BugSubscription.bug == self).one()
1120
6676.4.1 by Abel Deuring
filtering of bug notifications for structural subscriptions for BugNotificationlevel.COMMENTS
1121
    def getAlsoNotifiedSubscribers(self, recipients=None, level=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1122
        """See `IBug`.
3945.2.30 by kiko
Move recipients out of the interface description and into the docstrings of the database class' methods.
1123
1124
        See the comment in getDirectSubscribers for a description of the
1125
        recipients argument.
1126
        """
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
1127
        return get_also_notified_subscribers(self, recipients, level)
3554.3.6 by Brad Bollenbach
checkpoint
1128
16152.3.3 by Curtis Hovey
Differ the retreival bug notification recipients to cached helpers.
1129
    def _getBugNotificationRecipients(self, level):
1130
        """Get the recipients for the BugNotificationLevel."""
16147.2.2 by Curtis Hovey
Remove unused duplicateof param from Bug.getBugNotificationRecipients()
1131
        recipients = BugNotificationRecipients()
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
1132
        self.getDirectSubscribers(
1133
            recipients, level=level, filter_visible=True)
1134
        self.getIndirectSubscribers(recipients, level=level)
16152.3.3 by Curtis Hovey
Differ the retreival bug notification recipients to cached helpers.
1135
        return recipients
1136
1137
    @cachedproperty
1138
    def _notification_recipients_for_lifecycle(self):
1139
        """The cached BugNotificationRecipients for LIFECYCLE events."""
1140
        return self._getBugNotificationRecipients(
1141
            BugNotificationLevel.LIFECYCLE)
1142
1143
    @cachedproperty
1144
    def _notification_recipients_for_metadata(self):
1145
        """The cached BugNotificationRecipients for METADATA events."""
1146
        return self._getBugNotificationRecipients(
1147
            BugNotificationLevel.METADATA)
1148
1149
    @cachedproperty
1150
    def _notification_recipients_for_comments(self):
1151
        """The cached BugNotificationRecipients for COMMENT events."""
1152
        return self._getBugNotificationRecipients(
1153
            BugNotificationLevel.COMMENTS)
1154
1155
    def getBugNotificationRecipients(self,
1156
                                     level=BugNotificationLevel.LIFECYCLE):
1157
        """See `IBug`."""
1158
        recipients = BugNotificationRecipients()
1159
        if level == BugNotificationLevel.LIFECYCLE:
1160
            recipients.update(self._notification_recipients_for_lifecycle)
1161
        elif level == BugNotificationLevel.METADATA:
1162
            recipients.update(self._notification_recipients_for_metadata)
1163
        else:
1164
            recipients.update(self._notification_recipients_for_comments)
1165
        return recipients
1478 by Canonical.com Patch Queue Manager
refactor bug subscription code to look more like bounty subscription code
1166
16152.3.8 by Curtis Hovey
Ues a model method to control the clearing of the _notification_recipients*
1167
    def clearBugNotificationRecipientsCache(self):
1168
        cache = get_property_cache(self)
1169
        if getattr(cache, '_notification_recipients_for_lifecycle', False):
1170
            del cache._notification_recipients_for_lifecycle
1171
        if getattr(cache, '_notification_recipients_for_metadata', False):
1172
            del cache._notification_recipients_for_metadata
1173
        if getattr(cache, '_notification_recipients_for_comments', False):
1174
            del cache._notification_recipients_for_comments
1175
16856.2.1 by William Grant
Bug.addCommentNotification now permits overriding of the notification level.
1176
    def addCommentNotification(self, message, recipients=None, activity=None,
1177
                               level=BugNotificationLevel.COMMENTS):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1178
        """See `IBug`."""
5937.1.1 by Tom Berger
merge patches for the original branch
1179
        if recipients is None:
16856.2.1 by William Grant
Bug.addCommentNotification now permits overriding of the notification level.
1180
            recipients = self.getBugNotificationRecipients(level=level)
5937.1.1 by Tom Berger
merge patches for the original branch
1181
        getUtility(IBugNotificationSet).addNotification(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1182
             bug=self, is_comment=True, message=message, recipients=recipients,
1183
             activity=activity)
3254.1.14 by Bjorn Tillenius
checkpoint commit
1184
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1185
    def addChange(self, change, recipients=None, deferred=False,
1186
                  update_heat=True):
7947.1.1 by Graham Binns
Added the basics of the new API.
1187
        """See `IBug`."""
7947.2.9 by Graham Binns
Whole wodges of stuff changed. I can't remember what, though.
1188
        when = change.when
1189
        if when is None:
1190
            when = UTC_NOW
1191
7947.1.3 by Graham Binns
Added basic tests for BugActivity in addChange().
1192
        activity_data = change.getBugActivity()
1193
        if activity_data is not None:
12366.6.1 by Gary Poster
basic changes to make bugactivity an attribute of a notification as frequently as possible
1194
            activity = getUtility(IBugActivitySet).new(
7947.2.9 by Graham Binns
Whole wodges of stuff changed. I can't remember what, though.
1195
                self, when, change.person,
7947.1.3 by Graham Binns
Added basic tests for BugActivity in addChange().
1196
                activity_data['whatchanged'],
1197
                activity_data.get('oldvalue'),
1198
                activity_data.get('newvalue'),
1199
                activity_data.get('message'))
12366.6.1 by Gary Poster
basic changes to make bugactivity an attribute of a notification as frequently as possible
1200
        else:
1201
            activity = None
7947.1.1 by Graham Binns
Added the basics of the new API.
1202
7947.1.5 by Graham Binns
Added the remainder of the tests for notifications, etc.
1203
        notification_data = change.getBugNotification()
1204
        if notification_data is not None:
7947.1.7 by Graham Binns
Removed comment-handling code from Bug.addChange().
1205
            assert notification_data.get('text') is not None, (
1206
                "notification_data must include a `text` value.")
12289.11.2 by Gavin Panella
Remove addChangeNotification() entirely.
1207
            message = MessageSet().fromText(
1208
                self.followup_subject(), notification_data['text'],
1209
                owner=change.person, datecreated=when)
1210
            if recipients is None:
12289.11.4 by Gavin Panella
Assign to recipients to make the intent clearer.
1211
                recipients = self.getBugNotificationRecipients(
16856.2.4 by William Grant
Promote task add/delete/retarget events to LIFECYCLE.
1212
                    level=change.change_level)
12289.11.4 by Gavin Panella
Assign to recipients to make the intent clearer.
1213
            getUtility(IBugNotificationSet).addNotification(
1214
                bug=self, is_comment=False, message=message,
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
1215
                recipients=recipients, activity=activity,
1216
                deferred=deferred)
7947.1.5 by Graham Binns
Added the remainder of the tests for notifications, etc.
1217
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1218
        if update_heat:
1219
            update_bug_heat([self.id])
7675.472.31 by Graham Binns
Added tests and implementation for calculating bug heat upon bug activity.
1220
3691.440.23 by James Henstridge
expire pending bug notifications for newly created bugs
1221
    def expireNotifications(self):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1222
        """See `IBug`."""
3691.440.23 by James Henstridge
expire pending bug notifications for newly created bugs
1223
        for notification in BugNotification.selectBy(
1224
                bug=self, date_emailed=None):
1225
            notification.date_emailed = UTC_NOW
1226
            notification.syncUpdate()
1227
6293.1.1 by Tom Berger
reply to remore bug comments ui
1228
    def newMessage(self, owner=None, subject=None,
7337.7.4 by Graham Binns
Fixed spurious test failures.
1229
                   content=None, parent=None, bugwatch=None,
16071.2.1 by Curtis Hovey
Do not send notifications about new bug messages if they are supressed.
1230
                   remote_comment_id=None, send_notifications=True):
3254.1.24 by Bjorn Tillenius
fix bug 25724, remove comment_on_change hack.
1231
        """Create a new Message and link it to this bug."""
9037.1.2 by Tom Berger
instead of a None subject, use the followup subject when saving
1232
        if subject is None:
1233
            subject = self.followup_subject()
2938.2.4 by Brad Bollenbach
test fixes
1234
        msg = Message(
1235
            parent=parent, owner=owner, subject=subject,
1236
            rfc822msgid=make_msgid('malone'))
3691.62.21 by kiko
Clean up the use of ID/.id in select*By and constructors
1237
        MessageChunk(message=msg, content=content, sequence=1)
2938.2.10 by Brad Bollenbach
response to code review
1238
7337.7.4 by Graham Binns
Fixed spurious test failures.
1239
        bugmsg = self.linkMessage(
1240
            msg, bugwatch, remote_comment_id=remote_comment_id)
4187.5.2 by Abel Deuring
implemented reviewer's suggestions
1241
        if not bugmsg:
1242
            return
4187.5.1 by Abel Deuring
fix for bug 1804
1243
16071.2.1 by Curtis Hovey
Do not send notifications about new bug messages if they are supressed.
1244
        if send_notifications:
1245
            notify(ObjectCreatedEvent(bugmsg, user=owner))
2938.2.1 by Brad Bollenbach
checkpoint
1246
1247
        return bugmsg.message
2396 by Canonical.com Patch Queue Manager
[r=spiv] launchpad support tracker
1248
6002.8.4 by Bjorn Tillenius
add BugMessage.remote_comment_id and have the comment importer set it.
1249
    def linkMessage(self, message, bugwatch=None, user=None,
1250
                    remote_comment_id=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1251
        """See `IBug`."""
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
1252
        if message not in self.messages:
5548.9.8 by Graham Binns
Salgado's review changes.
1253
            if user is None:
1254
                user = message.owner
5292.2.6 by Graham Binns
Imported comments are not shown.
1255
            result = BugMessage(bug=self, message=message,
12346.2.2 by Robert Collins
Change all BugMessage object creation to set the index. This involved
1256
                bugwatch=bugwatch, remote_comment_id=remote_comment_id,
1257
                index=self.bug_messages.count())
4187.5.2 by Abel Deuring
implemented reviewer's suggestions
1258
            getUtility(IBugWatchSet).fromText(
5548.9.8 by Graham Binns
Salgado's review changes.
1259
                message.text_contents, self, user)
1260
            self.findCvesInText(message.text_contents, user)
12845.2.5 by Robert Collins
Serialise INCOMPLETE status to INCOMPLETE_WITHOUT_RESPONSE.
1261
            for bugtask in self.bugtasks:
1262
                # Check the stored value so we don't write to unaltered tasks.
13973.2.5 by Brad Crittenden
Version with lots of debugging junk
1263
                if (bugtask._status in (
1264
                    BugTaskStatus.INCOMPLETE,
1265
                    BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)):
12845.2.5 by Robert Collins
Serialise INCOMPLETE status to INCOMPLETE_WITHOUT_RESPONSE.
1266
                    # This is not a semantic change, so we don't update date
1267
                    # records or send email.
14039.1.8 by Brad Crittenden
Fixed lint
1268
                    bugtask._status = (
1269
                        BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
5821.5.20 by James Henstridge
* Add some flush calls to message/bugmessage creation, to make sure
1270
            # XXX 2008-05-27 jamesh:
1271
            # Ensure that BugMessages get flushed in same order as
1272
            # they are created.
1273
            Store.of(result).flush()
4187.5.2 by Abel Deuring
implemented reviewer's suggestions
1274
            return result
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
1275
16183.4.1 by Ian Booth
Improve bug form and model infrastructure to avoid repeated calls to various validation methods
1276
    def addTask(self, owner, target, validate_target=True):
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
1277
        """See `IBug`."""
16183.4.1 by Ian Booth
Improve bug form and model infrastructure to avoid repeated calls to various validation methods
1278
        return getUtility(IBugTaskSet).createTask(
1279
            self, owner, target, validate_target)
7705.1.1 by Graham Binns
Added IBug.addTask() as a nominal wrapper around IBugTaskSet.createTask().
1280
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
1281
    def addWatch(self, bugtracker, remotebug, owner):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1282
        """See `IBug`."""
3691.209.3 by Bjorn Tillenius
add page test to make sure +editstatus doesn't create duplicate bug watches.
1283
        # We shouldn't add duplicate bug watches.
1284
        bug_watch = self.getBugWatch(bugtracker, remotebug)
5821.2.57 by James Henstridge
more changes to resolve failing tests.
1285
        if bug_watch is None:
1286
            bug_watch = BugWatch(
3691.209.3 by Bjorn Tillenius
add page test to make sure +editstatus doesn't create duplicate bug watches.
1287
                bug=self, bugtracker=bugtracker,
1288
                remotebug=remotebug, owner=owner)
5821.2.57 by James Henstridge
more changes to resolve failing tests.
1289
            Store.of(bug_watch).flush()
7982.1.4 by Bjorn Tillenius
Make sure something is added to the activity log.
1290
        self.addChange(BugWatchAdded(UTC_NOW, owner, bug_watch))
7982.1.2 by Bjorn Tillenius
Fire off the event from inside addWatch().
1291
        notify(ObjectCreatedEvent(bug_watch, user=owner))
5821.2.57 by James Henstridge
more changes to resolve failing tests.
1292
        return bug_watch
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
1293
7982.1.6 by Bjorn Tillenius
Add Bug.removeWatch().
1294
    def removeWatch(self, bug_watch, user):
1295
        """See `IBug`."""
7982.1.7 by Bjorn Tillenius
Make sure bug watch removals are recorded properly.
1296
        self.addChange(BugWatchRemoved(UTC_NOW, user, bug_watch))
7982.1.6 by Bjorn Tillenius
Add Bug.removeWatch().
1297
        bug_watch.destroySelf()
1298
6655.4.14 by Gavin Panella
Fix up the doc for IBug.addAttachment, and other related fixes.
1299
    def addAttachment(self, owner, data, comment, filename, is_patch=False,
16473.2.2 by Steve Kowalik
Stop lp.services.webapp.publisher re-exporting get_current_browser_request and fix all import sites to use lazr.restful.utils. If IProductRelease.add_file or IBug.addAttachment are called via the API, call into the new method, get_raw_form_value_from_current_request which will get the non-encoded file content.
1300
                      content_type=None, description=None, from_api=False):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1301
        """See `IBug`."""
16473.2.3 by Steve Kowalik
Sprinkle in XXXes for the call sites of get_raw_form_value_from_current_request.
1302
        # XXX: StevenK 2013-02-06 bug=1116954: We should not need to refetch
1303
        # the file content from the request, since the passed in one has been
1304
        # wrongly encoded.
16473.2.2 by Steve Kowalik
Stop lp.services.webapp.publisher re-exporting get_current_browser_request and fix all import sites to use lazr.restful.utils. If IProductRelease.add_file or IBug.addAttachment are called via the API, call into the new method, get_raw_form_value_from_current_request which will get the non-encoded file content.
1305
        if from_api:
16951.1.1 by William Grant
Adjust usage of get_raw_form_value_from_current_request to preserve the original field if it isn't corrupted. It'll be uncorrupted if there's a filename.
1306
            data = get_raw_form_value_from_current_request(data, 'data')
6655.4.2 by Gavin Panella
First round of annotations, with tests.
1307
        if isinstance(data, str):
1308
            filecontent = data
1309
        else:
1310
            filecontent = data.read()
1311
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1312
        if is_patch:
1313
            content_type = 'text/plain'
1314
        else:
3691.320.5 by Bjorn Tillenius
support adding attachments to the bug report.
1315
            if content_type is None:
1316
                content_type, encoding = guess_content_type(
1317
                    name=filename, body=filecontent)
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1318
15903.1.2 by Steve Kowalik
Raise the exception in ILibraryFileAliasSet.create(), use an actual exception.
1319
        filealias = getUtility(ILibraryFileAliasSet).create(
1320
            name=filename, size=len(filecontent),
1321
            file=StringIO(filecontent), contentType=content_type,
1322
            restricted=self.private)
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1323
11235.7.4 by Abel Deuring
renamed Bug._linkAttachment() again to Bug.linkAttachment(); allowed the DB user 'bugnotification' to read the table bugattachment.
1324
        return self.linkAttachment(
7675.534.1 by Graham Binns
Added an an IBug.linkAttachment() method.
1325
            owner, filealias, comment, is_patch, description)
1326
11235.7.4 by Abel Deuring
renamed Bug._linkAttachment() again to Bug.linkAttachment(); allowed the DB user 'bugnotification' to read the table bugattachment.
1327
    def linkAttachment(self, owner, file_alias, comment, is_patch=False,
11634.2.9 by Robert Collins
Another missed fixture stateless-use.
1328
                       description=None, send_notifications=True):
11235.7.4 by Abel Deuring
renamed Bug._linkAttachment() again to Bug.linkAttachment(); allowed the DB user 'bugnotification' to read the table bugattachment.
1329
        """See `IBug`.
1330
1331
        This method should only be called by addAttachment() and
1332
        FileBugViewBase.submit_bug_action, otherwise
11235.7.1 by Abel Deuring
set the restricted flag of the Librarian record when an attachment is aded to a private bug; flip the restricted flag of Librarian files from bug attachments when the Bug.setPrivate() is called.
1333
        we may get inconsistent settings of bug.private and
1334
        file_alias.restricted.
11634.2.9 by Robert Collins
Another missed fixture stateless-use.
1335
1336
        :param send_notifications: Control sending of notifications for this
1337
            attachment. This is disabled when adding attachments from 'extra
1338
            data' in the filebug form, because that triggered hundreds of DB
1339
            inserts and thus timeouts. Defaults to sending notifications.
11235.7.1 by Abel Deuring
set the restricted flag of the Librarian record when an attachment is aded to a private bug; flip the restricted flag of Librarian files from bug attachments when the Bug.setPrivate() is called.
1340
        """
7675.534.1 by Graham Binns
Added an an IBug.linkAttachment() method.
1341
        if is_patch:
1342
            attach_type = BugAttachmentType.PATCH
1343
        else:
1344
            attach_type = BugAttachmentType.UNSPECIFIED
1345
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1346
        if description:
1347
            title = description
1348
        else:
7675.534.1 by Graham Binns
Added an an IBug.linkAttachment() method.
1349
            title = file_alias.filename
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1350
3644.1.14 by Brad Bollenbach
checkpoint
1351
        if IMessage.providedBy(comment):
1352
            message = comment
1353
        else:
1354
            message = self.newMessage(
1355
                owner=owner, subject=description, content=comment)
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1356
5796.14.1 by Abel Deuring
Fix for bug 195664
1357
        return getUtility(IBugAttachmentSet).create(
7675.534.1 by Graham Binns
Added an an IBug.linkAttachment() method.
1358
            bug=self, filealias=file_alias, attach_type=attach_type,
11634.2.9 by Robert Collins
Another missed fixture stateless-use.
1359
            title=title, message=message,
1360
            send_notifications=send_notifications)
3644.1.11 by Brad Bollenbach
refactor bug attachment API and allow adding an attachment while commenting
1361
8698.10.3 by Paul Hummer
Integrated IHasLinkedBranches into the interfaces
1362
    def linkBranch(self, branch, registrant):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1363
        """See `IBug`."""
17910.1.10 by William Grant
Don't leak BugBranches out of linkBranch.
1364
        if branch in self.linked_branches:
1365
            return
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
1366
17910.1.10 by William Grant
Don't leak BugBranches out of linkBranch.
1367
        BugBranch(branch=branch, bug=self, registrant=registrant)
5001.2.3 by Tim Penhey
More test details.
1368
        branch.date_last_modified = UTC_NOW
3283.3.1 by Brad Bollenbach
create a new branch for bzr integration, to avoid 3 hour merge time
1369
8640.2.2 by Gavin Panella
Don't send notifications for bug-branch linking/unlinking if the bug is complete.
1370
        self.addChange(BranchLinkedToBug(UTC_NOW, registrant, branch, self))
17910.1.8 by William Grant
Replace IObject(Created|Deleted)Events on IBugBranch with IObjectLinkedEvents on Bug.
1371
        notify(ObjectLinkedEvent(branch, self, user=registrant))
1372
        notify(ObjectLinkedEvent(self, branch, user=registrant))
3554.1.4 by Brad Bollenbach
make sure adding/editing a bug branch via the UI updates IBug.date_last_updated
1373
8698.10.3 by Paul Hummer
Integrated IHasLinkedBranches into the interfaces
1374
    def unlinkBranch(self, branch, user):
7977.2.7 by Bjorn Tillenius
Add Bug.removeBranch().
1375
        """See `IBug`."""
1376
        bug_branch = BugBranch.selectOneBy(bug=self, branch=branch)
1377
        if bug_branch is not None:
8640.2.2 by Gavin Panella
Don't send notifications for bug-branch linking/unlinking if the bug is complete.
1378
            self.addChange(BranchUnlinkedFromBug(UTC_NOW, user, branch, self))
17910.1.8 by William Grant
Replace IObject(Created|Deleted)Events on IBugBranch with IObjectLinkedEvents on Bug.
1379
            notify(ObjectUnlinkedEvent(branch, self, user=user))
1380
            notify(ObjectUnlinkedEvent(self, branch, user=user))
17910.1.9 by William Grant
Only allow BugBranch deletion via Bug.unlinkBranch.
1381
            Store.of(bug_branch).remove(bug_branch)
7977.2.7 by Bjorn Tillenius
Add Bug.removeBranch().
1382
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
1383
    def getVisibleLinkedBranches(self, user, eager_load=False):
18114.1.2 by Colin Watson
Implement the basics of bug linking for Git-based merge proposals.
1384
        """See `IBug`."""
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1385
        linked_branches = list(getUtility(IAllBranches).visibleByUser(
13827.1.1 by Gary Poster
add optimization for bug page with many branches: this time for sure!
1386
            user).linkedToBugs([self]).getBranches(eager_load=eager_load))
13277.4.11 by Graham Binns
Listify earlier to save multiple queries.
1387
        if len(linked_branches) == 0:
13277.4.8 by Graham Binns
Might have fixed some fundamental issues. Might not have. Unsure. I have a feeling that this might fail spectacularly.
1388
            return EmptyResultSet()
13277.4.11 by Graham Binns
Listify earlier to save multiple queries.
1389
        else:
1390
            branch_ids = [branch.id for branch in linked_branches]
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1391
            return Store.of(self).find(
13277.4.11 by Graham Binns
Listify earlier to save multiple queries.
1392
                BugBranch,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1393
                BugBranch.bug == self, In(BugBranch.branchID, branch_ids))
13277.4.1 by Graham Binns
Fixed the bug. May have compromised on performance.
1394
18114.1.2 by Colin Watson
Implement the basics of bug linking for Git-based merge proposals.
1395
    def linkMergeProposal(self, merge_proposal, user, check_permissions=True):
1396
        """See `IBug`."""
1397
        merge_proposal.linkBug(
1398
            self, user=user, check_permissions=check_permissions)
1399
1400
    def unlinkMergeProposal(self, merge_proposal, user,
1401
                            check_permissions=True):
1402
        """See `IBug`."""
1403
        merge_proposal.unlinkBug(
1404
            self, user=user, check_permissions=check_permissions)
1405
1406
    @property
1407
    def linked_merge_proposals(self):
1408
        from lp.code.model.branchmergeproposal import BranchMergeProposal
1409
        merge_proposal_ids = [
1410
            int(id) for _, id in getUtility(IXRefSet).findFrom(
1411
                (u'bug', unicode(self.id)), types=[u'merge_proposal'])]
1412
        return list(sorted(
1413
            bulk.load(BranchMergeProposal, merge_proposal_ids),
1414
            key=operator.attrgetter('id')))
1415
1416
    def getVisibleLinkedMergeProposals(self, user, eager_load=False):
1417
        """See `IBug`."""
1418
        linked_merge_proposal_ids = set(
1419
            bmp.id for bmp in self.linked_merge_proposals)
18114.1.3 by Colin Watson
Save a query if a bug has no linked merge proposals.
1420
        if not linked_merge_proposal_ids:
1421
            return EmptyResultSet()
1422
        else:
1423
            # XXX cjwatson 2016-06-24: This will also need to look at
1424
            # IAllBranches in the event that we start linking bugs directly
1425
            # to Bazaar-based merge proposals rather than to their source
1426
            # branches.
1427
            collection = getUtility(IAllGitRepositories).visibleByUser(user)
1428
            return collection.getMergeProposals(
1429
                merge_proposal_ids=linked_merge_proposal_ids,
1430
                eager_load=eager_load)
18114.1.2 by Colin Watson
Implement the basics of bug linking for Git-based merge proposals.
1431
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1432
    @cachedproperty
1433
    def has_cves(self):
1434
        """See `IBug`."""
1435
        return bool(self.cves)
1436
17775.1.1 by William Grant
(un)?link(Bug|Cve) can now have their permission check bypassed. Used by bugimport.
1437
    def linkCVE(self, cve, user, check_permissions=True):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1438
        """See `IBug`."""
17775.1.1 by William Grant
(un)?link(Bug|Cve) can now have their permission check bypassed. Used by bugimport.
1439
        cve.linkBug(self, user=user, check_permissions=check_permissions)
7242.1.1 by Tom Berger
expose bug CVEs via the API
1440
17775.1.1 by William Grant
(un)?link(Bug|Cve) can now have their permission check bypassed. Used by bugimport.
1441
    def unlinkCVE(self, cve, user, check_permissions=True):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1442
        """See `IBug`."""
17775.1.1 by William Grant
(un)?link(Bug|Cve) can now have their permission check bypassed. Used by bugimport.
1443
        cve.unlinkBug(self, user=user, check_permissions=check_permissions)
2450 by Canonical.com Patch Queue Manager
[r=jamesh] rework cve structure, and general polish
1444
4476.1.3 by Bjorn Tillenius
fix test to expose problem when creating CVEs on package uploads. Fix the test failure by requiring a user attribute for linkCVE and findCvesInText.
1445
    def findCvesInText(self, text, user):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1446
        """See `IBug`."""
2450 by Canonical.com Patch Queue Manager
[r=jamesh] rework cve structure, and general polish
1447
        cves = getUtility(ICveSet).inText(text)
1448
        for cve in cves:
17828.1.1 by William Grant
Bypass normal bug linking permission checks when linking CVES from comments.
1449
            self.linkCVE(cve, user, check_permissions=False)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
1450
3691.436.12 by Mark Shuttleworth
Make sure we don't see completed bugs or specs
1451
    # Several other classes need to generate lists of bugs, and
1452
    # one thing they often have to filter for is completeness. We maintain
1453
    # this single canonical query string here so that it does not have to be
1454
    # cargo culted into Product, Distribution, ProductSeries etc
10234.3.5 by Curtis Hovey
Quiet lint.
1455
    completeness_clause = """
3691.436.12 by Mark Shuttleworth
Make sure we don't see completed bugs or specs
1456
        BugTask.bug = Bug.id AND """ + BugTask.completeness_clause
1457
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1458
    def canBeAQuestion(self):
1459
        """See `IBug`."""
1460
        return (self._getQuestionTargetableBugTask() is not None
1461
            and self.getQuestionCreatedFromBug() is None)
1462
1463
    def _getQuestionTargetableBugTask(self):
1464
        """Return the only bugtask that can be a QuestionTarget, or None.
4755.1.16 by Curtis Hovey
Minor revisions made as preparation for the bug-question interface
1465
4755.1.26 by Curtis Hovey
Text revisions. Revisions to getQuestionTargetableBugTask per
1466
        Bugs that are also in external bug trackers cannot be converted
1467
        to questions. This is also true for bugs that are being developed.
1468
        None is returned when either of these conditions are true.
1469
4755.1.19 by Curtis Hovey
Added a interface test to verify that all bugtarget types are handled
1470
        The bugtask is selected by these rules:
4755.1.26 by Curtis Hovey
Text revisions. Revisions to getQuestionTargetableBugTask per
1471
        1. It's status is not Invalid.
1472
        2. It is not a conjoined slave.
1473
        Only one bugtask must meet both conditions to be return. When
1474
        zero or many bugtasks match, None is returned.
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1475
        """
4755.1.43 by Curtis Hovey
Revisions pre review.
1476
        # We may want to removed the bugtask.conjoined_master check
1477
        # below. It is used to simplify the task of converting
1478
        # conjoined bugtasks to question--since slaves cannot be
1479
        # directly updated anyway.
1480
        non_invalid_bugtasks = [
1481
            bugtask for bugtask in self.bugtasks
1482
            if (bugtask.status != BugTaskStatus.INVALID
1483
                and bugtask.conjoined_master is None)]
1484
        if len(non_invalid_bugtasks) != 1:
1485
            return None
1486
        [valid_bugtask] = non_invalid_bugtasks
14062.2.2 by Curtis Hovey
Do not permit bugs to be converted to question when the pillar does not
1487
        pillar = valid_bugtask.pillar
1488
        if (pillar.bug_tracking_usage == ServiceUsage.LAUNCHPAD
1489
            and pillar.answers_usage == ServiceUsage.LAUNCHPAD):
4755.1.43 by Curtis Hovey
Revisions pre review.
1490
            return valid_bugtask
1491
        else:
1492
            return None
1493
1494
    def convertToQuestion(self, person, comment=None):
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1495
        """See `IBug`."""
4755.1.5 by Curtis Hovey
Basic UI behaviour is present. The actual UI (button or link)
1496
        question = self.getQuestionCreatedFromBug()
1497
        assert question is None, (
1498
            'This bug was already converted to question #%s.' % question.id)
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1499
        bugtask = self._getQuestionTargetableBugTask()
1500
        assert bugtask is not None, (
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1501
            'A question cannot be created from this bug without a '
1502
            'valid bugtask.')
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1503
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1504
        bugtask_before_modification = Snapshot(
1505
            bugtask, providing=providedBy(bugtask))
4755.1.15 by Curtis Hovey
Revised the rules for choosing the BugTask/QuestionTarget.
1506
        bugtask.transitionToStatus(BugTaskStatus.INVALID, person)
4755.1.43 by Curtis Hovey
Revisions pre review.
1507
        edited_fields = ['status']
1508
        if comment is not None:
1509
            self.newMessage(
1510
                owner=person, subject=self.followup_subject(),
1511
                content=comment)
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1512
        notify(
7876.3.6 by Francis J. Lacoste
Used ISQLObject from lazr.lifecycle
1513
            ObjectModifiedEvent(
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1514
                object=bugtask,
1515
                object_before_modification=bugtask_before_modification,
4755.1.43 by Curtis Hovey
Revisions pre review.
1516
                edited_fields=edited_fields,
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1517
                user=person))
4755.1.5 by Curtis Hovey
Basic UI behaviour is present. The actual UI (button or link)
1518
4755.1.19 by Curtis Hovey
Added a interface test to verify that all bugtarget types are handled
1519
        question_target = IQuestionTarget(bugtask.target)
1520
        question = question_target.createQuestionFromBug(self)
8053.3.6 by Bjorn Tillenius
Use the new addChange() API when converting a bug to a question.
1521
        self.addChange(BugConvertedToQuestion(UTC_NOW, person, question))
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
1522
        get_property_cache(self)._question_from_bug = question
4755.1.18 by Curtis Hovey
Finally grocked how bug notification works.
1523
        notify(BugBecameQuestionEvent(self, question, person))
4755.1.5 by Curtis Hovey
Basic UI behaviour is present. The actual UI (button or link)
1524
        return question
1525
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1526
    @cachedproperty
1527
    def _question_from_bug(self):
4755.1.2 by Curtis Hovey
Added core functionality to create a questions from a bug. More tests are needed, particularly for IQuestionTarget ftests. The UI work and pagetests are not done; some direction is needed.
1528
        for question in self.questions:
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1529
            if (question.ownerID == self.ownerID
4755.1.43 by Curtis Hovey
Revisions pre review.
1530
                and question.datecreated == self.datecreated):
1531
                return question
4755.1.5 by Curtis Hovey
Basic UI behaviour is present. The actual UI (button or link)
1532
        return None
4755.1.2 by Curtis Hovey
Added core functionality to create a questions from a bug. More tests are needed, particularly for IQuestionTarget ftests. The UI work and pagetests are not done; some direction is needed.
1533
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1534
    def getQuestionCreatedFromBug(self):
1535
        """See `IBug`."""
1536
        return self._question_from_bug
1537
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1538
    def getMessagesForView(self, slice_info):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1539
        """See `IBug`."""
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
1540
        # Note that this function and indexed_messages have significant
1541
        # overlap and could stand to be refactored.
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1542
        slices = []
1543
        if slice_info is not None:
1544
            # NB: This isn't a full implementation of the slice protocol,
1545
            # merely the bits needed by BugTask:+index.
1546
            for slice in slice_info:
1547
                if not slice.start:
1548
                    assert slice.stop > 0, slice.stop
1549
                    slices.append(BugMessage.index < slice.stop)
1550
                elif not slice.stop:
1551
                    if slice.start < 0:
12376.1.6 by Robert Collins
Test (and fix) Bug.getMessagesForView.
1552
                        # If the high index is N, a slice of -1: should
1553
                        # return index N - so we need to add one to the
1554
                        # range.
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1555
                        slices.append(BugMessage.index >= SQL(
1556
                            "(select max(index) from "
12376.1.6 by Robert Collins
Test (and fix) Bug.getMessagesForView.
1557
                            "bugmessage where bug=%s) + 1 - %s" % (
1558
                            sqlvalues(self.id, -slice.start))))
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1559
                    else:
1560
                        slices.append(BugMessage.index >= slice.start)
1561
                else:
12376.1.6 by Robert Collins
Test (and fix) Bug.getMessagesForView.
1562
                    slices.append(And(BugMessage.index >= slice.start,
1563
                        BugMessage.index < slice.stop))
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1564
        if slices:
1565
            ranges = [Or(*slices)]
1566
        else:
1567
            ranges = []
1568
        # We expect:
1569
        # 1 bugmessage -> 1 message -> small N chunks. For now, using a wide
1570
        # query seems fine as we have to join out from bugmessage anyway.
1571
        result = Store.of(self).find((BugMessage, Message, MessageChunk),
13163.1.2 by Brad Crittenden
Fixed lint
1572
            Message.id == MessageChunk.messageID,
1573
            BugMessage.messageID == Message.id,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1574
            BugMessage.bug == self.id, *ranges)
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1575
        result.order_by(BugMessage.index, MessageChunk.sequence)
7675.1054.4 by Danilo Segan
Merge Gary's branch from stable.
1576
12376.1.2 by Robert Collins
Basic implementation in place, tests not updated.
1577
        def eager_load_owners(rows):
1578
            owners = set()
1579
            for row in rows:
1580
                owners.add(row[1].ownerID)
1581
            owners.discard(None)
1582
            if not owners:
1583
                return
12443.1.1 by Robert Collins
Actually eager load message owners in Bug._indexed_messages.
1584
            list(PersonSet().getPrecachedPersonsFromIDs(owners,
1585
                need_validity=True))
12376.1.6 by Robert Collins
Test (and fix) Bug.getMessagesForView.
1586
        return DecoratedResultSet(result, pre_iter_hook=eager_load_owners)
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
1587
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1588
    def addNomination(self, owner, target):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1589
        """See `IBug`."""
9206.3.13 by William Grant
Refuse to nominate if canBeNominatedFor fails, and make that return false if a non-series is given.
1590
        if not self.canBeNominatedFor(target):
1591
            raise NominationError(
1592
                "This bug cannot be nominated for %s." %
1593
                    target.bugtargetdisplayname)
11587.5.1 by Brian Murray
restrict adding nominations to the bug supervisor
1594
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1595
        distroseries = None
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1596
        productseries = None
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1597
        if IDistroSeries.providedBy(target):
1598
            distroseries = target
10054.26.1 by Adi Roiban
Refactor DistroSeriesStatus to SeriesStatus; Don't prompt for setting up translations for obsolete product series.
1599
            if target.status == SeriesStatus.OBSOLETE:
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1600
                raise NominationSeriesObsoleteError(
9206.3.13 by William Grant
Refuse to nominate if canBeNominatedFor fails, and make that return false if a non-series is given.
1601
                    "%s is an obsolete series." % target.bugtargetdisplayname)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1602
        else:
1603
            assert IProductSeries.providedBy(target)
1604
            productseries = target
1605
11587.5.7 by Brian Murray
make it so only bug supervisors or owners or drivers can nominate a bug for a series
1606
        if not (check_permission("launchpad.BugSupervisor", target) or
1607
                check_permission("launchpad.Driver", target)):
1608
            raise NominationError(
1609
                "Only bug supervisors or owners can nominate bugs.")
11587.5.2 by Brian Murray
move nomination permission checking to addNomination of bug
1610
14540.3.1 by Ian Booth
Allow a declined bug nomination to be re-nominated
1611
        # There may be an existing DECLINED nomination. If so, we set the
1612
        # status back to PROPOSED. We do not alter the original date_created.
1613
        nomination = None
1614
        try:
1615
            nomination = self.getNominationFor(target)
1616
        except NotFoundError:
1617
            pass
1618
        if nomination:
1619
            nomination.status = BugNominationStatus.PROPOSED
1620
            nomination.decider = None
1621
            nomination.date_decided = None
1622
        else:
1623
            nomination = BugNomination(
1624
                owner=owner, bug=self, distroseries=distroseries,
1625
                productseries=productseries)
9206.3.10 by William Grant
Drop auto-approval from IBug.addNomination. The view does it explicitly itself.
1626
        self.addChange(SeriesNominated(UTC_NOW, owner, target))
3691.434.4 by Bjorn Tillenius
move some logic from the bug nomination view code to database code.
1627
        return nomination
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1628
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1629
    def canBeNominatedFor(self, target):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1630
        """See `IBug`."""
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1631
        try:
14540.3.1 by Ian Booth
Allow a declined bug nomination to be re-nominated
1632
            nomination = self.getNominationFor(target)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1633
        except NotFoundError:
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1634
            # No nomination exists. Let's see if the bug is already
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1635
            # directly targeted to this nomination target.
1636
            if IDistroSeries.providedBy(target):
9206.3.14 by William Grant
Forbid nomination for a series if the series' pillar has no task.
1637
                series_getter = operator.attrgetter("distroseries")
1638
                pillar_getter = operator.attrgetter("distribution")
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1639
            elif IProductSeries.providedBy(target):
9206.3.14 by William Grant
Forbid nomination for a series if the series' pillar has no task.
1640
                series_getter = operator.attrgetter("productseries")
1641
                pillar_getter = operator.attrgetter("product")
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1642
            else:
9206.3.13 by William Grant
Refuse to nominate if canBeNominatedFor fails, and make that return false if a non-series is given.
1643
                return False
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1644
1645
            for task in self.bugtasks:
9206.3.14 by William Grant
Forbid nomination for a series if the series' pillar has no task.
1646
                if series_getter(task) == target:
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1647
                    # The bug is already targeted at this
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1648
                    # nomination target.
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1649
                    return False
1650
1651
            # No nomination or tasks are targeted at this
9206.3.14 by William Grant
Forbid nomination for a series if the series' pillar has no task.
1652
            # nomination target. But we also don't want to nominate for a
1653
            # series of a product or distro for which we don't have a
1654
            # plain pillar task.
1655
            for task in self.bugtasks:
1656
                if pillar_getter(task) == pillar_getter(target):
1657
                    return True
1658
1659
            # No tasks match the candidate's pillar. We must refuse.
1660
            return False
3614.1.71 by Brad Bollenbach
convert BugNominationView to a LaunchpadFormView. add and test duplicate nomination error handling for the web UI.
1661
        else:
14540.3.4 by Ian Booth
Fix tests
1662
            # The bug may be already nominated for this nomination target.
14540.3.1 by Ian Booth
Allow a declined bug nomination to be re-nominated
1663
            # If the status is declined, the bug can be renominated, else
1664
            # return False
14540.3.4 by Ian Booth
Fix tests
1665
            if nomination:
1666
                return nomination.status == BugNominationStatus.DECLINED
1667
            return False
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1668
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1669
    def getNominationFor(self, target):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1670
        """See `IBug`."""
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1671
        if IDistroSeries.providedBy(target):
1672
            filter_args = dict(distroseriesID=target.id)
9206.3.16 by William Grant
Don't crash if a nomination for a non-series is requested.
1673
        elif IProductSeries.providedBy(target):
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1674
            filter_args = dict(productseriesID=target.id)
15968.3.1 by j.c.sackett
Simple fix in place.
1675
        elif ISourcePackage.providedBy(target):
1676
            filter_args = dict(distroseriesID=target.series.id)
9206.3.16 by William Grant
Don't crash if a nomination for a non-series is requested.
1677
        else:
1678
            return None
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1679
1680
        nomination = BugNomination.selectOneBy(bugID=self.id, **filter_args)
1681
1682
        if nomination is None:
1683
            raise NotFoundError(
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1684
                "Bug #%d is not nominated for %s." % (
9206.3.12 by William Grant
Export the rest of the IBug nomination methods, and add some more tests.
1685
                self.id, target.displayname))
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1686
1687
        return nomination
1688
6291.1.2 by Bjorn Tillenius
get rid of all the repeated BugNomination queries.
1689
    def getNominations(self, target=None, nominations=None):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1690
        """See `IBug`."""
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1691
        # Define the function used as a sort key.
4002.7.32 by Matthew Paul Thomas
Renames bugtargetname to bugtargetdisplayname.
1692
        def by_bugtargetdisplayname(nomination):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1693
            """Return the friendly sort key verson of displayname."""
4002.7.32 by Matthew Paul Thomas
Renames bugtargetname to bugtargetdisplayname.
1694
            return nomination.target.bugtargetdisplayname.lower()
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1695
6291.1.2 by Bjorn Tillenius
get rid of all the repeated BugNomination queries.
1696
        if nominations is None:
1697
            nominations = BugNomination.selectBy(bugID=self.id)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1698
        if IProduct.providedBy(target):
1699
            filtered_nominations = []
1700
            for nomination in shortlist(nominations):
1701
                if (nomination.productseries and
1702
                    nomination.productseries.product == target):
1703
                    filtered_nominations.append(nomination)
1704
            nominations = filtered_nominations
1705
        elif IDistribution.providedBy(target):
1706
            filtered_nominations = []
1707
            for nomination in shortlist(nominations):
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1708
                if (nomination.distroseries and
1709
                    nomination.distroseries.distribution == target):
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1710
                    filtered_nominations.append(nomination)
1711
            nominations = filtered_nominations
1712
4002.7.32 by Matthew Paul Thomas
Renames bugtargetname to bugtargetdisplayname.
1713
        return sorted(nominations, key=by_bugtargetdisplayname)
3614.1.68 by Brad Bollenbach
reapply MaloneReleaseManagement
1714
3691.209.1 by Bjorn Tillenius
add IBug.getBugWatch
1715
    def getBugWatch(self, bugtracker, remote_bug):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1716
        """See `IBug`."""
5613.1.5 by Graham Binns
Bug.getBugWatch() now always return None for EMAILADDRESS bug trackers.
1717
        # If the bug tracker is of BugTrackerType.EMAILADDRESS we can
1718
        # never tell if a bug is already being watched upstream, since
1719
        # the remotebug field for such bug watches contains either '' or
1720
        # an RFC822 message ID. In these cases, then, we always return
1721
        # None for the sake of sanity.
5613.1.11 by Graham Binns
Fixed a very. very, very stupid bug.
1722
        if bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS:
1723
            return None
5613.1.5 by Graham Binns
Bug.getBugWatch() now always return None for EMAILADDRESS bug trackers.
1724
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1725
        # XXX: BjornT 2006-10-11:
1726
        # This matching is a bit fragile, since bugwatch.remotebug
1727
        # is a user editable text string. We should improve the
1728
        # matching so that for example '#42' matches '42' and so on.
3691.209.6 by Bjorn Tillenius
review comments.
1729
        return BugWatch.selectFirstBy(
5821.2.57 by James Henstridge
more changes to resolve failing tests.
1730
            bug=self, bugtracker=bugtracker, remotebug=str(remote_bug),
3691.209.6 by Bjorn Tillenius
review comments.
1731
            orderBy='id')
3691.209.1 by Bjorn Tillenius
add IBug.getBugWatch
1732
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1733
    def setStatus(self, target, status, user):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1734
        """See `IBug`."""
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1735
        bugtask = self.getBugTask(target)
1736
        if bugtask is None:
1737
            if IProductSeries.providedBy(target):
1738
                bugtask = self.getBugTask(target.product)
1739
            elif ISourcePackage.providedBy(target):
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1740
                current_distro_series = target.distribution.currentseries
1741
                current_package = current_distro_series.getSourcePackage(
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1742
                    target.sourcepackagename.name)
1743
                if self.getBugTask(current_package) is not None:
4285.2.1 by Mark Shuttleworth
Massive renaming of distrorelease to distroseries
1744
                    # The bug is targeted to the current series, don't
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1745
                    # fall back on the general distribution task.
1746
                    return None
1747
                distro_package = target.distribution.getSourcePackage(
1748
                    target.sourcepackagename.name)
1749
                bugtask = self.getBugTask(distro_package)
1750
            else:
1751
                return None
1752
1753
        if bugtask is None:
1754
            return None
1755
1756
        if bugtask.conjoined_master is not None:
1757
            bugtask = bugtask.conjoined_master
1758
5343.1.1 by Bjorn Tillenius
fix Bug.setStatus() not to return the bugtask if it wasn't edited.
1759
        if bugtask.status == status:
1760
            return None
1761
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1762
        bugtask_before_modification = Snapshot(
1763
            bugtask, providing=providedBy(bugtask))
4318.3.12 by Gavin Panella
Changing transitionToStatus to accept user argument, part 3.
1764
        bugtask.transitionToStatus(status, user)
7876.3.6 by Francis J. Lacoste
Used ISQLObject from lazr.lifecycle
1765
        notify(ObjectModifiedEvent(
5343.1.1 by Bjorn Tillenius
fix Bug.setStatus() not to return the bugtask if it wasn't edited.
1766
            bugtask, bugtask_before_modification, ['status'], user=user))
4053.1.5 by Bjorn Tillenius
add IBug.setStatus to make it easier to modify target's bug status.
1767
1768
        return bugtask
1769
14986.1.6 by Steve Kowalik
Fix ident.
1770
    def setPrivate(self, private, who):
13994.2.1 by Ian Booth
Implement new subscription behaviour
1771
        """See `IBug`.
1772
1773
        We also record who made the change and when the change took
1774
        place.
1775
        """
14986.1.4 by Steve Kowalik
Delete IBug.setPrivacyAndSecurityRelated(), replace all uses.
1776
        return self.transitionToInformationType(
1777
            convert_to_information_type(private, self.security_related), who)
13994.2.1 by Ian Booth
Implement new subscription behaviour
1778
1779
    def setSecurityRelated(self, security_related, who):
1780
        """Setter for the `security_related` property."""
14986.1.4 by Steve Kowalik
Delete IBug.setPrivacyAndSecurityRelated(), replace all uses.
1781
        return self.transitionToInformationType(
1782
            convert_to_information_type(self.private, security_related), who)
13994.2.1 by Ian Booth
Implement new subscription behaviour
1783
15693.3.3 by William Grant
Add Bug.getAllowedInformationTypes, like Branch.getAllowedInformationTypes.
1784
    def getAllowedInformationTypes(self, who):
1785
        """See `IBug`."""
1786
        types = set(InformationType.items)
1787
        for pillar in self.affected_pillars:
1788
            types.intersection_update(
1789
                set(pillar.getAllowedBugInformationTypes()))
15970.2.2 by Ian Booth
Introduce forbidden sharing policies and disallow branch creation and bug filing when policy is forbidden
1790
        types.add(self.information_type)
15693.3.3 by William Grant
Add Bug.getAllowedInformationTypes, like Branch.getAllowedInformationTypes.
1791
        return types
1792
15840.1.2 by Curtis Hovey
Allow bugs to be set Proprietary over the API.
1793
    def transitionToInformationType(self, information_type, who):
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
1794
        """See `IBug`."""
14986.1.9 by Steve Kowalik
Destroy use of is comparing against InformationType, expand some SQL commands to use the enum directly, hopefully clear up mail handling, and make use of
1795
        if self.information_type == information_type:
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
1796
            return False
15866.1.3 by William Grant
Bug.transitionToInformationType now respects bug_sharing_policy.
1797
        if information_type not in self.getAllowedInformationTypes(who):
1798
            raise CannotChangeInformationType("Forbidden by project policy.")
15866.1.5 by William Grant
Extend the single-pillar Proprietary bug checks to Embargoed too.
1799
        if (information_type in PROPRIETARY_INFORMATION_TYPES
1800
            and len(self.affected_pillars) > 1):
15866.1.2 by William Grant
Bug.transitionToInformationType now uses CannotChangeInformationType, rather than the misleading BugCannotBePrivate.
1801
            raise CannotChangeInformationType(
1802
                "Proprietary bugs can only affect one project.")
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
1803
        if information_type in PRIVATE_INFORMATION_TYPES:
1804
            self.who_made_private = who
1805
            self.date_made_private = UTC_NOW
1806
        else:
1807
            self.who_made_private = None
1808
            self.date_made_private = None
1809
        # XXX: This should be a bulk update. RBC 20100827
1810
        # bug=https://bugs.launchpad.net/storm/+bug/625071
1811
        for attachment in self.attachments_unpopulated:
1812
            attachment.libraryfile.restricted = (
1813
                information_type in PRIVATE_INFORMATION_TYPES)
15124.2.1 by j.c.sackett
Unrevert the previous branch.
1814
15721.3.2 by j.c.sackett
Moved the actual transition.
1815
        self.information_type = information_type
15721.3.5 by j.c.sackett
Reordered operations in transition.
1816
        self._reconcileAccess()
15721.3.3 by j.c.sackett
Some cleanup.
1817
16994.2.1 by William Grant
Stop subscribing the pillar driver, bug supervisor and/or owner (except for an Ubuntu special case, because why not) when a bug transitions to USERDATA. We have +sharing now, so direct subscriptions aren't necessary to retain access.
1818
        # If the new type is private, some people may no longer have
1819
        # access to the bug. We ensure that the bug reporter and the
1820
        # person changing the privacy can see the bug, and then remove
1821
        # subscriptions for anyone who can't see it any more.
15424.1.2 by Ian Booth
New findPeopleWithoutAccess service methods, ensure all subscribers are granted access to a bug when it becomes private
1822
        if information_type in PRIVATE_INFORMATION_TYPES:
16994.2.1 by William Grant
Stop subscribing the pillar driver, bug supervisor and/or owner (except for an Ubuntu special case, because why not) when a bug transitions to USERDATA. We have +sharing now, so direct subscriptions aren't necessary to retain access.
1823
            service = getUtility(IService, 'sharing')
1824
            for person in (who, self.owner):
17332.6.19 by Colin Watson
Add sharing service support for Git repositories.
1825
                bugs, _, _, _ = service.getVisibleArtifacts(
16994.2.1 by William Grant
Stop subscribing the pillar driver, bug supervisor and/or owner (except for an Ubuntu special case, because why not) when a bug transitions to USERDATA. We have +sharing now, so direct subscriptions aren't necessary to retain access.
1826
                    person, bugs=[self], ignore_permissions=True)
1827
                if not bugs:
1828
                    # subscribe() isn't sufficient if a subscription
1829
                    # already exists, as it will do nothing even if
1830
                    # there is no corresponding access grant. We
1831
                    # explicitly ensureAccessGrants() to ensure that the
1832
                    # subscription is retained.
15627.2.1 by Ian Booth
Ensure transitionToInformationType() works for branches without subscribers
1833
                    service.ensureAccessGrants(
16994.2.1 by William Grant
Stop subscribing the pillar driver, bug supervisor and/or owner (except for an Ubuntu special case, because why not) when a bug transitions to USERDATA. We have +sharing now, so direct subscriptions aren't necessary to retain access.
1834
                        [person], who, bugs=[self], ignore_permissions=True)
1835
                    self.subscribe(person, who)
15424.1.2 by Ian Booth
New findPeopleWithoutAccess service methods, ensure all subscribers are granted access to a bug when it becomes private
1836
16994.2.1 by William Grant
Stop subscribing the pillar driver, bug supervisor and/or owner (except for an Ubuntu special case, because why not) when a bug transitions to USERDATA. We have +sharing now, so direct subscriptions aren't necessary to retain access.
1837
            # And now fire off a job to remove any subscribers that can
1838
            # no longer see the bug.
1839
            getUtility(IRemoveArtifactSubscriptionsJobSource).create(
1840
                who, [self])
15721.3.10 by j.c.sackett
Shut up, lint.
1841
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1842
        update_bug_heat([self.id])
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
1843
        return True
1844
4053.1.1 by Bjorn Tillenius
add tests, and move getBugTask to IBug.
1845
    def getBugTask(self, target):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
1846
        """See `IBug`."""
4053.1.1 by Bjorn Tillenius
add tests, and move getBugTask to IBug.
1847
        for bugtask in self.bugtasks:
1848
            if bugtask.target == target:
1849
                return bugtask
1850
1851
        return None
1852
3691.40.1 by Bjorn Tillenius
add a tags attribute to IBug.
1853
    def _getTags(self):
3691.40.17 by Bjorn Tillenius
apply review comments.
1854
        """Get the tags as a sorted list of strings."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
1855
        return self._cached_tags
1856
1857
    @cachedproperty
1858
    def _cached_tags(self):
1859
        return list(Store.of(self).find(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1860
            BugTag.tag, BugTag.bugID == self.id).order_by(BugTag.tag))
3691.40.1 by Bjorn Tillenius
add a tags attribute to IBug.
1861
1862
    def _setTags(self, tags):
1863
        """Set the tags from a list of strings."""
14130.1.1 by Benji York
change bug status queries to fully utilize the new flavors of incomplete status
1864
        # Sets provide an easy way to get the difference between the old and
1865
        # new tags.
3691.40.17 by Bjorn Tillenius
apply review comments.
1866
        new_tags = set([tag.lower() for tag in tags])
1867
        old_tags = set(self.tags)
14130.1.4 by Benji York
move the tag cache clearing to a point at which the cache won't be repopulated incorrectly
1868
        # The cache will be stale after we add/remove tags, clear it.
1869
        del get_property_cache(self)._cached_tags
14130.1.1 by Benji York
change bug status queries to fully utilize the new flavors of incomplete status
1870
        # Find the set of tags that are to be removed and remove them.
3691.40.17 by Bjorn Tillenius
apply review comments.
1871
        removed_tags = old_tags.difference(new_tags)
1872
        for removed_tag in removed_tags:
3691.62.21 by kiko
Clean up the use of ID/.id in select*By and constructors
1873
            tag = BugTag.selectOneBy(bug=self, tag=removed_tag)
3691.40.1 by Bjorn Tillenius
add a tags attribute to IBug.
1874
            tag.destroySelf()
14130.1.1 by Benji York
change bug status queries to fully utilize the new flavors of incomplete status
1875
        # Find the set of tags that are to be added and add them.
1876
        added_tags = new_tags.difference(old_tags)
3691.40.17 by Bjorn Tillenius
apply review comments.
1877
        for added_tag in added_tags:
1878
            BugTag(bug=self, tag=added_tag)
14130.1.1 by Benji York
change bug status queries to fully utilize the new flavors of incomplete status
1879
        # Write all pending changes to the DB, including any pending non-tag
1880
        # changes.
5821.2.49 by James Henstridge
Add a manual flush in _setTags() so that the tags change is visible in
1881
        Store.of(self).flush()
3691.40.1 by Bjorn Tillenius
add a tags attribute to IBug.
1882
1883
    tags = property(_getTags, _setTags)
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
1884
6856.2.16 by Gavin Panella
Move getBugTasksByPackageName from BugTask to Bug. Discussed with BjornT.
1885
    @staticmethod
1886
    def getBugTasksByPackageName(bugtasks):
1887
        """See IBugTask."""
1888
        bugtasks_by_package = {}
1889
        for bugtask in bugtasks:
1890
            bugtasks_by_package.setdefault(bugtask.sourcepackagename, [])
1891
            bugtasks_by_package[bugtask.sourcepackagename].append(bugtask)
1892
        return bugtasks_by_package
1893
7030.5.3 by Tom Berger
changes following a review by sinzui
1894
    def _getAffectedUser(self, user):
6995.1.23 by Bjorn Tillenius
fix lint warnings.
1895
        """Return the `IBugAffectsPerson` for a user, or None
7030.5.3 by Tom Berger
changes following a review by sinzui
1896
6995.1.23 by Bjorn Tillenius
fix lint warnings.
1897
        :param user: An `IPerson` that may be affected by the bug.
1898
        :return: An `IBugAffectsPerson` or None.
1899
        """
10015.2.3 by Gavin Panella
Bug.isUserAffected(None) should return None.
1900
        if user is None:
1901
            return None
1902
        else:
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1903
            return Store.of(self).get(BugAffectsPerson, (self.id, user.id))
7030.5.3 by Tom Berger
changes following a review by sinzui
1904
7030.5.1 by Tom Berger
model for bug affects user
1905
    def isUserAffected(self, user):
1906
        """See `IBug`."""
7106.1.1 by Tom Berger
record both affected an unaffected users
1907
        bap = self._getAffectedUser(user)
1908
        if bap is not None:
1909
            return bap.affected
1910
        else:
1911
            return None
7030.5.1 by Tom Berger
model for bug affects user
1912
7030.5.7 by Tom Berger
extract common code to helper method
1913
    def _flushAndInvalidate(self):
1914
        """Flush all changes to the store and re-read `self` from the DB."""
1915
        store = Store.of(self)
1916
        store.flush()
1917
        store.invalidate(self)
1918
13445.1.4 by Gary Poster
add bugtask.transitionToTarget auto-confirm behavior.
1919
    def shouldConfirmBugtasks(self):
14918.2.3 by Steve Kowalik
Fix IHasLinkedBranches fun, drop IBugAdmin, and sort out interestingness between the model and interfaces.
1920
        """See `IBug`."""
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
1921
        # == 2 would probably be sufficient once we have all legacy bug tasks
1922
        # confirmed.  For now, this is a compromise: we don't need a migration
1923
        # step, but we will make some unnecessary comparisons.
1924
        return self.users_affected_count_with_dupes > 1
1925
13916.1.2 by Brad Crittenden
Remove unneeded enum
1926
    def maybeConfirmBugtasks(self):
14918.2.1 by Steve Kowalik
Blow up IBug into four classes and massively clean up the ZCML.
1927
        """See `IBug`."""
13445.1.5 by Gary Poster
markUserAffected now auto confirms
1928
        if self.shouldConfirmBugtasks():
1929
            for bugtask in self.bugtasks:
13916.1.2 by Brad Crittenden
Remove unneeded enum
1930
                bugtask.maybeConfirm()
13445.1.5 by Gary Poster
markUserAffected now auto confirms
1931
7106.1.1 by Tom Berger
record both affected an unaffected users
1932
    def markUserAffected(self, user, affected=True):
1933
        """See `IBug`."""
1934
        bap = self._getAffectedUser(user)
1935
        if bap is None:
1936
            BugAffectsPerson(bug=self, person=user, affected=affected)
1937
        else:
1938
            if bap.affected != affected:
1939
                bap.affected = affected
7675.472.37 by Graham Binns
Merged latest db-devel.
1940
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1941
        dupe_bug_ids = [dupe.id for dupe in self.duplicates]
1942
        # Where BugAffectsPerson records already exist for each duplicate,
1943
        # update the affected status.
1944
        if dupe_bug_ids:
16146.2.3 by Ian Booth
Remove new bulk update method and use ResultSet.set() instead
1945
            Store.of(self).find(
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
1946
                BugAffectsPerson, BugAffectsPerson.person == user,
16146.2.3 by Ian Booth
Remove new bulk update method and use ResultSet.set() instead
1947
                BugAffectsPerson.bugID.is_in(dupe_bug_ids),
1948
            ).set(affected=affected)
16155.1.2 by Ian Booth
Do flush in method, not test
1949
            for dupe in self.duplicates:
1950
                dupe._flushAndInvalidate()
16146.2.2 by Ian Booth
Move flush call
1951
        self._flushAndInvalidate()
1952
13445.1.7 by Gary Poster
some small cleanups
1953
        if affected:
13916.1.2 by Brad Crittenden
Remove unneeded enum
1954
            self.maybeConfirmBugtasks()
13445.1.1 by Gary Poster
bug._shouldConfirmBugtasks added
1955
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1956
        update_bug_heat(dupe_bug_ids + [self.id])
7030.5.1 by Tom Berger
model for bug affects user
1957
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1958
    def _markAsDuplicate(self, duplicate_of, affected_bug_ids):
12938.1.1 by Abel Deuring
avoid a pointless repetition of calls of BugTarget.recalculateBugHeatCache() in Bug.MarkAsDuplicate()
1959
        """Mark this bug as a duplicate of another.
1960
14753.2.2 by William Grant
Drop affected_targets leftovers.
1961
        Marking a bug as a duplicate requires a recalculation of the
1962
        heat of this bug and of the master bug. None of this is done
1963
        here in order to avoid unnecessary repetitions in recursive
1964
        calls for duplicates of this bug, which also become duplicates
12938.1.1 by Abel Deuring
avoid a pointless repetition of calls of BugTarget.recalculateBugHeatCache() in Bug.MarkAsDuplicate()
1965
        of the new master bug.
1966
        """
8040.2.1 by Tom Berger
enforce validation of IBug.duplicateof when accessed via the API by using a mutator
1967
        field = DuplicateBug()
1968
        field.context = self
7675.706.15 by Graham Binns
Hurrah, bug-heat.txt passes
1969
        current_duplicateof = self.duplicateof
8040.2.1 by Tom Berger
enforce validation of IBug.duplicateof when accessed via the API by using a mutator
1970
        try:
1971
            if duplicate_of is not None:
1972
                field._validate(duplicate_of)
11272.1.1 by Deryck Hodge
First pass at getting my dupe finder work that was
1973
            if self.duplicates:
13627.2.5 by Brad Crittenden
Ripped out unnecessary code
1974
                user = getUtility(ILaunchBag).user
11272.1.1 by Deryck Hodge
First pass at getting my dupe finder work that was
1975
                for duplicate in self.duplicates:
13627.2.4 by Brad Crittenden
Do not use object modification event model for duplicate changes which are deferred.
1976
                    old_value = duplicate.duplicateof
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1977
                    duplicate._markAsDuplicate(duplicate_of, affected_bug_ids)
13627.2.5 by Brad Crittenden
Ripped out unnecessary code
1978
                    # Put an entry into the BugNotification table for
13627.2.4 by Brad Crittenden
Do not use object modification event model for duplicate changes which are deferred.
1979
                    # later processing.
13627.2.14 by Brad Crittenden
Removed unneeded DeferredBugDuplicateChange class
1980
                    change = BugDuplicateChange(
13627.2.4 by Brad Crittenden
Do not use object modification event model for duplicate changes which are deferred.
1981
                        when=None, person=user,
1982
                        what_changed='duplicateof',
1983
                        old_value=old_value,
13627.2.5 by Brad Crittenden
Ripped out unnecessary code
1984
                        new_value=duplicate_of)
1985
                    empty_recipients = BugNotificationRecipients()
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
1986
                    duplicate.addChange(
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1987
                        change, empty_recipients, deferred=True,
1988
                        update_heat=False)
1989
                    affected_bug_ids.add(duplicate.id)
13627.2.4 by Brad Crittenden
Do not use object modification event model for duplicate changes which are deferred.
1990
8040.2.1 by Tom Berger
enforce validation of IBug.duplicateof when accessed via the API by using a mutator
1991
            self.duplicateof = duplicate_of
15523.4.1 by Colin Watson
Use new-style "except Exception as e" syntax rather than "except Exception, e".
1992
        except LaunchpadValidationError as validation_error:
16339.1.15 by j.c.sackett
Merged in devel, because this branch will never die.
1993
            raise InvalidDuplicateValue(validation_error, already_escaped=True)
7675.472.33 by Graham Binns
Added tests for marking and unmarking dupes.
1994
        if duplicate_of is not None:
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
1995
            affected_bug_ids.add(duplicate_of.id)
13916.1.3 by Brad Crittenden
Remove errant comments
1996
            # Maybe confirm bug tasks, now that more people might be affected
1997
            # by this bug from the duplicates.
13916.1.2 by Brad Crittenden
Remove unneeded enum
1998
            duplicate_of.maybeConfirmBugtasks()
14835.1.1 by William Grant
Stop forcing dupe heat to 0. They're hidden from searches by default, so it provides no benefit. This also lets getBugsWithOutdatedHeat use the index properly.
1999
2000
        # Update the former duplicateof's heat, as it will have been
2001
        # reduced by the unduping.
2002
        if current_duplicateof is not None:
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
2003
            affected_bug_ids.add(current_duplicateof.id)
12938.1.1 by Abel Deuring
avoid a pointless repetition of calls of BugTarget.recalculateBugHeatCache() in Bug.MarkAsDuplicate()
2004
2005
    def markAsDuplicate(self, duplicate_of):
2006
        """See `IBug`."""
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
2007
        affected_bug_ids = set()
2008
        self._markAsDuplicate(duplicate_of, affected_bug_ids)
2009
        update_bug_heat(affected_bug_ids)
7675.472.33 by Graham Binns
Added tests for marking and unmarking dupes.
2010
8137.17.24 by Barry Warsaw
thread merge
2011
    def setCommentVisibility(self, user, comment_number, visible):
2012
        """See `IBug`."""
2013
        bug_message_set = getUtility(IBugMessageSet)
2014
        bug_message = bug_message_set.getByBugAndMessage(
2015
            self, self.messages[comment_number])
14302.4.3 by Ian Booth
Add feature flag
2016
15587.5.1 by j.c.sackett
Removed flag code.
2017
        user_owns_comment = (bug_message.owner == user)
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2018
        if (not self.userCanSetCommentVisibility(user)
14302.4.3 by Ian Booth
Add feature flag
2019
            and not user_owns_comment):
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2020
            raise Unauthorized(
2021
                "User %s cannot hide or show bug comments" % user.name)
14302.4.5 by Ian Booth
Fix test failure
2022
        bug_message.message.setVisible(visible)
8137.17.24 by Barry Warsaw
thread merge
2023
11382.6.25 by Gavin Panella
Convert lp.bugs.model.bug to propertycache.
2024
    @cachedproperty
11307.2.24 by Robert Collins
When returning bugs viewable by a user, cache the fact that that user can see them on the bug.
2025
    def _known_viewers(self):
12156.8.20 by Brad Crittenden
Restored pillar owners to userCanView until the proper fix for bug 702429 can be done.
2026
        """A set of known persons able to view this bug.
2027
12415.7.1 by Robert Collins
Move pillar owner access rule for bugs to userCanView, removing 75 queries per bug search page.
2028
        This method must return an empty set or bug searches will trigger late
14902.5.27 by Guilherme Salgado
Move the code that looks up work items from getWorkItemsDueBefore() to a new private method (_getSpecificationWorkItemsDueBefore) and write tests for it
2029
        evaluation. Any 'should be set on load' properties must be done by the
12415.7.1 by Robert Collins
Move pillar owner access rule for bugs to userCanView, removing 75 queries per bug search page.
2030
        bug search.
2031
2032
        If you are tempted to change this method, don't. Instead see
2033
        userCanView which defines the just-in-time policy for bug visibility,
2034
        and BugTask._search which honours visibility rules.
12156.8.20 by Brad Crittenden
Restored pillar owners to userCanView until the proper fix for bug 702429 can be done.
2035
        """
12415.7.1 by Robert Collins
Move pillar owner access rule for bugs to userCanView, removing 75 queries per bug search page.
2036
        return set()
11307.2.24 by Robert Collins
When returning bugs viewable by a user, cache the fact that that user can see them on the bug.
2037
8486.16.7 by Graham Binns
Renamed isVisibleToUser() -> userCanView().
2038
    def userCanView(self, user):
11307.2.24 by Robert Collins
When returning bugs viewable by a user, cache the fact that that user can see them on the bug.
2039
        """See `IBug`.
11403.6.5 by Curtis Hovey
merged devel.
2040
13163.1.1 by Brad Crittenden
Fixed userCanView to handle anonymous users correctly.
2041
        This method is called by security adapters but only in the case for
2042
        authenticated users.  It is also called in other contexts where the
2043
        user may be anonymous.
12156.8.18 by Brad Crittenden
Manage the _known_viewers cached property. Fix tests failing due to an additional db query.
2044
14205.1.1 by William Grant
Bug.userCanView delegates most logic to get_bug_privacy_filter.
2045
        Most logic is delegated to the query provided by
2046
        get_bug_privacy_filter, but some short-circuits and caching are
2047
        reimplemented here.
2048
12156.8.18 by Brad Crittenden
Manage the _known_viewers cached property. Fix tests failing due to an additional db query.
2049
        If bug privacy rights are changed here, corresponding changes need
2050
        to be made to the queries which screen for privacy.  See
15230.2.7 by William Grant
Drop searchAsUser.
2051
        bugtasksearch's get_bug_privacy_filter.
11307.2.24 by Robert Collins
When returning bugs viewable by a user, cache the fact that that user can see them on the bug.
2052
        """
15749.1.2 by William Grant
Fix imports. Lots of imports. Oh god, the imports.
2053
        from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
14758.1.1 by William Grant
Extract most search query builders from BugTaskSet to lp.bugs.model.search.
2054
8486.16.3 by Graham Binns
Added an isVisibleToUser() method to IBug>
2055
        if not self.private:
2056
            # This is a public bug.
2057
            return True
13163.1.1 by Brad Crittenden
Fixed userCanView to handle anonymous users correctly.
2058
        # This method may be called for anonymous users.  For private bugs
2059
        # always return false for anonymous.
2060
        if user is None:
2061
            return False
2062
        if user.id in self._known_viewers:
2063
            return True
2064
15185.3.1 by William Grant
Bug.userCanView is now a bug search.
2065
        params = BugTaskSearchParams(user=user, bug=self)
2066
        if not getUtility(IBugTaskSet).search(params).is_empty():
14205.1.3 by William Grant
Populate _known_viewers again.
2067
            self._known_viewers.add(user.id)
8486.16.3 by Graham Binns
Added an isVisibleToUser() method to IBug>
2068
            return True
14205.1.3 by William Grant
Populate _known_viewers again.
2069
        return False
8486.16.3 by Graham Binns
Added an isVisibleToUser() method to IBug>
2070
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2071
    def userCanSetCommentVisibility(self, user):
14302.4.2 by Ian Booth
Move interface doc
2072
        """See `IBug`"""
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2073
        if user is None:
2074
            return False
15608.1.2 by j.c.sackett
Comment visibility update.
2075
        # Admins and registry experts always have permission.
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2076
        roles = IPersonRoles(user)
2077
        if roles.in_admin or roles.in_registry_experts:
2078
            return True
16115.1.2 by Ian Booth
Provide bulk checkPillarAccess interface to reduce query count on bugtask page
2079
        return getUtility(IService, 'sharing').checkPillarAccess(
2080
            self.affected_pillars, InformationType.USERDATA, user)
14302.4.1 by Ian Booth
Allow users in project roles and comment owners to hide comments
2081
8486.18.1 by Abel Deuring
Methods added to class Bug to add and remove links between a bug and a HWDB submission.
2082
    def linkHWSubmission(self, submission):
2083
        """See `IBug`."""
2084
        getUtility(IHWSubmissionBugSet).create(submission, self)
2085
2086
    def unlinkHWSubmission(self, submission):
2087
        """See `IBug`."""
2088
        getUtility(IHWSubmissionBugSet).remove(submission, self)
2089
2090
    def getHWSubmissions(self, user=None):
2091
        """See `IBug`."""
2092
        return getUtility(IHWSubmissionBugSet).submissionsForBug(self, user)
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2093
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2094
    def personIsDirectSubscriber(self, person):
2095
        """See `IBug`."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
2096
        if person in self._subscriber_cache:
2097
            return True
2098
        if person in self._unsubscribed_cache:
2099
            return False
2100
        if person is None:
2101
            return False
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2102
        store = Store.of(self)
2103
        subscriptions = store.find(
2104
            BugSubscription,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
2105
            BugSubscription.bug == self, BugSubscription.person == person)
9939.1.6 by Graham Binns
Review changes for Gavin.
2106
        return not subscriptions.is_empty()
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2107
9939.1.2 by Graham Binns
Added implementation and btests for personIsAlsoNotifiedSubscriber().
2108
    def personIsAlsoNotifiedSubscriber(self, person):
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2109
        """See `IBug`."""
9939.1.2 by Graham Binns
Added implementation and btests for personIsAlsoNotifiedSubscriber().
2110
        # We have to use getAlsoNotifiedSubscribers() here and iterate
2111
        # over what it returns because "also notified subscribers" is
15920.1.3 by Curtis Hovey
Do not use "bug contact" when "bug supervisor" is meant.
2112
        # actually a composite of bug structural subscribers and assignees.
2113
        # As such, it's not possible to get them all with one query.
9939.1.2 by Graham Binns
Added implementation and btests for personIsAlsoNotifiedSubscriber().
2114
        also_notified_subscribers = self.getAlsoNotifiedSubscribers()
12915.3.1 by Brad Crittenden
Fix personIsAlsoNotifiedSubscriber to return True if one of the user's teams has a structural subscription.
2115
        if person in also_notified_subscribers:
2116
            return True
2117
        # Otherwise check to see if the person is a member of any of the
2118
        # subscribed teams.
13167.1.1 by William Grant
Rollback r13154 for the third time. It breaks bugs with duplicate team subscriptions. Or something.
2119
        for subscriber in also_notified_subscribers:
2120
            if subscriber.is_team and person.inTeam(subscriber):
2121
                return True
12915.3.1 by Brad Crittenden
Fix personIsAlsoNotifiedSubscriber to return True if one of the user's teams has a structural subscription.
2122
        return False
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2123
2124
    def personIsSubscribedToDuplicate(self, person):
2125
        """See `IBug`."""
11582.2.2 by Robert Collins
Probably broken, but takes 46 queries off of a baseline BugTask:+index.
2126
        if person in self._subscriber_dups_cache:
2127
            return True
2128
        if person in self._unsubscribed_cache:
2129
            return False
2130
        if person is None:
2131
            return False
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
2132
        return not Store.of(self).find(
2133
            BugSubscription, Bug.duplicateof == self,
11536.1.1 by Gavin Panella
Convert BugSubscription to Storm.
2134
            BugSubscription.bug_id == Bug.id,
16234.1.1 by Steve Kowalik
Also check if the comment and the extra_description will be over 50001 chars.
2135
            BugSubscription.person == person).is_empty()
9939.1.1 by Graham Binns
Added IBug.personIsDirectSubscriber() and personIsSubscribedToDuplicate() methods.
2136
15354.3.13 by William Grant
reconcileAccess -> _reconcileAccess; it's private
2137
    def _reconcileAccess(self):
15354.3.15 by William Grant
Bug._reconcileAccess now only calculates the pillars if the bug is private. A minor optimisation.
2138
        # reconcile_access_for_artifact will only use the pillar list if
2139
        # the information type is private. But affected_pillars iterates
2140
        # over the tasks immediately, which is needless expense for
2141
        # public bugs.
2142
        if self.information_type in PRIVATE_INFORMATION_TYPES:
2143
            pillars = self.affected_pillars
2144
        else:
2145
            pillars = []
15354.3.1 by William Grant
Extract most of Bug.updateAccessPolicyArtifacts to a generic reconcile_access_for_artifact.
2146
        reconcile_access_for_artifact(
15354.3.15 by William Grant
Bug._reconcileAccess now only calculates the pillars if the bug is private. A minor optimisation.
2147
            self, self.information_type, pillars)
15323.4.9 by William Grant
updateAccessPolicyArtifacts now handles APAs as it says it does.
2148
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2149
    def _attachments_query(self):
2150
        """Helper for the attachments* properties."""
2151
        # bug attachments with no LibraryFileContent have been deleted - the
2152
        # garbo_daily run will remove the LibraryFileAlias asynchronously.
2153
        # See bug 542274 for more details.
2154
        store = Store.of(self)
2155
        return store.find(
12736.8.1 by William Grant
Preload LFCs alongside attachment LFAs.
2156
            (BugAttachment, LibraryFileAlias, LibraryFileContent),
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2157
            BugAttachment.bug == self,
12736.8.1 by William Grant
Preload LFCs alongside attachment LFAs.
2158
            BugAttachment.libraryfileID == LibraryFileAlias.id,
2159
            LibraryFileContent.id == LibraryFileAlias.contentID,
2160
            ).order_by(BugAttachment.id)
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2161
10606.5.3 by Abel Deuring
implemented reviewer's comments
2162
    @property
10606.5.4 by Abel Deuring
implemented more reviewer comments
2163
    def attachments(self):
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2164
        """See `IBug`.
11382.6.42 by Gavin Panella
Move some more code to propertycache.
2165
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
2166
        This property does eager loading of the index_messages so that
2167
        the API which wants the message_link for the attachment can
2168
        answer that without O(N^2) overhead. As such it is moderately
2169
        expensive to call (it currently retrieves all messages before
2170
        any attachments, and does this when attachments is evaluated,
2171
        not when the resultset is processed).
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2172
        """
2173
        message_to_indexed = {}
11544.1.6 by Robert Collins
review feedback.
2174
        for message in self._indexed_messages(include_parents=False):
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2175
            message_to_indexed[message.id] = message
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
2176
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2177
        def set_indexed_message(row):
2178
            attachment = row[0]
2179
            # row[1] - the LibraryFileAlias is now in the storm cache and
2180
            # will be found without a query when dereferenced.
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2181
            indexed_message = message_to_indexed.get(attachment._messageID)
2182
            if indexed_message is not None:
11789.2.4 by Gavin Panella
Change all uses of IPropertyCache outside of propertycache.py to get_property_cache.
2183
                get_property_cache(attachment).message = indexed_message
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2184
            return attachment
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2185
        rawresults = self._attachments_query()
11456.1.5 by Robert Collins
Make the bug/attachments API call stop performing O(N^2) work by permitting BugAttachment.message to be an IIndexedMessage, and then making it so from the property the API uses.
2186
        return DecoratedResultSet(rawresults, set_indexed_message)
10606.5.3 by Abel Deuring
implemented reviewer's comments
2187
11456.1.3 by Robert Collins
Create a dedicated property for API use for bug attachments.
2188
    @property
2189
    def attachments_unpopulated(self):
2190
        """See `IBug`.
11382.6.42 by Gavin Panella
Move some more code to propertycache.
2191
11456.1.9 by Robert Collins
Mention where the explanation for attachments_unpopulated is in the interface, and fix/clarify a couple of typos.
2192
        This version does not pre-lookup messages and LibraryFileAliases.
11382.6.42 by Gavin Panella
Move some more code to propertycache.
2193
11456.1.3 by Robert Collins
Create a dedicated property for API use for bug attachments.
2194
        The regular 'attachments' property does prepopulation because it is
2195
        exposed in the API.
2196
        """
11456.1.9 by Robert Collins
Mention where the explanation for attachments_unpopulated is in the interface, and fix/clarify a couple of typos.
2197
        # Grab the attachment only; the LibraryFileAlias will be eager loaded.
11456.1.6 by Robert Collins
Make the bug attachment API query count be constant.
2198
        return DecoratedResultSet(
2199
            self._attachments_query(),
2200
            operator.itemgetter(0))
11456.1.3 by Robert Collins
Create a dedicated property for API use for bug attachments.
2201
13955.1.1 by Graham Binns
Batched activity now no longer pulls in results that have already been shown.
2202
    def getActivityForDateRange(self, start_date, end_date):
2203
        """See `IBug`."""
2204
        store = Store.of(self)
2205
        activity_in_range = store.find(
2206
            BugActivity,
2207
            BugActivity.bug == self,
2208
            BugActivity.datechanged >= start_date,
2209
            BugActivity.datechanged <= end_date)
2210
        return activity_in_range
8486.16.3 by Graham Binns
Added an isVisibleToUser() method to IBug>
2211
14047.1.2 by Ian Booth
Lint
2212
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2213
@ProxyFactory
2214
def get_also_notified_subscribers(
2215
    bug_or_bugtask, recipients=None, level=None):
2216
    """Return the indirect subscribers for a bug or bug task.
2217
2218
    Return the list of people who should get notifications about changes
2219
    to the bug or task because of having an indirect subscription
2220
    relationship with it (by subscribing to a target, being an assignee
2221
    or owner, etc...)
2222
2223
    If `recipients` is present, add the subscribers to the set of
2224
    bug notification recipients.
2225
    """
2226
    if IBug.providedBy(bug_or_bugtask):
2227
        bug = bug_or_bugtask
2228
        bugtasks = bug.bugtasks
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2229
        info = bug.getSubscriptionInfo(level)
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2230
    elif IBugTask.providedBy(bug_or_bugtask):
2231
        bug = bug_or_bugtask.bug
2232
        bugtasks = [bug_or_bugtask]
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2233
        info = bug.getSubscriptionInfo(level).forTask(bug_or_bugtask)
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2234
    else:
2235
        raise ValueError('First argument must be bug or bugtask')
2236
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2237
    # Subscribers to exclude.
2238
    exclude_subscribers = frozenset().union(
14494.6.46 by Gavin Panella
Change all_direct_subscriptions to direct_subscriptions_at_all_levels and all_direct_subscribers to direct_subscribers_at_all_levels.
2239
        info.direct_subscribers_at_all_levels, info.muted_subscribers)
14494.6.35 by Gavin Panella
Update some comments and docstrings.
2240
    # Get also-notified subscribers at the given level for the given tasks.
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2241
    also_notified_subscribers = info.also_notified_subscribers
2242
2243
    if recipients is not None:
2244
        for bugtask in bugtasks:
2245
            assignee = bugtask.assignee
2246
            if assignee in also_notified_subscribers:
2247
                # We have an assignee that is not a direct subscriber.
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2248
                recipients.addAssignee(bugtask.assignee)
2249
2250
    # This structural subscribers code omits direct subscribers itself.
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2251
    # TODO: Pass the info object into get_structural_subscribers for
2252
    # efficiency... or do the recipients stuff here.
2253
    structural_subscribers = get_structural_subscribers(
2254
        bug_or_bugtask, recipients, level, exclude_subscribers)
2255
    assert also_notified_subscribers.issuperset(structural_subscribers)
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2256
14494.6.17 by Gavin Panella
Part change get_also_notified_subscribers() to use BugSubscriptionInfo.
2257
    return also_notified_subscribers.sorted
12541.2.5 by Gary Poster
refactor getAlsoNotifiedSubscribers to make it reusable, to use the structuralsubscriber function directly, and to better handle direct subscribers; eliminate duplicated code.
2258
2259
11869.17.4 by Gavin Panella
Move load_people() out of BugSubscriptionInfo.
2260
def load_people(*where):
2261
    """Get subscribers from subscriptions.
2262
11869.17.14 by Gavin Panella
Fix load_people to load people and teams without ValidPersonCache records.
2263
    Also preloads `ValidPersonCache` records if they exist.
11869.17.4 by Gavin Panella
Move load_people() out of BugSubscriptionInfo.
2264
2265
    :param people: An iterable sequence of `Person` IDs.
2266
    :return: A `DecoratedResultSet` of `Person` objects. The corresponding
2267
        `ValidPersonCache` records are loaded simultaneously.
2268
    """
11869.17.25 by Gavin Panella
Use PersonSet._getPrecachedPersons() because it's awesome.
2269
    return PersonSet()._getPrecachedPersons(
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2270
        origin=[Person], conditions=where, need_validity=True,
2271
        need_preferred_email=True)
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2272
2273
11869.17.13 by Gavin Panella
Rename SubscriberSet to BugSubscriberSet.
2274
class BugSubscriberSet(frozenset):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2275
    """A set of bug subscribers
2276
2277
    Every member should provide `IPerson`.
2278
    """
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2279
2280
    @cachedproperty
2281
    def sorted(self):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2282
        """A sorted tuple of this set's members.
2283
2284
        Sorted with `person_sort_key`, the default sort key for `Person`.
2285
        """
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2286
        return tuple(sorted(self, key=person_sort_key))
2287
2288
2289
class BugSubscriptionSet(frozenset):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2290
    """A set of bug subscriptions."""
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2291
2292
    @cachedproperty
2293
    def sorted(self):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2294
        """A sorted tuple of this set's members.
2295
2296
        Sorted with `person_sort_key` of the subscription owner.
2297
        """
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2298
        self.subscribers  # Pre-load subscribers.
2299
        sort_key = lambda sub: person_sort_key(sub.person)
2300
        return tuple(sorted(self, key=sort_key))
2301
2302
    @cachedproperty
2303
    def subscribers(self):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2304
        """A `BugSubscriberSet` of the owners of this set's members."""
11869.17.11 by Gavin Panella
Demonstrate query counts for BugSubscriptionInfo properties.
2305
        if len(self) == 0:
11869.17.13 by Gavin Panella
Rename SubscriberSet to BugSubscriberSet.
2306
            return BugSubscriberSet()
11869.17.11 by Gavin Panella
Demonstrate query counts for BugSubscriptionInfo properties.
2307
        else:
2308
            condition = Person.id.is_in(
2309
                removeSecurityProxy(subscription).person_id
2310
                for subscription in self)
11869.17.13 by Gavin Panella
Rename SubscriberSet to BugSubscriberSet.
2311
            return BugSubscriberSet(load_people(condition))
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2312
2313
2314
class StructuralSubscriptionSet(frozenset):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2315
    """A set of structural subscriptions."""
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2316
2317
    @cachedproperty
2318
    def sorted(self):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2319
        """A sorted tuple of this set's members.
2320
2321
        Sorted with `person_sort_key` of the subscription owner.
2322
        """
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2323
        self.subscribers  # Pre-load subscribers.
2324
        sort_key = lambda sub: person_sort_key(sub.subscriber)
2325
        return tuple(sorted(self, key=sort_key))
2326
2327
    @cachedproperty
2328
    def subscribers(self):
11869.17.26 by Gavin Panella
Docstrings for the new *Set classes and their properties.
2329
        """A `BugSubscriberSet` of the owners of this set's members."""
11869.17.11 by Gavin Panella
Demonstrate query counts for BugSubscriptionInfo properties.
2330
        if len(self) == 0:
11869.17.13 by Gavin Panella
Rename SubscriberSet to BugSubscriberSet.
2331
            return BugSubscriberSet()
11869.17.11 by Gavin Panella
Demonstrate query counts for BugSubscriptionInfo properties.
2332
        else:
2333
            condition = Person.id.is_in(
2334
                removeSecurityProxy(subscription).subscriberID
2335
                for subscription in self)
11869.17.13 by Gavin Panella
Rename SubscriberSet to BugSubscriberSet.
2336
            return BugSubscriberSet(load_people(condition))
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2337
2338
11869.17.34 by Gavin Panella
Add a bug number to the XXX about Zope checkers.
2339
# XXX: GavinPanella 2010-12-08 bug=694057: Subclasses of frozenset don't
2340
# appear to be granted those permissions given to frozenset. This would make
2341
# writing ZCML tedious, so I've opted for registering custom checkers (see
2342
# lp_sitecustomize for some other jiggery pokery in the same vein) while I
2343
# seek a better solution.
11869.17.16 by Gavin Panella
Zope security checker hacks to get *Set classes to work.
2344
from zope.security import checker
2345
checker_for_frozen_set = checker.getCheckerForInstancesOf(frozenset)
2346
checker_for_subscriber_set = checker.NamesChecker(["sorted"])
2347
checker_for_subscription_set = checker.NamesChecker(["sorted", "subscribers"])
2348
checker.BasicTypes[BugSubscriberSet] = checker.MultiChecker(
2349
    (checker_for_frozen_set.get_permissions,
2350
     checker_for_subscriber_set.get_permissions))
2351
checker.BasicTypes[BugSubscriptionSet] = checker.MultiChecker(
2352
    (checker_for_frozen_set.get_permissions,
2353
     checker_for_subscription_set.get_permissions))
2354
checker.BasicTypes[StructuralSubscriptionSet] = checker.MultiChecker(
2355
    (checker_for_frozen_set.get_permissions,
2356
     checker_for_subscription_set.get_permissions))
2357
2358
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2359
def freeze(factory):
2360
    """Return a decorator that wraps returned values with `factory`."""
2361
2362
    def decorate(func):
2363
        """Decorator that wraps returned values."""
2364
2365
        @wraps(func)
2366
        def wrapper(*args, **kwargs):
2367
            return factory(func(*args, **kwargs))
2368
        return wrapper
2369
2370
    return decorate
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2371
2372
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
2373
@implementer(IHasBug)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2374
class BugSubscriptionInfo:
11869.18.15 by Gavin Panella
More documentation for BugSubscriptionInfo.
2375
    """Represents bug subscription sets.
2376
2377
    The intention for this class is to encapsulate all calculations of
2378
    subscriptions and subscribers for a bug. Some design considerations:
2379
2380
    * Immutable.
2381
2382
    * Set-based.
2383
2384
    * Sets are cached.
2385
2386
    * Usable with a *snapshot* of a bug. This is interesting for two reasons:
2387
2388
      - Event subscribers commonly deal with snapshots. An instance of this
2389
        class could be added to a custom snapshot so that multiple subscribers
2390
        can share the information it contains.
2391
2392
      - Use outside of the web request. A serialized snapshot could be used to
2393
        calculate subscribers for a particular bug state. This could help us
2394
        to move even more bug mail processing out of the web request.
2395
2396
    """
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2397
2398
    def __init__(self, bug, level):
2399
        self.bug = bug
14494.6.12 by Gavin Panella
New methods forTask and forLevel on BugSubscriptionInfo.
2400
        self.bugtask = None  # Implies all.
11869.18.17 by Gavin Panella
Make test_subscribers_from_dupes_uses_level() fail when it should.
2401
        assert level is not None
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2402
        self.level = level
14494.6.35 by Gavin Panella
Update some comments and docstrings.
2403
        # This cache holds related `BugSubscriptionInfo` instances relating to
2404
        # the same bug but with different levels and/or choice of bugtask.
14494.6.12 by Gavin Panella
New methods forTask and forLevel on BugSubscriptionInfo.
2405
        self.cache = {self.cache_key: self}
14494.6.20 by Gavin Panella
Flush the bug's store when creating a BugSubscriptionInfo.
2406
        # This is often used in event handlers, many of which block implicit
2407
        # flushes. However, the data needs to be in the database for the
2408
        # queries herein to give correct answers.
2409
        Store.of(bug).flush()
14494.6.12 by Gavin Panella
New methods forTask and forLevel on BugSubscriptionInfo.
2410
2411
    @property
2412
    def cache_key(self):
14494.6.35 by Gavin Panella
Update some comments and docstrings.
2413
        """A (bug ID, bugtask ID, level) tuple for use as a hash key.
2414
2415
        This helps `forTask()` and `forLevel()` to be more efficient,
2416
        returning previously populated instances to avoid running the same
2417
        queries against the database again and again.
2418
        """
14494.6.12 by Gavin Panella
New methods forTask and forLevel on BugSubscriptionInfo.
2419
        bugtask_id = None if self.bugtask is None else self.bugtask.id
2420
        return self.bug.id, bugtask_id, self.level
2421
2422
    def forTask(self, bugtask):
2423
        """Create a new `BugSubscriptionInfo` limited to `bugtask`.
2424
2425
        The given task must refer to this object's bug. If `None` is passed a
2426
        new `BugSubscriptionInfo` instance is returned with no limit.
2427
        """
2428
        info = self.__class__(self.bug, self.level)
2429
        info.bugtask, info.cache = bugtask, self.cache
2430
        return self.cache.setdefault(info.cache_key, info)
2431
2432
    def forLevel(self, level):
2433
        """Create a new `BugSubscriptionInfo` limited to `level`."""
2434
        info = self.__class__(self.bug, level)
2435
        info.bugtask, info.cache = self.bugtask, self.cache
2436
        return self.cache.setdefault(info.cache_key, info)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2437
11869.17.8 by Gavin Panella
Cache BugSubscriptionInfo's properties.
2438
    @cachedproperty
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2439
    @freeze(BugSubscriberSet)
2440
    def muted_subscribers(self):
2441
        muted_people = Select(BugMute.person_id, BugMute.bug == self.bug)
2442
        return load_people(Person.id.is_in(muted_people))
2443
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2444
    def visible_recipients_filter(self, column):
2445
        # Circular fail :(
15745.5.4 by William Grant
visible_recipients_filter uses the bulk user filter.
2446
        from lp.bugs.model.bugtasksearch import (
2447
            get_bug_bulk_privacy_filter_terms,
2448
            )
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2449
2450
        if self.bug.private:
15745.5.4 by William Grant
visible_recipients_filter uses the bulk user filter.
2451
            return get_bug_bulk_privacy_filter_terms(column, self.bug.id)
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2452
        else:
2453
            return True
2454
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2455
    @cachedproperty
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
2456
    @freeze(BugSubscriptionSet)
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2457
    def direct_subscriptions(self):
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2458
        """The bug's direct subscriptions.
2459
2460
        Excludes muted subscriptions.
2461
        """
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
2462
        return IStore(BugSubscription).find(
2463
            BugSubscription,
2464
            BugSubscription.bug_notification_level >= self.level,
2465
            BugSubscription.bug == self.bug,
2466
            Not(In(BugSubscription.person_id,
15683.2.2 by Steve Kowalik
Forgive me for my indentation sins.
2467
                   Select(BugMute.person_id, BugMute.bug_id == self.bug.id))))
13627.2.7 by Brad Crittenden
Removed obsolete code, added TestGetDeferredNotifications, mark deferred notifications explicitly with a flag.
2468
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2469
    @property
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2470
    def direct_subscribers(self):
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2471
        """The bug's direct subscriptions.
2472
2473
        Excludes muted subscribers.
2474
        """
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2475
        return self.direct_subscriptions.subscribers
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2476
14494.6.13 by Gavin Panella
New properties for BugSubscriptionInfo: all_direct_subscriptions and all_direct_subscribers.
2477
    @property
14494.6.46 by Gavin Panella
Change all_direct_subscriptions to direct_subscriptions_at_all_levels and all_direct_subscribers to direct_subscribers_at_all_levels.
2478
    def direct_subscriptions_at_all_levels(self):
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2479
        """The bug's direct subscriptions at all levels.
2480
2481
        Excludes muted subscriptions.
2482
        """
14494.6.13 by Gavin Panella
New properties for BugSubscriptionInfo: all_direct_subscriptions and all_direct_subscribers.
2483
        return self.forLevel(
2484
            BugNotificationLevel.LIFECYCLE).direct_subscriptions
2485
2486
    @property
14494.6.46 by Gavin Panella
Change all_direct_subscriptions to direct_subscriptions_at_all_levels and all_direct_subscribers to direct_subscribers_at_all_levels.
2487
    def direct_subscribers_at_all_levels(self):
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2488
        """The bug's direct subscribers at all levels.
2489
2490
        Excludes muted subscribers.
2491
        """
14494.6.46 by Gavin Panella
Change all_direct_subscriptions to direct_subscriptions_at_all_levels and all_direct_subscribers to direct_subscribers_at_all_levels.
2492
        return self.direct_subscriptions_at_all_levels.subscribers
14494.6.13 by Gavin Panella
New properties for BugSubscriptionInfo: all_direct_subscriptions and all_direct_subscribers.
2493
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2494
    @cachedproperty
14494.6.47 by Gavin Panella
Nuke duplicate_subscriptions_and_subscribers because it looks efficient but is actually a potato hole (subscribers will not have anything preloaded).
2495
    @freeze(BugSubscriptionSet)
2496
    def duplicate_subscriptions(self):
2497
        """Subscriptions to duplicates of the bug.
2498
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2499
        Excludes muted subscriptions, and subscribers who can not see the
2500
        master bug.
14494.6.47 by Gavin Panella
Nuke duplicate_subscriptions_and_subscribers because it looks efficient but is actually a potato hole (subscribers will not have anything preloaded).
2501
        """
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2502
        return IStore(BugSubscription).find(
2503
            BugSubscription,
2504
            BugSubscription.bug_notification_level >= self.level,
2505
            BugSubscription.bug_id == Bug.id,
2506
            Bug.duplicateof == self.bug,
2507
            Not(In(
2508
                BugSubscription.person_id,
2509
                Select(
2510
                    BugMute.person_id, BugMute.bug_id == Bug.id,
2511
                    tables=[BugMute]))),
2512
            self.visible_recipients_filter(BugSubscription.person_id))
15683.2.4 by Steve Kowalik
Rename forbidden_recipients_filter() to the actually correct name of
2513
14494.6.47 by Gavin Panella
Nuke duplicate_subscriptions_and_subscribers because it looks efficient but is actually a potato hole (subscribers will not have anything preloaded).
2514
    @property
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2515
    def duplicate_subscribers(self):
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2516
        """Subscribers to duplicates of the bug.
2517
2518
        Excludes muted subscribers.
2519
        """
14494.6.47 by Gavin Panella
Nuke duplicate_subscriptions_and_subscribers because it looks efficient but is actually a potato hole (subscribers will not have anything preloaded).
2520
        return self.duplicate_subscriptions.subscribers
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2521
11869.17.8 by Gavin Panella
Cache BugSubscriptionInfo's properties.
2522
    @cachedproperty
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2523
    @freeze(BugSubscriptionSet)
2524
    def duplicate_only_subscriptions(self):
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2525
        """Subscriptions to duplicates of the bug only.
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2526
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2527
        Excludes muted subscriptions, subscriptions for people who have a
2528
        direct subscription, or who are also notified for another reason.
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2529
        """
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2530
        self.duplicate_subscribers  # Pre-load subscribers.
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2531
        higher_precedence = (
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2532
            self.direct_subscribers.union(
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2533
                self.also_notified_subscribers))
2534
        return (
2535
            subscription for subscription in self.duplicate_subscriptions
2536
            if subscription.person not in higher_precedence)
2537
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2538
    @property
2539
    def duplicate_only_subscribers(self):
2540
        """Subscribers to duplicates of the bug only.
2541
14494.6.15 by Gavin Panella
New muted_subscribers property, and muting-related stuff.
2542
        Excludes muted subscribers, subscribers who have a direct
2543
        subscription, or who are also notified for another reason.
14494.6.4 by Gavin Panella
Repair BugSubscriptionInfo so that it works more like it was originally intended, and get it to pre-load preferred email addresses for subscribers.
2544
        """
2545
        return self.duplicate_only_subscriptions.subscribers
2546
11869.18.6 by Gavin Panella
New property for BugSubscriptionInfo, duplicate_only_subscriptions.
2547
    @cachedproperty
11869.17.6 by Gavin Panella
Add *Set classes for subscriptions and subscribers. Disable query checks for now.
2548
    @freeze(StructuralSubscriptionSet)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2549
    def structural_subscriptions(self):
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2550
        """Structural subscriptions to the bug's targets.
2551
14494.6.23 by Gavin Panella
Update BugSubscriptionInfo.structural_* to use new function get_structural_subscriptions().
2552
        Excludes direct subscriptions.
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2553
        """
14494.6.23 by Gavin Panella
Update BugSubscriptionInfo.structural_* to use new function get_structural_subscriptions().
2554
        subject = self.bug if self.bugtask is None else self.bugtask
2555
        return get_structural_subscriptions(subject, self.level)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2556
14494.6.23 by Gavin Panella
Update BugSubscriptionInfo.structural_* to use new function get_structural_subscriptions().
2557
    @property
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2558
    def structural_subscribers(self):
14494.6.23 by Gavin Panella
Update BugSubscriptionInfo.structural_* to use new function get_structural_subscriptions().
2559
        """Structural subscribers to the bug's targets.
2560
2561
        Excludes direct subscribers.
2562
        """
2563
        return self.structural_subscriptions.subscribers
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2564
2565
    @cachedproperty
2566
    @freeze(BugSubscriberSet)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2567
    def all_assignees(self):
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2568
        """Assignees of the bug's tasks.
2569
2570
        *Does not* exclude muted subscribers.
2571
        """
14494.6.14 by Gavin Panella
Update all_assignees and all_pillar_owners_without_bug_supervisors to take the bugtask into account.
2572
        if self.bugtask is None:
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2573
            assignees = load_people(
2574
                Person.id.is_in(Select(BugTask.assigneeID,
2575
                    BugTask.bug == self.bug)))
2576
        else:
2577
            assignees = load_people(Person.id == self.bugtask.assigneeID)
2578
        if self.bug.private:
2579
            return IStore(Person).find(Person,
2580
                Person.id.is_in([a.id for a in assignees]),
2581
                self.visible_recipients_filter(Person.id))
2582
        else:
2583
            return assignees
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2584
11869.17.8 by Gavin Panella
Cache BugSubscriptionInfo's properties.
2585
    @cachedproperty
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2586
    def also_notified_subscribers(self):
14494.6.36 by Gavin Panella
Fix also_notified_subscribers' docstring.
2587
        """All subscribers except direct, dupe, and muted subscribers."""
15745.5.1 by William Grant
Resurrect private structsubs changes so they can be made performant.
2588
        subscribers = BugSubscriberSet().union(
2589
            self.structural_subscribers, self.all_assignees)
2590
        return subscribers.difference(
2591
            self.direct_subscribers_at_all_levels,
2592
            self.muted_subscribers)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2593
11869.17.8 by Gavin Panella
Cache BugSubscriptionInfo's properties.
2594
    @cachedproperty
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2595
    def indirect_subscribers(self):
14494.6.16 by Gavin Panella
New structural_subscribers property, and more muting-related stuff.
2596
        """All subscribers except direct subscribers.
2597
2598
        Excludes muted subscribers.
2599
        """
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2600
        return self.also_notified_subscribers.union(
13627.2.1 by Brad Crittenden
Defer notification to subscribers of duplicate bugs when one bug is made a duplicate of another.
2601
            self.duplicate_subscribers)
11869.17.2 by Gavin Panella
New class BugSubscriptionInfo.
2602
2603
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
2604
@implementer(IBugSet)
2938.1.1 by Bjorn Tillenius
remove unused and untested methods from IBugSet.
2605
class BugSet:
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2606
    """See BugSet."""
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
2607
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2608
    valid_bug_name_re = re.compile(r'''^[a-z][a-z0-9\\+\\.\\-]+$''')
2609
1309 by Canonical.com Patch Queue Manager
add the rest of the hard bits of implementing bug privacy. grow the
2610
    def get(self, bugid):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2611
        """See `IBugSet`."""
1924 by Canonical.com Patch Queue Manager
make traversing to non-existent bug IDs return a 404 instead of
2612
        try:
2613
            return Bug.get(bugid)
2614
        except SQLObjectNotFound:
2615
            raise NotFoundError(
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2616
                "Unable to locate bug with ID %s." % str(bugid))
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
2617
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2618
    def getByNameOrID(self, bugid):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2619
        """See `IBugSet`."""
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2620
        if self.valid_bug_name_re.match(bugid):
2621
            bug = Bug.selectOneBy(name=bugid)
2622
            if bug is None:
2623
                raise NotFoundError(
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2624
                    "Unable to locate bug with ID %s." % bugid)
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2625
        else:
3111.1.2 by Diogo Matsubara
Fixes https://launchpad.net/products/malone/+bug/31005 (ValueError on bugtask traversal) r=kiko
2626
            try:
2627
                bug = self.get(bugid)
2628
            except ValueError:
2629
                raise NotFoundError(
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2630
                    "Unable to locate bug with nickname %s." % bugid)
1716.3.3 by kiko
Fix for bug 5505: Bug nicknames no longer used. Fixes traversal by implementing an IBugSet.getByNameOrID() method, and using that in places which traverse to bugs
2631
        return bug
2632
13939.3.18 by Curtis Hovey
Reconcile the command changes with the handler using bug-emailinterface.txt
2633
    def createBug(self, bug_params, notify_event=True):
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2634
        """See `IBugSet`."""
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2635
        # Make a copy of the parameter object, because we might modify some
2636
        # of its attribute values below.
8342.5.27 by Gavin Panella
Factor out the snapshot of bug params.
2637
        params = snapshot_bug_params(bug_params)
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2638
15761.2.18 by William Grant
Rip out CreateBugParams/createBug support for exploded targets. setBugTarget(target=...) is all there is.
2639
        if ISeriesBugTarget.providedBy(params.target):
15761.2.12 by William Grant
Make createBug's series error a little more informative.
2640
            raise IllegalTarget(
2641
                "Can't create a bug on a series. Create it with a non-series "
2642
                "task instead, and target it to the series afterwards.")
15761.2.9 by William Grant
Let CreateBugParams take an IBugTarget, rather than a product, distribution and sourcepackagename.
2643
15761.2.8 by William Grant
Refactor createBug to use IBugTarget['pillar'].
2644
        if params.information_type is None:
2645
            params.information_type = (
15761.2.18 by William Grant
Rip out CreateBugParams/createBug support for exploded targets. setBugTarget(target=...) is all there is.
2646
                params.target.pillar.getDefaultBugInformationType())
15761.2.8 by William Grant
Refactor createBug to use IBugTarget['pillar'].
2647
2648
        bug, event = self._makeBug(params)
2649
17810.2.1 by Colin Watson
Set up AccessPolicyArtifacts before subscribing the branch/repository owner, to avoid excess AccessArtifactGrants.
2650
        # Create the initial task on the specified target.  This also
2651
        # reconciles access policies for this bug based on that target.
15761.2.4 by William Grant
Refactor createBug to have a single createTask call.
2652
        getUtility(IBugTaskSet).createTask(
15761.2.18 by William Grant
Rip out CreateBugParams/createBug support for exploded targets. setBugTarget(target=...) is all there is.
2653
            bug, params.owner, params.target, status=params.status)
15424.1.2 by Ian Booth
New findPeopleWithoutAccess service methods, ensure all subscribers are granted access to a bug when it becomes private
2654
15424.1.11 by Ian Booth
Remove old createBugWithoutTarget method, fix tests
2655
        if params.subscribe_owner:
2656
            bug.subscribe(params.owner, params.owner)
2657
        # Subscribe other users.
2658
        for subscriber in params.subscribers:
2659
            bug.subscribe(subscriber, params.owner)
2660
12926.1.6 by Graham Binns
Reverted to previous functionality.
2661
        bug_task = bug.default_bugtask
2662
        if params.assignee:
2663
            bug_task.transitionToAssignee(params.assignee)
2664
        if params.importance:
2665
            bug_task.transitionToImportance(params.importance, params.owner)
2666
        if params.milestone:
2667
            bug_task.transitionToMilestone(params.milestone, params.owner)
12926.1.1 by Graham Binns
Hurrah. You can now Do Things in createBug() that you couldn't before.
2668
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2669
        # Tell everyone.
13939.3.18 by Curtis Hovey
Reconcile the command changes with the handler using bug-emailinterface.txt
2670
        if notify_event:
2671
            notify(event)
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2672
7675.706.9 by Graham Binns
Fixed test failures in bug-heat.txt.
2673
        # Calculate the bug's initial heat.
16146.2.1 by Ian Booth
Introduce bulk update operations for bugs with duplicates affecting users and bug heat updates
2674
        update_bug_heat([bug.id])
7675.706.9 by Graham Binns
Fixed test failures in bug-heat.txt.
2675
13939.3.18 by Curtis Hovey
Reconcile the command changes with the handler using bug-emailinterface.txt
2676
        if not notify_event:
2677
            return bug, event
8342.5.16 by Gavin Panella
New method createBugWithoutTarget() to allow bug creation without, you guessed it, a target.
2678
        return bug
2679
15424.1.11 by Ian Booth
Remove old createBugWithoutTarget method, fix tests
2680
    def _makeBug(self, bug_params):
2681
        """Construct a bew bug object using the specified parameters."""
2682
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2683
        # Make a copy of the parameter object, because we might modify some
2684
        # of its attribute values below.
8342.5.27 by Gavin Panella
Factor out the snapshot of bug params.
2685
        params = snapshot_bug_params(bug_params)
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2686
2687
        if not (params.comment or params.description or params.msg):
2821.2.28 by Brad Bollenbach
add auto-subscribing of pkg bug contacts to public bug reports
2688
            raise AssertionError(
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2689
                'Either comment, msg, or description should be specified.')
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2690
6995.1.19 by Bjorn Tillenius
test the date format as well.
2691
        if not params.datecreated:
2692
            params.datecreated = UTC_NOW
2693
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2694
        # make sure we did not get TOO MUCH information
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2695
        assert params.comment is None or params.msg is None, (
4656.2.1 by Curtis Hovey
Fixed spelling in raised errors, updated docstrings.
2696
            "Expected either a comment or a msg, but got both.")
8342.5.16 by Gavin Panella
New method createBugWithoutTarget() to allow bug creation without, you guessed it, a target.
2697
2821.2.18 by Brad Bollenbach
checkpoint
2698
        # Create the bug comment if one was given.
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2699
        if params.comment:
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2700
            rfc822msgid = make_msgid('malonedeb')
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2701
            params.msg = Message(
8342.5.29 by Gavin Panella
First blast at removing Message.distribution.
2702
                subject=params.title, rfc822msgid=rfc822msgid,
2703
                owner=params.owner, datecreated=params.datecreated)
2489 by Canonical.com Patch Queue Manager
[r=kiko] Fix a bug on the Malone front page where 'latest bugs' wasn't
2704
            MessageChunk(
3691.62.21 by kiko
Clean up the use of ID/.id in select*By and constructors
2705
                message=params.msg, sequence=1, content=params.comment,
2706
                blob=None)
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2707
2821.2.18 by Brad Bollenbach
checkpoint
2708
        # Extract the details needed to create the bug and optional msg.
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2709
        if not params.description:
2710
            params.description = params.msg.text_contents
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2711
4813.12.9 by Gavin Panella
Set privacy audit info on bug creation.
2712
        extra_params = {}
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
2713
        if params.information_type in PRIVATE_INFORMATION_TYPES:
4813.12.9 by Gavin Panella
Set privacy audit info on bug creation.
2714
            # We add some auditing information. After bug creation
2715
            # time these attributes are updated by Bug.setPrivate().
2716
            extra_params.update(
2717
                date_made_private=params.datecreated,
2718
                who_made_private=params.owner)
2719
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2720
        bug = Bug(
3598.1.8 by Brad Bollenbach
refactor IBugSet.createBug to use a parameter object
2721
            title=params.title, description=params.description,
14986.1.1 by Steve Kowalik
Change CreateBugParams to take information_type, and drop IBug._{private,security_related}.
2722
            owner=params.owner, datecreated=params.datecreated,
2723
            information_type=params.information_type,
4813.12.9 by Gavin Panella
Set privacy audit info on bug creation.
2724
            **extra_params)
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2725
3691.415.2 by Bjorn Tillenius
add a Tags: field to the advanced filebug page.
2726
        if params.tags:
2727
            bug.tags = params.tags
3598.1.28 by Brad Bollenbach
merge from rf, resolving conflicts
2728
2821.2.18 by Brad Bollenbach
checkpoint
2729
        # Link the bug to the message.
12346.2.2 by Robert Collins
Change all BugMessage object creation to set the index. This involved
2730
        BugMessage(bug=bug, message=params.msg, index=0)
2407 by Canonical.com Patch Queue Manager
[trivial] remove unused, broken, code dealing with binary packages. remove BugFactory.
2731
8054.7.1 by Tom Berger
mark bug reporters as affected by a bug
2732
        # Mark the bug reporter as affected by that bug.
2733
        bug.markUserAffected(bug.owner)
2734
13939.3.10 by Curtis Hovey
Updated CreateBugParams to support CVEs.
2735
        if params.cve is not None:
2736
            bug.linkCVE(params.cve, params.owner)
2737
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2738
        # Populate the creation event.
8342.5.14 by Gavin Panella
Fold the user parameter to IBugSet.createByg() into CreateBugParams.
2739
        if params.filed_by is None:
8342.5.16 by Gavin Panella
New method createBugWithoutTarget() to allow bug creation without, you guessed it, a target.
2740
            event = ObjectCreatedEvent(bug, user=params.owner)
8342.5.5 by Gavin Panella
Allow the bug filer to be specified in a call to createBug().
2741
        else:
8342.5.16 by Gavin Panella
New method createBugWithoutTarget() to allow bug creation without, you guessed it, a target.
2742
            event = ObjectCreatedEvent(bug, user=params.filed_by)
8342.5.2 by Gavin Panella
Change BugSet.createBug() to send the ObjectCreatedEvent itself, with an update to test_bugchangesto test it.
2743
8342.5.18 by Gavin Panella
Move product and distribution (task) specific stuff to createBug(), from createBugWithoutTarget().
2744
        return (bug, event)
7030.5.1 by Tom Berger
model for bug affects user
2745
8486.16.9 by Graham Binns
Extracted common portion of BugTask.findSimilar() and FilebugShowSimilarBugsView.similar_bugs into IBugSet.getDistinctBugsForBugTasks().
2746
    def getDistinctBugsForBugTasks(self, bug_tasks, user, limit=10):
2747
        """See `IBugSet`."""
2748
        # XXX: Graham Binns 2009-05-28 bug=75764
8486.16.16 by Graham Binns
Fixed a typo.
2749
        #      We slice bug_tasks here to prevent this method from
2750
        #      causing timeouts, since if we try to iterate over it
8486.16.9 by Graham Binns
Extracted common portion of BugTask.findSimilar() and FilebugShowSimilarBugsView.similar_bugs into IBugSet.getDistinctBugsForBugTasks().
2751
        #      Transaction.iterSelect() will try to listify the results.
2752
        #      This can be fixed by selecting from Bugs directly, but
2753
        #      that's non-trivial.
13163.1.1 by Brad Crittenden
Fixed userCanView to handle anonymous users correctly.
2754
        # ---: Robert Collins 2010-08-18: if bug_tasks implements IResultSet
11307.2.30 by Robert Collins
Checkpoint for ec2test.
2755
        #      then it should be very possible to improve on it, though
2756
        #      DecoratedResultSets would need careful handling (e.g. type
2757
        #      driven callbacks on columns)
8486.16.9 by Graham Binns
Extracted common portion of BugTask.findSimilar() and FilebugShowSimilarBugsView.similar_bugs into IBugSet.getDistinctBugsForBugTasks().
2758
        # We select more than :limit: since if a bug affects more than
2759
        # one source package, it will be returned more than one time. 4
2760
        # is an arbitrary number that should be large enough.
2761
        bugs = []
13155.2.15 by Francis J. Lacoste
Lint blows but hoover sucks.
2762
        for bug_task in bug_tasks[:4 * limit]:
8486.16.9 by Graham Binns
Extracted common portion of BugTask.findSimilar() and FilebugShowSimilarBugsView.similar_bugs into IBugSet.getDistinctBugsForBugTasks().
2763
            bug = bug_task.bug
2764
            duplicateof = bug.duplicateof
2765
            if duplicateof is not None:
2766
                bug = duplicateof
2767
8486.16.21 by Graham Binns
BugSet.getDistinctBugsForBugTasks() will no longer return duplicate bugs which aren't visible to the user.
2768
            if not bug.userCanView(user):
2769
                continue
2770
8486.16.9 by Graham Binns
Extracted common portion of BugTask.findSimilar() and FilebugShowSimilarBugsView.similar_bugs into IBugSet.getDistinctBugsForBugTasks().
2771
            if bug not in bugs:
2772
                bugs.append(bug)
2773
                if len(bugs) >= limit:
2774
                    break
2775
2776
        return bugs
2777
9719.5.18 by Muharem Hrnjadovic
Refactored code:
2778
    def getByNumbers(self, bug_numbers):
10124.2.6 by Graham Binns
Added updateBugHeat().
2779
        """See `IBugSet`."""
9719.5.18 by Muharem Hrnjadovic
Refactored code:
2780
        if bug_numbers is None or len(bug_numbers) < 1:
2781
            return EmptyResultSet()
2782
        store = IStore(Bug)
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
2783
        result_set = store.find(Bug, Bug.id.is_in(bug_numbers))
9772.2.1 by Muharem Hrnjadovic
Improvements to BugSet.getByNumbers() and to the related tests.
2784
        return result_set.order_by('id')
9719.5.18 by Muharem Hrnjadovic
Refactored code:
2785
14835.1.2 by William Grant
Move the heat outdated cutoff calculation outside getBugsWithOutdatedHeat.
2786
    def getBugsWithOutdatedHeat(self, cutoff):
7675.582.6 by Graham Binns
Added IBug.getBugsWithOutdatedHeat().
2787
        """See `IBugSet`."""
2788
        store = IStore(Bug)
2789
        last_updated_clause = Or(
14835.1.2 by William Grant
Move the heat outdated cutoff calculation outside getBugsWithOutdatedHeat.
2790
            Bug.heat_last_updated < cutoff,
7675.582.6 by Graham Binns
Added IBug.getBugsWithOutdatedHeat().
2791
            Bug.heat_last_updated == None)
2792
14835.1.1 by William Grant
Stop forcing dupe heat to 0. They're hidden from searches by default, so it provides no benefit. This also lets getBugsWithOutdatedHeat use the index properly.
2793
        return store.find(Bug, last_updated_clause).order_by(
2794
            Bug.heat_last_updated)
7675.582.6 by Graham Binns
Added IBug.getBugsWithOutdatedHeat().
2795
7030.5.1 by Tom Berger
model for bug affects user
2796
2797
class BugAffectsPerson(SQLBase):
7030.5.2 by Tom Berger
missing docstring
2798
    """A bug is marked as affecting a user."""
7030.5.1 by Tom Berger
model for bug affects user
2799
    bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
2800
    person = ForeignKey(dbName='person', foreignKey='Person', notNull=True)
7106.1.1 by Tom Berger
record both affected an unaffected users
2801
    affected = BoolCol(notNull=True, default=True)
10015.2.1 by Gavin Panella
Add __storm_primary__ to BugAffectsPerson so that more queries can hit the cache.
2802
    __storm_primary__ = "bugID", "personID"
7675.547.6 by Graham Binns
FileBugData parsed from a blob by a ProcessApportBlobJob is now used during the filebug process, rather than parsing the blob in the request.
2803
2804
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
2805
@implementer(IFileBugData)
7675.547.6 by Graham Binns
FileBugData parsed from a blob by a ProcessApportBlobJob is now used during the filebug process, rather than parsing the blob in the request.
2806
class FileBugData:
2807
    """Extra data to be added to the bug."""
2808
2809
    def __init__(self, initial_summary=None, initial_tags=None,
2810
                 private=None, subscribers=None, extra_description=None,
2811
                 comments=None, attachments=None,
2812
                 hwdb_submission_keys=None):
2813
        if initial_tags is None:
2814
            initial_tags = []
2815
        if subscribers is None:
2816
            subscribers = []
2817
        if comments is None:
2818
            comments = []
2819
        if attachments is None:
2820
            attachments = []
2821
        if hwdb_submission_keys is None:
2822
            hwdb_submission_keys = []
2823
2824
        self.initial_summary = initial_summary
2825
        self.private = private
2826
        self.extra_description = extra_description
2827
        self.initial_tags = initial_tags
2828
        self.subscribers = subscribers
2829
        self.comments = comments
2830
        self.attachments = attachments
2831
        self.hwdb_submission_keys = hwdb_submission_keys
2832
2833
    def asDict(self):
2834
        """Return the FileBugData instance as a dict."""
2835
        return self.__dict__.copy()
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
2836
2837
17610.1.1 by Colin Watson
Switch zope.interface users from class advice to class decorators.
2838
@implementer(IBugMute)
7675.1138.5 by Danilo Segan
Move basic mute functionality to BugMute table as tested by TestBugSubscriptionMethods.
2839
class BugMute(StormBase):
2840
    """Contains bugs a person has decided to block notifications from."""
2841
2842
    __storm_table__ = "BugMute"
2843
2844
    def __init__(self, person=None, bug=None):
2845
        if person is not None:
2846
            self.person = person
2847
        if bug is not None:
2848
            self.bug_id = bug.id
2849
2850
    person_id = Int("person", allow_none=False, validator=validate_person)
2851
    person = Reference(person_id, "Person.id")
2852
2853
    bug_id = Int("bug", allow_none=False)
2854
    bug = Reference(bug_id, "Bug.id")
2855
2856
    __storm_primary__ = 'person_id', 'bug_id'
2857
2858
    date_created = DateTime(
2859
        "date_created", allow_none=False, default=UTC_NOW,
2860
        tzinfo=pytz.UTC)
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
2861
16122.2.3 by Steve Kowalik
Resurrect preloading witchcraft in browser/bugtask, correct whitespace in generate_subscription_with, and destroy PSI._getTaskPillar.
2862
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
2863
def generate_subscription_with(bug, person):
2864
    return [
2865
        With('all_bugsubscriptions', Select(
2866
            (BugSubscription.id, BugSubscription.person_id),
16122.2.3 by Steve Kowalik
Resurrect preloading witchcraft in browser/bugtask, correct whitespace in generate_subscription_with, and destroy PSI._getTaskPillar.
2867
            tables=[
2868
                BugSubscription, Join(Bug, Bug.id == BugSubscription.bug_id)],
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
2869
            where=Or(Bug.id == bug.id, Bug.duplicateofID == bug.id))),
2870
        With('bugsubscriptions', Select(
2871
            SQL('all_bugsubscriptions.id'),
2872
            tables=[
16122.2.3 by Steve Kowalik
Resurrect preloading witchcraft in browser/bugtask, correct whitespace in generate_subscription_with, and destroy PSI._getTaskPillar.
2873
                SQL('all_bugsubscriptions'),
2874
                Join(TeamParticipation, TeamParticipation.teamID == SQL(
2875
                    'all_bugsubscriptions.person'))],
16122.2.1 by Steve Kowalik
Rewrite the heavy-lifting queries in IBug.getSubscriptionForPerson() and IPersonSubscriptionInfo._getDirectAndDuplicateSubscriptions() to be performant using two CTEs, and bulk load all related people, bugtasks and pillars in the second function.
2876
            where=[TeamParticipation.personID == person.id]))]