~launchpad-pqm/launchpad/devel

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